| … | … | @@ -59,18 +59,72 @@ void main() |
| 59 | 59 | # The engine's curve: log density, colour averaged by density (RendererGL's |
| 60 | 60 | # tonemap). Kept in one place so the still and live paths cannot drift. |
| 61 | 61 | _TONEMAP_FRAG = """ |
| 62 | // Shading modes. A fractal flame is a density histogram, not geometry - there are |
| 63 | // no surfaces and no normals - so anything other than EMISSION has to invent them. |
| 64 | // EMISSION is the physically honest one; the others are stylistic. |
| 65 | #define MODE_EMISSION 0 |
| 66 | #define MODE_FLAT 1 |
| 67 | #define MODE_METAL 2 |
| 68 | |
| 69 | float density_at(ivec2 c) |
| 70 | { |
| 71 | return texelFetch(accum_tex, clamp(c, ivec2(0), out_size - 1), 0).w; |
| 72 | } |
| 73 | |
| 74 | float luminance_at(ivec2 c, float peak) |
| 75 | { |
| 76 | return log(1.0 + brightness * density_at(c) / peak) / log(10.0); |
| 77 | } |
| 78 | |
| 62 | 79 | void main() |
| 63 | 80 | { |
| 64 | 81 | ivec2 c = ivec2(uv * vec2(out_size)); |
| 65 | 82 | vec4 h = texelFetch(accum_tex, c, 0); |
| 66 | 83 | float peak = max(float(texelFetch(max_tex, ivec2(0, 0), 0).x), 1.0); |
| 67 | 84 | float d = max(h.w, 1e-12); |
| 68 | | // ADDITIVE, because a flame is EMISSION. Alpha-over needed an alpha, and the |
| 69 | | // only sensible one - the tonemapped density - is ~0.03 across a nebula's body, |
| 70 | | // which multiplied an already-tonemapped colour into invisibility. Adding light |
| 71 | | // leaves empty space untouched (rgb 0 adds nothing) AND keeps full brightness. |
| 72 | | float s = log(1.0 + brightness * h.w / peak) / log(10.0) / d; |
| 73 | | fragColor = vec4(clamp(h.rgb * s, 0.0, 1.0), 1.0); |
| 85 | float lum = log(1.0 + brightness * h.w / peak) / log(10.0); |
| 86 | |
| 87 | // Density-weighted average palette colour, the engine's own colouring. |
| 88 | vec3 base = clamp(h.rgb * (lum / d), 0.0, 1.0); |
| 89 | |
| 90 | if (shading_mode == MODE_EMISSION) |
| 91 | { |
| 92 | // Added to the scene, so alpha is unused by ADDITIVE_PREMULT. |
| 93 | fragColor = vec4(base, 1.0); |
| 94 | return; |
| 95 | } |
| 96 | |
| 97 | // Alpha-over modes need visible coverage from a value that is ~0.03 across a |
| 98 | // nebula, so it is lifted by a user-controlled gain rather than used raw. |
| 99 | float a = clamp(lum * opacity_gain, 0.0, 1.0); |
| 100 | |
| 101 | if (shading_mode == MODE_FLAT) |
| 102 | { |
| 103 | // Unlit: the palette colour at full strength, so it reads as a diagram. |
| 104 | vec3 flat_rgb = h.w > 0.0 ? clamp(h.rgb / d, 0.0, 1.0) : vec3(0.0); |
| 105 | fragColor = vec4(flat_rgb, a); |
| 106 | return; |
| 107 | } |
| 108 | |
| 109 | // MODE_METAL: treat tonemapped log-density as a heightfield and light it. |
| 110 | // Central differences give a gradient; the z term sets how pronounced the |
| 111 | // relief is. Purely stylistic - there is no real geometry to be normal to. |
| 112 | float hx = luminance_at(c + ivec2(1, 0), peak) - luminance_at(c - ivec2(1, 0), peak); |
| 113 | float hy = luminance_at(c + ivec2(0, 1), peak) - luminance_at(c - ivec2(0, 1), peak); |
| 114 | vec3 n = normalize(vec3(-hx * relief, -hy * relief, 1.0)); |
| 115 | |
| 116 | const vec3 light = normalize(vec3(-0.4, 0.5, 0.75)); |
| 117 | vec3 view = vec3(0.0, 0.0, 1.0); |
| 118 | vec3 half_v = normalize(light + view); |
| 119 | |
| 120 | float diffuse = max(dot(n, light), 0.0); |
| 121 | float spec = pow(max(dot(n, half_v), 0.0), 48.0); |
| 122 | float fresnel = pow(1.0 - max(dot(n, view), 0.0), 3.0); |
| 123 | |
| 124 | // Metal: little diffuse, tinted specular, a rim from the fresnel term. |
| 125 | vec3 tint = h.w > 0.0 ? clamp(h.rgb / d, 0.0, 1.0) : vec3(0.5); |
| 126 | vec3 rgb = tint * (0.12 + 0.25 * diffuse) + tint * spec * 1.6 + tint * fresnel * 0.5; |
| 127 | fragColor = vec4(clamp(rgb, 0.0, 1.0), a); |
| 74 | 128 | } |
| 75 | 129 | """ |
| 76 | 130 | |
| … | … | @@ -98,6 +152,9 @@ class Session: |
| 98 | 152 | |
| 99 | 153 | self.doc = ifs_document.load(document_path, self.library) |
| 100 | 154 | self.brightness = self.doc.brightness * 8.0 |
| 155 | self.shading_mode = 0 |
| 156 | self.opacity_gain = 4.0 |
| 157 | self.relief = 12.0 |
| 101 | 158 | |
| 102 | 159 | # What needs rebuilding before the next dispatch. Property callbacks fire |
| 103 | 160 | # continuously while a slider is dragged, so they only set flags here and |
| … | … | @@ -129,6 +186,8 @@ class Session: |
| 129 | 186 | elif kind != 'tonemap': |
| 130 | 187 | raise ValueError(f"unknown dirty kind {kind!r}") |
| 131 | 188 | |
| 189 | SHADING_MODES = {'EMISSION': 0, 'FLAT': 1, 'METAL': 2} |
| 190 | |
| 132 | 191 | def sync_from_scene(self, settings): |
| 133 | 192 | """ |
| 134 | 193 | Copies the panel's values onto the document, flagging what changed. |
| … | … | @@ -138,6 +197,9 @@ class Session: |
| 138 | 197 | property default of 8 on the first redraw and the fractal renders dim. |
| 139 | 198 | """ |
| 140 | 199 | self.brightness = settings.brightness |
| 200 | self.shading_mode = self.SHADING_MODES.get(settings.shading_mode, 0) |
| 201 | self.opacity_gain = settings.opacity_gain |
| 202 | self.relief = settings.relief |
| 141 | 203 | for item, iterator in zip(settings.iterators, self.doc.iterators): |
| 142 | 204 | iterator.opacity = item.opacity |
| 143 | 205 | iterator.color_index = item.color_index |
| … | … | @@ -191,6 +253,9 @@ class Session: |
| 191 | 253 | info.sampler(1, 'UINT_2D', "max_tex") |
| 192 | 254 | info.push_constant('IVEC2', "out_size") |
| 193 | 255 | info.push_constant('FLOAT', "brightness") |
| 256 | info.push_constant('INT', "shading_mode") |
| 257 | info.push_constant('FLOAT', "opacity_gain") |
| 258 | info.push_constant('FLOAT', "relief") |
| 194 | 259 | info.vertex_source(_TONEMAP_VERT) |
| 195 | 260 | info.fragment_source(_TONEMAP_FRAG) |
| 196 | 261 | return gpu.shader.create_from_info(info) |
| … | … | @@ -331,9 +396,11 @@ class Session: |
| 331 | 396 | self.tonemap.uniform_sampler('max_tex', self.maxbuf) |
| 332 | 397 | self.tonemap.uniform_int('out_size', (self.width, self.height)) |
| 333 | 398 | self.tonemap.uniform_float('brightness', self.brightness) |
| 334 | | # ADDITIVE_PREMULT is dst = src + dst: the fractal adds light to whatever |
| 335 | | # the viewport already drew, so the scene stays visible underneath. |
| 336 | | gpu.state.blend_set('ADDITIVE_PREMULT') |
| 399 | self.tonemap.uniform_int('shading_mode', (self.shading_mode,)) |
| 400 | self.tonemap.uniform_float('opacity_gain', self.opacity_gain) |
| 401 | self.tonemap.uniform_float('relief', self.relief) |
| 402 | # Emission ADDS light; the shaded modes are surfaces and composite over. |
| 403 | gpu.state.blend_set('ADDITIVE_PREMULT' if self.shading_mode == 0 else 'ALPHA') |
| 337 | 404 | self.batch.draw(self.tonemap) |
| 338 | 405 | gpu.state.blend_set('NONE') # leave the viewport's state as we found it |
| 339 | 406 | |