OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux commits / 6bce8df

Review the approaches, audit the UI, and stop drawing one setting twice
WRITE-UP: NOTES-approaches-and-accessibility.md Part 1 records the measurement that reorders every rendering approach. The live viewport plots 6,553,600 samples PER FRAME and accumulates without bound — 655 million after 100 frames — while the splat path emits a fixed 1,000,000 and redraws them. The splat view therefore shows ~0.15% of the samples the histogram holds after two seconds, which is why it reads as fuzzy dots however the radius is tuned, and why calibrating the radius up ~8x (≈64x the covered area) bought a performance regression and no structure. The dividing line is not 2D-vs-3D or overlay-vs-scene. It is whether the representation ACCUMULATES. Fixed sample sets — points, splats, PLY — are capped at what was emitted; the histogram and the volume grid keep earning detail. That is why the volume bake, despite mushy filaments, is the only IN-SCENE representation that can approach the native look. Recommendation recorded: build the F12 composite (exact, reuses the accumulate -> tonemap pipeline the splat rewire created), keep the volume bake for lighting the scene, keep ifs_splat for export, and drop the viewport splat toggle — it is a preview competing with one that is 655x better informed and cheaper. Part 2 audits the panel as written. Worst defect is an accessibility one: render, bake and emit each freeze Blender for seconds to minutes with no progress, no cancel and no message. A frozen UI is indistinguishable from a crash, and a screen-reader user gets nothing at all — no announcement, no focus change, no completion event. Also recorded: transient errors that cannot be re-read, controls that hide where the Output box correctly greys out, Brightness grouped under Output while driving the viewport tonemap, jargon in labels with the explanation only in tooltips, and an icon-only button. FIX: points_capacity and points_radius were drawn in BOTH the Live Viewport and Point Cloud sections with nothing saying they are one setting, so changing either silently changed the other. Introduced by the splat work. Now drawn once, owned by Point Cloud, with a note in each direction. iters_per_frame greys out under splats rather than vanishing — the convention the Output box already uses. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6bce8df1d9f3b20424fab1744c9924afb0b99bfe
by plankatron <fleeky@gmail.com> · 2026-08-18T15:33:03.000Z

Changed files (2) +158 -5

blender/NOTES-approaches-and-accessibility.md +147 -0
……@@ -0,0 +1,147 @@
1# Review: the rendering approaches, and a UI accessibility audit
2
32026-08-18. Written after a session that added a splat renderer to the viewport
4and then found it was the wrong tool — the useful part is why.
5
6---
7
8## Part 1 — the approaches
9
10### The measurement that reorders everything
11
12The live viewport plots **6,553,600 samples per frame** (16384 invocations x 400
13iterations) and accumulates indefinitely: **655 million** after 100 frames. The
14splat path emits a **fixed 1,000,000** samples and redraws them every frame.
15
16So the splat view shows roughly **0.15%** of the samples the histogram holds
17after two seconds. A flame's filaments are *traced by sample density*; with 1M
18points spread through a 3D attractor you get isolated dots, and no radius,
19gaussian falloff or tonemap can synthesise structure from samples that were never
20emitted. Reported from the viewport as "still just fuzzy dots" — correctly.
21
22It also explains the performance regression that came with the per-point radius:
231M splats is 4M vertices and 2M triangles redrawn every frame, and calibrating
24the radius up ~8x multiplied the covered area by ~64. We pay that every frame and
25keep nothing, while the histogram pays one compute dispatch and keeps everything.
26
27**The dividing line is not 2D-vs-3D or overlay-vs-scene. It is whether the
28representation ACCUMULATES.**
29
30| approach | native look | in scene | F12 | lights scene | accumulates? |
31|---|---|---|---|---|---|
32| Viewport histogram | **exact** | overlay + depth composite | no | no | **yes**, unbounded |
33| Viewport splats | tonemapped, sparse | overlay + depth composite | no | no | **no**, fixed 1M |
34| Volume bake (VDB) | approximate | yes, real data | yes | **yes** | **yes**, into the grid |
35| Point cloud | no tonemap | yes, real data | yes | emission only | **no**, fixed |
36| Splat PLY export | consumer's tonemap | external tools | n/a | no | **no**, fixed |
37| F12 composite | **exact** | composited on depth | yes | no | inherits the histogram |
38
39Anything shipping a fixed sample set is capped at what we emitted. Anything that
40accumulates keeps earning detail. That is why the volume bake — despite its
41cubic memory ceiling and mushy filaments — is the only *in-scene* representation
42that can approach the native look: it accumulates into its grid.
43
44### Ranking, for "looks like the binary AND is in the scene"
45
461. **F12 composite.** Render the existing accumulate -> tonemap pipeline offscreen
47 and composite it over Cycles on depth. Exact appearance, correct occlusion, no
48 light interaction. Not built. Cheapest remaining win, because the rewire that
49 made splats feed `accum` + `_tonemap_blit` also made this a *consumer* of an
50 existing pipeline rather than a second implementation. Both KIRI and splatviz
51 independently settled on this shape.
522. **Volume bake.** Real scene data, real integration, and the only option where
53 the flame *lights other objects*. Approximate look, resolution-limited. Works
54 today.
553. **Points / splats.** Structurally cannot reach the native look. Their value is
56 portability (any splat viewer opens a PLY) and unbounded spatial precision, not
57 fidelity to the binary.
58
59### What to do with the splat code
60
61Keep `ifs_splat`; drop the viewport toggle. The module is the right foundation
62for the F12 composite and for writing authored splats out. It is simply the wrong
63renderer for a preview where we already have one that is 655x better informed and
64cheaper.
65
66Two findings from it are worth keeping regardless:
67
68- **Isotropic + emissive deletes most of a 3DGS renderer.** No covariance
69 projection (a sphere projects to a circle), and no depth sort (emission
70 commutes). Blender's own EEVEE splat branch does not sort either — it uses
71 dithered/stochastic opacity, a third answer.
72- **The tonemap is not optional and cannot live in a material.** Auto-exposure
73 needs the whole-image maximum, which no per-ray shader can see. That is the
74 reason exact-native appearance and full path-traced integration cannot be had
75 simultaneously, and no tool in the ecosystem audit achieves both.
76
77---
78
79## Part 2 — UI accessibility audit
80
81Audited against `IFS_PT_main.draw` as written, not from memory.
82
83### Severity 1 — blocking operations with no progress or cancel
84
85`ifs.render`, `ifs.bake_volume`, `ifs.emit_points` and the first splat frame each
86freeze Blender for seconds to minutes with **no progress bar, no cancel, no
87cursor change, no message**. A frozen UI is indistinguishable from a crash.
88
89This is the worst defect and it is an accessibility one, not merely a polish one:
90a screen-reader user gets *nothing* — no announcement, no focus change, no
91completion event — and anyone who cannot see a busy cursor has no way to tell
92waiting from hung. Users who need longer to interpret state pay the most.
93
94The fix is a modal operator with `wm.progress_begin/update/end` and an ESC path,
95or chunked work in a timer. Already recorded as issue #6 item 7.
96
97### Severity 2 — the same property drawn in two places
98
99`points_capacity` and `points_radius` appeared under **Live Viewport** (when
100splats were on) and again under **Point Cloud**, with nothing indicating they are
101one setting. Changing either silently changed the other. Introduced by the splat
102work; fixed by drawing them once and stating the sharing.
103
104### Severity 2 — errors are transient
105
106Failures go to `self.report({'ERROR'})`, a popup that disappears. The only
107persistent channel is the single `status` string, which the next action
108overwrites. An error not seen at the moment it happened is unrecoverable.
109
110Persist the last error in `status` (or a dedicated field) with an `ERROR` icon so
111it stays readable and can be re-read.
112
113### Severity 3 — controls hide instead of greying out
114
115`opacity_gain` appears only for non-EMISSION shading, `relief` only for METAL.
116The Output box does the right thing instead — it keeps the resolution fields and
117sets `sub.enabled = False`. Two conventions in one panel, and disappearing
118controls are hard to track without vision and hard for anyone to find again.
119
120Prefer `enabled = False` over conditional drawing wherever the control still
121applies conceptually.
122
123### Severity 3 — grouping that misleads
124
125**Brightness** sits in the **Output** box but drives the Live Viewport tonemap. A
126control's location implies its scope. Move it, or draw it in both with the
127sharing stated (as the points settings now are).
128
129### Severity 4 — structure and labelling
130
131- One long ungrouped column: four boxes, ~20 controls, no collapsible child
132 panels — though `IFS_PT_iterators` proves we already use them. The same
133 complaint the Avalonia parity audit makes about the canonical app's "twelve
134 ALL-CAPS sections in sequence".
135- `load_template` is icon-only (`text=""`), relying entirely on its tooltip.
136- Jargon without units in the labels: *Dispatches* (batches of 16384x500),
137 *Voxels*, *Iterations/frame*, *Emission x*, *Radius x*. Every one has a good
138 `description=`; none of it is in the label a user scans.
139
140### What is already right
141
142- **Start/Stop changes its TEXT**, not just its icon and depress state — state is
143 not signalled by colour alone.
144- Every property carries a real `description=`, so tooltips are informative.
145- The Output box's enable/disable pattern is the correct convention.
146- Status messages name *which* document was used ("from the live viewport" vs a
147 filename), which is exactly the ambiguity that used to be invisible.
blender/__init__.py +11 -5
……@@ -652,12 +652,16 @@ class IFS_PT_main(Panel):
652652 if settings.shading_mode == 'METAL':
653653 box.prop(settings, "relief")
654654 box.prop(settings, "viewport_splats")
655 # `points_capacity` and `points_radius` are ONE setting shared with the
656 # Point Cloud section below. Drawing them in both places meant changing
657 # either silently changed the other, with nothing on screen saying so.
658 # Named here, owned there.
659 sub = box.column(align=True)
660 sub.enabled = not settings.viewport_splats
661 sub.prop(settings, "iters_per_frame")
655662 if settings.viewport_splats:
656 sub = box.column(align=True)
657 sub.prop(settings, "points_capacity")
658 sub.prop(settings, "points_radius")
659 else:
660 box.prop(settings, "iters_per_frame")
663 box.label(text="Uses Points / Radius from Point Cloud below",
664 icon='INFO')
661665 running = ifs_viewport.is_running()
662666 box.operator("ifs.toggle_viewport",
663667 text="Stop" if running else "Start",
……@@ -675,6 +679,8 @@ class IFS_PT_main(Panel):
675679
676680 box = layout.box()
677681 box.label(text="Point Cloud (Cycles and EEVEE)")
682 if settings.viewport_splats:
683 box.label(text="Also drives the splat viewport", icon='INFO')
678684 box.prop(settings, "points_capacity")
679685 box.prop(settings, "points_radius")
680686 box.prop(settings, "points_emission")