| … | … | @@ -212,18 +212,6 @@ class Session: |
| 212 | 212 | self.opacity_gain = 4.0 |
| 213 | 213 | self.relief = 12.0 |
| 214 | 214 | |
| 215 | | # Gaussian-splat preview. An ALTERNATIVE to the histogram path rather than |
| 216 | | # a stage of it: the histogram accumulates progressively into a 2D image, |
| 217 | | # splats draw the emitted points as geometry. Nothing here is built until |
| 218 | | # the mode is switched on, because emitting the points costs a full GPU |
| 219 | | # pass and most sessions never ask for it. |
| 220 | | self.splats_enabled = False |
| 221 | | self.splat_shader = None # the draw shader (ifs_splat) |
| 222 | | self.splat_emit_shader = None # the points kernel, cached across re-emits |
| 223 | | self.splat_cloud = None |
| 224 | | self.splat_stats = None |
| 225 | | self.splat_capacity = 1_000_000 |
| 226 | | self.splat_radius_scale = 1.0 |
| 227 | 215 | |
| 228 | 216 | # What needs rebuilding before the next dispatch. Property callbacks fire |
| 229 | 217 | # continuously while a slider is dragged, so they only set flags here and |
| … | … | @@ -293,28 +281,11 @@ class Session: |
| 293 | 281 | if kind == 'weights': |
| 294 | 282 | self._dirty_weights = True |
| 295 | 283 | self._dirty_params = True |
| 296 | | self.drop_splats() |
| 297 | 284 | elif kind == 'params': |
| 298 | 285 | self._dirty_params = True |
| 299 | | self.drop_splats() |
| 300 | | elif kind == 'splats': |
| 301 | | self.drop_splats() |
| 302 | 286 | elif kind != 'tonemap': |
| 303 | 287 | raise ValueError(f"unknown dirty kind {kind!r}") |
| 304 | 288 | |
| 305 | | def drop_splats(self): |
| 306 | | """ |
| 307 | | Discards the splat cloud so the next draw re-emits it. |
| 308 | | |
| 309 | | `params` and `weights` change the ATTRACTOR, and unlike the histogram — which |
| 310 | | merely restarts accumulating — a splat cloud is a fixed set of samples from |
| 311 | | the old fractal. Leaving it up would show the previous document indefinitely |
| 312 | | while the panel says otherwise, which is the "succeeds silently into a void" |
| 313 | | failure this add-on keeps producing. `tonemap` deliberately does NOT drop it: |
| 314 | | brightness does not move a sample. |
| 315 | | """ |
| 316 | | self.splat_cloud = None |
| 317 | | self.splat_stats = None |
| 318 | 289 | |
| 319 | 290 | SHADING_MODES = {'EMISSION': 0, 'FLAT': 1, 'METAL': 2} |
| 320 | 291 | |
| … | … | @@ -331,16 +302,6 @@ class Session: |
| 331 | 302 | self.opacity_gain = settings.opacity_gain |
| 332 | 303 | self.relief = settings.relief |
| 333 | 304 | |
| 334 | | # Splat settings. Capacity and radius are read every frame so the panel |
| 335 | | # sliders reach the preview; a change to CAPACITY needs a re-emit (it is |
| 336 | | # how many samples we keep), a change to RADIUS does not (it only scales |
| 337 | | # the quads), so only the former drops the cloud. |
| 338 | | self.splats_enabled = bool(getattr(settings, 'viewport_splats', False)) |
| 339 | | capacity = int(getattr(settings, 'points_capacity', self.splat_capacity)) |
| 340 | | if capacity != self.splat_capacity: |
| 341 | | self.splat_capacity = capacity |
| 342 | | self.drop_splats() |
| 343 | | self.splat_radius_scale = float(getattr(settings, 'points_radius', 1.0)) |
| 344 | 305 | |
| 345 | 306 | # When a node graph is bound it OWNS the iterators. Copying the panel's |
| 346 | 307 | # values on every frame would revert each graph edit one frame after it |
| … | … | @@ -536,14 +497,6 @@ class Session: |
| 536 | 497 | |
| 537 | 498 | space = context.space_data |
| 538 | 499 | |
| 539 | | # The splat path REPLACES the histogram rather than adding to it: the two |
| 540 | | # draw the same fractal by different means, and running both would double |
| 541 | | # the emission and cost a compute dispatch per frame for an image nobody |
| 542 | | # sees. Everything above this line still runs — the panel, the node graph |
| 543 | | # and the dirty flags all have to reach the document either way. |
| 544 | | if self.splats_enabled: |
| 545 | | self._draw_splats(context, space) |
| 546 | | return |
| 547 | 500 | |
| 548 | 501 | view_proj = self._view_projection(space.region_3d, space.lens) |
| 549 | 502 | moved = self._view_moved(view_proj) |
| … | … | @@ -650,88 +603,6 @@ class Session: |
| 650 | 603 | gpu.state.depth_test_set('NONE') |
| 651 | 604 | gpu.state.blend_set('NONE') # leave the viewport's state as we found it |
| 652 | 605 | |
| 653 | | def _build_splats(self): |
| 654 | | """ |
| 655 | | Emits the points once and uploads them as a splat cloud. |
| 656 | | |
| 657 | | This is a full GPU pass and it BLOCKS the redraw it runs in, so it happens |
| 658 | | only when the cloud is missing — on the first splat frame, and after an |
| 659 | | edit that changed the attractor. The points kernel is cached across |
| 660 | | re-emits because compiling it is most of the cost. |
| 661 | | """ |
| 662 | | import ifs_points |
| 663 | | import ifs_splat |
| 664 | | |
| 665 | | if self.splat_shader is None: |
| 666 | | self.splat_shader = ifs_splat.build_shader() |
| 667 | | if self.splat_emit_shader is None: |
| 668 | | self.splat_emit_shader, _lib = ifs_points.build_shader() |
| 669 | | |
| 670 | | positions, colors, stats = ifs_points.emit_document( |
| 671 | | self.doc, capacity=self.splat_capacity, shader=self.splat_emit_shader) |
| 672 | | |
| 673 | | # A radius PER POINT. The global estimate cannot serve a fractal: measured |
| 674 | | # on the shipped templates the nearest-neighbour distance spans 16-18x |
| 675 | | # within one document, so one value leaves the filaments as separate beads |
| 676 | | # while bloating the halo. `dimension` is reused from the emit rather than |
| 677 | | # re-estimated — it is the same cloud. |
| 678 | | radius = ifs_points.local_radius(positions, dimension=stats.get('dimension')) |
| 679 | | self.splat_cloud = ifs_splat.upload( |
| 680 | | positions, colors, radius * self.splat_radius_scale) |
| 681 | | self.splat_stats = stats |
| 682 | | |
| 683 | | def _draw_splats(self, context, space): |
| 684 | | """ |
| 685 | | The splat pass: emitted points accumulated as gaussians, then TONEMAPPED. |
| 686 | | |
| 687 | | The important word is tonemapped. An earlier version drew the gaussians |
| 688 | | straight to the viewport and it was wrong in a way no setting could fix — |
| 689 | | a flame's appearance comes from a curve applied to ACCUMULATED density, |
| 690 | | not from what one sample looks like, so raw emissive quads render as fuzzy |
| 691 | | beads and Brightness / shading mode / opacity gain do nothing at all |
| 692 | | because the shader that reads them never ran. |
| 693 | | |
| 694 | | So this fills the SAME `accum` buffer the histogram path fills and then |
| 695 | | calls the same `_tonemap_blit`. Splats become a different way of |
| 696 | | accumulating density rather than a different renderer, every existing |
| 697 | | control keeps working, and occlusion comes back because the fragment |
| 698 | | shader writes `depth_img` in the kernel's encoding. |
| 699 | | """ |
| 700 | | import ifs_splat |
| 701 | | |
| 702 | | region = context.region |
| 703 | | # Full resolution: the progressive divisor exists to keep an ACCUMULATING |
| 704 | | # render interactive, and a splat pass redraws the whole cloud each frame |
| 705 | | # regardless. |
| 706 | | self._resize(max(region.width, 16), max(region.height, 16)) |
| 707 | | |
| 708 | | if self.splat_cloud is None: |
| 709 | | try: |
| 710 | | self._build_splats() |
| 711 | | except Exception: |
| 712 | | # A raising draw handler removes itself and the viewport goes dark |
| 713 | | # with nothing said. Report once and fall back rather than that. |
| 714 | | import traceback |
| 715 | | traceback.print_exc() |
| 716 | | self.splats_enabled = False |
| 717 | | return |
| 718 | | |
| 719 | | # Both buffers are rebuilt from scratch every frame. Unlike the histogram |
| 720 | | # there is nothing to accumulate ACROSS frames — the cloud is a fixed set |
| 721 | | # of samples — so stale contents would just double-expose. |
| 722 | | self.accum.clear(format='FLOAT', value=(0.0, 0.0, 0.0, 0.0)) |
| 723 | | self.depthbuf.clear(format='UINT', value=(ifs_layout.DEPTH_CLEAR, 0, 0, 0)) |
| 724 | | |
| 725 | | framebuffer = gpu.types.GPUFrameBuffer(color_slots=(self.accum,)) |
| 726 | | with framebuffer.bind(): |
| 727 | | ifs_splat.draw(self.splat_cloud, self.splat_shader, |
| 728 | | space.region_3d.view_matrix, |
| 729 | | space.region_3d.window_matrix, |
| 730 | | self.depthbuf) |
| 731 | | |
| 732 | | self._tonemap_blit() |
| 733 | | self.frames += 1 |
| 734 | | |
| 735 | 606 | @property |
| 736 | 607 | def iterations(self): |
| 737 | 608 | return self.since_reset * self.iters_per_frame * ifs_render.INVOCATIONS |