| … | … | @@ -65,12 +65,12 @@ void main() |
| 65 | 65 | vec4 h = texelFetch(accum_tex, c, 0); |
| 66 | 66 | float peak = max(float(texelFetch(max_tex, ivec2(0, 0), 0).x), 1.0); |
| 67 | 67 | float d = max(h.w, 1e-12); |
| 68 | | // Alpha IS the tonemapped density, so empty space is transparent and the |
| 69 | | // fractal COMPOSITES over the viewport - grid, objects and gizmos stay visible. |
| 70 | | // An opaque quad here blanks the whole 3D view. |
| 71 | | float lum = log(1.0 + brightness * h.w / peak) / log(10.0); |
| 72 | | float s = lum / d; |
| 73 | | fragColor = vec4(clamp(h.rgb * s, 0.0, 1.0), clamp(lum, 0.0, 1.0)); |
| 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); |
| 74 | 74 | } |
| 75 | 75 | """ |
| 76 | 76 | |
| … | … | @@ -130,7 +130,13 @@ class Session: |
| 130 | 130 | raise ValueError(f"unknown dirty kind {kind!r}") |
| 131 | 131 | |
| 132 | 132 | def sync_from_scene(self, settings): |
| 133 | | """Copies the panel's values onto the document, flagging what changed.""" |
| 133 | """ |
| 134 | Copies the panel's values onto the document, flagging what changed. |
| 135 | |
| 136 | Brightness included - but the panel must have been seeded from the |
| 137 | document first, or this silently replaces a document's 14.55 with the |
| 138 | property default of 8 on the first redraw and the fractal renders dim. |
| 139 | """ |
| 134 | 140 | self.brightness = settings.brightness |
| 135 | 141 | for item, iterator in zip(settings.iterators, self.doc.iterators): |
| 136 | 142 | iterator.opacity = item.opacity |
| … | … | @@ -251,6 +257,18 @@ class Session: |
| 251 | 257 | (eye.x, eye.y, eye.z), (forward.x, forward.y, forward.z), |
| 252 | 258 | (up.x, up.y, up.z), fov_degrees=fov, engine_fov_quirk=False) |
| 253 | 259 | |
| 260 | # How much the view-projection may drift before the accumulation is thrown |
| 261 | # away. Exact comparison restarts on floating-point noise and on Blender's |
| 262 | # smooth-view easing, so the image resamples every frame and never settles - |
| 263 | # which looks like the fractal refusing to sit still. |
| 264 | VIEW_EPSILON = 1e-5 |
| 265 | |
| 266 | def _view_moved(self, view_proj) -> bool: |
| 267 | if self.last_view is None: |
| 268 | return True |
| 269 | return any(abs(a - b) > self.VIEW_EPSILON |
| 270 | for a, b in zip(view_proj, self.last_view)) |
| 271 | |
| 254 | 272 | # ---------------------------------------------------------------------- draw |
| 255 | 273 | |
| 256 | 274 | def draw(self, context): |
| … | … | @@ -264,7 +282,7 @@ class Session: |
| 264 | 282 | |
| 265 | 283 | space = context.space_data |
| 266 | 284 | view_proj = self._view_projection(space.region_3d, space.lens) |
| 267 | | if view_proj != self.last_view: |
| 285 | if self._view_moved(view_proj): |
| 268 | 286 | self.last_view = view_proj |
| 269 | 287 | self.invalidate() |
| 270 | 288 | |
| … | … | @@ -313,7 +331,9 @@ class Session: |
| 313 | 331 | self.tonemap.uniform_sampler('max_tex', self.maxbuf) |
| 314 | 332 | self.tonemap.uniform_int('out_size', (self.width, self.height)) |
| 315 | 333 | self.tonemap.uniform_float('brightness', self.brightness) |
| 316 | | gpu.state.blend_set('ALPHA') |
| 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') |
| 317 | 337 | self.batch.draw(self.tonemap) |
| 318 | 338 | gpu.state.blend_set('NONE') # leave the viewport's state as we found it |
| 319 | 339 | |