| … | … | @@ -0,0 +1,147 @@ |
| 1 | # Review: the rendering approaches, and a UI accessibility audit |
| 2 | |
| 3 | 2026-08-18. Written after a session that added a splat renderer to the viewport |
| 4 | and 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 | |
| 12 | The live viewport plots **6,553,600 samples per frame** (16384 invocations x 400 |
| 13 | iterations) and accumulates indefinitely: **655 million** after 100 frames. The |
| 14 | splat path emits a **fixed 1,000,000** samples and redraws them every frame. |
| 15 | |
| 16 | So the splat view shows roughly **0.15%** of the samples the histogram holds |
| 17 | after two seconds. A flame's filaments are *traced by sample density*; with 1M |
| 18 | points spread through a 3D attractor you get isolated dots, and no radius, |
| 19 | gaussian falloff or tonemap can synthesise structure from samples that were never |
| 20 | emitted. Reported from the viewport as "still just fuzzy dots" — correctly. |
| 21 | |
| 22 | It also explains the performance regression that came with the per-point radius: |
| 23 | 1M splats is 4M vertices and 2M triangles redrawn every frame, and calibrating |
| 24 | the radius up ~8x multiplied the covered area by ~64. We pay that every frame and |
| 25 | keep 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 |
| 28 | representation 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 | |
| 39 | Anything shipping a fixed sample set is capped at what we emitted. Anything that |
| 40 | accumulates keeps earning detail. That is why the volume bake — despite its |
| 41 | cubic memory ceiling and mushy filaments — is the only *in-scene* representation |
| 42 | that can approach the native look: it accumulates into its grid. |
| 43 | |
| 44 | ### Ranking, for "looks like the binary AND is in the scene" |
| 45 | |
| 46 | 1. **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. |
| 52 | 2. **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. |
| 55 | 3. **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 | |
| 61 | Keep `ifs_splat`; drop the viewport toggle. The module is the right foundation |
| 62 | for the F12 composite and for writing authored splats out. It is simply the wrong |
| 63 | renderer for a preview where we already have one that is 655x better informed and |
| 64 | cheaper. |
| 65 | |
| 66 | Two 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 | |
| 81 | Audited 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 |
| 86 | freeze Blender for seconds to minutes with **no progress bar, no cancel, no |
| 87 | cursor change, no message**. A frozen UI is indistinguishable from a crash. |
| 88 | |
| 89 | This is the worst defect and it is an accessibility one, not merely a polish one: |
| 90 | a screen-reader user gets *nothing* — no announcement, no focus change, no |
| 91 | completion event — and anyone who cannot see a busy cursor has no way to tell |
| 92 | waiting from hung. Users who need longer to interpret state pay the most. |
| 93 | |
| 94 | The fix is a modal operator with `wm.progress_begin/update/end` and an ESC path, |
| 95 | or 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 |
| 100 | splats were on) and again under **Point Cloud**, with nothing indicating they are |
| 101 | one setting. Changing either silently changed the other. Introduced by the splat |
| 102 | work; fixed by drawing them once and stating the sharing. |
| 103 | |
| 104 | ### Severity 2 — errors are transient |
| 105 | |
| 106 | Failures go to `self.report({'ERROR'})`, a popup that disappears. The only |
| 107 | persistent channel is the single `status` string, which the next action |
| 108 | overwrites. An error not seen at the moment it happened is unrecoverable. |
| 109 | |
| 110 | Persist the last error in `status` (or a dedicated field) with an `ERROR` icon so |
| 111 | it 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. |
| 116 | The Output box does the right thing instead — it keeps the resolution fields and |
| 117 | sets `sub.enabled = False`. Two conventions in one panel, and disappearing |
| 118 | controls are hard to track without vision and hard for anyone to find again. |
| 119 | |
| 120 | Prefer `enabled = False` over conditional drawing wherever the control still |
| 121 | applies conceptually. |
| 122 | |
| 123 | ### Severity 3 — grouping that misleads |
| 124 | |
| 125 | **Brightness** sits in the **Output** box but drives the Live Viewport tonemap. A |
| 126 | control's location implies its scope. Move it, or draw it in both with the |
| 127 | sharing 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. |