Rename doc/ to Documentation/ and unify version at 1.0.0-SNAPSHOT
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sat, 19 Sep 2026 21:41:33 +0000 (00:41 +0300)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sat, 19 Sep 2026 21:41:33 +0000 (00:41 +0300)
- Documentation/ picks up the whole former doc/ tree (org sources,
  diagrams, screenshots); export-docs.sh comments follow
- .gitignore, AGENTS.org and Tools/Update web site updated to the new
  layout (export scan, javadoc staging, graphs, upload)
- pom.xml: 1.5-SNAPSHOT -> 1.0.0-SNAPSHOT; correct <scm> to the
  current hosting URL

160 files changed:
.gitignore
AGENTS.org
Documentation/Agentic development/Golden workflow.svg [new file with mode: 0644]
Documentation/Agentic development/Headless lanes.svg [new file with mode: 0644]
Documentation/Agentic development/Pixel assertion.svg [new file with mode: 0644]
Documentation/Agentic development/diff-example.png [new file with mode: 0644]
Documentation/Agentic development/snapshot-example.png [new file with mode: 0644]
Documentation/CSG/BSP tree.svg [new file with mode: 0644]
Documentation/CSG/CSG demo.png [new file with mode: 0644]
Documentation/CSG/CSG intersect.svg [new file with mode: 0644]
Documentation/CSG/CSG operations.svg [new file with mode: 0644]
Documentation/CSG/CSG union.svg [new file with mode: 0644]
Documentation/CSG/Polygon clipping.svg [new file with mode: 0644]
Documentation/CSG/index.org [new file with mode: 0644]
Documentation/Coordinate system.svg [new file with mode: 0644]
Documentation/Depth buffer/index.org [new file with mode: 0644]
Documentation/Developer tools/Developer tools.png [new file with mode: 0644]
Documentation/Developer tools/Render alternative segments.png [new file with mode: 0644]
Documentation/Developer tools/Render polygon borders.png [new file with mode: 0644]
Documentation/Developer tools/Show segment boundaries.png [new file with mode: 0644]
Documentation/Developer tools/Thread timeline.png [new file with mode: 0644]
Documentation/Edge.svg [new file with mode: 0644]
Documentation/Example.png [new file with mode: 0644]
Documentation/Face triangle.svg [new file with mode: 0644]
Documentation/Frustum culling/Frustum diagram.svg [new file with mode: 0644]
Documentation/Frustum culling/P-vertex AABB.svg [new file with mode: 0644]
Documentation/Frustum culling/index.org [new file with mode: 0644]
Documentation/Global illumination/Bounce estimator.svg [new file with mode: 0644]
Documentation/Global illumination/GI pipeline.svg [new file with mode: 0644]
Documentation/Global illumination/Global illumination.png [new file with mode: 0644]
Documentation/Global illumination/Lightmap mapping.svg [new file with mode: 0644]
Documentation/Global illumination/gi-converged.png [new file with mode: 0644]
Documentation/Global illumination/gi-flat.png [new file with mode: 0644]
Documentation/Global illumination/gi-start.png [new file with mode: 0644]
Documentation/Global illumination/index.org [new file with mode: 0644]
Documentation/Mesh.svg [new file with mode: 0644]
Documentation/Near plane clip/Clip algorithm.svg [new file with mode: 0644]
Documentation/Near plane clip/Fan triangulation.svg [new file with mode: 0644]
Documentation/Near plane clip/Near plane straddle.svg [new file with mode: 0644]
Documentation/Near plane clip/index.org [new file with mode: 0644]
Documentation/Near plane clip/near-clip-after.png [new file with mode: 0644]
Documentation/Near plane clip/near-clip-before.png [new file with mode: 0644]
Documentation/Normal vector.svg [new file with mode: 0644]
Documentation/Perspective correct textures/Adaptive interval.svg [new file with mode: 0644]
Documentation/Perspective correct textures/Affine distortion.png [new file with mode: 0644]
Documentation/Perspective correct textures/Scanline correction.svg [new file with mode: 0644]
Documentation/Perspective correct textures/index.org [new file with mode: 0644]
Documentation/Point3D vertex.svg [new file with mode: 0644]
Documentation/Rendering loop/CPU scheduling.png [new file with mode: 0644]
Documentation/Rendering loop/Double buffering.svg [new file with mode: 0644]
Documentation/Rendering loop/Paint tiles.svg [new file with mode: 0644]
Documentation/Rendering loop/Painter's algorithm.svg [new file with mode: 0644]
Documentation/Rendering loop/Render pipeline.svg [new file with mode: 0644]
Documentation/Rendering loop/index.org [new file with mode: 0644]
Documentation/SDF textures/SDF concept.svg [new file with mode: 0644]
Documentation/SDF textures/SDF glyph pipeline.svg [new file with mode: 0644]
Documentation/SDF textures/SDF minification.svg [new file with mode: 0644]
Documentation/SDF textures/glyph-sdf-S.png [new file with mode: 0644]
Documentation/SDF textures/index.org [new file with mode: 0644]
Documentation/SDF textures/sdf-angled.png [new file with mode: 0644]
Documentation/SDF textures/sdf-far-zoom.png [new file with mode: 0644]
Documentation/SDF textures/sdf-far.png [new file with mode: 0644]
Documentation/SDF textures/sdf-mid.png [new file with mode: 0644]
Documentation/SDF textures/sdf-near.png [new file with mode: 0644]
Documentation/Shading/Ambient light comparison.svg [new file with mode: 0644]
Documentation/Shading/Distance attenuation.svg [new file with mode: 0644]
Documentation/Shading/Lambert cosine law.svg [new file with mode: 0644]
Documentation/Shading/Shaded sphere.png [new file with mode: 0644]
Documentation/Shading/Shading pipeline.svg [new file with mode: 0644]
Documentation/Shading/index.org [new file with mode: 0644]
Documentation/Stereoscopic rendering/Stereo geometry.svg [new file with mode: 0644]
Documentation/Stereoscopic rendering/Stereo per eye.svg [new file with mode: 0644]
Documentation/Stereoscopic rendering/Stereo pipeline.svg [new file with mode: 0644]
Documentation/Stereoscopic rendering/index.org [new file with mode: 0644]
Documentation/Stereoscopic rendering/mono-comparison.png [new file with mode: 0644]
Documentation/Stereoscopic rendering/stereo-side-by-side.png [new file with mode: 0644]
Documentation/Winding order.svg [new file with mode: 0644]
Documentation/export-docs.sh [new file with mode: 0755]
Documentation/index.org [new file with mode: 0644]
Documentation/style.css [new file with mode: 0644]
Tools/Update web site
doc/Agentic development/Golden workflow.svg [deleted file]
doc/Agentic development/Headless lanes.svg [deleted file]
doc/Agentic development/Pixel assertion.svg [deleted file]
doc/Agentic development/diff-example.png [deleted file]
doc/Agentic development/snapshot-example.png [deleted file]
doc/CSG/BSP tree.svg [deleted file]
doc/CSG/CSG demo.png [deleted file]
doc/CSG/CSG intersect.svg [deleted file]
doc/CSG/CSG operations.svg [deleted file]
doc/CSG/CSG union.svg [deleted file]
doc/CSG/Polygon clipping.svg [deleted file]
doc/CSG/index.org [deleted file]
doc/Coordinate system.svg [deleted file]
doc/Depth buffer/index.org [deleted file]
doc/Developer tools/Developer tools.png [deleted file]
doc/Developer tools/Render alternative segments.png [deleted file]
doc/Developer tools/Render polygon borders.png [deleted file]
doc/Developer tools/Show segment boundaries.png [deleted file]
doc/Developer tools/Thread timeline.png [deleted file]
doc/Edge.svg [deleted file]
doc/Example.png [deleted file]
doc/Face triangle.svg [deleted file]
doc/Frustum culling/Frustum diagram.svg [deleted file]
doc/Frustum culling/P-vertex AABB.svg [deleted file]
doc/Frustum culling/index.org [deleted file]
doc/Global illumination/Bounce estimator.svg [deleted file]
doc/Global illumination/GI pipeline.svg [deleted file]
doc/Global illumination/Global illumination.png [deleted file]
doc/Global illumination/Lightmap mapping.svg [deleted file]
doc/Global illumination/gi-converged.png [deleted file]
doc/Global illumination/gi-flat.png [deleted file]
doc/Global illumination/gi-start.png [deleted file]
doc/Global illumination/index.org [deleted file]
doc/Mesh.svg [deleted file]
doc/Near plane clip/Clip algorithm.svg [deleted file]
doc/Near plane clip/Fan triangulation.svg [deleted file]
doc/Near plane clip/Near plane straddle.svg [deleted file]
doc/Near plane clip/index.org [deleted file]
doc/Near plane clip/near-clip-after.png [deleted file]
doc/Near plane clip/near-clip-before.png [deleted file]
doc/Normal vector.svg [deleted file]
doc/Perspective correct textures/Adaptive interval.svg [deleted file]
doc/Perspective correct textures/Affine distortion.png [deleted file]
doc/Perspective correct textures/Scanline correction.svg [deleted file]
doc/Perspective correct textures/index.org [deleted file]
doc/Point3D vertex.svg [deleted file]
doc/Rendering loop/CPU scheduling.png [deleted file]
doc/Rendering loop/Double buffering.svg [deleted file]
doc/Rendering loop/Paint tiles.svg [deleted file]
doc/Rendering loop/Painter's algorithm.svg [deleted file]
doc/Rendering loop/Render pipeline.svg [deleted file]
doc/Rendering loop/index.org [deleted file]
doc/SDF textures/SDF concept.svg [deleted file]
doc/SDF textures/SDF glyph pipeline.svg [deleted file]
doc/SDF textures/SDF minification.svg [deleted file]
doc/SDF textures/glyph-sdf-S.png [deleted file]
doc/SDF textures/index.org [deleted file]
doc/SDF textures/sdf-angled.png [deleted file]
doc/SDF textures/sdf-far-zoom.png [deleted file]
doc/SDF textures/sdf-far.png [deleted file]
doc/SDF textures/sdf-mid.png [deleted file]
doc/SDF textures/sdf-near.png [deleted file]
doc/Shading/Ambient light comparison.svg [deleted file]
doc/Shading/Distance attenuation.svg [deleted file]
doc/Shading/Lambert cosine law.svg [deleted file]
doc/Shading/Shaded sphere.png [deleted file]
doc/Shading/Shading pipeline.svg [deleted file]
doc/Shading/index.org [deleted file]
doc/Stereoscopic rendering/Stereo geometry.svg [deleted file]
doc/Stereoscopic rendering/Stereo per eye.svg [deleted file]
doc/Stereoscopic rendering/Stereo pipeline.svg [deleted file]
doc/Stereoscopic rendering/index.org [deleted file]
doc/Stereoscopic rendering/mono-comparison.png [deleted file]
doc/Stereoscopic rendering/stereo-side-by-side.png [deleted file]
doc/Winding order.svg [deleted file]
doc/export-docs.sh [deleted file]
doc/index.org [deleted file]
doc/style.css [deleted file]
pom.xml

index 31378ad..319eac6 100644 (file)
@@ -3,7 +3,7 @@
 /.classpath
 /.project
 /.settings/
-/doc/graphs/
-/doc/apidocs/
+/Documentation/graphs/
+/Documentation/apidocs/
 /*.iml
 *.html
index 7dc28c0..2dc0fb8 100644 (file)
@@ -447,8 +447,8 @@ java -cp "target/classes:$(cat cp.txt)" \
   eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens --update  # regenerate goldens
 
 # Regenerate all HTML documentation (org -> HTML, darksun theme)
-doc/export-docs.sh            # export
-doc/export-docs.sh --check    # export + headless-Chrome screenshots to /tmp
+Documentation/export-docs.sh            # export
+Documentation/export-docs.sh --check    # export + headless-Chrome screenshots to /tmp
 #+end_src
 
 Test files: ~src/test/java/~ (JUnit 4)
@@ -558,18 +558,18 @@ rebuild the exact demo scene headlessly.
 
 | Path                                         | Topic                                                               |
 |----------------------------------------------+---------------------------------------------------------------------|
-| ~doc/index.org~                              | Main: coordinate system, shapes, CSG, developer tools               |
-| ~doc/Rendering loop/index.org~               | 5-phase pipeline, multi-threaded paint                              |
-| ~doc/Shading/index.org~                      | Lambert shading, lights, distance attenuation                       |
-| ~doc/CSG/index.org~                          | Boolean ops via BSP trees                                           |
-| ~doc/Frustum culling/index.org~              | View frustum culling                                                |
-| ~doc/Near plane clip/index.org~              | Near-plane polygon clipping (straddling geometry)                   |
-| ~doc/Global illumination/index.org~          | Progressive GI: lightmaps, bounces, convergence                     |
-| ~doc/Perspective correct textures/index.org~ | Texture mapping math                                                |
-| ~doc/Agentic development/index.org~          | Stub: headless-toolkit docs moved to ~doc/index.org~ :: Agentic development |
-| ~doc/Stereoscopic rendering/index.org~       | Side-by-side stereo: two passes, per-eye viewports, IPD             |
-| ~doc/BSP-tree painter's algorithm/index.org~ | BSP compile + rank traversal fixing average-Z sort                  |
-| ~doc/SDF textures/index.org~                 | SDF text: glyph fields, coverage window, TextCanvas                 |
-
-Regenerate all HTML: ~doc/export-docs.sh~ (add ~--check~ for rendered
+| ~Documentation/index.org~                              | Main: coordinate system, shapes, CSG, developer tools               |
+| ~Documentation/Rendering loop/index.org~               | 5-phase pipeline, multi-threaded paint                              |
+| ~Documentation/Shading/index.org~                      | Lambert shading, lights, distance attenuation                       |
+| ~Documentation/CSG/index.org~                          | Boolean ops via BSP trees                                           |
+| ~Documentation/Frustum culling/index.org~              | View frustum culling                                                |
+| ~Documentation/Near plane clip/index.org~              | Near-plane polygon clipping (straddling geometry)                   |
+| ~Documentation/Global illumination/index.org~          | Progressive GI: lightmaps, bounces, convergence                     |
+| ~Documentation/Perspective correct textures/index.org~ | Texture mapping math                                                |
+| ~Documentation/Agentic development/index.org~          | Stub: headless-toolkit docs moved to ~Documentation/index.org~ :: Agentic development |
+| ~Documentation/Stereoscopic rendering/index.org~       | Side-by-side stereo: two passes, per-eye viewports, IPD             |
+| ~Documentation/BSP-tree painter's algorithm/index.org~ | BSP compile + rank traversal fixing average-Z sort                  |
+| ~Documentation/SDF textures/index.org~                 | SDF text: glyph fields, coverage window, TextCanvas                 |
+
+Regenerate all HTML: ~Documentation/export-docs.sh~ (add ~--check~ for rendered
 screenshots of every page).
diff --git a/Documentation/Agentic development/Golden workflow.svg b/Documentation/Agentic development/Golden workflow.svg
new file mode 100644 (file)
index 0000000..aa0e7fb
--- /dev/null
@@ -0,0 +1,74 @@
+<svg viewBox="0 0 620 190" width="620" height="190" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="1.5" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+    <marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto">
+      <path d="M0 0 L8 4 L0 8 Z" fill="#40b0d0"/>
+    </marker>
+    <marker id="arrRed" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto">
+      <path d="M0 0 L8 4 L0 8 Z" fill="#FF4444"/>
+    </marker>
+  </defs>
+
+  <rect width="620" height="190" fill="#061018"/>
+  <g stroke="#1a3a4a" stroke-width="0.5">
+    <line x1="0" y1="40" x2="620" y2="40"/><line x1="0" y1="80" x2="620" y2="80"/>
+    <line x1="0" y1="120" x2="620" y2="120"/><line x1="0" y1="160" x2="620" y2="160"/>
+    <line x1="40" y1="0" x2="40" y2="190"/><line x1="80" y1="0" x2="80" y2="190"/>
+    <line x1="120" y1="0" x2="120" y2="190"/><line x1="160" y1="0" x2="160" y2="190"/>
+    <line x1="200" y1="0" x2="200" y2="190"/><line x1="240" y1="0" x2="240" y2="190"/>
+    <line x1="280" y1="0" x2="280" y2="190"/><line x1="320" y1="0" x2="320" y2="190"/>
+    <line x1="360" y1="0" x2="360" y2="190"/><line x1="400" y1="0" x2="400" y2="190"/>
+    <line x1="440" y1="0" x2="440" y2="190"/><line x1="480" y1="0" x2="480" y2="190"/>
+    <line x1="520" y1="0" x2="520" y2="190"/><line x1="560" y1="0" x2="560" y2="190"/>
+    <line x1="600" y1="0" x2="600" y2="190"/>
+  </g>
+
+  <!-- step 1: render -->
+  <rect x="20" y="55" width="110" height="60" rx="8" fill="rgba(32,112,192,0.12)" stroke="#2070c0" stroke-width="2"/>
+  <text x="75" y="80" fill="#2070c0" font-size="10" font-family="monospace" text-anchor="middle">Snapshot.render</text>
+  <text x="75" y="97" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">scene + pose</text>
+
+  <line x1="130" y1="85" x2="158" y2="85" stroke="#40b0d0" stroke-width="2" marker-end="url(#arr)"/>
+
+  <!-- step 2: golden file -->
+  <rect x="160" y="20" width="110" height="40" rx="8" fill="rgba(192,80,136,0.12)" stroke="#c05088" stroke-width="2"/>
+  <text x="215" y="38" fill="#c05088" font-size="10" font-family="monospace" text-anchor="middle">goldens/*.png</text>
+  <text x="215" y="52" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">committed reference</text>
+  <line x1="215" y1="60" x2="215" y2="80" stroke="#c05088" stroke-width="1.5" stroke-dasharray="3 2" marker-end="url(#arr)"/>
+
+  <!-- step 3: compare decision -->
+  <polygon points="215,85 265,55 315,85 265,115" fill="rgba(64,176,208,0.12)" stroke="#40b0d0" stroke-width="2"/>
+  <text x="265" y="82" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle">GoldenImage</text>
+  <text x="265" y="95" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle">.compare</text>
+
+  <!-- PASS branch -->
+  <line x1="315" y1="85" x2="348" y2="85" stroke="#39FF14" stroke-width="2" marker-end="url(#arr)"/>
+  <rect x="350" y="60" width="90" height="50" rx="8" fill="rgba(57,255,20,0.12)" stroke="#39FF14" stroke-width="2"/>
+  <text x="395" y="82" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#glow)">PASS</text>
+  <text x="395" y="98" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">exit 0</text>
+
+  <!-- FAIL branch -->
+  <path d="M265 115 L265 140 L 348 140" stroke="#FF4444" stroke-width="2" fill="none" marker-end="url(#arrRed)"/>
+  <rect x="350" y="115" width="120" height="50" rx="8" fill="rgba(255,68,68,0.1)" stroke="#FF4444" stroke-width="2"/>
+  <text x="410" y="136" fill="#FF4444" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#glow)">FAIL, exit 1</text>
+  <text x="410" y="152" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">+ diff PNG to /tmp</text>
+
+  <!-- human decision -->
+  <line x1="470" y1="140" x2="498" y2="140" stroke="#FF4444" stroke-width="2" marker-end="url(#arrRed)"/>
+  <rect x="500" y="115" width="100" height="50" rx="8" fill="rgba(255,136,51,0.1)" stroke="#FF8833" stroke-width="2"/>
+  <text x="550" y="133" fill="#FF8833" font-size="9" font-family="monospace" text-anchor="middle">bug? fix code</text>
+  <text x="550" y="146" fill="#FF8833" font-size="9" font-family="monospace" text-anchor="middle">intended? run</text>
+  <text x="550" y="158" fill="#FF8833" font-size="9" font-family="monospace" text-anchor="middle">--update</text>
+
+  <!-- update loop back to golden -->
+  <path d="M550 115 L 550 30 L 272 30" stroke="#FF8833" stroke-width="1.5" fill="none" stroke-dasharray="4 3" marker-end="url(#arr)"/>
+  <text x="420" y="22" fill="#FF8833" font-size="9" font-family="monospace" text-anchor="middle">regenerates reference</text>
+
+  <text x="310" y="182" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">tolerance: per-channel delta + max differing-pixel fraction — shading is deterministic, keep both tight</text>
+</svg>
diff --git a/Documentation/Agentic development/Headless lanes.svg b/Documentation/Agentic development/Headless lanes.svg
new file mode 100644 (file)
index 0000000..1303a80
--- /dev/null
@@ -0,0 +1,78 @@
+<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="2" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+    <marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto">
+      <path d="M0 0 L8 4 L0 8 Z" fill="#40b0d0"/>
+    </marker>
+  </defs>
+
+  <rect width="640" height="480" fill="#061018"/>
+  <g stroke="#1a3a4a" stroke-width="0.5">
+    <line x1="0" y1="40" x2="640" y2="40"/><line x1="0" y1="80" x2="640" y2="80"/>
+    <line x1="0" y1="120" x2="640" y2="120"/><line x1="0" y1="160" x2="640" y2="160"/>
+    <line x1="0" y1="200" x2="640" y2="200"/><line x1="0" y1="240" x2="640" y2="240"/>
+    <line x1="0" y1="280" x2="640" y2="280"/><line x1="0" y1="320" x2="640" y2="320"/>
+    <line x1="0" y1="360" x2="640" y2="360"/><line x1="0" y1="400" x2="640" y2="400"/>
+    <line x1="0" y1="440" x2="640" y2="440"/>
+    <line x1="40" y1="0" x2="40" y2="480"/><line x1="80" y1="0" x2="80" y2="480"/>
+    <line x1="120" y1="0" x2="120" y2="480"/><line x1="160" y1="0" x2="160" y2="480"/>
+    <line x1="200" y1="0" x2="200" y2="480"/><line x1="240" y1="0" x2="240" y2="480"/>
+    <line x1="280" y1="0" x2="280" y2="480"/><line x1="320" y1="0" x2="320" y2="480"/>
+    <line x1="360" y1="0" x2="360" y2="480"/><line x1="400" y1="0" x2="400" y2="480"/>
+    <line x1="440" y1="0" x2="440" y2="480"/><line x1="480" y1="0" x2="480" y2="480"/>
+    <line x1="520" y1="0" x2="520" y2="480"/><line x1="560" y1="0" x2="560" y2="480"/>
+    <line x1="600" y1="0" x2="600" y2="480"/>
+  </g>
+
+  <!-- ============ two entry lanes feeding ONE shared pipeline ============ -->
+
+  <!-- lane 1: on-screen app -->
+  <rect x="40" y="60" width="200" height="66" rx="8" fill="rgba(255,136,51,0.12)" stroke="#FF8833" stroke-width="2"/>
+  <text x="140" y="86" fill="#FF8833" font-size="12" font-family="monospace" text-anchor="middle" filter="url(#glow)">ViewPanel</text>
+  <text x="140" y="103" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">window + render thread</text>
+  <text x="140" y="116" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">camera from user input</text>
+
+  <!-- lane 2: headless -->
+  <rect x="40" y="354" width="200" height="66" rx="8" fill="rgba(57,255,20,0.1)" stroke="#39FF14" stroke-width="2"/>
+  <text x="140" y="380" fill="#39FF14" font-size="12" font-family="monospace" text-anchor="middle" filter="url(#glow)">Snapshot.render()</text>
+  <text x="140" y="397" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">no window, no display</text>
+  <text x="140" y="410" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">camera from pose string</text>
+
+  <!-- shared pipeline core -->
+  <rect x="300" y="170" width="140" height="140" rx="10" fill="rgba(32,112,192,0.12)" stroke="#2070c0" stroke-width="2.5"/>
+  <text x="370" y="196" fill="#2070c0" font-size="13" font-family="monospace" text-anchor="middle" filter="url(#glow)">SAME pipeline</text>
+  <text x="370" y="228" fill="#ccc" font-size="10" font-family="monospace" text-anchor="middle">transform</text>
+  <text x="370" y="248" fill="#ccc" font-size="10" font-family="monospace" text-anchor="middle">&#8595;</text>
+  <text x="370" y="266" fill="#ccc" font-size="10" font-family="monospace" text-anchor="middle">sort by Z</text>
+  <text x="370" y="284" fill="#ccc" font-size="10" font-family="monospace" text-anchor="middle">&#8595;</text>
+  <text x="370" y="302" fill="#ccc" font-size="10" font-family="monospace" text-anchor="middle">paint</text>
+
+  <!-- outputs -->
+  <rect x="500" y="60" width="110" height="50" rx="8" fill="rgba(255,136,51,0.08)" stroke="#FF8833" stroke-width="1.5"/>
+  <text x="555" y="81" fill="#FF8833" font-size="11" font-family="monospace" text-anchor="middle">screen</text>
+  <text x="555" y="97" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">BufferStrategy</text>
+
+  <rect x="490" y="330" width="130" height="90" rx="8" fill="rgba(57,255,20,0.07)" stroke="#39FF14" stroke-width="1.5"/>
+  <text x="555" y="352" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">BufferedImage</text>
+  <text x="555" y="370" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">&#8594; PNG (Snapshot.save)</text>
+  <text x="555" y="385" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">&#8594; PixelAssertions</text>
+  <text x="555" y="400" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">&#8594; GoldenImage</text>
+
+  <!-- lane arrows converging into the core -->
+  <path d="M240 93 C 290 93, 270 210, 298 225" stroke="#40b0d0" stroke-width="2" fill="none" marker-end="url(#arr)"/>
+  <path d="M240 387 C 290 387, 270 270, 298 255" stroke="#40b0d0" stroke-width="2" fill="none" marker-end="url(#arr)"/>
+
+  <!-- output arrows -->
+  <path d="M440 210 C 480 210, 460 90, 498 88" stroke="#40b0d0" stroke-width="2" fill="none" marker-end="url(#arr)"/>
+  <path d="M440 270 C 480 270, 460 372, 488 374" stroke="#40b0d0" stroke-width="2" fill="none" marker-end="url(#arr)"/>
+
+  <!-- emphasis labels -->
+  <text x="270" y="150" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle">identical results</text>
+  <text x="320" y="462" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">headless rendering drives the very same code the window uses — a test render IS the real render</text>
+</svg>
diff --git a/Documentation/Agentic development/Pixel assertion.svg b/Documentation/Agentic development/Pixel assertion.svg
new file mode 100644 (file)
index 0000000..a9218f6
--- /dev/null
@@ -0,0 +1,65 @@
+<svg viewBox="0 0 620 300" width="620" height="300" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="1.5" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+    <marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto">
+      <path d="M0 0 L8 4 L0 8 Z" fill="#40b0d0"/>
+    </marker>
+  </defs>
+
+  <rect width="620" height="300" fill="#061018"/>
+  <g stroke="#1a3a4a" stroke-width="0.5">
+    <line x1="0" y1="40" x2="620" y2="40"/><line x1="0" y1="80" x2="620" y2="80"/>
+    <line x1="0" y1="120" x2="620" y2="120"/><line x1="0" y1="160" x2="620" y2="160"/>
+    <line x1="0" y1="200" x2="620" y2="200"/><line x1="0" y1="240" x2="620" y2="240"/>
+    <line x1="0" y1="280" x2="620" y2="280"/>
+    <line x1="40" y1="0" x2="40" y2="300"/><line x1="80" y1="0" x2="80" y2="300"/>
+    <line x1="120" y1="0" x2="120" y2="300"/><line x1="160" y1="0" x2="160" y2="300"/>
+    <line x1="200" y1="0" x2="200" y2="300"/><line x1="240" y1="0" x2="240" y2="300"/>
+    <line x1="280" y1="0" x2="280" y2="300"/><line x1="320" y1="0" x2="320" y2="300"/>
+    <line x1="360" y1="0" x2="360" y2="300"/><line x1="400" y1="0" x2="400" y2="300"/>
+    <line x1="440" y1="0" x2="440" y2="300"/><line x1="480" y1="0" x2="480" y2="300"/>
+    <line x1="520" y1="0" x2="520" y2="300"/><line x1="560" y1="0" x2="560" y2="300"/>
+    <line x1="600" y1="0" x2="600" y2="300"/>
+  </g>
+
+  <!-- fake rendered frame: sentinel background + painted shapes -->
+  <rect x="60" y="40" width="320" height="220" fill="#0a1520" stroke="#40b0d0" stroke-width="2"/>
+  <text x="220" y="32" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle">rendered frame (640&#215;480)</text>
+
+  <!-- painted content: simple house-like blocks -->
+  <rect x="90" y="120" width="80" height="100" fill="rgba(192,80,136,0.55)" stroke="#c05088" stroke-width="1.5"/>
+  <rect x="190" y="80" width="120" height="60" fill="rgba(32,112,192,0.45)" stroke="#2070c0" stroke-width="1.5"/>
+  <rect x="190" y="160" width="150" height="80" fill="rgba(192,80,136,0.35)" stroke="#c05088" stroke-width="1.5"/>
+  <text x="130" y="175" fill="#e090b8" font-size="9" font-family="monospace" text-anchor="middle">painted</text>
+  <text x="255" y="205" fill="#e090b8" font-size="9" font-family="monospace" text-anchor="middle">painted</text>
+
+  <!-- the probed region: relative rect -->
+  <rect x="108" y="139" width="224" height="121" fill="none" stroke="#39FF14" stroke-width="2.5" stroke-dasharray="8 4"/>
+  <text x="338" y="132" fill="#39FF14" font-size="9" font-family="monospace" text-anchor="end" filter="url(#glow)">region (0.15, 0.45) &#8594; (0.85, 1.0)</text>
+
+  <!-- a hole: sentinel showing through -->
+  <rect x="225" y="200" width="45" height="30" fill="#000000" stroke="#FF4444" stroke-width="2" stroke-dasharray="4 3"/>
+  <text x="247" y="218" fill="#FF4444" font-size="9" font-family="monospace" text-anchor="middle">hole</text>
+
+  <!-- right: the assertion code and verdict -->
+  <rect x="410" y="60" width="195" height="150" rx="8" fill="rgba(64,176,208,0.08)" stroke="#40b0d0" stroke-width="1.5"/>
+  <text x="508" y="80" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle" filter="url(#glow)">PixelAssertions</text>
+  <text x="420" y="102" fill="#ccc" font-size="9" font-family="monospace">unpaintedFraction(img,</text>
+  <text x="420" y="116" fill="#ccc" font-size="9" font-family="monospace">  bg=0x000000,</text>
+  <text x="420" y="130" fill="#ccc" font-size="9" font-family="monospace">  0.15, 0.45,</text>
+  <text x="420" y="144" fill="#ccc" font-size="9" font-family="monospace">  0.85, 1.0)</text>
+  <text x="420" y="168" fill="#999" font-size="9" font-family="monospace">counts pixels that still</text>
+  <text x="420" y="181" fill="#999" font-size="9" font-family="monospace">equal the background</text>
+  <text x="420" y="200" fill="#FF4444" font-size="10" font-family="monospace">&#8594; 0.017 &gt; 0.01 FAIL</text>
+
+  <line x1="380" y1="150" x2="408" y2="140" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr)"/>
+
+  <!-- bottom notes -->
+  <text x="60" y="282" fill="#999" font-size="9" font-family="monospace">sentinel background: "nothing rendered here" is unambiguous, even in dark scenes</text>
+</svg>
diff --git a/Documentation/Agentic development/diff-example.png b/Documentation/Agentic development/diff-example.png
new file mode 100644 (file)
index 0000000..941d5ca
Binary files /dev/null and b/Documentation/Agentic development/diff-example.png differ
diff --git a/Documentation/Agentic development/snapshot-example.png b/Documentation/Agentic development/snapshot-example.png
new file mode 100644 (file)
index 0000000..92ea03c
Binary files /dev/null and b/Documentation/Agentic development/snapshot-example.png differ
diff --git a/Documentation/CSG/BSP tree.svg b/Documentation/CSG/BSP tree.svg
new file mode 100644 (file)
index 0000000..eb89c2c
--- /dev/null
@@ -0,0 +1,45 @@
+<svg viewBox="0 0 620 220" width="620" height="220" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="bsp-glow"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+  </defs>
+  <rect width="620" height="220" fill="#061018"/>
+
+  <!-- ── Root node ── -->
+  <rect x="250" y="18" width="120" height="32" rx="4" fill="rgba(64,176,208,0.15)" stroke="#40b0d0" stroke-width="1.5"/>
+  <text x="310" y="39" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#bsp-glow)">Plane P₁</text>
+
+  <!-- ── Connectors: root → children ── -->
+  <line x1="310" y1="50" x2="310" y2="62" stroke="#40b0d0" stroke-width="1"/>
+  <line x1="310" y1="62" x2="160" y2="62" stroke="#40b0d0" stroke-width="1"/>
+  <line x1="310" y1="62" x2="460" y2="62" stroke="#40b0d0" stroke-width="1"/>
+  <line x1="160" y1="62" x2="160" y2="72" stroke="#40b0d0" stroke-width="1"/>
+  <line x1="460" y1="62" x2="460" y2="72" stroke="#40b0d0" stroke-width="1"/>
+  <!-- Branch labels -->
+  <text x="225" y="58" fill="#30a050" font-size="8" font-family="monospace" text-anchor="middle">front</text>
+  <text x="395" y="58" fill="#d04040" font-size="8" font-family="monospace" text-anchor="middle">back</text>
+
+  <!-- ── Front child ── -->
+  <rect x="100" y="72" width="120" height="32" rx="4" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+  <text x="160" y="93" fill="#30a050" font-size="11" font-family="monospace" text-anchor="middle">Front (P₂)</text>
+
+  <!-- ── Back child ── -->
+  <rect x="400" y="72" width="120" height="32" rx="4" fill="rgba(208,64,64,0.15)" stroke="#d04040" stroke-width="1.5"/>
+  <text x="460" y="93" fill="#d04040" font-size="11" font-family="monospace" text-anchor="middle">Back (P₃)</text>
+
+  <!-- ── Connectors: front child → grandchildren ── -->
+  <line x1="160" y1="104" x2="160" y2="116" stroke="#30a050" stroke-width="1"/>
+  <line x1="160" y1="116" x2="85" y2="116" stroke="#30a050" stroke-width="1"/>
+  <line x1="160" y1="116" x2="235" y2="116" stroke="#30a050" stroke-width="1"/>
+  <line x1="85" y1="116" x2="85" y2="126" stroke="#30a050" stroke-width="1"/>
+  <line x1="235" y1="116" x2="235" y2="126" stroke="#30a050" stroke-width="1"/>
+
+  <!-- ── Leaf nodes ── -->
+  <rect x="45" y="126" width="80" height="28" rx="4" fill="rgba(100,100,100,0.08)" stroke="#aaa" stroke-width="1" stroke-dasharray="3 2"/>
+  <text x="85" y="145" fill="#bbb" font-size="10" font-family="monospace" text-anchor="middle">leaf</text>
+  <rect x="195" y="126" width="80" height="28" rx="4" fill="rgba(100,100,100,0.08)" stroke="#aaa" stroke-width="1" stroke-dasharray="3 2"/>
+  <text x="235" y="145" fill="#bbb" font-size="10" font-family="monospace" text-anchor="middle">leaf</text>
+
+  <!-- ── Explanation ── -->
+  <text x="310" y="185" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Each plane divides space into front (normal side) and back (opposite)</text>
+  <text x="310" y="200" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Polygons are classified and split at each partitioning plane</text>
+</svg>
diff --git a/Documentation/CSG/CSG demo.png b/Documentation/CSG/CSG demo.png
new file mode 100644 (file)
index 0000000..2275350
Binary files /dev/null and b/Documentation/CSG/CSG demo.png differ
diff --git a/Documentation/CSG/CSG intersect.svg b/Documentation/CSG/CSG intersect.svg
new file mode 100644 (file)
index 0000000..a912f81
--- /dev/null
@@ -0,0 +1,41 @@
+<svg viewBox="0 0 620 170" width="620" height="170" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="i-glow"><feGaussianBlur stdDeviation="2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+    <clipPath id="i-clip-rect"><rect x="370" y="35" width="80" height="80"/></clipPath>
+    <clipPath id="i-clip-circ"><circle cx="450" cy="75" r="42"/></clipPath>
+  </defs>
+  <rect width="620" height="170" fill="#061018"/>
+
+  <!-- ── Input ── -->
+  <text x="110" y="20" fill="#ccc" font-size="11" font-family="monospace" text-anchor="middle">Input</text>
+  <rect x="50" y="35" width="80" height="80" rx="2" fill="rgba(57,255,20,0.2)" stroke="#39FF14" stroke-width="2"/>
+  <text x="90" y="80" fill="#39FF14" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">A</text>
+  <circle cx="130" cy="75" r="42" fill="rgba(255,102,0,0.2)" stroke="#FF6600" stroke-width="2"/>
+  <text x="148" y="80" fill="#FF6600" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">B</text>
+
+  <!-- ── Operator ── -->
+  <text x="250" y="70" fill="#40b0d0" font-size="22" font-family="monospace" text-anchor="middle" filter="url(#i-glow)">∩</text>
+  <text x="250" y="92" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle">intersect</text>
+
+  <!-- ── Arrow ── -->
+  <line x1="280" y1="78" x2="330" y2="78" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="4 3"/>
+  <polygon points="330,73 340,78 330,83" fill="#40b0d0"/>
+
+  <!-- ── Result: only the overlap region ── -->
+  <text x="440" y="20" fill="#ccc" font-size="11" font-family="monospace" text-anchor="middle">Result: A ∩ B</text>
+  <!-- Ghost outlines of original shapes (faint) -->
+  <rect x="370" y="35" width="80" height="80" rx="2" fill="none" stroke="#39FF14" stroke-width="0.5" opacity="0.2" stroke-dasharray="4 3"/>
+  <circle cx="450" cy="75" r="42" fill="none" stroke="#FF6600" stroke-width="0.5" opacity="0.2" stroke-dasharray="4 3"/>
+  <!-- Intersection fill: lens shape = circle arc clipped to rect -->
+  <circle cx="450" cy="75" r="42" fill="rgba(57,255,20,0.08)" stroke="none" clip-path="url(#i-clip-rect)"/>
+  <!-- Left boundary: arc from circle (orange, from B) -->
+  <path d="M450,33 A42,42 0 0,0 450,117" fill="none" stroke="#FF6600" stroke-width="1.5" clip-path="url(#i-clip-rect)"/>
+  <!-- Right boundary: straight edge from rect (green, from A) -->
+  <line x1="450" y1="35" x2="450" y2="115" stroke="#39FF14" stroke-width="1.5" clip-path="url(#i-clip-circ)"/>
+
+  <!-- ── Descriptions ── -->
+  <text x="110" y="142" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Find overlap</text>
+  <text x="110" y="155" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">between both</text>
+  <text x="440" y="142" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Only shared volume</text>
+  <text x="440" y="155" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">remains</text>
+</svg>
diff --git a/Documentation/CSG/CSG operations.svg b/Documentation/CSG/CSG operations.svg
new file mode 100644 (file)
index 0000000..3f73cbe
--- /dev/null
@@ -0,0 +1,37 @@
+<svg viewBox="0 0 620 170" width="620" height="170" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="s-glow"><feGaussianBlur stdDeviation="2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+    <clipPath id="s-cube"><rect x="370" y="35" width="80" height="80"/></clipPath>
+  </defs>
+  <rect width="620" height="170" fill="#061018"/>
+
+  <!-- ── Input ── -->
+  <text x="110" y="20" fill="#aaa" font-size="11" font-family="monospace" text-anchor="middle">Input</text>
+  <rect x="50" y="35" width="80" height="80" rx="2" fill="rgba(57,255,20,0.2)" stroke="#39FF14" stroke-width="2"/>
+  <text x="90" y="80" fill="#39FF14" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">A</text>
+  <circle cx="130" cy="75" r="42" fill="rgba(255,102,0,0.12)" stroke="#FF6600" stroke-width="2" stroke-dasharray="5 3"/>
+  <text x="148" y="80" fill="#FF6600" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">B</text>
+
+  <!-- ── Operator ── -->
+  <text x="250" y="70" fill="#40b0d0" font-size="22" font-family="monospace" text-anchor="middle" filter="url(#s-glow)">−</text>
+  <text x="250" y="92" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle">subtract</text>
+
+  <!-- ── Arrow ── -->
+  <line x1="280" y1="78" x2="330" y2="78" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="4 3"/>
+  <polygon points="330,73 340,78 330,83" fill="#40b0d0"/>
+
+  <!-- ── Result ── -->
+  <text x="440" y="20" fill="#aaa" font-size="11" font-family="monospace" text-anchor="middle">Result: A − B</text>
+  <!-- Cube body -->
+  <rect x="370" y="35" width="80" height="80" rx="2" fill="rgba(57,255,20,0.15)" stroke="#39FF14" stroke-width="1.5"/>
+  <!-- Cavity: dark hole with single solid arc edge -->
+  <circle cx="450" cy="75" r="42" fill="rgba(6,16,24,0.8)" stroke="none" clip-path="url(#s-cube)"/>
+  <path d="M450,33 A42,42 0 0,0 450,117" fill="none" stroke="#FF6600" stroke-width="1.5" clip-path="url(#s-cube)"/>
+  <text x="425" y="78" fill="#40b0d0" font-size="7" font-family="monospace" text-anchor="middle" opacity="0.7">cavity</text>
+
+  <!-- ── Descriptions ── -->
+  <text x="110" y="142" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">B is the "cutter"</text>
+  <text x="110" y="155" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">carves out of A</text>
+  <text x="440" y="142" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">Cube with cavity</text>
+  <text x="440" y="155" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">interior faces visible</text>
+</svg>
diff --git a/Documentation/CSG/CSG union.svg b/Documentation/CSG/CSG union.svg
new file mode 100644 (file)
index 0000000..f1eedec
--- /dev/null
@@ -0,0 +1,38 @@
+<svg viewBox="0 0 620 170" width="620" height="170" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="u-glow"><feGaussianBlur stdDeviation="2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+  </defs>
+  <rect width="620" height="170" fill="#061018"/>
+
+  <!-- ── Input ── -->
+  <text x="110" y="20" fill="#ccc" font-size="11" font-family="monospace" text-anchor="middle">Input</text>
+  <rect x="50" y="35" width="80" height="80" rx="2" fill="rgba(57,255,20,0.2)" stroke="#39FF14" stroke-width="2"/>
+  <text x="90" y="80" fill="#39FF14" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">A</text>
+  <circle cx="130" cy="75" r="42" fill="rgba(255,102,0,0.2)" stroke="#FF6600" stroke-width="2"/>
+  <text x="148" y="80" fill="#FF6600" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">B</text>
+
+  <!-- ── Operator ── -->
+  <text x="250" y="70" fill="#40b0d0" font-size="22" font-family="monospace" text-anchor="middle" filter="url(#u-glow)">+</text>
+  <text x="250" y="92" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle">union</text>
+
+  <!-- ── Arrow ── -->
+  <line x1="280" y1="78" x2="330" y2="78" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="4 3"/>
+  <polygon points="330,73 340,78 330,83" fill="#40b0d0"/>
+
+  <!-- ── Result ── -->
+  <text x="440" y="20" fill="#ccc" font-size="11" font-family="monospace" text-anchor="middle">Result: A + B</text>
+  <!-- Merged outer boundary: cube left + top + circle right + cube bottom -->
+  <path d="M370,35 L370,115 L450,115 L450,103 A42,42 0 0,0 450,47 L450,35 Z"
+        fill="rgba(57,255,20,0.12)" stroke="#39FF14" stroke-width="1.5"/>
+  <path d="M450,47 A42,42 0 0,1 450,103"
+        fill="rgba(255,102,0,0.12)" stroke="#FF6600" stroke-width="1.5"/>
+  <!-- Interior seam removed indicator -->
+  <line x1="450" y1="47" x2="450" y2="103" stroke="#40b0d0" stroke-width="1" stroke-dasharray="3 2" opacity="0.5"/>
+  <text x="458" y="78" fill="#40b0d0" font-size="7" font-family="monospace" opacity="0.7">removed</text>
+
+  <!-- ── Descriptions ── -->
+  <text x="110" y="142" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Keeps all geometry</text>
+  <text x="110" y="155" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">from both shapes</text>
+  <text x="440" y="142" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Single combined volume</text>
+  <text x="440" y="155" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">interior faces removed</text>
+</svg>
diff --git a/Documentation/CSG/Polygon clipping.svg b/Documentation/CSG/Polygon clipping.svg
new file mode 100644 (file)
index 0000000..41de628
--- /dev/null
@@ -0,0 +1,39 @@
+<svg viewBox="0 0 620 190" width="620" height="190" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="c-glow"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+  </defs>
+  <rect width="620" height="190" fill="#061018"/>
+
+  <!-- ── Original polygon crossing a plane ── -->
+  <text x="120" y="20" fill="#ccc" font-size="11" font-family="monospace" text-anchor="middle">Polygon crosses plane</text>
+  <polygon points="60,50 180,50 120,140" fill="rgba(32,112,192,0.15)" stroke="#2070c0" stroke-width="1.5"/>
+  <line x1="30" y1="70" x2="210" y2="110" stroke="#40b0d0" stroke-width="2" filter="url(#c-glow)"/>
+  <text x="38" y="64" fill="#40b0d0" font-size="9" font-family="monospace">plane</text>
+  <circle cx="81" cy="81" r="3.5" fill="#40b0d0"/>
+  <circle cx="149" cy="96" r="3.5" fill="#40b0d0"/>
+
+  <!-- ── Arrow ── -->
+  <text x="268" y="85" fill="#40b0d0" font-size="18" font-family="monospace" text-anchor="middle" filter="url(#c-glow)">→</text>
+  <text x="268" y="105" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle">split</text>
+
+  <!-- ── Split result ── -->
+  <text x="460" y="20" fill="#ccc" font-size="11" font-family="monospace" text-anchor="middle">Split into fragments</text>
+  <line x1="370" y1="70" x2="550" y2="110" stroke="#40b0d0" stroke-width="0.7" opacity="0.25" stroke-dasharray="4 3"/>
+
+  <!-- Front fragment (above plane) — trapezoid -->
+  <polygon points="400,50 520,50 489,96 421,81" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+  <text x="458" y="68" fill="#30a050" font-size="9" font-family="monospace" text-anchor="middle">front</text>
+
+  <!-- Back fragment (below plane) — triangle -->
+  <polygon points="421,81 489,96 460,140" fill="rgba(208,64,64,0.15)" stroke="#d04040" stroke-width="1.5"/>
+  <text x="457" y="115" fill="#d04040" font-size="9" font-family="monospace" text-anchor="middle">back</text>
+
+  <!-- New edge at split -->
+  <line x1="421" y1="81" x2="489" y2="96" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="4 2"/>
+  <circle cx="421" cy="81" r="3.5" fill="#40b0d0"/>
+  <circle cx="489" cy="96" r="3.5" fill="#40b0d0"/>
+  <text x="470" y="78" fill="#40b0d0" font-size="7" font-family="monospace" text-anchor="middle" opacity="0.8">new edge</text>
+
+  <!-- ── Explanation ── -->
+  <text x="310" y="172" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Spanning polygons are split; each fragment goes to its respective subtree</text>
+</svg>
diff --git a/Documentation/CSG/index.org b/Documentation/CSG/index.org
new file mode 100644 (file)
index 0000000..ebdbad6
--- /dev/null
@@ -0,0 +1,284 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Constructive Solid Geometry - Aukio 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
+
+[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
+
+* What is CSG?
+:PROPERTIES:
+:CUSTOM_ID: what-is-csg
+:ID:       a1b2c3d4-e5f6-7890-abcd-ef1234567890
+:END:
+
+*Constructive Solid Geometry* (CSG) is a modeling technique that builds
+complex 3D shapes by combining simpler primitives using boolean
+operations. Instead of manually creating every vertex and face, you
+define shapes as the result of operations like "merge these two cubes"
+or "carve a hole using this sphere."
+
+CSG is particularly powerful for:
+- *Procedural modeling* — generate complex geometry algorithmically
+- *CAD/CAM applications* — define parts as combinations of primitives
+- *Game development* — create architectural elements, holes, cavities
+- *Rapid prototyping* — iterate on designs by adjusting operations
+
+The three fundamental CSG operations are:
+
+| Operation   | Symbol | Result                                    |
+|-------------+--------+-------------------------------------------|
+| Subtract    | A - B  | A with B carved out (holes, cavities)     |
+| Union       | A + B  | Combined volume (both shapes merged)      |
+| Intersect   | A ∩ B  | Volume where both overlap                 |
+
+See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#csg-demo][CSG demo]] for an interactive visualization.
+
+* The Three Operations
+:PROPERTIES:
+:CUSTOM_ID: the-three-operations
+:END:
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 1000px
+[[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#csg-demo][file:CSG%20demo.png]]
+
+The screenshot above shows all three operations displayed left to right:
+subtract (green cube with spherical cavity), union (merged green and
+orange shapes), and intersect (only the overlapping region in blue).
+
+The diagrams below use the same green cube (A) and orange sphere (B) as
+the screenshot above. Each operation transforms these inputs differently,
+producing the results shown from left to right in the image.
+
+** Subtract (A - B)
+:PROPERTIES:
+:CUSTOM_ID: subtract-operation
+:END:
+
+#+INCLUDE: "CSG operations.svg" export html
+
+*Subtract* removes the orange sphere (B) from the green cube (A), carving
+out a cavity. The diagram shows B acting as a "cutter" — where it overlaps
+A, a hole is created. Interior faces *are preserved* and become visible,
+allowing you to see inside the carved-out space (shown as the orange dashed
+curve in the result).
+
+This matches the leftmost shape in the screenshot: a green cube with a
+visible spherical hollow inside, showing the interior surfaces created by
+the subtraction.
+
+This operation is ideal for creating:
+- Holes and tunnels
+- Carved-out spaces
+- Hollow objects
+
+#+BEGIN_SRC java
+SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN);
+SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE);
+
+cube.subtract(sphere);  // cube now has a spherical cavity
+#+END_SRC
+
+** Union (A + B)
+:PROPERTIES:
+:CUSTOM_ID: union-operation
+:END:
+
+#+INCLUDE: "CSG union.svg" export html
+
+*Union* merges the green cube (A) and orange sphere (B) into one continuous
+volume. The diagram shows both shapes combining — the interior seam (where
+they overlap) is removed, creating a single solid surface with no internal
+boundaries (indicated by the dashed blue line labeled "removed").
+
+This corresponds to the center shape in the screenshot: both green and
+orange colors present but seamlessly joined, forming one unified object.
+
+#+BEGIN_SRC java
+SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN);
+SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE);
+
+cube.union(sphere);  // cube now contains the merged result
+#+END_SRC
+
+** Intersect (A ∩ B)
+:PROPERTIES:
+:CUSTOM_ID: intersect-operation
+:END:
+
+#+INCLUDE: "CSG intersect.svg" export html
+
+*Intersect* keeps only the volume where the green cube (A) and orange
+sphere (B) overlap — the region that is inside *both* shapes
+simultaneously. The diagram shows this as the blue-shaded area: the
+portion of the sphere that fits within the cube boundaries. Everything
+else is discarded.
+
+This is the rightmost shape in the screenshot: only the overlapping
+portion remains, showing which parts of space were occupied by both the
+cube and sphere at the same time.
+
+This operation is useful for:
+- Creating shapes constrained by multiple boundaries
+- Finding collision regions
+- Trimming geometry to fit within bounds
+
+#+BEGIN_SRC java
+SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN);
+SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE);
+
+cube.intersect(sphere);  // only the overlapping region remains
+#+END_SRC
+
+* BSP Tree Algorithm
+:PROPERTIES:
+:CUSTOM_ID: bsp-tree-algorithm
+:END:
+
+CSG boolean operations are implemented using *Binary Space Partitioning*
+(BSP) trees. A BSP tree recursively divides 3D space using planes,
+creating a hierarchical structure that enables efficient polygon clipping
+and spatial queries.
+
+** BSP Tree Structure
+:PROPERTIES:
+:CUSTOM_ID: bsp-tree-structure
+:END:
+
+#+INCLUDE: "BSP tree.svg" export html
+
+Each BSP node contains:
+- A *partitioning plane* that divides space into two half-spaces
+- *Polygons* that lie exactly on this plane (coplanar)
+- *Front* subtree — polygons on the same side as the plane's normal
+- *Back* subtree — polygons on the opposite side
+
+** Key BSP Operations
+:PROPERTIES:
+:CUSTOM_ID: key-bsp-operations
+:END:
+
+The BSP tree provides three core operations that enable CSG:
+
+| Operation      | Description                                      |
+|----------------+--------------------------------------------------|
+| =invert()=     | Flip all normals, swap front/back children       |
+| =clipTo(tree)= | Remove polygons inside the other tree's solid    |
+| =addPolygons()= | Insert new polygons, splitting at planes         |
+
+*Invert* is fundamental to CSG. By flipping inside/outside, we can
+transform subtraction and intersection into variations of clipping:
+
+- **Subtract** = invert A, clip against B, add B's clipped parts, invert back
+- **Intersect** = invert A, clip B against A, invert B, clip A against B, combine, invert A back
+
+** Polygon Clipping
+:PROPERTIES:
+:CUSTOM_ID: polygon-clipping
+:END:
+
+When a polygon crosses a partitioning plane, it's *split* into two
+fragments:
+
+#+INCLUDE: "Polygon clipping.svg" export html
+
+This recursive splitting ensures that all polygons are cleanly classified
+as entirely in front, entirely behind, or exactly on a plane — never
+"spanning" across.
+
+* Using CSG in Aukio 3D
+:PROPERTIES:
+:CUSTOM_ID: using-csg-in-aukio-3d
+:END:
+
+CSG operations are methods on [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]. They modify the
+shape *in-place* — the result replaces the original geometry.
+
+** Basic Usage
+:PROPERTIES:
+:CUSTOM_ID: basic-usage
+:END:
+
+#+BEGIN_SRC java
+import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.*;
+
+// Create two shapes
+SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 100, Color.GREEN);
+SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 70, 12, Color.ORANGE);
+
+// Perform CSG operations (in-place modification)
+cube.subtract(sphere);   // Cube with spherical cavity
+// or
+cube.union(sphere);      // Merged shape
+// or
+cube.intersect(sphere);  // Only overlapping region
+
+// Add to scene
+shapes.addShape(cube.setBackfaceCulling(true));
+#+END_SRC
+
+** Child Handling Behavior
+:PROPERTIES:
+:CUSTOM_ID: child-handling
+:END:
+
+CSG operations only affect *SolidPolygon* geometry. Other children are
+preserved as objects:
+
+| Child Type            | Union            | Subtract         | Intersect        |
+|-----------------------+------------------+------------------+------------------|
+| SolidPolygon (this)   | Replaced with result | Replaced with result | Replaced with result |
+| SolidPolygon (other)  | Merged into result | Discarded (cutter) | Discarded        |
+| Line, TextCanvas (this) | Preserved      | Preserved        | Preserved        |
+| Line, TextCanvas (other) | Merged into this shape | Discarded | Discarded     |
+| Nested composite      | Preserved as object — but see below | same | same |
+
+*Nested composites are not CSG-safe.* Polygon extraction recurses into
+them, so their SolidPolygons are included in the BSP result — while the
+nested composite object itself is also preserved, duplicating that
+geometry in the render. Apply CSG to flat composites, or extract the
+nested polygons first.
+
+This allows you to attach labels, decorations, or wireframe overlays to
+shapes without them being affected by CSG operations (for union, the
+other shape's decorations are copied over too).
+
+** Important Notes
+:PROPERTIES:
+:CUSTOM_ID: important-notes
+:END:
+
+1. *Shapes are modified in-place*. The original geometry is replaced.
+   Clone shapes beforehand if you need to preserve the originals.
+
+2. *CSG works on SolidPolygon children only*. TexturedTriangle and other
+   shape types are not processed.
+
+3. *Result quality depends on mesh density*. Low-polygon inputs may
+   produce visible artifacts at intersection boundaries. Use higher
+   subdivision counts for smoother results.
+
+4. *Backface culling is recommended*. CSG results often have internal
+   faces from the cutting operation. Enable culling to hide backfaces:
+   =shape.setBackfaceCulling(true)=
+
+* Related Classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class                    | Purpose                                              |
+|--------------------------+------------------------------------------------------|
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/BspTree.html][BspTree]]                  | BSP tree for spatial partitioning and CSG operations |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Plane]]                    | Partitioning plane used by BSP nodes                 |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]]             | Polygon shape processed by CSG                       |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]   | Base class with union/subtract/intersect methods     |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.html][SolidPolygonMesh]]         | Custom polygon mesh for arbitrary geometry           |
+| SolidPolygon* primitives | See [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery]] for all available shapes           |
diff --git a/Documentation/Coordinate system.svg b/Documentation/Coordinate system.svg
new file mode 100644 (file)
index 0000000..4497bf3
--- /dev/null
@@ -0,0 +1,18 @@
+<svg viewBox="0 0 640 520" width="640" height="520" xmlns="http://www.w3.org/2000/svg">
+  <rect width="640" height="520" fill="#061018"/>
+  <circle cx="280" cy="260" r="10" fill="rgba(80,96,192,0.1)" stroke="rgba(80,96,192,0.3)" stroke-width="2"/>
+  <line x1="280" y1="260" x2="560" y2="260" stroke="#d04040" stroke-width="5"/>
+  <polygon points="560,260 540,250 540,270" fill="#d04040"/>
+  <text x="568" y="268" fill="#d04040" font-size="28" font-weight="700" font-family="monospace">X</text>
+  <text x="400" y="304" fill="#bbb" font-size="18" font-family="monospace">right (+) / left (-)</text>
+  <line x1="280" y1="260" x2="280" y2="480" stroke="#30a050" stroke-width="5"/>
+  <polygon points="280,480 270,460 290,460" fill="#30a050"/>
+  <text x="292" y="504" fill="#30a050" font-size="28" font-weight="700" font-family="monospace">Y</text>
+  <text x="292" y="456" fill="#bbb" font-size="18" font-family="monospace">down (+) / up (-)</text>
+  <line x1="280" y1="260" x2="120" y2="140" stroke="#2070c0" stroke-width="5"/>
+  <polygon points="120,140 140,144 132,164" fill="#2070c0"/>
+  <text x="84" y="124" fill="#2070c0" font-size="28" font-weight="700" font-family="monospace">Z</text>
+  <text x="120" y="112" fill="#bbb" font-size="18" font-family="monospace">away (+) / towards (-)</text>
+  <text x="300" y="204" fill="#aaa" font-size="22" font-weight="600" font-family="monospace">Origin</text>
+  <text x="294" y="230" fill="#bbb" font-size="18" font-family="monospace">(0, 0, 0)</text>
+</svg>
\ No newline at end of file
diff --git a/Documentation/Depth buffer/index.org b/Documentation/Depth buffer/index.org
new file mode 100644 (file)
index 0000000..2f2c75d
--- /dev/null
@@ -0,0 +1,130 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Depth Buffer - Aukio 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \setlength{\parindent}{15pt}
+#+LATEX_HEADER: \usepackage{palatino}
+#+LATEX_HEADER: \usepackage{charter}
+#+HTML_HEAD: <style type="text/css">body { max-width: 100%;}</style>
+
+[[file:../index.html#outline-container-depth-buffer][<- Back to index]]
+
+* Per-pixel visibility
+:PROPERTIES:
+:CUSTOM_ID: per-pixel
+:END:
+
+The engine resolves visibility with a depth buffer, not paint order.
+Every rasterized triangle carries a per-pixel depth quantity =zw =
+1/z= (camera-space), interpolated linearly across each span — =1/z= is
+affine in screen space, so it rides the same edge interpolators as the
+texture gradients. A fragment wins a pixel only where
+
+#+BEGIN_EXAMPLE
+zw > stored - margin * zw^2        (margin = 0 by default)
+#+END_EXAMPLE
+
+Default margin 0 is *strict depth*: the nearer fragment always wins,
+regardless of paint order, so overlapping depth ranges — a floor tile
+extending under furniture, a wall seen through a doorway — come out
+correct per pixel. A nonzero margin (= RenderingContext.DEPTH_MARGIN_DZ =,
+=-Daukio.zbuffer.margin=, world units) re-opens a tolerance window
+*behind* the stored depth for near-coplanar pairs; any nonzero window
+re-imports per-triangle sort errors into per-pixel occlusion, which is
+why the default is strict.
+
+The depth buffer is allocated once per frame context
+(=RenderingContext.depth=, float per pixel) and cleared per tile
+together with the pixel buffer.
+
+* Two passes
+:PROPERTIES:
+:CUSTOM_ID: two-passes
+:END:
+
+=RenderAggregator.paintSorted= paints the sorted queue in two passes:
+
+1. *Opaque pass* — opaque-class triangles, iterated front-to-back (the
+   queue is back-to-front, so reversed), depth test + depth write.
+   Front-to-back order is a pure performance hint: hidden fragments
+   die on the depth test *before* the texture fetch (early-z).
+2. *Alpha pass* — alpha-class triangles (translucent solid polygons,
+   alpha-carrying textures, SDF text), iterated back-to-front in queue
+   order, depth test but *no depth write*. Translucency never
+   occludes, and overlapping translucent surfaces keep painter-coherent
+   mutual order.
+
+A shape's class comes from its paint color or texture: solid polygons
+with =alpha = 255= are opaque, anything translucent is alpha-class;
+textured triangles are alpha-class when the texture has alpha or is an
+SDF mask.
+
+* Which shapes carry depth
+:PROPERTIES:
+:CUSTOM_ID: shapes
+:END:
+
+- =TexturedTriangle= — opaque or alpha class by texture.
+- =SolidPolygon= — depth-tested since 2026-09-17; opaque when its
+  (possibly shaded) color is fully opaque, translucent otherwise.
+  Near-plane-clipped quads fan-triangulate with depth like any other
+  triangles.
+- =LightmappedTriangle= — a textured triangle, so the same rules.
+- =Line=, =Billboard=, =GlowingPoint= — no depth by design: they are
+  2D overlays (wireframes, markers, sprites) and always paint on top,
+  in the alpha pass.
+
+Because every occluder writes depth, scene code no longer needs any
+ordering structure: composites just fan-triangulate their polygons.
+(=LightmappedCompositeShape= exists only to wrap polygons as lightmap
+carriers for the GI system, not to order them.)
+
+* Hi-Z occlusion pyramid
+:PROPERTIES:
+:CUSTOM_ID: hi-z
+:END:
+
+After each successful paint, =ViewPanel= builds a Hi-Z pyramid from
+the depth buffer (gui/HiZPyramid): 8-pixel tiles pooled upward,
+each tile storing the *minimum* =zw= (farthest written depth —
+max-pooling would store the nearest occluder and wrongly cull geometry
+visible between near gaps). Next frame, =TriangleMeshBlock= projects
+its world AABB's 8 corners and, when the nearest corner is still
+behind the pyramid's stored depth, skips the whole block before any
+per-triangle work.
+
+The test is conservative by construction (min-pooling plus sky pixels
+at =-inf=), so it never culls visible geometry; wrong culls under
+camera motion self-heal in one frame. Knobs: =-Daukio.hiz.margin=0.02=,
+kill switch =-Daukio.hiz=false=. Headless snapshots never build the
+pyramid, so golden renders are structurally unaffected. Stereo skips
+the test (the pyramid is mono).
+
+* Determinism and depth dumps
+:PROPERTIES:
+:CUSTOM_ID: determinism
+:END:
+
+The renderer is bit-deterministic: same scene and camera give
+bit-identical pixels across runs, which is what the golden-image
+regression tests compare. =Snapshot= (the headless toolkit) supports
+=-Daukio.zbuffer.dumpDepth=path.png= to write a grayscale depth map
+alongside the color image — useful when hunting depth-window bugs
+(bisect those with =-Daukio.zbuffer.margin=0=).
+
+* Related classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class                | Role                                                     |
+|----------------------+----------------------------------------------------------|
+| =RenderingContext=   | =depth= buffer, =depthPass=, =DEPTH_MARGIN_DZ= constant  |
+| =RenderAggregator=   | =paintSorted= two-pass driver, queue sort                |
+| =TexturedTriangle=   | Z span writers (perspective and affine)                  |
+| =SolidPolygon=       | flat-color Z span writer, two-pass classification        |
+| =HiZPyramid=         | temporal whole-block occlusion culling                   |
+| =ViewPanel=          | per-tile depth clear, pyramid rebuild after paint        |
+
+[[file:../index.html#outline-container-depth-buffer][Back to main documentation]]
diff --git a/Documentation/Developer tools/Developer tools.png b/Documentation/Developer tools/Developer tools.png
new file mode 100644 (file)
index 0000000..825b0de
Binary files /dev/null and b/Documentation/Developer tools/Developer tools.png differ
diff --git a/Documentation/Developer tools/Render alternative segments.png b/Documentation/Developer tools/Render alternative segments.png
new file mode 100644 (file)
index 0000000..e2bd569
Binary files /dev/null and b/Documentation/Developer tools/Render alternative segments.png differ
diff --git a/Documentation/Developer tools/Render polygon borders.png b/Documentation/Developer tools/Render polygon borders.png
new file mode 100644 (file)
index 0000000..5ec2182
Binary files /dev/null and b/Documentation/Developer tools/Render polygon borders.png differ
diff --git a/Documentation/Developer tools/Show segment boundaries.png b/Documentation/Developer tools/Show segment boundaries.png
new file mode 100644 (file)
index 0000000..01a1978
Binary files /dev/null and b/Documentation/Developer tools/Show segment boundaries.png differ
diff --git a/Documentation/Developer tools/Thread timeline.png b/Documentation/Developer tools/Thread timeline.png
new file mode 100644 (file)
index 0000000..dd1d378
Binary files /dev/null and b/Documentation/Developer tools/Thread timeline.png differ
diff --git a/Documentation/Edge.svg b/Documentation/Edge.svg
new file mode 100644 (file)
index 0000000..e9af1cf
--- /dev/null
@@ -0,0 +1,12 @@
+<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
+  <rect width="640" height="480" fill="#061018"/>
+  <polygon points="320,100 160,380 480,380" fill="rgba(100,100,200,0.04)" stroke="rgba(100,100,200,0.2)" stroke-width="2"/>
+  <line x1="320" y1="100" x2="480" y2="380" stroke="#5060c0" stroke-width="6" stroke-linecap="round"/>
+  <circle cx="320" cy="100" r="10" fill="#5060c0"/>
+  <circle cx="160" cy="380" r="8" fill="rgba(80,96,192,0.5)"/>
+  <circle cx="480" cy="380" r="10" fill="#5060c0"/>
+  <text x="300" y="80" fill="#aaa" font-size="20" font-family="monospace">V₁</text>
+  <text x="492" y="388" fill="#aaa" font-size="20" font-family="monospace">V₂</text>
+  <text x="120" y="400" fill="#bbb" font-size="20" font-family="monospace">V₃</text>
+  <text x="420" y="220" fill="#5060c0" font-size="24" font-weight="700" font-family="monospace" transform="rotate(30 420 220)">edge</text>
+</svg>
diff --git a/Documentation/Example.png b/Documentation/Example.png
new file mode 100644 (file)
index 0000000..7094240
Binary files /dev/null and b/Documentation/Example.png differ
diff --git a/Documentation/Face triangle.svg b/Documentation/Face triangle.svg
new file mode 100644 (file)
index 0000000..509c841
--- /dev/null
@@ -0,0 +1,14 @@
+<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
+  <rect width="640" height="480" fill="#061018"/>
+  <polygon points="320,80 120,400 520,400" fill="rgba(200,80,140,0.15)" stroke="#c05088" stroke-width="3"/>
+  <line x1="200" y1="280" x2="440" y2="280" stroke="rgba(200,80,140,0.1)" stroke-width="1"/>
+  <line x1="240" y1="320" x2="400" y2="320" stroke="rgba(200,80,140,0.08)" stroke-width="1"/>
+  <line x1="164" y1="360" x2="476" y2="360" stroke="rgba(200,80,140,0.06)" stroke-width="1"/>
+  <circle cx="320" cy="80" r="8" fill="#c05088"/>
+  <circle cx="120" cy="400" r="8" fill="#c05088"/>
+  <circle cx="520" cy="400" r="8" fill="#c05088"/>
+  <text x="296" y="60" fill="#c05088" font-size="20" font-weight="700" font-family="monospace">V₁</text>
+  <text x="76" y="420" fill="#c05088" font-size="20" font-weight="700" font-family="monospace">V₂</text>
+  <text x="532" y="420" fill="#c05088" font-size="20" font-weight="700" font-family="monospace">V₃</text>
+  <text x="264" y="300" fill="rgba(192,80,136,0.5)" font-size="28" font-weight="700" font-family="monospace">FACE</text>
+</svg>
diff --git a/Documentation/Frustum culling/Frustum diagram.svg b/Documentation/Frustum culling/Frustum diagram.svg
new file mode 100644 (file)
index 0000000..b59d4a8
--- /dev/null
@@ -0,0 +1,58 @@
+<svg viewBox="0 0 620 300" width="620" height="300" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="f-glow"><feGaussianBlur stdDeviation="2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+  </defs>
+  <rect width="620" height="300" fill="#061018"/>
+
+  <!-- Z axis -->
+  <line x1="60" y1="150" x2="590" y2="150" stroke="rgba(32,112,192,0.2)" stroke-width="1" stroke-dasharray="6 3"/>
+  <text x="570" y="143" fill="#2070c0" font-size="11" font-weight="700" font-family="monospace">+Z</text>
+  <text x="530" y="163" fill="#999" font-size="8" font-family="monospace">(view direction)</text>
+
+  <!-- Camera -->
+  <circle cx="60" cy="150" r="7" fill="rgba(32,112,192,0.3)" stroke="#2070c0" stroke-width="2" filter="url(#f-glow)"/>
+  <text x="74" y="154" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace">Camera</text>
+
+  <!-- Frustum edges: camera → near corners -->
+  <line x1="60" y1="150" x2="170" y2="105" stroke="rgba(48,160,80,0.3)" stroke-width="1"/>
+  <line x1="60" y1="150" x2="170" y2="195" stroke="rgba(48,160,80,0.3)" stroke-width="1"/>
+  <!-- Frustum edges: near → far corners -->
+  <line x1="170" y1="105" x2="440" y2="40" stroke="rgba(48,160,80,0.25)" stroke-width="1"/>
+  <line x1="170" y1="195" x2="440" y2="260" stroke="rgba(48,160,80,0.25)" stroke-width="1"/>
+  <!-- Extended rays behind far (faint dashed) -->
+  <line x1="440" y1="40" x2="520" y2="10" stroke="rgba(48,160,80,0.1)" stroke-width="1" stroke-dasharray="3 3"/>
+  <line x1="440" y1="260" x2="520" y2="290" stroke="rgba(48,160,80,0.1)" stroke-width="1" stroke-dasharray="3 3"/>
+
+  <!-- Near plane -->
+  <rect x="168" y="105" width="4" height="90" rx="1" fill="rgba(48,160,80,0.25)" stroke="#30a050" stroke-width="1.5"/>
+  <text x="155" y="96" fill="#30a050" font-size="10" font-family="monospace" font-weight="700">Near</text>
+
+  <!-- Far plane -->
+  <rect x="438" y="40" width="4" height="220" rx="1" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+  <text x="425" y="32" fill="#30a050" font-size="10" font-family="monospace" font-weight="700">Far</text>
+
+  <!-- Frustum fill (the visible volume) -->
+  <polygon points="170,105 170,195 440,260 440,40" fill="rgba(48,160,80,0.06)" stroke="none"/>
+
+  <!-- Visible region label -->
+  <text x="290" y="145" fill="#30a050" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle" opacity="0.6">visible region</text>
+
+  <!-- Plane labels along frustum edges -->
+  <text x="295" y="62" fill="#999" font-size="8" font-family="monospace" text-anchor="middle">Top plane</text>
+  <text x="295" y="242" fill="#999" font-size="8" font-family="monospace" text-anchor="middle">Bottom plane</text>
+
+  <!-- Object inside frustum (rendered) -->
+  <rect x="270" y="130" width="28" height="28" rx="2" fill="rgba(48,160,80,0.2)" stroke="#30a050" stroke-width="1.5"/>
+  <text x="276" y="150" fill="#30a050" font-size="14" font-family="monospace">✓</text>
+  <text x="258" y="172" fill="#30a050" font-size="8" font-family="monospace">rendered</text>
+
+  <!-- Object outside frustum (culled — above) -->
+  <rect x="480" y="18" width="24" height="24" rx="2" fill="rgba(208,64,64,0.12)" stroke="rgba(208,64,64,0.5)" stroke-width="1.5" stroke-dasharray="3 2"/>
+  <text x="485" y="36" fill="rgba(208,64,64,0.8)" font-size="13" font-family="monospace">✗</text>
+  <text x="470" y="52" fill="rgba(208,64,64,0.6)" font-size="8" font-family="monospace">culled</text>
+
+  <!-- Object outside frustum (culled — below) -->
+  <rect x="310" y="266" width="24" height="24" rx="2" fill="rgba(208,64,64,0.12)" stroke="rgba(208,64,64,0.5)" stroke-width="1.5" stroke-dasharray="3 2"/>
+  <text x="315" y="284" fill="rgba(208,64,64,0.8)" font-size="13" font-family="monospace">✗</text>
+  <text x="300" y="300" fill="rgba(208,64,64,0.6)" font-size="8" font-family="monospace">culled</text>
+</svg>
diff --git a/Documentation/Frustum culling/P-vertex AABB.svg b/Documentation/Frustum culling/P-vertex AABB.svg
new file mode 100644 (file)
index 0000000..a3acfb8
--- /dev/null
@@ -0,0 +1,38 @@
+<svg viewBox="0 0 520 200" width="520" height="200" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="p-glow"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+  </defs>
+  <rect width="520" height="200" fill="#061018"/>
+
+  <!-- Explanation text -->
+  <text x="30" y="22" fill="#aaa" font-size="10" font-family="monospace">P-vertex: corner most aligned with plane normal</text>
+  <text x="30" y="36" fill="#bbb" font-size="9" font-family="monospace">If P is behind the plane → entire AABB is outside</text>
+
+  <!-- Frustum plane (diagonal line) -->
+  <line x1="50" y1="170" x2="430" y2="55" stroke="#b09020" stroke-width="2" filter="url(#p-glow)"/>
+  <text x="432" y="52" fill="#b09020" font-size="10" font-weight="700" font-family="monospace">Plane</text>
+
+  <!-- "inside" region label -->
+  <text x="100" y="80" fill="rgba(48,160,80,0.4)" font-size="10" font-family="monospace">inside frustum</text>
+  <!-- "outside" region label -->
+  <text x="310" y="170" fill="rgba(208,64,64,0.4)" font-size="10" font-family="monospace">outside frustum</text>
+
+  <!-- Normal vector arrow -->
+  <line x1="270" y1="100" x2="230" y2="78" stroke="#b09020" stroke-width="1.5"/>
+  <polygon points="230,78 237,76 236,83" fill="#b09020"/>
+  <text x="222" y="72" fill="#b09020" font-size="9" font-family="monospace" font-weight="700">N</text>
+
+  <!-- AABB inside frustum (fully visible) -->
+  <rect x="80" y="100" width="60" height="50" rx="2" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+  <text x="97" y="130" fill="#30a050" font-size="9" font-family="monospace" text-anchor="middle">inside</text>
+  <!-- P-vertex for inside box (top-right corner, closest to plane) -->
+  <circle cx="140" cy="100" r="3.5" fill="#30a050"/>
+  <text x="145" y="97" fill="#30a050" font-size="7" font-family="monospace">P</text>
+
+  <!-- AABB outside frustum (fully culled) -->
+  <rect x="340" y="110" width="60" height="50" rx="2" fill="rgba(208,64,64,0.12)" stroke="rgba(208,64,64,0.5)" stroke-width="1.5" stroke-dasharray="3 2"/>
+  <text x="356" y="140" fill="rgba(208,64,64,0.7)" font-size="9" font-family="monospace" text-anchor="middle">outside</text>
+  <!-- P-vertex for outside box (top-left corner, closest to plane) -->
+  <circle cx="340" cy="110" r="3.5" fill="rgba(208,64,64,0.8)"/>
+  <text x="327" y="107" fill="rgba(208,64,64,0.8)" font-size="7" font-family="monospace">P</text>
+</svg>
diff --git a/Documentation/Frustum culling/index.org b/Documentation/Frustum culling/index.org
new file mode 100644 (file)
index 0000000..9c4a941
--- /dev/null
@@ -0,0 +1,177 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Frustum & View Frustum Culling - Aukio 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
+
+[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
+
+* Frustum & View Frustum Culling
+:PROPERTIES:
+:CUSTOM_ID: frustum-view-frustum-culling
+:END:
+
+#+INCLUDE: "Frustum diagram.svg" export html
+
+The *view frustum* is a truncated pyramid-shaped volume that represents
+everything the camera can see. Objects completely outside this volume are
+skipped during rendering — a powerful optimization called *frustum culling*.
+
+** The Six Frustum Planes
+:PROPERTIES:
+:CUSTOM_ID: frustum-planes
+:END:
+
+The frustum is defined by six clipping planes:
+
+| Plane   | Purpose                                    |
+|---------+--------------------------------------------|
+| Left    | Left edge of viewport                      |
+| Right   | Right edge of viewport                     |
+| Top     | Top edge of viewport (smaller Y in Y-down) |
+| Bottom  | Bottom edge of viewport (larger Y)         |
+| Near    | Closest visible distance from camera       |
+| Far     | Farthest visible distance from camera      |
+
+Each plane divides 3D space into "inside" (visible) and "outside"
+(culled). An object must pass all six plane tests to be considered
+potentially visible.
+
+** Frustum Culling vs Backface Culling
+:PROPERTIES:
+:CUSTOM_ID: frustum-vs-backface-culling
+:END:
+
+These are complementary optimizations at different levels:
+
+| Optimization    | Level        | What it skips                    |
+|-----------------+--------------+----------------------------------|
+| Frustum culling | Object level | Entire composite shapes + children |
+| Backface culling | Polygon level | Individual triangles facing away |
+
+*Frustum culling* happens first during the transform phase — entire
+object trees are skipped with a single bounding box test. *Backface
+culling* happens later during rasterization — individual triangles
+are checked before being drawn.
+
+For best performance, use both: organize your scene with composite
+shapes for effective frustum culling, and enable backface culling on
+closed meshes.
+
+* How Frustum Culling Works in Aukio 3D
+:PROPERTIES:
+:CUSTOM_ID: frustum-culling-implementation
+:END:
+
+Frustum culling is applied automatically to all [[../index.org#mesh][composite shapes]]
+during Phase 1 (transform) of the [[../Rendering loop/][rendering loop]]:
+
+1. *Update frustum*: Compute 6 planes from camera FOV and viewport size
+2. *For each composite shape*:
+   - Get its [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Box.html][Axis-Aligned Bounding Box (AABB)]]
+   - Transform all 8 corners to view space
+   - Test against frustum using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Frustum.html#intersectsAABB][intersectsAABB()]]
+   - If outside: skip the entire composite and all children
+   - If inside: continue transforming children
+
+(The root composite itself is never tested — it is always rendered.)
+
+** The AABB Intersection Algorithm
+:PROPERTIES:
+:CUSTOM_ID: aabb-intersection-algorithm
+:END:
+
+The intersection test uses an optimized "P-vertex" approach:
+
+#+INCLUDE: "P-vertex AABB.svg" export html
+
+For each plane, instead of testing all 8 corners of the bounding box,
+we test only the *P-vertex* — the corner most aligned with the plane
+normal. If this "best" corner is behind the plane, the entire box must
+be outside the frustum.
+
+- Plane normal points *into* the frustum (toward visible region)
+- P-vertex: select corner based on normal direction
+  - If normal.x > 0 → use maxX (rightmost corner)
+  - If normal.x < 0 → use minX (leftmost corner)
+  - Same logic for Y and Z
+- Test: =dot(normal, P-vertex) < distance= → outside
+
+This reduces from 48 tests (8 corners × 6 planes) to just 6 tests per
+object.
+
+* Performance Benefits
+:PROPERTIES:
+:CUSTOM_ID: frustum-performance
+:END:
+
+Frustum culling can dramatically improve performance for large scenes:
+
+- *High cull % (60-90%)*: Excellent — most objects skipped entirely
+- *Medium cull % (20-60%)*: Moderate benefit
+- *Low cull % (0-20%)*: Limited benefit — most objects visible
+
+A composite shape that is culled skips:
+- Transforming all its children
+- Computing bounding boxes for children
+- All polygon-level operations (backface culling, rasterization)
+
+Open Developer Tools (F12) to see real-time [[../index.org#frustum-culling-statistics][frustum culling statistics]].
+
+* Scene Design for Effective Culling
+:PROPERTIES:
+:CUSTOM_ID: frustum-scene-design
+:END:
+
+Frustum culling works best when you organize your scene into
+well-defined composite shapes:
+
+#+BEGIN_SRC java
+// Good: Each building is a separate composite
+AbstractCompositeShape cityBlock = new AbstractCompositeShape();
+for (Building building : buildings) {
+    AbstractCompositeShape buildingComposite = new AbstractCompositeShape();
+    buildingComposite.addShape(buildingWalls);
+    buildingComposite.addShape(buildingRoof);
+    buildingComposite.addShape(buildingInterior);
+    cityBlock.addShape(buildingComposite);
+}
+
+// Less effective: Everything in one giant composite
+AbstractCompositeShape allObjects = new AbstractCompositeShape();
+allObjects.addShape(building1Walls);
+allObjects.addShape(building1Roof);
+allObjects.addShape(building2Walls);
+// ... hundreds of shapes directly in root
+#+END_SRC
+
+*Best practices:*
+
+- Use composites to group objects that occupy a bounded region of space
+- Keep bounding boxes tight (don't add distant objects to the same composite)
+- Nest composites hierarchically for multi-level culling (city → block → building)
+- Call [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.html#invalidateBounds()][invalidateBounds()]] after moving shapes — the bounding box is
+  recomputed lazily on next use
+
+* Technical Details
+:PROPERTIES:
+:CUSTOM_ID: frustum-technical-details
+:END:
+
+The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Frustum.html][Frustum]] class:
+
+- Computes planes in *view space* (camera at origin, looking along +Z)
+- FOV derived from =projectionScale = width / 3= (≈112° horizontal FOV)
+- Default clip distances: Near = 1.0, Far = 10000.0
+- Planes stored in [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Hesse normal form]]: (normal vector, distance)
+
+The frustum is updated once per render pass in
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.html][ShapeCollection.transformShapesBegin()]], from the camera state and
+the stereo viewport width — in stereo mode each eye gets its own
+frustum, so the update runs twice per frame.
diff --git a/Documentation/Global illumination/Bounce estimator.svg b/Documentation/Global illumination/Bounce estimator.svg
new file mode 100644 (file)
index 0000000..0d1972e
--- /dev/null
@@ -0,0 +1,76 @@
+<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="2" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+    <marker id="arrowGold" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
+      <polygon points="0,0 8,3 0,6" fill="#b09020"/>
+    </marker>
+    <marker id="arrowCyan" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
+      <polygon points="0,0 8,3 0,6" fill="#40b0d0"/>
+    </marker>
+    <marker id="arrowGreen" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
+      <polygon points="0,0 8,3 0,6" fill="#39FF14"/>
+    </marker>
+  </defs>
+  <rect width="640" height="480" fill="#061018"/>
+
+  <!-- floor -->
+  <line x1="50" y1="390" x2="600" y2="390" stroke="#30a050" stroke-width="3"/>
+  <text x="60" y="410" fill="#30a050" font-size="11" font-family="monospace">surface</text>
+
+  <!-- wall on the right -->
+  <line x1="480" y1="170" x2="480" y2="390" stroke="#30a050" stroke-width="3"/>
+
+  <!-- sample point P + normal -->
+  <circle cx="230" cy="390" r="6" fill="#c05088" filter="url(#glow)"/>
+  <text x="218" y="414" fill="#c05088" font-size="13" font-family="monospace" text-anchor="middle">P</text>
+  <line x1="230" y1="384" x2="230" y2="300" stroke="#b09020" stroke-width="2" marker-end="url(#arrowGold)"/>
+  <text x="240" y="330" fill="#b09020" font-size="11" font-family="monospace">normal</text>
+
+  <!-- cosine hemisphere dome -->
+  <path d="M 140 390 A 90 90 0 0 1 320 390" fill="none" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="6 3"/>
+  <text x="150" y="292" fill="#40b0d0" font-size="10" font-family="monospace">cosine-weighted</text>
+  <text x="150" y="306" fill="#40b0d0" font-size="10" font-family="monospace">hemisphere</text>
+
+  <!-- a few faint sample rays -->
+  <line x1="230" y1="384" x2="160" y2="310" stroke="#40b0d0" stroke-width="1" stroke-dasharray="3 2"/>
+  <line x1="230" y1="384" x2="300" y2="306" stroke="#40b0d0" stroke-width="1" stroke-dasharray="3 2"/>
+
+  <!-- THE bounce ray: P to Q on the wall -->
+  <line x1="230" y1="384" x2="472" y2="288" stroke="#40b0d0" stroke-width="2.5" marker-end="url(#arrowCyan)" filter="url(#glow)"/>
+  <circle cx="476" cy="285" r="6" fill="#40b0d0" filter="url(#glow)"/>
+  <text x="492" y="280" fill="#40b0d0" font-size="13" font-family="monospace">Q</text>
+
+  <!-- lamp -->
+  <circle cx="540" cy="70" r="14" fill="rgba(255,102,0,0.25)" stroke="#FF6600" stroke-width="2" filter="url(#glow)"/>
+  <circle cx="540" cy="70" r="5" fill="#FF6600"/>
+  <text x="540" y="104" fill="#FF8833" font-size="11" font-family="monospace" text-anchor="middle">light</text>
+
+  <!-- shadow ray P -> light (visible) -->
+  <line x1="236" y1="384" x2="528" y2="76" stroke="#39FF14" stroke-width="1.5" stroke-dasharray="8 4" marker-end="url(#arrowGreen)"/>
+  <text x="330" y="230" fill="#39FF14" font-size="10" font-family="monospace" transform="rotate(-52 330 230)">shadow ray: clear</text>
+
+  <!-- occluded shadow ray from a second point -->
+  <circle cx="400" cy="390" r="4" fill="rgba(192,80,136,0.6)"/>
+  <line x1="404" y1="384" x2="452" y2="240" stroke="#FF4444" stroke-width="1.5" stroke-dasharray="4 3"/>
+  <rect x="438" y="196" width="34" height="44" fill="rgba(255,68,68,0.15)" stroke="#FF4444" stroke-width="1.5"/>
+  <text x="455" y="188" fill="#FF4444" font-size="10" font-family="monospace" text-anchor="middle">occluder</text>
+  <text x="438" y="330" fill="#FF4444" font-size="10" font-family="monospace" transform="rotate(-62 438 330)">blocked</text>
+
+  <!-- what happens at Q -->
+  <rect x="360" y="120" width="260" height="58" rx="6" fill="rgba(32,112,192,0.1)" stroke="#2070c0" stroke-width="1.5"/>
+  <text x="372" y="142" fill="#2070c0" font-size="11" font-family="monospace">at Q: direct light (cached shadow bits)</text>
+  <text x="372" y="160" fill="#2070c0" font-size="11" font-family="monospace">     + Q's current indirect estimate</text>
+
+  <!-- P's update -->
+  <rect x="60" y="60" width="280" height="44" rx="6" fill="rgba(48,160,80,0.1)" stroke="#30a050" stroke-width="1.5"/>
+  <text x="72" y="78" fill="#39FF14" font-size="11" font-family="monospace">P's target = (albedo / &#960;) x (direct + indirect)</text>
+  <text x="72" y="94" fill="#30a050" font-size="10" font-family="monospace">blended in with an exponential moving average</text>
+
+  <text x="320" y="462" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">one sample per texel per visit: one shadow ray + one bounce ray — bounce light ripples deeper every sweep</text>
+</svg>
diff --git a/Documentation/Global illumination/GI pipeline.svg b/Documentation/Global illumination/GI pipeline.svg
new file mode 100644 (file)
index 0000000..0f63023
--- /dev/null
@@ -0,0 +1,55 @@
+<svg viewBox="0 0 620 170" width="620" height="170" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="1.5" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+    <marker id="arrowhead" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
+      <polygon points="0,0 8,3 0,6" fill="#40b0d0"/>
+    </marker>
+  </defs>
+  <rect width="620" height="170" fill="#061018"/>
+
+  <!-- pipeline boxes -->
+  <rect x="10" y="45" width="104" height="64" rx="3" fill="rgba(80,96,192,0.15)" stroke="#5060c0" stroke-width="1.5"/>
+  <text x="62" y="66" fill="#5060c0" font-size="10" font-family="monospace" text-anchor="middle" font-weight="bold">scene snapshot</text>
+  <text x="62" y="80" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">triangles + lights</text>
+  <text x="62" y="92" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">+ BVH</text>
+
+  <rect x="136" y="45" width="104" height="64" rx="3" fill="rgba(255,102,0,0.15)" stroke="#FF6600" stroke-width="1.5"/>
+  <text x="188" y="66" fill="#FF8833" font-size="10" font-family="monospace" text-anchor="middle" font-weight="bold">GI workers</text>
+  <text x="188" y="80" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">Monte Carlo</text>
+  <text x="188" y="92" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">sweeps</text>
+
+  <rect x="262" y="45" width="104" height="64" rx="3" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+  <text x="314" y="66" fill="#39FF14" font-size="10" font-family="monospace" text-anchor="middle" font-weight="bold">lightmaps</text>
+  <text x="314" y="80" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">per-texel indirect</text>
+  <text x="314" y="92" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">+ shadow bits</text>
+
+  <rect x="388" y="45" width="104" height="64" rx="3" fill="rgba(192,80,136,0.15)" stroke="#c05088" stroke-width="1.5"/>
+  <text x="440" y="60" fill="#c05088" font-size="10" font-family="monospace" text-anchor="middle" font-weight="bold">composite</text>
+  <text x="440" y="74" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">baseColor x light</text>
+  <text x="440" y="86" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">double-buffered</text>
+  <text x="440" y="98" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">swap</text>
+
+  <rect x="514" y="45" width="96" height="64" rx="3" fill="rgba(64,176,208,0.15)" stroke="#40b0d0" stroke-width="1.5"/>
+  <text x="562" y="66" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle" font-weight="bold">painter</text>
+  <text x="562" y="80" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">plain texture</text>
+  <text x="562" y="92" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">lookup</text>
+
+  <!-- arrows -->
+  <line x1="114" y1="77" x2="134" y2="77" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+  <line x1="240" y1="77" x2="260" y2="77" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+  <line x1="366" y1="77" x2="386" y2="77" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+  <line x1="492" y1="77" x2="512" y2="77" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+
+  <!-- feedback arrow: lightmaps feed the next sweep's bounce targets -->
+  <path d="M 314 109 L 314 135 L 188 135 L 188 111" fill="none" stroke="#30a050" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#arrowhead)"/>
+  <text x="251" y="147" fill="#30a050" font-size="9" font-family="monospace" text-anchor="middle">bounce reads last sweep's estimate</text>
+
+  <text x="562" y="135" fill="#666" font-size="9" font-family="monospace" text-anchor="middle">zero ray casting</text>
+  <text x="562" y="147" fill="#666" font-size="9" font-family="monospace" text-anchor="middle">on render threads</text>
+</svg>
diff --git a/Documentation/Global illumination/Global illumination.png b/Documentation/Global illumination/Global illumination.png
new file mode 100644 (file)
index 0000000..9909193
Binary files /dev/null and b/Documentation/Global illumination/Global illumination.png differ
diff --git a/Documentation/Global illumination/Lightmap mapping.svg b/Documentation/Global illumination/Lightmap mapping.svg
new file mode 100644 (file)
index 0000000..2d82fea
--- /dev/null
@@ -0,0 +1,75 @@
+<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="1.5" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+    <marker id="arrowhead2" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
+      <polygon points="0,0 8,3 0,6" fill="#b09020"/>
+    </marker>
+  </defs>
+  <rect width="640" height="480" fill="#061018"/>
+
+  <!-- bounding square: the full texture; valid region is the lower-left half (u+v <= 1) -->
+  <rect x="140" y="80" width="320" height="320" fill="rgba(255,68,68,0.06)" stroke="#FF4444" stroke-width="1.5" stroke-dasharray="4 3"/>
+  <text x="372" y="110" fill="#FF4444" font-size="10" font-family="monospace" text-anchor="middle">invalid half (u+v &gt; 1)</text>
+  <text x="372" y="126" fill="#FF4444" font-size="9" font-family="monospace" text-anchor="middle">filled from neighbors so sampling</text>
+  <text x="372" y="138" fill="#FF4444" font-size="9" font-family="monospace" text-anchor="middle">near the hypotenuse stays clean</text>
+
+  <!-- the triangle (valid region) -->
+  <polygon points="140,400 460,400 140,80" fill="rgba(48,160,80,0.12)" stroke="#39FF14" stroke-width="2" filter="url(#glow)"/>
+
+  <!-- texel grid inside the triangle (8x8) -->
+  <line x1="180" y1="360" x2="180" y2="400" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="220" y1="320" x2="220" y2="400" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="260" y1="280" x2="260" y2="400" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="300" y1="240" x2="300" y2="400" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="340" y1="200" x2="340" y2="400" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="380" y1="160" x2="380" y2="400" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="420" y1="120" x2="420" y2="400" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="140" y1="360" x2="420" y2="360" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="140" y1="320" x2="380" y2="320" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="140" y1="280" x2="340" y2="280" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="140" y1="240" x2="300" y2="240" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="140" y1="200" x2="260" y2="200" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="140" y1="160" x2="220" y2="160" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="140" y1="120" x2="180" y2="120" stroke="#1a3a4a" stroke-width="1"/>
+
+  <!-- texel center dots -->
+  <circle cx="160" cy="380" r="2" fill="#c05088"/>
+  <circle cx="200" cy="380" r="2" fill="#c05088"/>
+  <circle cx="240" cy="380" r="2" fill="#c05088"/>
+  <circle cx="160" cy="340" r="2" fill="#c05088"/>
+  <circle cx="200" cy="340" r="2" fill="#c05088"/>
+  <circle cx="160" cy="300" r="2" fill="#c05088"/>
+
+  <!-- one texel highlighted with its world position -->
+  <rect x="200" y="280" width="40" height="40" fill="rgba(255,136,51,0.2)" stroke="#FF8833" stroke-width="1.5"/>
+  <circle cx="220" cy="300" r="3.5" fill="#FF6600" filter="url(#glow)"/>
+  <text x="190" y="348" fill="#FF8833" font-size="10" font-family="monospace">one texel = one surface</text>
+  <text x="190" y="362" fill="#FF8833" font-size="10" font-family="monospace">patch, sampled at center</text>
+
+  <!-- vertices -->
+  <circle cx="140" cy="400" r="6" fill="#c05088"/>
+  <text x="128" y="420" fill="#c05088" font-size="12" font-family="monospace" text-anchor="middle">a (u=0, v=0)</text>
+  <circle cx="460" cy="400" r="6" fill="#c05088"/>
+  <text x="460" y="420" fill="#c05088" font-size="12" font-family="monospace" text-anchor="middle">b (u=1, v=0)</text>
+  <circle cx="140" cy="80" r="6" fill="#c05088"/>
+  <text x="128" y="70" fill="#c05088" font-size="12" font-family="monospace" text-anchor="middle">c (u=0, v=1)</text>
+
+  <!-- edge vectors -->
+  <line x1="140" y1="396" x2="300" y2="396" stroke="#b09020" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
+  <text x="225" y="440" fill="#b09020" font-size="11" font-family="monospace" text-anchor="middle">e1 = b &#8722; a</text>
+  <line x1="144" y1="400" x2="144" y2="240" stroke="#b09020" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
+  <text x="100" y="330" fill="#b09020" font-size="11" font-family="monospace" text-anchor="middle">e2 = c &#8722; a</text>
+
+  <!-- formula box -->
+  <rect x="390" y="180" width="230" height="64" rx="6" fill="rgba(32,112,192,0.1)" stroke="#2070c0" stroke-width="1.5"/>
+  <text x="402" y="204" fill="#2070c0" font-size="12" font-family="monospace">world(u,v) = a + e1&#183;u + e2&#183;v</text>
+  <text x="402" y="226" fill="#2070c0" font-size="10" font-family="monospace">texels = edge / unitsPerTexel</text>
+
+  <text x="320" y="462" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">every texel owns a fixed patch of the triangle — shadows and gradients live INSIDE the surface</text>
+</svg>
diff --git a/Documentation/Global illumination/gi-converged.png b/Documentation/Global illumination/gi-converged.png
new file mode 100644 (file)
index 0000000..1f81bfd
Binary files /dev/null and b/Documentation/Global illumination/gi-converged.png differ
diff --git a/Documentation/Global illumination/gi-flat.png b/Documentation/Global illumination/gi-flat.png
new file mode 100644 (file)
index 0000000..f135bd2
Binary files /dev/null and b/Documentation/Global illumination/gi-flat.png differ
diff --git a/Documentation/Global illumination/gi-start.png b/Documentation/Global illumination/gi-start.png
new file mode 100644 (file)
index 0000000..6f7f3f0
Binary files /dev/null and b/Documentation/Global illumination/gi-start.png differ
diff --git a/Documentation/Global illumination/index.org b/Documentation/Global illumination/index.org
new file mode 100644 (file)
index 0000000..9568956
--- /dev/null
@@ -0,0 +1,251 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Global Illumination - Aukio 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
+
+[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
+
+* What global illumination adds
+:PROPERTIES:
+:CUSTOM_ID: what-gi-adds
+:END:
+
+Plain per-polygon shading lights every polygon with one flat color:
+no shadows, and surfaces that receive no direct light stay uniformly
+dark. A room corner reads as a flat silhouette instead of a corner.
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 640px
+[[file:gi-flat.png]]
+
+*Aukio 3D* can optionally compute *global illumination* progressively
+on background CPU threads: shadows appear, light pools under lamps
+with smooth falloff, and colored light *bleeds* — a red sofa tints the
+floor next to it red. All of it converges gradually over the first
+seconds of a scene, then idles.
+
+Same camera, same house: flat shading (top) versus converged GI
+(below). Note the soft shadow of the partition wall, the lamp glow on
+the ceiling, and the subtle color variation across the floor.
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 640px
+[[file:gi-converged.png]]
+
+* The big idea: GI off the render path
+:PROPERTIES:
+:CUSTOM_ID: off-the-render-path
+:END:
+
+Ray tracing is far too slow to run per frame in a software renderer,
+so it doesn't: *the render loop never traces a single ray.* Painting a
+lightmapped triangle is an ordinary texture lookup, exactly as fast as
+any textured polygon.
+
+All the expensive work happens on dedicated low-priority worker threads
+that continuously refine per-surface lighting values. Whenever the
+values have improved enough, the workers regenerate each triangle's
+*composite texture* (baseColor x total lighting) into a back buffer
+and swap it in atomically — painters never see a half-updated texture.
+
+#+INCLUDE: "GI pipeline.svg" export html
+
+Because painters only read finished textures, frame rate is completely
+decoupled from GI quality: you can crank lightmap resolution up and the
+only cost is CPU time on the worker threads, not frame time.
+
+* Lightmaps: a texture per triangle
+:PROPERTIES:
+:CUSTOM_ID: lightmaps
+:END:
+
+Flat shading can only color a polygon uniformly — shadows and gradients
+need resolution *inside* the polygon. Every lightmapped triangle
+therefore owns a small generated texture, its *lightmap*, whose texels
+map onto the triangle surface by an affine rule:
+
+#+INCLUDE: "Lightmap mapping.svg" export html
+
+The triangle's UVs are pinned to (0,0), (1,0), (0,1), so the valid
+texel region is the half where u+v <= 1; the other half of the square
+texture is flood-filled from valid neighbors so that nearest sampling
+near the hypotenuse never picks up garbage.
+
+Each texel stores two things, both written only by GI threads:
+
+- *Indirect irradiance* (RGB floats) — the accumulated bounced light.
+- *Per-light visibility bits* — whether the last shadow ray from this
+  texel reached each lamp (cached so later queries cost nothing).
+
+Resolution is set in world units per texel
+(=LightmappedCompositeShape.setLightmapUnitsPerTexel()=, default 12): a 100-unit
+wall cell gets an 8x8 lightmap. Halving the units quadruples the
+tracing work.
+
+*What you trace is what you see:* the composite texture the painter
+samples is the lightmap itself, at native texel resolution — there is
+no upscaling step. Shadow-edge smoothness comes from tracing at finer
+resolution, never from interpolation. (An earlier bilinear-upscaling
+pass produced visibly artificial results and was removed; finer texels
+cost more CPU on the GI threads but look right.)
+
+* One sample: a shadow ray and a bounce ray
+:PROPERTIES:
+:CUSTOM_ID: one-sample
+:END:
+
+The work list is flat: one item per lightmap texel (and one per plain
+polygon, see below). Worker threads walk it round-robin, and every
+visit to a texel casts exactly two rays from the texel's world
+position, nudged slightly off the surface along the normal:
+
+1. *Shadow ray* toward a lamp. Answers visible/occluded, cached in the
+   texel's visibility bits. On a texel's *first* visit all lamps are
+   tested at once, so direct light and hard shadows appear after a
+   single sweep instead of trickling in lamp by lamp; later visits
+   re-test one lamp at a time, round-robin.
+2. *Bounce ray* in a random cosine-weighted direction around the
+   normal. Wherever it lands (point Q), the sample reads Q's *direct*
+   lighting — using Q's cached shadow bits, no new shadow rays — plus
+   Q's *current indirect estimate*, and blends the sum into the texel's
+   own indirect value.
+
+#+INCLUDE: "Bounce estimator.svg" export html
+
+Reading Q's current indirect estimate instead of recursing is what
+makes bounce light propagate: sweep 1 learns "Q is directly lit",
+sweep 2 learns "P sees a lit Q", sweep 3 learns "R sees a lit P"...
+Light ripples one surface deeper with every sweep, with no recursion
+limit and no exponential ray explosion. The =1/pi= diffuse gain keeps
+the feedback loop from diverging: without it the indirect term
+amplifies itself and the scene saturates to white.
+
+* Progressive convergence
+:PROPERTIES:
+:CUSTOM_ID: convergence
+:END:
+
+Monte Carlo samples are noisy, so blending happens at *two nested
+levels*, both exponential moving averages:
+
+1. *Inner, per sample*: each texel blends every new bounce-ray result
+   into its indirect estimate with a constant weight (=alpha = 0.15=,
+   mode =fixed=). Every ray hit stays equally intensive forever — an
+   unlit area fades to darkness at the same rate a lit area brightens.
+   (The old =adaptive= mode, which decays alpha with sample count, is
+   still available; see the knobs below.)
+2. *Outer, per composite update*: the value that reaches the screen is
+   a second EMA over the COMPLETE sum =ambient + direct + indirect=.
+   The texture can only move =e3d.gi.compositeAlpha= (default 0.2) of
+   the remaining distance per 500 ms update — so direct light, shadows
+   and bounce light all glide in together over a few seconds, and no
+   single-frame jump is possible by construction.
+
+The world starts at a *uniform medium irradiance*
+(=e3d.gi.initialIrradiance=, default 128): the scene is visible from
+the very first frame, then lit areas brighten and unlit areas sink to
+darkness as the workers sweep — lights and shadows gradually become
+distinguished instead of the old pitch-black start with a sudden flash
+once the first sweep landed:
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 640px
+[[file:gi-start.png]]
+
+Consequences of the design:
+
+- *Hysteresis is free*: when a lamp moves or geometry changes, old
+  light fades out gradually instead of popping — the same pair of EMAs
+  that accumulates light also drains it.
+- *Convergence detection*: per-sample deltas are pure noise (and with a
+  constant alpha they never settle), so the system watches the average
+  per-texel movement of the on-screen estimate; after five composite
+  updates below =e3d.gi.calmThreshold= (default 1.0 light unit) the
+  workers drop to a low duty cycle (~50% of sweep time, capped) instead
+  of burning CPU. Any scene or light change rebuilds the snapshot and
+  restarts full-speed tracing.
+- *Despeckle*: at composite time the indirect channel is blended 50/50
+  with the mean of its valid 4-neighbors, killing single-texel Monte
+  Carlo spikes without blurring real gradients.
+- *Composite cadence*: textures regenerate at most every 500 ms — one
+  atomic swap per triangle, invisible to painters.
+
+* Plain polygons get GI too
+:PROPERTIES:
+:CUSTOM_ID: plain-polygons
+:END:
+
+Surfaces that are not lightmapped (ordinary =SolidPolygon= with shading
+enabled) still benefit, at per-polygon resolution, through the
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.html][GiLightProvider]]
+interface that =GlobalIllumination= installs into the
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]]:
+
+- =isLightVisible(polygon, light)= answers from cached shadow bits —
+  direct-light shadows fade in on flat-shaded geometry.
+- =addIndirectLight(polygon, baseColor, result)= adds the polygon's
+  bounced-light estimate into the flat-shaded color.
+
+Both are called from parallel render-pool threads, so they only read
+volatile caches — never trace.
+
+* Enabling GI
+:PROPERTIES:
+:CUSTOM_ID: enabling
+:END:
+
+#+BEGIN_SRC java
+// Per-texel lightmaps on composite geometry (the House demo setup):
+LightmappedCompositeShape house = new LightmappedCompositeShape();
+house.setLightmappingEnabled(true);
+house.setLightmapUnitsPerTexel(3.0);   // fine texels: quality from traced rays
+
+// Start the workers (2 threads by default; more converge faster):
+viewPanel.enableGlobalIllumination(4);
+#+END_SRC
+
+Tuning knobs (system properties):
+
+| Property                  | Default | Effect                                  |
+|---------------------------+---------+-----------------------------------------|
+| =e3d.gi.alphaMode=        | fixed   | =adaptive= decays the inner EMA alpha with sample count |
+| =e3d.gi.alphaFloor=       | 0.08    | adaptive-mode floor; higher adapts faster but noisier |
+| =e3d.gi.compositeAlpha=   | 0.2     | outer EMA: fade speed of the on-screen estimate per 500 ms update |
+| =e3d.gi.initialIrradiance=| 128     | uniform medium start (0..255 light units) |
+| =e3d.gi.calmThreshold=    | 1.0     | convergence: avg estimate movement (light units) |
+| =e3d.gi.despeckle=        | true    | neighbor-smoothing of indirect at composite time |
+| =e3d.gi.debug=            | false   | sweep statistics to stdout              |
+| =e3d.gi.dumpLightmaps=    | (unset) | dump composite lightmaps as PNGs to the given dir |
+
+* Limitations
+:PROPERTIES:
+:CUSTOM_ID: limitations
+:END:
+
+- *Diffuse light only* — no specular bounce, no caustics.
+- Polygon vertices are traced in composite-local space; scenes that put
+  non-identity transforms on composites are traced incorrectly.
+- The bounce estimate is one ray deep per sample — correctness comes
+  from sweep-over-sweep propagation, so deeply indirect corners take
+  several sweeps to brighten.
+
+* Related Classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class                | Purpose                                                       |
+|----------------------+---------------------------------------------------------------|
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/GlobalIllumination.html][GlobalIllumination]] | Progressive tracer: sweeps, EMA convergence, composite swaps |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/Lightmap.html][Lightmap]]           | Per-triangle texel state + double-buffered composite textures |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.html][LightmappedTriangle]] | Textured triangle whose texture is the GI composite        |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/TriangleBvh.html][TriangleBvh]]        | BVH over world triangles: nearest-hit and any-hit ray queries (Möller–Trumbore) |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.html][GiLightProvider]]    | Cache-read interface feeding the flat-shading path          |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightmappedCompositeShape.html][LightmappedCompositeShape]]  | Wraps polygons into lightmapped triangles                     |
diff --git a/Documentation/Mesh.svg b/Documentation/Mesh.svg
new file mode 100644 (file)
index 0000000..8a20f46
--- /dev/null
@@ -0,0 +1,22 @@
+<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
+  <rect width="640" height="480" fill="#061018"/>
+  <ellipse cx="320" cy="240" rx="180" ry="180" fill="none" stroke="rgba(80,96,192,0.1)" stroke-width="1"/>
+  <ellipse cx="320" cy="240" rx="180" ry="40" fill="none" stroke="rgba(80,96,192,0.25)" stroke-width="1.6"/>
+  <ellipse cx="320" cy="180" rx="150" ry="32" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="1.2"/>
+  <ellipse cx="320" cy="300" rx="150" ry="32" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="1.2"/>
+  <ellipse cx="320" cy="120" rx="90" ry="20" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="1"/>
+  <ellipse cx="320" cy="360" rx="90" ry="20" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="1"/>
+  <ellipse cx="320" cy="240" rx="40" ry="180" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="1.2"/>
+  <ellipse cx="320" cy="240" rx="110" ry="180" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="1"/>
+  <polygon points="320,60 370,116 280,110" fill="rgba(80,96,192,0.15)" stroke="#5060c0" stroke-width="2"/>
+  <polygon points="370,116 410,176 320,164" fill="rgba(80,96,192,0.1)" stroke="#5060c0" stroke-width="1.6"/>
+  <polygon points="320,164 370,116 280,110" fill="rgba(80,96,192,0.07)" stroke="rgba(80,96,192,0.5)" stroke-width="1.2"/>
+  <circle cx="320" cy="60" r="5" fill="#5060c0"/>
+  <circle cx="370" cy="116" r="5" fill="#5060c0"/>
+  <circle cx="280" cy="110" r="5" fill="#5060c0"/>
+  <circle cx="410" cy="176" r="5" fill="#5060c0"/>
+  <circle cx="320" cy="164" r="5" fill="#5060c0"/>
+  <text x="436" y="140" fill="#5060c0" font-size="20" font-weight="600" font-family="monospace">triangulated</text>
+  <text x="436" y="164" fill="#5060c0" font-size="20" font-weight="600" font-family="monospace">section</text>
+  <line x1="412" y1="150" x2="428" y2="150" stroke="#5060c0" stroke-width="1.6"/>
+</svg>
diff --git a/Documentation/Near plane clip/Clip algorithm.svg b/Documentation/Near plane clip/Clip algorithm.svg
new file mode 100644 (file)
index 0000000..1d09a7e
--- /dev/null
@@ -0,0 +1,66 @@
+<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="2" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+  </defs>
+  <rect width="640" height="480" fill="#061018"/>
+
+  <!-- grid -->
+  <line x1="40" y1="120" x2="620" y2="120" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="40" y1="200" x2="620" y2="200" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="40" y1="280" x2="620" y2="280" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="40" y1="360" x2="620" y2="360" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="160" y1="60" x2="160" y2="400" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="300" y1="60" x2="300" y2="400" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="440" y1="60" x2="440" y2="400" stroke="#1a3a4a" stroke-width="1"/>
+
+  <!-- half-space labels -->
+  <text x="170" y="84" fill="#FF4444" font-size="12" font-family="monospace" text-anchor="middle">behind  (z &#8804; near)</text>
+  <text x="460" y="84" fill="#30a050" font-size="12" font-family="monospace" text-anchor="middle">in front  (z &gt; near)</text>
+
+  <!-- near plane -->
+  <line x1="300" y1="95" x2="300" y2="400" stroke="#40b0d0" stroke-width="2" stroke-dasharray="6 3"/>
+  <text x="310" y="110" fill="#40b0d0" font-size="12" font-family="monospace" text-anchor="start" filter="url(#glow)">near plane</text>
+
+  <!-- discarded part of the triangle -->
+  <polygon points="120,260 300,200 300,320" fill="rgba(255,68,68,0.08)" stroke="#FF4444" stroke-width="1.5" stroke-dasharray="4 3"/>
+
+  <!-- kept fragment: the clipped quad -->
+  <polygon points="480,140 480,380 300,320 300,200" fill="rgba(48,160,80,0.12)" stroke="#39FF14" stroke-width="2" filter="url(#glow)"/>
+
+  <!-- original edges continuing behind the plane (ghost) -->
+  <line x1="300" y1="200" x2="120" y2="260" stroke="#c05088" stroke-width="1.5" stroke-dasharray="3 2"/>
+  <line x1="300" y1="320" x2="120" y2="260" stroke="#c05088" stroke-width="1.5" stroke-dasharray="3 2"/>
+
+  <!-- vertices -->
+  <circle cx="480" cy="140" r="6" fill="#c05088"/>
+  <text x="496" y="144" fill="#c05088" font-size="13" font-family="monospace">v0</text>
+  <circle cx="480" cy="380" r="6" fill="#c05088"/>
+  <text x="496" y="384" fill="#c05088" font-size="13" font-family="monospace">v1</text>
+  <circle cx="120" cy="260" r="6" fill="rgba(192,80,136,0.45)"/>
+  <text x="104" y="246" fill="#c05088" font-size="13" font-family="monospace" opacity="0.7">v2</text>
+
+  <!-- intersection vertices -->
+  <circle cx="300" cy="200" r="6" fill="#39FF14" filter="url(#glow)"/>
+  <text x="284" y="190" fill="#39FF14" font-size="13" font-family="monospace" text-anchor="end">p&#8242;</text>
+  <circle cx="300" cy="320" r="6" fill="#39FF14" filter="url(#glow)"/>
+  <text x="314" y="340" fill="#39FF14" font-size="13" font-family="monospace" text-anchor="start">p&#8243;</text>
+
+  <!-- t parameter marker on edge v0-v2, with leader line -->
+  <text x="210" y="140" fill="#FF8833" font-size="11" font-family="monospace" text-anchor="middle">t = 0.5 along v0 &#8594; v2</text>
+  <line x1="280" y1="146" x2="380" y2="166" stroke="#FF8833" stroke-width="1" stroke-dasharray="3 2"/>
+
+  <!-- formula box -->
+  <rect x="50" y="320" width="210" height="86" rx="6" fill="rgba(32,112,192,0.1)" stroke="#2070c0" stroke-width="1.5"/>
+  <text x="62" y="342" fill="#2070c0" font-size="12" font-family="monospace">t  = (near &#8722; z1) / (z2 &#8722; z1)</text>
+  <text x="62" y="362" fill="#2070c0" font-size="12" font-family="monospace">p  = p1 + t&#183;(p2 &#8722; p1)</text>
+  <text x="62" y="382" fill="#2070c0" font-size="12" font-family="monospace">uv = uv1 + t&#183;(uv2 &#8722; uv1)</text>
+
+  <text x="320" y="438" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">every edge crossing the plane spawns an interpolated vertex;</text>
+  <text x="320" y="454" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">in-front vertices pass through unchanged</text>
+</svg>
diff --git a/Documentation/Near plane clip/Fan triangulation.svg b/Documentation/Near plane clip/Fan triangulation.svg
new file mode 100644 (file)
index 0000000..94a8656
--- /dev/null
@@ -0,0 +1,39 @@
+<svg viewBox="0 0 620 240" width="620" height="240" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="1.5" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+  </defs>
+  <rect width="620" height="240" fill="#061018"/>
+
+  <!-- clipped quad -->
+  <polygon points="120,50 330,50 400,180 170,180" fill="rgba(48,160,80,0.12)" stroke="#39FF14" stroke-width="2"/>
+
+  <!-- fan split from v0 -->
+  <line x1="120" y1="50" x2="400" y2="180" stroke="#FF6600" stroke-width="1.5" stroke-dasharray="6 3"/>
+
+  <!-- vertices -->
+  <circle cx="120" cy="50" r="5" fill="#c05088"/>
+  <text x="108" y="42" fill="#c05088" font-size="12" font-family="monospace" text-anchor="middle">v0</text>
+  <circle cx="330" cy="50" r="5" fill="#c05088"/>
+  <text x="330" y="42" fill="#c05088" font-size="12" font-family="monospace" text-anchor="middle">v1</text>
+  <circle cx="400" cy="180" r="5" fill="#39FF14" filter="url(#glow)"/>
+  <text x="408" y="198" fill="#39FF14" font-size="12" font-family="monospace">p&#8243;</text>
+  <circle cx="170" cy="180" r="5" fill="#39FF14" filter="url(#glow)"/>
+  <text x="162" y="198" fill="#39FF14" font-size="12" font-family="monospace" text-anchor="end">p&#8242;</text>
+
+  <!-- triangle labels -->
+  <text x="245" y="100" fill="#FF8833" font-size="12" font-family="monospace" text-anchor="middle">T1 = (v0, v1, p&#8243;)</text>
+  <text x="225" y="152" fill="#FF8833" font-size="12" font-family="monospace" text-anchor="middle">T2 = (v0, p&#8243;, p&#8242;)</text>
+
+  <!-- caption -->
+  <text x="460" y="60" fill="#aaa" font-size="11" font-family="monospace">a triangle cut once</text>
+  <text x="460" y="78" fill="#aaa" font-size="11" font-family="monospace">becomes a quad;</text>
+  <text x="460" y="96" fill="#aaa" font-size="11" font-family="monospace">the rasterizer paints it</text>
+  <text x="460" y="114" fill="#aaa" font-size="11" font-family="monospace">as a 2-triangle fan</text>
+  <text x="460" y="132" fill="#aaa" font-size="11" font-family="monospace">sharing v0</text>
+</svg>
diff --git a/Documentation/Near plane clip/Near plane straddle.svg b/Documentation/Near plane clip/Near plane straddle.svg
new file mode 100644 (file)
index 0000000..aa2c9a6
--- /dev/null
@@ -0,0 +1,57 @@
+<svg viewBox="0 0 620 300" width="620" height="300" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="2" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+  </defs>
+  <rect width="620" height="300" fill="#061018"/>
+
+  <!-- grid -->
+  <line x1="40" y1="80" x2="600" y2="80" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="40" y1="140" x2="600" y2="140" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="40" y1="200" x2="600" y2="200" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="40" y1="260" x2="600" y2="260" stroke="#1a3a4a" stroke-width="1"/>
+
+  <!-- axes -->
+  <line x1="40" y1="270" x2="600" y2="270" stroke="#2070c0" stroke-width="2"/>
+  <text x="592" y="290" fill="#2070c0" font-size="12" font-family="monospace" text-anchor="end">z (depth) &#8594;</text>
+  <text x="44" y="46" fill="#d04040" font-size="12" font-family="monospace">x &#8595;</text>
+
+  <!-- camera eye -->
+  <circle cx="70" cy="200" r="10" fill="rgba(255,102,0,0.2)" stroke="#FF6600" stroke-width="2" filter="url(#glow)"/>
+  <circle cx="70" cy="200" r="3" fill="#FF6600"/>
+  <text x="70" y="232" fill="#FF8833" font-size="11" font-family="monospace" text-anchor="middle">camera</text>
+  <line x1="80" y1="200" x2="140" y2="200" stroke="#FF6600" stroke-width="1.5" stroke-dasharray="4 3"/>
+
+  <!-- near plane -->
+  <line x1="180" y1="55" x2="180" y2="262" stroke="#40b0d0" stroke-width="2" stroke-dasharray="6 3"/>
+  <text x="180" y="46" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#glow)">near plane  z = 1</text>
+
+  <!-- floor tiles seen edge-on (the floor is a row of quads at this x height) -->
+  <line x1="270" y1="200" x2="340" y2="200" stroke="#c05088" stroke-width="3"/>
+  <line x1="350" y1="200" x2="420" y2="200" stroke="#c05088" stroke-width="3"/>
+  <line x1="430" y1="200" x2="500" y2="200" stroke="#c05088" stroke-width="3"/>
+  <line x1="510" y1="200" x2="580" y2="200" stroke="#c05088" stroke-width="3"/>
+  <text x="460" y="186" fill="#c05088" font-size="11" font-family="monospace" text-anchor="middle">floor tiles</text>
+
+  <!-- the straddling tile: behind part discarded, front part kept -->
+  <line x1="130" y1="200" x2="180" y2="200" stroke="#FF4444" stroke-width="3" stroke-dasharray="4 3"/>
+  <line x1="180" y1="200" x2="260" y2="200" stroke="#39FF14" stroke-width="3.5" filter="url(#glow)"/>
+  <circle cx="180" cy="200" r="6" fill="#39FF14" filter="url(#glow)"/>
+  <text x="235" y="248" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">kept fragment</text>
+  <text x="128" y="248" fill="#FF4444" font-size="10" font-family="monospace" text-anchor="middle">cut away</text>
+
+  <!-- old behaviour callout -->
+  <text x="140" y="120" fill="#FF4444" font-size="11" font-family="monospace" text-anchor="middle">old: one vertex behind &#8658;</text>
+  <text x="140" y="136" fill="#FF4444" font-size="11" font-family="monospace" text-anchor="middle">whole tile dropped</text>
+  <line x1="150" y1="142" x2="165" y2="192" stroke="#FF4444" stroke-width="1" stroke-dasharray="3 2"/>
+
+  <!-- new behaviour callout -->
+  <text x="330" y="120" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">new: clip at the plane,</text>
+  <text x="330" y="136" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">paint the surviving fragment</text>
+  <line x1="260" y1="142" x2="205" y2="192" stroke="#39FF14" stroke-width="1" stroke-dasharray="3 2"/>
+</svg>
diff --git a/Documentation/Near plane clip/index.org b/Documentation/Near plane clip/index.org
new file mode 100644 (file)
index 0000000..0c59a70
--- /dev/null
@@ -0,0 +1,151 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Near-Plane Clipping - Aukio 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
+
+[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
+
+* The problem
+:PROPERTIES:
+:CUSTOM_ID: the-problem
+:END:
+
+When the camera brushes against geometry — a floor tile under your
+feet, a wall you lean into — part of a polygon can end up *behind* the
+viewer while the rest stays in front. Perspective projection divides by
+depth (=screenX = x / z=), so a vertex at z ≤ 0 has no meaningful screen
+position at all.
+
+The naive way out — dropping any polygon that has even one vertex
+behind the camera — makes whole tiles vanish exactly when they are
+closest and largest on screen. Walking through the House demo, floor
+tiles blinked out of existence at the bottom of the frame:
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 640px
+[[file:near-clip-before.png]]
+
+*Aukio 3D* instead *clips the polygon against the near plane* and
+renders the surviving fragment. The same frame with clipping enabled —
+the floor is solid to the bottom edge:
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 640px
+[[file:near-clip-after.png]]
+
+Think of the camera plane as the edge of a table and the polygon as a
+sheet of paper partly hanging off it. Dropping the polygon means
+throwing away the whole sheet. Clipping takes scissors, cuts the sheet
+along the table edge, and keeps the part that lies on the table.
+
+#+INCLUDE: "Near plane straddle.svg" export html
+
+* Why not just clamp z?
+:PROPERTIES:
+:CUSTOM_ID: why-not-clamp-z
+:END:
+
+A tempting one-liner is to force every vertex to =z = max(z, epsilon)=
+and project anyway. It fails geometrically: a vertex at z = −50 clamped
+to z = 0.01 projects to a screen coordinate thousands of pixels away,
+*in the wrong direction* — the sign flip of the division mirrors it
+through the camera. The polygon smears into giant streaks across the
+frame instead of ending cleanly at the screen edge.
+
+Clipping produces the geometrically correct cut: the polygon's new edge
+lies exactly on the near plane, and everything the rasterizer receives
+has z > 0.
+
+* How the clipping works
+:PROPERTIES:
+:CUSTOM_ID: how-it-works
+:END:
+
+Clipping happens in *camera space*, after the transform stack has moved
+vertices relative to the viewer but *before* the perspective divide.
+The vertex loop is walked edge by edge (Sutherland-Hodgman style)
+against the plane =z = nearPlaneDistance=:
+
+1. An in-front vertex passes through unchanged.
+2. An edge that crosses the plane spawns a new vertex at the
+   intersection, with position, UV and normal all interpolated with the
+   same parameter =t=.
+3. A behind-plane vertex is skipped.
+
+#+INCLUDE: "Clip algorithm.svg" export html
+
+Interpolating UVs linearly along the 3D edge is exactly right for the
+perspective-correct texture mapper: the intersection vertex is a real
+point on the original edge, so its texture coordinate is the same blend
+of the endpoints' UVs. Textured fragments therefore show the correct
+texels right up to the cut, with no seam.
+
+Only a polygon with *all* vertices behind the plane is culled — the
+legitimate version of the old behavior.
+
+* From clipped loop to pixels
+:PROPERTIES:
+:CUSTOM_ID: from-clip-to-pixels
+:END:
+
+A convex N-gon crossing the plane clips to a single contiguous loop of
+at most N+1 vertices. For the triangle-based rasterizers this means a
+triangle can become a *quad*, which is painted as a two-triangle fan
+sharing the first vertex — exact, because the clip of a convex polygon
+stays convex:
+
+#+INCLUDE: "Fan triangulation.svg" export html
+
+Shape support:
+
+| Shape              | Behavior when straddling                          |
+|--------------------+---------------------------------------------------|
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]]     | Clipped loop painted as triangle fan              |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | Fan-painted with interpolated UVs (also inherited by lightmapped GI fragments) |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]]             | Shortened to the in-front endpoint + intersection |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.html][Billboard]]        | Single anchor point: culled when behind, as before |
+
+Implementation notes:
+
+- Clipped output is stored *per pipeline slot* on the shape
+  (=clippedVertices(ctx)=), so the triple-buffered pipeline can
+  transform frame N+1 while frame N is still painting.
+- Depth sorting and tile binning use the clipped vertices' average Z
+  and screen bounds — a clipped tile sorts as the fragment it became,
+  not as the polygon that reached behind you.
+- New intersection vertices exist only in camera space; they are
+  projected directly via
+  [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html#setCameraSpaceCoordinate(double,double,double,eu.svjatoslav.aukio.e3d.gui.RenderingContext)][Vertex.setCameraSpaceCoordinate()]],
+  bypassing the transform stack.
+
+* Configuration
+:PROPERTIES:
+:CUSTOM_ID: configuration
+:END:
+
+The near plane distance is a per-context knob, in world units:
+
+#+BEGIN_SRC java
+// Default is 1.0; smaller values let the camera press closer to
+// geometry before the scissors bite, at the cost of larger projected
+// coordinates for clipped fragments.
+viewPanel.getRenderingContext().nearPlaneDistance = 0.5;
+#+END_SRC
+
+* Related Classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class                     | Purpose                                                        |
+|---------------------------+----------------------------------------------------------------|
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]] | Vertex-loop clipping in =transform()=, per-slot clip storage   |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]]                  | Camera-space projection for generated intersection vertices    |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]]        | Carries =nearPlaneDistance=                                    |
diff --git a/Documentation/Near plane clip/near-clip-after.png b/Documentation/Near plane clip/near-clip-after.png
new file mode 100644 (file)
index 0000000..f135bd2
Binary files /dev/null and b/Documentation/Near plane clip/near-clip-after.png differ
diff --git a/Documentation/Near plane clip/near-clip-before.png b/Documentation/Near plane clip/near-clip-before.png
new file mode 100644 (file)
index 0000000..6b27c44
Binary files /dev/null and b/Documentation/Near plane clip/near-clip-before.png differ
diff --git a/Documentation/Normal vector.svg b/Documentation/Normal vector.svg
new file mode 100644 (file)
index 0000000..016136e
--- /dev/null
@@ -0,0 +1,18 @@
+<svg viewBox="0 0 640 520" width="640" height="520" xmlns="http://www.w3.org/2000/svg">
+  <rect width="640" height="520" fill="#061018"/>
+  <polygon points="120,400 320,360 520,400 320,440" fill="rgba(180,150,30,0.1)" stroke="rgba(180,150,30,0.4)" stroke-width="2"/>
+  <line x1="180" y1="396" x2="460" y2="396" stroke="rgba(180,150,30,0.08)" stroke-width="1"/>
+  <line x1="220" y1="388" x2="420" y2="388" stroke="rgba(180,150,30,0.06)" stroke-width="1"/>
+  <line x1="320" y1="396" x2="320" y2="120" stroke="#b09020" stroke-width="5"/>
+  <polygon points="320,120 310,144 330,144" fill="#b09020"/>
+  <path d="M320,396 L320,356 L340,360" fill="none" stroke="rgba(180,150,30,0.5)" stroke-width="2"/>
+  <text x="336" y="112" fill="#b09020" font-size="26" font-weight="700" font-family="monospace">N̂</text>
+  <text x="336" y="144" fill="#bbb" font-size="18" font-family="monospace">unit normal</text>
+  <text x="336" y="172" fill="#bbb" font-size="18" font-family="monospace">(perpendicular</text>
+  <text x="336" y="196" fill="#bbb" font-size="18" font-family="monospace"> to surface)</text>
+  <circle cx="140" cy="120" r="28" fill="rgba(180,150,30,0.08)" stroke="rgba(180,150,30,0.3)" stroke-width="2"/>
+  <circle cx="140" cy="120" r="8" fill="rgba(180,150,30,0.6)"/>
+  <text x="112" y="84" fill="#bbb" font-size="18" font-family="monospace">Light</text>
+  <line x1="160" y1="136" x2="300" y2="340" stroke="rgba(180,150,30,0.2)" stroke-width="2" stroke-dasharray="8 6"/>
+  <text x="164" y="284" fill="rgba(180,150,30,0.5)" font-size="18" font-family="monospace">L · N = brightness</text>
+</svg>
diff --git a/Documentation/Perspective correct textures/Adaptive interval.svg b/Documentation/Perspective correct textures/Adaptive interval.svg
new file mode 100644 (file)
index 0000000..924bc5c
--- /dev/null
@@ -0,0 +1,59 @@
+<svg viewBox="0 0 620 270" width="620" height="270" xmlns="http://www.w3.org/2000/svg">
+  <rect width="620" height="270" fill="#061018"/>
+
+  <!-- curvature curve -->
+  <polyline points="70.0,128.0 72.5,127.8 75.0,127.7 77.5,127.5 80.0,127.3 82.5,127.2 85.0,127.0 87.5,126.8 90.0,126.6 92.5,126.5 95.0,126.3 97.5,126.1 100.0,125.9 102.5,125.8 105.0,125.6 107.5,125.4 110.0,125.2 112.5,125.0 115.0,124.8 117.5,124.7 120.0,124.5 122.5,124.3 125.0,124.1 127.5,123.9 130.0,123.7 132.5,123.5 135.0,123.3 137.5,123.1 140.0,122.9 142.5,122.7 145.0,122.5 147.5,122.3 150.0,122.1 152.5,121.9 155.0,121.7 157.5,121.5 160.0,121.3 162.5,121.1 165.0,120.9 167.5,120.7 170.0,120.5 172.5,120.2 175.0,120.0 177.5,119.8 180.0,119.6 182.5,119.4 185.0,119.1 187.5,118.9 190.0,118.7 192.5,118.5 195.0,118.2 197.5,118.0 200.0,117.8 202.5,117.5 205.0,117.3 207.5,117.1 210.0,116.8 212.5,116.6 215.0,116.3 217.5,116.1 220.0,115.9 222.5,115.6 225.0,115.4 227.5,115.1 230.0,114.9 232.5,114.6 235.0,114.3 237.5,114.1 240.0,113.8 242.5,113.6 245.0,113.3 247.5,113.0 250.0,112.8 252.5,112.5 255.0,112.2 257.5,111.9 260.0,111.7 262.5,111.4 265.0,111.1 267.5,110.8 270.0,110.5 272.5,110.2 275.0,109.9 277.5,109.7 280.0,109.4 282.5,109.1 285.0,108.8 287.5,108.5 290.0,108.2 292.5,107.8 295.0,107.5 297.5,107.2 300.0,106.9 302.5,106.6 305.0,106.3 307.5,105.9 310.0,105.6 312.5,105.3 315.0,105.0 317.5,104.6 320.0,104.3 322.5,103.9 325.0,103.6 327.5,103.3 330.0,102.9 332.5,102.6 335.0,102.2 337.5,101.8 340.0,101.5 342.5,101.1 345.0,100.7 347.5,100.4 350.0,100.0 352.5,99.6 355.0,99.2 357.5,98.9 360.0,98.5 362.5,98.1 365.0,97.7 367.5,97.3 370.0,96.9 372.5,96.5 375.0,96.1 377.5,95.6 380.0,95.2 382.5,94.8 385.0,94.4 387.5,93.9 390.0,93.5 392.5,93.1 395.0,92.6 397.5,92.2 400.0,91.7 402.5,91.3 405.0,90.8 407.5,90.3 410.0,89.9 412.5,89.4 415.0,88.9 417.5,88.4 420.0,87.9 422.5,87.4 425.0,86.9 427.5,86.4 430.0,85.9 432.5,85.4 435.0,84.9 437.5,84.3 440.0,83.8 442.5,83.3 445.0,82.7 447.5,82.2 450.0,81.6 452.5,81.1 455.0,80.5 457.5,79.9 460.0,79.3 462.5,78.7 465.0,78.1 467.5,77.5 470.0,76.9 472.5,76.3 475.0,75.7 477.5,75.0 480.0,74.4 482.5,73.8 485.0,73.1 487.5,72.4 490.0,71.8 492.5,71.1 495.0,70.4 497.5,69.7 500.0,69.0 502.5,68.3 505.0,67.6 507.5,66.8 510.0,66.1 512.5,65.4 515.0,64.6 517.5,63.8 520.0,63.0 522.5,62.3 525.0,61.5 527.5,60.6 530.0,59.8 532.5,59.0 535.0,58.1 537.5,57.3 540.0,56.4 542.5,55.5 545.0,54.7 547.5,53.7 550.0,52.8 552.5,51.9 555.0,51.0 557.5,50.0 560.0,49.0 562.5,48.0 565.0,47.0 567.5,46.0 570.0,45.0" fill="none" stroke="#c05088" stroke-width="2"/>
+  <text x="80.0" y="34" fill="#c05088" font-size="10" font-family="monospace">perspective curvature &#8594; rises toward the far end</text>
+
+  <!-- span bar with adaptive blocks -->
+  <rect x="70.0" y="150" width="80.0" height="34" fill="rgba(48,160,80,0.45)" stroke="#30a050" stroke-width="1"/>
+  <rect x="150.0" y="150" width="80.0" height="34" fill="rgba(48,160,80,0.45)" stroke="#30a050" stroke-width="1"/>
+  <rect x="230.0" y="150" width="80.0" height="34" fill="rgba(48,160,80,0.45)" stroke="#30a050" stroke-width="1"/>
+  <rect x="310.0" y="150" width="40.0" height="34" fill="rgba(221,153,0,0.45)" stroke="#dd9900" stroke-width="1"/>
+  <rect x="350.0" y="150" width="40.0" height="34" fill="rgba(221,153,0,0.45)" stroke="#dd9900" stroke-width="1"/>
+  <rect x="390.0" y="150" width="20.0" height="34" fill="rgba(255,102,0,0.45)" stroke="#FF6600" stroke-width="1"/>
+  <rect x="410.0" y="150" width="20.0" height="34" fill="rgba(255,102,0,0.45)" stroke="#FF6600" stroke-width="1"/>
+  <rect x="430.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="440.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="450.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="460.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="470.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="480.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="490.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="500.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="510.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="520.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="530.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="540.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="550.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <rect x="560.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
+  <line x1="70.0" y1="146" x2="70.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="150.0" y1="146" x2="150.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="230.0" y1="146" x2="230.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="310.0" y1="146" x2="310.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="350.0" y1="146" x2="350.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="390.0" y1="146" x2="390.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="410.0" y1="146" x2="410.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="430.0" y1="146" x2="430.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="440.0" y1="146" x2="440.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="450.0" y1="146" x2="450.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="460.0" y1="146" x2="460.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="470.0" y1="146" x2="470.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="480.0" y1="146" x2="480.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="490.0" y1="146" x2="490.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="500.0" y1="146" x2="500.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="510.0" y1="146" x2="510.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="520.0" y1="146" x2="520.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="530.0" y1="146" x2="530.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="540.0" y1="146" x2="540.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="550.0" y1="146" x2="550.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="560.0" y1="146" x2="560.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <line x1="570.0" y1="146" x2="570.0" y2="150" stroke="#aaa" stroke-width="1"/>
+  <text x="190.0" y="202" fill="#30a050" font-size="11" font-family="monospace" text-anchor="middle">16 px</text>
+  <text x="350.0" y="218" fill="#dd9900" font-size="11" font-family="monospace" text-anchor="middle">8 px</text>
+  <text x="410.0" y="202" fill="#FF6600" font-size="11" font-family="monospace" text-anchor="middle">4 px</text>
+  <text x="500.0" y="218" fill="#d04040" font-size="11" font-family="monospace" text-anchor="middle">2 px</text>
+
+  <text x="70" y="248" fill="#666" font-size="10" font-family="monospace">one scanline, 100 px &#8594;</text>
+  <text x="570" y="248" fill="#666" font-size="10" font-family="monospace" text-anchor="end">grazing-angle floor, far side</text>
+</svg>
diff --git a/Documentation/Perspective correct textures/Affine distortion.png b/Documentation/Perspective correct textures/Affine distortion.png
new file mode 100644 (file)
index 0000000..8d3722b
Binary files /dev/null and b/Documentation/Perspective correct textures/Affine distortion.png differ
diff --git a/Documentation/Perspective correct textures/Scanline correction.svg b/Documentation/Perspective correct textures/Scanline correction.svg
new file mode 100644 (file)
index 0000000..cc5fc8d
--- /dev/null
@@ -0,0 +1,41 @@
+<svg viewBox="0 0 620 280" width="620" height="280" xmlns="http://www.w3.org/2000/svg">
+  <rect width="620" height="280" fill="#061018"/>
+
+  <!-- grid -->
+  <line x1="70" y1="185.0" x2="570" y2="185.0" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="70" y1="140.0" x2="570" y2="140.0" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="70" y1="95.0" x2="570" y2="95.0" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="195.0" y1="50" x2="195.0" y2="230" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="320.0" y1="50" x2="320.0" y2="230" stroke="#1a3a4a" stroke-width="1"/>
+  <line x1="445.0" y1="50" x2="445.0" y2="230" stroke="#1a3a4a" stroke-width="1"/>
+
+  <!-- axes -->
+  <line x1="70" y1="230" x2="570" y2="230" stroke="#445566" stroke-width="1.5"/>
+  <line x1="70" y1="230" x2="70" y2="50" stroke="#445566" stroke-width="1.5"/>
+  <text x="570" y="252" fill="#666" font-size="10" font-family="monospace" text-anchor="end">screen pixel &#8594;</text>
+  <text x="62" y="42" fill="#666" font-size="10" font-family="monospace" text-anchor="start">texel u &#8593;</text>
+
+  <!-- affine (straight, wrong) -->
+  <polyline points="70.0,230.0 570.0,50.0" fill="none" stroke="#c05088" stroke-width="2" stroke-dasharray="6 4"/>
+
+  <!-- exact perspective curve -->
+  <polyline points="70.0,230.0 72.5,229.6 75.0,229.3 77.5,228.9 80.0,228.5 82.5,228.2 85.0,227.8 87.5,227.4 90.0,227.0 92.5,226.7 95.0,226.3 97.5,225.9 100.0,225.5 102.5,225.1 105.0,224.7 107.5,224.3 110.0,223.9 112.5,223.6 115.0,223.2 117.5,222.7 120.0,222.3 122.5,221.9 125.0,221.5 127.5,221.1 130.0,220.7 132.5,220.3 135.0,219.8 137.5,219.4 140.0,219.0 142.5,218.6 145.0,218.1 147.5,217.7 150.0,217.3 152.5,216.8 155.0,216.4 157.5,215.9 160.0,215.5 162.5,215.0 165.0,214.6 167.5,214.1 170.0,213.6 172.5,213.2 175.0,212.7 177.5,212.2 180.0,211.8 182.5,211.3 185.0,210.8 187.5,210.3 190.0,209.8 192.5,209.3 195.0,208.8 197.5,208.3 200.0,207.8 202.5,207.3 205.0,206.8 207.5,206.3 210.0,205.8 212.5,205.2 215.0,204.7 217.5,204.2 220.0,203.7 222.5,203.1 225.0,202.6 227.5,202.0 230.0,201.5 232.5,200.9 235.0,200.4 237.5,199.8 240.0,199.2 242.5,198.7 245.0,198.1 247.5,197.5 250.0,196.9 252.5,196.4 255.0,195.8 257.5,195.2 260.0,194.6 262.5,194.0 265.0,193.3 267.5,192.7 270.0,192.1 272.5,191.5 275.0,190.8 277.5,190.2 280.0,189.6 282.5,188.9 285.0,188.3 287.5,187.6 290.0,187.0 292.5,186.3 295.0,185.6 297.5,184.9 300.0,184.3 302.5,183.6 305.0,182.9 307.5,182.2 310.0,181.5 312.5,180.7 315.0,180.0 317.5,179.3 320.0,178.6 322.5,177.8 325.0,177.1 327.5,176.3 330.0,175.6 332.5,174.8 335.0,174.0 337.5,173.3 340.0,172.5 342.5,171.7 345.0,170.9 347.5,170.1 350.0,169.3 352.5,168.5 355.0,167.6 357.5,166.8 360.0,166.0 362.5,165.1 365.0,164.2 367.5,163.4 370.0,162.5 372.5,161.6 375.0,160.7 377.5,159.8 380.0,158.9 382.5,158.0 385.0,157.1 387.5,156.1 390.0,155.2 392.5,154.2 395.0,153.3 397.5,152.3 400.0,151.3 402.5,150.3 405.0,149.3 407.5,148.3 410.0,147.3 412.5,146.3 415.0,145.2 417.5,144.2 420.0,143.1 422.5,142.0 425.0,140.9 427.5,139.8 430.0,138.7 432.5,137.6 435.0,136.5 437.5,135.3 440.0,134.2 442.5,133.0 445.0,131.8 447.5,130.6 450.0,129.4 452.5,128.2 455.0,127.0 457.5,125.7 460.0,124.4 462.5,123.2 465.0,121.9 467.5,120.6 470.0,119.2 472.5,117.9 475.0,116.5 477.5,115.2 480.0,113.8 482.5,112.4 485.0,111.0 487.5,109.5 490.0,108.1 492.5,106.6 495.0,105.1 497.5,103.6 500.0,102.1 502.5,100.5 505.0,99.0 507.5,97.4 510.0,95.8 512.5,94.1 515.0,92.5 517.5,90.8 520.0,89.1 522.5,87.4 525.0,85.7 527.5,83.9 530.0,82.1 532.5,80.3 535.0,78.5 537.5,76.7 540.0,74.8 542.5,72.9 545.0,70.9 547.5,69.0 550.0,67.0 552.5,65.0 555.0,62.9 557.5,60.8 560.0,58.7 562.5,56.6 565.0,54.4 567.5,52.2 570.0,50.0" fill="none" stroke="#39FF14" stroke-width="2"/>
+
+  <!-- subdivided correction -->
+  <polyline points="70.0,230.0 150.0,217.3 230.0,201.5 310.0,181.5 390.0,155.2 470.0,119.2 570.0,50.0" fill="none" stroke="#FF6600" stroke-width="2"/>
+  <circle cx="70.0" cy="230.0" r="3.5" fill="#FF6600"/>
+  <circle cx="150.0" cy="217.3" r="3.5" fill="#FF6600"/>
+  <circle cx="230.0" cy="201.5" r="3.5" fill="#FF6600"/>
+  <circle cx="310.0" cy="181.5" r="3.5" fill="#FF6600"/>
+  <circle cx="390.0" cy="155.2" r="3.5" fill="#FF6600"/>
+  <circle cx="470.0" cy="119.2" r="3.5" fill="#FF6600"/>
+  <circle cx="570.0" cy="50.0" r="3.5" fill="#FF6600"/>
+
+  <!-- legend -->
+  <line x1="380" y1="20" x2="410" y2="20" stroke="#39FF14" stroke-width="2"/>
+  <text x="416" y="24" fill="#39FF14" font-size="10" font-family="monospace">exact perspective</text>
+  <line x1="380" y1="38" x2="410" y2="38" stroke="#FF6600" stroke-width="2"/>
+  <text x="416" y="42" fill="#FF6600" font-size="10" font-family="monospace">corrected every 16 px</text>
+  <line x1="380" y1="56" x2="410" y2="56" stroke="#c05088" stroke-width="2" stroke-dasharray="6 4"/>
+  <text x="416" y="60" fill="#c05088" font-size="10" font-family="monospace">plain affine</text>
+</svg>
diff --git a/Documentation/Perspective correct textures/index.org b/Documentation/Perspective correct textures/index.org
new file mode 100644 (file)
index 0000000..3438f01
--- /dev/null
@@ -0,0 +1,177 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Perspective-Correct Textures - Aukio 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
+
+[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
+
+* The problem
+:PROPERTIES:
+:CUSTOM_ID: introduction
+:ID:       a2b3c4d5-e6f7-8901-bcde-f23456789012
+:END:
+
+When a textured polygon is rendered at an angle to the viewer, naive
+linear interpolation of texture coordinates produces visible
+distortion.
+
+Consider a large textured floor extending toward the horizon. Without
+perspective correction, the texture appears to "swim" or distort
+because the texture coordinates are interpolated linearly across
+screen space, not accounting for depth.
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 1000px
+[[file:Affine distortion.png]]
+
+The *Aukio 3D* engine solves this with *subdivided perspective
+correction* inside the scanline rasterizer — the same technique Quake
+used.
+
+* How perspective correction works
+:PROPERTIES:
+:CUSTOM_ID: how-perspective-correction-works
+:END:
+
+Texture coordinates (u, v) are not linear in screen space, so they
+cannot simply be stepped per pixel. But divide them by depth and they
+become linear: *(u/z, v/z, 1/z) all interpolate linearly* across the
+triangle in screen space.
+
+The rasterizer exploits this:
+
+1. Compute (u/z, v/z, 1/z) at each vertex
+2. Interpolate all three across the scanline with plain additions
+3. Every N pixels, recover the exact texture coordinate with one
+   division: =u = (u/z) / (1/z)=
+4. Between correction points, step u/v affinely toward the next exact
+   point
+
+#+INCLUDE: "Scanline correction.svg" export html
+
+The orange polyline hugs the exact green curve: within each 16-pixel
+block it is a straight line, but every block starts exactly on the
+curve. The dashed pink line is plain affine interpolation — visibly
+wrong everywhere except the endpoints.
+
+Think of it like walking with a map that is slightly distorted: you
+walk in a straight line, but every 16 steps you check a landmark and
+correct your course. The correction (a division) costs something, so
+you do it every N pixels instead of every pixel — the divide cost is
+amortized to 1/16th of a per-pixel-correct rasterizer.
+
+#+BEGIN_SRC java
+// Per scanline (simplified from drawHorizontalLinePerspective):
+while (done < span) {
+    // Advance (u/z, v/z, 1/z) to the end of this block
+    su += dsu * block;  sv += dsv * block;  sw += dsw * block;
+    double txNext = su / sw;   // one reciprocal = exact texture position
+    double tyNext = sv / sw;
+
+    // Step affinely through the block
+    double txStep = (txNext - tx) / block;
+    double tyStep = (tyNext - ty) / block;
+    for (int i = 0; i < block; i++) {
+        plot(x++, texture.sample(tx, ty));
+        tx += txStep;  ty += tyStep;
+    }
+    done += block;
+}
+#+END_SRC
+
+** Adaptive correction interval
+
+Quake used a fixed 16-pixel interval. This engine keeps 16 as the
+default but *shrinks the interval when the error bound demands it*.
+
+The error of affine stepping within a block grows with both the
+texture gradient (texels per pixel) and the perspective curvature
+(how fast 1/z changes across the span). Each scanline computes
+
+#+BEGIN_EXAMPLE
+error(texels) ≈ texelRate · interval² · k / 2
+#+END_EXAMPLE
+
+where =k = |d(1/z)| / min(1/z)= is the per-pixel relative depth change,
+and picks the largest power-of-two interval from the ladder 16, 8, 4,
+2, 1 that keeps the bound under half a texel. Flat, gently angled
+spans keep the fast 16-pixel cadence; a floor tile seen at a grazing
+angle drops to shorter intervals exactly where the curvature is high.
+
+#+INCLUDE: "Adaptive interval.svg" export html
+
+** When affine is good enough
+
+For small or nearly flat triangles, plain affine mapping is already
+within half a texel of exact perspective, so the perspective setup is
+skipped entirely. The test compares the texture range the triangle
+covers against its depth variation:
+
+#+BEGIN_EXAMPLE
+affine is sufficient when  texelSpan · (zMax/zMin − 1) < 2
+#+END_EXAMPLE
+
+Note the criterion is the *texel* span, not the pixel size — a tiny
+on-screen triangle can still map many texels into few pixels. Distant
+clusters of small triangles (a common case) all render through the
+cheaper affine path.
+
+Triangles straddling the near plane (any vertex closer than z = 0.001)
+also fall back to affine, because 1/z interpolation is invalid there.
+
+** Toggling the correction
+
+Perspective correction can be switched off globally for A/B comparison
+or debugging:
+
+#+BEGIN_SRC java
+TexturedTriangle.setPerspectiveCorrectionEnabled(false);  // plain affine everywhere
+#+END_SRC
+
+With correction disabled, large triangles at steep angles visibly warp
+— useful for demonstrating what the correction actually buys.
+
+* Mipmap selection
+:PROPERTIES:
+:CUSTOM_ID: mipmap-selection
+:END:
+
+Perspective correction fixes *where* a texel is sampled; mipmapping
+decides *which resolution* to sample from. Each triangle estimates its
+screen-pixels-per-texel ratio from edge lengths:
+
+#+BEGIN_EXAMPLE
+scaleFactor = (sum of screen edge lengths) / (sum of UV edge lengths) · 1.2
+#+END_EXAMPLE
+
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html#getMipmapForScale(double)][Texture.getMipmapForScale()]]
+then picks the lazily-generated mipmap level closest to that scale:
+halved resolutions when the texture is minified, doubled when strongly
+magnified. Sampling a smaller mipmap under minification both speeds up
+rendering (better cache behavior) and reduces aliasing.
+
+* Related Classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class                         | Purpose                                                            |
+|-------------------------------+--------------------------------------------------------------------|
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]]              | Textured triangle with perspective-correct and SDF rendering paths |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PerspectiveBorderInterpolator.html][PerspectiveBorderInterpolator]] | Edge walker interpolating (u/z, v/z, 1/z) along triangle borders   |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.html][PolygonBorderInterpolator]]     | Edge walker for plain affine mapping                               |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html][Texture]]                       | Mipmap container; also carries the SDF mask and color layers       |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html][TextureBitmap]]                 | Raw pixel array for one mipmap level                               |
+
+*See also:*
+
+- [[file:../SDF textures/][SDF textures]] — signed-distance-field glyph rendering, the
+  alternative sampling path in =TexturedTriangle= that reuses the same
+  perspective-correct interpolation for crisp text at any angle.
diff --git a/Documentation/Point3D vertex.svg b/Documentation/Point3D vertex.svg
new file mode 100644 (file)
index 0000000..0954bac
--- /dev/null
@@ -0,0 +1,105 @@
+<svg width="100%" viewBox="0 0 680 320" xmlns="http://www.w3.org/2000/svg"><rect width="680" height="320" fill="#061018"/><defs><mask id="imagine-text-gaps-eqff6y" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="680" height="350" fill="white"/><rect x="134.36932373046875" y="19.672515869140625" width="71.26135635375977" height="22.184885025024414" fill="black" rx="2"/><rect x="120.89203643798828" y="40.63203048706055" width="98.2159194946289" height="16.12325668334961" fill="black" rx="2"/><rect x="182" y="129.87673950195312" width="78.42510223388672" height="19.42959976196289" fill="black" rx="2"/><rect x="-4.000310796312988" y="66.63202667236328" width="104.23031616210938" height="16.12325668334961" fill="black" rx="2"/><rect x="268" y="66.63202667236328" width="62.12955093383789" height="16.12325668334961" fill="black" rx="2"/><rect x="26.07363510131836" y="132.6320343017578" width="44.086368560791016" height="16.12325668334961" fill="black" rx="2"/><rect x="-3.9983373035211116" y="146.6320343017578" width="74.15834045410156" height="16.12325668334961" fill="black" rx="2"/><rect x="268" y="140.6320343017578" width="98.2159194946289" height="16.12325668334961" fill="black" rx="2"/><rect x="50.129241943359375" y="198.6320343017578" width="50.10076141357422" height="16.12325668334961" fill="black" rx="2"/><rect x="56.14363479614258" y="212.6320343017578" width="44.086368560791016" height="16.12325668334961" fill="black" rx="2"/><rect x="268" y="206.6320343017578" width="74.15834045410156" height="16.12325668334961" fill="black" rx="2"/><rect x="108.86325073242188" y="268.63201904296875" width="122.27349853515625" height="16.12325668334961" fill="black" rx="2"/><rect x="93.82726287841797" y="284.63201904296875" width="152.34547424316406" height="16.12325668334961" fill="black" rx="2"/><rect x="478.88800048828125" y="19.672515869140625" width="62.224021911621094" height="22.184885025024414" fill="black" rx="2"/><rect x="436.83447265625" y="40.63203048706055" width="146.33108520507812" height="16.12325668334961" fill="black" rx="2"/><rect x="414" y="89.52991485595703" width="74.28429412841797" height="17.225370407104492" fill="black" rx="2"/><rect x="544" y="91.18309020996094" width="45.9127311706543" height="15.021142959594727" fill="black" rx="2"/><rect x="352" y="108.28520202636719" width="32.089067459106445" height="13.919028282165527" fill="black" rx="2"/><rect x="402" y="129.52992248535156" width="147.19703674316406" height="17.225370407104492" fill="black" rx="2"/><rect x="402" y="169.52992248535156" width="127.31173706054688" height="17.225370407104492" fill="black" rx="2"/><rect x="402" y="209.52992248535156" width="120.68330383300781" height="17.225370407104492" fill="black" rx="2"/><rect x="594" y="211.18309020996094" width="18.832207679748535" height="15.021142959594727" fill="black" rx="2"/><rect x="402" y="249.52992248535156" width="47.77057647705078" height="17.225370407104492" fill="black" rx="2"/><rect x="473" y="251.18309020996094" width="45.9127311706543" height="15.021142959594727" fill="black" rx="2"/><rect x="612.9194946289062" y="87.18309020996094" width="35.08052062988281" height="15.021142959594727" fill="black" rx="2"/><rect x="607.5033569335938" y="127.18309020996094" width="40.49662780761719" height="15.021142959594727" fill="black" rx="2"/><rect x="612.9194946289062" y="137.18309020996094" width="35.08052062988281" height="15.021142959594727" fill="black" rx="2"/><rect x="629.1677856445312" y="167.18309020996094" width="18.832207679748535" height="15.021142959594727" fill="black" rx="2"/><rect x="607.5033569335938" y="177.18309020996094" width="40.49662780761719" height="15.021142959594727" fill="black" rx="2"/><rect x="647.0900268554688" y="80.28520202636719" width="32.089067459106445" height="13.919028282165527" fill="black" rx="2"/><rect x="647.0900268554688" y="260.2851867675781" width="36.90688133239746" height="13.919028282165527" fill="black" rx="2"/><rect x="385.71209716796875" y="298.63201904296875" width="248.57579040527344" height="16.12325668334961" fill="black" rx="2"/></mask></defs>
+
+
+<!-- Divider -->
+<line x1="340" y1="30" x2="340" y2="310" stroke="#1a2a38" stroke-width="1" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(26, 42, 56);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+
+<!-- ===== LEFT: Point3D ===== -->
+<text x="170" y="36" text-anchor="middle" fill="#2070c0" font-size="15" font-weight="700" font-family="monospace" style="fill:rgb(32, 112, 192);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:auto">Point3D</text>
+<text x="170" y="52" text-anchor="middle" fill="#556677" font-size="10" font-family="monospace" style="fill:rgb(85, 102, 119);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:middle;dominant-baseline:auto">raw coordinates</text>
+
+<!-- Glow rings + point -->
+<circle cx="170" cy="148" r="36" fill="rgba(56,140,248,0.04)" stroke="none" style="fill:rgba(56, 140, 248, 0.04);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<circle cx="170" cy="148" r="20" fill="rgba(56,140,248,0.08)" stroke="none" style="fill:rgba(56, 140, 248, 0.08);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<circle cx="170" cy="148" r="8" fill="rgba(56,140,248,0.2)" stroke="none" style="fill:rgba(56, 140, 248, 0.2);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<circle cx="170" cy="148" r="4" fill="#2070c0" style="fill:rgb(32, 112, 192);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="186" y="144" fill="#2070c0" font-size="13" font-weight="700" font-family="monospace" style="fill:rgb(32, 112, 192);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:13px;font-weight:700;text-anchor:start;dominant-baseline:auto">(x, y, z)</text>
+
+<!-- Radial leader lines + labels — 6 directions, all consistent -->
+<!-- Top-left: distance -->
+<line x1="155" y1="132" x2="68" y2="82" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="3 2" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<circle cx="68" cy="82" r="1.5" fill="#2a3a4a" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="96.23" y="78" text-anchor="end" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:end;dominant-baseline:auto">.getDistanceTo()</text>
+
+<!-- Top-right: rotate -->
+<line x1="188" y1="134" x2="268" y2="82" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="3 2" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<circle cx="268" cy="82" r="1.5" fill="#2a3a4a" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="272" y="78" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:start;dominant-baseline:auto">.rotate()</text>
+
+<!-- Left: add/subtract -->
+<line x1="150" y1="148" x2="58" y2="148" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="3 2" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<circle cx="58" cy="148" r="1.5" fill="#2a3a4a" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="66.16" y="144" text-anchor="end" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:end;dominant-baseline:auto">.add()</text>
+<text x="66.16" y="158" text-anchor="end" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:end;dominant-baseline:auto">.subtract()</text>
+
+<!-- Right: crossProduct -->
+<line x1="190" y1="148" x2="268" y2="148" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="3 2" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<circle cx="268" cy="148" r="1.5" fill="#2a3a4a" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="272" y="152" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:start;dominant-baseline:auto">.crossProduct()</text>
+
+<!-- Bottom-left: unit/dot -->
+<line x1="155" y1="164" x2="68" y2="214" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="3 2" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<circle cx="68" cy="214" r="1.5" fill="#2a3a4a" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="96.23" y="210" text-anchor="end" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:end;dominant-baseline:auto">.unit()</text>
+<text x="96.23" y="224" text-anchor="end" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:end;dominant-baseline:auto">.dot()</text>
+
+<!-- Bottom-right: multiply -->
+<line x1="188" y1="162" x2="268" y2="214" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="3 2" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<circle cx="268" cy="214" r="1.5" fill="#2a3a4a" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="272" y="218" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:start;dominant-baseline:auto">.multiply()</text>
+
+<!-- Summary -->
+<text x="170" y="280" text-anchor="middle" fill="#556677" font-size="10" font-family="monospace" style="fill:rgb(85, 102, 119);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Mutable, fluent API</text>
+<text x="170" y="296" text-anchor="middle" fill="#556677" font-size="10" font-family="monospace" style="fill:rgb(85, 102, 119);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Positions, vectors, math</text>
+
+<!-- ===== RIGHT: Vertex ===== -->
+<text x="510" y="36" text-anchor="middle" fill="#c05088" font-size="15" font-weight="700" font-family="monospace" style="fill:rgb(192, 80, 136);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:auto">Vertex</text>
+<text x="510" y="52" text-anchor="middle" fill="#556677" font-size="10" font-family="monospace" style="fill:rgb(85, 102, 119);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:middle;dominant-baseline:auto">rendering-ready wrapper</text>
+
+<!-- Outer wrapper box -->
+<rect x="375" y="68" width="270" height="230" rx="6" fill="rgba(192,80,136,0.04)" stroke="rgba(192,80,136,0.2)" stroke-width="0.5" style="fill:rgba(192, 80, 136, 0.04);stroke:rgba(192, 80, 136, 0.2);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+
+<!-- coordinate (Point3D) -->
+<rect x="390" y="82" width="240" height="32" rx="4" fill="rgba(56,140,248,0.1)" stroke="rgba(56,140,248,0.3)" stroke-width="0.5" style="fill:rgba(56, 140, 248, 0.1);stroke:rgba(56, 140, 248, 0.3);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<circle cx="406" cy="98" r="3" fill="#2070c0" style="fill:rgb(32, 112, 192);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="418" y="102" fill="#2070c0" font-size="11" font-weight="700" font-family="monospace" style="fill:rgb(32, 112, 192);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:11px;font-weight:700;text-anchor:start;dominant-baseline:auto">coordinate</text>
+<text x="548" y="102" fill="#445566" font-size="9" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:start;dominant-baseline:auto">Point3D</text>
+
+<!-- "wraps" arrow -->
+<line x1="340" y1="148" x2="388" y2="98" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="4 3" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:4px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="356" y="118" fill="#334455" font-size="8" font-family="monospace" transform="rotate(-30 356 118)" style="fill:rgb(51, 68, 85);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:start;dominant-baseline:auto">wraps</text>
+
+<!-- transformedCoordinate -->
+<rect x="390" y="122" width="240" height="32" rx="4" fill="rgba(48,160,80,0.08)" stroke="rgba(48,160,80,0.25)" stroke-width="0.5" style="fill:rgba(48, 160, 80, 0.08);stroke:rgba(48, 160, 80, 0.25);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="406" y="142" fill="#30a050" font-size="11" font-weight="700" font-family="monospace" style="fill:rgb(48, 160, 80);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:11px;font-weight:700;text-anchor:start;dominant-baseline:auto">transformedCoordinate</text>
+
+<!-- onScreenCoordinate -->
+<rect x="390" y="162" width="240" height="32" rx="4" fill="rgba(255,102,0,0.08)" stroke="rgba(255,102,0,0.25)" stroke-width="0.5" style="fill:rgba(255, 102, 0, 0.08);stroke:rgba(255, 102, 0, 0.25);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="406" y="182" fill="#FF6600" font-size="11" font-weight="700" font-family="monospace" style="fill:rgb(255, 102, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:11px;font-weight:700;text-anchor:start;dominant-baseline:auto">onScreenCoordinate</text>
+
+<!-- textureCoordinate -->
+<rect x="390" y="202" width="240" height="32" rx="4" fill="rgba(176,144,32,0.08)" stroke="rgba(176,144,32,0.25)" stroke-width="0.5" style="fill:rgba(176, 144, 32, 0.08);stroke:rgba(176, 144, 32, 0.25);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="406" y="222" fill="#b09020" font-size="11" font-weight="700" font-family="monospace" style="fill:rgb(176, 144, 32);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:11px;font-weight:700;text-anchor:start;dominant-baseline:auto">textureCoordinate</text>
+<text x="598" y="222" fill="#445566" font-size="9" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:start;dominant-baseline:auto">UV</text>
+
+<!-- normal -->
+<rect x="390" y="242" width="240" height="32" rx="4" fill="rgba(80,96,192,0.08)" stroke="rgba(80,96,192,0.25)" stroke-width="0.5" style="fill:rgba(80, 96, 192, 0.08);stroke:rgba(80, 96, 192, 0.25);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="406" y="262" fill="#5060c0" font-size="11" font-weight="700" font-family="monospace" style="fill:rgb(80, 96, 192);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:11px;font-weight:700;text-anchor:start;dominant-baseline:auto">normal</text>
+<text x="477" y="262" fill="#445566" font-size="9" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:start;dominant-baseline:auto">for CSG</text>
+
+<!-- Right-side annotations -->
+<text x="644" y="98" fill="#334455" font-size="9" font-family="monospace" text-anchor="end" style="fill:rgb(51, 68, 85);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:end;dominant-baseline:auto">local</text>
+<text x="644" y="138" fill="#334455" font-size="9" font-family="monospace" text-anchor="end" style="fill:rgb(51, 68, 85);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:end;dominant-baseline:auto">camera</text>
+<text x="644" y="148" fill="#334455" font-size="9" font-family="monospace" text-anchor="end" style="fill:rgb(51, 68, 85);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:end;dominant-baseline:auto">space</text>
+<text x="644" y="178" fill="#334455" font-size="9" font-family="monospace" text-anchor="end" style="fill:rgb(51, 68, 85);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:end;dominant-baseline:auto">2D</text>
+<text x="644" y="188" fill="#334455" font-size="9" font-family="monospace" text-anchor="end" style="fill:rgb(51, 68, 85);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:end;dominant-baseline:auto">pixels</text>
+
+<!-- Pipeline arrow -->
+<line x1="654" y1="92" x2="654" y2="270" stroke="rgba(192,80,136,0.15)" stroke-width="1" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgba(192, 80, 136, 0.15);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="651.09" y="90" fill="#445566" font-size="8" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:start;dominant-baseline:auto">local</text>
+<text x="651.09" y="270" fill="#445566" font-size="8" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:start;dominant-baseline:auto">screen</text>
+<polygon points="654,272 650,265 658,265" fill="rgba(192,80,136,0.3)" style="fill:rgba(192, 80, 136, 0.3);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+
+<!-- Summary -->
+<text x="510" y="310" text-anchor="middle" fill="#556677" font-size="10" font-family="monospace" style="fill:rgb(85, 102, 119);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Tracks position across coordinate spaces</text>
+</svg>
diff --git a/Documentation/Rendering loop/CPU scheduling.png b/Documentation/Rendering loop/CPU scheduling.png
new file mode 100644 (file)
index 0000000..12aa23d
Binary files /dev/null and b/Documentation/Rendering loop/CPU scheduling.png differ
diff --git a/Documentation/Rendering loop/Double buffering.svg b/Documentation/Rendering loop/Double buffering.svg
new file mode 100644 (file)
index 0000000..141dad6
--- /dev/null
@@ -0,0 +1,47 @@
+<svg viewBox="0 0 520 180" width="520" height="180" xmlns="http://www.w3.org/2000/svg">
+  <rect width="520" height="180" fill="#061018"/>
+
+  <!-- LEFT: Tearing -->
+  <text x="125" y="18" fill="#d04040" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">Without double-buffering</text>
+
+  <rect x="25" y="28" width="200" height="140" stroke="rgba(100,100,100,0.4)" stroke-width="1" fill="none" rx="3"/>
+  <text x="125" y="44" fill="#aaa" font-size="8" font-family="monospace" text-anchor="middle">display shows partial update</text>
+
+  <!-- Old frame top half -->
+  <rect x="45" y="52" width="160" height="45" fill="rgba(208,64,64,0.12)" stroke="rgba(208,64,64,0.4)" stroke-width="1"/>
+  <text x="125" y="79" fill="rgba(208,64,64,0.6)" font-size="9" font-family="monospace" text-anchor="middle">old frame</text>
+
+  <!-- Tear line -->
+  <line x1="45" y1="97" x2="205" y2="97" stroke="#d04040" stroke-width="2" stroke-dasharray="4,3"/>
+  <text x="228" y="100" fill="#d04040" font-size="8" font-family="monospace">← tear</text>
+
+  <!-- New frame bottom half -->
+  <rect x="45" y="97" width="160" height="55" fill="rgba(48,160,80,0.1)" stroke="rgba(48,160,80,0.4)" stroke-width="1"/>
+  <text x="125" y="130" fill="rgba(48,160,80,0.6)" font-size="9" font-family="monospace" text-anchor="middle">new frame</text>
+
+  <!-- RIGHT: Double-buffered -->
+  <text x="400" y="18" fill="#30a050" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">With double-buffering</text>
+
+  <!-- Back buffer -->
+  <rect x="295" y="35" width="90" height="130" fill="rgba(32,112,192,0.08)" stroke="rgba(32,112,192,0.5)" stroke-width="1.5" rx="2"/>
+  <text x="340" y="58" fill="#2070c0" font-size="9" font-family="monospace" text-anchor="middle">Back buffer</text>
+  <text x="340" y="72" fill="#aaa" font-size="7" font-family="monospace" text-anchor="middle">(draw here)</text>
+  <!-- Scribble lines to suggest "being drawn" -->
+  <line x1="310" y1="90" x2="365" y2="90" stroke="rgba(32,112,192,0.25)" stroke-width="1"/>
+  <line x1="310" y1="100" x2="355" y2="100" stroke="rgba(32,112,192,0.2)" stroke-width="1"/>
+  <line x1="310" y1="110" x2="345" y2="110" stroke="rgba(32,112,192,0.15)" stroke-width="1"/>
+
+  <!-- Swap arrow -->
+  <line x1="390" y1="100" x2="415" y2="100" stroke="#30a050" stroke-width="1.5"/>
+  <polygon points="415,96 423,100 415,104" fill="#30a050"/>
+  <text x="407" y="90" fill="#30a050" font-size="7" font-family="monospace" text-anchor="middle">swap</text>
+
+  <!-- Front buffer -->
+  <rect x="428" y="35" width="80" height="130" fill="rgba(48,160,80,0.1)" stroke="#30a050" stroke-width="1.5" rx="2"/>
+  <text x="468" y="58" fill="#30a050" font-size="9" font-family="monospace" text-anchor="middle">Front buffer</text>
+  <text x="468" y="72" fill="#aaa" font-size="7" font-family="monospace" text-anchor="middle">(displayed)</text>
+  <!-- Solid fill to suggest complete frame -->
+  <rect x="440" y="85" width="56" height="65" fill="rgba(48,160,80,0.08)" rx="1"/>
+  <text x="468" y="122" fill="rgba(48,160,80,0.5)" font-size="8" font-family="monospace" text-anchor="middle">complete</text>
+  <text x="468" y="133" fill="rgba(48,160,80,0.5)" font-size="8" font-family="monospace" text-anchor="middle">frame</text>
+</svg>
diff --git a/Documentation/Rendering loop/Paint tiles.svg b/Documentation/Rendering loop/Paint tiles.svg
new file mode 100644 (file)
index 0000000..e27d5f8
--- /dev/null
@@ -0,0 +1,34 @@
+<svg viewBox="0 0 400 200" width="400" height="200" xmlns="http://www.w3.org/2000/svg">
+  <rect width="400" height="200" fill="#061018"/>
+
+  <!-- Tile grid: 5 columns x 4 rows = 20 tiles over a 380x160 viewport -->
+  <g fill="#1a3a4a" stroke="#30a050" stroke-width="1">
+    <rect x="10"  y="5" width="76" height="40"/>
+    <rect x="86"  y="5" width="76" height="40"/>
+    <rect x="162" y="5" width="76" height="40"/>
+    <rect x="238" y="5" width="76" height="40"/>
+    <rect x="314" y="5" width="76" height="40"/>
+    <rect x="10"  y="45" width="76" height="40"/>
+    <rect x="86"  y="45" width="76" height="40"/>
+    <rect x="162" y="45" width="76" height="40"/>
+    <rect x="238" y="45" width="76" height="40"/>
+    <rect x="314" y="45" width="76" height="40"/>
+    <rect x="10"  y="85" width="76" height="40"/>
+    <rect x="86"  y="85" width="76" height="40"/>
+    <rect x="162" y="85" width="76" height="40"/>
+    <rect x="238" y="85" width="76" height="40"/>
+    <rect x="314" y="85" width="76" height="40"/>
+    <rect x="10"  y="125" width="76" height="40"/>
+    <rect x="86"  y="125" width="76" height="40"/>
+    <rect x="162" y="125" width="76" height="40"/>
+    <rect x="238" y="125" width="76" height="40"/>
+    <rect x="314" y="125" width="76" height="40"/>
+  </g>
+
+  <!-- A shape overlapping several tiles gets binned into each of them -->
+  <ellipse cx="200" cy="85" rx="90" ry="45" fill="rgba(255,102,0,0.25)" stroke="#FF6600" stroke-width="1.5"/>
+  <text x="200" y="89" fill="#FF6600" font-size="10" font-family="monospace" text-anchor="middle">one shape</text>
+
+  <text x="10" y="182" fill="#30a050" font-size="10" font-family="monospace">~10 tiles per thread; threads steal pending</text>
+  <text x="10" y="195" fill="#30a050" font-size="10" font-family="monospace">tiles — no fixed thread↔tile assignment</text>
+</svg>
diff --git a/Documentation/Rendering loop/Painter's algorithm.svg b/Documentation/Rendering loop/Painter's algorithm.svg
new file mode 100644 (file)
index 0000000..7727fc2
--- /dev/null
@@ -0,0 +1,13 @@
+
+<svg viewBox="0 0 520 180" width="520" height="180" xmlns="http://www.w3.org/2000/svg">
+  <rect width="520" height="180" fill="#061018"/>
+  <!-- Far -->
+  <rect x="30" y="15" width="440" height="150" fill="rgba(48,160,80,0.06)" stroke="rgba(48,160,80,0.35)" stroke-width="1.5"/>
+  <text x="250" y="38" fill="rgba(48,160,80,0.7)" font-size="11" font-family="monospace" text-anchor="middle">Far (Z=500) — painted first</text>
+  <!-- Medium -->
+  <rect x="70" y="48" width="360" height="105" fill="rgba(32,112,192,0.10)" stroke="rgba(32,112,192,0.5)" stroke-width="1.5"/>
+  <text x="250" y="80" fill="rgba(32,112,192,0.85)" font-size="11" font-family="monospace" text-anchor="middle">Medium (Z=300) — painted second</text>
+  <!-- Near -->
+  <rect x="115" y="90" width="270" height="55" fill="rgba(200,80,140,0.18)" stroke="#c05088" stroke-width="2"/>
+  <text x="250" y="123" fill="#c05088" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">Near (Z=100) — painted last</text>
+</svg>
diff --git a/Documentation/Rendering loop/Render pipeline.svg b/Documentation/Rendering loop/Render pipeline.svg
new file mode 100644 (file)
index 0000000..927e357
--- /dev/null
@@ -0,0 +1,47 @@
+<svg viewBox="0 0 620 80" width="620" height="80" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <marker id="arrowhead" viewBox="0 0 10 10" refX="9" refY="5"
+            markerWidth="6" markerHeight="6" orient="auto">
+      <path d="M 0 0 L 10 5 L 0 10 z" fill="#30a050"/>
+    </marker>
+  </defs>
+  <rect width="620" height="80" fill="#061018"/>
+
+  <!-- Boxes -->
+  <rect x="8" y="25" width="70" height="30" rx="3" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+  <text x="43" y="43" fill="#30a050" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Shapes</text>
+
+  <rect x="96" y="25" width="90" height="30" rx="3" fill="rgba(32,112,192,0.15)" stroke="#2070c0" stroke-width="1.5"/>
+  <text x="141" y="43" fill="#2070c0" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Transform</text>
+
+  <rect x="204" y="25" width="60" height="30" rx="3" fill="rgba(200,80,140,0.15)" stroke="#c05088" stroke-width="1.5"/>
+  <text x="234" y="43" fill="#c05088" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Sort</text>
+
+  <rect x="282" y="25" width="60" height="30" rx="3" fill="rgba(255,170,0,0.15)" stroke="#dd9900" stroke-width="1.5"/>
+  <text x="312" y="43" fill="#dd9900" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Bin</text>
+
+  <rect x="360" y="25" width="70" height="30" rx="3" fill="rgba(255,102,0,0.15)" stroke="#FF6600" stroke-width="1.5"/>
+  <text x="395" y="43" fill="#FF6600" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Paint</text>
+
+  <rect x="448" y="25" width="80" height="30" rx="3" fill="rgba(57,255,20,0.15)" stroke="#39FF14" stroke-width="1.5"/>
+  <text x="488" y="43" fill="#39FF14" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Present</text>
+
+  <rect x="546" y="25" width="66" height="30" rx="3" fill="rgba(100,100,100,0.2)" stroke="#aaa" stroke-width="1.5"/>
+  <text x="579" y="43" fill="#aaa" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Screen</text>
+
+  <!-- Arrows -->
+  <line x1="78" y1="40" x2="92" y2="40" stroke="#30a050" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+  <line x1="186" y1="40" x2="200" y2="40" stroke="#2070c0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+  <line x1="264" y1="40" x2="278" y2="40" stroke="#c05088" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+  <line x1="342" y1="40" x2="356" y2="40" stroke="#dd9900" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+  <line x1="430" y1="40" x2="444" y2="40" stroke="#FF6600" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+  <line x1="528" y1="40" x2="542" y2="40" stroke="#39FF14" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+
+  <!-- Labels below -->
+  <text x="43" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">3D vertices</text>
+  <text x="141" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">world→screen</text>
+  <text x="234" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">back-to-front</text>
+  <text x="312" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">per tile</text>
+  <text x="395" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">tile grid</text>
+  <text x="488" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">own thread</text>
+</svg>
diff --git a/Documentation/Rendering loop/index.org b/Documentation/Rendering loop/index.org
new file mode 100644 (file)
index 0000000..c7fe2e0
--- /dev/null
@@ -0,0 +1,523 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Rendering Loop - Aukio 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
+
+[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
+
+* Rendering loop
+:PROPERTIES:
+:CUSTOM_ID: rendering-loop
+:ID:       a1b2c3d4-e5f6-7890-abcd-ef1234567890
+:END:
+
+The rendering loop is the heart of the engine, continuously generating
+frames on a dedicated background thread. It orchestrates the entire
+rendering pipeline from 3D world space to pixels on screen.
+
+** What is a render loop?
+:PROPERTIES:
+:CUSTOM_ID: what-is-a-render-loop
+:END:
+
+A *render loop* is a continuous process that generates visual frames
+from 3D data. Think of it like a movie camera: each "frame" captures
+the current state of the 3D world and converts it into a 2D image that
+can be displayed on screen.
+
+The process transforms shapes through multiple coordinate systems:
+
+#+INCLUDE: "Render pipeline.svg" export html
+
+Each step has a specific purpose:
+
+| Step      | Input | Output | Purpose |
+|-----------+-------+--------+---------|
+| Shapes    | 3D [[file:../index.org::#vertex][vertices]], [[file:../index.org::#mesh][meshes]] | Scene data | Objects waiting to be drawn |
+| Transform | World coordinates | Screen coordinates | Convert 3D positions to where they appear on screen (see [[file:../index.org::#coordinate-system][coordinate system]]); cull shapes outside the view frustum. Parallel: heavy subtrees fork onto the worker pool |
+| Sort      | Unordered shapes | Ordered by depth | Ensure correct visibility (far objects painted first). Parallel merge sort for large scenes |
+| Bin       | Sorted shapes | Per-tile shape lists | Each paint tile iterates only shapes that can touch it. Parallel over the worker pool |
+| Paint     | Per-tile shape lists | Pixels in buffer | Tiles split the screen into independent work units so clearing and rasterization run in parallel across CPU cores |
+| Present   | Pixel buffer | Screen image | Hand the completed frame to a dedicated thread that copies it to the display |
+
+This pipeline runs repeatedly, targeting 60 frames per second by
+default. Even if nothing moves, the loop continues running—but the
+engine [[#frame-listeners][skips unnecessary work]] when the scene is static.
+
+The steps above describe one frame *logically*, in the order data flows
+through it. In execution the engine is a software pipeline: transform
+of the next frame already runs while the previous frame is still being
+painted, and presentation happens on its own thread. See
+[[#software-pipeline][Software pipeline]].
+
+** Main loop structure
+:PROPERTIES:
+:CUSTOM_ID: main-loop-structure
+:END:
+
+The engine runs two dedicated daemon threads:
+
+- =e3d-render= — produces frames. It runs continuously:
+
+#+BEGIN_SRC java
+while (renderThreadRunning) {
+    ensureThatViewIsUpToDate();  // Produce one frame (or skip)
+    maintainTargetFps();         // Sleep if ahead of schedule
+}
+#+END_SRC
+
+- =e3d-present= — presents frames. It takes completed frames from a
+  mailbox and performs all display-path work (the multi-megabyte
+  =drawImage=, =BufferStrategy.show()= and the X server round-trip), so
+  the render thread never blocks on the display.
+
+Both threads are daemons, so they stop automatically when the JVM
+exits. You can stop them explicitly with [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#stop()][ViewPanel.stop()]].
+
+** Frame rate control
+:PROPERTIES:
+:CUSTOM_ID: frame-rate-control
+:END:
+
+The engine supports two modes:
+
+- *Target FPS mode*: Set with [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#setFrameRate(int)][setFrameRate(int)]].
+  The engine tries to maintain the target rate by sleeping between frames.
+
+  - *When rendering is slower than target*: No sleeping occurs. The engine
+    runs at maximum hardware speed. Missed frames are skipped, not
+    rendered later — the timing simply resets to current time.
+
+  - *When rendering is faster than target*: The thread sleeps to limit FPS
+    to the target rate, avoiding unnecessary CPU usage.
+
+  For example, with a 60 FPS target:
+  - If a complex scene takes 30ms per frame, you get ~33 FPS (hardware limit)
+  - If the scene later simplifies to 10ms per frame, you get exactly
+    60 FPS (throttled by sleeping)
+
+- *Unlimited mode*: Set =setFrameRate(0)= or negative. No sleeping —
+  renders as fast as possible, and frames are produced even when the
+  scene reports no changes, so the measured rate reflects maximum
+  achievable throughput. Useful for benchmarking.
+
+*Production vs presentation.* These are measured separately:
+
+- *Production rate* (=getMeasuredFPS()=) counts frames the pipeline
+  completes per second. This is the benchmark number.
+- *Presentation rate* is how fast frames actually reach the screen. In
+  capped-FPS mode the present thread is paced to 60 blits per second
+  (override with =-Daukio3d.presentRate=N=); the cap exists because the
+  X server also dispatches input, and flooding it with blits causes
+  desktop-wide mouse/keyboard jitter. When production outruns
+  presentation, stale frames are dropped from the mailbox instead of
+  piling up latency. In unlimited (benchmark) mode presentation pacing
+  is disabled entirely, so it cannot throttle production.
+
+* Software pipeline
+:PROPERTIES:
+:CUSTOM_ID: software-pipeline
+:END:
+
+The phases below are described per frame, but consecutive frames
+*overlap*. The engine triple-buffers everything a frame writes:
+
+- 3 framebuffers (each with its own =RenderingContext=)
+- 3 projection buffer slots (per-vertex screen state)
+- 3 render aggregators (transform output, sort/bin state)
+
+A render pass P (one per frame, or one per eye in stereo) transforms
+into slot P mod 3, so it only conflicts with the paint of pass P-3.
+Before each transform the render thread *flushes* completed paint
+passes (mouse hits, frame deposit) and blocks only if paint P-3 is
+still running — which steady-state worker throughput prevents. Workers
+finishing one pass's tiles flow straight into the next pass's queued
+tiles with no idle gap.
+
+Completed frames go to a *presentation mailbox* that keeps only the
+newest frame: if the display path is slower than production, stale
+frames are dropped (and their buffers released) instead of
+accumulating latency — swapchain "mailbox mode".
+
+A per-buffer *present gate* guarantees painting frame F+3 never
+overwrites a buffer the present thread is still blitting frame F from.
+
+The goal of all this overlap is throughput: keep every CPU core busy,
+all the time. No phase waits for another phase of the same frame when
+it could already be working on the next one. The Developer Tools
+thread-activity timeline shows it working — all 18 worker rows packed
+solid with paint, bin and sort tasks from up to three frames at once,
+while the render thread (top row) and present thread tick along above
+them:
+
+#+attr_html: :class responsive-img
+[[file:CPU scheduling.png]]
+
+The pipeline can be disabled with =-Daukio3d.pipeline=false=, restoring
+strictly sequential phase order (each paint pass is awaited
+immediately). This is a kill switch for benchmarking and regression
+hunting.
+
+* Rendering phases
+:PROPERTIES:
+:CUSTOM_ID: rendering-phases
+:END:
+
+Each frame goes through 6 phases. Phases 2–4 run inside an
+asynchronous continuation on the shared worker pool, and phases of
+consecutive frames overlap as described in [[#software-pipeline][Software pipeline]].
+
+** Phase 1: Transform shapes
+:PROPERTIES:
+:CUSTOM_ID: phase-1-transform-shapes
+:END:
+
+All shapes are transformed from world space to screen space:
+
+1. Build camera-relative transform (inverse of camera position/rotation)
+2. Update the view frustum from camera state and viewport dimensions
+3. Walk the scene tree:
+   - Cull composite shapes whose bounding box misses the frustum
+   - Apply camera transform
+   - Project 3D → 2D (perspective projection)
+   - Calculate depth for sorting
+   - Queue for rendering
+
+*What is coordinate transformation?*
+
+Every shape exists in "world space" — its own position in the 3D world.
+To render it, we must convert to "screen space" — where it appears on
+your monitor. This involves:
+
+- *Translation*: Move coordinates relative to camera position
+- *Rotation*: Rotate coordinates based on camera orientation
+- *Projection*: Convert 3D (x, y, z) to 2D (x, y) screen pixels
+
+Objects further away appear smaller (perspective). The [[file:../index.org::#coordinate-system][coordinate system]]
+uses Y-down to match screen conventions, making projection straightforward.
+
+The transform is *parallel and non-blocking*: composites with enough
+children fork their render lists into chunk tasks on the shared worker
+pool (at any nesting level), and the render thread returns without
+waiting. The chunk tasks are drained and merged on a worker thread
+inside the paint continuation, while the render thread is already
+walking the next pass.
+
+*Frustum culling* happens here: composites test their bounding box
+against the frustum and skip invisible subtrees entirely, saving both
+transform and paint work. Per-frame culling statistics are collected
+for the developer tools panel.
+
+** Phase 2: Sort shapes by depth
+:PROPERTIES:
+:CUSTOM_ID: phase-2-sort-shapes
+:END:
+
+Shapes are sorted by depth in descending order (farthest first), with
+the shape id as a deterministic tiebreaker:
+
+#+BEGIN_SRC java
+// ShapesZIndexComparator: descending Z, ties broken by shape id
+if (z1 < z2) return 1;        // z1 is nearer -> sort after z2
+else if (z1 > z2) return -1;  // z1 is farther -> sort before z2
+return Integer.compare(o1.shapeId, o2.shapeId);
+#+END_SRC
+
+Above 8192 queued shapes the sort runs as an instrumented parallel
+merge sort on the shared worker pool; below that it is single-threaded.
+
+*Why sort back-to-front?*
+
+This implements the *painter's algorithm* — like painting a landscape:
+first paint the sky (farthest), then mountains, then trees, then the
+foreground. Each layer covers what's behind it.
+
+#+INCLUDE: "Painter's algorithm.svg" export html
+
+Without sorting, nearby objects might be painted first and then covered
+by distant ones, causing visual errors. This is especially important for
+*transparent objects* — you need to see through the near ones to what's
+behind.
+
+The Z value represents distance from the camera after transformation.
+Larger values = further away. The id tiebreaker keeps the order
+deterministic frame-to-frame, which tiled rendering relies on: every
+tile paints its shapes in the same global (Z, id) order.
+
+** Phase 3: Bin shapes into tiles
+:PROPERTIES:
+:CUSTOM_ID: phase-3-bin-shapes-into-tiles
+:END:
+
+The sorted queue is binned per paint tile by screen-space overlap:
+each tile's bin lists only the shapes whose vertex bounds (plus a
+paint margin) can touch that tile. A shape overlapping several tiles
+is added to each of their bins.
+
+This means a paint thread iterates a short local list instead of the
+whole scene, and it is what makes the tile grid scale: refining the
+grid shrinks each bin instead of just subdividing the clearing work.
+
+Binning is parallelized over the shared worker pool.
+
+** Phase 4: Clear and paint tiles (multi-threaded)
+:PROPERTIES:
+:CUSTOM_ID: phase-4-clear-paint-tiles
+:END:
+
+The viewport is divided into a grid of rectangular *tiles* — roughly
+10 tiles per render thread, split into near-squares (square tiles
+minimize boundary crossings, i.e. how many tiles each shape overlaps).
+These are *not* horizontal bands: each tile has both X and Y bounds.
+
+#+INCLUDE: "Paint tiles.svg" export html
+
+Painting is work-stolen, not pre-assigned. All tile tasks go onto a
+shared =ForkJoinPool= (sized to 75% of CPU threads by default, at most
+cores − 1, so one thread stays free for the rest of the system;
+changeable at runtime via =setNumRenderThreads(int)=). A worker that
+finishes a cheap tile immediately pulls the next queued task — another
+tile (of this or an adjacent frame's pass), a transform chunk, a sort
+piece — so cores never idle behind a busy thread.
+
+Each tile task:
+
+1. *Clear tile*: fill its rectangle with background color
+2. *Paint shapes*: rasterize the tile's bin, back-to-front, clipping
+   at tile bounds
+
+Both operations happen within the same task, so clearing always
+completes before painting on that tile. Parallel clearing across
+disjoint tiles maximizes memory bandwidth utilization.
+
+Each tile renders through a =SegmentRenderingContext= — a view of the
+frame context carrying the tile's X/Y bounds and a =Graphics2D=
+pre-clipped to the tile rectangle for thread-safe text and
+anti-aliased drawing. (The class name predates the tile grid; a
+"segment" is now a tile.) Mouse hit detection happens during painting,
+before clipping.
+
+A =CountDownLatch= tracks completion of all the pass's tiles — but the
+render thread does *not* wait for it here. The latch is awaited one
+pass later, during the flush (see [[#phase-5-flush-completed-passes][Phase 5]]).
+
+** Phase 5: Flush completed passes
+:PROPERTIES:
+:CUSTOM_ID: phase-5-flush-completed-passes
+:END:
+
+Before each new transform, the render thread flushes paint passes that
+have completed. For each flushed pass:
+
+1. Await its tile latch (blocks only when correctness demands it —
+   transform of pass P may not start before paint of pass P-3 finished)
+2. *Combine mouse results*: during painting, each tile tracked which
+   shape is under the mouse cursor. Since all tiles paint the same
+   back-to-front order, they should all report the same hit; the first
+   non-null result wins:
+
+#+BEGIN_SRC java
+for (SegmentRenderingContext ctx : segmentContexts) {
+    if (ctx.getSegmentMouseHit() != null) {
+        context.setCurrentObjectUnderMouseCursor(ctx.getSegmentMouseHit());
+        return;
+    }
+}
+#+END_SRC
+
+   In stereo mode this only runs for the eye whose viewport actually
+   contains the cursor — each eye sees a different camera position, so
+   combining for the wrong eye would overwrite a valid hit with null.
+3. If this pass completed a frame, deposit the frame into the
+   presentation mailbox (see [[#software-pipeline][Software pipeline]]). The render
+   thread never blocks on the display.
+
+Passes that finished painting are flushed without any blocking, so
+completed frames reach the mailbox as early as possible.
+
+** Phase 6: Present frame
+:PROPERTIES:
+:CUSTOM_ID: phase-6-present-frame
+:END:
+
+The =e3d-present= thread takes the newest mailbox frame (dropping any
+unshown older frame) and copies its =BufferedImage= to the screen using
+[[https://cr.openjdk.org/~iris/se/17/latestSpec/api/java.desktop/java/awt/image/BufferStrategy.html][BufferStrategy]] for tear-free page-flipping:
+
+#+BEGIN_SRC java
+do {
+    Graphics2D g = bufferStrategy.getDrawGraphics();
+    g.drawImage(context.bufferedImage, 0, 0, null);
+    g.dispose();
+} while (bufferStrategy.contentsRestored());
+
+// framebuffer released for reuse here
+bufferStrategy.show();
+Toolkit.getDefaultToolkit().sync();
+#+END_SRC
+
+The frame's buffer is released for reuse right after the =drawImage=
+loop — =show()= and =sync()= touch only the BufferStrategy's own back
+buffer and the X connection, and at high resolutions they cost more
+than the draw itself, so the next frame's painters don't wait for them.
+
+*What is double-buffering?*
+
+Without double-buffering, the screen updates while pixels are being
+written. This causes *screen tearing* — visible horizontal splits where
+the top of the frame shows old content while the bottom shows new.
+
+#+INCLUDE: "Double buffering.svg" export html
+
+Double-buffering uses two pixel buffers:
+- *Back buffer*: Where rendering happens (offscreen, invisible)
+- *Front buffer*: What's currently displayed on screen
+
+When rendering completes, the buffers *swap* in one atomic operation.
+The viewer always sees complete frames, never partial updates.
+
+The =do-while= loop handles the case where the OS recreates the back
+buffer (common during window resizing). Since our offscreen
+=BufferedImage= still has the correct pixels, we only need to re-blit,
+not re-render.
+
+* Frame listeners and smart repaint skipping
+:PROPERTIES:
+:CUSTOM_ID: frame-listeners
+:ID:       e360a877-cca6-4cba-a9a4-ea40b0f1a183
+:END:
+
+A *FrameListener* is a callback that runs custom logic before each potential
+frame. Think of it as your "per-frame hook" — the engine calls all registered
+listeners, giving them a chance to update animations, physics, or game logic.
+
+** Registering a frame listener
+
+Use [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#addFrameListener(eu.svjatoslav.aukio.e3d.gui.FrameListener)][addFrameListener()]] to register your callback:
+
+#+BEGIN_SRC java
+// This is how you register a frame listener
+viewPanel.addFrameListener((panel, deltaMs) -> {
+    // Example: simple animation listener
+    double rotationSpeed = 1.0;  // radians per second
+    shape.rotate(rotationSpeed * deltaMs / 1000.0);  // Framerate-independent rotation
+    return true;  // Request repaint (shape moved)
+});
+#+END_SRC
+
+The listener receives two parameters:
+- =panel=: The ViewPanel that's rendering
+- =deltaMs=: Milliseconds since last frame (for framerate-independent animation)
+
+The return value controls whether the frame gets rendered:
+- =true=: "Something changed — repaint the screen"
+- =false=: "Nothing changed — can skip this frame"
+
+** Frame skipping optimization
+
+The engine avoids unnecessary rendering. A frame is skipped when:
+- *All listeners return false* (nothing changed in your scene)
+- *Camera did not move* (built-in Camera listener returns false once
+  the camera comes to rest)
+- *No resize or repaint requests*
+
+This means a static scene with no animations consumes almost zero CPU.
+The render thread keeps running (checking for changes), but actual pixel
+rendering is skipped entirely. Skipped frames still flush any pending
+paint passes from earlier frames, so in-flight frames always reach the
+screen.
+
+Two exceptions force a frame regardless of listeners:
+- *Unlimited (benchmark) mode* (=targetFPS <= 0=) renders continuously,
+  so the measured rate reflects maximum throughput
+- An explicit repaint request (resize, stereo toggle,
+  =repaintDuringNextViewUpdate()=, etc.)
+
+#+BEGIN_SRC java
+// Example: listener that only requests repaint when needed
+viewPanel.addFrameListener((panel, deltaMs) -> {
+    if (gameState.hasUpdates()) {
+        gameState.processUpdates();
+        return true;   // Only repaint when game state actually changed
+    }
+    return false;      // Skip frame — nothing to update
+});
+#+END_SRC
+
+** Built-in listeners
+
+The engine registers these listeners by default:
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/Camera.html][Camera]] — applies movement velocity and friction each frame, and
+  returns true when the camera actually moved (more than a small
+  threshold), i.e. while the user is actively navigating
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.html][InputManager]] — processes mouse/keyboard events
+
+When the camera stops moving and you release all keys, the Camera listener
+returns false. If your custom listeners also return false, the frame is
+skipped until something changes.
+
+* Rendering context
+:PROPERTIES:
+:CUSTOM_ID: rendering-context
+:END:
+
+The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] holds all state for rendering into one framebuffer:
+the pixel buffer, projection parameters, and per-frame bookkeeping.
+
+| Field | Purpose |
+|-------+---------|
+| =pixels[]= | Raw pixel buffer (int[] in RGB format) |
+| =bufferedImage= | Java2D wrapper around pixels |
+| =graphics= | Graphics2D for text, lines, shapes |
+| =width=, =height= | Full framebuffer dimensions |
+| =centerCoordinate= | Screen center of the active viewport (for projection) |
+| =projectionScale= | Perspective scale factor, derived from viewport width. Mutable: each stereo eye sets its own |
+| =renderMinX=, =renderMaxX= | X bounds of the active viewport or tile |
+| =renderMinY=, =renderMaxY= | Y bounds (full height on the frame context, tile bounds on segment views) |
+| =stereoEye=, =stereoViewportWidth=, =stereoViewportOffsetX= | Which eye this pass renders and where its viewport sits in the buffer |
+| =tilesX=, =tilesY=, =viewportCount=, =numRenderSegments= | Tile grid geometry (segments = tilesX × tilesY × viewports) |
+| =frustum= | View frustum for culling, rebuilt each pass from camera state |
+| =frameNumber= | Per-context frame counter |
+| =transformCycleId= | Globally unique transform-cycle id, safe key for per-cycle memoization |
+| =vertexSlot= | Projection buffer slot (0–2) this pass transforms into |
+
+** Triple-buffered frame contexts
+
+The engine keeps /three/ frame contexts, cycled by frame parity. While
+frame N is still being painted from one buffer, frame N+1 already
+transforms into the next — paint threads never idle waiting for the
+transform phase, and vice versa. A per-buffer *present gate* prevents
+painting frame F+3 into a buffer the present thread is still blitting
+frame F from.
+
+All three contexts are recreated together when the window is resized,
+when the tile grid changes (render thread count), or when stereo mode
+is toggled. Otherwise they are reused — =prepareForNewFrameRendering()=
+just resets per-frame state like mouse tracking.
+
+** Per-pass copies
+
+Each render pass (one per eye in stereo) works on a private /copy/ of
+the frame context. The copy shares the pixel buffer, graphics and
+services, but owns the projection fields (center, scale, viewport,
+vertex slot), so the next pass's setup cannot disturb a pass whose
+transform or paint is still in flight.
+
+Consequence for engine code: per-frame mutable state must be allocated
+eagerly on the frame context. Anything created lazily inside a pass
+lands on the throwaway copy and is lost.
+
+** Tile segment views
+
+Each paint tile gets a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/SegmentRenderingContext.html][SegmentRenderingContext]],
+a view that shares the framebuffer with its parent but carries its own
+X/Y tile bounds and a pre-clipped =Graphics2D= for thread-safe text and
+shape drawing. Mouse hits are tracked per tile and combined after all
+tiles finish painting.
diff --git a/Documentation/SDF textures/SDF concept.svg b/Documentation/SDF textures/SDF concept.svg
new file mode 100644 (file)
index 0000000..bc1b750
--- /dev/null
@@ -0,0 +1,81 @@
+<svg viewBox="0 0 640 430" width="640" height="430" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="2" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+  </defs>
+
+  <rect width="640" height="430" fill="#061018"/>
+
+  <text x="320" y="32" fill="#40b0d0" font-size="16" font-family="monospace" text-anchor="middle" filter="url(#glow)">Why a distance field, not a bitmap</text>
+  <text x="320" y="50" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">one glyph edge, magnified 8x - stored coverage vs re-derived coverage</text>
+
+  <!-- LEFT: stored bitmap coverage -->
+  <text x="160" y="86" fill="#FF8833" font-size="12" font-family="monospace" text-anchor="middle" filter="url(#glow)">BITMAP coverage</text>
+  <text x="160" y="101" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">what you store is what you get</text>
+
+  <!-- blocky stair edge -->
+  <g stroke="none">
+    <rect x="60"  y="120" width="40" height="40" fill="#39FF14" opacity="0.85"/>
+    <rect x="100" y="120" width="40" height="40" fill="#39FF14" opacity="0.5"/>
+    <rect x="60"  y="160" width="40" height="40" fill="#39FF14" opacity="0.85"/>
+    <rect x="100" y="160" width="40" height="40" fill="#39FF14" opacity="0.85"/>
+    <rect x="140" y="160" width="40" height="40" fill="#39FF14" opacity="0.4"/>
+    <rect x="60"  y="200" width="40" height="40" fill="#39FF14" opacity="0.85"/>
+    <rect x="100" y="200" width="40" height="40" fill="#39FF14" opacity="0.85"/>
+    <rect x="140" y="200" width="40" height="40" fill="#39FF14" opacity="0.85"/>
+    <rect x="180" y="200" width="40" height="40" fill="#39FF14" opacity="0.3"/>
+    <rect x="60"  y="240" width="40" height="40" fill="#39FF14" opacity="0.85"/>
+    <rect x="100" y="240" width="40" height="40" fill="#39FF14" opacity="0.85"/>
+    <rect x="140" y="240" width="40" height="40" fill="#39FF14" opacity="0.85"/>
+    <rect x="180" y="240" width="40" height="40" fill="#39FF14" opacity="0.7"/>
+  </g>
+  <g stroke="#1a3a4a" stroke-width="1" fill="none">
+    <rect x="60" y="120" width="200" height="160"/>
+    <line x1="100" y1="120" x2="100" y2="280"/>
+    <line x1="140" y1="120" x2="140" y2="280"/>
+    <line x1="180" y1="120" x2="180" y2="280"/>
+    <line x1="220" y1="120" x2="220" y2="280"/>
+    <line x1="60" y1="160" x2="260" y2="160"/>
+    <line x1="60" y1="200" x2="260" y2="200"/>
+    <line x1="60" y1="240" x2="260" y2="240"/>
+  </g>
+  <text x="160" y="305" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">texel grid IS the resolution limit:</text>
+  <text x="160" y="318" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">edges stair-step, curves become blocks</text>
+
+  <!-- RIGHT: distance field -->
+  <text x="480" y="86" fill="#39FF14" font-size="12" font-family="monospace" text-anchor="middle" filter="url(#glow)">SDF: distance to edge</text>
+  <text x="480" y="101" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">a smooth field - the edge is re-derived per pixel</text>
+
+  <!-- smooth edge line through a gradient field -->
+  <defs>
+    <linearGradient id="sdfGrad" x1="0" y1="0" x2="1" y2="1">
+      <stop offset="0" stop-color="#39FF14" stop-opacity="0.85"/>
+      <stop offset="0.42" stop-color="#39FF14" stop-opacity="0.55"/>
+      <stop offset="0.55" stop-color="#39FF14" stop-opacity="0.25"/>
+      <stop offset="0.7" stop-color="#39FF14" stop-opacity="0.06"/>
+      <stop offset="1" stop-color="#39FF14" stop-opacity="0"/>
+    </linearGradient>
+  </defs>
+  <rect x="380" y="120" width="200" height="160" fill="url(#sdfGrad)"/>
+  <rect x="380" y="120" width="200" height="160" fill="none" stroke="#1a3a4a" stroke-width="1"/>
+  <!-- the re-derived edge: crisp at any zoom -->
+  <line x1="420" y1="280" x2="530" y2="120" stroke="#40b0d0" stroke-width="2" filter="url(#glow)"/>
+  <text x="500" y="270" fill="#40b0d0" font-size="9" font-family="monospace">edge recovered at</text>
+  <text x="500" y="282" fill="#40b0d0" font-size="9" font-family="monospace">display resolution</text>
+
+  <!-- distance annotations -->
+  <text x="410" y="150" fill="#999" font-size="9" font-family="monospace">d &lt; 0: inside ink</text>
+  <text x="520" y="140" fill="#999" font-size="9" font-family="monospace">d &gt; 0: outside</text>
+  <text x="455" y="205" fill="#40b0d0" font-size="9" font-family="monospace" transform="rotate(-52 455 205)">d = 0: the edge</text>
+
+  <!-- bottom takeaway -->
+  <rect x="40" y="340" width="560" height="64" rx="6" fill="rgba(64,176,208,0.06)" stroke="#40b0d0" stroke-width="1"/>
+  <text x="320" y="364" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#glow)">coverage = (127.5 - d) * aaK + 128</text>
+  <text x="320" y="382" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">aaK scales the gradient window to the pixel footprint - the same 16x32 texel field</text>
+  <text x="320" y="395" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">serves a 4-pixel label and a full-screen billboard</text>
+</svg>
diff --git a/Documentation/SDF textures/SDF glyph pipeline.svg b/Documentation/SDF textures/SDF glyph pipeline.svg
new file mode 100644 (file)
index 0000000..09473d6
--- /dev/null
@@ -0,0 +1,88 @@
+<svg viewBox="0 0 640 470" width="640" height="470" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="2" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+    <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
+      <path d="M 0 0 L 10 5 L 0 10 z" fill="#40b0d0"/>
+    </marker>
+  </defs>
+
+  <rect width="640" height="470" fill="#061018"/>
+
+  <text x="320" y="30" fill="#40b0d0" font-size="16" font-family="monospace" text-anchor="middle" filter="url(#glow)">Glyph field generation (SdfGlyphCache)</text>
+  <text x="320" y="48" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">once per character, then cached - stamping is a block copy</text>
+
+  <!-- step 1: hi-res rasterize -->
+  <rect x="40" y="70" width="170" height="84" rx="6" fill="rgba(57,255,20,0.07)" stroke="#39FF14" stroke-width="1.5"/>
+  <text x="125" y="90" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">1. rasterize glyph</text>
+  <text x="125" y="106" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">Liberation Mono Bold, AA on</text>
+  <text x="125" y="119" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">64x128 px (4x supersample)</text>
+  <text x="125" y="132" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">font auto-sized to fit cell</text>
+  <text x="125" y="148" fill="#c05088" font-size="9" font-family="monospace" text-anchor="middle">advance 0.6em (Courier-compat)</text>
+
+  <line x1="210" y1="112" x2="240" y2="112" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr)"/>
+
+  <!-- step 2: EDT -->
+  <rect x="245" y="70" width="170" height="84" rx="6" fill="rgba(64,176,208,0.07)" stroke="#40b0d0" stroke-width="1.5"/>
+  <text x="330" y="90" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle">2. distance transform</text>
+  <text x="330" y="106" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">exact Euclidean EDT</text>
+  <text x="330" y="119" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">(Felzenszwalb-Huttenlocher,</text>
+  <text x="330" y="132" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">two separable 1-D passes)</text>
+  <text x="330" y="148" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">dOut to ink, dIn to background</text>
+
+  <line x1="415" y1="112" x2="445" y2="112" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr)"/>
+
+  <!-- step 3: signed + clamp + downsample -->
+  <rect x="450" y="70" width="160" height="96" rx="6" fill="rgba(255,136,51,0.07)" stroke="#FF8833" stroke-width="1.5"/>
+  <text x="530" y="88" fill="#FF8833" font-size="11" font-family="monospace" text-anchor="middle">3. sign, clamp, average</text>
+  <text x="530" y="104" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">signed = dOut - dIn</text>
+  <text x="530" y="117" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">clamp to +/- 2 texels spread</text>
+  <text x="530" y="130" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">average FIELD down to 16x32</text>
+  <text x="530" y="148" fill="#c05088" font-size="9" font-family="monospace" text-anchor="middle">(averaging the field, not coverage,</text>
+  <text x="530" y="160" fill="#c05088" font-size="9" font-family="monospace" text-anchor="middle">preserves the edge position)</text>
+
+  <!-- down to mask encoding -->
+  <line x1="530" y1="180" x2="530" y2="196" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr)"/>
+
+  <!-- encoding bar -->
+  <rect x="120" y="200" width="420" height="54" rx="6" fill="rgba(192,80,136,0.07)" stroke="#c05088" stroke-width="1.5"/>
+  <text x="330" y="220" fill="#c05088" font-size="11" font-family="monospace" text-anchor="middle">mask encoding (per texel, 0..255)</text>
+  <text x="330" y="236" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">0 = deep inside ink    127.5 = the edge    255 = far outside</text>
+
+  <!-- gradient strip -->
+  <defs>
+    <linearGradient id="maskBar" x1="0" y1="0" x2="1" y2="0">
+      <stop offset="0" stop-color="#000000"/>
+      <stop offset="0.5" stop-color="#808080"/>
+      <stop offset="1" stop-color="#ffffff"/>
+    </linearGradient>
+  </defs>
+  <rect x="170" y="242" width="320" height="8" fill="url(#maskBar)"/>
+
+  <!-- consumers -->
+  <line x1="240" y1="254" x2="240" y2="286" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr)"/>
+  <line x1="430" y1="254" x2="430" y2="286" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr)"/>
+
+  <rect x="120" y="290" width="240" height="66" rx="6" fill="rgba(57,255,20,0.06)" stroke="#39FF14" stroke-width="1"/>
+  <text x="240" y="310" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">TextCanvas.putChar</text>
+  <text x="240" y="326" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">stamps the cached 16x32 mask</text>
+  <text x="240" y="339" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">into the cell position of sdfMask</text>
+
+  <rect x="380" y="290" width="190" height="66" rx="6" fill="rgba(32,112,192,0.07)" stroke="#2070c0" stroke-width="1"/>
+  <text x="475" y="310" fill="#2070c0" font-size="11" font-family="monospace" text-anchor="middle">three texture layers</text>
+  <text x="475" y="326" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">sdfMask: glyph SHAPES (bilinear)</text>
+  <text x="475" y="339" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">sdfForeground + primary: colors</text>
+
+  <text x="60" y="392" fill="#999" font-size="9" font-family="monospace">why sans-serif bold: Courier's serifs and hairline strokes decay into unresolvable noise</text>
+  <text x="60" y="405" fill="#999" font-size="9" font-family="monospace">when the distance field is minified - uniform sturdy strokes survive</text>
+
+  <!-- why not mipmap note -->
+  <rect x="60" y="418" width="520" height="40" rx="6" fill="rgba(255,68,68,0.05)" stroke="#FF4444" stroke-width="1"/>
+  <text x="320" y="434" fill="#FF4444" font-size="10" font-family="monospace" text-anchor="middle">NO mipmaps on the mask: the edge gradient spans ~2 texels,</text>
+  <text x="320" y="448" fill="#FF4444" font-size="10" font-family="monospace" text-anchor="middle">half-res masks melt glyph edges - minification is analytic instead</text>
+</svg>
diff --git a/Documentation/SDF textures/SDF minification.svg b/Documentation/SDF textures/SDF minification.svg
new file mode 100644 (file)
index 0000000..95cce51
--- /dev/null
@@ -0,0 +1,71 @@
+<svg viewBox="0 0 640 420" width="640" height="420" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="2" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+    <marker id="arr2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
+      <path d="M 0 0 L 10 5 L 0 10 z" fill="#40b0d0"/>
+    </marker>
+  </defs>
+
+  <rect width="640" height="420" fill="#061018"/>
+
+  <text x="320" y="30" fill="#40b0d0" font-size="16" font-family="monospace" text-anchor="middle" filter="url(#glow)">Minification: analytic coverage window</text>
+  <text x="320" y="48" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">one screen pixel covering many texels still resolves the edge correctly</text>
+
+  <!-- screen pixel footprint diagram -->
+  <rect x="50" y="70" width="270" height="215" rx="6" fill="rgba(57,255,20,0.05)" stroke="#39FF14" stroke-width="1"/>
+  <text x="185" y="90" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">a screen pixel on the texture</text>
+
+  <!-- texel grid 6x5 -->
+  <g stroke="#1a3a4a" stroke-width="1">
+    <rect x="70" y="105" width="180" height="125"/>
+    <line x1="100" y1="105" x2="100" y2="230"/>
+    <line x1="130" y1="105" x2="130" y2="230"/>
+    <line x1="160" y1="105" x2="160" y2="230"/>
+    <line x1="190" y1="105" x2="190" y2="230"/>
+    <line x1="220" y1="105" x2="220" y2="230"/>
+    <line x1="70" y1="130" x2="250" y2="130"/>
+    <line x1="70" y1="155" x2="250" y2="155"/>
+    <line x1="70" y1="180" x2="250" y2="180"/>
+    <line x1="70" y1="205" x2="250" y2="205"/>
+  </g>
+  <!-- glyph edge crossing the grid -->
+  <line x1="90" y1="230" x2="230" y2="105" stroke="#c05088" stroke-width="2"/>
+  <!-- pixel footprint box -->
+  <rect x="115" y="130" width="90" height="75" fill="rgba(255,136,51,0.10)" stroke="#FF8833" stroke-width="2" stroke-dasharray="5 3"/>
+  <text x="160" y="245" fill="#FF8833" font-size="9" font-family="monospace" text-anchor="middle">footprint: many texels per pixel</text>
+  <text x="185" y="262" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">a bitmap would average to mush or alias;</text>
+  <text x="185" y="275" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">the field still knows where the edge is</text>
+
+  <!-- formula flow -->
+  <rect x="350" y="70" width="250" height="60" rx="6" fill="rgba(64,176,208,0.07)" stroke="#40b0d0" stroke-width="1.5"/>
+  <text x="475" y="92" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle">per-axis footprint from UV gradients</text>
+  <text x="475" y="106" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">footX = |dUV/dx|, footY = |dUV/dy|</text>
+  <text x="475" y="119" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">window follows the SHARPEST axis</text>
+
+  <line x1="475" y1="130" x2="475" y2="150" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr2)"/>
+
+  <rect x="350" y="155" width="250" height="44" rx="6" fill="rgba(192,80,136,0.07)" stroke="#c05088" stroke-width="1.5"/>
+  <text x="475" y="173" fill="#c05088" font-size="10" font-family="monospace" text-anchor="middle">aaK = 2*spread / texelsPerPixel</text>
+  <text x="475" y="188" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">widens the coverage window as pixels grow</text>
+
+  <line x1="475" y1="199" x2="475" y2="219" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr2)"/>
+
+  <rect x="350" y="224" width="250" height="66" rx="6" fill="rgba(255,136,51,0.07)" stroke="#FF8833" stroke-width="1.5"/>
+  <text x="475" y="242" fill="#FF8833" font-size="10" font-family="monospace" text-anchor="middle">perceptual corrections (minified text</text>
+  <text x="475" y="254" fill="#FF8833" font-size="10" font-family="monospace" text-anchor="middle">else reads as gray haze):</text>
+  <text x="475" y="270" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">SHARPEN x2: sub-pixel window kills halo</text>
+  <text x="475" y="283" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">coverage gamma &lt; 1: stem darkening</text>
+
+  <!-- result strip -->
+  <rect x="60" y="310" width="520" height="90" rx="6" fill="rgba(57,255,20,0.05)" stroke="#39FF14" stroke-width="1"/>
+  <text x="320" y="332" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#glow)">result: graceful degradation</text>
+  <text x="320" y="350" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">magnified: edges re-derived at display resolution - razor sharp</text>
+  <text x="320" y="365" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">minified: coverage fades smoothly to clean gray, no crawling aliases</text>
+  <text x="320" y="380" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">angled: the uncompressed axis keeps its sharpness</text>
+</svg>
diff --git a/Documentation/SDF textures/glyph-sdf-S.png b/Documentation/SDF textures/glyph-sdf-S.png
new file mode 100644 (file)
index 0000000..ecc1de5
Binary files /dev/null and b/Documentation/SDF textures/glyph-sdf-S.png differ
diff --git a/Documentation/SDF textures/index.org b/Documentation/SDF textures/index.org
new file mode 100644 (file)
index 0000000..ff4cf78
--- /dev/null
@@ -0,0 +1,252 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: SDF Textures - Aukio 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+
+[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
+
+* What SDF textures are
+:PROPERTIES:
+:CUSTOM_ID: what-sdf-is
+:END:
+
+A regular texture stores *coverage*: each texel says "this much ink
+here". That is a photocopy of the glyph — resample it (magnify, minify,
+view at an angle) and the stored pixels blur or alias, because the
+information about /where the edge is/ was thrown away when the glyph
+was rasterized.
+
+A *signed distance field* (SDF) texture stores something smarter: per
+texel, the *distance to the nearest edge* — negative inside the ink,
+positive outside, zero exactly on the boundary. The rasterizer then
+re-derives coverage per screen pixel from this smooth field. The edge
+position survives resampling because the field around it is linear —
+bilinear interpolation of a linear ramp is exact.
+
+#+ATTR_HTML: :width 640
+[[file:SDF concept.svg]]
+
+In Aukio 3D the mask is a grayscale field in =texture.sdfMask=:
+
+- =0= — deep inside the ink
+- =127.5= — exactly on the edge
+- =255= — far outside any glyph
+
+The gradient spans only =SPREAD_TEXELS = 2.0= texels around the edge —
+that narrow band is all the rasterizer needs.
+
+Here is a real field, dumped straight from =SdfGlyphCache= (glyph "S",
+16x32 texels, upscaled 12x with nearest so you can see the texels):
+
+[[file:glyph-sdf-S.png]]
+
+Dark inside the strokes, bright outside, and a smooth gray ramp exactly
+two texels wide around the contour.
+
+* Generating glyph fields
+:PROPERTIES:
+:CUSTOM_ID: glyph-pipeline
+:END:
+
+=SdfGlyphCache= generates each character's distance field once and
+caches it in a =ConcurrentHashMap=; stamping a glyph into a canvas is
+then just a block copy.
+
+#+ATTR_HTML: :width 640
+[[file:SDF glyph pipeline.svg]]
+
+The steps:
+
+1. *Rasterize* the glyph with AWT at 4x the cell size (64x128 pixels)
+   with anti-aliasing on, using Liberation Mono Bold (metric-compatible
+   with Courier New, so the cell grid is unchanged). The font size is
+   auto-shrunk until the widest glyph fits the scratch without clipping
+   — a clipped glyph would corrupt the distance field at the cell edge.
+2. *Distance transform*: an exact Euclidean distance transform
+   (Felzenszwalb & Huttenlocher, two separable 1-D passes over parabola
+   envelopes) is run twice — once for distance to nearest ink pixel,
+   once for distance to nearest background pixel.
+3. *Sign, clamp, average*: signed distance = dOut - dIn, clamped to
+   +/-2 texels of spread, then the *field* (not coverage) is averaged
+   down to the 16x32 cell resolution. Averaging the field preserves the
+   edge position; averaging coverage would not.
+
+The font choice matters: Courier's serifs and hairline strokes decay
+into unresolvable noise when the field is minified. A uniform-stroke
+bold sans-serif survives.
+
+* The rendering path
+:PROPERTIES:
+:CUSTOM_ID: render-path
+:END:
+
+When =texture.isSdf()= is true (an =sdfMask= is attached),
+=TexturedTriangle.paintSdf= takes over. Three layers are involved:
+
+| Layer            | Contents                    | Sampling |
+|------------------+-----------------------------+----------|
+| =sdfMask=        | glyph shapes (the field)    | bilinear |
+| =sdfForeground=  | ink color, flat per cell    | nearest  |
+| =primaryBitmap=  | background color, per cell  | nearest  |
+
+Per screen pixel:
+
+1. Sample the mask bilinearly (fixed-point) -> distance =d=.
+2. Convert to coverage: =cov = (127.5 - d) * aaK + 128=, clamped to
+   [0, 256]. =aaK= scales the 2-texel gradient window to the current
+   pixel footprint (see next section).
+3. Blend: =pixel = bg * (1 - cov) + fg * cov=.
+
+Perspective-correct interpolation applies to SDF triangles exactly as
+it does to regular textured triangles — same affine-sufficiency test,
+same subdivided correction. See
+[[file:../Perspective correct textures/index.org][Perspective-correct
+textures]]; only the per-pixel sampling differs.
+
+* Minification without mipmaps
+:PROPERTIES:
+:CUSTOM_ID: minification
+:END:
+
+*There is deliberately no mipmap chain for SDF layers.* A distance
+field's edge gradient spans ~2 texels; a half-resolution mask melts the
+glyph edges. Worse, the two triangles of a rectangle cross mip
+thresholds at slightly different distances, producing a hard diagonal
+quality split and sudden blur steps while dollying (observed in
+practice).
+
+Minification is instead handled *analytically*: the coverage window is
+widened by the screen-space pixel footprint, giving area-correct
+coverage straight from the primary field.
+
+#+ATTR_HTML: :width 640
+[[file:SDF minification.svg]]
+
+The footprint is computed per axis from the screen-space UV gradients —
+=text on an angled plane is minified mostly along one axis=, and an
+isotropic average would blur the axis that still has resolution to
+spare. The coverage window follows the sharpest axis.
+
+Area-correct coverage alone reads as a low-contrast gray haze, so two
+perceptual corrections (A/B-tuned on far + angled text) kick in under
+minification:
+
+- *Sharpening* (=SDF_SHARPEN=, default 2): narrows the coverage window
+  below one pixel — kills the haze halo at the cost of slight shimmer.
+- *Coverage gamma* (< 1, automatic from the footprint): darkens stems
+  like a small-size font rasterizer, keeping thin strokes present.
+
+Real output, rendered headlessly through the [[file:../index.org::#snapshot][Snapshot tool]]:
+
+Magnified — edges re-derived at display resolution, razor sharp:
+
+[[file:sdf-near.png]]
+
+At moderate distance:
+
+[[file:sdf-mid.png]]
+
+Far away — small but clean, fading to gray instead of disintegrating
+into aliases (right: 4x nearest zoom of the center):
+
+[[file:sdf-far.png]]
+
+[[file:sdf-far-zoom.png]]
+
+At an oblique angle — foreshortened along one axis, still sharp along
+the other:
+
+[[file:sdf-angled.png]]
+
+* Using it
+:PROPERTIES:
+:CUSTOM_ID: using-sdf
+:END:
+
+*TextCanvas* is the main entry point: a textured rectangle carrying a
+character grid in 3D space. World cell size 8x16 units, texture cell
+16x32 texels (2 texels per world unit).
+
+#+BEGIN_SRC java
+Transform location = new Transform(new Point3D(0, 0, 500));
+TextCanvas canvas = new TextCanvas(location, "Hello, World!",
+        Color.WHITE, Color.BLACK);
+shapeCollection.addShape(canvas);
+
+// blank canvas + cursor writing
+TextCanvas blank = new TextCanvas(location, new TextPointer(10, 40),
+        Color.GREEN, Color.BLACK);
+blank.locate(0, 0);
+blank.print("Line 1");
+blank.locate(1, 0);
+blank.print("Line 2");
+blank.setForegroundColor(Color.RED);   // affects subsequent writes
+blank.setTextColor(Color.CYAN);        // recolors existing ink only
+#+END_SRC
+
+Colors are per-cell: each =putChar= fills the cell's rectangle in the
+background and foreground layers, so one canvas can hold many colors.
+
+*ForwardOrientedTextBlock* renders the same pipeline onto a billboard
+that always faces the camera — for labels that must stay readable from
+any angle:
+
+#+BEGIN_SRC java
+ForwardOrientedTextBlock label = new ForwardOrientedTextBlock(
+        new Point3D(0, -50, 300), 1.0, 2, "Hello, World!", Color.RED);
+shapeCollection.addShape(label);
+#+END_SRC
+
+Real use in the demos: the life demo's help panel (=life_demo/Main.java=
+=createHelpPanel()=) and the axis labels in =CoordinateSystemDemo=.
+
+* Tuning knobs
+:PROPERTIES:
+:CUSTOM_ID: tuning
+:END:
+
+JVM properties (A/B tuning knobs in =TexturedTriangle=):
+
+| Property          | Default | Effect                                    |
+|-------------------+---------+-------------------------------------------|
+| =e3d.sdf.gamma=   | 0 (auto) | fixed coverage gamma; auto derives from footprint |
+| =e3d.sdf.sharpen= | 2       | coverage window narrowing; 1 = pixel-exact |
+| =e3d.sdf.debug=   | false   | prints per-triangle footprints and path decisions to stderr |
+
+* Limitations
+:PROPERTIES:
+:CUSTOM_ID: limitations
+:END:
+
+- *Fixed cell grid*: TextCanvas is monospace by construction (16x32
+  texel cells). Proportional fonts would need a different stamping
+  scheme.
+- *ASCII-oriented cache*: =SdfGlyphCache= measures printable ASCII
+  (33..126) when sizing the font; exotic glyphs may fit worse.
+- *Under extreme minification* text fades to gray by design — that is
+  the correct physical answer (a sub-pixel glyph has no shape left),
+  but it means distant labels are decorative, not readable.
+- *Bandwidth under minification*: sampling the primary field (no mip
+  chain) costs more bandwidth per pixel. Text surfaces are small, so
+  this is the right trade — do not attach SDF masks to huge surfaces.
+
+* Related classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class                       | Role                                             |
+|-----------------------------+--------------------------------------------------|
+| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.html][TextCanvas]]                  | Character grid surface in 3D; owns the layers    |
+| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/SdfGlyphCache.html][SdfGlyphCache]]               | Per-glyph field generation + cache (EDT inside)  |
+| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.html][ForwardOrientedTextBlock]]    | Camera-facing text billboard                     |
+| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]]            | =paintSdf= — the scanline path                   |
+| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html][Texture]]                     | =sdfMask=, =sdfForeground=, =sdfSpreadTexels=    |
+
+*See also:*
+
+- [[file:../Perspective correct textures/][Perspective-correct textures]] — the scanline texture-mapping path
+  that SDF rendering builds on; both paths live in =TexturedTriangle=
+  and share the same interpolated UVs.
+
diff --git a/Documentation/SDF textures/sdf-angled.png b/Documentation/SDF textures/sdf-angled.png
new file mode 100644 (file)
index 0000000..34814bf
Binary files /dev/null and b/Documentation/SDF textures/sdf-angled.png differ
diff --git a/Documentation/SDF textures/sdf-far-zoom.png b/Documentation/SDF textures/sdf-far-zoom.png
new file mode 100644 (file)
index 0000000..f0c0a44
Binary files /dev/null and b/Documentation/SDF textures/sdf-far-zoom.png differ
diff --git a/Documentation/SDF textures/sdf-far.png b/Documentation/SDF textures/sdf-far.png
new file mode 100644 (file)
index 0000000..64898a2
Binary files /dev/null and b/Documentation/SDF textures/sdf-far.png differ
diff --git a/Documentation/SDF textures/sdf-mid.png b/Documentation/SDF textures/sdf-mid.png
new file mode 100644 (file)
index 0000000..ba8ed3e
Binary files /dev/null and b/Documentation/SDF textures/sdf-mid.png differ
diff --git a/Documentation/SDF textures/sdf-near.png b/Documentation/SDF textures/sdf-near.png
new file mode 100644 (file)
index 0000000..1d493a1
Binary files /dev/null and b/Documentation/SDF textures/sdf-near.png differ
diff --git a/Documentation/Shading/Ambient light comparison.svg b/Documentation/Shading/Ambient light comparison.svg
new file mode 100644 (file)
index 0000000..ce07a00
--- /dev/null
@@ -0,0 +1,51 @@
+<svg viewBox="0 0 640 290" width="640" height="290" xmlns="http://www.w3.org/2000/svg">
+<defs>
+  <filter id="glows"><feGaussianBlur stdDeviation="1.5" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+<mask id="imagine-text-gaps-2vose8" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="640" height="290" fill="white"/><rect x="261.183349609375" y="6.583333969116211" width="117.63333129882812" height="20.91666603088379" fill="black" rx="2"/><rect x="110.16667175292969" y="26.25" width="419.6666564941406" height="15.083333015441895" fill="black" rx="2"/><rect x="53.083335876464844" y="223.25" width="83.83333587646484" height="15.083333015441895" fill="black" rx="2"/><rect x="54.900001525878906" y="237.6666717529297" width="80.19999694824219" height="16.25" fill="black" rx="2"/><rect x="35.608333587646484" y="252.4166717529297" width="118.78333282470703" height="13.916666984558105" fill="black" rx="2"/><rect x="257.9583435058594" y="223.25" width="100.08333587646484" height="15.083333015441895" fill="black" rx="2"/><rect x="273.9166564941406" y="237.6666717529297" width="68.16666793823242" height="16.25" fill="black" rx="2"/><rect x="267.875" y="252.4166717529297" width="80.25" height="13.916666984558105" fill="black" rx="2"/><rect x="463.8333435058594" y="223.25" width="116.33333587646484" height="15.083333015441895" fill="black" rx="2"/><rect x="487.9166564941406" y="237.6666717529297" width="68.16666793823242" height="16.25" fill="black" rx="2"/><rect x="477.058349609375" y="252.4166717529297" width="89.88333129882812" height="13.916666984558105" fill="black" rx="2"/><rect x="161.86666870117188" y="267.4166564941406" width="316.26666259765625" height="13.916666984558105" fill="black" rx="2"/></mask></defs>
+<rect width="640" height="280" fill="#061018" style="fill:rgb(6, 16, 24);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+
+<text x="320" y="22" fill="#ccc" font-size="14" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)" style="fill:rgb(204, 204, 204);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:14px;font-weight:700;text-anchor:middle;dominant-baseline:auto">Ambient Light</text>
+<text x="320" y="37" fill="#3a4a5a" font-size="9" font-family="monospace" text-anchor="middle" style="fill:rgb(58, 74, 90);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">base illumination applied to all surfaces equally, regardless of orientation</text>
+
+<line x1="213" y1="42" x2="213" y2="268" stroke="#0c1a26" stroke-width="1.5" mask="url(#imagine-text-gaps-2vose8)" style="fill:rgb(0, 0, 0);stroke:rgb(12, 26, 38);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="427" y1="42" x2="427" y2="268" stroke="#0c1a26" stroke-width="1.5" mask="url(#imagine-text-gaps-2vose8)" style="fill:rgb(0, 0, 0);stroke:rgb(12, 26, 38);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="5" y1="220" x2="208" y2="220" stroke="#0c1a26" stroke-width="1" style="fill:rgb(0, 0, 0);stroke:rgb(12, 26, 38);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="218" y1="220" x2="422" y2="220" stroke="#0c1a26" stroke-width="1" style="fill:rgb(0, 0, 0);stroke:rgb(12, 26, 38);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="432" y1="220" x2="635" y2="220" stroke="#0c1a26" stroke-width="1" style="fill:rgb(0, 0, 0);stroke:rgb(12, 26, 38);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+
+<!-- PANEL 1: No ambient -->
+<circle cx="30" cy="57" r="7" fill="rgba(255,102,0,0.45)" stroke="#FF6600" stroke-width="1.5" style="fill:rgba(255, 102, 0, 0.45);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="30" y1="48" x2="30" y2="42" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="37" y1="51" x2="42" y2="46" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="23" y1="51" x2="18" y2="46" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<polygon points="22,82 102,70 102,200 22,212" fill="rgba(48,160,80,0.68)" stroke="#30a050" stroke-width="1.5" style="fill:rgba(48, 160, 80, 0.68);stroke:rgb(48, 160, 80);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<polygon points="102,70 168,85 168,215 102,200" fill="rgba(5,10,7,0.97)" stroke="#1c2820" stroke-width="1.5" style="fill:rgba(5, 10, 7, 0.97);stroke:rgb(28, 40, 32);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="95" y="234" fill="rgba(208,64,64,0.75)" font-size="9" font-family="monospace" text-anchor="middle" style="fill:rgba(208, 64, 64, 0.75);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Color(0, 0, 0)</text>
+<text x="95" y="249" fill="rgba(208,64,64,0.9)" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)" style="fill:rgba(208, 64, 64, 0.9);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:700;text-anchor:middle;dominant-baseline:auto">✗ pure black</text>
+<text x="95" y="262" fill="#3a4a5a" font-size="8" font-family="monospace" text-anchor="middle" style="fill:rgb(58, 74, 90);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:middle;dominant-baseline:auto">harsh shadows, no depth</text>
+
+<!-- PANEL 2: Default ambient (50,50,50) -->
+<circle cx="243" cy="57" r="7" fill="rgba(255,102,0,0.45)" stroke="#FF6600" stroke-width="1.5" style="fill:rgba(255, 102, 0, 0.45);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="243" y1="48" x2="243" y2="42" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="250" y1="51" x2="255" y2="46" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="236" y1="51" x2="231" y2="46" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<polygon points="235,82 315,70 315,200 235,212" fill="rgba(48,160,80,0.68)" stroke="#30a050" stroke-width="1.5" style="fill:rgba(48, 160, 80, 0.68);stroke:rgb(48, 160, 80);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<polygon points="315,70 381,85 381,215 315,200" fill="rgba(40,75,46,0.75)" stroke="#285c30" stroke-width="1" style="fill:rgba(40, 75, 46, 0.75);stroke:rgb(40, 92, 48);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="308" y="234" fill="#39FF14" font-size="9" font-family="monospace" text-anchor="middle" style="fill:rgb(57, 255, 20);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Color(50, 50, 50)</text>
+<text x="308" y="249" fill="#39FF14" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)" style="fill:rgb(57, 255, 20);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:700;text-anchor:middle;dominant-baseline:auto">✓ balanced</text>
+<text x="308" y="262" fill="#3a4a5a" font-size="8" font-family="monospace" text-anchor="middle" style="fill:rgb(58, 74, 90);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:middle;dominant-baseline:auto">depth preserved</text>
+
+<!-- PANEL 3: Too much ambient (150,150,150) -->
+<circle cx="457" cy="57" r="7" fill="rgba(255,102,0,0.45)" stroke="#FF6600" stroke-width="1.5" style="fill:rgba(255, 102, 0, 0.45);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="457" y1="48" x2="457" y2="42" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="464" y1="51" x2="469" y2="46" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<line x1="450" y1="51" x2="445" y2="46" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<polygon points="449,82 529,70 529,200 449,212" fill="rgba(48,160,80,0.68)" stroke="#30a050" stroke-width="1.5" style="fill:rgba(48, 160, 80, 0.68);stroke:rgb(48, 160, 80);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<polygon points="529,70 595,85 595,215 529,200" fill="rgba(85,145,92,0.72)" stroke="#30a050" stroke-width="1.5" style="fill:rgba(85, 145, 92, 0.72);stroke:rgb(48, 160, 80);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="522" y="234" fill="#FF6600" font-size="9" font-family="monospace" text-anchor="middle" style="fill:rgb(255, 102, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Color(150, 150, 150)</text>
+<text x="522" y="249" fill="#FF6600" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)" style="fill:rgb(255, 102, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:700;text-anchor:middle;dominant-baseline:auto">✗ too flat</text>
+<text x="522" y="262" fill="#3a4a5a" font-size="8" font-family="monospace" text-anchor="middle" style="fill:rgb(58, 74, 90);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:middle;dominant-baseline:auto">no depth contrast</text>
+
+<line x1="20" y1="268" x2="620" y2="268" stroke="#0c1a26" stroke-width="1" mask="url(#imagine-text-gaps-2vose8)" style="fill:rgb(0, 0, 0);stroke:rgb(12, 26, 38);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+<text x="320" y="277" fill="#2a3a4a" font-size="8" font-family="monospace" text-anchor="middle" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:middle;dominant-baseline:auto">lightingManager.setAmbientLight(new Color(50, 50, 50))  ←  default</text>
+</svg>
\ No newline at end of file
diff --git a/Documentation/Shading/Distance attenuation.svg b/Documentation/Shading/Distance attenuation.svg
new file mode 100644 (file)
index 0000000..2edf492
--- /dev/null
@@ -0,0 +1,91 @@
+<svg viewBox="0 0 640 295" width="640" height="295" xmlns="http://www.w3.org/2000/svg">
+<defs>
+  <filter id="glows"><feGaussianBlur stdDeviation="1.5" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+  <marker id="ax" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="5" markerHeight="5" orient="auto">
+    <path d="M 0 0 L 8 4 L 0 8 z" fill="#3a5060"/>
+  </marker>
+</defs>
+<rect width="640" height="295" fill="#061018"/>
+
+<text x="320" y="22" fill="#ccc" font-size="14" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)">Distance Attenuation</text>
+<text x="320" y="37" fill="#3a4a5a" font-size="9" font-family="monospace" text-anchor="middle">light intensity falls off with distance from source</text>
+
+<line x1="400" y1="45" x2="400" y2="282" stroke="#0c1a26" stroke-width="1.5"/>
+
+<!-- Light source -->
+<circle cx="52" cy="110" r="22" fill="rgba(255,102,0,0.06)"/>
+<circle cx="52" cy="110" r="14" fill="rgba(255,102,0,0.2)" stroke="rgba(255,102,0,0.45)" stroke-width="1.5"/>
+<circle cx="52" cy="110" r="6" fill="rgba(255,102,0,0.7)" stroke="#FF6600" stroke-width="1.5"/>
+<line x1="52" y1="92" x2="52" y2="84" stroke="#FF6600" stroke-width="1.1" opacity="0.55"/>
+<line x1="65" y1="97" x2="72" y2="90" stroke="#FF6600" stroke-width="1.1" opacity="0.55"/>
+<line x1="39" y1="97" x2="32" y2="90" stroke="#FF6600" stroke-width="1.1" opacity="0.55"/>
+<line x1="70" y1="110" x2="78" y2="110" stroke="#FF6600" stroke-width="1.1" opacity="0.55"/>
+<line x1="65" y1="123" x2="72" y2="130" stroke="#FF6600" stroke-width="1.1" opacity="0.55"/>
+<text x="52" y="82" fill="#FF6600" font-size="9" font-weight="700" font-family="monospace" text-anchor="middle">Light</text>
+
+<line x1="52" y1="155" x2="380" y2="155" stroke="#1a2a3a" stroke-width="1" stroke-dasharray="4 3"/>
+
+<!-- Surface d=100, att=0.99 -->
+<line x1="65" y1="110" x2="126" y2="110" stroke="#FF6600" stroke-width="1" stroke-dasharray="4 2" opacity="0.40"/>
+<polygon points="128,77 163,77 158,148 133,148" fill="rgba(48,160,80,0.70)" stroke="#30a050" stroke-width="1.5"/>
+<text x="145" y="65" fill="#30a050" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)">0.99</text>
+<line x1="145" y1="152" x2="145" y2="160" stroke="#2a3a4a" stroke-width="1"/>
+<text x="145" y="172" fill="#555" font-size="8" font-family="monospace" text-anchor="middle">d = 100</text>
+
+<!-- Surface d=300, att=0.52 -->
+<line x1="65" y1="110" x2="223" y2="110" stroke="#FF6600" stroke-width="1" stroke-dasharray="4 2" opacity="0.22"/>
+<polygon points="225,77 259,77 255,148 229,148" fill="rgba(48,160,80,0.36)" stroke="rgba(48,160,80,0.65)" stroke-width="1.2"/>
+<text x="242" y="65" fill="rgba(48,160,80,0.8)" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">0.52</text>
+<line x1="242" y1="152" x2="242" y2="160" stroke="#2a3a4a" stroke-width="1"/>
+<text x="242" y="172" fill="#555" font-size="8" font-family="monospace" text-anchor="middle">d = 300</text>
+
+<!-- Surface d=500, att=0.29 -->
+<line x1="65" y1="110" x2="316" y2="110" stroke="#FF6600" stroke-width="1" stroke-dasharray="4 2" opacity="0.10"/>
+<polygon points="318,77 352,77 349,148 321,148" fill="rgba(48,160,80,0.18)" stroke="rgba(48,160,80,0.38)" stroke-width="1"/>
+<text x="335" y="65" fill="rgba(48,160,80,0.55)" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">0.29</text>
+<line x1="335" y1="152" x2="335" y2="160" stroke="#2a3a4a" stroke-width="1"/>
+<text x="335" y="172" fill="#555" font-size="8" font-family="monospace" text-anchor="middle">d = 500</text>
+
+<text x="195" y="192" fill="#2a3a4a" font-size="8" font-family="monospace" text-anchor="middle">← attenuation factor shown above each surface →</text>
+
+<!-- Chart -->
+<text x="513" y="57" fill="#aaa" font-size="9" font-weight="700" font-family="monospace" text-anchor="middle">attenuation vs distance</text>
+<line x1="415" y1="210" x2="630" y2="210" stroke="#2a3a4a" stroke-width="1" marker-end="url(#ax)"/>
+<line x1="415" y1="210" x2="415" y2="67" stroke="#2a3a4a" stroke-width="1" marker-end="url(#ax)"/>
+<text x="622" y="222" fill="#3a5060" font-size="8" font-family="monospace">d</text>
+<text x="408" y="65" fill="#3a5060" font-size="8" font-family="monospace" text-anchor="end">att</text>
+<line x1="411" y1="210" x2="419" y2="210" stroke="#2a3a4a" stroke-width="1"/>
+<text x="408" y="213" fill="#3a5060" font-size="7" font-family="monospace" text-anchor="end">0</text>
+<line x1="411" y1="138" x2="419" y2="138" stroke="#2a3a4a" stroke-width="1"/>
+<line x1="415" y1="138" x2="625" y2="138" stroke="#0d1e2e" stroke-width="1"/>
+<text x="408" y="141" fill="#3a5060" font-size="7" font-family="monospace" text-anchor="end">0.5</text>
+<line x1="411" y1="67" x2="419" y2="67" stroke="#2a3a4a" stroke-width="1"/>
+<line x1="415" y1="67" x2="625" y2="67" stroke="#0d1e2e" stroke-width="1"/>
+<text x="408" y="70" fill="#3a5060" font-size="7" font-family="monospace" text-anchor="end">1.0</text>
+<line x1="450" y1="206" x2="450" y2="214" stroke="#2a3a4a" stroke-width="1"/>
+<line x1="450" y1="67" x2="450" y2="210" stroke="#0d1e2e" stroke-width="1" stroke-dasharray="2 4"/>
+<text x="450" y="222" fill="#3a5060" font-size="7" font-family="monospace" text-anchor="middle">100</text>
+<line x1="520" y1="206" x2="520" y2="214" stroke="#2a3a4a" stroke-width="1"/>
+<line x1="520" y1="67" x2="520" y2="210" stroke="#0d1e2e" stroke-width="1" stroke-dasharray="2 4"/>
+<text x="520" y="222" fill="#3a5060" font-size="7" font-family="monospace" text-anchor="middle">300</text>
+<line x1="590" y1="206" x2="590" y2="214" stroke="#2a3a4a" stroke-width="1"/>
+<line x1="590" y1="67" x2="590" y2="210" stroke="#0d1e2e" stroke-width="1" stroke-dasharray="2 4"/>
+<text x="590" y="222" fill="#3a5060" font-size="7" font-family="monospace" text-anchor="middle">500</text>
+<path d="M 415,67 C 430,67 440,68 450,68 C 468,68 492,100 520,136 C 548,170 575,175 625,178"
+      fill="none" stroke="#FF6600" stroke-width="2" opacity="0.85"/>
+<circle cx="415" cy="67" r="3" fill="#FF6600" opacity="0.70"/>
+<circle cx="450" cy="68" r="3" fill="#FF6600" opacity="0.85"/>
+<circle cx="520" cy="136" r="3" fill="#FF6600" opacity="0.85"/>
+<circle cx="590" cy="169" r="3" fill="#FF6600" opacity="0.85"/>
+<text x="453" y="62" fill="rgba(255,102,0,0.75)" font-size="7" font-family="monospace">0.99</text>
+<text x="523" y="131" fill="rgba(255,102,0,0.75)" font-size="7" font-family="monospace">0.52</text>
+<text x="593" y="164" fill="rgba(255,102,0,0.75)" font-size="7" font-family="monospace">0.29</text>
+
+<!-- Formula box -->
+<rect x="405" y="230" width="225" height="46" rx="4" fill="rgba(32,112,192,0.08)" stroke="#2070c0" stroke-width="1.5"/>
+<text x="517" y="249" fill="#2070c0" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">attenuation =</text>
+<text x="517" y="267" fill="#2070c0" font-size="11" font-family="monospace" text-anchor="middle">1 / (1 + 0.0001 · d²)</text>
+
+<line x1="20" y1="282" x2="620" y2="282" stroke="#0c1a26" stroke-width="1"/>
+<text x="320" y="291" fill="#2a3a4a" font-size="8" font-family="monospace" text-anchor="middle">coefficient 0.0001 was tuned for typical scene scales in Aukio 3D</text>
+</svg>
diff --git a/Documentation/Shading/Lambert cosine law.svg b/Documentation/Shading/Lambert cosine law.svg
new file mode 100644 (file)
index 0000000..1f4e216
--- /dev/null
@@ -0,0 +1,92 @@
+<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
+<defs>
+  <filter id="glow"><feGaussianBlur stdDeviation="2" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+  <filter id="glows"><feGaussianBlur stdDeviation="1.5" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
+  <marker id="an" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#30a050"/></marker>
+  <marker id="al" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#FF6600"/></marker>
+</defs>
+<rect width="640" height="480" fill="#061018"/>
+<text x="320" y="30" fill="#ccc" font-size="17" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)">Lambert Cosine Law</text>
+<text x="320" y="48" fill="#3a4a5a" font-size="10" font-family="monospace" text-anchor="middle">how surface orientation determines light intensity</text>
+<line x1="385" y1="58" x2="385" y2="305" stroke="#0c1a26" stroke-width="1.5"/>
+<line x1="20" y1="308" x2="620" y2="308" stroke="#0c1a26" stroke-width="1.5"/>
+<polygon points="60,275 280,255 295,165 75,185" fill="none" stroke="rgba(48,160,80,0.15)" stroke-width="8"/>
+<polygon points="60,275 280,255 295,165 75,185" fill="rgba(48,160,80,0.13)" stroke="#30a050" stroke-width="2"/>
+<circle cx="178" cy="220" r="4" fill="#30a050"/>
+<line x1="178" y1="220" x2="148" y2="75" stroke="#30a050" stroke-width="2.5" marker-end="url(#an)"/>
+<text x="128" y="70" fill="#30a050" font-size="18" font-weight="700" font-family="monospace" filter="url(#glow)">N̂</text>
+<text x="130" y="84" fill="#aaa" font-size="9" font-family="monospace">normal</text>
+<circle cx="345" cy="85" r="28" fill="rgba(255,102,0,0.06)"/>
+<circle cx="345" cy="85" r="17" fill="rgba(255,102,0,0.2)" stroke="rgba(255,102,0,0.45)" stroke-width="1.5"/>
+<circle cx="345" cy="85" r="7" fill="rgba(255,102,0,0.7)" stroke="#FF6600" stroke-width="1.5"/>
+<line x1="345" y1="62" x2="345" y2="52" stroke="#FF6600" stroke-width="1.2" opacity="0.55"/>
+<line x1="362" y1="67" x2="370" y2="60" stroke="#FF6600" stroke-width="1.2" opacity="0.55"/>
+<line x1="328" y1="67" x2="320" y2="60" stroke="#FF6600" stroke-width="1.2" opacity="0.55"/>
+<line x1="368" y1="85" x2="377" y2="85" stroke="#FF6600" stroke-width="1.2" opacity="0.55"/>
+<line x1="322" y1="85" x2="313" y2="85" stroke="#FF6600" stroke-width="1.2" opacity="0.55"/>
+<text x="370" y="89" fill="#FF6600" font-size="11" font-weight="700" font-family="monospace" text-anchor="start">Light</text>
+<line x1="333" y1="97" x2="185" y2="215" stroke="#FF6600" stroke-width="2" stroke-dasharray="6 3" marker-end="url(#al)"/>
+<text x="274" y="147" fill="#FF6600" font-size="15" font-weight="700" font-family="monospace" filter="url(#glow)">L̂</text>
+<path d="M 167,166 A 55,55 0 0,1 221,185" fill="none" stroke="#b09020" stroke-width="2"/>
+<text x="213" y="160" fill="#b09020" font-size="14" font-weight="700" font-family="monospace" filter="url(#glows)">θ</text>
+<text x="178" y="244" fill="rgba(48,160,80,0.6)" font-size="10" font-family="monospace" text-anchor="middle">surface polygon</text>
+<rect x="398" y="68" width="218" height="105" rx="4" fill="rgba(32,112,192,0.08)" stroke="#2070c0" stroke-width="1.5"/>
+<text x="507" y="93" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">brightness =</text>
+<text x="507" y="118" fill="#2070c0" font-size="16" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)">dot( N̂ , L̂ )</text>
+<line x1="408" y1="127" x2="606" y2="127" stroke="#2070c0" stroke-width="1" opacity="0.3"/>
+<text x="507" y="152" fill="#2070c0" font-size="13" font-family="monospace" text-anchor="middle">= cos( θ )</text>
+<rect x="398" y="183" width="218" height="115" rx="4" fill="rgba(80,96,192,0.05)" stroke="rgba(80,96,192,0.4)" stroke-width="1"/>
+<text x="412" y="204" fill="#666" font-size="10" font-family="monospace">θ = 0°</text>
+<rect x="458" y="193" width="80" height="13" rx="2" fill="rgba(57,255,20,0.15)" stroke="#39FF14" stroke-width="1"/>
+<rect x="458" y="193" width="80" height="13" rx="2" fill="rgba(57,255,20,0.7)"/>
+<text x="548" y="204" fill="#39FF14" font-size="10" font-weight="700" font-family="monospace">1.00</text>
+<text x="412" y="225" fill="#666" font-size="10" font-family="monospace">θ = 45°</text>
+<rect x="458" y="214" width="80" height="13" rx="2" fill="rgba(48,160,80,0.08)" stroke="#30a050" stroke-width="1"/>
+<rect x="458" y="214" width="57" height="13" rx="2" fill="rgba(48,160,80,0.55)"/>
+<text x="548" y="225" fill="#bbb" font-size="10" font-family="monospace">0.71</text>
+<text x="412" y="246" fill="#666" font-size="10" font-family="monospace">θ = 90°</text>
+<rect x="458" y="235" width="80" height="13" rx="2" fill="rgba(48,160,80,0.04)" stroke="rgba(48,160,80,0.2)" stroke-width="1"/>
+<text x="548" y="246" fill="#555" font-size="10" font-family="monospace">0.00</text>
+<line x1="408" y1="255" x2="606" y2="255" stroke="rgba(80,96,192,0.3)" stroke-width="1"/>
+<text x="412" y="271" fill="#555" font-size="10" font-family="monospace">θ &gt; 90°</text>
+<text x="460" y="271" fill="rgba(208,64,64,0.75)" font-size="10" font-family="monospace">back-face → skip</text>
+<text x="412" y="287" fill="#3a4a5a" font-size="9" font-family="monospace">dot &lt; 0  →  no contribution</text>
+<text x="320" y="326" fill="#2a3a4a" font-size="10" font-family="monospace" text-anchor="middle">— angle examples —</text>
+<g transform="translate(40,338)">
+  <rect x="10" y="78" width="100" height="13" rx="2" fill="rgba(48,160,80,0.25)" stroke="#30a050" stroke-width="1.5"/>
+  <line x1="60" y1="78" x2="60" y2="32" stroke="#30a050" stroke-width="2" marker-end="url(#an)"/>
+  <circle cx="60" cy="13" r="7" fill="rgba(255,102,0,0.5)" stroke="#FF6600" stroke-width="1.5"/>
+  <line x1="60" y1="20" x2="60" y2="34" stroke="#FF6600" stroke-width="2" marker-end="url(#al)"/>
+  <text x="60" y="103" fill="#39FF14" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">θ = 0°</text>
+  <rect x="10" y="112" width="100" height="11" rx="2" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1"/>
+  <rect x="10" y="112" width="100" height="11" rx="2" fill="#30a050"/>
+  <text x="60" y="121" fill="#061018" font-size="8" font-weight="700" font-family="monospace" text-anchor="middle">100%</text>
+</g>
+<g transform="translate(250,338)">
+  <rect x="10" y="78" width="100" height="13" rx="2" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+  <line x1="60" y1="78" x2="60" y2="32" stroke="#30a050" stroke-width="2" marker-end="url(#an)"/>
+  <circle cx="104" cy="34" r="7" fill="rgba(255,102,0,0.5)" stroke="#FF6600" stroke-width="1.5"/>
+  <line x1="99" y1="39" x2="68" y2="70" stroke="#FF6600" stroke-width="2" marker-end="url(#al)"/>
+  <path d="M 60,50 A 28,28 0 0,1 80,58" fill="none" stroke="#b09020" stroke-width="1.5"/>
+  <text x="83" y="51" fill="#b09020" font-size="9" font-weight="700" font-family="monospace">45°</text>
+  <text x="60" y="103" fill="#bbb" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">θ = 45°</text>
+  <rect x="10" y="112" width="100" height="11" rx="2" fill="rgba(48,160,80,0.08)" stroke="#30a050" stroke-width="1"/>
+  <rect x="10" y="112" width="71" height="11" rx="2" fill="rgba(48,160,80,0.55)"/>
+  <text x="60" y="121" fill="#ccc" font-size="8" font-weight="700" font-family="monospace" text-anchor="middle">71%</text>
+</g>
+<g transform="translate(462,338)">
+  <rect x="10" y="78" width="100" height="13" rx="2" fill="rgba(208,64,64,0.06)" stroke="rgba(208,64,64,0.4)" stroke-width="1.5" stroke-dasharray="4 3"/>
+  <line x1="60" y1="78" x2="60" y2="32" stroke="#30a050" stroke-width="2" marker-end="url(#an)"/>
+  <circle cx="122" cy="78" r="7" fill="rgba(255,102,0,0.5)" stroke="#FF6600" stroke-width="1.5"/>
+  <line x1="115" y1="78" x2="76" y2="78" stroke="#FF6600" stroke-width="2" marker-end="url(#al)"/>
+  <path d="M 60,50 A 28,28 0 0,1 88,78" fill="none" stroke="#b09020" stroke-width="1.5"/>
+  <text x="80" y="57" fill="#b09020" font-size="9" font-weight="700" font-family="monospace">90°</text>
+  <text x="60" y="103" fill="rgba(208,64,64,0.8)" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">θ = 90°</text>
+  <rect x="10" y="112" width="100" height="11" rx="2" fill="rgba(208,64,64,0.05)" stroke="rgba(208,64,64,0.4)" stroke-width="1" stroke-dasharray="3 2"/>
+  <text x="60" y="121" fill="rgba(208,64,64,0.7)" font-size="8" font-weight="700" font-family="monospace" text-anchor="middle">0%  (skip)</text>
+</g>
+<line x1="40" y1="472" x2="62" y2="472" stroke="#30a050" stroke-width="2"/>
+<text x="68" y="476" fill="#30a050" font-size="9" font-family="monospace">N̂  surface normal</text>
+<line x1="250" y1="472" x2="272" y2="472" stroke="#FF6600" stroke-width="2" stroke-dasharray="5 2"/>
+<text x="278" y="476" fill="#FF6600" font-size="9" font-family="monospace">L̂  light direction</text>
+</svg>
diff --git a/Documentation/Shading/Shaded sphere.png b/Documentation/Shading/Shaded sphere.png
new file mode 100644 (file)
index 0000000..fbc6487
Binary files /dev/null and b/Documentation/Shading/Shaded sphere.png differ
diff --git a/Documentation/Shading/Shading pipeline.svg b/Documentation/Shading/Shading pipeline.svg
new file mode 100644 (file)
index 0000000..a58c431
--- /dev/null
@@ -0,0 +1,35 @@
+<svg viewBox="0 0 620 80" width="620" height="80" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <marker id="arrowhead2" viewBox="0 0 10 10" refX="9" refY="5"
+            markerWidth="6" markerHeight="6" orient="auto">
+      <path d="M 0 0 L 10 5 L 0 10 z" fill="#30a050"/>
+    </marker>
+  </defs>
+  <rect width="620" height="80" fill="#061018"/>
+
+  <!-- Transform phase (where shading happens) -->
+  <rect x="140" y="25" width="90" height="30" rx="3" fill="rgba(176,144,32,0.15)" stroke="#b09020" stroke-width="2"/>
+  <text x="185" y="43" fill="#b09020" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Transform</text>
+  <text x="185" y="67" fill="#b09020" font-size="8" font-family="monospace" text-anchor="middle">compute lighting</text>
+
+  <!-- Other phases -->
+  <rect x="15" y="25" width="90" height="30" rx="3" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+  <text x="60" y="43" fill="#30a050" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Shapes</text>
+
+  <rect x="265" y="25" width="70" height="30" rx="3" fill="rgba(200,80,140,0.15)" stroke="#c05088" stroke-width="1.5"/>
+  <text x="300" y="43" fill="#c05088" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Sort</text>
+
+  <rect x="365" y="25" width="80" height="30" rx="3" fill="rgba(255,102,0,0.15)" stroke="#FF6600" stroke-width="1.5"/>
+  <text x="405" y="43" fill="#FF6600" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Paint</text>
+  <text x="405" y="67" fill="#bbb" font-size="8" font-family="monospace" text-anchor="middle">use cached color</text>
+
+  <rect x="480" y="25" width="60" height="30" rx="3" fill="rgba(57,255,20,0.15)" stroke="#39FF14" stroke-width="1.5"/>
+  <text x="510" y="43" fill="#39FF14" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Blit</text>
+
+  <!-- Arrows -->
+  <line x1="105" y1="40" x2="135" y2="40" stroke="#30a050" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
+  <line x1="230" y1="40" x2="260" y2="40" stroke="#2070c0" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
+  <line x1="335" y1="40" x2="360" y2="40" stroke="#c05088" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
+  <line x1="445" y1="40" x2="475" y2="40" stroke="#FF6600" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
+  <line x1="540" y1="40" x2="565" y2="40" stroke="#39FF14" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
+</svg>
diff --git a/Documentation/Shading/index.org b/Documentation/Shading/index.org
new file mode 100644 (file)
index 0000000..fdf95a0
--- /dev/null
@@ -0,0 +1,266 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Shading & Lighting - Aukio 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
+
+[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
+
+* Overview
+:PROPERTIES:
+:CUSTOM_ID: shading-lighting
+:END:
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 1000px
+[[file:Shaded sphere.png]]
+
+*Aukio 3D* implements *flat shading* using the [[https://en.wikipedia.org/wiki/Lambert%27s_cosine_law][Lambert cosine
+law]]. Each polygon receives a single color based on its orientation
+relative to light sources. This is a simple yet effective lighting
+model that gives 3D objects depth and realism.
+
+** The Lighting Model: Lambert Cosine Law
+:PROPERTIES:
+:CUSTOM_ID: lambert-cosine-law
+:END:
+
+#+INCLUDE: "Lambert cosine law.svg" export html
+
+The *Lambert cosine law* determines how much light a surface receives
+based on its orientation. A surface facing directly toward a light source
+receives maximum illumination; as it tilts away, the illumination decreases
+proportionally until it reaches zero when perpendicular to the light
+direction. This fundamental principle creates the visual cues that make 3D
+objects appear solid and dimensional rather than flat.
+
+The engine implements this law through the dot product of two vectors. The
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] computes a unit vector pointing from the polygon's center
+to each [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]], then calculates the dot product with the surface
+normal. When the dot product equals 1.0, the surface faces the light
+directly and receives full brightness. At 0.71 (a 45-degree angle), it
+receives about 71% illumination. At zero or below, the surface faces away
+from the light and receives no direct contribution from that source. The
+implementation in [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager.computeLighting()]] explicitly checks for
+positive dot products before adding light contributions, ensuring that
+back-facing surfaces skip unnecessary calculations.
+
+The surface normal itself is computed by [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]], which takes
+the first three vertices of a polygon and calculates their cross product to
+find the perpendicular direction. This normal, along with the polygon's
+center point calculated by [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]], is passed to the lighting manager
+during the [[file:../Rendering loop/][transform phase]] of the rendering loop. The transform phase runs
+in parallel, but each polygon is transformed by exactly one worker per
+pass, so its cached =shadedColor= field has a single writer — the
+result is reused allocation-free during the subsequent multi-threaded
+paint phase. See the
+[[file:../index.org::#normal-vector][Normal Vector]] section for more details on how normals are computed and used
+throughout the engine.
+
+* Light Sources
+:PROPERTIES:
+:CUSTOM_ID: light-sources
+:END:
+
+Each light source has three properties:
+
+| Property   | Description                          |
+|------------+--------------------------------------|
+| Position   | 3D world coordinates of the light    |
+| Color      | RGB color of emitted light           |
+| Intensity  | Brightness multiplier (1.0 = normal) |
+
+#+BEGIN_SRC java
+import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource;
+import eu.svjatoslav.aukio.e3d.geometry.Point3D;
+import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
+
+// Create a bright yellow light to the right
+LightSource rightLight = new LightSource(
+    new Point3D(200, -100, 0),  // position: right, above, at viewer level
+    Color.YELLOW,               // color
+    2.0                         // intensity: extra bright
+);
+
+// Create a dim blue light from the left
+LightSource leftLight = new LightSource(
+    new Point3D(-150, 50, 100),
+    Color.BLUE,
+    0.5                         // intensity: dim
+);
+#+END_SRC
+
+Multiple light sources add their contributions together, allowing for
+complex lighting setups like the screenshot above showing a sphere lit
+by two lights from the right.
+
+** Distance Attenuation
+:PROPERTIES:
+:CUSTOM_ID: distance-attenuation
+:END:
+
+#+INCLUDE: "Distance attenuation.svg" export html
+
+Light intensity decreases with distance using a *simplified inverse
+square law*:
+
+#+BEGIN_SRC
+attenuation = 1.0 / (1.0 + 0.0001 * distance²)
+#+END_SRC
+
+- At distance 0: attenuation = 1.0 (full intensity)
+- At distance 100: attenuation ≈ 0.99 (almost full)
+- At distance 300: attenuation ≈ 0.52 (half intensity)
+- At distance 500: attenuation ≈ 0.29 (about 30%)
+
+This simplified formula prevents harsh cutoffs while still providing
+distance-based dimming. The =0.0001= coefficient was tuned for typical
+scene scales in Aukio 3D.
+
+* Ambient Light
+:PROPERTIES:
+:CUSTOM_ID: ambient-light
+:END:
+
+#+INCLUDE: "Ambient light comparison.svg" export html
+
+*Ambient light* provides base illumination that affects all surfaces
+equally, regardless of orientation. Without ambient light, surfaces not
+directly facing a light source would be pure black.
+
+- Default ambient: =Color(50, 50, 50)= (dim gray) — set by the ViewPanel
+  constructor; a standalone =new LightingManager()= starts at
+  =Color(10, 10, 10)=
+- Configurable via =lightingManager.setAmbientLight()=
+- Too much ambient: flat appearance (no contrast)
+- Too little ambient: harsh shadows (pure black areas)
+
+#+BEGIN_SRC java
+// Increase ambient for softer shadows
+viewPanel.getLightingManager().setAmbientLight(new Color(80, 80, 80));
+
+// Reduce ambient for dramatic contrast
+viewPanel.getLightingManager().setAmbientLight(new Color(20, 20, 20));
+#+END_SRC
+
+* Using Shading in Your Scene
+:PROPERTIES:
+:CUSTOM_ID: using-shading
+:END:
+
+**Adding light sources:**
+
+#+BEGIN_SRC java
+import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
+import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource;
+
+ViewPanel viewPanel = new ViewPanel();
+
+// Get the lighting manager
+LightingManager lighting = viewPanel.getLightingManager();
+
+// Add light sources
+lighting.addLight(new LightSource(
+    new Point3D(200, -100, 0),  // right side, above
+    Color.YELLOW,
+    1.5                         // bright
+));
+
+lighting.addLight(new LightSource(
+    new Point3D(-100, 0, 200),  // left side, further away
+    new Color(255, 200, 150),   // warm white
+    1.0
+));
+
+// Configure ambient light
+lighting.setAmbientLight(new Color(40, 40, 40));
+#+END_SRC
+
+**Enabling shading on shapes:**
+
+#+BEGIN_SRC java
+import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonRectangularBox;
+
+// Create a shaded box
+SolidPolygonRectangularBox box = new SolidPolygonRectangularBox(
+    new Point3D(-50, -50, 100),  // min corner
+    new Point3D(50, 50, 200),   // max corner
+    Color.RED
+);
+
+// Enable shading on the box and all its sub-polygons
+box.setShadingEnabled(true);
+
+// Also enable backface culling for closed meshes
+box.setBackfaceCulling(true);
+
+// Add to scene
+viewPanel.getRootShapeCollection().addShape(box);
+#+END_SRC
+
+Shading propagates through composite shapes — calling
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setShadingEnabled(boolean)][setShadingEnabled(true)]] on a composite enables shading for all its
+sub-polygons.
+
+** Related Classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class | Purpose |
+|-------+---------|
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] | Manages light sources and computes shading |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]] | Individual light with position, color, intensity |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Polygon shape with shading support |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] | Composite shape with shading propagation |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html][ViewPanel]] | Provides access to LightingManager |
+* Implementation details
+:PROPERTIES:
+:CUSTOM_ID: implementation-details
+:END:
+
+#+INCLUDE: "Shading pipeline.svg" export html
+
+Lighting is computed during *Phase 1* (transform phase) of the
+[[file:../Rendering loop/][rendering loop]]:
+
+1. Each shaded polygon calculates its center point and surface normal
+2. [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] computes lighting from all sources
+3. Result stored in reusable =shadedColor= field
+4. During *Phase 4* (paint), the cached color is used directly
+
+**Why during transform phase?**
+
+- Lighting computed *once per polygon per pass* — not per pixel
+- Each polygon is transformed by a single worker, so its cached result
+  has exactly one writer even though the transform phase runs in parallel
+- Result reused during multi-threaded paint phase — efficient
+
+** Performance Characteristics
+:PROPERTIES:
+:CUSTOM_ID: performance
+:END:
+
+| Aspect       | Cost                          |
+|--------------+-------------------------------|
+| Computation  | Per polygon, not per pixel    |
+| Phase        | Parallel transform (single writer per polygon) |
+| Allocation   | Zero (reuses Color instance)  |
+| Cache        | One shadedColor per polygon   |
+
+The shading implementation is optimized for CPU rendering:
+
+- *Flat shading*: One lighting calculation per polygon (N-vertex polygon = 1 calculation)
+- *Reusable Color*: Result stored in existing field, no allocation during render
+- *Thread-safe*: One writer per polygon per pass, so no synchronization needed
+- *Pre-computed*: All paint workers (tile grid, ~75% of CPU cores by default) read the same cached result
+
+This approach trades visual fidelity (no per-pixel lighting) for
+performance — essential for software rendering where per-pixel lighting
+would be prohibitively expensive.
diff --git a/Documentation/Stereoscopic rendering/Stereo geometry.svg b/Documentation/Stereoscopic rendering/Stereo geometry.svg
new file mode 100644 (file)
index 0000000..2f62d50
--- /dev/null
@@ -0,0 +1,79 @@
+<svg viewBox="0 0 640 460" width="640" height="460" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="2" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+  </defs>
+
+  <!-- background -->
+  <rect width="640" height="460" fill="#061018"/>
+
+  <!-- faint grid -->
+  <g stroke="#1a3a4a" stroke-width="0.5">
+    <line x1="60" y1="60" x2="600" y2="60"/>
+    <line x1="60" y1="130" x2="600" y2="130"/>
+    <line x1="60" y1="200" x2="600" y2="200"/>
+    <line x1="60" y1="270" x2="600" y2="270"/>
+    <line x1="60" y1="340" x2="600" y2="340"/>
+    <line x1="60" y1="410" x2="600" y2="410"/>
+  </g>
+
+  <text x="320" y="34" fill="#40b0d0" font-size="17" font-family="monospace" text-anchor="middle" filter="url(#glow)">Two parallel cameras, one screen</text>
+  <text x="320" y="52" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">top-down view of the scene (z grows downward = into the scene)</text>
+
+  <!-- projection plane line (screen) -->
+  <line x1="100" y1="140" x2="560" y2="140" stroke="#c05088" stroke-width="2" filter="url(#glow)"/>
+  <text x="560" y="130" fill="#c05088" font-size="10" font-family="monospace" text-anchor="end">screen plane (per eye)</text>
+
+  <!-- world objects -->
+  <circle cx="330" cy="240" r="8" fill="#FF8833" filter="url(#glow)"/>
+  <text x="330" y="262" fill="#FF8833" font-size="10" font-family="monospace" text-anchor="middle">near object</text>
+  <circle cx="330" cy="380" r="8" fill="#2070c0" filter="url(#glow)"/>
+  <text x="330" y="402" fill="#2070c0" font-size="10" font-family="monospace" text-anchor="middle">far object</text>
+
+  <!-- eyes -->
+  <circle cx="240" cy="70" r="7" fill="#39FF14" filter="url(#glow)"/>
+  <text x="222" y="74" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="end">left eye</text>
+  <text x="222" y="88" fill="#39FF14" font-size="9" font-family="monospace" text-anchor="end">x - IPD/2</text>
+  <circle cx="420" cy="70" r="7" fill="#40b0d0" filter="url(#glow)"/>
+  <text x="438" y="74" fill="#40b0d0" font-size="11" font-family="monospace">right eye</text>
+  <text x="438" y="88" fill="#40b0d0" font-size="9" font-family="monospace">x + IPD/2</text>
+
+  <!-- IPD brace -->
+  <line x1="240" y1="98" x2="420" y2="98" stroke="#b09020" stroke-width="1.5" filter="url(#glow)"/>
+  <line x1="240" y1="92" x2="240" y2="104" stroke="#b09020" stroke-width="1.5"/>
+  <line x1="420" y1="92" x2="420" y2="104" stroke="#b09020" stroke-width="1.5"/>
+  <text x="330" y="92" fill="#b09020" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#glow)">IPD = 6.5 units (cm)</text>
+
+  <!-- rays: left eye -->
+  <line x1="240" y1="70" x2="330" y2="240" stroke="#39FF14" stroke-width="1.5" stroke-opacity="0.8"/>
+  <line x1="240" y1="70" x2="330" y2="380" stroke="#39FF14" stroke-width="1" stroke-opacity="0.45" stroke-dasharray="5 3"/>
+  <!-- rays: right eye -->
+  <line x1="420" y1="70" x2="330" y2="240" stroke="#40b0d0" stroke-width="1.5" stroke-opacity="0.8"/>
+  <line x1="420" y1="70" x2="330" y2="380" stroke="#40b0d0" stroke-width="1" stroke-opacity="0.45" stroke-dasharray="5 3"/>
+
+  <!-- projections of the near object on the screen plane -->
+  <!-- left eye: ray from (240,70) to (330,240); at y=140: t=(140-70)/(240-70)=0.4118, x=240+0.4118*90=277 -->
+  <circle cx="277" cy="140" r="4" fill="#FF8833" stroke="#39FF14" stroke-width="1.5" filter="url(#glow)"/>
+  <!-- right eye: ray from (420,70) to (330,240); at y=140: x=420-0.4118*90=383 -->
+  <circle cx="383" cy="140" r="4" fill="#FF8833" stroke="#40b0d0" stroke-width="1.5" filter="url(#glow)"/>
+  <!-- projections of the far object -->
+  <!-- left: (240,70)->(330,380); t=(140-70)/(380-70)=0.2258; x=240+0.2258*90=260 -->
+  <circle cx="260" cy="140" r="3.5" fill="#2070c0" stroke="#39FF14" stroke-width="1.5"/>
+  <!-- right: x=420-0.2258*90=400 -->
+  <circle cx="400" cy="140" r="3.5" fill="#2070c0" stroke="#40b0d0" stroke-width="1.5"/>
+
+  <!-- disparity braces on the screen plane -->
+  <line x1="277" y1="152" x2="383" y2="152" stroke="#FF8833" stroke-width="1.5" filter="url(#glow)"/>
+  <line x1="277" y1="147" x2="277" y2="157" stroke="#FF8833" stroke-width="1.5"/>
+  <line x1="383" y1="147" x2="383" y2="157" stroke="#FF8833" stroke-width="1.5"/>
+  <text x="330" y="168" fill="#FF8833" font-size="10" font-family="monospace" text-anchor="middle">large disparity = close</text>
+  <line x1="260" y1="126" x2="400" y2="126" stroke="#2070c0" stroke-width="1" stroke-opacity="0.9"/>
+  <text x="330" y="120" fill="#2070c0" font-size="10" font-family="monospace" text-anchor="middle">small disparity = far</text>
+
+  <text x="60" y="446" fill="#999" font-size="10" font-family="monospace">cameras stay PARALLEL (no toe-in) - depth comes purely from the x offset</text>
+</svg>
diff --git a/Documentation/Stereoscopic rendering/Stereo per eye.svg b/Documentation/Stereoscopic rendering/Stereo per eye.svg
new file mode 100644 (file)
index 0000000..5fc9cfd
--- /dev/null
@@ -0,0 +1,49 @@
+<svg viewBox="0 0 640 300" width="640" height="300" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="1.5" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+  </defs>
+
+  <!-- background -->
+  <rect width="640" height="300" fill="#061018"/>
+
+  <text x="320" y="30" fill="#40b0d0" font-size="15" font-family="monospace" text-anchor="middle" filter="url(#glow)">What changes per eye</text>
+
+  <!-- table-ish rows -->
+  <g font-family="monospace">
+    <!-- header -->
+    <text x="60" y="62" fill="#999" font-size="10">concern</text>
+    <text x="330" y="62" fill="#999" font-size="10">per-eye behavior</text>
+    <line x1="55" y1="70" x2="600" y2="70" stroke="#1a3a4a" stroke-width="1"/>
+
+    <text x="60" y="92" fill="#c05088" font-size="10">camera</text>
+    <text x="330" y="92" fill="#40b0d0" font-size="10">translation.x += &#177;IPD/2 (restored after the pass)</text>
+    <line x1="55" y1="100" x2="600" y2="100" stroke="#1a3a4a" stroke-width="0.5"/>
+
+    <text x="60" y="122" fill="#c05088" font-size="10">projection</text>
+    <text x="330" y="122" fill="#40b0d0" font-size="10">scale = eyeWidth/3; x += stereoViewportOffsetX</text>
+    <line x1="55" y1="130" x2="600" y2="130" stroke="#1a3a4a" stroke-width="0.5"/>
+
+    <text x="60" y="152" fill="#c05088" font-size="10">frustum culling</text>
+    <text x="330" y="152" fill="#40b0d0" font-size="10">built from stereoViewportWidth - narrower FOV per eye</text>
+    <line x1="55" y1="160" x2="600" y2="160" stroke="#1a3a4a" stroke-width="0.5"/>
+
+    <text x="60" y="182" fill="#c05088" font-size="10">painting</text>
+    <text x="330" y="182" fill="#40b0d0" font-size="10">clipped to [renderMinX, renderMaxX) = the eye's half</text>
+    <line x1="55" y1="190" x2="600" y2="190" stroke="#1a3a4a" stroke-width="0.5"/>
+
+    <text x="60" y="212" fill="#c05088" font-size="10">mouse picking</text>
+    <text x="330" y="212" fill="#40b0d0" font-size="10">hits combined only for the eye containing the cursor</text>
+    <line x1="55" y1="220" x2="600" y2="220" stroke="#1a3a4a" stroke-width="0.5"/>
+
+    <text x="60" y="242" fill="#c05088" font-size="10">HUD / overlays</text>
+    <text x="330" y="242" fill="#40b0d0" font-size="10">drawn once, spanning the full frame (zero disparity)</text>
+  </g>
+
+  <text x="60" y="282" fill="#999" font-size="9" font-family="monospace">everything else - geometry, textures, lightmaps, GI - is shared: the scene is identical, only the viewpoint moves</text>
+</svg>
diff --git a/Documentation/Stereoscopic rendering/Stereo pipeline.svg b/Documentation/Stereoscopic rendering/Stereo pipeline.svg
new file mode 100644 (file)
index 0000000..895e1fc
--- /dev/null
@@ -0,0 +1,68 @@
+<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <filter id="glow">
+      <feGaussianBlur stdDeviation="2" result="blur"/>
+      <feMerge>
+        <feMergeNode in="blur"/>
+        <feMergeNode in="SourceGraphic"/>
+      </feMerge>
+    </filter>
+    <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
+      <path d="M 0 0 L 10 5 L 0 10 z" fill="#40b0d0"/>
+    </marker>
+    <marker id="arrowG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
+      <path d="M 0 0 L 10 5 L 0 10 z" fill="#39FF14"/>
+    </marker>
+  </defs>
+
+  <!-- background -->
+  <rect width="640" height="480" fill="#061018"/>
+
+  <text x="320" y="32" fill="#40b0d0" font-size="17" font-family="monospace" text-anchor="middle" filter="url(#glow)">One frame = two passes</text>
+  <text x="320" y="50" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">the triple-buffered pipeline runs the same phases twice, once per eye</text>
+
+  <!-- camera box -->
+  <rect x="230" y="70" width="180" height="44" rx="5" fill="rgba(176,144,32,0.08)" stroke="#b09020" stroke-width="1.5"/>
+  <text x="320" y="88" fill="#b09020" font-size="11" font-family="monospace" text-anchor="middle">camera</text>
+  <text x="320" y="104" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">translation.x nudged +/- IPD/2</text>
+
+  <!-- left pass lane -->
+  <rect x="40" y="150" width="250" height="60" rx="5" fill="rgba(57,255,20,0.06)" stroke="#39FF14" stroke-width="1.5" filter="url(#glow)"/>
+  <text x="165" y="172" fill="#39FF14" font-size="12" font-family="monospace" text-anchor="middle">pass LEFT</text>
+  <text x="165" y="188" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">transform &#8594; sort &#8594; tile-bin</text>
+  <text x="165" y="201" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">viewport [0, w/2)</text>
+
+  <!-- right pass lane -->
+  <rect x="350" y="150" width="250" height="60" rx="5" fill="rgba(64,176,208,0.06)" stroke="#40b0d0" stroke-width="1.5" filter="url(#glow)"/>
+  <text x="475" y="172" fill="#40b0d0" font-size="12" font-family="monospace" text-anchor="middle">pass RIGHT</text>
+  <text x="475" y="188" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">transform &#8594; sort &#8594; tile-bin</text>
+  <text x="475" y="201" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">viewport [w/2, w)</text>
+
+  <!-- arrows from camera -->
+  <line x1="285" y1="114" x2="180" y2="148" stroke="#39FF14" stroke-width="1.5" marker-end="url(#arrowG)"/>
+  <line x1="355" y1="114" x2="460" y2="148" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arrow)"/>
+
+  <!-- per-pass context note -->
+  <rect x="130" y="236" width="380" height="52" rx="5" fill="rgba(192,80,136,0.06)" stroke="#c05088" stroke-width="1.5"/>
+  <text x="320" y="256" fill="#c05088" font-size="10" font-family="monospace" text-anchor="middle">each pass owns a RenderingContext copy</text>
+  <text x="320" y="272" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">stereoEye, stereoViewportWidth/OffsetX, renderMinX..renderMaxX</text>
+
+  <line x1="165" y1="210" x2="250" y2="234" stroke="#39FF14" stroke-width="1.2" marker-end="url(#arrowG)"/>
+  <line x1="475" y1="210" x2="390" y2="234" stroke="#40b0d0" stroke-width="1.2" marker-end="url(#arrow)"/>
+
+  <!-- shared frame buffer -->
+  <rect x="70" y="330" width="500" height="80" rx="5" fill="rgba(255,255,255,0.03)" stroke="#b09020" stroke-width="1.5" filter="url(#glow)"/>
+  <rect x="70" y="330" width="250" height="80" fill="rgba(57,255,20,0.07)"/>
+  <rect x="320" y="330" width="250" height="80" fill="rgba(64,176,208,0.07)"/>
+  <line x1="320" y1="330" x2="320" y2="410" stroke="#c05088" stroke-width="2" stroke-dasharray="6 4"/>
+  <text x="195" y="365" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">left eye pixels</text>
+  <text x="195" y="382" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">painting clipped to left half</text>
+  <text x="445" y="365" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle">right eye pixels</text>
+  <text x="445" y="382" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">painting clipped to right half</text>
+  <text x="320" y="428" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">ONE shared frame buffer &#8594; one blit to screen (side-by-side image)</text>
+
+  <line x1="250" y1="288" x2="195" y2="328" stroke="#39FF14" stroke-width="1.2" marker-end="url(#arrowG)"/>
+  <line x1="390" y1="288" x2="445" y2="328" stroke="#40b0d0" stroke-width="1.2" marker-end="url(#arrow)"/>
+
+  <text x="60" y="464" fill="#999" font-size="9" font-family="monospace">vertex buffers, aggregators and paint slots still cycle through 3 slots - the two passes overlap freely</text>
+</svg>
diff --git a/Documentation/Stereoscopic rendering/index.org b/Documentation/Stereoscopic rendering/index.org
new file mode 100644 (file)
index 0000000..f1ceee8
--- /dev/null
@@ -0,0 +1,190 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Stereoscopic Rendering - Aukio 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \setlength{\parindent}{15pt}
+#+LATEX_HEADER: \usepackage{palatino}
+#+LATEX_HEADER: \usepackage{charter}
+#+HTML_HEAD: <style type="text/css">body { max-width: 100%;}</style>
+
+[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
+
+* What stereo rendering adds
+:PROPERTIES:
+:CUSTOM_ID: what-stereo-adds
+:END:
+
+A single rendered image is flat: the brain infers depth only from
+monocular cues (occlusion, shading, perspective, motion parallax while
+you move). *Stereoscopic rendering* adds the strongest depth cue of
+all — /binocular disparity/: your two eyes see slightly different
+images, and the visual cortex turns the difference into a direct
+sensation of depth.
+
+Aukio 3D implements the simplest and most portable form: *side-by-side
+stereo*. Every frame renders the scene twice — once from the left eye
+position, once from the right — into the left and right halves of the
+same image. A VR headset, 3D TV, or a pair of XR glasses in
+side-by-side mode feeds each half to the corresponding eye, and the
+scene gains real volume.
+
+#+CAPTION: A side-by-side stereoscopic frame of the House demo, rendered headlessly with [[file:../index.org::#snapshot][the Snapshot tool]]. Left half: left eye. Right half: right eye. Compare the dark cube and the doorway between the halves — the horizontal shift is the disparity your brain reads as depth.
+[[file:stereo-side-by-side.png]]
+
+* The geometry: two parallel cameras
+:PROPERTIES:
+:CUSTOM_ID: geometry
+:END:
+
+The two eye cameras are identical to the mono camera except for one
+thing: the left eye's position is shifted by -IPD/2 and the right
+eye's by +IPD/2 along the *world* X axis (the offset is applied to the
+camera translation's =x= component directly, then restored). IPD
+(inter-pupillary distance) defaults to *6.5 world units* — the House
+demo treats 1 unit as 1 cm, and 6.5 cm is the median human IPD.
+
+Both cameras look in exactly the same direction (*parallel cameras*,
+no toe-in). Objects at different depths then land at different
+horizontal offsets between the two images — that offset is the
+disparity:
+
+#+CAPTION: Top-down view: the two eye positions and how a near and a far object project onto the screen plane. The near object separates much more between the eyes than the far one.
+[[file:Stereo geometry.svg]]
+
+- near object -> large disparity -> feels close,
+- far object -> small disparity -> feels far,
+- object at infinity -> zero disparity.
+
+Larger IPD exaggerates disparity (stronger but potentially straining
+depth); smaller IPD flattens the scene. =+=/=-= keys adjust it live in
+0.5-unit steps while stereo is active.
+
+* One frame = two passes
+:PROPERTIES:
+:CUSTOM_ID: two-passes
+:END:
+
+Stereo does not add a second pipeline — it runs the existing
+triple-buffered pipeline *twice per frame*. The render thread in
+=ViewPanel.renderFrame()= executes two render passes back to back:
+
+#+CAPTION: Per frame, the camera is nudged left, a full transform/sort/bin pass runs for the left viewport, then the camera is nudged right and a second pass runs for the right viewport. Both paint into one shared frame buffer, clipped to their half.
+[[file:Stereo pipeline.svg]]
+
+1. *Pass LEFT:* camera translation.x is temporarily decreased by
+   IPD/2, the scene is transformed, depth-sorted and tile-binned into a
+   per-pass =RenderingContext= copy whose viewport is the left half of
+   the frame (=[0, width/2)=), and the paint continuation is submitted
+   to the worker pool.
+2. *Pass RIGHT:* the same with +IPD/2 and the right viewport
+   (=[width/2, width)=). The camera offset is always restored in a
+   =finally= block, so the camera never drifts.
+3. The two paints write into *one shared frame buffer* — each clipped
+   to its half — and the completed side-by-side image is blitted to
+   the screen in one go.
+
+The triple-buffer machinery (3 vertex slots, 3 aggregator slots, 3
+framebuffers) does not change: a *pass* takes the slot =passCounter %
+3=, so left and right passes of the same frame simply occupy
+consecutive slots and overlap exactly like consecutive mono frames do.
+Workers flow from one pass's tiles straight into the next pass's tiles
+with no idle gap.
+
+* What adapts per eye
+:PROPERTIES:
+:CUSTOM_ID: per-eye
+:END:
+
+The scene itself — geometry, textures, lightmaps, global illumination
+— is shared and identical for both eyes. Only the *viewpoint* moves,
+so only view-dependent stages differ per pass:
+
+#+CAPTION: The per-eye surface area of the engine. Everything not listed here is eye-independent.
+[[file:Stereo per eye.svg]]
+
+- *Projection:* =Vertex= projects with =projectionScale = eyeWidth/3=
+  (per-eye horizontal FOV) and adds =stereoViewportOffsetX= so the
+  projected image lands in the correct half of the buffer. The same
+  offset is applied for near-plane-clip vertices created directly in
+  camera space.
+- *Frustum culling:* the frustum is rebuilt per pass from
+  =stereoViewportWidth=, so each eye culls against its own (narrower)
+  view volume — nothing leaks in from the other eye's half.
+- *Painting:* every painter clips X to =[renderMinX, renderMaxX)=,
+  which the pass set to its viewport. No eye can paint into the other
+  half, even if a polygon crosses the center line.
+- *Mouse picking:* in stereo each eye shows the same object at a
+  different screen X, so a hit can only be resolved against one eye.
+  =ViewPanel= combines mouse results only for the pass whose viewport
+  actually contains the cursor.
+- *HUD/overlays:* developer tools, crosshair and text are drawn once
+  over the finished frame, at zero disparity — they sit on the screen
+  surface, not in the world.
+
+* Enabling stereo
+:PROPERTIES:
+:CUSTOM_ID: enabling
+:END:
+
+#+BEGIN_SRC java
+ViewPanel viewPanel = ...;
+
+// Side-by-side stereo on:
+viewPanel.setStereoModeEnabled(true);
+
+// Optional: match the viewer (default 6.5 world units):
+viewPanel.setStereoIPD(6.5);
+#+END_SRC
+
+In the demos, *SHIFT+F11* toggles stereo and fullscreen together (XR
+glasses want both); plain *F11* remains fullscreen-only. With stereo
+active, *+* and *-* adjust the IPD in 0.5-unit steps (clamped at 0.5)
+so the viewer can tune comfort at runtime.
+
+#+CAPTION: The same scene rendered as a normal mono frame (for comparison with the pair above). Notice there is no horizontal offset to read depth from — the picture is flat.
+[[file:mono-comparison.png]]
+
+* Performance and limitations
+:PROPERTIES:
+:CUSTOM_ID: limitations
+:END:
+
+- Stereo *doubles the per-frame transform, sort and paint work* — two
+  full passes instead of one. The pipeline overlaps them the same way
+  it overlaps consecutive mono frames, so throughput drops less than
+  2x on a multi-core machine, but expect a real cost.
+- Each eye gets *half the horizontal resolution* of the panel. On a
+  1920x1080 fullscreen window each eye sees 960x1080 — pixels are
+  shared, not duplicated.
+- IPD is in *world units*: 6.5 only means "6.5 cm" if the scene is
+  modeled at 1 unit = 1 cm. In a scene with a different scale, divide
+  or multiply accordingly — or just tune with =+=/=-= until the depth
+  feels right.
+- The eye offset is applied along the *world X axis*, not the camera's
+  right vector: it is exactly correct when the camera faces along Z
+  (yaw = 0) and degrades as you turn — at yaw = 90° the eyes would be
+  offset front-to-back instead of side-to-side. For a fixed-viewing-
+  direction demo this is fine; a fully rotational stereo camera would
+  need to apply the IPD along the rotated right vector.
+- Side-by-side is a *display format*, not a headset driver: the engine
+  produces the image; an XR viewer, 3D TV or video player is
+  responsible for delivering the halves to the eyes.
+- Global illumination is unaffected: lightmaps live on the surfaces,
+  so both eyes sample the same converged lighting for free.
+
+* Related Classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class                | Role in stereo rendering                                        |
+|----------------------+-----------------------------------------------------------------|
+| =ViewPanel=          | owns stereoModeEnabled/stereoIPD; runs the two passes per frame |
+| =StereoEye=          | NONE / LEFT / RIGHT tag carried by each pass context            |
+| =RenderingContext=   | per-eye viewport fields: stereoViewportWidth/OffsetX, renderMin/MaxX |
+| =Vertex=             | per-eye projection: scale from eye width + viewport X offset    |
+| =ShapeCollection=    | rebuilds the frustum per pass from the eye's viewport width     |
+| =InputManager=       | SHIFT+F11 stereo toggle, +/- live IPD adjustment                |
+
+[[file:../index.html#outline-container-understanding-3d-engine][Back to main documentation]]
diff --git a/Documentation/Stereoscopic rendering/mono-comparison.png b/Documentation/Stereoscopic rendering/mono-comparison.png
new file mode 100644 (file)
index 0000000..3b70ee0
Binary files /dev/null and b/Documentation/Stereoscopic rendering/mono-comparison.png differ
diff --git a/Documentation/Stereoscopic rendering/stereo-side-by-side.png b/Documentation/Stereoscopic rendering/stereo-side-by-side.png
new file mode 100644 (file)
index 0000000..12fa4fc
Binary files /dev/null and b/Documentation/Stereoscopic rendering/stereo-side-by-side.png differ
diff --git a/Documentation/Winding order.svg b/Documentation/Winding order.svg
new file mode 100644 (file)
index 0000000..d82048e
--- /dev/null
@@ -0,0 +1,35 @@
+<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
+  <defs>
+    <marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5"
+            markerWidth="8" markerHeight="8" orient="auto-start-reverse">
+      <path d="M 0 0 L 10 5 L 0 10 z" fill="#30a050"/>
+    </marker>
+    <marker id="arrow-red" viewBox="0 0 10 10" refX="10" refY="5"
+            markerWidth="8" markerHeight="8" orient="auto-start-reverse">
+      <path d="M 0 0 L 10 5 L 0 10 z" fill="rgba(208,64,64,0.5)"/>
+    </marker>
+  </defs>
+  <rect width="640" height="480" fill="#061018"/>
+
+  <!-- Green front-face triangle: V1=top, V2=bottom-left, V3=bottom-right -->
+  <polygon points="160,100 260,360 60,360" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="3"/>
+  <!-- CCW arrow: arc from near V1, curves LEFT and DOWN toward V2 -->
+  <path d="M140,144 A 104,104 0 0,0 74,310" fill="none" stroke="#30a050" stroke-width="3" stroke-dasharray="8 4" marker-end="url(#arrow-green)"/>
+  <text x="68" y="240" fill="#30a050" font-size="20" font-weight="700" font-family="monospace">CCW</text>
+  <circle cx="160" cy="100" r="6" fill="#30a050"/>
+  <circle cx="60" cy="360" r="6" fill="#30a050"/>
+  <circle cx="260" cy="360" r="6" fill="#30a050"/>
+  <text x="156" y="88" fill="#aaa" font-size="18" font-family="monospace">V₁</text>
+  <text x="28" y="396" fill="#aaa" font-size="18" font-family="monospace">V₂</text>
+  <text x="264" y="396" fill="#aaa" font-size="18" font-family="monospace">V₃</text>
+  <text x="72" y="440" fill="#30a050" font-size="22" font-weight="700" font-family="monospace">FRONT FACE ✓</text>
+  <!-- Red back-face triangle -->
+  <polygon points="480,100 580,360 380,360" fill="rgba(208,64,64,0.06)" stroke="rgba(208,64,64,0.3)" stroke-width="3" stroke-dasharray="12 6"/>
+  <!-- CW arrow: arc from near V1, curves RIGHT and DOWN -->
+  <path d="M500,144 A 104,104 0 0,1 566,310" fill="none" stroke="rgba(208,64,64,0.5)" stroke-width="3" stroke-dasharray="8 4" marker-end="url(#arrow-red)"/>
+  <text x="536" y="240" fill="rgba(208,64,64,0.6)" font-size="20" font-weight="700" font-family="monospace">CW</text>
+  <line x1="456" y1="216" x2="504" y2="264" stroke="rgba(208,64,64,0.4)" stroke-width="6"/>
+  <line x1="504" y1="216" x2="456" y2="264" stroke="rgba(208,64,64,0.4)" stroke-width="6"/>
+  <text x="372" y="440" fill="rgba(208,64,64,0.7)" font-size="22" font-weight="700" font-family="monospace">BACK FACE ✗</text>
+  <text x="390" y="468" fill="#aaa" font-size="18" font-family="monospace">(culled — not drawn)</text>
+</svg>
diff --git a/Documentation/export-docs.sh b/Documentation/export-docs.sh
new file mode 100755 (executable)
index 0000000..7ef274d
--- /dev/null
@@ -0,0 +1,46 @@
+#!/bin/bash
+# export-docs.sh — export all org-mode documentation pages to HTML.
+#
+# Exports every Documentation/**/index.org (and Documentation/index.org)
+# with the darksun theme, using the user's Emacs configuration. Run from
+# anywhere:
+#
+#   Documentation/export-docs.sh            # export all pages
+#   Documentation/export-docs.sh --check    # export, then render every page with
+#                                 # headless Chrome to /tmp/doc-check-*.png
+#                                 # for visual inspection
+#
+# Requires: emacs (with ~/.emacs providing the org HTML setup),
+#           google-chrome (only for --check).
+
+set -euo pipefail
+DOC_DIR="$(cd "$(dirname "$0")" && pwd)"
+
+mapfile -t PAGES < <(find "$DOC_DIR" -name index.org | sort)
+
+echo "Exporting ${#PAGES[@]} pages..."
+for page in "${PAGES[@]}"; do
+    rel="${page#"$DOC_DIR"/}"
+    if emacs --batch -l ~/.emacs --visit="$page" \
+            --funcall=org-html-export-to-html --kill 2>&1 \
+            | grep -qi "aborted\|unable to resolve link"; then
+        echo "FAIL $rel"
+        exit 1
+    fi
+    echo "  ok $rel"
+done
+
+if [[ "${1:-}" == "--check" ]]; then
+    echo "Rendering pages for visual check..."
+    for page in "${PAGES[@]}"; do
+        rel="${page#"$DOC_DIR"/}"
+        html="${page%.org}.html"
+        out="/tmp/doc-check-$(echo "$rel" | tr '/ ' '__').png"
+        google-chrome --headless --disable-gpu --hide-scrollbars \
+            --virtual-time-budget=8000 --window-size=1100,2000 \
+            --screenshot="$out" "file://$html" 2>/dev/null
+        echo "  shot $out"
+    done
+fi
+
+echo "Done."
diff --git a/Documentation/index.org b/Documentation/index.org
new file mode 100644 (file)
index 0000000..4068d09
--- /dev/null
@@ -0,0 +1,1224 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Aukio 3D - Realtime 3D engine
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+HTML_HEAD: <link rel="stylesheet" href="style.css"/>
+
+* Introduction
+:PROPERTIES:
+:CUSTOM_ID: overview
+:ID:       a31a1f4d-5368-4fd9-aaf8-fa6d81851187
+:END:
+
+[[file:Example.png]]
+
+*Aukio 3D* is a realtime 3D rendering engine written in pure Java. It
+runs entirely on the CPU — no GPU required, no OpenGL, no Vulkan, no
+native libraries. Just Java.
+
+The motivation is simple: GPU-based 3D is a minefield of accidental
+complexity. Drivers are buggy or missing entirely. Features you need
+aren't supported on your target hardware. You run out of GPU RAM. You
+wrestle with platform-specific interop layers, shader compilation
+quirks, and dependency hell. Every GPU API comes with its own
+ecosystem of pain — version mismatches, incomplete implementations,
+vendor-specific workarounds. I want a library that "just works".
+
+*Aukio 3D* takes a different path. By rendering everything in software
+on the CPU, the entire GPU problem space simply disappears. You add a
+Maven dependency, write some Java, and you have a 3D scene. It runs
+wherever Java runs.
+
+This approach is quite practical for many use-cases. Modern systems
+ship with many CPU cores, and those with unified memory architectures
+offer high bandwidth between CPU and RAM. Software rendering that once
+seemed wasteful is now a reasonable choice where you need good-enough
+performance without the overhead of a full GPU pipeline. Java's JIT
+compiler helps too, optimizing hot rendering paths at runtime.
+
+Beyond convenience, CPU rendering gives you complete control. You own
+every pixel. You can freely experiment with custom rendering
+algorithms, optimization strategies, and visual effects without being
+constrained by what a GPU API exposes. Instead of brute-forcing
+everything through a fixed GPU pipeline, you can implement clever,
+application-specific optimizations.
+
+*Aukio 3D* is part of the larger [[https://www3.svjatoslav.eu/projects/aukio/][Aukio project]], with the long-term goal
+of providing a platform for 3D user interfaces and interactive data
+visualization. It can also be used as a standalone 3D engine in any
+Java project. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/][demos]] for examples of what it can do today.
+
+*Major features:*
+** Global Illumination
+:PROPERTIES:
+:CUSTOM_ID: global-illumination
+:END:
+
+#+attr_html: :width 600px
+#+attr_latex: :width 600px
+[[file:Global illumination/Global illumination.png]]
+
+On top of flat shading, the engine computes progressive *global
+illumination* on background CPU threads: real shadows, smooth light
+falloff inside polygons (per-texel lightmaps), and indirect bounce
+light — while the render loop itself never traces a single ray.
+
+Read more about [[file:Global illumination/][global illumination]].
+
+** Side-by-side stereoscopic rendering support
+:PROPERTIES:
+:CUSTOM_ID: stereoscopic
+:END:
+
+#+attr_html: :width 600px
+#+attr_latex: :width 600px
+[[file:Stereoscopic rendering/stereo-side-by-side.png]]
+
+The engine can render every frame twice — once per eye — into the left
+and right halves of the same image, for XR glasses and 3D displays.
+The cameras stay parallel and are offset by a configurable IPD
+(inter-pupillary distance). Two render passes share one triple-buffered
+pipeline, each clipped to its half of the frame buffer; per-eye
+projection, frustum culling and mouse picking adapt automatically.
+
+See [[file:Stereoscopic%20rendering/][Stereoscopic rendering]] for the
+geometry, pipeline and tuning.
+
+** Constructive Solid Geometry
+
+#+attr_html: :width 600px
+#+attr_latex: :width 600px
+[[file:CSG/CSG demo.png]]
+
+*Aukio 3D* allows performing boolean operations against geometry shapes.
+So one can subtract, unionize or intersect shapes.
+
+To understand CSG boolean operations, read more about [[file:CSG/][Constructive
+Solid Geometry]].
+
+** SDF textures for sharp text
+:PROPERTIES:
+:CUSTOM_ID: sdf-text
+:END:
+
+[[file:SDF textures/sdf-angled.png]]
+
+Text and vector-art surfaces do not store coverage; they store a
+*signed distance field* — per texel, the distance to the nearest glyph
+edge. The rasterizer re-derives coverage per screen pixel from that
+smooth field, so text stays sharp at any zoom and fades to clean gray
+under minification, all without a mipmap chain.
+
+See [[file:SDF%20textures/][SDF textures]] for the glyph pipeline, the
+render path, analytic minification and tuning knobs.
+
+* How take engine into use
+:PROPERTIES:
+:CUSTOM_ID: taking-engine-into-use
+:END:
+
+Add the *Aukio 3D* dependency to your Maven project:
+
+#+BEGIN_SRC xml
+<dependencies>
+    <dependency>
+        <groupId>eu.svjatoslav</groupId>
+        <artifactId>aukio-3d</artifactId>
+        <version>1.4</version>
+    </dependency>
+</dependencies>
+#+END_SRC
+
+Also add the repository (the library is not on Maven Central):
+
+#+BEGIN_SRC xml
+<repositories>
+    <repository>
+        <id>svjatoslav.eu</id>
+        <name>Svjatoslav repository</name>
+        <url>https://www3.svjatoslav.eu/maven/</url>
+    </repository>
+</repositories>
+#+END_SRC
+
+- Library requires Java 21 or newer.
+
+- Study the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/][demo applications]] for practical examples. Start with the
+  [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#minimal-example][minimal example]] to see the basic boilerplate needed to render a 3D
+  scene.
+
+- Study [[#understanding-3d-engine][how Aukio 3D engine works]].
+- Read online [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/][JavaDoc]].
+- See [[https://www3.svjatoslav.eu/projects/aukio-3d/graphs/][*Aukio 3D* class diagrams]]. (Diagrams were generated by using
+  [[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility)
+
+* Essential theory
+:PROPERTIES:
+:CUSTOM_ID: understanding-3d-engine
+:ID:       4b6c1355-0afe-40c6-86c3-14bf8a11a8d0
+:END:
+** Coordinate System (X, Y, Z)
+:PROPERTIES:
+:CUSTOM_ID: coordinate-system
+:END:
+
+#+INCLUDE: "Coordinate system.svg" export html
+
+*Aukio 3D* uses a **left-handed coordinate system with X pointing right
+and Y pointing down**, matching standard 2D screen coordinates. This
+coordinate system should feel intuitive for people with preexisting 2D
+graphics background.
+
+| Axis | Direction                          | Meaning                                   |
+|------+------------------------------------+-------------------------------------------|
+| X    | Horizontal, positive = RIGHT       | Objects with larger X appear to the right |
+| Y    | Vertical, positive = DOWN          | Lower Y = higher visually (up)            |
+| Z    | Depth, positive = away from viewer | Negative Z = closer to camera             |
+
+*Practical Examples*
+
+- A point at =(0, 0, 0)= is at the origin.
+- A point at =(100, 50, 200)= is: 100 units right, 50 units down
+  visually, 200 units away from the camera.
+- To place object A "above" object B, give A a **smaller Y value**
+  than B.
+
+Coordinates in this system are stored using the
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point3D.html][Point3D]] class — a mutable container with public =x=, =y=, =z= fields
+supporting vector operations like distance, rotation, and translation.
+Vertices (see [[#vertex][below]]) are positioned within this coordinate system.
+
+The [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#coordinate-system][aukio-3d-demos]] project includes an interactive
+coordinate system reference showing X, Y, Z axes as colored arrows
+with a grid plane for spatial context.
+
+** Point3D and Vertex
+:PROPERTIES:
+:CUSTOM_ID: vertex
+:END:
+
+#+INCLUDE: "Point3D vertex.svg" export html
+
+Every 3D object is built from *vertices* — corner points that define
+the shape's geometry. A triangle has 3 vertices, a cube has 8, and
+complex meshes have thousands. The engine uses two related classes to
+represent points in 3D space, each serving a different purpose.
+
+
+
+*** Point3D — Raw Coordinates
+
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point3D.html][Point3D]] is the fundamental coordinate type throughout the engine. It
+stores a position or vector with three public fields: =x=, =y=, =z=.
+The class provides vector math operations: distance calculation,
+rotation, translation, scaling, dot/cross products, and interpolation. Methods follow a fluent API convention where mutating
+operations (like =add=, =multiply=) return =this= for chaining, while
+non-mutating variants (like =withAdded=, =withMultiplied=) return new
+instances.
+
+Use =Point3D= for:
+- Storing positions, vectors, or any raw 3D coordinate
+- Distance and angle calculations between points
+- Vector math (dot product, cross product, normalization)
+- Rotating or translating positions before shape construction
+
+#+BEGIN_SRC java
+Point3D p1 = new Point3D(100, 50, 200);
+Point3D p2 = new Point3D(0, 0, 100);
+double distance = p1.getDistanceTo(p2);                  // Euclidean distance
+Point3D direction = p1.withSubtracted(p2).unit();        // New point: unit vector from p2 to p1
+p1.rotate(new Point3D(0,0,0), Math.PI/4, 0);             // Rotate p1 in place, 45° in XZ plane
+#+END_SRC
+
+*** Vertex — Rendering-Ready Coordinates
+
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] wraps a =Point3D= and adds the coordinate spaces needed during
+rendering. As a shape transforms through the render pipeline, each
+vertex tracks its position in multiple spaces:
+
+| Field                  | Purpose                                                |
+|------------------------+--------------------------------------------------------|
+| =coordinate=           | Original position in local/model space                 |
+| =transformedCoordinate(ctx)=  | Position relative to camera (after transform stack) |
+| =onScreenCoordinate(ctx)=     | 2D screen pixels (after perspective projection)     |
+| =textureCoordinate=    | Optional UV coords in pixel units (not normalized)     |
+| =normal=               | Optional normal vector for CSG polygon splitting       |
+
+=transformedCoordinate= and =onScreenCoordinate= are accessor methods,
+not plain fields: each vertex carries three slots for each, one per
+pipeline projection slot, and the accessor picks the slot of the
+context's current render pass. This is what lets the triple-buffered
+pipeline transform the next frame while previous frames are still
+being painted (see [[file:Rendering loop/index.org::#software-pipeline][software pipeline]]).
+
+During rendering, the vertex is transformed through all spaces: first
+applying the
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/TransformStack.html][TransformStack]] to get the camera-relative coordinate, then
+projecting to 2D. Results are cached per frame per slot to avoid
+recomputing for vertices shared across multiple shapes.
+
+Use =Vertex= when:
+- Constructing triangles, polygons, or textured shapes
+- Your geometry needs texture UV coordinates
+- You're performing CSG boolean operations (requires =normal=)
+
+#+BEGIN_SRC java
+// Create a textured triangle (texture coordinates use pixel units)
+// For a 256x256 texture: (0,0)=top-left, (256,256)=bottom-right
+Vertex v1 = new Vertex(new Point3D(0, 0, 100), new Point2D(0, 0));
+Vertex v2 = new Vertex(new Point3D(100, 0, 100), new Point2D(256, 0));
+Vertex v3 = new Vertex(new Point3D(50, 100, 100), new Point2D(128, 256));
+TexturedTriangle triangle = new TexturedTriangle(v1, v2, v3, texture);
+#+END_SRC
+
+*** When to Use Each
+
+| Use Point3D                          | Use Vertex                                    |
+|--------------------------------------+-----------------------------------------------|
+| Positioning shapes, cameras, lights  | Building triangles and polygons               |
+| Vector math (distances, directions)  | Texture-mapped geometry                      |
+| Rotating or translating positions    | CSG operations                                |
+| Temporary calculations               | Shapes that render through transform pipeline |
+
+For simple shapes without textures, you can pass raw =Point3D=
+coordinates directly to constructors — the shape will internally wrap
+them in =Vertex= objects. The [[#coordinate-system][coordinate system]] above defines the
+meaning of all =x=, =y=, =z= values in both classes.
+
+** Edge
+:PROPERTIES:
+:CUSTOM_ID: edge
+:END:
+
+#+INCLUDE: "Edge.svg" export html
+
+An *edge* is a straight line segment connecting two [[#vertex][vertices]]. Edges
+form the wireframe skeleton of a 3D model — the structural framework
+visible when surfaces are not rendered. A triangle has 3 edges, a cube
+has 12 edges, and complex meshes have thousands.
+
+In *Aukio 3D*, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class implements edges as renderable shapes. Each
+Line connects two [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] endpoints and stores two properties: a
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#width][width]] in world units (adjusted for perspective during rendering) and a
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#color][color]] with alpha transparency. The rendering algorithm switches
+between two modes based on the projected screen width: thin lines below
+the threshold are drawn as single pixels with alpha-adjusted coloring,
+while thicker lines are rendered as filled rectangles with perspective-correct
+edge fading using four [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineInterpolator.html][LineInterpolator]] scanline boundaries.
+
+Wireframe shapes are composite objects built from multiple Line instances.
+For example, [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.html][WireframeBox]] creates 12 Line objects — four edges parallel to
+each axis — using a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineAppearance.html][LineAppearance]] factory to ensure consistent styling across
+all edges. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.html][WireframeCube]] convenience subclass provides a center-point
+constructor. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual comparison of
+wireframe (edges only) versus solid polygon (surfaces with lighting)
+rendering modes.
+
+** Face (Triangle)
+:PROPERTIES:
+:CUSTOM_ID: face-triangle
+:END:
+
+#+INCLUDE: "Face triangle.svg" export html
+
+A *face* is a flat surface enclosed by edges — the visible skin of a 3D
+object. While faces can theoretically have any number of sides, 3D
+engines standardize on *triangles* because three points always define a
+flat plane. A quad (4 vertices) or pentagon (5 vertices) might be
+non-planar depending on vertex positions, causing rendering artifacts.
+Triangles avoid this problem entirely.
+
+*** SolidPolygon — Solid-Color Faces
+
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] is the primary face type, supporting any number of vertices
+(3 or more). Triangles render directly via scanline rasterization.
+N-vertex polygons (quads, pentagons, etc.) are triangulated using fan
+decomposition — a quad becomes 2 triangles, a pentagon 3 — but only
+when the polygon lives inside an
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]
+(the scene graph root is one): the composite triangulates while
+building its render list. A standalone SolidPolygon with more than 3
+vertices cannot be painted directly and throws IllegalStateException.
+
+Each SolidPolygon stores a single fill color with optional alpha
+transparency. When shading is enabled, the lighting manager computes
+the polygon's illumination once during the transform phase, then
+applies the shaded color during painting. Backface culling (see
+[[#winding-order-backface-culling][Winding Order & Backface Culling]]) can be enabled per-polygon, or
+applied recursively to an entire composite shape via
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] — this propagates the
+setting to all SolidPolygon and TexturedTriangle sub-shapes, including
+nested composites.
+
+#+BEGIN_SRC java
+// Create a red triangle
+SolidPolygon triangle = SolidPolygon.triangle(
+    new Point3D(0, 0, 100),
+    new Point3D(50, 0, 100),
+    new Point3D(25, 50, 100),
+    Color.RED
+);
+
+// Create a blue quad (internally triangulated)
+SolidPolygon quad = SolidPolygon.quad(
+    new Point3D(-50, -50, 100),
+    new Point3D(50, -50, 100),
+    new Point3D(50, 50, 100),
+    new Point3D(-50, 50, 100),
+    Color.BLUE
+);
+
+// Enable lighting and culling for a closed mesh
+quad.setShadingEnabled(true);
+quad.setBackfaceCulling(true);
+
+// Add to the scene — the root composite triangulates the quad
+viewPanel.getRootShapeCollection().addShape(quad);
+#+END_SRC
+
+*** TexturedTriangle — UV-Mapped Faces
+
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] renders faces with image textures mapped via UV
+coordinates. Each of the three [[#vertex][vertices]] stores a =textureCoordinate=
+(a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point2D.html][Point2D]] with U and V values in *pixel units* matching the texture
+dimensions). For a 256×256 texture, coordinates range from (0,0) at the
+top-left corner to (256,256) at the bottom-right. During rasterization, the
+engine interpolates these UV coordinates across the triangle's surface,
+sampling the texture at each pixel. When mipmaps are used, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html#multiplicationFactor][multiplicationFactor]]
+scales coordinates to match the selected mipmap resolution.
+
+The texture system supports mipmaps — pre-scaled versions of the texture
+selected based on the triangle's screen size to reduce aliasing artifacts
+on distant surfaces. Texture coordinates are mapped with
+perspective-correct interpolation inside the scanline rasterizer, so
+large triangles at steep angles render without distortion — see the
+[[file:Perspective correct textures/][perspective-correct textures]] page.
+
+#+BEGIN_SRC java
+// Create a 256x256 texture
+Texture texture = new Texture(256, 256, 2);  // width, height, maxUpscale
+
+// Create a textured triangle with UV coordinates in pixel units
+Vertex v1 = new Vertex(new Point3D(0, 0, 100), new Point2D(0, 0));      // top-left
+Vertex v2 = new Vertex(new Point3D(100, 0, 100), new Point2D(256, 0));  // top-right
+Vertex v3 = new Vertex(new Point3D(50, 100, 100), new Point2D(128, 256)); // bottom-center
+
+TexturedTriangle triangle = new TexturedTriangle(v1, v2, v3, texture);
+triangle.setBackfaceCulling(true);
+#+END_SRC
+
+Both SolidPolygon and TexturedTriangle extend
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]], which handles vertex transformation and depth
+sorting. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual comparison of solid
+versus textured polygon rendering.
+
+** Normal Vector
+:PROPERTIES:
+:CUSTOM_ID: normal-vector
+:END:
+
+#+INCLUDE: "Normal vector.svg" export html
+
+A *normal* is a vector perpendicular to a surface. It tells the
+renderer which direction a face is pointing. Normals are critical for
+*lighting* — the angle between the light direction and the normal
+determines how bright a surface appears.
+
+**Use cases:**
+
+| Use case             | API                                          | Computation                | Location          |
+|----------------------+----------------------------------------------+----------------------------+-------------------|
+| BSP/CSG operations   | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#getPlane()][SolidPolygon.getPlane()]] → [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#normal][Plane.normal]]       | Lazy-cached once           | =Plane=           |
+| Per-frame shading    | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]] → =cachedNormal= field | Recomputed every frame     | =SolidPolygon=    |
+| Lighting calculation | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html#computeLighting()][LightingManager.computeLighting()]]            | Uses normal via =dot(L,N)= | =LightingManager= |
+
+**Implementation notes:**
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]]: shared zero-allocation helper for computing normals from three points
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Plane]]: stores normals in Hesse normal form (normal + distance) for BSP spatial partitioning
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html#normal][Vertex.normal]]: optional field for CSG polygon splitting (not used for rendering)
+
+** Mesh
+:PROPERTIES:
+:CUSTOM_ID: mesh
+:END:
+
+#+INCLUDE: "Mesh.svg" export html
+
+A *mesh* is a collection of vertices, edges, and faces that together
+define the shape of a 3D object. Even curved surfaces like spheres are
+approximated by many small triangles — more triangles means a smoother
+appearance. A cube has 8 vertices forming 12 triangular faces, while a
+smooth sphere requires hundreds or thousands of triangles depending on
+the desired quality.
+
+In *Aukio 3D*, meshes are built through composition rather than
+monolithic vertex/index buffers. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]] class is
+the foundation for primitive shapes — each instance stores its own
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html#vertices][List&lt;Vertex&gt;]] directly. This includes [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] (N-vertex
+convex polygons, not limited to triangles), [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]]
+(UV-mapped triangles), and [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] (wireframe edges). The
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] class groups multiple shapes into a single
+object with its own position, rotation, and transform — useful for
+complex models that move or rotate together.
+
+Complex meshes are constructed procedurally by adding primitive shapes
+during initialization. For example, [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.html][SolidPolygonSphere]] generates
+triangles using a latitude-longitude grid: with 16 segments, it
+creates 960 SolidPolygon triangles by looping through
+rings and sectors, calling [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#addShape(eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape)][addShape()]] for each. The generic
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.html][SolidPolygonMesh]] accepts any list of triangles, allowing custom
+geometry from procedural generation or external sources.
+
+During rendering, several automatic optimizations occur. N-vertex
+polygons (quads, pentagons, etc.) are triangulated using fan
+triangulation inside the composite's render-list builder, converting
+an N-vertex polygon into N-2 triangles. Textured triangles render with
+perspective-correct texture mapping (see [[file:Perspective correct textures/][perspective-correct textures]]).
+Composites perform view frustum culling to skip rendering when entirely
+off-screen. Sub-shapes can be organized into named groups via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/SubShape.html][SubShape]]
+wrappers, allowing [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#showGroup(java.lang.String)][showGroup()]] and [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#hideGroup(java.lang.String)][hideGroup()]] to toggle visibility of
+entire sections. Composite shapes also support CSG boolean operations
+— see the [[file:CSG/][Constructive Solid Geometry]] documentation for union,
+subtract, and intersect operations.
+
+The [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] showcases all primitive shapes available in
+*Aukio 3D*, rendered in both wireframe mode (edges only via
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.html][WireframeBox]] and similar) and solid polygon mode (filled surfaces with
+dynamic lighting).
+
+** Working with Colors
+:PROPERTIES:
+:CUSTOM_ID: working-with-colors
+:ID:       f2c9642a-a093-444f-8992-76c97ff28c16
+:END:
+
+Aukio 3D uses its own [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html][Color class]] instead of [[https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/Color.html][java.awt.Color]]. This
+custom implementation is designed specifically for the engine's
+software rasterizer, where avoiding object allocation during rendering
+is critical for performance. When rendering thousands of polygons per
+frame, creating new Color instances for each one would generate
+excessive garbage and trigger frequent garbage collection
+pauses. Instead, the engine's Color class uses mutable fields that can
+be reused across frames.
+
+The class stores RGBA components as public integer fields in the range
+0–255. This format matches the engine's pixel buffer layout and avoids
+costly float-to-int conversions during rasterization. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#r][r]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#g][g]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#b][b]], and
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#a][a]] fields are accessible directly, allowing lighting calculations and
+alpha blending to modify colors in-place without allocating new
+objects. For example, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] class maintains a reusable
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][shadedColor]] field that gets updated during each frame's lighting
+calculation instead of creating a new Color instance per polygon.
+
+Color provides several constructors for different input formats. The
+most common approach is using hex strings via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#hex(java.lang.String)][Color.hex(String)]] or the
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(java.lang.String)][String constructor]], which support formats like ="F80"= (3-digit RGB),
+="FF8800"= (6-digit RGB), ="F808"= (4-digit RGBA), and ="FF8800CC"=
+(8-digit RGBA). You can also create colors from integer RGBA
+components (0–255) using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(int,int,int,int)][new Color(r, g, b, a)]], from floating-point
+components (0.0–1.0) via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(double,double,double,double)][new Color(double r, double g, double b,
+double a)]], or from a packed RGB integer like =0xFF8800= using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(int)][new
+Color(int rgb)]]. The class also provides predefined constants for
+common colors: [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#RED][Color.RED]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#GREEN][Color.GREEN]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#BLUE][Color.BLUE]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#YELLOW][Color.YELLOW]],
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#CYAN][Color.CYAN]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#MAGENTA][Color.MAGENTA]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#WHITE][Color.WHITE]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#BLACK][Color.BLACK]], and
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#TRANSPARENT][Color.TRANSPARENT]].
+
+The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#set(int,int,int,int)][set(int r, int g, int b, int a)]] method modifies a Color in-place
+and returns =this= for method chaining, which is essential for
+performance during rendering. For example, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]]
+calculates lighting contributions from all light sources and stores
+the final shaded color directly into a reusable Color instance via
+=set()=, avoiding any allocation. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#toAwtColor()][toAwtColor()]] method converts a
+Aukio 3D Color to a java.awt.Color when needed for Java2D graphics
+operations, caching the result to avoid repeated conversion. The
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#toInt()][toInt()]] method packs the color into an ARGB integer suitable for the
+engine's pixel buffer, used during rasterization to write pixels
+directly.
+
+#+BEGIN_SRC java
+import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
+import static eu.svjatoslav.aukio.e3d.renderer.raster.Color.hex;
+
+// Using predefined color constants
+Color red = Color.RED;
+Color transparent = Color.TRANSPARENT;
+
+// Create from hex string (recommended for clarity)
+Color orange = hex("FF8800");           // RGB, fully opaque
+Color semiTransparent = hex("FF880080"); // RGBA, 50% transparent
+
+// Create from integer components (0-255)
+Color custom = new Color(255, 128, 64, 200);
+
+// Create from packed RGB integer
+Color packed = new Color(0xFF8800);
+
+// Modify existing color in-place (no allocation)
+Color reusable = new Color();
+reusable.set(100, 200, 50, 255);
+
+// Convert to AWT color for Java2D operations
+java.awt.Color awtColor = custom.toAwtColor();
+
+// Use in lighting calculations (LightingManager modifies in-place)
+// See the Shading & Lighting documentation for details
+#+END_SRC
+
+The alpha component controls transparency during rendering. A value of
+0 makes the color fully transparent, while 255 makes it fully
+opaque. The rasterizer implements alpha blending during the paint
+phase: when drawing a semi-transparent pixel, the engine blends the
+source color with the existing background pixel proportionally based
+on the alpha value. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html#drawPixel(int,int\[\],int)][TextureBitmap.drawPixel()]] method handles this
+blending, multiplying source colors by alpha and background colors by
+=(255 - alpha)=, then combining them. You can test whether a color is
+fully transparent using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#isTransparent()][isTransparent()]], which returns true when alpha
+equals zero.
+
+For lighting calculations, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]] class uses Color to
+represent the color and intensity of emitted light. Multiple light
+sources contribute to the final shaded color of each polygon, as
+described in the [[file:Shading/index.org::#shading-lighting][Shading & Lighting]] documentation. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html#setAmbientLight(eu.svjatoslav.aukio.e3d.renderer.raster.Color)][ambient light]]
+provides base illumination that affects all surfaces equally,
+regardless of orientation. Colors are also used for wireframe
+rendering via the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class, where the color field determines the
+line's appearance.
+
+** Shading & Lighting
+
+#+attr_html: :width 600px
+#+attr_latex: :width 600px
+[[file:Shading/Shaded%20sphere.png]]
+
+
+*Aukio 3D* implements *flat shading* — one normal per polygon,
+computed from the first three vertices. Each polygon receives a single
+color based on its orientation relative to light sources.
+
+To understand lighting and shading, read more about [[file:Shading/][shading & lighting]].
+
+* 3D engine internals
+** Main render loop
+
+The rendering loop is the heart of the engine, continuously generating
+frames at a target rate (typically 60 FPS). Each frame transforms 3D
+shapes through a multi-stage pipeline before displaying them on screen.
+
+#+INCLUDE: "Rendering loop/Render pipeline.svg" export html
+
+The render loop runs on a dedicated background daemon thread managed by
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html][ViewPanel]], which can optionally sleep between frames to maintain a
+target FPS or run unlimited for benchmarking.
+
+For a detailed walkthrough of each phase with diagrams and code
+examples, see the dedicated page: [[file:Rendering loop/][Rendering loop]].
+
+** Near-Plane Clipping
+:PROPERTIES:
+:CUSTOM_ID: near-plane-clipping
+:END:
+
+Individual polygons that straddle the camera's near plane are not
+dropped wholesale: the vertex loop is clipped against the plane, new
+intersection vertices are generated with 3D-interpolated UVs and
+normals, and the clipped polygon — a triangle can become a quad,
+painted as a triangle fan — renders normally. Only polygons entirely
+behind the near plane are culled. This keeps floor and wall tiles
+visible when the camera brushes against them.
+
+#+INCLUDE: "Near plane clip/Near plane straddle.svg" export html
+
+Read more about [[file:Near plane clip/][near-plane clipping]].
+
+** Depth buffer
+:PROPERTIES:
+:CUSTOM_ID: depth-buffer
+:END:
+
+Visibility is resolved per pixel by a depth buffer: every triangle
+interpolates =1/z= across its spans and wins a pixel only where it is
+nearer than the surface already there. Opaque geometry — textured
+triangles, solid polygons — paints front-to-back with depth writes;
+translucent geometry paints back-to-front with depth tests but no
+writes, so it never occludes. Lines and billboards stay painter-ordered
+overlays by design.
+
+See [[file:Depth%20buffer/][Depth buffer]] for the full treatment.
+
+** Frustum & View Frustum Culling
+
+*Aukio 3D* implements view frustum culling.
+
+#+INCLUDE: "Frustum culling/Frustum diagram.svg" export html
+
+To understand frustum culling and object-level visibility
+optimization, read more about [[file:Frustum culling/][frustum & view frustum culling.]]
+
+** Winding Order & Backface Culling
+:PROPERTIES:
+:CUSTOM_ID: winding-order-backface-culling
+:END:
+
+#+INCLUDE: "Winding order.svg" export html
+
+The order in which a triangle's vertices are listed determines its
+*winding order*. In *Aukio 3D*, screen coordinates have Y-axis pointing
+*down*, which inverts the apparent winding direction compared to
+standard mathematical convention (Y-up). *Counter-clockwise (CCW)* in
+screen space means front-facing. *Backface culling* skips rendering
+triangles that face away from the camera — a major performance
+optimization.
+
+- CCW winding (in screen space) → front face (visible)
+- CW winding (in screen space) → back face (culled)
+- When viewing a polygon from outside: define vertices in *counter-clockwise* order as seen from the camera
+- Saves ~50% of triangle rendering
+- Implementation uses signed area: =signedArea < 0= means front-facing
+  (in Y-down screen coordinates, negative signed area corresponds to
+  visually CCW winding)
+
+In *Aukio 3D*, backface culling is *optional* and disabled by default. Enable it per-shape:
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#setBackfaceCulling(boolean)][SolidPolygon.setBackfaceCulling(true)]]
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html#setBackfaceCulling(boolean)][TexturedTriangle.setBackfaceCulling(true)]]
+- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] (applies to all
+  sub-shapes)
+
+See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#winding-order][Winding Order demo]] for an interactive visualization.
+
+** Perspective correct textures
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 1000px
+[[file:Perspective correct textures/Affine distortion.png]]
+
+*Aukio 3D* tries to do perspective-correct texture rendering. Read more
+about [[file:Perspective correct textures/][perspective-correct texture implementation]].
+
+* Developer tools
+:PROPERTIES:
+:CUSTOM_ID: developer-tools
+:ID:       8c5e2a1f-9d3b-4f6a-b8e7-1c4d5f7a9b2e
+:END:
+
+Press *F12* anywhere in the application to open the Developer Tools
+panel:
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 1000px
+[[file:Developer tools/Developer tools.png]]
+
+This debugging interface helps you understand what the engine is doing
+internally and diagnose rendering issues. Pressing F12 again closes
+the panel.
+
+** Diagnostic toggles
+:PROPERTIES:
+:CUSTOM_ID: diagnostic-toggles
+:END:
+
+*** Show polygon borders
+:PROPERTIES:
+:CUSTOM_ID: show-polygon-borders
+:END:
+
+When enabled, each [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] draws yellow outlines around its
+three edges after rendering its texture content. This overlays the
+triangle mesh onto the final image:
+
+#+attr_html: :class responsive-img
+[[file:Developer tools/Render polygon borders.png]]
+
+Use this visualization when investigating:
+
+- Mesh structure: see the actual triangles as the rasterizer receives
+  them
+- Geometry bugs: spot T-junction gaps and overlapping geometry
+- Texture distortion: compare triangle shapes against visible warping
+
+*** Render alternate segments (overdraw debug)
+:PROPERTIES:
+:CUSTOM_ID: render-alternate-segments
+:END:
+
+Renders only even-numbered paint tiles while leaving odd-numbered ones
+black. (The screen is divided into a grid of rectangular tiles for
+parallel rendering — see [[file:Rendering loop/index.org::#phase-4-clear-paint-tiles][the rendering loop documentation]].
+"Segments" is the older name for tiles.)
+
+#+attr_html: :class responsive-img
+[[file:Developer tools/Render alternative segments.png]]
+
+This toggle helps detect overdraw: threads writing outside their
+allocated tile. If you see rendering artifacts in the black tiles, a
+paint task is writing pixels outside its assigned area — a clear sign
+of a bug.
+
+*** Show segment boundaries
+:PROPERTIES:
+:CUSTOM_ID: show-segment-boundaries
+:END:
+
+Draws red lines along the paint tile boundaries, making it easy to see
+exactly where each tile's rendered area begins and ends. In stereo
+mode each eye's viewport gets its own grid:
+
+#+attr_html: :class responsive-img
+[[file:Developer tools/Show segment boundaries.png]]
+
+Useful for:
+
+- Verifying the tile grid division
+- Debugging tile-boundary rendering issues (e.g. clipped text or
+  missing slivers at tile edges)
+- Understanding the parallel rendering architecture visually
+
+** Camera position
+:PROPERTIES:
+:CUSTOM_ID: camera-position
+:END:
+
+Displays the current camera coordinates and orientation in real-time:
+
+| Parameter | Description                              |
+|-----------+------------------------------------------|
+| x, y, z   | Camera position in 3D world space        |
+| yaw       | Rotation around the Y axis (left/right)  |
+| pitch     | Rotation around the X axis (up/down)     |
+| roll      | Rotation around the Z axis (tilt)        |
+
+The *Copy* button copies the full camera position string to the
+clipboard in a format ready to paste into bug reports or configuration
+files.
+
+Use this for:
+- Reporting exact camera positions when filing bugs
+- Saving interesting viewpoints for later reference
+- Understanding camera movement during navigation
+- Sharing specific views with other developers
+
+Example copied format:
+#+BEGIN_EXAMPLE
+500.00, -300.00, -800.00, 0.60, -0.50, -0.00
+#+END_EXAMPLE
+
+The six numbers map 1:1 onto
+[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Transform.html#set(double,double,double,double,double,double)][Transform.set(x, y, z, yaw, pitch, roll)]],
+so a copied viewpoint can be restored at startup — this is how the
+demo applications freeze a good camera position into code:
+
+#+BEGIN_SRC java
+// Camera position captured via Developer Tools -> Copy
+viewPanel.getCamera().getTransform().set(
+        130.66, -65.49, -248.18,   // x, y, z
+        -0.06, -0.36, -0.00);      // yaw, pitch, roll
+#+END_SRC
+
+** Frustum culling statistics
+:PROPERTIES:
+:CUSTOM_ID: frustum-culling-statistics
+:END:
+
+Shows real-time statistics about composite shape frustum culling
+efficiency (see the dedicated [[file:Frustum culling/][frustum culling]] page for how
+culling itself works):
+
+| Statistic | Description                                              |
+|-----------+----------------------------------------------------------|
+| Total     | Number of composite shapes tested against the frustum    |
+| Culled    | Number of composites rejected (outside view frustum)     |
+| Culled %  | Percentage of composites that were culled (0-100%)       |
+
+*How to interpret the numbers:*
+
+- *High cull % (60-90%)*: Excellent — most objects are being correctly culled
+- *Medium cull % (20-60%)*: Moderate — some optimization benefit
+- *Low cull % (0-20%)*: Limited benefit — either all objects are visible, or scene needs restructuring
+
+*Example:*
+#+BEGIN_EXAMPLE
+Total: 473  Culled: 425 (89.9%)
+#+END_EXAMPLE
+
+This means 473 composite shapes were tested, 425 were outside the view
+and skipped entirely, and only 48 composites (with all their children)
+actually needed to be rendered. This is excellent culling efficiency.
+
+The statistics update every 200ms while the panel is open. Note that
+the root composite is never frustum-tested (it's always rendered), so
+the "Total" count excludes it.
+
+** Render threads
+:PROPERTIES:
+:CUSTOM_ID: render-threads
+:END:
+
+Shows the number of active render threads versus available CPU cores.
+The engine defaults to 75% of available threads (at most cores − 1, so
+one thread always stays free for the rest of the system). The count is
+changeable at runtime via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#setNumRenderThreads(int)][ViewPanel.setNumRenderThreads(int)]];
+the worker pool is recreated lazily on the next frame.
+
+** Frame rate
+:PROPERTIES:
+:CUSTOM_ID: frame-rate
+:END:
+
+Shows the current target FPS and the measured production rate (frames
+completed per second, averaged over a ~500 ms window). The measured
+number counts produced frames regardless of how quickly the display
+path presents them — see [[file:Rendering loop/index.org::#frame-rate-control][frame rate control]].
+
+The *Unlock FPS* toggle switches to unlimited (benchmark) mode: the
+engine renders continuously as fast as possible, even when the scene
+is static. Toggling off restores the previously locked target rate.
+
+** Thread activity timeline
+:PROPERTIES:
+:CUSTOM_ID: thread-activity-timeline
+:END:
+
+A per-thread occupancy view — the software-renderer equivalent of a
+GPU frame profiler. Each thread gets a row (the render thread and
+present thread on top, then one row per worker), time runs along the X
+axis, and each colored block is one recorded work interval. Idle time
+is black.
+
+#+attr_html: :class responsive-img
+[[file:Developer tools/Thread timeline.png]]
+
+Press *Record* to start capturing. The colors encode both the task
+kind and which frame the task belongs to — transform, paint, and
+binning come in three frame-parity variants (f0/f1/f2), so you can see
+up to three frames in flight simultaneously. Additional colors mark
+render-thread orchestration, blocked time, blits, and the sort/drain
+sub-phases.
+
+Navigation: mouse wheel scrolls, Ctrl+wheel zooms, and a scrollbar
+moves along the captured range.
+
+The legend colors, exactly as the timeline paints them:
+
+| Color | Legend label | What it shows |
+|-------+--------------+---------------|
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#2ECC40;border:1px solid #444;vertical-align:middle"></span>@@ =#2ECC40= | =transform f0= | Vertex transform chunk task, frame slot 0 |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#B8D900;border:1px solid #444;vertical-align:middle"></span>@@ =#B8D900= | =transform f1= | Vertex transform chunk task, frame slot 1 |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#6B8E23;border:1px solid #444;vertical-align:middle"></span>@@ =#6B8E23= | =transform f2= | Vertex transform chunk task, frame slot 2 |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#0074D9;border:1px solid #444;vertical-align:middle"></span>@@ =#0074D9= | =paint f0= | Paint tile task (clear + rasterize one tile), frame slot 0 |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#F012BE;border:1px solid #444;vertical-align:middle"></span>@@ =#F012BE= | =paint f1= | Paint tile task, frame slot 1 |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#B10DC9;border:1px solid #444;vertical-align:middle"></span>@@ =#B10DC9= | =paint f2= | Paint tile task, frame slot 2 |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#39CCCC;border:1px solid #444;vertical-align:middle"></span>@@ =#39CCCC= | =bin f0= | Tile binning task (assign sorted shapes to tiles), frame slot 0 |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#008B8B;border:1px solid #444;vertical-align:middle"></span>@@ =#008B8B= | =bin f1= | Tile binning task, frame slot 1 |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#007070;border:1px solid #444;vertical-align:middle"></span>@@ =#007070= | =bin f2= | Tile binning task, frame slot 2 |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#A0A0A0;border:1px solid #444;vertical-align:middle"></span>@@ =#A0A0A0= | =render serial= | Render thread orchestration: tree walk and pass submission |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#8B0000;border:1px solid #444;vertical-align:middle"></span>@@ =#8B0000= | =blocked= | Render thread waiting for an older paint pass or the present gate |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#FFFFFF;border:1px solid #444;vertical-align:middle"></span>@@ =#FFFFFF= | =blit= | Present thread copying a finished frame to the screen |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#FF851B;border:1px solid #444;vertical-align:middle"></span>@@ =#FF851B= | =sort+bin= | A pass's async continuation as a whole: drain, sort, bin, submit paint |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#8B4513;border:1px solid #444;vertical-align:middle"></span>@@ =#8B4513= | =drain= | Continuation sub-phase: await and merge parallel transform chunks |
+| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#FFD700;border:1px solid #444;vertical-align:middle"></span>@@ =#FFD700= | =sort= | Continuation sub-phase: depth sort of the pass's shapes |
+
+The f0/f1/f2 suffixes are the frame's projection slot (frame number
+modulo 3) — the triple-buffering from the [[file:Rendering loop/index.org::#software-pipeline][software pipeline]].
+When the pipeline is healthy you see interleaved colors from two or
+three frames on the worker rows at once: paint tasks of an older frame
+overlapping transform and binning of the newer one. Wide =blocked=
+spans on the render row, or worker rows with black gaps, mean the
+pipeline is starved rather than busy.
+
+Recording is cheap but not free (~100 ns per task; a single volatile
+read when disabled), so leave it off during benchmarking runs. The
+captured intervals live in a fixed-size ring buffer — long recordings
+keep only the most recent history.
+
+What to look for:
+
+- *Solidly packed worker rows* mean the pipeline is keeping all cores
+  busy — the design goal (see [[file:Rendering loop/index.org::#software-pipeline][software pipeline]])
+- *Long "blocked" spans on the render row* mean the render thread is
+  waiting for paint passes — workers are the bottleneck
+- *Long "blit" spans on the present row* mean the display path
+  (X server) is the bottleneck; excess frames are being dropped from
+  the presentation mailbox
+
+** Live log viewer
+:PROPERTIES:
+:CUSTOM_ID: live-log-viewer
+:END:
+
+The scrollable text area shows captured debug output in real-time:
+- Green text on black background for readability
+- Auto-scrolls to show latest entries
+- Updates every 200ms while panel is open
+- Captures logs even when panel is closed (replays when reopened)
+
+Use the *Clear Logs* button to reset the log buffer for fresh
+diagnostic captures.
+
+** Headless & agentic tooling
+:PROPERTIES:
+:CUSTOM_ID: headless-agentic
+:END:
+
+For windowless rendering, pixel assertions, golden-image regression
+tests and scene dumps — built for automated verification and AI agents —
+see [[#agentic-development][agentic development tools]].
+
+* Agentic development
+:PROPERTIES:
+:CUSTOM_ID: agentic-development
+:END:
+
+*Aukio 3D* provides good support for automated AI coding agents
+(for example OpenCode, Hermes Agent, etc..).
+
+Thanks to facilities is =eu.svjatoslav.aukio.e3d.headless= package, an
+AI agent can render any scene from any pose, assert what got painted,
+compare against committed reference images, and dump the full scene
+state for a bug report — all without a window, a display, or the
+render thread.
+
+** One pipeline, two drivers
+:PROPERTIES:
+:CUSTOM_ID: one-pipeline
+:END:
+
+The key design decision: the headless path drives *the very same
+transform &rarr; sort &rarr; paint pipeline* the on-screen ViewPanel
+uses. Nothing is reimplemented, so a passing headless test proves the
+real render works, and a bug reproduced headlessly is the real bug.
+
+#+INCLUDE: "Agentic development/Headless lanes.svg" export html
+
+Making this possible required three small engine changes:
+
+- ~ShapeCollection.transformShapes(Camera, RenderingContext)~ — the
+  transform phase now accepts a camera directly; the ViewPanel variant
+  just forwards its camera. Headless code never touches Swing.
+- ~RenderingContext.getImage()~ — hands out the backing BufferedImage
+  the rasterizer paints into.
+- ~GlobalIllumination.isRunning()~ / ~isConverged()~ /
+  ~getWorkItemCount()~ — GI state became inspectable.
+
+** Snapshot: render without a window
+:PROPERTIES:
+:CUSTOM_ID: snapshot
+:END:
+
+#+BEGIN_SRC java
+import eu.svjatoslav.aukio.e3d.headless.Snapshot;
+
+ShapeCollection scene = new ShapeCollection();
+scene.addShape(myShape);
+LightingManager lighting = new LightingManager();
+lighting.setAmbientLight(Color.hex("181818"));
+
+// One call: build context, transform, sort, paint, return the image.
+BufferedImage image = Snapshot.render(scene, lighting,
+        "290.31, -35.59, -2.10, -0.58, -0.15, 0.0", 640, 480);
+Snapshot.save(image, "/tmp/snapshot.png");
+#+END_SRC
+
+The pose string is the *same "x, y, z, yaw, pitch, roll" format the
+demos print* and users quote in bug reports — paste the pose, reproduce
+the exact view. ~Snapshot.cameraFromPose()~ and ~Snapshot.poseString()~
+convert in both directions.
+
+For tests that need to detect *unpainted* pixels (holes), the
+~renderInto()~ variant fills the background with a caller-chosen
+sentinel color first, so "nothing was painted here" is unambiguous even
+in a pitch-black scene:
+
+#+BEGIN_SRC java
+RenderingContext ctx = new RenderingContext(640, 480, 1);
+ctx.lightingManager = lighting;
+Snapshot.renderInto(scene, camera, ctx, 0x00010203); // sentinel
+#+END_SRC
+
+A real headless render — the House demo from a bug-report pose:
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 640px
+[[file:Agentic development/snapshot-example.png]]
+
+** PixelAssertions: did this region get painted?
+:PROPERTIES:
+:CUSTOM_ID: pixel-assertions
+:END:
+
+The recurring debugging question — "did the floor actually render, or
+did clipping eat it?" — becomes a library call:
+
+#+BEGIN_SRC java
+import eu.svjatoslav.aukio.e3d.headless.PixelAssertions;
+
+// Fraction of a relative rectangle still equal to the background:
+double holes = PixelAssertions.unpaintedFraction(image, 0,
+        0.15, 0.45, 0.85, 1.0);   // lower-center band
+if (holes > 0.05)
+    throw new AssertionError("floor has holes: " + holes);
+
+long red = PixelAssertions.countColor(image, 0xFF0000);   // flat-color tests
+String grid = PixelAssertions.dumpPixelGrid(image, 320, 240, 3, 8); // hex dump
+#+END_SRC
+
+#+INCLUDE: "Agentic development/Pixel assertion.svg" export html
+
+** GoldenImage: compare against a reference
+:PROPERTIES:
+:CUSTOM_ID: golden-image
+:END:
+
+A pixel counts as different when any RGB channel drifts more than a
+per-channel tolerance; the comparison fails when the fraction of
+differing pixels exceeds a threshold. Deterministic flat-shaded renders
+can use tight tolerances (4, 0.005); noisier paths relax them.
+
+#+BEGIN_SRC java
+import eu.svjatoslav.aukio.e3d.headless.GoldenImage;
+
+GoldenImage.Result r = GoldenImage.compare(actual,
+        new File("goldens/house-flat.png"), 4, 0.005);
+if (!r.passed)
+    GoldenImage.saveDiff(actual, goldenFile, "/tmp/diff.png"); // red = differs
+#+END_SRC
+
+#+INCLUDE: "Agentic development/Golden workflow.svg" export html
+
+There is also a CLI for shell scripts — exit 0 = match, 1 = differ:
+
+#+BEGIN_SRC bash
+java eu.svjatoslav.aukio.e3d.headless.GoldenImage actual.png golden.png 4 0.005
+#+END_SRC
+
+A real diff: the house rendered with the living-room lamp removed,
+compared against the golden. The red region is exactly the room that
+lost its light:
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 640px
+[[file:Agentic development/diff-example.png]]
+
+** SceneDump: the reproducible bug report
+:PROPERTIES:
+:CUSTOM_ID: scene-dump
+:END:
+
+One call produces everything needed to reproduce what a frame shows:
+
+#+BEGIN_SRC java
+System.out.println(SceneDump.dump(scene, lighting, camera, gi));
+#+END_SRC
+
+#+BEGIN_EXAMPLE
+== SceneDump ==
+shapes: 5 top-level, 546 queued for rendering
+lights: 4 (ambient #181818)
+  [0] pos=(-800.0, -240.0, 0.0) color=FFD890 intensity=6.0
+  [1] pos=(0.0, -240.0, 0.0) color=D8E4FF intensity=5.0
+  [2] pos=(800.0, -240.0, 0.0) color=FFB060 intensity=6.0
+  [3] pos=(250.0, -60.0, -250.0) color=60FF90 intensity=2.0
+camera: 290.31, -35.59, -2.10, -0.58, -0.15, -0.00
+GI: running, 152034 work items, converged
+#+END_EXAMPLE
+
+The camera line is a pose string — it feeds straight back into
+~Snapshot.render()~.
+
+** HouseGoldens: ready-made regression tests
+:PROPERTIES:
+:CUSTOM_ID: house-goldens
+:END:
+
+The aukio-3d-demos repo contains a working example of all of the above:
+~eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens~ renders the
+House demo at two poses (the default view and the near-plane straddle
+bug pose), compares both against committed goldens, and independently
+asserts the floor has no holes.
+
+#+BEGIN_SRC bash
+cd aukio-3d-demos
+mvn clean package
+mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt
+java -cp "target/classes:$(cat cp.txt)" \
+  eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens           # verify
+java -cp "target/classes:$(cat cp.txt)" \
+  eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens --update  # regenerate goldens
+#+END_SRC
+
+Exit code 0 = all pass, 1 = any mismatch (with a diff PNG in /tmp).
+Demos that want the same treatment expose their scene construction:
+~HouseDemo.buildHouse()~, ~addFurniture()~ and ~addLights()~ are public
+for exactly this reason.
+
+** export-docs.sh: regenerate the documentation
+:PROPERTIES:
+:CUSTOM_ID: export-docs
+:END:
+
+All engine documentation (the pages you are reading) lives as org-mode
+files under =doc/=. One script exports every page to HTML with the
+darksun theme:
+
+#+BEGIN_SRC bash
+doc/export-docs.sh            # export all pages
+doc/export-docs.sh --check    # + render every page with headless Chrome
+                              #   to /tmp/doc-check-*.png for visual review
+#+END_SRC
+
+The =--check= mode is how an agent verifies its own documentation: SVG
+label collisions, broken image links and table breakage all show up in
+the rendered screenshots.
+
+** A typical agent session
+:PROPERTIES:
+:CUSTOM_ID: typical-session
+:END:
+
+#+BEGIN_EXAMPLE
+1. Reproduce:   Snapshot.render(scene, lighting, bugReportPose, 640, 480)
+2. Inspect:     SceneDump.dump(...) + view the PNG
+3. Fix the engine
+4. Verify:      PixelAssertions.unpaintedFraction(...) == 0
+5. Regression:  HouseGoldens  (must stay ALL PASS)
+6. Document:    edit doc pages, export-docs.sh --check, review shots
+#+END_EXAMPLE
+
+** Related Classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class           | Purpose                                            |
+|-----------------+----------------------------------------------------|
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/Snapshot.html][Snapshot]]         | Windowless render facade + pose string conversion  |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/PixelAssertions.html][PixelAssertions]]  | Painted-region / color-count / hex-grid assertions |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/GoldenImage.html][GoldenImage]]      | Golden-PNG comparison, diff writer, CLI            |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/SceneDump.html][SceneDump]]        | Scene state as a reproducible text block           |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.html][ShapeCollection]]  | ~transformShapes(Camera, ...)~ headless overload   |
+| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] | ~getImage()~ exposes the painted frame             |
+
+* Source code
+:PROPERTIES:
+:CUSTOM_ID: source-code
+:ID:       978b7ea2-e246-45d0-be76-4d561308e9f3
+:END:
+
+*This program is free software: released under Creative Commons Zero
+(CC0) license*
+
+*Program author:*
+- Svjatoslav Agejenko
+- Homepage: https://svjatoslav.eu
+- Email: mailto://svjatoslav@svjatoslav.eu
+- See also: [[https://www.svjatoslav.eu/projects/][Other software projects hosted at svjatoslav.eu]]
+
+*Getting the source code:*
+- [[https://www2.svjatoslav.eu/gitweb/?p=aukio-3d.git;a=snapshot;h=HEAD;sf=tgz][Download latest source code snapshot in TAR GZ format]]
+- [[https://www2.svjatoslav.eu/gitweb/?p=aukio-3d.git;a=summary][Browse Git repository online]]
+- Clone Git repository using command:
+  : git clone https://www3.svjatoslav.eu/git/aukio-3d.git
diff --git a/Documentation/style.css b/Documentation/style.css
new file mode 100644 (file)
index 0000000..3403e0e
--- /dev/null
@@ -0,0 +1,35 @@
+.flex-center {
+  display: flex;
+  justify-content: center;
+}
+
+.flex-center video {
+  width: min(90%, 1000px);
+  height: auto;
+}
+
+.responsive-img {
+  width: min(100%, 1000px);
+  height: auto;
+}
+
+/* === SVG diagram theme === */
+svg > rect:first-child {
+  fill: #061018;
+}
+
+svg text[fill="#666"],
+svg text[fill="#999"] {
+  fill: #aaa !important;
+}
+
+svg line[stroke="#ccc"] {
+  stroke: #445566 !important;
+}
+
+svg {
+  background-color: #061018;
+  border-radius: 8px;
+  display: block;
+  margin: 0 auto;
+}
\ No newline at end of file
index e80b377..a38f227 100755 (executable)
@@ -26,10 +26,10 @@ export_org_to_html() {
 }
 
 export_org_files_to_html() {
-    echo "🔍 Searching for .org files in doc/ ..."
+    echo "🔍 Searching for .org files in Documentation/ ..."
     echo "======================================="
 
-    mapfile -t ORG_FILES < <(find doc -type f -name "*.org" | sort)
+    mapfile -t ORG_FILES < <(find Documentation -type f -name "*.org" | sort)
 
     if [ ${#ORG_FILES[@]} -eq 0 ]; then
         echo "❌ No .org files found!"
@@ -61,22 +61,22 @@ export_org_files_to_html() {
 }
 
 build_visualization_graphs() {
-    rm -rf doc/graphs/
-    mkdir -p doc/graphs/
+    rm -rf Documentation/graphs/
+    mkdir -p Documentation/graphs/
 
-    javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d doc/graphs/ -n "All classes" -t png -ho
-    javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d doc/graphs/ -n "GUI" -t png -w "eu.svjatoslav.aukio.e3d.gui.*" -ho
-    javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d doc/graphs/ -n "Raster engine" -t png -w "eu.svjatoslav.aukio.e3d.renderer.raster.*" -ho
+    javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d Documentation/graphs/ -n "All classes" -t png -ho
+    javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d Documentation/graphs/ -n "GUI" -t png -w "eu.svjatoslav.aukio.e3d.gui.*" -ho
+    javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d Documentation/graphs/ -n "Raster engine" -t png -w "eu.svjatoslav.aukio.e3d.renderer.raster.*" -ho
 
-    meviz index -w doc/graphs/ -t "Aukio 3D classes"
+    meviz index -w Documentation/graphs/ -t "Aukio 3D classes"
 }
 
 # Build project jar file and JavaDocs
 mvn clean package
 
 # Put generated JavaDoc HTML files to documentation directory
-rm -rf doc/apidocs/
-cp -r target/apidocs/ doc/
+rm -rf Documentation/apidocs/
+cp -r target/apidocs/ Documentation/
 
 # Publish Emacs org-mode files into HTML format
 export_org_files_to_html
@@ -87,7 +87,7 @@ build_visualization_graphs
 
 ## Upload assembled documentation to server
 echo "📤 Uploading to server..."
-rsync -avz --delete -e 'ssh -p 10006' doc/ \
+rsync -avz --delete -e 'ssh -p 10006' Documentation/ \
       n0@www3.svjatoslav.eu:/mnt/big/projects/aukio-3d/
 
 if [ $? -eq 0 ]; then
diff --git a/doc/Agentic development/Golden workflow.svg b/doc/Agentic development/Golden workflow.svg
deleted file mode 100644 (file)
index aa0e7fb..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-<svg viewBox="0 0 620 190" width="620" height="190" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="1.5" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-    <marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto">
-      <path d="M0 0 L8 4 L0 8 Z" fill="#40b0d0"/>
-    </marker>
-    <marker id="arrRed" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto">
-      <path d="M0 0 L8 4 L0 8 Z" fill="#FF4444"/>
-    </marker>
-  </defs>
-
-  <rect width="620" height="190" fill="#061018"/>
-  <g stroke="#1a3a4a" stroke-width="0.5">
-    <line x1="0" y1="40" x2="620" y2="40"/><line x1="0" y1="80" x2="620" y2="80"/>
-    <line x1="0" y1="120" x2="620" y2="120"/><line x1="0" y1="160" x2="620" y2="160"/>
-    <line x1="40" y1="0" x2="40" y2="190"/><line x1="80" y1="0" x2="80" y2="190"/>
-    <line x1="120" y1="0" x2="120" y2="190"/><line x1="160" y1="0" x2="160" y2="190"/>
-    <line x1="200" y1="0" x2="200" y2="190"/><line x1="240" y1="0" x2="240" y2="190"/>
-    <line x1="280" y1="0" x2="280" y2="190"/><line x1="320" y1="0" x2="320" y2="190"/>
-    <line x1="360" y1="0" x2="360" y2="190"/><line x1="400" y1="0" x2="400" y2="190"/>
-    <line x1="440" y1="0" x2="440" y2="190"/><line x1="480" y1="0" x2="480" y2="190"/>
-    <line x1="520" y1="0" x2="520" y2="190"/><line x1="560" y1="0" x2="560" y2="190"/>
-    <line x1="600" y1="0" x2="600" y2="190"/>
-  </g>
-
-  <!-- step 1: render -->
-  <rect x="20" y="55" width="110" height="60" rx="8" fill="rgba(32,112,192,0.12)" stroke="#2070c0" stroke-width="2"/>
-  <text x="75" y="80" fill="#2070c0" font-size="10" font-family="monospace" text-anchor="middle">Snapshot.render</text>
-  <text x="75" y="97" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">scene + pose</text>
-
-  <line x1="130" y1="85" x2="158" y2="85" stroke="#40b0d0" stroke-width="2" marker-end="url(#arr)"/>
-
-  <!-- step 2: golden file -->
-  <rect x="160" y="20" width="110" height="40" rx="8" fill="rgba(192,80,136,0.12)" stroke="#c05088" stroke-width="2"/>
-  <text x="215" y="38" fill="#c05088" font-size="10" font-family="monospace" text-anchor="middle">goldens/*.png</text>
-  <text x="215" y="52" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">committed reference</text>
-  <line x1="215" y1="60" x2="215" y2="80" stroke="#c05088" stroke-width="1.5" stroke-dasharray="3 2" marker-end="url(#arr)"/>
-
-  <!-- step 3: compare decision -->
-  <polygon points="215,85 265,55 315,85 265,115" fill="rgba(64,176,208,0.12)" stroke="#40b0d0" stroke-width="2"/>
-  <text x="265" y="82" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle">GoldenImage</text>
-  <text x="265" y="95" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle">.compare</text>
-
-  <!-- PASS branch -->
-  <line x1="315" y1="85" x2="348" y2="85" stroke="#39FF14" stroke-width="2" marker-end="url(#arr)"/>
-  <rect x="350" y="60" width="90" height="50" rx="8" fill="rgba(57,255,20,0.12)" stroke="#39FF14" stroke-width="2"/>
-  <text x="395" y="82" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#glow)">PASS</text>
-  <text x="395" y="98" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">exit 0</text>
-
-  <!-- FAIL branch -->
-  <path d="M265 115 L265 140 L 348 140" stroke="#FF4444" stroke-width="2" fill="none" marker-end="url(#arrRed)"/>
-  <rect x="350" y="115" width="120" height="50" rx="8" fill="rgba(255,68,68,0.1)" stroke="#FF4444" stroke-width="2"/>
-  <text x="410" y="136" fill="#FF4444" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#glow)">FAIL, exit 1</text>
-  <text x="410" y="152" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">+ diff PNG to /tmp</text>
-
-  <!-- human decision -->
-  <line x1="470" y1="140" x2="498" y2="140" stroke="#FF4444" stroke-width="2" marker-end="url(#arrRed)"/>
-  <rect x="500" y="115" width="100" height="50" rx="8" fill="rgba(255,136,51,0.1)" stroke="#FF8833" stroke-width="2"/>
-  <text x="550" y="133" fill="#FF8833" font-size="9" font-family="monospace" text-anchor="middle">bug? fix code</text>
-  <text x="550" y="146" fill="#FF8833" font-size="9" font-family="monospace" text-anchor="middle">intended? run</text>
-  <text x="550" y="158" fill="#FF8833" font-size="9" font-family="monospace" text-anchor="middle">--update</text>
-
-  <!-- update loop back to golden -->
-  <path d="M550 115 L 550 30 L 272 30" stroke="#FF8833" stroke-width="1.5" fill="none" stroke-dasharray="4 3" marker-end="url(#arr)"/>
-  <text x="420" y="22" fill="#FF8833" font-size="9" font-family="monospace" text-anchor="middle">regenerates reference</text>
-
-  <text x="310" y="182" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">tolerance: per-channel delta + max differing-pixel fraction — shading is deterministic, keep both tight</text>
-</svg>
diff --git a/doc/Agentic development/Headless lanes.svg b/doc/Agentic development/Headless lanes.svg
deleted file mode 100644 (file)
index 1303a80..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="2" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-    <marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto">
-      <path d="M0 0 L8 4 L0 8 Z" fill="#40b0d0"/>
-    </marker>
-  </defs>
-
-  <rect width="640" height="480" fill="#061018"/>
-  <g stroke="#1a3a4a" stroke-width="0.5">
-    <line x1="0" y1="40" x2="640" y2="40"/><line x1="0" y1="80" x2="640" y2="80"/>
-    <line x1="0" y1="120" x2="640" y2="120"/><line x1="0" y1="160" x2="640" y2="160"/>
-    <line x1="0" y1="200" x2="640" y2="200"/><line x1="0" y1="240" x2="640" y2="240"/>
-    <line x1="0" y1="280" x2="640" y2="280"/><line x1="0" y1="320" x2="640" y2="320"/>
-    <line x1="0" y1="360" x2="640" y2="360"/><line x1="0" y1="400" x2="640" y2="400"/>
-    <line x1="0" y1="440" x2="640" y2="440"/>
-    <line x1="40" y1="0" x2="40" y2="480"/><line x1="80" y1="0" x2="80" y2="480"/>
-    <line x1="120" y1="0" x2="120" y2="480"/><line x1="160" y1="0" x2="160" y2="480"/>
-    <line x1="200" y1="0" x2="200" y2="480"/><line x1="240" y1="0" x2="240" y2="480"/>
-    <line x1="280" y1="0" x2="280" y2="480"/><line x1="320" y1="0" x2="320" y2="480"/>
-    <line x1="360" y1="0" x2="360" y2="480"/><line x1="400" y1="0" x2="400" y2="480"/>
-    <line x1="440" y1="0" x2="440" y2="480"/><line x1="480" y1="0" x2="480" y2="480"/>
-    <line x1="520" y1="0" x2="520" y2="480"/><line x1="560" y1="0" x2="560" y2="480"/>
-    <line x1="600" y1="0" x2="600" y2="480"/>
-  </g>
-
-  <!-- ============ two entry lanes feeding ONE shared pipeline ============ -->
-
-  <!-- lane 1: on-screen app -->
-  <rect x="40" y="60" width="200" height="66" rx="8" fill="rgba(255,136,51,0.12)" stroke="#FF8833" stroke-width="2"/>
-  <text x="140" y="86" fill="#FF8833" font-size="12" font-family="monospace" text-anchor="middle" filter="url(#glow)">ViewPanel</text>
-  <text x="140" y="103" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">window + render thread</text>
-  <text x="140" y="116" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">camera from user input</text>
-
-  <!-- lane 2: headless -->
-  <rect x="40" y="354" width="200" height="66" rx="8" fill="rgba(57,255,20,0.1)" stroke="#39FF14" stroke-width="2"/>
-  <text x="140" y="380" fill="#39FF14" font-size="12" font-family="monospace" text-anchor="middle" filter="url(#glow)">Snapshot.render()</text>
-  <text x="140" y="397" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">no window, no display</text>
-  <text x="140" y="410" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">camera from pose string</text>
-
-  <!-- shared pipeline core -->
-  <rect x="300" y="170" width="140" height="140" rx="10" fill="rgba(32,112,192,0.12)" stroke="#2070c0" stroke-width="2.5"/>
-  <text x="370" y="196" fill="#2070c0" font-size="13" font-family="monospace" text-anchor="middle" filter="url(#glow)">SAME pipeline</text>
-  <text x="370" y="228" fill="#ccc" font-size="10" font-family="monospace" text-anchor="middle">transform</text>
-  <text x="370" y="248" fill="#ccc" font-size="10" font-family="monospace" text-anchor="middle">&#8595;</text>
-  <text x="370" y="266" fill="#ccc" font-size="10" font-family="monospace" text-anchor="middle">sort by Z</text>
-  <text x="370" y="284" fill="#ccc" font-size="10" font-family="monospace" text-anchor="middle">&#8595;</text>
-  <text x="370" y="302" fill="#ccc" font-size="10" font-family="monospace" text-anchor="middle">paint</text>
-
-  <!-- outputs -->
-  <rect x="500" y="60" width="110" height="50" rx="8" fill="rgba(255,136,51,0.08)" stroke="#FF8833" stroke-width="1.5"/>
-  <text x="555" y="81" fill="#FF8833" font-size="11" font-family="monospace" text-anchor="middle">screen</text>
-  <text x="555" y="97" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">BufferStrategy</text>
-
-  <rect x="490" y="330" width="130" height="90" rx="8" fill="rgba(57,255,20,0.07)" stroke="#39FF14" stroke-width="1.5"/>
-  <text x="555" y="352" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">BufferedImage</text>
-  <text x="555" y="370" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">&#8594; PNG (Snapshot.save)</text>
-  <text x="555" y="385" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">&#8594; PixelAssertions</text>
-  <text x="555" y="400" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">&#8594; GoldenImage</text>
-
-  <!-- lane arrows converging into the core -->
-  <path d="M240 93 C 290 93, 270 210, 298 225" stroke="#40b0d0" stroke-width="2" fill="none" marker-end="url(#arr)"/>
-  <path d="M240 387 C 290 387, 270 270, 298 255" stroke="#40b0d0" stroke-width="2" fill="none" marker-end="url(#arr)"/>
-
-  <!-- output arrows -->
-  <path d="M440 210 C 480 210, 460 90, 498 88" stroke="#40b0d0" stroke-width="2" fill="none" marker-end="url(#arr)"/>
-  <path d="M440 270 C 480 270, 460 372, 488 374" stroke="#40b0d0" stroke-width="2" fill="none" marker-end="url(#arr)"/>
-
-  <!-- emphasis labels -->
-  <text x="270" y="150" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle">identical results</text>
-  <text x="320" y="462" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">headless rendering drives the very same code the window uses — a test render IS the real render</text>
-</svg>
diff --git a/doc/Agentic development/Pixel assertion.svg b/doc/Agentic development/Pixel assertion.svg
deleted file mode 100644 (file)
index a9218f6..0000000
+++ /dev/null
@@ -1,65 +0,0 @@
-<svg viewBox="0 0 620 300" width="620" height="300" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="1.5" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-    <marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto">
-      <path d="M0 0 L8 4 L0 8 Z" fill="#40b0d0"/>
-    </marker>
-  </defs>
-
-  <rect width="620" height="300" fill="#061018"/>
-  <g stroke="#1a3a4a" stroke-width="0.5">
-    <line x1="0" y1="40" x2="620" y2="40"/><line x1="0" y1="80" x2="620" y2="80"/>
-    <line x1="0" y1="120" x2="620" y2="120"/><line x1="0" y1="160" x2="620" y2="160"/>
-    <line x1="0" y1="200" x2="620" y2="200"/><line x1="0" y1="240" x2="620" y2="240"/>
-    <line x1="0" y1="280" x2="620" y2="280"/>
-    <line x1="40" y1="0" x2="40" y2="300"/><line x1="80" y1="0" x2="80" y2="300"/>
-    <line x1="120" y1="0" x2="120" y2="300"/><line x1="160" y1="0" x2="160" y2="300"/>
-    <line x1="200" y1="0" x2="200" y2="300"/><line x1="240" y1="0" x2="240" y2="300"/>
-    <line x1="280" y1="0" x2="280" y2="300"/><line x1="320" y1="0" x2="320" y2="300"/>
-    <line x1="360" y1="0" x2="360" y2="300"/><line x1="400" y1="0" x2="400" y2="300"/>
-    <line x1="440" y1="0" x2="440" y2="300"/><line x1="480" y1="0" x2="480" y2="300"/>
-    <line x1="520" y1="0" x2="520" y2="300"/><line x1="560" y1="0" x2="560" y2="300"/>
-    <line x1="600" y1="0" x2="600" y2="300"/>
-  </g>
-
-  <!-- fake rendered frame: sentinel background + painted shapes -->
-  <rect x="60" y="40" width="320" height="220" fill="#0a1520" stroke="#40b0d0" stroke-width="2"/>
-  <text x="220" y="32" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle">rendered frame (640&#215;480)</text>
-
-  <!-- painted content: simple house-like blocks -->
-  <rect x="90" y="120" width="80" height="100" fill="rgba(192,80,136,0.55)" stroke="#c05088" stroke-width="1.5"/>
-  <rect x="190" y="80" width="120" height="60" fill="rgba(32,112,192,0.45)" stroke="#2070c0" stroke-width="1.5"/>
-  <rect x="190" y="160" width="150" height="80" fill="rgba(192,80,136,0.35)" stroke="#c05088" stroke-width="1.5"/>
-  <text x="130" y="175" fill="#e090b8" font-size="9" font-family="monospace" text-anchor="middle">painted</text>
-  <text x="255" y="205" fill="#e090b8" font-size="9" font-family="monospace" text-anchor="middle">painted</text>
-
-  <!-- the probed region: relative rect -->
-  <rect x="108" y="139" width="224" height="121" fill="none" stroke="#39FF14" stroke-width="2.5" stroke-dasharray="8 4"/>
-  <text x="338" y="132" fill="#39FF14" font-size="9" font-family="monospace" text-anchor="end" filter="url(#glow)">region (0.15, 0.45) &#8594; (0.85, 1.0)</text>
-
-  <!-- a hole: sentinel showing through -->
-  <rect x="225" y="200" width="45" height="30" fill="#000000" stroke="#FF4444" stroke-width="2" stroke-dasharray="4 3"/>
-  <text x="247" y="218" fill="#FF4444" font-size="9" font-family="monospace" text-anchor="middle">hole</text>
-
-  <!-- right: the assertion code and verdict -->
-  <rect x="410" y="60" width="195" height="150" rx="8" fill="rgba(64,176,208,0.08)" stroke="#40b0d0" stroke-width="1.5"/>
-  <text x="508" y="80" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle" filter="url(#glow)">PixelAssertions</text>
-  <text x="420" y="102" fill="#ccc" font-size="9" font-family="monospace">unpaintedFraction(img,</text>
-  <text x="420" y="116" fill="#ccc" font-size="9" font-family="monospace">  bg=0x000000,</text>
-  <text x="420" y="130" fill="#ccc" font-size="9" font-family="monospace">  0.15, 0.45,</text>
-  <text x="420" y="144" fill="#ccc" font-size="9" font-family="monospace">  0.85, 1.0)</text>
-  <text x="420" y="168" fill="#999" font-size="9" font-family="monospace">counts pixels that still</text>
-  <text x="420" y="181" fill="#999" font-size="9" font-family="monospace">equal the background</text>
-  <text x="420" y="200" fill="#FF4444" font-size="10" font-family="monospace">&#8594; 0.017 &gt; 0.01 FAIL</text>
-
-  <line x1="380" y1="150" x2="408" y2="140" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr)"/>
-
-  <!-- bottom notes -->
-  <text x="60" y="282" fill="#999" font-size="9" font-family="monospace">sentinel background: "nothing rendered here" is unambiguous, even in dark scenes</text>
-</svg>
diff --git a/doc/Agentic development/diff-example.png b/doc/Agentic development/diff-example.png
deleted file mode 100644 (file)
index 941d5ca..0000000
Binary files a/doc/Agentic development/diff-example.png and /dev/null differ
diff --git a/doc/Agentic development/snapshot-example.png b/doc/Agentic development/snapshot-example.png
deleted file mode 100644 (file)
index 92ea03c..0000000
Binary files a/doc/Agentic development/snapshot-example.png and /dev/null differ
diff --git a/doc/CSG/BSP tree.svg b/doc/CSG/BSP tree.svg
deleted file mode 100644 (file)
index eb89c2c..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-<svg viewBox="0 0 620 220" width="620" height="220" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="bsp-glow"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-  </defs>
-  <rect width="620" height="220" fill="#061018"/>
-
-  <!-- ── Root node ── -->
-  <rect x="250" y="18" width="120" height="32" rx="4" fill="rgba(64,176,208,0.15)" stroke="#40b0d0" stroke-width="1.5"/>
-  <text x="310" y="39" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#bsp-glow)">Plane P₁</text>
-
-  <!-- ── Connectors: root → children ── -->
-  <line x1="310" y1="50" x2="310" y2="62" stroke="#40b0d0" stroke-width="1"/>
-  <line x1="310" y1="62" x2="160" y2="62" stroke="#40b0d0" stroke-width="1"/>
-  <line x1="310" y1="62" x2="460" y2="62" stroke="#40b0d0" stroke-width="1"/>
-  <line x1="160" y1="62" x2="160" y2="72" stroke="#40b0d0" stroke-width="1"/>
-  <line x1="460" y1="62" x2="460" y2="72" stroke="#40b0d0" stroke-width="1"/>
-  <!-- Branch labels -->
-  <text x="225" y="58" fill="#30a050" font-size="8" font-family="monospace" text-anchor="middle">front</text>
-  <text x="395" y="58" fill="#d04040" font-size="8" font-family="monospace" text-anchor="middle">back</text>
-
-  <!-- ── Front child ── -->
-  <rect x="100" y="72" width="120" height="32" rx="4" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
-  <text x="160" y="93" fill="#30a050" font-size="11" font-family="monospace" text-anchor="middle">Front (P₂)</text>
-
-  <!-- ── Back child ── -->
-  <rect x="400" y="72" width="120" height="32" rx="4" fill="rgba(208,64,64,0.15)" stroke="#d04040" stroke-width="1.5"/>
-  <text x="460" y="93" fill="#d04040" font-size="11" font-family="monospace" text-anchor="middle">Back (P₃)</text>
-
-  <!-- ── Connectors: front child → grandchildren ── -->
-  <line x1="160" y1="104" x2="160" y2="116" stroke="#30a050" stroke-width="1"/>
-  <line x1="160" y1="116" x2="85" y2="116" stroke="#30a050" stroke-width="1"/>
-  <line x1="160" y1="116" x2="235" y2="116" stroke="#30a050" stroke-width="1"/>
-  <line x1="85" y1="116" x2="85" y2="126" stroke="#30a050" stroke-width="1"/>
-  <line x1="235" y1="116" x2="235" y2="126" stroke="#30a050" stroke-width="1"/>
-
-  <!-- ── Leaf nodes ── -->
-  <rect x="45" y="126" width="80" height="28" rx="4" fill="rgba(100,100,100,0.08)" stroke="#aaa" stroke-width="1" stroke-dasharray="3 2"/>
-  <text x="85" y="145" fill="#bbb" font-size="10" font-family="monospace" text-anchor="middle">leaf</text>
-  <rect x="195" y="126" width="80" height="28" rx="4" fill="rgba(100,100,100,0.08)" stroke="#aaa" stroke-width="1" stroke-dasharray="3 2"/>
-  <text x="235" y="145" fill="#bbb" font-size="10" font-family="monospace" text-anchor="middle">leaf</text>
-
-  <!-- ── Explanation ── -->
-  <text x="310" y="185" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Each plane divides space into front (normal side) and back (opposite)</text>
-  <text x="310" y="200" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Polygons are classified and split at each partitioning plane</text>
-</svg>
diff --git a/doc/CSG/CSG demo.png b/doc/CSG/CSG demo.png
deleted file mode 100644 (file)
index 2275350..0000000
Binary files a/doc/CSG/CSG demo.png and /dev/null differ
diff --git a/doc/CSG/CSG intersect.svg b/doc/CSG/CSG intersect.svg
deleted file mode 100644 (file)
index a912f81..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<svg viewBox="0 0 620 170" width="620" height="170" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="i-glow"><feGaussianBlur stdDeviation="2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-    <clipPath id="i-clip-rect"><rect x="370" y="35" width="80" height="80"/></clipPath>
-    <clipPath id="i-clip-circ"><circle cx="450" cy="75" r="42"/></clipPath>
-  </defs>
-  <rect width="620" height="170" fill="#061018"/>
-
-  <!-- ── Input ── -->
-  <text x="110" y="20" fill="#ccc" font-size="11" font-family="monospace" text-anchor="middle">Input</text>
-  <rect x="50" y="35" width="80" height="80" rx="2" fill="rgba(57,255,20,0.2)" stroke="#39FF14" stroke-width="2"/>
-  <text x="90" y="80" fill="#39FF14" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">A</text>
-  <circle cx="130" cy="75" r="42" fill="rgba(255,102,0,0.2)" stroke="#FF6600" stroke-width="2"/>
-  <text x="148" y="80" fill="#FF6600" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">B</text>
-
-  <!-- ── Operator ── -->
-  <text x="250" y="70" fill="#40b0d0" font-size="22" font-family="monospace" text-anchor="middle" filter="url(#i-glow)">∩</text>
-  <text x="250" y="92" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle">intersect</text>
-
-  <!-- ── Arrow ── -->
-  <line x1="280" y1="78" x2="330" y2="78" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="4 3"/>
-  <polygon points="330,73 340,78 330,83" fill="#40b0d0"/>
-
-  <!-- ── Result: only the overlap region ── -->
-  <text x="440" y="20" fill="#ccc" font-size="11" font-family="monospace" text-anchor="middle">Result: A ∩ B</text>
-  <!-- Ghost outlines of original shapes (faint) -->
-  <rect x="370" y="35" width="80" height="80" rx="2" fill="none" stroke="#39FF14" stroke-width="0.5" opacity="0.2" stroke-dasharray="4 3"/>
-  <circle cx="450" cy="75" r="42" fill="none" stroke="#FF6600" stroke-width="0.5" opacity="0.2" stroke-dasharray="4 3"/>
-  <!-- Intersection fill: lens shape = circle arc clipped to rect -->
-  <circle cx="450" cy="75" r="42" fill="rgba(57,255,20,0.08)" stroke="none" clip-path="url(#i-clip-rect)"/>
-  <!-- Left boundary: arc from circle (orange, from B) -->
-  <path d="M450,33 A42,42 0 0,0 450,117" fill="none" stroke="#FF6600" stroke-width="1.5" clip-path="url(#i-clip-rect)"/>
-  <!-- Right boundary: straight edge from rect (green, from A) -->
-  <line x1="450" y1="35" x2="450" y2="115" stroke="#39FF14" stroke-width="1.5" clip-path="url(#i-clip-circ)"/>
-
-  <!-- ── Descriptions ── -->
-  <text x="110" y="142" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Find overlap</text>
-  <text x="110" y="155" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">between both</text>
-  <text x="440" y="142" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Only shared volume</text>
-  <text x="440" y="155" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">remains</text>
-</svg>
diff --git a/doc/CSG/CSG operations.svg b/doc/CSG/CSG operations.svg
deleted file mode 100644 (file)
index 3f73cbe..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<svg viewBox="0 0 620 170" width="620" height="170" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="s-glow"><feGaussianBlur stdDeviation="2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-    <clipPath id="s-cube"><rect x="370" y="35" width="80" height="80"/></clipPath>
-  </defs>
-  <rect width="620" height="170" fill="#061018"/>
-
-  <!-- ── Input ── -->
-  <text x="110" y="20" fill="#aaa" font-size="11" font-family="monospace" text-anchor="middle">Input</text>
-  <rect x="50" y="35" width="80" height="80" rx="2" fill="rgba(57,255,20,0.2)" stroke="#39FF14" stroke-width="2"/>
-  <text x="90" y="80" fill="#39FF14" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">A</text>
-  <circle cx="130" cy="75" r="42" fill="rgba(255,102,0,0.12)" stroke="#FF6600" stroke-width="2" stroke-dasharray="5 3"/>
-  <text x="148" y="80" fill="#FF6600" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">B</text>
-
-  <!-- ── Operator ── -->
-  <text x="250" y="70" fill="#40b0d0" font-size="22" font-family="monospace" text-anchor="middle" filter="url(#s-glow)">−</text>
-  <text x="250" y="92" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle">subtract</text>
-
-  <!-- ── Arrow ── -->
-  <line x1="280" y1="78" x2="330" y2="78" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="4 3"/>
-  <polygon points="330,73 340,78 330,83" fill="#40b0d0"/>
-
-  <!-- ── Result ── -->
-  <text x="440" y="20" fill="#aaa" font-size="11" font-family="monospace" text-anchor="middle">Result: A − B</text>
-  <!-- Cube body -->
-  <rect x="370" y="35" width="80" height="80" rx="2" fill="rgba(57,255,20,0.15)" stroke="#39FF14" stroke-width="1.5"/>
-  <!-- Cavity: dark hole with single solid arc edge -->
-  <circle cx="450" cy="75" r="42" fill="rgba(6,16,24,0.8)" stroke="none" clip-path="url(#s-cube)"/>
-  <path d="M450,33 A42,42 0 0,0 450,117" fill="none" stroke="#FF6600" stroke-width="1.5" clip-path="url(#s-cube)"/>
-  <text x="425" y="78" fill="#40b0d0" font-size="7" font-family="monospace" text-anchor="middle" opacity="0.7">cavity</text>
-
-  <!-- ── Descriptions ── -->
-  <text x="110" y="142" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">B is the "cutter"</text>
-  <text x="110" y="155" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">carves out of A</text>
-  <text x="440" y="142" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">Cube with cavity</text>
-  <text x="440" y="155" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">interior faces visible</text>
-</svg>
diff --git a/doc/CSG/CSG union.svg b/doc/CSG/CSG union.svg
deleted file mode 100644 (file)
index f1eedec..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<svg viewBox="0 0 620 170" width="620" height="170" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="u-glow"><feGaussianBlur stdDeviation="2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-  </defs>
-  <rect width="620" height="170" fill="#061018"/>
-
-  <!-- ── Input ── -->
-  <text x="110" y="20" fill="#ccc" font-size="11" font-family="monospace" text-anchor="middle">Input</text>
-  <rect x="50" y="35" width="80" height="80" rx="2" fill="rgba(57,255,20,0.2)" stroke="#39FF14" stroke-width="2"/>
-  <text x="90" y="80" fill="#39FF14" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">A</text>
-  <circle cx="130" cy="75" r="42" fill="rgba(255,102,0,0.2)" stroke="#FF6600" stroke-width="2"/>
-  <text x="148" y="80" fill="#FF6600" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">B</text>
-
-  <!-- ── Operator ── -->
-  <text x="250" y="70" fill="#40b0d0" font-size="22" font-family="monospace" text-anchor="middle" filter="url(#u-glow)">+</text>
-  <text x="250" y="92" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle">union</text>
-
-  <!-- ── Arrow ── -->
-  <line x1="280" y1="78" x2="330" y2="78" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="4 3"/>
-  <polygon points="330,73 340,78 330,83" fill="#40b0d0"/>
-
-  <!-- ── Result ── -->
-  <text x="440" y="20" fill="#ccc" font-size="11" font-family="monospace" text-anchor="middle">Result: A + B</text>
-  <!-- Merged outer boundary: cube left + top + circle right + cube bottom -->
-  <path d="M370,35 L370,115 L450,115 L450,103 A42,42 0 0,0 450,47 L450,35 Z"
-        fill="rgba(57,255,20,0.12)" stroke="#39FF14" stroke-width="1.5"/>
-  <path d="M450,47 A42,42 0 0,1 450,103"
-        fill="rgba(255,102,0,0.12)" stroke="#FF6600" stroke-width="1.5"/>
-  <!-- Interior seam removed indicator -->
-  <line x1="450" y1="47" x2="450" y2="103" stroke="#40b0d0" stroke-width="1" stroke-dasharray="3 2" opacity="0.5"/>
-  <text x="458" y="78" fill="#40b0d0" font-size="7" font-family="monospace" opacity="0.7">removed</text>
-
-  <!-- ── Descriptions ── -->
-  <text x="110" y="142" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Keeps all geometry</text>
-  <text x="110" y="155" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">from both shapes</text>
-  <text x="440" y="142" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Single combined volume</text>
-  <text x="440" y="155" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">interior faces removed</text>
-</svg>
diff --git a/doc/CSG/Polygon clipping.svg b/doc/CSG/Polygon clipping.svg
deleted file mode 100644 (file)
index 41de628..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<svg viewBox="0 0 620 190" width="620" height="190" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="c-glow"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-  </defs>
-  <rect width="620" height="190" fill="#061018"/>
-
-  <!-- ── Original polygon crossing a plane ── -->
-  <text x="120" y="20" fill="#ccc" font-size="11" font-family="monospace" text-anchor="middle">Polygon crosses plane</text>
-  <polygon points="60,50 180,50 120,140" fill="rgba(32,112,192,0.15)" stroke="#2070c0" stroke-width="1.5"/>
-  <line x1="30" y1="70" x2="210" y2="110" stroke="#40b0d0" stroke-width="2" filter="url(#c-glow)"/>
-  <text x="38" y="64" fill="#40b0d0" font-size="9" font-family="monospace">plane</text>
-  <circle cx="81" cy="81" r="3.5" fill="#40b0d0"/>
-  <circle cx="149" cy="96" r="3.5" fill="#40b0d0"/>
-
-  <!-- ── Arrow ── -->
-  <text x="268" y="85" fill="#40b0d0" font-size="18" font-family="monospace" text-anchor="middle" filter="url(#c-glow)">→</text>
-  <text x="268" y="105" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle">split</text>
-
-  <!-- ── Split result ── -->
-  <text x="460" y="20" fill="#ccc" font-size="11" font-family="monospace" text-anchor="middle">Split into fragments</text>
-  <line x1="370" y1="70" x2="550" y2="110" stroke="#40b0d0" stroke-width="0.7" opacity="0.25" stroke-dasharray="4 3"/>
-
-  <!-- Front fragment (above plane) — trapezoid -->
-  <polygon points="400,50 520,50 489,96 421,81" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
-  <text x="458" y="68" fill="#30a050" font-size="9" font-family="monospace" text-anchor="middle">front</text>
-
-  <!-- Back fragment (below plane) — triangle -->
-  <polygon points="421,81 489,96 460,140" fill="rgba(208,64,64,0.15)" stroke="#d04040" stroke-width="1.5"/>
-  <text x="457" y="115" fill="#d04040" font-size="9" font-family="monospace" text-anchor="middle">back</text>
-
-  <!-- New edge at split -->
-  <line x1="421" y1="81" x2="489" y2="96" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="4 2"/>
-  <circle cx="421" cy="81" r="3.5" fill="#40b0d0"/>
-  <circle cx="489" cy="96" r="3.5" fill="#40b0d0"/>
-  <text x="470" y="78" fill="#40b0d0" font-size="7" font-family="monospace" text-anchor="middle" opacity="0.8">new edge</text>
-
-  <!-- ── Explanation ── -->
-  <text x="310" y="172" fill="#ccc" font-size="9" font-family="monospace" text-anchor="middle">Spanning polygons are split; each fragment goes to its respective subtree</text>
-</svg>
diff --git a/doc/CSG/index.org b/doc/CSG/index.org
deleted file mode 100644 (file)
index ebdbad6..0000000
+++ /dev/null
@@ -1,284 +0,0 @@
-#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
-#+TITLE: Constructive Solid Geometry - Aukio 3D
-#+LANGUAGE: en
-#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
-#+LATEX_HEADER: \usepackage{parskip}
-#+LATEX_HEADER: \usepackage[none]{hyphenat}
-
-#+OPTIONS: H:20 num:20
-#+OPTIONS: author:nil
-
-#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
-
-[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
-
-* What is CSG?
-:PROPERTIES:
-:CUSTOM_ID: what-is-csg
-:ID:       a1b2c3d4-e5f6-7890-abcd-ef1234567890
-:END:
-
-*Constructive Solid Geometry* (CSG) is a modeling technique that builds
-complex 3D shapes by combining simpler primitives using boolean
-operations. Instead of manually creating every vertex and face, you
-define shapes as the result of operations like "merge these two cubes"
-or "carve a hole using this sphere."
-
-CSG is particularly powerful for:
-- *Procedural modeling* — generate complex geometry algorithmically
-- *CAD/CAM applications* — define parts as combinations of primitives
-- *Game development* — create architectural elements, holes, cavities
-- *Rapid prototyping* — iterate on designs by adjusting operations
-
-The three fundamental CSG operations are:
-
-| Operation   | Symbol | Result                                    |
-|-------------+--------+-------------------------------------------|
-| Subtract    | A - B  | A with B carved out (holes, cavities)     |
-| Union       | A + B  | Combined volume (both shapes merged)      |
-| Intersect   | A ∩ B  | Volume where both overlap                 |
-
-See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#csg-demo][CSG demo]] for an interactive visualization.
-
-* The Three Operations
-:PROPERTIES:
-:CUSTOM_ID: the-three-operations
-:END:
-
-#+attr_html: :class responsive-img
-#+attr_latex: :width 1000px
-[[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#csg-demo][file:CSG%20demo.png]]
-
-The screenshot above shows all three operations displayed left to right:
-subtract (green cube with spherical cavity), union (merged green and
-orange shapes), and intersect (only the overlapping region in blue).
-
-The diagrams below use the same green cube (A) and orange sphere (B) as
-the screenshot above. Each operation transforms these inputs differently,
-producing the results shown from left to right in the image.
-
-** Subtract (A - B)
-:PROPERTIES:
-:CUSTOM_ID: subtract-operation
-:END:
-
-#+INCLUDE: "CSG operations.svg" export html
-
-*Subtract* removes the orange sphere (B) from the green cube (A), carving
-out a cavity. The diagram shows B acting as a "cutter" — where it overlaps
-A, a hole is created. Interior faces *are preserved* and become visible,
-allowing you to see inside the carved-out space (shown as the orange dashed
-curve in the result).
-
-This matches the leftmost shape in the screenshot: a green cube with a
-visible spherical hollow inside, showing the interior surfaces created by
-the subtraction.
-
-This operation is ideal for creating:
-- Holes and tunnels
-- Carved-out spaces
-- Hollow objects
-
-#+BEGIN_SRC java
-SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN);
-SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE);
-
-cube.subtract(sphere);  // cube now has a spherical cavity
-#+END_SRC
-
-** Union (A + B)
-:PROPERTIES:
-:CUSTOM_ID: union-operation
-:END:
-
-#+INCLUDE: "CSG union.svg" export html
-
-*Union* merges the green cube (A) and orange sphere (B) into one continuous
-volume. The diagram shows both shapes combining — the interior seam (where
-they overlap) is removed, creating a single solid surface with no internal
-boundaries (indicated by the dashed blue line labeled "removed").
-
-This corresponds to the center shape in the screenshot: both green and
-orange colors present but seamlessly joined, forming one unified object.
-
-#+BEGIN_SRC java
-SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN);
-SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE);
-
-cube.union(sphere);  // cube now contains the merged result
-#+END_SRC
-
-** Intersect (A ∩ B)
-:PROPERTIES:
-:CUSTOM_ID: intersect-operation
-:END:
-
-#+INCLUDE: "CSG intersect.svg" export html
-
-*Intersect* keeps only the volume where the green cube (A) and orange
-sphere (B) overlap — the region that is inside *both* shapes
-simultaneously. The diagram shows this as the blue-shaded area: the
-portion of the sphere that fits within the cube boundaries. Everything
-else is discarded.
-
-This is the rightmost shape in the screenshot: only the overlapping
-portion remains, showing which parts of space were occupied by both the
-cube and sphere at the same time.
-
-This operation is useful for:
-- Creating shapes constrained by multiple boundaries
-- Finding collision regions
-- Trimming geometry to fit within bounds
-
-#+BEGIN_SRC java
-SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN);
-SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE);
-
-cube.intersect(sphere);  // only the overlapping region remains
-#+END_SRC
-
-* BSP Tree Algorithm
-:PROPERTIES:
-:CUSTOM_ID: bsp-tree-algorithm
-:END:
-
-CSG boolean operations are implemented using *Binary Space Partitioning*
-(BSP) trees. A BSP tree recursively divides 3D space using planes,
-creating a hierarchical structure that enables efficient polygon clipping
-and spatial queries.
-
-** BSP Tree Structure
-:PROPERTIES:
-:CUSTOM_ID: bsp-tree-structure
-:END:
-
-#+INCLUDE: "BSP tree.svg" export html
-
-Each BSP node contains:
-- A *partitioning plane* that divides space into two half-spaces
-- *Polygons* that lie exactly on this plane (coplanar)
-- *Front* subtree — polygons on the same side as the plane's normal
-- *Back* subtree — polygons on the opposite side
-
-** Key BSP Operations
-:PROPERTIES:
-:CUSTOM_ID: key-bsp-operations
-:END:
-
-The BSP tree provides three core operations that enable CSG:
-
-| Operation      | Description                                      |
-|----------------+--------------------------------------------------|
-| =invert()=     | Flip all normals, swap front/back children       |
-| =clipTo(tree)= | Remove polygons inside the other tree's solid    |
-| =addPolygons()= | Insert new polygons, splitting at planes         |
-
-*Invert* is fundamental to CSG. By flipping inside/outside, we can
-transform subtraction and intersection into variations of clipping:
-
-- **Subtract** = invert A, clip against B, add B's clipped parts, invert back
-- **Intersect** = invert A, clip B against A, invert B, clip A against B, combine, invert A back
-
-** Polygon Clipping
-:PROPERTIES:
-:CUSTOM_ID: polygon-clipping
-:END:
-
-When a polygon crosses a partitioning plane, it's *split* into two
-fragments:
-
-#+INCLUDE: "Polygon clipping.svg" export html
-
-This recursive splitting ensures that all polygons are cleanly classified
-as entirely in front, entirely behind, or exactly on a plane — never
-"spanning" across.
-
-* Using CSG in Aukio 3D
-:PROPERTIES:
-:CUSTOM_ID: using-csg-in-aukio-3d
-:END:
-
-CSG operations are methods on [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]. They modify the
-shape *in-place* — the result replaces the original geometry.
-
-** Basic Usage
-:PROPERTIES:
-:CUSTOM_ID: basic-usage
-:END:
-
-#+BEGIN_SRC java
-import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.*;
-
-// Create two shapes
-SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 100, Color.GREEN);
-SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 70, 12, Color.ORANGE);
-
-// Perform CSG operations (in-place modification)
-cube.subtract(sphere);   // Cube with spherical cavity
-// or
-cube.union(sphere);      // Merged shape
-// or
-cube.intersect(sphere);  // Only overlapping region
-
-// Add to scene
-shapes.addShape(cube.setBackfaceCulling(true));
-#+END_SRC
-
-** Child Handling Behavior
-:PROPERTIES:
-:CUSTOM_ID: child-handling
-:END:
-
-CSG operations only affect *SolidPolygon* geometry. Other children are
-preserved as objects:
-
-| Child Type            | Union            | Subtract         | Intersect        |
-|-----------------------+------------------+------------------+------------------|
-| SolidPolygon (this)   | Replaced with result | Replaced with result | Replaced with result |
-| SolidPolygon (other)  | Merged into result | Discarded (cutter) | Discarded        |
-| Line, TextCanvas (this) | Preserved      | Preserved        | Preserved        |
-| Line, TextCanvas (other) | Merged into this shape | Discarded | Discarded     |
-| Nested composite      | Preserved as object — but see below | same | same |
-
-*Nested composites are not CSG-safe.* Polygon extraction recurses into
-them, so their SolidPolygons are included in the BSP result — while the
-nested composite object itself is also preserved, duplicating that
-geometry in the render. Apply CSG to flat composites, or extract the
-nested polygons first.
-
-This allows you to attach labels, decorations, or wireframe overlays to
-shapes without them being affected by CSG operations (for union, the
-other shape's decorations are copied over too).
-
-** Important Notes
-:PROPERTIES:
-:CUSTOM_ID: important-notes
-:END:
-
-1. *Shapes are modified in-place*. The original geometry is replaced.
-   Clone shapes beforehand if you need to preserve the originals.
-
-2. *CSG works on SolidPolygon children only*. TexturedTriangle and other
-   shape types are not processed.
-
-3. *Result quality depends on mesh density*. Low-polygon inputs may
-   produce visible artifacts at intersection boundaries. Use higher
-   subdivision counts for smoother results.
-
-4. *Backface culling is recommended*. CSG results often have internal
-   faces from the cutting operation. Enable culling to hide backfaces:
-   =shape.setBackfaceCulling(true)=
-
-* Related Classes
-:PROPERTIES:
-:CUSTOM_ID: related-classes
-:END:
-
-| Class                    | Purpose                                              |
-|--------------------------+------------------------------------------------------|
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/BspTree.html][BspTree]]                  | BSP tree for spatial partitioning and CSG operations |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Plane]]                    | Partitioning plane used by BSP nodes                 |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]]             | Polygon shape processed by CSG                       |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]   | Base class with union/subtract/intersect methods     |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.html][SolidPolygonMesh]]         | Custom polygon mesh for arbitrary geometry           |
-| SolidPolygon* primitives | See [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery]] for all available shapes           |
diff --git a/doc/Coordinate system.svg b/doc/Coordinate system.svg
deleted file mode 100644 (file)
index 4497bf3..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<svg viewBox="0 0 640 520" width="640" height="520" xmlns="http://www.w3.org/2000/svg">
-  <rect width="640" height="520" fill="#061018"/>
-  <circle cx="280" cy="260" r="10" fill="rgba(80,96,192,0.1)" stroke="rgba(80,96,192,0.3)" stroke-width="2"/>
-  <line x1="280" y1="260" x2="560" y2="260" stroke="#d04040" stroke-width="5"/>
-  <polygon points="560,260 540,250 540,270" fill="#d04040"/>
-  <text x="568" y="268" fill="#d04040" font-size="28" font-weight="700" font-family="monospace">X</text>
-  <text x="400" y="304" fill="#bbb" font-size="18" font-family="monospace">right (+) / left (-)</text>
-  <line x1="280" y1="260" x2="280" y2="480" stroke="#30a050" stroke-width="5"/>
-  <polygon points="280,480 270,460 290,460" fill="#30a050"/>
-  <text x="292" y="504" fill="#30a050" font-size="28" font-weight="700" font-family="monospace">Y</text>
-  <text x="292" y="456" fill="#bbb" font-size="18" font-family="monospace">down (+) / up (-)</text>
-  <line x1="280" y1="260" x2="120" y2="140" stroke="#2070c0" stroke-width="5"/>
-  <polygon points="120,140 140,144 132,164" fill="#2070c0"/>
-  <text x="84" y="124" fill="#2070c0" font-size="28" font-weight="700" font-family="monospace">Z</text>
-  <text x="120" y="112" fill="#bbb" font-size="18" font-family="monospace">away (+) / towards (-)</text>
-  <text x="300" y="204" fill="#aaa" font-size="22" font-weight="600" font-family="monospace">Origin</text>
-  <text x="294" y="230" fill="#bbb" font-size="18" font-family="monospace">(0, 0, 0)</text>
-</svg>
\ No newline at end of file
diff --git a/doc/Depth buffer/index.org b/doc/Depth buffer/index.org
deleted file mode 100644 (file)
index 2f2c75d..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
-#+TITLE: Depth Buffer - Aukio 3D
-#+LANGUAGE: en
-#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
-#+LATEX_HEADER: \usepackage{parskip}
-#+LATEX_HEADER: \setlength{\parindent}{15pt}
-#+LATEX_HEADER: \usepackage{palatino}
-#+LATEX_HEADER: \usepackage{charter}
-#+HTML_HEAD: <style type="text/css">body { max-width: 100%;}</style>
-
-[[file:../index.html#outline-container-depth-buffer][<- Back to index]]
-
-* Per-pixel visibility
-:PROPERTIES:
-:CUSTOM_ID: per-pixel
-:END:
-
-The engine resolves visibility with a depth buffer, not paint order.
-Every rasterized triangle carries a per-pixel depth quantity =zw =
-1/z= (camera-space), interpolated linearly across each span — =1/z= is
-affine in screen space, so it rides the same edge interpolators as the
-texture gradients. A fragment wins a pixel only where
-
-#+BEGIN_EXAMPLE
-zw > stored - margin * zw^2        (margin = 0 by default)
-#+END_EXAMPLE
-
-Default margin 0 is *strict depth*: the nearer fragment always wins,
-regardless of paint order, so overlapping depth ranges — a floor tile
-extending under furniture, a wall seen through a doorway — come out
-correct per pixel. A nonzero margin (= RenderingContext.DEPTH_MARGIN_DZ =,
-=-Daukio.zbuffer.margin=, world units) re-opens a tolerance window
-*behind* the stored depth for near-coplanar pairs; any nonzero window
-re-imports per-triangle sort errors into per-pixel occlusion, which is
-why the default is strict.
-
-The depth buffer is allocated once per frame context
-(=RenderingContext.depth=, float per pixel) and cleared per tile
-together with the pixel buffer.
-
-* Two passes
-:PROPERTIES:
-:CUSTOM_ID: two-passes
-:END:
-
-=RenderAggregator.paintSorted= paints the sorted queue in two passes:
-
-1. *Opaque pass* — opaque-class triangles, iterated front-to-back (the
-   queue is back-to-front, so reversed), depth test + depth write.
-   Front-to-back order is a pure performance hint: hidden fragments
-   die on the depth test *before* the texture fetch (early-z).
-2. *Alpha pass* — alpha-class triangles (translucent solid polygons,
-   alpha-carrying textures, SDF text), iterated back-to-front in queue
-   order, depth test but *no depth write*. Translucency never
-   occludes, and overlapping translucent surfaces keep painter-coherent
-   mutual order.
-
-A shape's class comes from its paint color or texture: solid polygons
-with =alpha = 255= are opaque, anything translucent is alpha-class;
-textured triangles are alpha-class when the texture has alpha or is an
-SDF mask.
-
-* Which shapes carry depth
-:PROPERTIES:
-:CUSTOM_ID: shapes
-:END:
-
-- =TexturedTriangle= — opaque or alpha class by texture.
-- =SolidPolygon= — depth-tested since 2026-09-17; opaque when its
-  (possibly shaded) color is fully opaque, translucent otherwise.
-  Near-plane-clipped quads fan-triangulate with depth like any other
-  triangles.
-- =LightmappedTriangle= — a textured triangle, so the same rules.
-- =Line=, =Billboard=, =GlowingPoint= — no depth by design: they are
-  2D overlays (wireframes, markers, sprites) and always paint on top,
-  in the alpha pass.
-
-Because every occluder writes depth, scene code no longer needs any
-ordering structure: composites just fan-triangulate their polygons.
-(=LightmappedCompositeShape= exists only to wrap polygons as lightmap
-carriers for the GI system, not to order them.)
-
-* Hi-Z occlusion pyramid
-:PROPERTIES:
-:CUSTOM_ID: hi-z
-:END:
-
-After each successful paint, =ViewPanel= builds a Hi-Z pyramid from
-the depth buffer (gui/HiZPyramid): 8-pixel tiles pooled upward,
-each tile storing the *minimum* =zw= (farthest written depth —
-max-pooling would store the nearest occluder and wrongly cull geometry
-visible between near gaps). Next frame, =TriangleMeshBlock= projects
-its world AABB's 8 corners and, when the nearest corner is still
-behind the pyramid's stored depth, skips the whole block before any
-per-triangle work.
-
-The test is conservative by construction (min-pooling plus sky pixels
-at =-inf=), so it never culls visible geometry; wrong culls under
-camera motion self-heal in one frame. Knobs: =-Daukio.hiz.margin=0.02=,
-kill switch =-Daukio.hiz=false=. Headless snapshots never build the
-pyramid, so golden renders are structurally unaffected. Stereo skips
-the test (the pyramid is mono).
-
-* Determinism and depth dumps
-:PROPERTIES:
-:CUSTOM_ID: determinism
-:END:
-
-The renderer is bit-deterministic: same scene and camera give
-bit-identical pixels across runs, which is what the golden-image
-regression tests compare. =Snapshot= (the headless toolkit) supports
-=-Daukio.zbuffer.dumpDepth=path.png= to write a grayscale depth map
-alongside the color image — useful when hunting depth-window bugs
-(bisect those with =-Daukio.zbuffer.margin=0=).
-
-* Related classes
-:PROPERTIES:
-:CUSTOM_ID: related-classes
-:END:
-
-| Class                | Role                                                     |
-|----------------------+----------------------------------------------------------|
-| =RenderingContext=   | =depth= buffer, =depthPass=, =DEPTH_MARGIN_DZ= constant  |
-| =RenderAggregator=   | =paintSorted= two-pass driver, queue sort                |
-| =TexturedTriangle=   | Z span writers (perspective and affine)                  |
-| =SolidPolygon=       | flat-color Z span writer, two-pass classification        |
-| =HiZPyramid=         | temporal whole-block occlusion culling                   |
-| =ViewPanel=          | per-tile depth clear, pyramid rebuild after paint        |
-
-[[file:../index.html#outline-container-depth-buffer][Back to main documentation]]
diff --git a/doc/Developer tools/Developer tools.png b/doc/Developer tools/Developer tools.png
deleted file mode 100644 (file)
index 825b0de..0000000
Binary files a/doc/Developer tools/Developer tools.png and /dev/null differ
diff --git a/doc/Developer tools/Render alternative segments.png b/doc/Developer tools/Render alternative segments.png
deleted file mode 100644 (file)
index e2bd569..0000000
Binary files a/doc/Developer tools/Render alternative segments.png and /dev/null differ
diff --git a/doc/Developer tools/Render polygon borders.png b/doc/Developer tools/Render polygon borders.png
deleted file mode 100644 (file)
index 5ec2182..0000000
Binary files a/doc/Developer tools/Render polygon borders.png and /dev/null differ
diff --git a/doc/Developer tools/Show segment boundaries.png b/doc/Developer tools/Show segment boundaries.png
deleted file mode 100644 (file)
index 01a1978..0000000
Binary files a/doc/Developer tools/Show segment boundaries.png and /dev/null differ
diff --git a/doc/Developer tools/Thread timeline.png b/doc/Developer tools/Thread timeline.png
deleted file mode 100644 (file)
index dd1d378..0000000
Binary files a/doc/Developer tools/Thread timeline.png and /dev/null differ
diff --git a/doc/Edge.svg b/doc/Edge.svg
deleted file mode 100644 (file)
index e9af1cf..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
-  <rect width="640" height="480" fill="#061018"/>
-  <polygon points="320,100 160,380 480,380" fill="rgba(100,100,200,0.04)" stroke="rgba(100,100,200,0.2)" stroke-width="2"/>
-  <line x1="320" y1="100" x2="480" y2="380" stroke="#5060c0" stroke-width="6" stroke-linecap="round"/>
-  <circle cx="320" cy="100" r="10" fill="#5060c0"/>
-  <circle cx="160" cy="380" r="8" fill="rgba(80,96,192,0.5)"/>
-  <circle cx="480" cy="380" r="10" fill="#5060c0"/>
-  <text x="300" y="80" fill="#aaa" font-size="20" font-family="monospace">V₁</text>
-  <text x="492" y="388" fill="#aaa" font-size="20" font-family="monospace">V₂</text>
-  <text x="120" y="400" fill="#bbb" font-size="20" font-family="monospace">V₃</text>
-  <text x="420" y="220" fill="#5060c0" font-size="24" font-weight="700" font-family="monospace" transform="rotate(30 420 220)">edge</text>
-</svg>
diff --git a/doc/Example.png b/doc/Example.png
deleted file mode 100644 (file)
index 7094240..0000000
Binary files a/doc/Example.png and /dev/null differ
diff --git a/doc/Face triangle.svg b/doc/Face triangle.svg
deleted file mode 100644 (file)
index 509c841..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
-  <rect width="640" height="480" fill="#061018"/>
-  <polygon points="320,80 120,400 520,400" fill="rgba(200,80,140,0.15)" stroke="#c05088" stroke-width="3"/>
-  <line x1="200" y1="280" x2="440" y2="280" stroke="rgba(200,80,140,0.1)" stroke-width="1"/>
-  <line x1="240" y1="320" x2="400" y2="320" stroke="rgba(200,80,140,0.08)" stroke-width="1"/>
-  <line x1="164" y1="360" x2="476" y2="360" stroke="rgba(200,80,140,0.06)" stroke-width="1"/>
-  <circle cx="320" cy="80" r="8" fill="#c05088"/>
-  <circle cx="120" cy="400" r="8" fill="#c05088"/>
-  <circle cx="520" cy="400" r="8" fill="#c05088"/>
-  <text x="296" y="60" fill="#c05088" font-size="20" font-weight="700" font-family="monospace">V₁</text>
-  <text x="76" y="420" fill="#c05088" font-size="20" font-weight="700" font-family="monospace">V₂</text>
-  <text x="532" y="420" fill="#c05088" font-size="20" font-weight="700" font-family="monospace">V₃</text>
-  <text x="264" y="300" fill="rgba(192,80,136,0.5)" font-size="28" font-weight="700" font-family="monospace">FACE</text>
-</svg>
diff --git a/doc/Frustum culling/Frustum diagram.svg b/doc/Frustum culling/Frustum diagram.svg
deleted file mode 100644 (file)
index b59d4a8..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-<svg viewBox="0 0 620 300" width="620" height="300" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="f-glow"><feGaussianBlur stdDeviation="2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-  </defs>
-  <rect width="620" height="300" fill="#061018"/>
-
-  <!-- Z axis -->
-  <line x1="60" y1="150" x2="590" y2="150" stroke="rgba(32,112,192,0.2)" stroke-width="1" stroke-dasharray="6 3"/>
-  <text x="570" y="143" fill="#2070c0" font-size="11" font-weight="700" font-family="monospace">+Z</text>
-  <text x="530" y="163" fill="#999" font-size="8" font-family="monospace">(view direction)</text>
-
-  <!-- Camera -->
-  <circle cx="60" cy="150" r="7" fill="rgba(32,112,192,0.3)" stroke="#2070c0" stroke-width="2" filter="url(#f-glow)"/>
-  <text x="74" y="154" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace">Camera</text>
-
-  <!-- Frustum edges: camera → near corners -->
-  <line x1="60" y1="150" x2="170" y2="105" stroke="rgba(48,160,80,0.3)" stroke-width="1"/>
-  <line x1="60" y1="150" x2="170" y2="195" stroke="rgba(48,160,80,0.3)" stroke-width="1"/>
-  <!-- Frustum edges: near → far corners -->
-  <line x1="170" y1="105" x2="440" y2="40" stroke="rgba(48,160,80,0.25)" stroke-width="1"/>
-  <line x1="170" y1="195" x2="440" y2="260" stroke="rgba(48,160,80,0.25)" stroke-width="1"/>
-  <!-- Extended rays behind far (faint dashed) -->
-  <line x1="440" y1="40" x2="520" y2="10" stroke="rgba(48,160,80,0.1)" stroke-width="1" stroke-dasharray="3 3"/>
-  <line x1="440" y1="260" x2="520" y2="290" stroke="rgba(48,160,80,0.1)" stroke-width="1" stroke-dasharray="3 3"/>
-
-  <!-- Near plane -->
-  <rect x="168" y="105" width="4" height="90" rx="1" fill="rgba(48,160,80,0.25)" stroke="#30a050" stroke-width="1.5"/>
-  <text x="155" y="96" fill="#30a050" font-size="10" font-family="monospace" font-weight="700">Near</text>
-
-  <!-- Far plane -->
-  <rect x="438" y="40" width="4" height="220" rx="1" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
-  <text x="425" y="32" fill="#30a050" font-size="10" font-family="monospace" font-weight="700">Far</text>
-
-  <!-- Frustum fill (the visible volume) -->
-  <polygon points="170,105 170,195 440,260 440,40" fill="rgba(48,160,80,0.06)" stroke="none"/>
-
-  <!-- Visible region label -->
-  <text x="290" y="145" fill="#30a050" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle" opacity="0.6">visible region</text>
-
-  <!-- Plane labels along frustum edges -->
-  <text x="295" y="62" fill="#999" font-size="8" font-family="monospace" text-anchor="middle">Top plane</text>
-  <text x="295" y="242" fill="#999" font-size="8" font-family="monospace" text-anchor="middle">Bottom plane</text>
-
-  <!-- Object inside frustum (rendered) -->
-  <rect x="270" y="130" width="28" height="28" rx="2" fill="rgba(48,160,80,0.2)" stroke="#30a050" stroke-width="1.5"/>
-  <text x="276" y="150" fill="#30a050" font-size="14" font-family="monospace">✓</text>
-  <text x="258" y="172" fill="#30a050" font-size="8" font-family="monospace">rendered</text>
-
-  <!-- Object outside frustum (culled — above) -->
-  <rect x="480" y="18" width="24" height="24" rx="2" fill="rgba(208,64,64,0.12)" stroke="rgba(208,64,64,0.5)" stroke-width="1.5" stroke-dasharray="3 2"/>
-  <text x="485" y="36" fill="rgba(208,64,64,0.8)" font-size="13" font-family="monospace">✗</text>
-  <text x="470" y="52" fill="rgba(208,64,64,0.6)" font-size="8" font-family="monospace">culled</text>
-
-  <!-- Object outside frustum (culled — below) -->
-  <rect x="310" y="266" width="24" height="24" rx="2" fill="rgba(208,64,64,0.12)" stroke="rgba(208,64,64,0.5)" stroke-width="1.5" stroke-dasharray="3 2"/>
-  <text x="315" y="284" fill="rgba(208,64,64,0.8)" font-size="13" font-family="monospace">✗</text>
-  <text x="300" y="300" fill="rgba(208,64,64,0.6)" font-size="8" font-family="monospace">culled</text>
-</svg>
diff --git a/doc/Frustum culling/P-vertex AABB.svg b/doc/Frustum culling/P-vertex AABB.svg
deleted file mode 100644 (file)
index a3acfb8..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-<svg viewBox="0 0 520 200" width="520" height="200" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="p-glow"><feGaussianBlur stdDeviation="1.5" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-  </defs>
-  <rect width="520" height="200" fill="#061018"/>
-
-  <!-- Explanation text -->
-  <text x="30" y="22" fill="#aaa" font-size="10" font-family="monospace">P-vertex: corner most aligned with plane normal</text>
-  <text x="30" y="36" fill="#bbb" font-size="9" font-family="monospace">If P is behind the plane → entire AABB is outside</text>
-
-  <!-- Frustum plane (diagonal line) -->
-  <line x1="50" y1="170" x2="430" y2="55" stroke="#b09020" stroke-width="2" filter="url(#p-glow)"/>
-  <text x="432" y="52" fill="#b09020" font-size="10" font-weight="700" font-family="monospace">Plane</text>
-
-  <!-- "inside" region label -->
-  <text x="100" y="80" fill="rgba(48,160,80,0.4)" font-size="10" font-family="monospace">inside frustum</text>
-  <!-- "outside" region label -->
-  <text x="310" y="170" fill="rgba(208,64,64,0.4)" font-size="10" font-family="monospace">outside frustum</text>
-
-  <!-- Normal vector arrow -->
-  <line x1="270" y1="100" x2="230" y2="78" stroke="#b09020" stroke-width="1.5"/>
-  <polygon points="230,78 237,76 236,83" fill="#b09020"/>
-  <text x="222" y="72" fill="#b09020" font-size="9" font-family="monospace" font-weight="700">N</text>
-
-  <!-- AABB inside frustum (fully visible) -->
-  <rect x="80" y="100" width="60" height="50" rx="2" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
-  <text x="97" y="130" fill="#30a050" font-size="9" font-family="monospace" text-anchor="middle">inside</text>
-  <!-- P-vertex for inside box (top-right corner, closest to plane) -->
-  <circle cx="140" cy="100" r="3.5" fill="#30a050"/>
-  <text x="145" y="97" fill="#30a050" font-size="7" font-family="monospace">P</text>
-
-  <!-- AABB outside frustum (fully culled) -->
-  <rect x="340" y="110" width="60" height="50" rx="2" fill="rgba(208,64,64,0.12)" stroke="rgba(208,64,64,0.5)" stroke-width="1.5" stroke-dasharray="3 2"/>
-  <text x="356" y="140" fill="rgba(208,64,64,0.7)" font-size="9" font-family="monospace" text-anchor="middle">outside</text>
-  <!-- P-vertex for outside box (top-left corner, closest to plane) -->
-  <circle cx="340" cy="110" r="3.5" fill="rgba(208,64,64,0.8)"/>
-  <text x="327" y="107" fill="rgba(208,64,64,0.8)" font-size="7" font-family="monospace">P</text>
-</svg>
diff --git a/doc/Frustum culling/index.org b/doc/Frustum culling/index.org
deleted file mode 100644 (file)
index 9c4a941..0000000
+++ /dev/null
@@ -1,177 +0,0 @@
-#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
-#+TITLE: Frustum & View Frustum Culling - Aukio 3D
-#+LANGUAGE: en
-#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
-#+LATEX_HEADER: \usepackage{parskip}
-#+LATEX_HEADER: \usepackage[none]{hyphenat}
-
-#+OPTIONS: H:20 num:20
-#+OPTIONS: author:nil
-
-#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
-
-[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
-
-* Frustum & View Frustum Culling
-:PROPERTIES:
-:CUSTOM_ID: frustum-view-frustum-culling
-:END:
-
-#+INCLUDE: "Frustum diagram.svg" export html
-
-The *view frustum* is a truncated pyramid-shaped volume that represents
-everything the camera can see. Objects completely outside this volume are
-skipped during rendering — a powerful optimization called *frustum culling*.
-
-** The Six Frustum Planes
-:PROPERTIES:
-:CUSTOM_ID: frustum-planes
-:END:
-
-The frustum is defined by six clipping planes:
-
-| Plane   | Purpose                                    |
-|---------+--------------------------------------------|
-| Left    | Left edge of viewport                      |
-| Right   | Right edge of viewport                     |
-| Top     | Top edge of viewport (smaller Y in Y-down) |
-| Bottom  | Bottom edge of viewport (larger Y)         |
-| Near    | Closest visible distance from camera       |
-| Far     | Farthest visible distance from camera      |
-
-Each plane divides 3D space into "inside" (visible) and "outside"
-(culled). An object must pass all six plane tests to be considered
-potentially visible.
-
-** Frustum Culling vs Backface Culling
-:PROPERTIES:
-:CUSTOM_ID: frustum-vs-backface-culling
-:END:
-
-These are complementary optimizations at different levels:
-
-| Optimization    | Level        | What it skips                    |
-|-----------------+--------------+----------------------------------|
-| Frustum culling | Object level | Entire composite shapes + children |
-| Backface culling | Polygon level | Individual triangles facing away |
-
-*Frustum culling* happens first during the transform phase — entire
-object trees are skipped with a single bounding box test. *Backface
-culling* happens later during rasterization — individual triangles
-are checked before being drawn.
-
-For best performance, use both: organize your scene with composite
-shapes for effective frustum culling, and enable backface culling on
-closed meshes.
-
-* How Frustum Culling Works in Aukio 3D
-:PROPERTIES:
-:CUSTOM_ID: frustum-culling-implementation
-:END:
-
-Frustum culling is applied automatically to all [[../index.org#mesh][composite shapes]]
-during Phase 1 (transform) of the [[../Rendering loop/][rendering loop]]:
-
-1. *Update frustum*: Compute 6 planes from camera FOV and viewport size
-2. *For each composite shape*:
-   - Get its [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Box.html][Axis-Aligned Bounding Box (AABB)]]
-   - Transform all 8 corners to view space
-   - Test against frustum using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Frustum.html#intersectsAABB][intersectsAABB()]]
-   - If outside: skip the entire composite and all children
-   - If inside: continue transforming children
-
-(The root composite itself is never tested — it is always rendered.)
-
-** The AABB Intersection Algorithm
-:PROPERTIES:
-:CUSTOM_ID: aabb-intersection-algorithm
-:END:
-
-The intersection test uses an optimized "P-vertex" approach:
-
-#+INCLUDE: "P-vertex AABB.svg" export html
-
-For each plane, instead of testing all 8 corners of the bounding box,
-we test only the *P-vertex* — the corner most aligned with the plane
-normal. If this "best" corner is behind the plane, the entire box must
-be outside the frustum.
-
-- Plane normal points *into* the frustum (toward visible region)
-- P-vertex: select corner based on normal direction
-  - If normal.x > 0 → use maxX (rightmost corner)
-  - If normal.x < 0 → use minX (leftmost corner)
-  - Same logic for Y and Z
-- Test: =dot(normal, P-vertex) < distance= → outside
-
-This reduces from 48 tests (8 corners × 6 planes) to just 6 tests per
-object.
-
-* Performance Benefits
-:PROPERTIES:
-:CUSTOM_ID: frustum-performance
-:END:
-
-Frustum culling can dramatically improve performance for large scenes:
-
-- *High cull % (60-90%)*: Excellent — most objects skipped entirely
-- *Medium cull % (20-60%)*: Moderate benefit
-- *Low cull % (0-20%)*: Limited benefit — most objects visible
-
-A composite shape that is culled skips:
-- Transforming all its children
-- Computing bounding boxes for children
-- All polygon-level operations (backface culling, rasterization)
-
-Open Developer Tools (F12) to see real-time [[../index.org#frustum-culling-statistics][frustum culling statistics]].
-
-* Scene Design for Effective Culling
-:PROPERTIES:
-:CUSTOM_ID: frustum-scene-design
-:END:
-
-Frustum culling works best when you organize your scene into
-well-defined composite shapes:
-
-#+BEGIN_SRC java
-// Good: Each building is a separate composite
-AbstractCompositeShape cityBlock = new AbstractCompositeShape();
-for (Building building : buildings) {
-    AbstractCompositeShape buildingComposite = new AbstractCompositeShape();
-    buildingComposite.addShape(buildingWalls);
-    buildingComposite.addShape(buildingRoof);
-    buildingComposite.addShape(buildingInterior);
-    cityBlock.addShape(buildingComposite);
-}
-
-// Less effective: Everything in one giant composite
-AbstractCompositeShape allObjects = new AbstractCompositeShape();
-allObjects.addShape(building1Walls);
-allObjects.addShape(building1Roof);
-allObjects.addShape(building2Walls);
-// ... hundreds of shapes directly in root
-#+END_SRC
-
-*Best practices:*
-
-- Use composites to group objects that occupy a bounded region of space
-- Keep bounding boxes tight (don't add distant objects to the same composite)
-- Nest composites hierarchically for multi-level culling (city → block → building)
-- Call [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.html#invalidateBounds()][invalidateBounds()]] after moving shapes — the bounding box is
-  recomputed lazily on next use
-
-* Technical Details
-:PROPERTIES:
-:CUSTOM_ID: frustum-technical-details
-:END:
-
-The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Frustum.html][Frustum]] class:
-
-- Computes planes in *view space* (camera at origin, looking along +Z)
-- FOV derived from =projectionScale = width / 3= (≈112° horizontal FOV)
-- Default clip distances: Near = 1.0, Far = 10000.0
-- Planes stored in [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Hesse normal form]]: (normal vector, distance)
-
-The frustum is updated once per render pass in
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.html][ShapeCollection.transformShapesBegin()]], from the camera state and
-the stereo viewport width — in stereo mode each eye gets its own
-frustum, so the update runs twice per frame.
diff --git a/doc/Global illumination/Bounce estimator.svg b/doc/Global illumination/Bounce estimator.svg
deleted file mode 100644 (file)
index 0d1972e..0000000
+++ /dev/null
@@ -1,76 +0,0 @@
-<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="2" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-    <marker id="arrowGold" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
-      <polygon points="0,0 8,3 0,6" fill="#b09020"/>
-    </marker>
-    <marker id="arrowCyan" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
-      <polygon points="0,0 8,3 0,6" fill="#40b0d0"/>
-    </marker>
-    <marker id="arrowGreen" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
-      <polygon points="0,0 8,3 0,6" fill="#39FF14"/>
-    </marker>
-  </defs>
-  <rect width="640" height="480" fill="#061018"/>
-
-  <!-- floor -->
-  <line x1="50" y1="390" x2="600" y2="390" stroke="#30a050" stroke-width="3"/>
-  <text x="60" y="410" fill="#30a050" font-size="11" font-family="monospace">surface</text>
-
-  <!-- wall on the right -->
-  <line x1="480" y1="170" x2="480" y2="390" stroke="#30a050" stroke-width="3"/>
-
-  <!-- sample point P + normal -->
-  <circle cx="230" cy="390" r="6" fill="#c05088" filter="url(#glow)"/>
-  <text x="218" y="414" fill="#c05088" font-size="13" font-family="monospace" text-anchor="middle">P</text>
-  <line x1="230" y1="384" x2="230" y2="300" stroke="#b09020" stroke-width="2" marker-end="url(#arrowGold)"/>
-  <text x="240" y="330" fill="#b09020" font-size="11" font-family="monospace">normal</text>
-
-  <!-- cosine hemisphere dome -->
-  <path d="M 140 390 A 90 90 0 0 1 320 390" fill="none" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="6 3"/>
-  <text x="150" y="292" fill="#40b0d0" font-size="10" font-family="monospace">cosine-weighted</text>
-  <text x="150" y="306" fill="#40b0d0" font-size="10" font-family="monospace">hemisphere</text>
-
-  <!-- a few faint sample rays -->
-  <line x1="230" y1="384" x2="160" y2="310" stroke="#40b0d0" stroke-width="1" stroke-dasharray="3 2"/>
-  <line x1="230" y1="384" x2="300" y2="306" stroke="#40b0d0" stroke-width="1" stroke-dasharray="3 2"/>
-
-  <!-- THE bounce ray: P to Q on the wall -->
-  <line x1="230" y1="384" x2="472" y2="288" stroke="#40b0d0" stroke-width="2.5" marker-end="url(#arrowCyan)" filter="url(#glow)"/>
-  <circle cx="476" cy="285" r="6" fill="#40b0d0" filter="url(#glow)"/>
-  <text x="492" y="280" fill="#40b0d0" font-size="13" font-family="monospace">Q</text>
-
-  <!-- lamp -->
-  <circle cx="540" cy="70" r="14" fill="rgba(255,102,0,0.25)" stroke="#FF6600" stroke-width="2" filter="url(#glow)"/>
-  <circle cx="540" cy="70" r="5" fill="#FF6600"/>
-  <text x="540" y="104" fill="#FF8833" font-size="11" font-family="monospace" text-anchor="middle">light</text>
-
-  <!-- shadow ray P -> light (visible) -->
-  <line x1="236" y1="384" x2="528" y2="76" stroke="#39FF14" stroke-width="1.5" stroke-dasharray="8 4" marker-end="url(#arrowGreen)"/>
-  <text x="330" y="230" fill="#39FF14" font-size="10" font-family="monospace" transform="rotate(-52 330 230)">shadow ray: clear</text>
-
-  <!-- occluded shadow ray from a second point -->
-  <circle cx="400" cy="390" r="4" fill="rgba(192,80,136,0.6)"/>
-  <line x1="404" y1="384" x2="452" y2="240" stroke="#FF4444" stroke-width="1.5" stroke-dasharray="4 3"/>
-  <rect x="438" y="196" width="34" height="44" fill="rgba(255,68,68,0.15)" stroke="#FF4444" stroke-width="1.5"/>
-  <text x="455" y="188" fill="#FF4444" font-size="10" font-family="monospace" text-anchor="middle">occluder</text>
-  <text x="438" y="330" fill="#FF4444" font-size="10" font-family="monospace" transform="rotate(-62 438 330)">blocked</text>
-
-  <!-- what happens at Q -->
-  <rect x="360" y="120" width="260" height="58" rx="6" fill="rgba(32,112,192,0.1)" stroke="#2070c0" stroke-width="1.5"/>
-  <text x="372" y="142" fill="#2070c0" font-size="11" font-family="monospace">at Q: direct light (cached shadow bits)</text>
-  <text x="372" y="160" fill="#2070c0" font-size="11" font-family="monospace">     + Q's current indirect estimate</text>
-
-  <!-- P's update -->
-  <rect x="60" y="60" width="280" height="44" rx="6" fill="rgba(48,160,80,0.1)" stroke="#30a050" stroke-width="1.5"/>
-  <text x="72" y="78" fill="#39FF14" font-size="11" font-family="monospace">P's target = (albedo / &#960;) x (direct + indirect)</text>
-  <text x="72" y="94" fill="#30a050" font-size="10" font-family="monospace">blended in with an exponential moving average</text>
-
-  <text x="320" y="462" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">one sample per texel per visit: one shadow ray + one bounce ray — bounce light ripples deeper every sweep</text>
-</svg>
diff --git a/doc/Global illumination/GI pipeline.svg b/doc/Global illumination/GI pipeline.svg
deleted file mode 100644 (file)
index 0f63023..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-<svg viewBox="0 0 620 170" width="620" height="170" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="1.5" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-    <marker id="arrowhead" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
-      <polygon points="0,0 8,3 0,6" fill="#40b0d0"/>
-    </marker>
-  </defs>
-  <rect width="620" height="170" fill="#061018"/>
-
-  <!-- pipeline boxes -->
-  <rect x="10" y="45" width="104" height="64" rx="3" fill="rgba(80,96,192,0.15)" stroke="#5060c0" stroke-width="1.5"/>
-  <text x="62" y="66" fill="#5060c0" font-size="10" font-family="monospace" text-anchor="middle" font-weight="bold">scene snapshot</text>
-  <text x="62" y="80" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">triangles + lights</text>
-  <text x="62" y="92" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">+ BVH</text>
-
-  <rect x="136" y="45" width="104" height="64" rx="3" fill="rgba(255,102,0,0.15)" stroke="#FF6600" stroke-width="1.5"/>
-  <text x="188" y="66" fill="#FF8833" font-size="10" font-family="monospace" text-anchor="middle" font-weight="bold">GI workers</text>
-  <text x="188" y="80" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">Monte Carlo</text>
-  <text x="188" y="92" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">sweeps</text>
-
-  <rect x="262" y="45" width="104" height="64" rx="3" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
-  <text x="314" y="66" fill="#39FF14" font-size="10" font-family="monospace" text-anchor="middle" font-weight="bold">lightmaps</text>
-  <text x="314" y="80" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">per-texel indirect</text>
-  <text x="314" y="92" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">+ shadow bits</text>
-
-  <rect x="388" y="45" width="104" height="64" rx="3" fill="rgba(192,80,136,0.15)" stroke="#c05088" stroke-width="1.5"/>
-  <text x="440" y="60" fill="#c05088" font-size="10" font-family="monospace" text-anchor="middle" font-weight="bold">composite</text>
-  <text x="440" y="74" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">baseColor x light</text>
-  <text x="440" y="86" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">double-buffered</text>
-  <text x="440" y="98" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">swap</text>
-
-  <rect x="514" y="45" width="96" height="64" rx="3" fill="rgba(64,176,208,0.15)" stroke="#40b0d0" stroke-width="1.5"/>
-  <text x="562" y="66" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle" font-weight="bold">painter</text>
-  <text x="562" y="80" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">plain texture</text>
-  <text x="562" y="92" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">lookup</text>
-
-  <!-- arrows -->
-  <line x1="114" y1="77" x2="134" y2="77" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
-  <line x1="240" y1="77" x2="260" y2="77" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
-  <line x1="366" y1="77" x2="386" y2="77" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
-  <line x1="492" y1="77" x2="512" y2="77" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
-
-  <!-- feedback arrow: lightmaps feed the next sweep's bounce targets -->
-  <path d="M 314 109 L 314 135 L 188 135 L 188 111" fill="none" stroke="#30a050" stroke-width="1.2" stroke-dasharray="4 3" marker-end="url(#arrowhead)"/>
-  <text x="251" y="147" fill="#30a050" font-size="9" font-family="monospace" text-anchor="middle">bounce reads last sweep's estimate</text>
-
-  <text x="562" y="135" fill="#666" font-size="9" font-family="monospace" text-anchor="middle">zero ray casting</text>
-  <text x="562" y="147" fill="#666" font-size="9" font-family="monospace" text-anchor="middle">on render threads</text>
-</svg>
diff --git a/doc/Global illumination/Global illumination.png b/doc/Global illumination/Global illumination.png
deleted file mode 100644 (file)
index 9909193..0000000
Binary files a/doc/Global illumination/Global illumination.png and /dev/null differ
diff --git a/doc/Global illumination/Lightmap mapping.svg b/doc/Global illumination/Lightmap mapping.svg
deleted file mode 100644 (file)
index 2d82fea..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="1.5" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-    <marker id="arrowhead2" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
-      <polygon points="0,0 8,3 0,6" fill="#b09020"/>
-    </marker>
-  </defs>
-  <rect width="640" height="480" fill="#061018"/>
-
-  <!-- bounding square: the full texture; valid region is the lower-left half (u+v <= 1) -->
-  <rect x="140" y="80" width="320" height="320" fill="rgba(255,68,68,0.06)" stroke="#FF4444" stroke-width="1.5" stroke-dasharray="4 3"/>
-  <text x="372" y="110" fill="#FF4444" font-size="10" font-family="monospace" text-anchor="middle">invalid half (u+v &gt; 1)</text>
-  <text x="372" y="126" fill="#FF4444" font-size="9" font-family="monospace" text-anchor="middle">filled from neighbors so sampling</text>
-  <text x="372" y="138" fill="#FF4444" font-size="9" font-family="monospace" text-anchor="middle">near the hypotenuse stays clean</text>
-
-  <!-- the triangle (valid region) -->
-  <polygon points="140,400 460,400 140,80" fill="rgba(48,160,80,0.12)" stroke="#39FF14" stroke-width="2" filter="url(#glow)"/>
-
-  <!-- texel grid inside the triangle (8x8) -->
-  <line x1="180" y1="360" x2="180" y2="400" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="220" y1="320" x2="220" y2="400" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="260" y1="280" x2="260" y2="400" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="300" y1="240" x2="300" y2="400" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="340" y1="200" x2="340" y2="400" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="380" y1="160" x2="380" y2="400" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="420" y1="120" x2="420" y2="400" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="140" y1="360" x2="420" y2="360" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="140" y1="320" x2="380" y2="320" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="140" y1="280" x2="340" y2="280" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="140" y1="240" x2="300" y2="240" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="140" y1="200" x2="260" y2="200" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="140" y1="160" x2="220" y2="160" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="140" y1="120" x2="180" y2="120" stroke="#1a3a4a" stroke-width="1"/>
-
-  <!-- texel center dots -->
-  <circle cx="160" cy="380" r="2" fill="#c05088"/>
-  <circle cx="200" cy="380" r="2" fill="#c05088"/>
-  <circle cx="240" cy="380" r="2" fill="#c05088"/>
-  <circle cx="160" cy="340" r="2" fill="#c05088"/>
-  <circle cx="200" cy="340" r="2" fill="#c05088"/>
-  <circle cx="160" cy="300" r="2" fill="#c05088"/>
-
-  <!-- one texel highlighted with its world position -->
-  <rect x="200" y="280" width="40" height="40" fill="rgba(255,136,51,0.2)" stroke="#FF8833" stroke-width="1.5"/>
-  <circle cx="220" cy="300" r="3.5" fill="#FF6600" filter="url(#glow)"/>
-  <text x="190" y="348" fill="#FF8833" font-size="10" font-family="monospace">one texel = one surface</text>
-  <text x="190" y="362" fill="#FF8833" font-size="10" font-family="monospace">patch, sampled at center</text>
-
-  <!-- vertices -->
-  <circle cx="140" cy="400" r="6" fill="#c05088"/>
-  <text x="128" y="420" fill="#c05088" font-size="12" font-family="monospace" text-anchor="middle">a (u=0, v=0)</text>
-  <circle cx="460" cy="400" r="6" fill="#c05088"/>
-  <text x="460" y="420" fill="#c05088" font-size="12" font-family="monospace" text-anchor="middle">b (u=1, v=0)</text>
-  <circle cx="140" cy="80" r="6" fill="#c05088"/>
-  <text x="128" y="70" fill="#c05088" font-size="12" font-family="monospace" text-anchor="middle">c (u=0, v=1)</text>
-
-  <!-- edge vectors -->
-  <line x1="140" y1="396" x2="300" y2="396" stroke="#b09020" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
-  <text x="225" y="440" fill="#b09020" font-size="11" font-family="monospace" text-anchor="middle">e1 = b &#8722; a</text>
-  <line x1="144" y1="400" x2="144" y2="240" stroke="#b09020" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
-  <text x="100" y="330" fill="#b09020" font-size="11" font-family="monospace" text-anchor="middle">e2 = c &#8722; a</text>
-
-  <!-- formula box -->
-  <rect x="390" y="180" width="230" height="64" rx="6" fill="rgba(32,112,192,0.1)" stroke="#2070c0" stroke-width="1.5"/>
-  <text x="402" y="204" fill="#2070c0" font-size="12" font-family="monospace">world(u,v) = a + e1&#183;u + e2&#183;v</text>
-  <text x="402" y="226" fill="#2070c0" font-size="10" font-family="monospace">texels = edge / unitsPerTexel</text>
-
-  <text x="320" y="462" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">every texel owns a fixed patch of the triangle — shadows and gradients live INSIDE the surface</text>
-</svg>
diff --git a/doc/Global illumination/gi-converged.png b/doc/Global illumination/gi-converged.png
deleted file mode 100644 (file)
index 1f81bfd..0000000
Binary files a/doc/Global illumination/gi-converged.png and /dev/null differ
diff --git a/doc/Global illumination/gi-flat.png b/doc/Global illumination/gi-flat.png
deleted file mode 100644 (file)
index f135bd2..0000000
Binary files a/doc/Global illumination/gi-flat.png and /dev/null differ
diff --git a/doc/Global illumination/gi-start.png b/doc/Global illumination/gi-start.png
deleted file mode 100644 (file)
index 6f7f3f0..0000000
Binary files a/doc/Global illumination/gi-start.png and /dev/null differ
diff --git a/doc/Global illumination/index.org b/doc/Global illumination/index.org
deleted file mode 100644 (file)
index 9568956..0000000
+++ /dev/null
@@ -1,251 +0,0 @@
-#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
-#+TITLE: Global Illumination - Aukio 3D
-#+LANGUAGE: en
-#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
-#+LATEX_HEADER: \usepackage{parskip}
-#+LATEX_HEADER: \usepackage[none]{hyphenat}
-
-#+OPTIONS: H:20 num:20
-#+OPTIONS: author:nil
-
-#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
-
-[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
-
-* What global illumination adds
-:PROPERTIES:
-:CUSTOM_ID: what-gi-adds
-:END:
-
-Plain per-polygon shading lights every polygon with one flat color:
-no shadows, and surfaces that receive no direct light stay uniformly
-dark. A room corner reads as a flat silhouette instead of a corner.
-
-#+attr_html: :class responsive-img
-#+attr_latex: :width 640px
-[[file:gi-flat.png]]
-
-*Aukio 3D* can optionally compute *global illumination* progressively
-on background CPU threads: shadows appear, light pools under lamps
-with smooth falloff, and colored light *bleeds* — a red sofa tints the
-floor next to it red. All of it converges gradually over the first
-seconds of a scene, then idles.
-
-Same camera, same house: flat shading (top) versus converged GI
-(below). Note the soft shadow of the partition wall, the lamp glow on
-the ceiling, and the subtle color variation across the floor.
-
-#+attr_html: :class responsive-img
-#+attr_latex: :width 640px
-[[file:gi-converged.png]]
-
-* The big idea: GI off the render path
-:PROPERTIES:
-:CUSTOM_ID: off-the-render-path
-:END:
-
-Ray tracing is far too slow to run per frame in a software renderer,
-so it doesn't: *the render loop never traces a single ray.* Painting a
-lightmapped triangle is an ordinary texture lookup, exactly as fast as
-any textured polygon.
-
-All the expensive work happens on dedicated low-priority worker threads
-that continuously refine per-surface lighting values. Whenever the
-values have improved enough, the workers regenerate each triangle's
-*composite texture* (baseColor x total lighting) into a back buffer
-and swap it in atomically — painters never see a half-updated texture.
-
-#+INCLUDE: "GI pipeline.svg" export html
-
-Because painters only read finished textures, frame rate is completely
-decoupled from GI quality: you can crank lightmap resolution up and the
-only cost is CPU time on the worker threads, not frame time.
-
-* Lightmaps: a texture per triangle
-:PROPERTIES:
-:CUSTOM_ID: lightmaps
-:END:
-
-Flat shading can only color a polygon uniformly — shadows and gradients
-need resolution *inside* the polygon. Every lightmapped triangle
-therefore owns a small generated texture, its *lightmap*, whose texels
-map onto the triangle surface by an affine rule:
-
-#+INCLUDE: "Lightmap mapping.svg" export html
-
-The triangle's UVs are pinned to (0,0), (1,0), (0,1), so the valid
-texel region is the half where u+v <= 1; the other half of the square
-texture is flood-filled from valid neighbors so that nearest sampling
-near the hypotenuse never picks up garbage.
-
-Each texel stores two things, both written only by GI threads:
-
-- *Indirect irradiance* (RGB floats) — the accumulated bounced light.
-- *Per-light visibility bits* — whether the last shadow ray from this
-  texel reached each lamp (cached so later queries cost nothing).
-
-Resolution is set in world units per texel
-(=LightmappedCompositeShape.setLightmapUnitsPerTexel()=, default 12): a 100-unit
-wall cell gets an 8x8 lightmap. Halving the units quadruples the
-tracing work.
-
-*What you trace is what you see:* the composite texture the painter
-samples is the lightmap itself, at native texel resolution — there is
-no upscaling step. Shadow-edge smoothness comes from tracing at finer
-resolution, never from interpolation. (An earlier bilinear-upscaling
-pass produced visibly artificial results and was removed; finer texels
-cost more CPU on the GI threads but look right.)
-
-* One sample: a shadow ray and a bounce ray
-:PROPERTIES:
-:CUSTOM_ID: one-sample
-:END:
-
-The work list is flat: one item per lightmap texel (and one per plain
-polygon, see below). Worker threads walk it round-robin, and every
-visit to a texel casts exactly two rays from the texel's world
-position, nudged slightly off the surface along the normal:
-
-1. *Shadow ray* toward a lamp. Answers visible/occluded, cached in the
-   texel's visibility bits. On a texel's *first* visit all lamps are
-   tested at once, so direct light and hard shadows appear after a
-   single sweep instead of trickling in lamp by lamp; later visits
-   re-test one lamp at a time, round-robin.
-2. *Bounce ray* in a random cosine-weighted direction around the
-   normal. Wherever it lands (point Q), the sample reads Q's *direct*
-   lighting — using Q's cached shadow bits, no new shadow rays — plus
-   Q's *current indirect estimate*, and blends the sum into the texel's
-   own indirect value.
-
-#+INCLUDE: "Bounce estimator.svg" export html
-
-Reading Q's current indirect estimate instead of recursing is what
-makes bounce light propagate: sweep 1 learns "Q is directly lit",
-sweep 2 learns "P sees a lit Q", sweep 3 learns "R sees a lit P"...
-Light ripples one surface deeper with every sweep, with no recursion
-limit and no exponential ray explosion. The =1/pi= diffuse gain keeps
-the feedback loop from diverging: without it the indirect term
-amplifies itself and the scene saturates to white.
-
-* Progressive convergence
-:PROPERTIES:
-:CUSTOM_ID: convergence
-:END:
-
-Monte Carlo samples are noisy, so blending happens at *two nested
-levels*, both exponential moving averages:
-
-1. *Inner, per sample*: each texel blends every new bounce-ray result
-   into its indirect estimate with a constant weight (=alpha = 0.15=,
-   mode =fixed=). Every ray hit stays equally intensive forever — an
-   unlit area fades to darkness at the same rate a lit area brightens.
-   (The old =adaptive= mode, which decays alpha with sample count, is
-   still available; see the knobs below.)
-2. *Outer, per composite update*: the value that reaches the screen is
-   a second EMA over the COMPLETE sum =ambient + direct + indirect=.
-   The texture can only move =e3d.gi.compositeAlpha= (default 0.2) of
-   the remaining distance per 500 ms update — so direct light, shadows
-   and bounce light all glide in together over a few seconds, and no
-   single-frame jump is possible by construction.
-
-The world starts at a *uniform medium irradiance*
-(=e3d.gi.initialIrradiance=, default 128): the scene is visible from
-the very first frame, then lit areas brighten and unlit areas sink to
-darkness as the workers sweep — lights and shadows gradually become
-distinguished instead of the old pitch-black start with a sudden flash
-once the first sweep landed:
-
-#+attr_html: :class responsive-img
-#+attr_latex: :width 640px
-[[file:gi-start.png]]
-
-Consequences of the design:
-
-- *Hysteresis is free*: when a lamp moves or geometry changes, old
-  light fades out gradually instead of popping — the same pair of EMAs
-  that accumulates light also drains it.
-- *Convergence detection*: per-sample deltas are pure noise (and with a
-  constant alpha they never settle), so the system watches the average
-  per-texel movement of the on-screen estimate; after five composite
-  updates below =e3d.gi.calmThreshold= (default 1.0 light unit) the
-  workers drop to a low duty cycle (~50% of sweep time, capped) instead
-  of burning CPU. Any scene or light change rebuilds the snapshot and
-  restarts full-speed tracing.
-- *Despeckle*: at composite time the indirect channel is blended 50/50
-  with the mean of its valid 4-neighbors, killing single-texel Monte
-  Carlo spikes without blurring real gradients.
-- *Composite cadence*: textures regenerate at most every 500 ms — one
-  atomic swap per triangle, invisible to painters.
-
-* Plain polygons get GI too
-:PROPERTIES:
-:CUSTOM_ID: plain-polygons
-:END:
-
-Surfaces that are not lightmapped (ordinary =SolidPolygon= with shading
-enabled) still benefit, at per-polygon resolution, through the
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.html][GiLightProvider]]
-interface that =GlobalIllumination= installs into the
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]]:
-
-- =isLightVisible(polygon, light)= answers from cached shadow bits —
-  direct-light shadows fade in on flat-shaded geometry.
-- =addIndirectLight(polygon, baseColor, result)= adds the polygon's
-  bounced-light estimate into the flat-shaded color.
-
-Both are called from parallel render-pool threads, so they only read
-volatile caches — never trace.
-
-* Enabling GI
-:PROPERTIES:
-:CUSTOM_ID: enabling
-:END:
-
-#+BEGIN_SRC java
-// Per-texel lightmaps on composite geometry (the House demo setup):
-LightmappedCompositeShape house = new LightmappedCompositeShape();
-house.setLightmappingEnabled(true);
-house.setLightmapUnitsPerTexel(3.0);   // fine texels: quality from traced rays
-
-// Start the workers (2 threads by default; more converge faster):
-viewPanel.enableGlobalIllumination(4);
-#+END_SRC
-
-Tuning knobs (system properties):
-
-| Property                  | Default | Effect                                  |
-|---------------------------+---------+-----------------------------------------|
-| =e3d.gi.alphaMode=        | fixed   | =adaptive= decays the inner EMA alpha with sample count |
-| =e3d.gi.alphaFloor=       | 0.08    | adaptive-mode floor; higher adapts faster but noisier |
-| =e3d.gi.compositeAlpha=   | 0.2     | outer EMA: fade speed of the on-screen estimate per 500 ms update |
-| =e3d.gi.initialIrradiance=| 128     | uniform medium start (0..255 light units) |
-| =e3d.gi.calmThreshold=    | 1.0     | convergence: avg estimate movement (light units) |
-| =e3d.gi.despeckle=        | true    | neighbor-smoothing of indirect at composite time |
-| =e3d.gi.debug=            | false   | sweep statistics to stdout              |
-| =e3d.gi.dumpLightmaps=    | (unset) | dump composite lightmaps as PNGs to the given dir |
-
-* Limitations
-:PROPERTIES:
-:CUSTOM_ID: limitations
-:END:
-
-- *Diffuse light only* — no specular bounce, no caustics.
-- Polygon vertices are traced in composite-local space; scenes that put
-  non-identity transforms on composites are traced incorrectly.
-- The bounce estimate is one ray deep per sample — correctness comes
-  from sweep-over-sweep propagation, so deeply indirect corners take
-  several sweeps to brighten.
-
-* Related Classes
-:PROPERTIES:
-:CUSTOM_ID: related-classes
-:END:
-
-| Class                | Purpose                                                       |
-|----------------------+---------------------------------------------------------------|
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/GlobalIllumination.html][GlobalIllumination]] | Progressive tracer: sweeps, EMA convergence, composite swaps |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/Lightmap.html][Lightmap]]           | Per-triangle texel state + double-buffered composite textures |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.html][LightmappedTriangle]] | Textured triangle whose texture is the GI composite        |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/TriangleBvh.html][TriangleBvh]]        | BVH over world triangles: nearest-hit and any-hit ray queries (Möller–Trumbore) |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.html][GiLightProvider]]    | Cache-read interface feeding the flat-shading path          |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightmappedCompositeShape.html][LightmappedCompositeShape]]  | Wraps polygons into lightmapped triangles                     |
diff --git a/doc/Mesh.svg b/doc/Mesh.svg
deleted file mode 100644 (file)
index 8a20f46..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
-  <rect width="640" height="480" fill="#061018"/>
-  <ellipse cx="320" cy="240" rx="180" ry="180" fill="none" stroke="rgba(80,96,192,0.1)" stroke-width="1"/>
-  <ellipse cx="320" cy="240" rx="180" ry="40" fill="none" stroke="rgba(80,96,192,0.25)" stroke-width="1.6"/>
-  <ellipse cx="320" cy="180" rx="150" ry="32" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="1.2"/>
-  <ellipse cx="320" cy="300" rx="150" ry="32" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="1.2"/>
-  <ellipse cx="320" cy="120" rx="90" ry="20" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="1"/>
-  <ellipse cx="320" cy="360" rx="90" ry="20" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="1"/>
-  <ellipse cx="320" cy="240" rx="40" ry="180" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="1.2"/>
-  <ellipse cx="320" cy="240" rx="110" ry="180" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="1"/>
-  <polygon points="320,60 370,116 280,110" fill="rgba(80,96,192,0.15)" stroke="#5060c0" stroke-width="2"/>
-  <polygon points="370,116 410,176 320,164" fill="rgba(80,96,192,0.1)" stroke="#5060c0" stroke-width="1.6"/>
-  <polygon points="320,164 370,116 280,110" fill="rgba(80,96,192,0.07)" stroke="rgba(80,96,192,0.5)" stroke-width="1.2"/>
-  <circle cx="320" cy="60" r="5" fill="#5060c0"/>
-  <circle cx="370" cy="116" r="5" fill="#5060c0"/>
-  <circle cx="280" cy="110" r="5" fill="#5060c0"/>
-  <circle cx="410" cy="176" r="5" fill="#5060c0"/>
-  <circle cx="320" cy="164" r="5" fill="#5060c0"/>
-  <text x="436" y="140" fill="#5060c0" font-size="20" font-weight="600" font-family="monospace">triangulated</text>
-  <text x="436" y="164" fill="#5060c0" font-size="20" font-weight="600" font-family="monospace">section</text>
-  <line x1="412" y1="150" x2="428" y2="150" stroke="#5060c0" stroke-width="1.6"/>
-</svg>
diff --git a/doc/Near plane clip/Clip algorithm.svg b/doc/Near plane clip/Clip algorithm.svg
deleted file mode 100644 (file)
index 1d09a7e..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="2" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-  </defs>
-  <rect width="640" height="480" fill="#061018"/>
-
-  <!-- grid -->
-  <line x1="40" y1="120" x2="620" y2="120" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="40" y1="200" x2="620" y2="200" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="40" y1="280" x2="620" y2="280" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="40" y1="360" x2="620" y2="360" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="160" y1="60" x2="160" y2="400" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="300" y1="60" x2="300" y2="400" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="440" y1="60" x2="440" y2="400" stroke="#1a3a4a" stroke-width="1"/>
-
-  <!-- half-space labels -->
-  <text x="170" y="84" fill="#FF4444" font-size="12" font-family="monospace" text-anchor="middle">behind  (z &#8804; near)</text>
-  <text x="460" y="84" fill="#30a050" font-size="12" font-family="monospace" text-anchor="middle">in front  (z &gt; near)</text>
-
-  <!-- near plane -->
-  <line x1="300" y1="95" x2="300" y2="400" stroke="#40b0d0" stroke-width="2" stroke-dasharray="6 3"/>
-  <text x="310" y="110" fill="#40b0d0" font-size="12" font-family="monospace" text-anchor="start" filter="url(#glow)">near plane</text>
-
-  <!-- discarded part of the triangle -->
-  <polygon points="120,260 300,200 300,320" fill="rgba(255,68,68,0.08)" stroke="#FF4444" stroke-width="1.5" stroke-dasharray="4 3"/>
-
-  <!-- kept fragment: the clipped quad -->
-  <polygon points="480,140 480,380 300,320 300,200" fill="rgba(48,160,80,0.12)" stroke="#39FF14" stroke-width="2" filter="url(#glow)"/>
-
-  <!-- original edges continuing behind the plane (ghost) -->
-  <line x1="300" y1="200" x2="120" y2="260" stroke="#c05088" stroke-width="1.5" stroke-dasharray="3 2"/>
-  <line x1="300" y1="320" x2="120" y2="260" stroke="#c05088" stroke-width="1.5" stroke-dasharray="3 2"/>
-
-  <!-- vertices -->
-  <circle cx="480" cy="140" r="6" fill="#c05088"/>
-  <text x="496" y="144" fill="#c05088" font-size="13" font-family="monospace">v0</text>
-  <circle cx="480" cy="380" r="6" fill="#c05088"/>
-  <text x="496" y="384" fill="#c05088" font-size="13" font-family="monospace">v1</text>
-  <circle cx="120" cy="260" r="6" fill="rgba(192,80,136,0.45)"/>
-  <text x="104" y="246" fill="#c05088" font-size="13" font-family="monospace" opacity="0.7">v2</text>
-
-  <!-- intersection vertices -->
-  <circle cx="300" cy="200" r="6" fill="#39FF14" filter="url(#glow)"/>
-  <text x="284" y="190" fill="#39FF14" font-size="13" font-family="monospace" text-anchor="end">p&#8242;</text>
-  <circle cx="300" cy="320" r="6" fill="#39FF14" filter="url(#glow)"/>
-  <text x="314" y="340" fill="#39FF14" font-size="13" font-family="monospace" text-anchor="start">p&#8243;</text>
-
-  <!-- t parameter marker on edge v0-v2, with leader line -->
-  <text x="210" y="140" fill="#FF8833" font-size="11" font-family="monospace" text-anchor="middle">t = 0.5 along v0 &#8594; v2</text>
-  <line x1="280" y1="146" x2="380" y2="166" stroke="#FF8833" stroke-width="1" stroke-dasharray="3 2"/>
-
-  <!-- formula box -->
-  <rect x="50" y="320" width="210" height="86" rx="6" fill="rgba(32,112,192,0.1)" stroke="#2070c0" stroke-width="1.5"/>
-  <text x="62" y="342" fill="#2070c0" font-size="12" font-family="monospace">t  = (near &#8722; z1) / (z2 &#8722; z1)</text>
-  <text x="62" y="362" fill="#2070c0" font-size="12" font-family="monospace">p  = p1 + t&#183;(p2 &#8722; p1)</text>
-  <text x="62" y="382" fill="#2070c0" font-size="12" font-family="monospace">uv = uv1 + t&#183;(uv2 &#8722; uv1)</text>
-
-  <text x="320" y="438" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">every edge crossing the plane spawns an interpolated vertex;</text>
-  <text x="320" y="454" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">in-front vertices pass through unchanged</text>
-</svg>
diff --git a/doc/Near plane clip/Fan triangulation.svg b/doc/Near plane clip/Fan triangulation.svg
deleted file mode 100644 (file)
index 94a8656..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<svg viewBox="0 0 620 240" width="620" height="240" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="1.5" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-  </defs>
-  <rect width="620" height="240" fill="#061018"/>
-
-  <!-- clipped quad -->
-  <polygon points="120,50 330,50 400,180 170,180" fill="rgba(48,160,80,0.12)" stroke="#39FF14" stroke-width="2"/>
-
-  <!-- fan split from v0 -->
-  <line x1="120" y1="50" x2="400" y2="180" stroke="#FF6600" stroke-width="1.5" stroke-dasharray="6 3"/>
-
-  <!-- vertices -->
-  <circle cx="120" cy="50" r="5" fill="#c05088"/>
-  <text x="108" y="42" fill="#c05088" font-size="12" font-family="monospace" text-anchor="middle">v0</text>
-  <circle cx="330" cy="50" r="5" fill="#c05088"/>
-  <text x="330" y="42" fill="#c05088" font-size="12" font-family="monospace" text-anchor="middle">v1</text>
-  <circle cx="400" cy="180" r="5" fill="#39FF14" filter="url(#glow)"/>
-  <text x="408" y="198" fill="#39FF14" font-size="12" font-family="monospace">p&#8243;</text>
-  <circle cx="170" cy="180" r="5" fill="#39FF14" filter="url(#glow)"/>
-  <text x="162" y="198" fill="#39FF14" font-size="12" font-family="monospace" text-anchor="end">p&#8242;</text>
-
-  <!-- triangle labels -->
-  <text x="245" y="100" fill="#FF8833" font-size="12" font-family="monospace" text-anchor="middle">T1 = (v0, v1, p&#8243;)</text>
-  <text x="225" y="152" fill="#FF8833" font-size="12" font-family="monospace" text-anchor="middle">T2 = (v0, p&#8243;, p&#8242;)</text>
-
-  <!-- caption -->
-  <text x="460" y="60" fill="#aaa" font-size="11" font-family="monospace">a triangle cut once</text>
-  <text x="460" y="78" fill="#aaa" font-size="11" font-family="monospace">becomes a quad;</text>
-  <text x="460" y="96" fill="#aaa" font-size="11" font-family="monospace">the rasterizer paints it</text>
-  <text x="460" y="114" fill="#aaa" font-size="11" font-family="monospace">as a 2-triangle fan</text>
-  <text x="460" y="132" fill="#aaa" font-size="11" font-family="monospace">sharing v0</text>
-</svg>
diff --git a/doc/Near plane clip/Near plane straddle.svg b/doc/Near plane clip/Near plane straddle.svg
deleted file mode 100644 (file)
index aa2c9a6..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-<svg viewBox="0 0 620 300" width="620" height="300" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="2" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-  </defs>
-  <rect width="620" height="300" fill="#061018"/>
-
-  <!-- grid -->
-  <line x1="40" y1="80" x2="600" y2="80" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="40" y1="140" x2="600" y2="140" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="40" y1="200" x2="600" y2="200" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="40" y1="260" x2="600" y2="260" stroke="#1a3a4a" stroke-width="1"/>
-
-  <!-- axes -->
-  <line x1="40" y1="270" x2="600" y2="270" stroke="#2070c0" stroke-width="2"/>
-  <text x="592" y="290" fill="#2070c0" font-size="12" font-family="monospace" text-anchor="end">z (depth) &#8594;</text>
-  <text x="44" y="46" fill="#d04040" font-size="12" font-family="monospace">x &#8595;</text>
-
-  <!-- camera eye -->
-  <circle cx="70" cy="200" r="10" fill="rgba(255,102,0,0.2)" stroke="#FF6600" stroke-width="2" filter="url(#glow)"/>
-  <circle cx="70" cy="200" r="3" fill="#FF6600"/>
-  <text x="70" y="232" fill="#FF8833" font-size="11" font-family="monospace" text-anchor="middle">camera</text>
-  <line x1="80" y1="200" x2="140" y2="200" stroke="#FF6600" stroke-width="1.5" stroke-dasharray="4 3"/>
-
-  <!-- near plane -->
-  <line x1="180" y1="55" x2="180" y2="262" stroke="#40b0d0" stroke-width="2" stroke-dasharray="6 3"/>
-  <text x="180" y="46" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#glow)">near plane  z = 1</text>
-
-  <!-- floor tiles seen edge-on (the floor is a row of quads at this x height) -->
-  <line x1="270" y1="200" x2="340" y2="200" stroke="#c05088" stroke-width="3"/>
-  <line x1="350" y1="200" x2="420" y2="200" stroke="#c05088" stroke-width="3"/>
-  <line x1="430" y1="200" x2="500" y2="200" stroke="#c05088" stroke-width="3"/>
-  <line x1="510" y1="200" x2="580" y2="200" stroke="#c05088" stroke-width="3"/>
-  <text x="460" y="186" fill="#c05088" font-size="11" font-family="monospace" text-anchor="middle">floor tiles</text>
-
-  <!-- the straddling tile: behind part discarded, front part kept -->
-  <line x1="130" y1="200" x2="180" y2="200" stroke="#FF4444" stroke-width="3" stroke-dasharray="4 3"/>
-  <line x1="180" y1="200" x2="260" y2="200" stroke="#39FF14" stroke-width="3.5" filter="url(#glow)"/>
-  <circle cx="180" cy="200" r="6" fill="#39FF14" filter="url(#glow)"/>
-  <text x="235" y="248" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">kept fragment</text>
-  <text x="128" y="248" fill="#FF4444" font-size="10" font-family="monospace" text-anchor="middle">cut away</text>
-
-  <!-- old behaviour callout -->
-  <text x="140" y="120" fill="#FF4444" font-size="11" font-family="monospace" text-anchor="middle">old: one vertex behind &#8658;</text>
-  <text x="140" y="136" fill="#FF4444" font-size="11" font-family="monospace" text-anchor="middle">whole tile dropped</text>
-  <line x1="150" y1="142" x2="165" y2="192" stroke="#FF4444" stroke-width="1" stroke-dasharray="3 2"/>
-
-  <!-- new behaviour callout -->
-  <text x="330" y="120" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">new: clip at the plane,</text>
-  <text x="330" y="136" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">paint the surviving fragment</text>
-  <line x1="260" y1="142" x2="205" y2="192" stroke="#39FF14" stroke-width="1" stroke-dasharray="3 2"/>
-</svg>
diff --git a/doc/Near plane clip/index.org b/doc/Near plane clip/index.org
deleted file mode 100644 (file)
index 0c59a70..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
-#+TITLE: Near-Plane Clipping - Aukio 3D
-#+LANGUAGE: en
-#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
-#+LATEX_HEADER: \usepackage{parskip}
-#+LATEX_HEADER: \usepackage[none]{hyphenat}
-
-#+OPTIONS: H:20 num:20
-#+OPTIONS: author:nil
-
-#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
-
-[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
-
-* The problem
-:PROPERTIES:
-:CUSTOM_ID: the-problem
-:END:
-
-When the camera brushes against geometry — a floor tile under your
-feet, a wall you lean into — part of a polygon can end up *behind* the
-viewer while the rest stays in front. Perspective projection divides by
-depth (=screenX = x / z=), so a vertex at z ≤ 0 has no meaningful screen
-position at all.
-
-The naive way out — dropping any polygon that has even one vertex
-behind the camera — makes whole tiles vanish exactly when they are
-closest and largest on screen. Walking through the House demo, floor
-tiles blinked out of existence at the bottom of the frame:
-
-#+attr_html: :class responsive-img
-#+attr_latex: :width 640px
-[[file:near-clip-before.png]]
-
-*Aukio 3D* instead *clips the polygon against the near plane* and
-renders the surviving fragment. The same frame with clipping enabled —
-the floor is solid to the bottom edge:
-
-#+attr_html: :class responsive-img
-#+attr_latex: :width 640px
-[[file:near-clip-after.png]]
-
-Think of the camera plane as the edge of a table and the polygon as a
-sheet of paper partly hanging off it. Dropping the polygon means
-throwing away the whole sheet. Clipping takes scissors, cuts the sheet
-along the table edge, and keeps the part that lies on the table.
-
-#+INCLUDE: "Near plane straddle.svg" export html
-
-* Why not just clamp z?
-:PROPERTIES:
-:CUSTOM_ID: why-not-clamp-z
-:END:
-
-A tempting one-liner is to force every vertex to =z = max(z, epsilon)=
-and project anyway. It fails geometrically: a vertex at z = −50 clamped
-to z = 0.01 projects to a screen coordinate thousands of pixels away,
-*in the wrong direction* — the sign flip of the division mirrors it
-through the camera. The polygon smears into giant streaks across the
-frame instead of ending cleanly at the screen edge.
-
-Clipping produces the geometrically correct cut: the polygon's new edge
-lies exactly on the near plane, and everything the rasterizer receives
-has z > 0.
-
-* How the clipping works
-:PROPERTIES:
-:CUSTOM_ID: how-it-works
-:END:
-
-Clipping happens in *camera space*, after the transform stack has moved
-vertices relative to the viewer but *before* the perspective divide.
-The vertex loop is walked edge by edge (Sutherland-Hodgman style)
-against the plane =z = nearPlaneDistance=:
-
-1. An in-front vertex passes through unchanged.
-2. An edge that crosses the plane spawns a new vertex at the
-   intersection, with position, UV and normal all interpolated with the
-   same parameter =t=.
-3. A behind-plane vertex is skipped.
-
-#+INCLUDE: "Clip algorithm.svg" export html
-
-Interpolating UVs linearly along the 3D edge is exactly right for the
-perspective-correct texture mapper: the intersection vertex is a real
-point on the original edge, so its texture coordinate is the same blend
-of the endpoints' UVs. Textured fragments therefore show the correct
-texels right up to the cut, with no seam.
-
-Only a polygon with *all* vertices behind the plane is culled — the
-legitimate version of the old behavior.
-
-* From clipped loop to pixels
-:PROPERTIES:
-:CUSTOM_ID: from-clip-to-pixels
-:END:
-
-A convex N-gon crossing the plane clips to a single contiguous loop of
-at most N+1 vertices. For the triangle-based rasterizers this means a
-triangle can become a *quad*, which is painted as a two-triangle fan
-sharing the first vertex — exact, because the clip of a convex polygon
-stays convex:
-
-#+INCLUDE: "Fan triangulation.svg" export html
-
-Shape support:
-
-| Shape              | Behavior when straddling                          |
-|--------------------+---------------------------------------------------|
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]]     | Clipped loop painted as triangle fan              |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | Fan-painted with interpolated UVs (also inherited by lightmapped GI fragments) |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]]             | Shortened to the in-front endpoint + intersection |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.html][Billboard]]        | Single anchor point: culled when behind, as before |
-
-Implementation notes:
-
-- Clipped output is stored *per pipeline slot* on the shape
-  (=clippedVertices(ctx)=), so the triple-buffered pipeline can
-  transform frame N+1 while frame N is still painting.
-- Depth sorting and tile binning use the clipped vertices' average Z
-  and screen bounds — a clipped tile sorts as the fragment it became,
-  not as the polygon that reached behind you.
-- New intersection vertices exist only in camera space; they are
-  projected directly via
-  [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html#setCameraSpaceCoordinate(double,double,double,eu.svjatoslav.aukio.e3d.gui.RenderingContext)][Vertex.setCameraSpaceCoordinate()]],
-  bypassing the transform stack.
-
-* Configuration
-:PROPERTIES:
-:CUSTOM_ID: configuration
-:END:
-
-The near plane distance is a per-context knob, in world units:
-
-#+BEGIN_SRC java
-// Default is 1.0; smaller values let the camera press closer to
-// geometry before the scissors bite, at the cost of larger projected
-// coordinates for clipped fragments.
-viewPanel.getRenderingContext().nearPlaneDistance = 0.5;
-#+END_SRC
-
-* Related Classes
-:PROPERTIES:
-:CUSTOM_ID: related-classes
-:END:
-
-| Class                     | Purpose                                                        |
-|---------------------------+----------------------------------------------------------------|
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]] | Vertex-loop clipping in =transform()=, per-slot clip storage   |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]]                  | Camera-space projection for generated intersection vertices    |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]]        | Carries =nearPlaneDistance=                                    |
diff --git a/doc/Near plane clip/near-clip-after.png b/doc/Near plane clip/near-clip-after.png
deleted file mode 100644 (file)
index f135bd2..0000000
Binary files a/doc/Near plane clip/near-clip-after.png and /dev/null differ
diff --git a/doc/Near plane clip/near-clip-before.png b/doc/Near plane clip/near-clip-before.png
deleted file mode 100644 (file)
index 6b27c44..0000000
Binary files a/doc/Near plane clip/near-clip-before.png and /dev/null differ
diff --git a/doc/Normal vector.svg b/doc/Normal vector.svg
deleted file mode 100644 (file)
index 016136e..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<svg viewBox="0 0 640 520" width="640" height="520" xmlns="http://www.w3.org/2000/svg">
-  <rect width="640" height="520" fill="#061018"/>
-  <polygon points="120,400 320,360 520,400 320,440" fill="rgba(180,150,30,0.1)" stroke="rgba(180,150,30,0.4)" stroke-width="2"/>
-  <line x1="180" y1="396" x2="460" y2="396" stroke="rgba(180,150,30,0.08)" stroke-width="1"/>
-  <line x1="220" y1="388" x2="420" y2="388" stroke="rgba(180,150,30,0.06)" stroke-width="1"/>
-  <line x1="320" y1="396" x2="320" y2="120" stroke="#b09020" stroke-width="5"/>
-  <polygon points="320,120 310,144 330,144" fill="#b09020"/>
-  <path d="M320,396 L320,356 L340,360" fill="none" stroke="rgba(180,150,30,0.5)" stroke-width="2"/>
-  <text x="336" y="112" fill="#b09020" font-size="26" font-weight="700" font-family="monospace">N̂</text>
-  <text x="336" y="144" fill="#bbb" font-size="18" font-family="monospace">unit normal</text>
-  <text x="336" y="172" fill="#bbb" font-size="18" font-family="monospace">(perpendicular</text>
-  <text x="336" y="196" fill="#bbb" font-size="18" font-family="monospace"> to surface)</text>
-  <circle cx="140" cy="120" r="28" fill="rgba(180,150,30,0.08)" stroke="rgba(180,150,30,0.3)" stroke-width="2"/>
-  <circle cx="140" cy="120" r="8" fill="rgba(180,150,30,0.6)"/>
-  <text x="112" y="84" fill="#bbb" font-size="18" font-family="monospace">Light</text>
-  <line x1="160" y1="136" x2="300" y2="340" stroke="rgba(180,150,30,0.2)" stroke-width="2" stroke-dasharray="8 6"/>
-  <text x="164" y="284" fill="rgba(180,150,30,0.5)" font-size="18" font-family="monospace">L · N = brightness</text>
-</svg>
diff --git a/doc/Perspective correct textures/Adaptive interval.svg b/doc/Perspective correct textures/Adaptive interval.svg
deleted file mode 100644 (file)
index 924bc5c..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-<svg viewBox="0 0 620 270" width="620" height="270" xmlns="http://www.w3.org/2000/svg">
-  <rect width="620" height="270" fill="#061018"/>
-
-  <!-- curvature curve -->
-  <polyline points="70.0,128.0 72.5,127.8 75.0,127.7 77.5,127.5 80.0,127.3 82.5,127.2 85.0,127.0 87.5,126.8 90.0,126.6 92.5,126.5 95.0,126.3 97.5,126.1 100.0,125.9 102.5,125.8 105.0,125.6 107.5,125.4 110.0,125.2 112.5,125.0 115.0,124.8 117.5,124.7 120.0,124.5 122.5,124.3 125.0,124.1 127.5,123.9 130.0,123.7 132.5,123.5 135.0,123.3 137.5,123.1 140.0,122.9 142.5,122.7 145.0,122.5 147.5,122.3 150.0,122.1 152.5,121.9 155.0,121.7 157.5,121.5 160.0,121.3 162.5,121.1 165.0,120.9 167.5,120.7 170.0,120.5 172.5,120.2 175.0,120.0 177.5,119.8 180.0,119.6 182.5,119.4 185.0,119.1 187.5,118.9 190.0,118.7 192.5,118.5 195.0,118.2 197.5,118.0 200.0,117.8 202.5,117.5 205.0,117.3 207.5,117.1 210.0,116.8 212.5,116.6 215.0,116.3 217.5,116.1 220.0,115.9 222.5,115.6 225.0,115.4 227.5,115.1 230.0,114.9 232.5,114.6 235.0,114.3 237.5,114.1 240.0,113.8 242.5,113.6 245.0,113.3 247.5,113.0 250.0,112.8 252.5,112.5 255.0,112.2 257.5,111.9 260.0,111.7 262.5,111.4 265.0,111.1 267.5,110.8 270.0,110.5 272.5,110.2 275.0,109.9 277.5,109.7 280.0,109.4 282.5,109.1 285.0,108.8 287.5,108.5 290.0,108.2 292.5,107.8 295.0,107.5 297.5,107.2 300.0,106.9 302.5,106.6 305.0,106.3 307.5,105.9 310.0,105.6 312.5,105.3 315.0,105.0 317.5,104.6 320.0,104.3 322.5,103.9 325.0,103.6 327.5,103.3 330.0,102.9 332.5,102.6 335.0,102.2 337.5,101.8 340.0,101.5 342.5,101.1 345.0,100.7 347.5,100.4 350.0,100.0 352.5,99.6 355.0,99.2 357.5,98.9 360.0,98.5 362.5,98.1 365.0,97.7 367.5,97.3 370.0,96.9 372.5,96.5 375.0,96.1 377.5,95.6 380.0,95.2 382.5,94.8 385.0,94.4 387.5,93.9 390.0,93.5 392.5,93.1 395.0,92.6 397.5,92.2 400.0,91.7 402.5,91.3 405.0,90.8 407.5,90.3 410.0,89.9 412.5,89.4 415.0,88.9 417.5,88.4 420.0,87.9 422.5,87.4 425.0,86.9 427.5,86.4 430.0,85.9 432.5,85.4 435.0,84.9 437.5,84.3 440.0,83.8 442.5,83.3 445.0,82.7 447.5,82.2 450.0,81.6 452.5,81.1 455.0,80.5 457.5,79.9 460.0,79.3 462.5,78.7 465.0,78.1 467.5,77.5 470.0,76.9 472.5,76.3 475.0,75.7 477.5,75.0 480.0,74.4 482.5,73.8 485.0,73.1 487.5,72.4 490.0,71.8 492.5,71.1 495.0,70.4 497.5,69.7 500.0,69.0 502.5,68.3 505.0,67.6 507.5,66.8 510.0,66.1 512.5,65.4 515.0,64.6 517.5,63.8 520.0,63.0 522.5,62.3 525.0,61.5 527.5,60.6 530.0,59.8 532.5,59.0 535.0,58.1 537.5,57.3 540.0,56.4 542.5,55.5 545.0,54.7 547.5,53.7 550.0,52.8 552.5,51.9 555.0,51.0 557.5,50.0 560.0,49.0 562.5,48.0 565.0,47.0 567.5,46.0 570.0,45.0" fill="none" stroke="#c05088" stroke-width="2"/>
-  <text x="80.0" y="34" fill="#c05088" font-size="10" font-family="monospace">perspective curvature &#8594; rises toward the far end</text>
-
-  <!-- span bar with adaptive blocks -->
-  <rect x="70.0" y="150" width="80.0" height="34" fill="rgba(48,160,80,0.45)" stroke="#30a050" stroke-width="1"/>
-  <rect x="150.0" y="150" width="80.0" height="34" fill="rgba(48,160,80,0.45)" stroke="#30a050" stroke-width="1"/>
-  <rect x="230.0" y="150" width="80.0" height="34" fill="rgba(48,160,80,0.45)" stroke="#30a050" stroke-width="1"/>
-  <rect x="310.0" y="150" width="40.0" height="34" fill="rgba(221,153,0,0.45)" stroke="#dd9900" stroke-width="1"/>
-  <rect x="350.0" y="150" width="40.0" height="34" fill="rgba(221,153,0,0.45)" stroke="#dd9900" stroke-width="1"/>
-  <rect x="390.0" y="150" width="20.0" height="34" fill="rgba(255,102,0,0.45)" stroke="#FF6600" stroke-width="1"/>
-  <rect x="410.0" y="150" width="20.0" height="34" fill="rgba(255,102,0,0.45)" stroke="#FF6600" stroke-width="1"/>
-  <rect x="430.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="440.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="450.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="460.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="470.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="480.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="490.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="500.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="510.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="520.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="530.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="540.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="550.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <rect x="560.0" y="150" width="10.0" height="34" fill="rgba(208,64,64,0.45)" stroke="#d04040" stroke-width="1"/>
-  <line x1="70.0" y1="146" x2="70.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="150.0" y1="146" x2="150.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="230.0" y1="146" x2="230.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="310.0" y1="146" x2="310.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="350.0" y1="146" x2="350.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="390.0" y1="146" x2="390.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="410.0" y1="146" x2="410.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="430.0" y1="146" x2="430.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="440.0" y1="146" x2="440.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="450.0" y1="146" x2="450.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="460.0" y1="146" x2="460.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="470.0" y1="146" x2="470.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="480.0" y1="146" x2="480.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="490.0" y1="146" x2="490.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="500.0" y1="146" x2="500.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="510.0" y1="146" x2="510.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="520.0" y1="146" x2="520.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="530.0" y1="146" x2="530.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="540.0" y1="146" x2="540.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="550.0" y1="146" x2="550.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="560.0" y1="146" x2="560.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <line x1="570.0" y1="146" x2="570.0" y2="150" stroke="#aaa" stroke-width="1"/>
-  <text x="190.0" y="202" fill="#30a050" font-size="11" font-family="monospace" text-anchor="middle">16 px</text>
-  <text x="350.0" y="218" fill="#dd9900" font-size="11" font-family="monospace" text-anchor="middle">8 px</text>
-  <text x="410.0" y="202" fill="#FF6600" font-size="11" font-family="monospace" text-anchor="middle">4 px</text>
-  <text x="500.0" y="218" fill="#d04040" font-size="11" font-family="monospace" text-anchor="middle">2 px</text>
-
-  <text x="70" y="248" fill="#666" font-size="10" font-family="monospace">one scanline, 100 px &#8594;</text>
-  <text x="570" y="248" fill="#666" font-size="10" font-family="monospace" text-anchor="end">grazing-angle floor, far side</text>
-</svg>
diff --git a/doc/Perspective correct textures/Affine distortion.png b/doc/Perspective correct textures/Affine distortion.png
deleted file mode 100644 (file)
index 8d3722b..0000000
Binary files a/doc/Perspective correct textures/Affine distortion.png and /dev/null differ
diff --git a/doc/Perspective correct textures/Scanline correction.svg b/doc/Perspective correct textures/Scanline correction.svg
deleted file mode 100644 (file)
index cc5fc8d..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-<svg viewBox="0 0 620 280" width="620" height="280" xmlns="http://www.w3.org/2000/svg">
-  <rect width="620" height="280" fill="#061018"/>
-
-  <!-- grid -->
-  <line x1="70" y1="185.0" x2="570" y2="185.0" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="70" y1="140.0" x2="570" y2="140.0" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="70" y1="95.0" x2="570" y2="95.0" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="195.0" y1="50" x2="195.0" y2="230" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="320.0" y1="50" x2="320.0" y2="230" stroke="#1a3a4a" stroke-width="1"/>
-  <line x1="445.0" y1="50" x2="445.0" y2="230" stroke="#1a3a4a" stroke-width="1"/>
-
-  <!-- axes -->
-  <line x1="70" y1="230" x2="570" y2="230" stroke="#445566" stroke-width="1.5"/>
-  <line x1="70" y1="230" x2="70" y2="50" stroke="#445566" stroke-width="1.5"/>
-  <text x="570" y="252" fill="#666" font-size="10" font-family="monospace" text-anchor="end">screen pixel &#8594;</text>
-  <text x="62" y="42" fill="#666" font-size="10" font-family="monospace" text-anchor="start">texel u &#8593;</text>
-
-  <!-- affine (straight, wrong) -->
-  <polyline points="70.0,230.0 570.0,50.0" fill="none" stroke="#c05088" stroke-width="2" stroke-dasharray="6 4"/>
-
-  <!-- exact perspective curve -->
-  <polyline points="70.0,230.0 72.5,229.6 75.0,229.3 77.5,228.9 80.0,228.5 82.5,228.2 85.0,227.8 87.5,227.4 90.0,227.0 92.5,226.7 95.0,226.3 97.5,225.9 100.0,225.5 102.5,225.1 105.0,224.7 107.5,224.3 110.0,223.9 112.5,223.6 115.0,223.2 117.5,222.7 120.0,222.3 122.5,221.9 125.0,221.5 127.5,221.1 130.0,220.7 132.5,220.3 135.0,219.8 137.5,219.4 140.0,219.0 142.5,218.6 145.0,218.1 147.5,217.7 150.0,217.3 152.5,216.8 155.0,216.4 157.5,215.9 160.0,215.5 162.5,215.0 165.0,214.6 167.5,214.1 170.0,213.6 172.5,213.2 175.0,212.7 177.5,212.2 180.0,211.8 182.5,211.3 185.0,210.8 187.5,210.3 190.0,209.8 192.5,209.3 195.0,208.8 197.5,208.3 200.0,207.8 202.5,207.3 205.0,206.8 207.5,206.3 210.0,205.8 212.5,205.2 215.0,204.7 217.5,204.2 220.0,203.7 222.5,203.1 225.0,202.6 227.5,202.0 230.0,201.5 232.5,200.9 235.0,200.4 237.5,199.8 240.0,199.2 242.5,198.7 245.0,198.1 247.5,197.5 250.0,196.9 252.5,196.4 255.0,195.8 257.5,195.2 260.0,194.6 262.5,194.0 265.0,193.3 267.5,192.7 270.0,192.1 272.5,191.5 275.0,190.8 277.5,190.2 280.0,189.6 282.5,188.9 285.0,188.3 287.5,187.6 290.0,187.0 292.5,186.3 295.0,185.6 297.5,184.9 300.0,184.3 302.5,183.6 305.0,182.9 307.5,182.2 310.0,181.5 312.5,180.7 315.0,180.0 317.5,179.3 320.0,178.6 322.5,177.8 325.0,177.1 327.5,176.3 330.0,175.6 332.5,174.8 335.0,174.0 337.5,173.3 340.0,172.5 342.5,171.7 345.0,170.9 347.5,170.1 350.0,169.3 352.5,168.5 355.0,167.6 357.5,166.8 360.0,166.0 362.5,165.1 365.0,164.2 367.5,163.4 370.0,162.5 372.5,161.6 375.0,160.7 377.5,159.8 380.0,158.9 382.5,158.0 385.0,157.1 387.5,156.1 390.0,155.2 392.5,154.2 395.0,153.3 397.5,152.3 400.0,151.3 402.5,150.3 405.0,149.3 407.5,148.3 410.0,147.3 412.5,146.3 415.0,145.2 417.5,144.2 420.0,143.1 422.5,142.0 425.0,140.9 427.5,139.8 430.0,138.7 432.5,137.6 435.0,136.5 437.5,135.3 440.0,134.2 442.5,133.0 445.0,131.8 447.5,130.6 450.0,129.4 452.5,128.2 455.0,127.0 457.5,125.7 460.0,124.4 462.5,123.2 465.0,121.9 467.5,120.6 470.0,119.2 472.5,117.9 475.0,116.5 477.5,115.2 480.0,113.8 482.5,112.4 485.0,111.0 487.5,109.5 490.0,108.1 492.5,106.6 495.0,105.1 497.5,103.6 500.0,102.1 502.5,100.5 505.0,99.0 507.5,97.4 510.0,95.8 512.5,94.1 515.0,92.5 517.5,90.8 520.0,89.1 522.5,87.4 525.0,85.7 527.5,83.9 530.0,82.1 532.5,80.3 535.0,78.5 537.5,76.7 540.0,74.8 542.5,72.9 545.0,70.9 547.5,69.0 550.0,67.0 552.5,65.0 555.0,62.9 557.5,60.8 560.0,58.7 562.5,56.6 565.0,54.4 567.5,52.2 570.0,50.0" fill="none" stroke="#39FF14" stroke-width="2"/>
-
-  <!-- subdivided correction -->
-  <polyline points="70.0,230.0 150.0,217.3 230.0,201.5 310.0,181.5 390.0,155.2 470.0,119.2 570.0,50.0" fill="none" stroke="#FF6600" stroke-width="2"/>
-  <circle cx="70.0" cy="230.0" r="3.5" fill="#FF6600"/>
-  <circle cx="150.0" cy="217.3" r="3.5" fill="#FF6600"/>
-  <circle cx="230.0" cy="201.5" r="3.5" fill="#FF6600"/>
-  <circle cx="310.0" cy="181.5" r="3.5" fill="#FF6600"/>
-  <circle cx="390.0" cy="155.2" r="3.5" fill="#FF6600"/>
-  <circle cx="470.0" cy="119.2" r="3.5" fill="#FF6600"/>
-  <circle cx="570.0" cy="50.0" r="3.5" fill="#FF6600"/>
-
-  <!-- legend -->
-  <line x1="380" y1="20" x2="410" y2="20" stroke="#39FF14" stroke-width="2"/>
-  <text x="416" y="24" fill="#39FF14" font-size="10" font-family="monospace">exact perspective</text>
-  <line x1="380" y1="38" x2="410" y2="38" stroke="#FF6600" stroke-width="2"/>
-  <text x="416" y="42" fill="#FF6600" font-size="10" font-family="monospace">corrected every 16 px</text>
-  <line x1="380" y1="56" x2="410" y2="56" stroke="#c05088" stroke-width="2" stroke-dasharray="6 4"/>
-  <text x="416" y="60" fill="#c05088" font-size="10" font-family="monospace">plain affine</text>
-</svg>
diff --git a/doc/Perspective correct textures/index.org b/doc/Perspective correct textures/index.org
deleted file mode 100644 (file)
index 3438f01..0000000
+++ /dev/null
@@ -1,177 +0,0 @@
-#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
-#+TITLE: Perspective-Correct Textures - Aukio 3D
-#+LANGUAGE: en
-#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
-#+LATEX_HEADER: \usepackage{parskip}
-#+LATEX_HEADER: \usepackage[none]{hyphenat}
-
-#+OPTIONS: H:20 num:20
-#+OPTIONS: author:nil
-
-#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
-
-[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
-
-* The problem
-:PROPERTIES:
-:CUSTOM_ID: introduction
-:ID:       a2b3c4d5-e6f7-8901-bcde-f23456789012
-:END:
-
-When a textured polygon is rendered at an angle to the viewer, naive
-linear interpolation of texture coordinates produces visible
-distortion.
-
-Consider a large textured floor extending toward the horizon. Without
-perspective correction, the texture appears to "swim" or distort
-because the texture coordinates are interpolated linearly across
-screen space, not accounting for depth.
-
-#+attr_html: :class responsive-img
-#+attr_latex: :width 1000px
-[[file:Affine distortion.png]]
-
-The *Aukio 3D* engine solves this with *subdivided perspective
-correction* inside the scanline rasterizer — the same technique Quake
-used.
-
-* How perspective correction works
-:PROPERTIES:
-:CUSTOM_ID: how-perspective-correction-works
-:END:
-
-Texture coordinates (u, v) are not linear in screen space, so they
-cannot simply be stepped per pixel. But divide them by depth and they
-become linear: *(u/z, v/z, 1/z) all interpolate linearly* across the
-triangle in screen space.
-
-The rasterizer exploits this:
-
-1. Compute (u/z, v/z, 1/z) at each vertex
-2. Interpolate all three across the scanline with plain additions
-3. Every N pixels, recover the exact texture coordinate with one
-   division: =u = (u/z) / (1/z)=
-4. Between correction points, step u/v affinely toward the next exact
-   point
-
-#+INCLUDE: "Scanline correction.svg" export html
-
-The orange polyline hugs the exact green curve: within each 16-pixel
-block it is a straight line, but every block starts exactly on the
-curve. The dashed pink line is plain affine interpolation — visibly
-wrong everywhere except the endpoints.
-
-Think of it like walking with a map that is slightly distorted: you
-walk in a straight line, but every 16 steps you check a landmark and
-correct your course. The correction (a division) costs something, so
-you do it every N pixels instead of every pixel — the divide cost is
-amortized to 1/16th of a per-pixel-correct rasterizer.
-
-#+BEGIN_SRC java
-// Per scanline (simplified from drawHorizontalLinePerspective):
-while (done < span) {
-    // Advance (u/z, v/z, 1/z) to the end of this block
-    su += dsu * block;  sv += dsv * block;  sw += dsw * block;
-    double txNext = su / sw;   // one reciprocal = exact texture position
-    double tyNext = sv / sw;
-
-    // Step affinely through the block
-    double txStep = (txNext - tx) / block;
-    double tyStep = (tyNext - ty) / block;
-    for (int i = 0; i < block; i++) {
-        plot(x++, texture.sample(tx, ty));
-        tx += txStep;  ty += tyStep;
-    }
-    done += block;
-}
-#+END_SRC
-
-** Adaptive correction interval
-
-Quake used a fixed 16-pixel interval. This engine keeps 16 as the
-default but *shrinks the interval when the error bound demands it*.
-
-The error of affine stepping within a block grows with both the
-texture gradient (texels per pixel) and the perspective curvature
-(how fast 1/z changes across the span). Each scanline computes
-
-#+BEGIN_EXAMPLE
-error(texels) ≈ texelRate · interval² · k / 2
-#+END_EXAMPLE
-
-where =k = |d(1/z)| / min(1/z)= is the per-pixel relative depth change,
-and picks the largest power-of-two interval from the ladder 16, 8, 4,
-2, 1 that keeps the bound under half a texel. Flat, gently angled
-spans keep the fast 16-pixel cadence; a floor tile seen at a grazing
-angle drops to shorter intervals exactly where the curvature is high.
-
-#+INCLUDE: "Adaptive interval.svg" export html
-
-** When affine is good enough
-
-For small or nearly flat triangles, plain affine mapping is already
-within half a texel of exact perspective, so the perspective setup is
-skipped entirely. The test compares the texture range the triangle
-covers against its depth variation:
-
-#+BEGIN_EXAMPLE
-affine is sufficient when  texelSpan · (zMax/zMin − 1) < 2
-#+END_EXAMPLE
-
-Note the criterion is the *texel* span, not the pixel size — a tiny
-on-screen triangle can still map many texels into few pixels. Distant
-clusters of small triangles (a common case) all render through the
-cheaper affine path.
-
-Triangles straddling the near plane (any vertex closer than z = 0.001)
-also fall back to affine, because 1/z interpolation is invalid there.
-
-** Toggling the correction
-
-Perspective correction can be switched off globally for A/B comparison
-or debugging:
-
-#+BEGIN_SRC java
-TexturedTriangle.setPerspectiveCorrectionEnabled(false);  // plain affine everywhere
-#+END_SRC
-
-With correction disabled, large triangles at steep angles visibly warp
-— useful for demonstrating what the correction actually buys.
-
-* Mipmap selection
-:PROPERTIES:
-:CUSTOM_ID: mipmap-selection
-:END:
-
-Perspective correction fixes *where* a texel is sampled; mipmapping
-decides *which resolution* to sample from. Each triangle estimates its
-screen-pixels-per-texel ratio from edge lengths:
-
-#+BEGIN_EXAMPLE
-scaleFactor = (sum of screen edge lengths) / (sum of UV edge lengths) · 1.2
-#+END_EXAMPLE
-
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html#getMipmapForScale(double)][Texture.getMipmapForScale()]]
-then picks the lazily-generated mipmap level closest to that scale:
-halved resolutions when the texture is minified, doubled when strongly
-magnified. Sampling a smaller mipmap under minification both speeds up
-rendering (better cache behavior) and reduces aliasing.
-
-* Related Classes
-:PROPERTIES:
-:CUSTOM_ID: related-classes
-:END:
-
-| Class                         | Purpose                                                            |
-|-------------------------------+--------------------------------------------------------------------|
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]]              | Textured triangle with perspective-correct and SDF rendering paths |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PerspectiveBorderInterpolator.html][PerspectiveBorderInterpolator]] | Edge walker interpolating (u/z, v/z, 1/z) along triangle borders   |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.html][PolygonBorderInterpolator]]     | Edge walker for plain affine mapping                               |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html][Texture]]                       | Mipmap container; also carries the SDF mask and color layers       |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html][TextureBitmap]]                 | Raw pixel array for one mipmap level                               |
-
-*See also:*
-
-- [[file:../SDF textures/][SDF textures]] — signed-distance-field glyph rendering, the
-  alternative sampling path in =TexturedTriangle= that reuses the same
-  perspective-correct interpolation for crisp text at any angle.
diff --git a/doc/Point3D vertex.svg b/doc/Point3D vertex.svg
deleted file mode 100644 (file)
index 0954bac..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-<svg width="100%" viewBox="0 0 680 320" xmlns="http://www.w3.org/2000/svg"><rect width="680" height="320" fill="#061018"/><defs><mask id="imagine-text-gaps-eqff6y" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="680" height="350" fill="white"/><rect x="134.36932373046875" y="19.672515869140625" width="71.26135635375977" height="22.184885025024414" fill="black" rx="2"/><rect x="120.89203643798828" y="40.63203048706055" width="98.2159194946289" height="16.12325668334961" fill="black" rx="2"/><rect x="182" y="129.87673950195312" width="78.42510223388672" height="19.42959976196289" fill="black" rx="2"/><rect x="-4.000310796312988" y="66.63202667236328" width="104.23031616210938" height="16.12325668334961" fill="black" rx="2"/><rect x="268" y="66.63202667236328" width="62.12955093383789" height="16.12325668334961" fill="black" rx="2"/><rect x="26.07363510131836" y="132.6320343017578" width="44.086368560791016" height="16.12325668334961" fill="black" rx="2"/><rect x="-3.9983373035211116" y="146.6320343017578" width="74.15834045410156" height="16.12325668334961" fill="black" rx="2"/><rect x="268" y="140.6320343017578" width="98.2159194946289" height="16.12325668334961" fill="black" rx="2"/><rect x="50.129241943359375" y="198.6320343017578" width="50.10076141357422" height="16.12325668334961" fill="black" rx="2"/><rect x="56.14363479614258" y="212.6320343017578" width="44.086368560791016" height="16.12325668334961" fill="black" rx="2"/><rect x="268" y="206.6320343017578" width="74.15834045410156" height="16.12325668334961" fill="black" rx="2"/><rect x="108.86325073242188" y="268.63201904296875" width="122.27349853515625" height="16.12325668334961" fill="black" rx="2"/><rect x="93.82726287841797" y="284.63201904296875" width="152.34547424316406" height="16.12325668334961" fill="black" rx="2"/><rect x="478.88800048828125" y="19.672515869140625" width="62.224021911621094" height="22.184885025024414" fill="black" rx="2"/><rect x="436.83447265625" y="40.63203048706055" width="146.33108520507812" height="16.12325668334961" fill="black" rx="2"/><rect x="414" y="89.52991485595703" width="74.28429412841797" height="17.225370407104492" fill="black" rx="2"/><rect x="544" y="91.18309020996094" width="45.9127311706543" height="15.021142959594727" fill="black" rx="2"/><rect x="352" y="108.28520202636719" width="32.089067459106445" height="13.919028282165527" fill="black" rx="2"/><rect x="402" y="129.52992248535156" width="147.19703674316406" height="17.225370407104492" fill="black" rx="2"/><rect x="402" y="169.52992248535156" width="127.31173706054688" height="17.225370407104492" fill="black" rx="2"/><rect x="402" y="209.52992248535156" width="120.68330383300781" height="17.225370407104492" fill="black" rx="2"/><rect x="594" y="211.18309020996094" width="18.832207679748535" height="15.021142959594727" fill="black" rx="2"/><rect x="402" y="249.52992248535156" width="47.77057647705078" height="17.225370407104492" fill="black" rx="2"/><rect x="473" y="251.18309020996094" width="45.9127311706543" height="15.021142959594727" fill="black" rx="2"/><rect x="612.9194946289062" y="87.18309020996094" width="35.08052062988281" height="15.021142959594727" fill="black" rx="2"/><rect x="607.5033569335938" y="127.18309020996094" width="40.49662780761719" height="15.021142959594727" fill="black" rx="2"/><rect x="612.9194946289062" y="137.18309020996094" width="35.08052062988281" height="15.021142959594727" fill="black" rx="2"/><rect x="629.1677856445312" y="167.18309020996094" width="18.832207679748535" height="15.021142959594727" fill="black" rx="2"/><rect x="607.5033569335938" y="177.18309020996094" width="40.49662780761719" height="15.021142959594727" fill="black" rx="2"/><rect x="647.0900268554688" y="80.28520202636719" width="32.089067459106445" height="13.919028282165527" fill="black" rx="2"/><rect x="647.0900268554688" y="260.2851867675781" width="36.90688133239746" height="13.919028282165527" fill="black" rx="2"/><rect x="385.71209716796875" y="298.63201904296875" width="248.57579040527344" height="16.12325668334961" fill="black" rx="2"/></mask></defs>
-
-
-<!-- Divider -->
-<line x1="340" y1="30" x2="340" y2="310" stroke="#1a2a38" stroke-width="1" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(26, 42, 56);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-
-<!-- ===== LEFT: Point3D ===== -->
-<text x="170" y="36" text-anchor="middle" fill="#2070c0" font-size="15" font-weight="700" font-family="monospace" style="fill:rgb(32, 112, 192);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:auto">Point3D</text>
-<text x="170" y="52" text-anchor="middle" fill="#556677" font-size="10" font-family="monospace" style="fill:rgb(85, 102, 119);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:middle;dominant-baseline:auto">raw coordinates</text>
-
-<!-- Glow rings + point -->
-<circle cx="170" cy="148" r="36" fill="rgba(56,140,248,0.04)" stroke="none" style="fill:rgba(56, 140, 248, 0.04);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<circle cx="170" cy="148" r="20" fill="rgba(56,140,248,0.08)" stroke="none" style="fill:rgba(56, 140, 248, 0.08);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<circle cx="170" cy="148" r="8" fill="rgba(56,140,248,0.2)" stroke="none" style="fill:rgba(56, 140, 248, 0.2);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<circle cx="170" cy="148" r="4" fill="#2070c0" style="fill:rgb(32, 112, 192);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="186" y="144" fill="#2070c0" font-size="13" font-weight="700" font-family="monospace" style="fill:rgb(32, 112, 192);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:13px;font-weight:700;text-anchor:start;dominant-baseline:auto">(x, y, z)</text>
-
-<!-- Radial leader lines + labels — 6 directions, all consistent -->
-<!-- Top-left: distance -->
-<line x1="155" y1="132" x2="68" y2="82" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="3 2" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<circle cx="68" cy="82" r="1.5" fill="#2a3a4a" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="96.23" y="78" text-anchor="end" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:end;dominant-baseline:auto">.getDistanceTo()</text>
-
-<!-- Top-right: rotate -->
-<line x1="188" y1="134" x2="268" y2="82" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="3 2" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<circle cx="268" cy="82" r="1.5" fill="#2a3a4a" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="272" y="78" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:start;dominant-baseline:auto">.rotate()</text>
-
-<!-- Left: add/subtract -->
-<line x1="150" y1="148" x2="58" y2="148" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="3 2" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<circle cx="58" cy="148" r="1.5" fill="#2a3a4a" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="66.16" y="144" text-anchor="end" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:end;dominant-baseline:auto">.add()</text>
-<text x="66.16" y="158" text-anchor="end" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:end;dominant-baseline:auto">.subtract()</text>
-
-<!-- Right: crossProduct -->
-<line x1="190" y1="148" x2="268" y2="148" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="3 2" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<circle cx="268" cy="148" r="1.5" fill="#2a3a4a" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="272" y="152" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:start;dominant-baseline:auto">.crossProduct()</text>
-
-<!-- Bottom-left: unit/dot -->
-<line x1="155" y1="164" x2="68" y2="214" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="3 2" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<circle cx="68" cy="214" r="1.5" fill="#2a3a4a" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="96.23" y="210" text-anchor="end" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:end;dominant-baseline:auto">.unit()</text>
-<text x="96.23" y="224" text-anchor="end" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:end;dominant-baseline:auto">.dot()</text>
-
-<!-- Bottom-right: multiply -->
-<line x1="188" y1="162" x2="268" y2="214" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="3 2" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:3px, 2px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<circle cx="268" cy="214" r="1.5" fill="#2a3a4a" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="272" y="218" fill="#445566" font-size="10" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:start;dominant-baseline:auto">.multiply()</text>
-
-<!-- Summary -->
-<text x="170" y="280" text-anchor="middle" fill="#556677" font-size="10" font-family="monospace" style="fill:rgb(85, 102, 119);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Mutable, fluent API</text>
-<text x="170" y="296" text-anchor="middle" fill="#556677" font-size="10" font-family="monospace" style="fill:rgb(85, 102, 119);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Positions, vectors, math</text>
-
-<!-- ===== RIGHT: Vertex ===== -->
-<text x="510" y="36" text-anchor="middle" fill="#c05088" font-size="15" font-weight="700" font-family="monospace" style="fill:rgb(192, 80, 136);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:15px;font-weight:700;text-anchor:middle;dominant-baseline:auto">Vertex</text>
-<text x="510" y="52" text-anchor="middle" fill="#556677" font-size="10" font-family="monospace" style="fill:rgb(85, 102, 119);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:middle;dominant-baseline:auto">rendering-ready wrapper</text>
-
-<!-- Outer wrapper box -->
-<rect x="375" y="68" width="270" height="230" rx="6" fill="rgba(192,80,136,0.04)" stroke="rgba(192,80,136,0.2)" stroke-width="0.5" style="fill:rgba(192, 80, 136, 0.04);stroke:rgba(192, 80, 136, 0.2);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-
-<!-- coordinate (Point3D) -->
-<rect x="390" y="82" width="240" height="32" rx="4" fill="rgba(56,140,248,0.1)" stroke="rgba(56,140,248,0.3)" stroke-width="0.5" style="fill:rgba(56, 140, 248, 0.1);stroke:rgba(56, 140, 248, 0.3);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<circle cx="406" cy="98" r="3" fill="#2070c0" style="fill:rgb(32, 112, 192);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="418" y="102" fill="#2070c0" font-size="11" font-weight="700" font-family="monospace" style="fill:rgb(32, 112, 192);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:11px;font-weight:700;text-anchor:start;dominant-baseline:auto">coordinate</text>
-<text x="548" y="102" fill="#445566" font-size="9" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:start;dominant-baseline:auto">Point3D</text>
-
-<!-- "wraps" arrow -->
-<line x1="340" y1="148" x2="388" y2="98" stroke="#2a3a4a" stroke-width="0.5" stroke-dasharray="4 3" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgb(42, 58, 74);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-dasharray:4px, 3px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="356" y="118" fill="#334455" font-size="8" font-family="monospace" transform="rotate(-30 356 118)" style="fill:rgb(51, 68, 85);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:start;dominant-baseline:auto">wraps</text>
-
-<!-- transformedCoordinate -->
-<rect x="390" y="122" width="240" height="32" rx="4" fill="rgba(48,160,80,0.08)" stroke="rgba(48,160,80,0.25)" stroke-width="0.5" style="fill:rgba(48, 160, 80, 0.08);stroke:rgba(48, 160, 80, 0.25);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="406" y="142" fill="#30a050" font-size="11" font-weight="700" font-family="monospace" style="fill:rgb(48, 160, 80);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:11px;font-weight:700;text-anchor:start;dominant-baseline:auto">transformedCoordinate</text>
-
-<!-- onScreenCoordinate -->
-<rect x="390" y="162" width="240" height="32" rx="4" fill="rgba(255,102,0,0.08)" stroke="rgba(255,102,0,0.25)" stroke-width="0.5" style="fill:rgba(255, 102, 0, 0.08);stroke:rgba(255, 102, 0, 0.25);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="406" y="182" fill="#FF6600" font-size="11" font-weight="700" font-family="monospace" style="fill:rgb(255, 102, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:11px;font-weight:700;text-anchor:start;dominant-baseline:auto">onScreenCoordinate</text>
-
-<!-- textureCoordinate -->
-<rect x="390" y="202" width="240" height="32" rx="4" fill="rgba(176,144,32,0.08)" stroke="rgba(176,144,32,0.25)" stroke-width="0.5" style="fill:rgba(176, 144, 32, 0.08);stroke:rgba(176, 144, 32, 0.25);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="406" y="222" fill="#b09020" font-size="11" font-weight="700" font-family="monospace" style="fill:rgb(176, 144, 32);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:11px;font-weight:700;text-anchor:start;dominant-baseline:auto">textureCoordinate</text>
-<text x="598" y="222" fill="#445566" font-size="9" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:start;dominant-baseline:auto">UV</text>
-
-<!-- normal -->
-<rect x="390" y="242" width="240" height="32" rx="4" fill="rgba(80,96,192,0.08)" stroke="rgba(80,96,192,0.25)" stroke-width="0.5" style="fill:rgba(80, 96, 192, 0.08);stroke:rgba(80, 96, 192, 0.25);color:rgb(251, 251, 254);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="406" y="262" fill="#5060c0" font-size="11" font-weight="700" font-family="monospace" style="fill:rgb(80, 96, 192);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:11px;font-weight:700;text-anchor:start;dominant-baseline:auto">normal</text>
-<text x="477" y="262" fill="#445566" font-size="9" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:start;dominant-baseline:auto">for CSG</text>
-
-<!-- Right-side annotations -->
-<text x="644" y="98" fill="#334455" font-size="9" font-family="monospace" text-anchor="end" style="fill:rgb(51, 68, 85);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:end;dominant-baseline:auto">local</text>
-<text x="644" y="138" fill="#334455" font-size="9" font-family="monospace" text-anchor="end" style="fill:rgb(51, 68, 85);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:end;dominant-baseline:auto">camera</text>
-<text x="644" y="148" fill="#334455" font-size="9" font-family="monospace" text-anchor="end" style="fill:rgb(51, 68, 85);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:end;dominant-baseline:auto">space</text>
-<text x="644" y="178" fill="#334455" font-size="9" font-family="monospace" text-anchor="end" style="fill:rgb(51, 68, 85);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:end;dominant-baseline:auto">2D</text>
-<text x="644" y="188" fill="#334455" font-size="9" font-family="monospace" text-anchor="end" style="fill:rgb(51, 68, 85);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:end;dominant-baseline:auto">pixels</text>
-
-<!-- Pipeline arrow -->
-<line x1="654" y1="92" x2="654" y2="270" stroke="rgba(192,80,136,0.15)" stroke-width="1" mask="url(#imagine-text-gaps-eqff6y)" style="fill:rgb(0, 0, 0);stroke:rgba(192, 80, 136, 0.15);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="651.09" y="90" fill="#445566" font-size="8" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:start;dominant-baseline:auto">local</text>
-<text x="651.09" y="270" fill="#445566" font-size="8" font-family="monospace" style="fill:rgb(68, 85, 102);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:start;dominant-baseline:auto">screen</text>
-<polygon points="654,272 650,265 658,265" fill="rgba(192,80,136,0.3)" style="fill:rgba(192, 80, 136, 0.3);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-
-<!-- Summary -->
-<text x="510" y="310" text-anchor="middle" fill="#556677" font-size="10" font-family="monospace" style="fill:rgb(85, 102, 119);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Tracks position across coordinate spaces</text>
-</svg>
diff --git a/doc/Rendering loop/CPU scheduling.png b/doc/Rendering loop/CPU scheduling.png
deleted file mode 100644 (file)
index 12aa23d..0000000
Binary files a/doc/Rendering loop/CPU scheduling.png and /dev/null differ
diff --git a/doc/Rendering loop/Double buffering.svg b/doc/Rendering loop/Double buffering.svg
deleted file mode 100644 (file)
index 141dad6..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<svg viewBox="0 0 520 180" width="520" height="180" xmlns="http://www.w3.org/2000/svg">
-  <rect width="520" height="180" fill="#061018"/>
-
-  <!-- LEFT: Tearing -->
-  <text x="125" y="18" fill="#d04040" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">Without double-buffering</text>
-
-  <rect x="25" y="28" width="200" height="140" stroke="rgba(100,100,100,0.4)" stroke-width="1" fill="none" rx="3"/>
-  <text x="125" y="44" fill="#aaa" font-size="8" font-family="monospace" text-anchor="middle">display shows partial update</text>
-
-  <!-- Old frame top half -->
-  <rect x="45" y="52" width="160" height="45" fill="rgba(208,64,64,0.12)" stroke="rgba(208,64,64,0.4)" stroke-width="1"/>
-  <text x="125" y="79" fill="rgba(208,64,64,0.6)" font-size="9" font-family="monospace" text-anchor="middle">old frame</text>
-
-  <!-- Tear line -->
-  <line x1="45" y1="97" x2="205" y2="97" stroke="#d04040" stroke-width="2" stroke-dasharray="4,3"/>
-  <text x="228" y="100" fill="#d04040" font-size="8" font-family="monospace">← tear</text>
-
-  <!-- New frame bottom half -->
-  <rect x="45" y="97" width="160" height="55" fill="rgba(48,160,80,0.1)" stroke="rgba(48,160,80,0.4)" stroke-width="1"/>
-  <text x="125" y="130" fill="rgba(48,160,80,0.6)" font-size="9" font-family="monospace" text-anchor="middle">new frame</text>
-
-  <!-- RIGHT: Double-buffered -->
-  <text x="400" y="18" fill="#30a050" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">With double-buffering</text>
-
-  <!-- Back buffer -->
-  <rect x="295" y="35" width="90" height="130" fill="rgba(32,112,192,0.08)" stroke="rgba(32,112,192,0.5)" stroke-width="1.5" rx="2"/>
-  <text x="340" y="58" fill="#2070c0" font-size="9" font-family="monospace" text-anchor="middle">Back buffer</text>
-  <text x="340" y="72" fill="#aaa" font-size="7" font-family="monospace" text-anchor="middle">(draw here)</text>
-  <!-- Scribble lines to suggest "being drawn" -->
-  <line x1="310" y1="90" x2="365" y2="90" stroke="rgba(32,112,192,0.25)" stroke-width="1"/>
-  <line x1="310" y1="100" x2="355" y2="100" stroke="rgba(32,112,192,0.2)" stroke-width="1"/>
-  <line x1="310" y1="110" x2="345" y2="110" stroke="rgba(32,112,192,0.15)" stroke-width="1"/>
-
-  <!-- Swap arrow -->
-  <line x1="390" y1="100" x2="415" y2="100" stroke="#30a050" stroke-width="1.5"/>
-  <polygon points="415,96 423,100 415,104" fill="#30a050"/>
-  <text x="407" y="90" fill="#30a050" font-size="7" font-family="monospace" text-anchor="middle">swap</text>
-
-  <!-- Front buffer -->
-  <rect x="428" y="35" width="80" height="130" fill="rgba(48,160,80,0.1)" stroke="#30a050" stroke-width="1.5" rx="2"/>
-  <text x="468" y="58" fill="#30a050" font-size="9" font-family="monospace" text-anchor="middle">Front buffer</text>
-  <text x="468" y="72" fill="#aaa" font-size="7" font-family="monospace" text-anchor="middle">(displayed)</text>
-  <!-- Solid fill to suggest complete frame -->
-  <rect x="440" y="85" width="56" height="65" fill="rgba(48,160,80,0.08)" rx="1"/>
-  <text x="468" y="122" fill="rgba(48,160,80,0.5)" font-size="8" font-family="monospace" text-anchor="middle">complete</text>
-  <text x="468" y="133" fill="rgba(48,160,80,0.5)" font-size="8" font-family="monospace" text-anchor="middle">frame</text>
-</svg>
diff --git a/doc/Rendering loop/Paint tiles.svg b/doc/Rendering loop/Paint tiles.svg
deleted file mode 100644 (file)
index e27d5f8..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-<svg viewBox="0 0 400 200" width="400" height="200" xmlns="http://www.w3.org/2000/svg">
-  <rect width="400" height="200" fill="#061018"/>
-
-  <!-- Tile grid: 5 columns x 4 rows = 20 tiles over a 380x160 viewport -->
-  <g fill="#1a3a4a" stroke="#30a050" stroke-width="1">
-    <rect x="10"  y="5" width="76" height="40"/>
-    <rect x="86"  y="5" width="76" height="40"/>
-    <rect x="162" y="5" width="76" height="40"/>
-    <rect x="238" y="5" width="76" height="40"/>
-    <rect x="314" y="5" width="76" height="40"/>
-    <rect x="10"  y="45" width="76" height="40"/>
-    <rect x="86"  y="45" width="76" height="40"/>
-    <rect x="162" y="45" width="76" height="40"/>
-    <rect x="238" y="45" width="76" height="40"/>
-    <rect x="314" y="45" width="76" height="40"/>
-    <rect x="10"  y="85" width="76" height="40"/>
-    <rect x="86"  y="85" width="76" height="40"/>
-    <rect x="162" y="85" width="76" height="40"/>
-    <rect x="238" y="85" width="76" height="40"/>
-    <rect x="314" y="85" width="76" height="40"/>
-    <rect x="10"  y="125" width="76" height="40"/>
-    <rect x="86"  y="125" width="76" height="40"/>
-    <rect x="162" y="125" width="76" height="40"/>
-    <rect x="238" y="125" width="76" height="40"/>
-    <rect x="314" y="125" width="76" height="40"/>
-  </g>
-
-  <!-- A shape overlapping several tiles gets binned into each of them -->
-  <ellipse cx="200" cy="85" rx="90" ry="45" fill="rgba(255,102,0,0.25)" stroke="#FF6600" stroke-width="1.5"/>
-  <text x="200" y="89" fill="#FF6600" font-size="10" font-family="monospace" text-anchor="middle">one shape</text>
-
-  <text x="10" y="182" fill="#30a050" font-size="10" font-family="monospace">~10 tiles per thread; threads steal pending</text>
-  <text x="10" y="195" fill="#30a050" font-size="10" font-family="monospace">tiles — no fixed thread↔tile assignment</text>
-</svg>
diff --git a/doc/Rendering loop/Painter's algorithm.svg b/doc/Rendering loop/Painter's algorithm.svg
deleted file mode 100644 (file)
index 7727fc2..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-
-<svg viewBox="0 0 520 180" width="520" height="180" xmlns="http://www.w3.org/2000/svg">
-  <rect width="520" height="180" fill="#061018"/>
-  <!-- Far -->
-  <rect x="30" y="15" width="440" height="150" fill="rgba(48,160,80,0.06)" stroke="rgba(48,160,80,0.35)" stroke-width="1.5"/>
-  <text x="250" y="38" fill="rgba(48,160,80,0.7)" font-size="11" font-family="monospace" text-anchor="middle">Far (Z=500) — painted first</text>
-  <!-- Medium -->
-  <rect x="70" y="48" width="360" height="105" fill="rgba(32,112,192,0.10)" stroke="rgba(32,112,192,0.5)" stroke-width="1.5"/>
-  <text x="250" y="80" fill="rgba(32,112,192,0.85)" font-size="11" font-family="monospace" text-anchor="middle">Medium (Z=300) — painted second</text>
-  <!-- Near -->
-  <rect x="115" y="90" width="270" height="55" fill="rgba(200,80,140,0.18)" stroke="#c05088" stroke-width="2"/>
-  <text x="250" y="123" fill="#c05088" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">Near (Z=100) — painted last</text>
-</svg>
diff --git a/doc/Rendering loop/Render pipeline.svg b/doc/Rendering loop/Render pipeline.svg
deleted file mode 100644 (file)
index 927e357..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-<svg viewBox="0 0 620 80" width="620" height="80" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <marker id="arrowhead" viewBox="0 0 10 10" refX="9" refY="5"
-            markerWidth="6" markerHeight="6" orient="auto">
-      <path d="M 0 0 L 10 5 L 0 10 z" fill="#30a050"/>
-    </marker>
-  </defs>
-  <rect width="620" height="80" fill="#061018"/>
-
-  <!-- Boxes -->
-  <rect x="8" y="25" width="70" height="30" rx="3" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
-  <text x="43" y="43" fill="#30a050" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Shapes</text>
-
-  <rect x="96" y="25" width="90" height="30" rx="3" fill="rgba(32,112,192,0.15)" stroke="#2070c0" stroke-width="1.5"/>
-  <text x="141" y="43" fill="#2070c0" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Transform</text>
-
-  <rect x="204" y="25" width="60" height="30" rx="3" fill="rgba(200,80,140,0.15)" stroke="#c05088" stroke-width="1.5"/>
-  <text x="234" y="43" fill="#c05088" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Sort</text>
-
-  <rect x="282" y="25" width="60" height="30" rx="3" fill="rgba(255,170,0,0.15)" stroke="#dd9900" stroke-width="1.5"/>
-  <text x="312" y="43" fill="#dd9900" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Bin</text>
-
-  <rect x="360" y="25" width="70" height="30" rx="3" fill="rgba(255,102,0,0.15)" stroke="#FF6600" stroke-width="1.5"/>
-  <text x="395" y="43" fill="#FF6600" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Paint</text>
-
-  <rect x="448" y="25" width="80" height="30" rx="3" fill="rgba(57,255,20,0.15)" stroke="#39FF14" stroke-width="1.5"/>
-  <text x="488" y="43" fill="#39FF14" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Present</text>
-
-  <rect x="546" y="25" width="66" height="30" rx="3" fill="rgba(100,100,100,0.2)" stroke="#aaa" stroke-width="1.5"/>
-  <text x="579" y="43" fill="#aaa" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Screen</text>
-
-  <!-- Arrows -->
-  <line x1="78" y1="40" x2="92" y2="40" stroke="#30a050" stroke-width="1.5" marker-end="url(#arrowhead)"/>
-  <line x1="186" y1="40" x2="200" y2="40" stroke="#2070c0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
-  <line x1="264" y1="40" x2="278" y2="40" stroke="#c05088" stroke-width="1.5" marker-end="url(#arrowhead)"/>
-  <line x1="342" y1="40" x2="356" y2="40" stroke="#dd9900" stroke-width="1.5" marker-end="url(#arrowhead)"/>
-  <line x1="430" y1="40" x2="444" y2="40" stroke="#FF6600" stroke-width="1.5" marker-end="url(#arrowhead)"/>
-  <line x1="528" y1="40" x2="542" y2="40" stroke="#39FF14" stroke-width="1.5" marker-end="url(#arrowhead)"/>
-
-  <!-- Labels below -->
-  <text x="43" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">3D vertices</text>
-  <text x="141" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">world→screen</text>
-  <text x="234" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">back-to-front</text>
-  <text x="312" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">per tile</text>
-  <text x="395" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">tile grid</text>
-  <text x="488" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">own thread</text>
-</svg>
diff --git a/doc/Rendering loop/index.org b/doc/Rendering loop/index.org
deleted file mode 100644 (file)
index c7fe2e0..0000000
+++ /dev/null
@@ -1,523 +0,0 @@
-#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
-#+TITLE: Rendering Loop - Aukio 3D
-#+LANGUAGE: en
-#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
-#+LATEX_HEADER: \usepackage{parskip}
-#+LATEX_HEADER: \usepackage[none]{hyphenat}
-
-#+OPTIONS: H:20 num:20
-#+OPTIONS: author:nil
-
-#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
-
-[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
-
-* Rendering loop
-:PROPERTIES:
-:CUSTOM_ID: rendering-loop
-:ID:       a1b2c3d4-e5f6-7890-abcd-ef1234567890
-:END:
-
-The rendering loop is the heart of the engine, continuously generating
-frames on a dedicated background thread. It orchestrates the entire
-rendering pipeline from 3D world space to pixels on screen.
-
-** What is a render loop?
-:PROPERTIES:
-:CUSTOM_ID: what-is-a-render-loop
-:END:
-
-A *render loop* is a continuous process that generates visual frames
-from 3D data. Think of it like a movie camera: each "frame" captures
-the current state of the 3D world and converts it into a 2D image that
-can be displayed on screen.
-
-The process transforms shapes through multiple coordinate systems:
-
-#+INCLUDE: "Render pipeline.svg" export html
-
-Each step has a specific purpose:
-
-| Step      | Input | Output | Purpose |
-|-----------+-------+--------+---------|
-| Shapes    | 3D [[file:../index.org::#vertex][vertices]], [[file:../index.org::#mesh][meshes]] | Scene data | Objects waiting to be drawn |
-| Transform | World coordinates | Screen coordinates | Convert 3D positions to where they appear on screen (see [[file:../index.org::#coordinate-system][coordinate system]]); cull shapes outside the view frustum. Parallel: heavy subtrees fork onto the worker pool |
-| Sort      | Unordered shapes | Ordered by depth | Ensure correct visibility (far objects painted first). Parallel merge sort for large scenes |
-| Bin       | Sorted shapes | Per-tile shape lists | Each paint tile iterates only shapes that can touch it. Parallel over the worker pool |
-| Paint     | Per-tile shape lists | Pixels in buffer | Tiles split the screen into independent work units so clearing and rasterization run in parallel across CPU cores |
-| Present   | Pixel buffer | Screen image | Hand the completed frame to a dedicated thread that copies it to the display |
-
-This pipeline runs repeatedly, targeting 60 frames per second by
-default. Even if nothing moves, the loop continues running—but the
-engine [[#frame-listeners][skips unnecessary work]] when the scene is static.
-
-The steps above describe one frame *logically*, in the order data flows
-through it. In execution the engine is a software pipeline: transform
-of the next frame already runs while the previous frame is still being
-painted, and presentation happens on its own thread. See
-[[#software-pipeline][Software pipeline]].
-
-** Main loop structure
-:PROPERTIES:
-:CUSTOM_ID: main-loop-structure
-:END:
-
-The engine runs two dedicated daemon threads:
-
-- =e3d-render= — produces frames. It runs continuously:
-
-#+BEGIN_SRC java
-while (renderThreadRunning) {
-    ensureThatViewIsUpToDate();  // Produce one frame (or skip)
-    maintainTargetFps();         // Sleep if ahead of schedule
-}
-#+END_SRC
-
-- =e3d-present= — presents frames. It takes completed frames from a
-  mailbox and performs all display-path work (the multi-megabyte
-  =drawImage=, =BufferStrategy.show()= and the X server round-trip), so
-  the render thread never blocks on the display.
-
-Both threads are daemons, so they stop automatically when the JVM
-exits. You can stop them explicitly with [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#stop()][ViewPanel.stop()]].
-
-** Frame rate control
-:PROPERTIES:
-:CUSTOM_ID: frame-rate-control
-:END:
-
-The engine supports two modes:
-
-- *Target FPS mode*: Set with [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#setFrameRate(int)][setFrameRate(int)]].
-  The engine tries to maintain the target rate by sleeping between frames.
-
-  - *When rendering is slower than target*: No sleeping occurs. The engine
-    runs at maximum hardware speed. Missed frames are skipped, not
-    rendered later — the timing simply resets to current time.
-
-  - *When rendering is faster than target*: The thread sleeps to limit FPS
-    to the target rate, avoiding unnecessary CPU usage.
-
-  For example, with a 60 FPS target:
-  - If a complex scene takes 30ms per frame, you get ~33 FPS (hardware limit)
-  - If the scene later simplifies to 10ms per frame, you get exactly
-    60 FPS (throttled by sleeping)
-
-- *Unlimited mode*: Set =setFrameRate(0)= or negative. No sleeping —
-  renders as fast as possible, and frames are produced even when the
-  scene reports no changes, so the measured rate reflects maximum
-  achievable throughput. Useful for benchmarking.
-
-*Production vs presentation.* These are measured separately:
-
-- *Production rate* (=getMeasuredFPS()=) counts frames the pipeline
-  completes per second. This is the benchmark number.
-- *Presentation rate* is how fast frames actually reach the screen. In
-  capped-FPS mode the present thread is paced to 60 blits per second
-  (override with =-Daukio3d.presentRate=N=); the cap exists because the
-  X server also dispatches input, and flooding it with blits causes
-  desktop-wide mouse/keyboard jitter. When production outruns
-  presentation, stale frames are dropped from the mailbox instead of
-  piling up latency. In unlimited (benchmark) mode presentation pacing
-  is disabled entirely, so it cannot throttle production.
-
-* Software pipeline
-:PROPERTIES:
-:CUSTOM_ID: software-pipeline
-:END:
-
-The phases below are described per frame, but consecutive frames
-*overlap*. The engine triple-buffers everything a frame writes:
-
-- 3 framebuffers (each with its own =RenderingContext=)
-- 3 projection buffer slots (per-vertex screen state)
-- 3 render aggregators (transform output, sort/bin state)
-
-A render pass P (one per frame, or one per eye in stereo) transforms
-into slot P mod 3, so it only conflicts with the paint of pass P-3.
-Before each transform the render thread *flushes* completed paint
-passes (mouse hits, frame deposit) and blocks only if paint P-3 is
-still running — which steady-state worker throughput prevents. Workers
-finishing one pass's tiles flow straight into the next pass's queued
-tiles with no idle gap.
-
-Completed frames go to a *presentation mailbox* that keeps only the
-newest frame: if the display path is slower than production, stale
-frames are dropped (and their buffers released) instead of
-accumulating latency — swapchain "mailbox mode".
-
-A per-buffer *present gate* guarantees painting frame F+3 never
-overwrites a buffer the present thread is still blitting frame F from.
-
-The goal of all this overlap is throughput: keep every CPU core busy,
-all the time. No phase waits for another phase of the same frame when
-it could already be working on the next one. The Developer Tools
-thread-activity timeline shows it working — all 18 worker rows packed
-solid with paint, bin and sort tasks from up to three frames at once,
-while the render thread (top row) and present thread tick along above
-them:
-
-#+attr_html: :class responsive-img
-[[file:CPU scheduling.png]]
-
-The pipeline can be disabled with =-Daukio3d.pipeline=false=, restoring
-strictly sequential phase order (each paint pass is awaited
-immediately). This is a kill switch for benchmarking and regression
-hunting.
-
-* Rendering phases
-:PROPERTIES:
-:CUSTOM_ID: rendering-phases
-:END:
-
-Each frame goes through 6 phases. Phases 2–4 run inside an
-asynchronous continuation on the shared worker pool, and phases of
-consecutive frames overlap as described in [[#software-pipeline][Software pipeline]].
-
-** Phase 1: Transform shapes
-:PROPERTIES:
-:CUSTOM_ID: phase-1-transform-shapes
-:END:
-
-All shapes are transformed from world space to screen space:
-
-1. Build camera-relative transform (inverse of camera position/rotation)
-2. Update the view frustum from camera state and viewport dimensions
-3. Walk the scene tree:
-   - Cull composite shapes whose bounding box misses the frustum
-   - Apply camera transform
-   - Project 3D → 2D (perspective projection)
-   - Calculate depth for sorting
-   - Queue for rendering
-
-*What is coordinate transformation?*
-
-Every shape exists in "world space" — its own position in the 3D world.
-To render it, we must convert to "screen space" — where it appears on
-your monitor. This involves:
-
-- *Translation*: Move coordinates relative to camera position
-- *Rotation*: Rotate coordinates based on camera orientation
-- *Projection*: Convert 3D (x, y, z) to 2D (x, y) screen pixels
-
-Objects further away appear smaller (perspective). The [[file:../index.org::#coordinate-system][coordinate system]]
-uses Y-down to match screen conventions, making projection straightforward.
-
-The transform is *parallel and non-blocking*: composites with enough
-children fork their render lists into chunk tasks on the shared worker
-pool (at any nesting level), and the render thread returns without
-waiting. The chunk tasks are drained and merged on a worker thread
-inside the paint continuation, while the render thread is already
-walking the next pass.
-
-*Frustum culling* happens here: composites test their bounding box
-against the frustum and skip invisible subtrees entirely, saving both
-transform and paint work. Per-frame culling statistics are collected
-for the developer tools panel.
-
-** Phase 2: Sort shapes by depth
-:PROPERTIES:
-:CUSTOM_ID: phase-2-sort-shapes
-:END:
-
-Shapes are sorted by depth in descending order (farthest first), with
-the shape id as a deterministic tiebreaker:
-
-#+BEGIN_SRC java
-// ShapesZIndexComparator: descending Z, ties broken by shape id
-if (z1 < z2) return 1;        // z1 is nearer -> sort after z2
-else if (z1 > z2) return -1;  // z1 is farther -> sort before z2
-return Integer.compare(o1.shapeId, o2.shapeId);
-#+END_SRC
-
-Above 8192 queued shapes the sort runs as an instrumented parallel
-merge sort on the shared worker pool; below that it is single-threaded.
-
-*Why sort back-to-front?*
-
-This implements the *painter's algorithm* — like painting a landscape:
-first paint the sky (farthest), then mountains, then trees, then the
-foreground. Each layer covers what's behind it.
-
-#+INCLUDE: "Painter's algorithm.svg" export html
-
-Without sorting, nearby objects might be painted first and then covered
-by distant ones, causing visual errors. This is especially important for
-*transparent objects* — you need to see through the near ones to what's
-behind.
-
-The Z value represents distance from the camera after transformation.
-Larger values = further away. The id tiebreaker keeps the order
-deterministic frame-to-frame, which tiled rendering relies on: every
-tile paints its shapes in the same global (Z, id) order.
-
-** Phase 3: Bin shapes into tiles
-:PROPERTIES:
-:CUSTOM_ID: phase-3-bin-shapes-into-tiles
-:END:
-
-The sorted queue is binned per paint tile by screen-space overlap:
-each tile's bin lists only the shapes whose vertex bounds (plus a
-paint margin) can touch that tile. A shape overlapping several tiles
-is added to each of their bins.
-
-This means a paint thread iterates a short local list instead of the
-whole scene, and it is what makes the tile grid scale: refining the
-grid shrinks each bin instead of just subdividing the clearing work.
-
-Binning is parallelized over the shared worker pool.
-
-** Phase 4: Clear and paint tiles (multi-threaded)
-:PROPERTIES:
-:CUSTOM_ID: phase-4-clear-paint-tiles
-:END:
-
-The viewport is divided into a grid of rectangular *tiles* — roughly
-10 tiles per render thread, split into near-squares (square tiles
-minimize boundary crossings, i.e. how many tiles each shape overlaps).
-These are *not* horizontal bands: each tile has both X and Y bounds.
-
-#+INCLUDE: "Paint tiles.svg" export html
-
-Painting is work-stolen, not pre-assigned. All tile tasks go onto a
-shared =ForkJoinPool= (sized to 75% of CPU threads by default, at most
-cores − 1, so one thread stays free for the rest of the system;
-changeable at runtime via =setNumRenderThreads(int)=). A worker that
-finishes a cheap tile immediately pulls the next queued task — another
-tile (of this or an adjacent frame's pass), a transform chunk, a sort
-piece — so cores never idle behind a busy thread.
-
-Each tile task:
-
-1. *Clear tile*: fill its rectangle with background color
-2. *Paint shapes*: rasterize the tile's bin, back-to-front, clipping
-   at tile bounds
-
-Both operations happen within the same task, so clearing always
-completes before painting on that tile. Parallel clearing across
-disjoint tiles maximizes memory bandwidth utilization.
-
-Each tile renders through a =SegmentRenderingContext= — a view of the
-frame context carrying the tile's X/Y bounds and a =Graphics2D=
-pre-clipped to the tile rectangle for thread-safe text and
-anti-aliased drawing. (The class name predates the tile grid; a
-"segment" is now a tile.) Mouse hit detection happens during painting,
-before clipping.
-
-A =CountDownLatch= tracks completion of all the pass's tiles — but the
-render thread does *not* wait for it here. The latch is awaited one
-pass later, during the flush (see [[#phase-5-flush-completed-passes][Phase 5]]).
-
-** Phase 5: Flush completed passes
-:PROPERTIES:
-:CUSTOM_ID: phase-5-flush-completed-passes
-:END:
-
-Before each new transform, the render thread flushes paint passes that
-have completed. For each flushed pass:
-
-1. Await its tile latch (blocks only when correctness demands it —
-   transform of pass P may not start before paint of pass P-3 finished)
-2. *Combine mouse results*: during painting, each tile tracked which
-   shape is under the mouse cursor. Since all tiles paint the same
-   back-to-front order, they should all report the same hit; the first
-   non-null result wins:
-
-#+BEGIN_SRC java
-for (SegmentRenderingContext ctx : segmentContexts) {
-    if (ctx.getSegmentMouseHit() != null) {
-        context.setCurrentObjectUnderMouseCursor(ctx.getSegmentMouseHit());
-        return;
-    }
-}
-#+END_SRC
-
-   In stereo mode this only runs for the eye whose viewport actually
-   contains the cursor — each eye sees a different camera position, so
-   combining for the wrong eye would overwrite a valid hit with null.
-3. If this pass completed a frame, deposit the frame into the
-   presentation mailbox (see [[#software-pipeline][Software pipeline]]). The render
-   thread never blocks on the display.
-
-Passes that finished painting are flushed without any blocking, so
-completed frames reach the mailbox as early as possible.
-
-** Phase 6: Present frame
-:PROPERTIES:
-:CUSTOM_ID: phase-6-present-frame
-:END:
-
-The =e3d-present= thread takes the newest mailbox frame (dropping any
-unshown older frame) and copies its =BufferedImage= to the screen using
-[[https://cr.openjdk.org/~iris/se/17/latestSpec/api/java.desktop/java/awt/image/BufferStrategy.html][BufferStrategy]] for tear-free page-flipping:
-
-#+BEGIN_SRC java
-do {
-    Graphics2D g = bufferStrategy.getDrawGraphics();
-    g.drawImage(context.bufferedImage, 0, 0, null);
-    g.dispose();
-} while (bufferStrategy.contentsRestored());
-
-// framebuffer released for reuse here
-bufferStrategy.show();
-Toolkit.getDefaultToolkit().sync();
-#+END_SRC
-
-The frame's buffer is released for reuse right after the =drawImage=
-loop — =show()= and =sync()= touch only the BufferStrategy's own back
-buffer and the X connection, and at high resolutions they cost more
-than the draw itself, so the next frame's painters don't wait for them.
-
-*What is double-buffering?*
-
-Without double-buffering, the screen updates while pixels are being
-written. This causes *screen tearing* — visible horizontal splits where
-the top of the frame shows old content while the bottom shows new.
-
-#+INCLUDE: "Double buffering.svg" export html
-
-Double-buffering uses two pixel buffers:
-- *Back buffer*: Where rendering happens (offscreen, invisible)
-- *Front buffer*: What's currently displayed on screen
-
-When rendering completes, the buffers *swap* in one atomic operation.
-The viewer always sees complete frames, never partial updates.
-
-The =do-while= loop handles the case where the OS recreates the back
-buffer (common during window resizing). Since our offscreen
-=BufferedImage= still has the correct pixels, we only need to re-blit,
-not re-render.
-
-* Frame listeners and smart repaint skipping
-:PROPERTIES:
-:CUSTOM_ID: frame-listeners
-:ID:       e360a877-cca6-4cba-a9a4-ea40b0f1a183
-:END:
-
-A *FrameListener* is a callback that runs custom logic before each potential
-frame. Think of it as your "per-frame hook" — the engine calls all registered
-listeners, giving them a chance to update animations, physics, or game logic.
-
-** Registering a frame listener
-
-Use [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#addFrameListener(eu.svjatoslav.aukio.e3d.gui.FrameListener)][addFrameListener()]] to register your callback:
-
-#+BEGIN_SRC java
-// This is how you register a frame listener
-viewPanel.addFrameListener((panel, deltaMs) -> {
-    // Example: simple animation listener
-    double rotationSpeed = 1.0;  // radians per second
-    shape.rotate(rotationSpeed * deltaMs / 1000.0);  // Framerate-independent rotation
-    return true;  // Request repaint (shape moved)
-});
-#+END_SRC
-
-The listener receives two parameters:
-- =panel=: The ViewPanel that's rendering
-- =deltaMs=: Milliseconds since last frame (for framerate-independent animation)
-
-The return value controls whether the frame gets rendered:
-- =true=: "Something changed — repaint the screen"
-- =false=: "Nothing changed — can skip this frame"
-
-** Frame skipping optimization
-
-The engine avoids unnecessary rendering. A frame is skipped when:
-- *All listeners return false* (nothing changed in your scene)
-- *Camera did not move* (built-in Camera listener returns false once
-  the camera comes to rest)
-- *No resize or repaint requests*
-
-This means a static scene with no animations consumes almost zero CPU.
-The render thread keeps running (checking for changes), but actual pixel
-rendering is skipped entirely. Skipped frames still flush any pending
-paint passes from earlier frames, so in-flight frames always reach the
-screen.
-
-Two exceptions force a frame regardless of listeners:
-- *Unlimited (benchmark) mode* (=targetFPS <= 0=) renders continuously,
-  so the measured rate reflects maximum throughput
-- An explicit repaint request (resize, stereo toggle,
-  =repaintDuringNextViewUpdate()=, etc.)
-
-#+BEGIN_SRC java
-// Example: listener that only requests repaint when needed
-viewPanel.addFrameListener((panel, deltaMs) -> {
-    if (gameState.hasUpdates()) {
-        gameState.processUpdates();
-        return true;   // Only repaint when game state actually changed
-    }
-    return false;      // Skip frame — nothing to update
-});
-#+END_SRC
-
-** Built-in listeners
-
-The engine registers these listeners by default:
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/Camera.html][Camera]] — applies movement velocity and friction each frame, and
-  returns true when the camera actually moved (more than a small
-  threshold), i.e. while the user is actively navigating
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.html][InputManager]] — processes mouse/keyboard events
-
-When the camera stops moving and you release all keys, the Camera listener
-returns false. If your custom listeners also return false, the frame is
-skipped until something changes.
-
-* Rendering context
-:PROPERTIES:
-:CUSTOM_ID: rendering-context
-:END:
-
-The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] holds all state for rendering into one framebuffer:
-the pixel buffer, projection parameters, and per-frame bookkeeping.
-
-| Field | Purpose |
-|-------+---------|
-| =pixels[]= | Raw pixel buffer (int[] in RGB format) |
-| =bufferedImage= | Java2D wrapper around pixels |
-| =graphics= | Graphics2D for text, lines, shapes |
-| =width=, =height= | Full framebuffer dimensions |
-| =centerCoordinate= | Screen center of the active viewport (for projection) |
-| =projectionScale= | Perspective scale factor, derived from viewport width. Mutable: each stereo eye sets its own |
-| =renderMinX=, =renderMaxX= | X bounds of the active viewport or tile |
-| =renderMinY=, =renderMaxY= | Y bounds (full height on the frame context, tile bounds on segment views) |
-| =stereoEye=, =stereoViewportWidth=, =stereoViewportOffsetX= | Which eye this pass renders and where its viewport sits in the buffer |
-| =tilesX=, =tilesY=, =viewportCount=, =numRenderSegments= | Tile grid geometry (segments = tilesX × tilesY × viewports) |
-| =frustum= | View frustum for culling, rebuilt each pass from camera state |
-| =frameNumber= | Per-context frame counter |
-| =transformCycleId= | Globally unique transform-cycle id, safe key for per-cycle memoization |
-| =vertexSlot= | Projection buffer slot (0–2) this pass transforms into |
-
-** Triple-buffered frame contexts
-
-The engine keeps /three/ frame contexts, cycled by frame parity. While
-frame N is still being painted from one buffer, frame N+1 already
-transforms into the next — paint threads never idle waiting for the
-transform phase, and vice versa. A per-buffer *present gate* prevents
-painting frame F+3 into a buffer the present thread is still blitting
-frame F from.
-
-All three contexts are recreated together when the window is resized,
-when the tile grid changes (render thread count), or when stereo mode
-is toggled. Otherwise they are reused — =prepareForNewFrameRendering()=
-just resets per-frame state like mouse tracking.
-
-** Per-pass copies
-
-Each render pass (one per eye in stereo) works on a private /copy/ of
-the frame context. The copy shares the pixel buffer, graphics and
-services, but owns the projection fields (center, scale, viewport,
-vertex slot), so the next pass's setup cannot disturb a pass whose
-transform or paint is still in flight.
-
-Consequence for engine code: per-frame mutable state must be allocated
-eagerly on the frame context. Anything created lazily inside a pass
-lands on the throwaway copy and is lost.
-
-** Tile segment views
-
-Each paint tile gets a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/SegmentRenderingContext.html][SegmentRenderingContext]],
-a view that shares the framebuffer with its parent but carries its own
-X/Y tile bounds and a pre-clipped =Graphics2D= for thread-safe text and
-shape drawing. Mouse hits are tracked per tile and combined after all
-tiles finish painting.
diff --git a/doc/SDF textures/SDF concept.svg b/doc/SDF textures/SDF concept.svg
deleted file mode 100644 (file)
index bc1b750..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-<svg viewBox="0 0 640 430" width="640" height="430" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="2" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-  </defs>
-
-  <rect width="640" height="430" fill="#061018"/>
-
-  <text x="320" y="32" fill="#40b0d0" font-size="16" font-family="monospace" text-anchor="middle" filter="url(#glow)">Why a distance field, not a bitmap</text>
-  <text x="320" y="50" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">one glyph edge, magnified 8x - stored coverage vs re-derived coverage</text>
-
-  <!-- LEFT: stored bitmap coverage -->
-  <text x="160" y="86" fill="#FF8833" font-size="12" font-family="monospace" text-anchor="middle" filter="url(#glow)">BITMAP coverage</text>
-  <text x="160" y="101" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">what you store is what you get</text>
-
-  <!-- blocky stair edge -->
-  <g stroke="none">
-    <rect x="60"  y="120" width="40" height="40" fill="#39FF14" opacity="0.85"/>
-    <rect x="100" y="120" width="40" height="40" fill="#39FF14" opacity="0.5"/>
-    <rect x="60"  y="160" width="40" height="40" fill="#39FF14" opacity="0.85"/>
-    <rect x="100" y="160" width="40" height="40" fill="#39FF14" opacity="0.85"/>
-    <rect x="140" y="160" width="40" height="40" fill="#39FF14" opacity="0.4"/>
-    <rect x="60"  y="200" width="40" height="40" fill="#39FF14" opacity="0.85"/>
-    <rect x="100" y="200" width="40" height="40" fill="#39FF14" opacity="0.85"/>
-    <rect x="140" y="200" width="40" height="40" fill="#39FF14" opacity="0.85"/>
-    <rect x="180" y="200" width="40" height="40" fill="#39FF14" opacity="0.3"/>
-    <rect x="60"  y="240" width="40" height="40" fill="#39FF14" opacity="0.85"/>
-    <rect x="100" y="240" width="40" height="40" fill="#39FF14" opacity="0.85"/>
-    <rect x="140" y="240" width="40" height="40" fill="#39FF14" opacity="0.85"/>
-    <rect x="180" y="240" width="40" height="40" fill="#39FF14" opacity="0.7"/>
-  </g>
-  <g stroke="#1a3a4a" stroke-width="1" fill="none">
-    <rect x="60" y="120" width="200" height="160"/>
-    <line x1="100" y1="120" x2="100" y2="280"/>
-    <line x1="140" y1="120" x2="140" y2="280"/>
-    <line x1="180" y1="120" x2="180" y2="280"/>
-    <line x1="220" y1="120" x2="220" y2="280"/>
-    <line x1="60" y1="160" x2="260" y2="160"/>
-    <line x1="60" y1="200" x2="260" y2="200"/>
-    <line x1="60" y1="240" x2="260" y2="240"/>
-  </g>
-  <text x="160" y="305" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">texel grid IS the resolution limit:</text>
-  <text x="160" y="318" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">edges stair-step, curves become blocks</text>
-
-  <!-- RIGHT: distance field -->
-  <text x="480" y="86" fill="#39FF14" font-size="12" font-family="monospace" text-anchor="middle" filter="url(#glow)">SDF: distance to edge</text>
-  <text x="480" y="101" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">a smooth field - the edge is re-derived per pixel</text>
-
-  <!-- smooth edge line through a gradient field -->
-  <defs>
-    <linearGradient id="sdfGrad" x1="0" y1="0" x2="1" y2="1">
-      <stop offset="0" stop-color="#39FF14" stop-opacity="0.85"/>
-      <stop offset="0.42" stop-color="#39FF14" stop-opacity="0.55"/>
-      <stop offset="0.55" stop-color="#39FF14" stop-opacity="0.25"/>
-      <stop offset="0.7" stop-color="#39FF14" stop-opacity="0.06"/>
-      <stop offset="1" stop-color="#39FF14" stop-opacity="0"/>
-    </linearGradient>
-  </defs>
-  <rect x="380" y="120" width="200" height="160" fill="url(#sdfGrad)"/>
-  <rect x="380" y="120" width="200" height="160" fill="none" stroke="#1a3a4a" stroke-width="1"/>
-  <!-- the re-derived edge: crisp at any zoom -->
-  <line x1="420" y1="280" x2="530" y2="120" stroke="#40b0d0" stroke-width="2" filter="url(#glow)"/>
-  <text x="500" y="270" fill="#40b0d0" font-size="9" font-family="monospace">edge recovered at</text>
-  <text x="500" y="282" fill="#40b0d0" font-size="9" font-family="monospace">display resolution</text>
-
-  <!-- distance annotations -->
-  <text x="410" y="150" fill="#999" font-size="9" font-family="monospace">d &lt; 0: inside ink</text>
-  <text x="520" y="140" fill="#999" font-size="9" font-family="monospace">d &gt; 0: outside</text>
-  <text x="455" y="205" fill="#40b0d0" font-size="9" font-family="monospace" transform="rotate(-52 455 205)">d = 0: the edge</text>
-
-  <!-- bottom takeaway -->
-  <rect x="40" y="340" width="560" height="64" rx="6" fill="rgba(64,176,208,0.06)" stroke="#40b0d0" stroke-width="1"/>
-  <text x="320" y="364" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#glow)">coverage = (127.5 - d) * aaK + 128</text>
-  <text x="320" y="382" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">aaK scales the gradient window to the pixel footprint - the same 16x32 texel field</text>
-  <text x="320" y="395" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">serves a 4-pixel label and a full-screen billboard</text>
-</svg>
diff --git a/doc/SDF textures/SDF glyph pipeline.svg b/doc/SDF textures/SDF glyph pipeline.svg
deleted file mode 100644 (file)
index 09473d6..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-<svg viewBox="0 0 640 470" width="640" height="470" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="2" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-    <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
-      <path d="M 0 0 L 10 5 L 0 10 z" fill="#40b0d0"/>
-    </marker>
-  </defs>
-
-  <rect width="640" height="470" fill="#061018"/>
-
-  <text x="320" y="30" fill="#40b0d0" font-size="16" font-family="monospace" text-anchor="middle" filter="url(#glow)">Glyph field generation (SdfGlyphCache)</text>
-  <text x="320" y="48" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">once per character, then cached - stamping is a block copy</text>
-
-  <!-- step 1: hi-res rasterize -->
-  <rect x="40" y="70" width="170" height="84" rx="6" fill="rgba(57,255,20,0.07)" stroke="#39FF14" stroke-width="1.5"/>
-  <text x="125" y="90" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">1. rasterize glyph</text>
-  <text x="125" y="106" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">Liberation Mono Bold, AA on</text>
-  <text x="125" y="119" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">64x128 px (4x supersample)</text>
-  <text x="125" y="132" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">font auto-sized to fit cell</text>
-  <text x="125" y="148" fill="#c05088" font-size="9" font-family="monospace" text-anchor="middle">advance 0.6em (Courier-compat)</text>
-
-  <line x1="210" y1="112" x2="240" y2="112" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr)"/>
-
-  <!-- step 2: EDT -->
-  <rect x="245" y="70" width="170" height="84" rx="6" fill="rgba(64,176,208,0.07)" stroke="#40b0d0" stroke-width="1.5"/>
-  <text x="330" y="90" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle">2. distance transform</text>
-  <text x="330" y="106" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">exact Euclidean EDT</text>
-  <text x="330" y="119" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">(Felzenszwalb-Huttenlocher,</text>
-  <text x="330" y="132" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">two separable 1-D passes)</text>
-  <text x="330" y="148" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">dOut to ink, dIn to background</text>
-
-  <line x1="415" y1="112" x2="445" y2="112" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr)"/>
-
-  <!-- step 3: signed + clamp + downsample -->
-  <rect x="450" y="70" width="160" height="96" rx="6" fill="rgba(255,136,51,0.07)" stroke="#FF8833" stroke-width="1.5"/>
-  <text x="530" y="88" fill="#FF8833" font-size="11" font-family="monospace" text-anchor="middle">3. sign, clamp, average</text>
-  <text x="530" y="104" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">signed = dOut - dIn</text>
-  <text x="530" y="117" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">clamp to +/- 2 texels spread</text>
-  <text x="530" y="130" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">average FIELD down to 16x32</text>
-  <text x="530" y="148" fill="#c05088" font-size="9" font-family="monospace" text-anchor="middle">(averaging the field, not coverage,</text>
-  <text x="530" y="160" fill="#c05088" font-size="9" font-family="monospace" text-anchor="middle">preserves the edge position)</text>
-
-  <!-- down to mask encoding -->
-  <line x1="530" y1="180" x2="530" y2="196" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr)"/>
-
-  <!-- encoding bar -->
-  <rect x="120" y="200" width="420" height="54" rx="6" fill="rgba(192,80,136,0.07)" stroke="#c05088" stroke-width="1.5"/>
-  <text x="330" y="220" fill="#c05088" font-size="11" font-family="monospace" text-anchor="middle">mask encoding (per texel, 0..255)</text>
-  <text x="330" y="236" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">0 = deep inside ink    127.5 = the edge    255 = far outside</text>
-
-  <!-- gradient strip -->
-  <defs>
-    <linearGradient id="maskBar" x1="0" y1="0" x2="1" y2="0">
-      <stop offset="0" stop-color="#000000"/>
-      <stop offset="0.5" stop-color="#808080"/>
-      <stop offset="1" stop-color="#ffffff"/>
-    </linearGradient>
-  </defs>
-  <rect x="170" y="242" width="320" height="8" fill="url(#maskBar)"/>
-
-  <!-- consumers -->
-  <line x1="240" y1="254" x2="240" y2="286" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr)"/>
-  <line x1="430" y1="254" x2="430" y2="286" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr)"/>
-
-  <rect x="120" y="290" width="240" height="66" rx="6" fill="rgba(57,255,20,0.06)" stroke="#39FF14" stroke-width="1"/>
-  <text x="240" y="310" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">TextCanvas.putChar</text>
-  <text x="240" y="326" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">stamps the cached 16x32 mask</text>
-  <text x="240" y="339" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">into the cell position of sdfMask</text>
-
-  <rect x="380" y="290" width="190" height="66" rx="6" fill="rgba(32,112,192,0.07)" stroke="#2070c0" stroke-width="1"/>
-  <text x="475" y="310" fill="#2070c0" font-size="11" font-family="monospace" text-anchor="middle">three texture layers</text>
-  <text x="475" y="326" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">sdfMask: glyph SHAPES (bilinear)</text>
-  <text x="475" y="339" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">sdfForeground + primary: colors</text>
-
-  <text x="60" y="392" fill="#999" font-size="9" font-family="monospace">why sans-serif bold: Courier's serifs and hairline strokes decay into unresolvable noise</text>
-  <text x="60" y="405" fill="#999" font-size="9" font-family="monospace">when the distance field is minified - uniform sturdy strokes survive</text>
-
-  <!-- why not mipmap note -->
-  <rect x="60" y="418" width="520" height="40" rx="6" fill="rgba(255,68,68,0.05)" stroke="#FF4444" stroke-width="1"/>
-  <text x="320" y="434" fill="#FF4444" font-size="10" font-family="monospace" text-anchor="middle">NO mipmaps on the mask: the edge gradient spans ~2 texels,</text>
-  <text x="320" y="448" fill="#FF4444" font-size="10" font-family="monospace" text-anchor="middle">half-res masks melt glyph edges - minification is analytic instead</text>
-</svg>
diff --git a/doc/SDF textures/SDF minification.svg b/doc/SDF textures/SDF minification.svg
deleted file mode 100644 (file)
index 95cce51..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-<svg viewBox="0 0 640 420" width="640" height="420" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="2" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-    <marker id="arr2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
-      <path d="M 0 0 L 10 5 L 0 10 z" fill="#40b0d0"/>
-    </marker>
-  </defs>
-
-  <rect width="640" height="420" fill="#061018"/>
-
-  <text x="320" y="30" fill="#40b0d0" font-size="16" font-family="monospace" text-anchor="middle" filter="url(#glow)">Minification: analytic coverage window</text>
-  <text x="320" y="48" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">one screen pixel covering many texels still resolves the edge correctly</text>
-
-  <!-- screen pixel footprint diagram -->
-  <rect x="50" y="70" width="270" height="215" rx="6" fill="rgba(57,255,20,0.05)" stroke="#39FF14" stroke-width="1"/>
-  <text x="185" y="90" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">a screen pixel on the texture</text>
-
-  <!-- texel grid 6x5 -->
-  <g stroke="#1a3a4a" stroke-width="1">
-    <rect x="70" y="105" width="180" height="125"/>
-    <line x1="100" y1="105" x2="100" y2="230"/>
-    <line x1="130" y1="105" x2="130" y2="230"/>
-    <line x1="160" y1="105" x2="160" y2="230"/>
-    <line x1="190" y1="105" x2="190" y2="230"/>
-    <line x1="220" y1="105" x2="220" y2="230"/>
-    <line x1="70" y1="130" x2="250" y2="130"/>
-    <line x1="70" y1="155" x2="250" y2="155"/>
-    <line x1="70" y1="180" x2="250" y2="180"/>
-    <line x1="70" y1="205" x2="250" y2="205"/>
-  </g>
-  <!-- glyph edge crossing the grid -->
-  <line x1="90" y1="230" x2="230" y2="105" stroke="#c05088" stroke-width="2"/>
-  <!-- pixel footprint box -->
-  <rect x="115" y="130" width="90" height="75" fill="rgba(255,136,51,0.10)" stroke="#FF8833" stroke-width="2" stroke-dasharray="5 3"/>
-  <text x="160" y="245" fill="#FF8833" font-size="9" font-family="monospace" text-anchor="middle">footprint: many texels per pixel</text>
-  <text x="185" y="262" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">a bitmap would average to mush or alias;</text>
-  <text x="185" y="275" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">the field still knows where the edge is</text>
-
-  <!-- formula flow -->
-  <rect x="350" y="70" width="250" height="60" rx="6" fill="rgba(64,176,208,0.07)" stroke="#40b0d0" stroke-width="1.5"/>
-  <text x="475" y="92" fill="#40b0d0" font-size="10" font-family="monospace" text-anchor="middle">per-axis footprint from UV gradients</text>
-  <text x="475" y="106" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">footX = |dUV/dx|, footY = |dUV/dy|</text>
-  <text x="475" y="119" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">window follows the SHARPEST axis</text>
-
-  <line x1="475" y1="130" x2="475" y2="150" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr2)"/>
-
-  <rect x="350" y="155" width="250" height="44" rx="6" fill="rgba(192,80,136,0.07)" stroke="#c05088" stroke-width="1.5"/>
-  <text x="475" y="173" fill="#c05088" font-size="10" font-family="monospace" text-anchor="middle">aaK = 2*spread / texelsPerPixel</text>
-  <text x="475" y="188" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">widens the coverage window as pixels grow</text>
-
-  <line x1="475" y1="199" x2="475" y2="219" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arr2)"/>
-
-  <rect x="350" y="224" width="250" height="66" rx="6" fill="rgba(255,136,51,0.07)" stroke="#FF8833" stroke-width="1.5"/>
-  <text x="475" y="242" fill="#FF8833" font-size="10" font-family="monospace" text-anchor="middle">perceptual corrections (minified text</text>
-  <text x="475" y="254" fill="#FF8833" font-size="10" font-family="monospace" text-anchor="middle">else reads as gray haze):</text>
-  <text x="475" y="270" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">SHARPEN x2: sub-pixel window kills halo</text>
-  <text x="475" y="283" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">coverage gamma &lt; 1: stem darkening</text>
-
-  <!-- result strip -->
-  <rect x="60" y="310" width="520" height="90" rx="6" fill="rgba(57,255,20,0.05)" stroke="#39FF14" stroke-width="1"/>
-  <text x="320" y="332" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#glow)">result: graceful degradation</text>
-  <text x="320" y="350" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">magnified: edges re-derived at display resolution - razor sharp</text>
-  <text x="320" y="365" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">minified: coverage fades smoothly to clean gray, no crawling aliases</text>
-  <text x="320" y="380" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">angled: the uncompressed axis keeps its sharpness</text>
-</svg>
diff --git a/doc/SDF textures/glyph-sdf-S.png b/doc/SDF textures/glyph-sdf-S.png
deleted file mode 100644 (file)
index ecc1de5..0000000
Binary files a/doc/SDF textures/glyph-sdf-S.png and /dev/null differ
diff --git a/doc/SDF textures/index.org b/doc/SDF textures/index.org
deleted file mode 100644 (file)
index ff4cf78..0000000
+++ /dev/null
@@ -1,252 +0,0 @@
-#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
-#+TITLE: SDF Textures - Aukio 3D
-#+LANGUAGE: en
-#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
-#+LATEX_HEADER: \usepackage{parskip}
-
-[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
-
-* What SDF textures are
-:PROPERTIES:
-:CUSTOM_ID: what-sdf-is
-:END:
-
-A regular texture stores *coverage*: each texel says "this much ink
-here". That is a photocopy of the glyph — resample it (magnify, minify,
-view at an angle) and the stored pixels blur or alias, because the
-information about /where the edge is/ was thrown away when the glyph
-was rasterized.
-
-A *signed distance field* (SDF) texture stores something smarter: per
-texel, the *distance to the nearest edge* — negative inside the ink,
-positive outside, zero exactly on the boundary. The rasterizer then
-re-derives coverage per screen pixel from this smooth field. The edge
-position survives resampling because the field around it is linear —
-bilinear interpolation of a linear ramp is exact.
-
-#+ATTR_HTML: :width 640
-[[file:SDF concept.svg]]
-
-In Aukio 3D the mask is a grayscale field in =texture.sdfMask=:
-
-- =0= — deep inside the ink
-- =127.5= — exactly on the edge
-- =255= — far outside any glyph
-
-The gradient spans only =SPREAD_TEXELS = 2.0= texels around the edge —
-that narrow band is all the rasterizer needs.
-
-Here is a real field, dumped straight from =SdfGlyphCache= (glyph "S",
-16x32 texels, upscaled 12x with nearest so you can see the texels):
-
-[[file:glyph-sdf-S.png]]
-
-Dark inside the strokes, bright outside, and a smooth gray ramp exactly
-two texels wide around the contour.
-
-* Generating glyph fields
-:PROPERTIES:
-:CUSTOM_ID: glyph-pipeline
-:END:
-
-=SdfGlyphCache= generates each character's distance field once and
-caches it in a =ConcurrentHashMap=; stamping a glyph into a canvas is
-then just a block copy.
-
-#+ATTR_HTML: :width 640
-[[file:SDF glyph pipeline.svg]]
-
-The steps:
-
-1. *Rasterize* the glyph with AWT at 4x the cell size (64x128 pixels)
-   with anti-aliasing on, using Liberation Mono Bold (metric-compatible
-   with Courier New, so the cell grid is unchanged). The font size is
-   auto-shrunk until the widest glyph fits the scratch without clipping
-   — a clipped glyph would corrupt the distance field at the cell edge.
-2. *Distance transform*: an exact Euclidean distance transform
-   (Felzenszwalb & Huttenlocher, two separable 1-D passes over parabola
-   envelopes) is run twice — once for distance to nearest ink pixel,
-   once for distance to nearest background pixel.
-3. *Sign, clamp, average*: signed distance = dOut - dIn, clamped to
-   +/-2 texels of spread, then the *field* (not coverage) is averaged
-   down to the 16x32 cell resolution. Averaging the field preserves the
-   edge position; averaging coverage would not.
-
-The font choice matters: Courier's serifs and hairline strokes decay
-into unresolvable noise when the field is minified. A uniform-stroke
-bold sans-serif survives.
-
-* The rendering path
-:PROPERTIES:
-:CUSTOM_ID: render-path
-:END:
-
-When =texture.isSdf()= is true (an =sdfMask= is attached),
-=TexturedTriangle.paintSdf= takes over. Three layers are involved:
-
-| Layer            | Contents                    | Sampling |
-|------------------+-----------------------------+----------|
-| =sdfMask=        | glyph shapes (the field)    | bilinear |
-| =sdfForeground=  | ink color, flat per cell    | nearest  |
-| =primaryBitmap=  | background color, per cell  | nearest  |
-
-Per screen pixel:
-
-1. Sample the mask bilinearly (fixed-point) -> distance =d=.
-2. Convert to coverage: =cov = (127.5 - d) * aaK + 128=, clamped to
-   [0, 256]. =aaK= scales the 2-texel gradient window to the current
-   pixel footprint (see next section).
-3. Blend: =pixel = bg * (1 - cov) + fg * cov=.
-
-Perspective-correct interpolation applies to SDF triangles exactly as
-it does to regular textured triangles — same affine-sufficiency test,
-same subdivided correction. See
-[[file:../Perspective correct textures/index.org][Perspective-correct
-textures]]; only the per-pixel sampling differs.
-
-* Minification without mipmaps
-:PROPERTIES:
-:CUSTOM_ID: minification
-:END:
-
-*There is deliberately no mipmap chain for SDF layers.* A distance
-field's edge gradient spans ~2 texels; a half-resolution mask melts the
-glyph edges. Worse, the two triangles of a rectangle cross mip
-thresholds at slightly different distances, producing a hard diagonal
-quality split and sudden blur steps while dollying (observed in
-practice).
-
-Minification is instead handled *analytically*: the coverage window is
-widened by the screen-space pixel footprint, giving area-correct
-coverage straight from the primary field.
-
-#+ATTR_HTML: :width 640
-[[file:SDF minification.svg]]
-
-The footprint is computed per axis from the screen-space UV gradients —
-=text on an angled plane is minified mostly along one axis=, and an
-isotropic average would blur the axis that still has resolution to
-spare. The coverage window follows the sharpest axis.
-
-Area-correct coverage alone reads as a low-contrast gray haze, so two
-perceptual corrections (A/B-tuned on far + angled text) kick in under
-minification:
-
-- *Sharpening* (=SDF_SHARPEN=, default 2): narrows the coverage window
-  below one pixel — kills the haze halo at the cost of slight shimmer.
-- *Coverage gamma* (< 1, automatic from the footprint): darkens stems
-  like a small-size font rasterizer, keeping thin strokes present.
-
-Real output, rendered headlessly through the [[file:../index.org::#snapshot][Snapshot tool]]:
-
-Magnified — edges re-derived at display resolution, razor sharp:
-
-[[file:sdf-near.png]]
-
-At moderate distance:
-
-[[file:sdf-mid.png]]
-
-Far away — small but clean, fading to gray instead of disintegrating
-into aliases (right: 4x nearest zoom of the center):
-
-[[file:sdf-far.png]]
-
-[[file:sdf-far-zoom.png]]
-
-At an oblique angle — foreshortened along one axis, still sharp along
-the other:
-
-[[file:sdf-angled.png]]
-
-* Using it
-:PROPERTIES:
-:CUSTOM_ID: using-sdf
-:END:
-
-*TextCanvas* is the main entry point: a textured rectangle carrying a
-character grid in 3D space. World cell size 8x16 units, texture cell
-16x32 texels (2 texels per world unit).
-
-#+BEGIN_SRC java
-Transform location = new Transform(new Point3D(0, 0, 500));
-TextCanvas canvas = new TextCanvas(location, "Hello, World!",
-        Color.WHITE, Color.BLACK);
-shapeCollection.addShape(canvas);
-
-// blank canvas + cursor writing
-TextCanvas blank = new TextCanvas(location, new TextPointer(10, 40),
-        Color.GREEN, Color.BLACK);
-blank.locate(0, 0);
-blank.print("Line 1");
-blank.locate(1, 0);
-blank.print("Line 2");
-blank.setForegroundColor(Color.RED);   // affects subsequent writes
-blank.setTextColor(Color.CYAN);        // recolors existing ink only
-#+END_SRC
-
-Colors are per-cell: each =putChar= fills the cell's rectangle in the
-background and foreground layers, so one canvas can hold many colors.
-
-*ForwardOrientedTextBlock* renders the same pipeline onto a billboard
-that always faces the camera — for labels that must stay readable from
-any angle:
-
-#+BEGIN_SRC java
-ForwardOrientedTextBlock label = new ForwardOrientedTextBlock(
-        new Point3D(0, -50, 300), 1.0, 2, "Hello, World!", Color.RED);
-shapeCollection.addShape(label);
-#+END_SRC
-
-Real use in the demos: the life demo's help panel (=life_demo/Main.java=
-=createHelpPanel()=) and the axis labels in =CoordinateSystemDemo=.
-
-* Tuning knobs
-:PROPERTIES:
-:CUSTOM_ID: tuning
-:END:
-
-JVM properties (A/B tuning knobs in =TexturedTriangle=):
-
-| Property          | Default | Effect                                    |
-|-------------------+---------+-------------------------------------------|
-| =e3d.sdf.gamma=   | 0 (auto) | fixed coverage gamma; auto derives from footprint |
-| =e3d.sdf.sharpen= | 2       | coverage window narrowing; 1 = pixel-exact |
-| =e3d.sdf.debug=   | false   | prints per-triangle footprints and path decisions to stderr |
-
-* Limitations
-:PROPERTIES:
-:CUSTOM_ID: limitations
-:END:
-
-- *Fixed cell grid*: TextCanvas is monospace by construction (16x32
-  texel cells). Proportional fonts would need a different stamping
-  scheme.
-- *ASCII-oriented cache*: =SdfGlyphCache= measures printable ASCII
-  (33..126) when sizing the font; exotic glyphs may fit worse.
-- *Under extreme minification* text fades to gray by design — that is
-  the correct physical answer (a sub-pixel glyph has no shape left),
-  but it means distant labels are decorative, not readable.
-- *Bandwidth under minification*: sampling the primary field (no mip
-  chain) costs more bandwidth per pixel. Text surfaces are small, so
-  this is the right trade — do not attach SDF masks to huge surfaces.
-
-* Related classes
-:PROPERTIES:
-:CUSTOM_ID: related-classes
-:END:
-
-| Class                       | Role                                             |
-|-----------------------------+--------------------------------------------------|
-| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.html][TextCanvas]]                  | Character grid surface in 3D; owns the layers    |
-| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/SdfGlyphCache.html][SdfGlyphCache]]               | Per-glyph field generation + cache (EDT inside)  |
-| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.html][ForwardOrientedTextBlock]]    | Camera-facing text billboard                     |
-| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]]            | =paintSdf= — the scanline path                   |
-| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html][Texture]]                     | =sdfMask=, =sdfForeground=, =sdfSpreadTexels=    |
-
-*See also:*
-
-- [[file:../Perspective correct textures/][Perspective-correct textures]] — the scanline texture-mapping path
-  that SDF rendering builds on; both paths live in =TexturedTriangle=
-  and share the same interpolated UVs.
-
diff --git a/doc/SDF textures/sdf-angled.png b/doc/SDF textures/sdf-angled.png
deleted file mode 100644 (file)
index 34814bf..0000000
Binary files a/doc/SDF textures/sdf-angled.png and /dev/null differ
diff --git a/doc/SDF textures/sdf-far-zoom.png b/doc/SDF textures/sdf-far-zoom.png
deleted file mode 100644 (file)
index f0c0a44..0000000
Binary files a/doc/SDF textures/sdf-far-zoom.png and /dev/null differ
diff --git a/doc/SDF textures/sdf-far.png b/doc/SDF textures/sdf-far.png
deleted file mode 100644 (file)
index 64898a2..0000000
Binary files a/doc/SDF textures/sdf-far.png and /dev/null differ
diff --git a/doc/SDF textures/sdf-mid.png b/doc/SDF textures/sdf-mid.png
deleted file mode 100644 (file)
index ba8ed3e..0000000
Binary files a/doc/SDF textures/sdf-mid.png and /dev/null differ
diff --git a/doc/SDF textures/sdf-near.png b/doc/SDF textures/sdf-near.png
deleted file mode 100644 (file)
index 1d493a1..0000000
Binary files a/doc/SDF textures/sdf-near.png and /dev/null differ
diff --git a/doc/Shading/Ambient light comparison.svg b/doc/Shading/Ambient light comparison.svg
deleted file mode 100644 (file)
index ce07a00..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-<svg viewBox="0 0 640 290" width="640" height="290" xmlns="http://www.w3.org/2000/svg">
-<defs>
-  <filter id="glows"><feGaussianBlur stdDeviation="1.5" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-<mask id="imagine-text-gaps-2vose8" maskUnits="userSpaceOnUse"><rect x="0" y="0" width="640" height="290" fill="white"/><rect x="261.183349609375" y="6.583333969116211" width="117.63333129882812" height="20.91666603088379" fill="black" rx="2"/><rect x="110.16667175292969" y="26.25" width="419.6666564941406" height="15.083333015441895" fill="black" rx="2"/><rect x="53.083335876464844" y="223.25" width="83.83333587646484" height="15.083333015441895" fill="black" rx="2"/><rect x="54.900001525878906" y="237.6666717529297" width="80.19999694824219" height="16.25" fill="black" rx="2"/><rect x="35.608333587646484" y="252.4166717529297" width="118.78333282470703" height="13.916666984558105" fill="black" rx="2"/><rect x="257.9583435058594" y="223.25" width="100.08333587646484" height="15.083333015441895" fill="black" rx="2"/><rect x="273.9166564941406" y="237.6666717529297" width="68.16666793823242" height="16.25" fill="black" rx="2"/><rect x="267.875" y="252.4166717529297" width="80.25" height="13.916666984558105" fill="black" rx="2"/><rect x="463.8333435058594" y="223.25" width="116.33333587646484" height="15.083333015441895" fill="black" rx="2"/><rect x="487.9166564941406" y="237.6666717529297" width="68.16666793823242" height="16.25" fill="black" rx="2"/><rect x="477.058349609375" y="252.4166717529297" width="89.88333129882812" height="13.916666984558105" fill="black" rx="2"/><rect x="161.86666870117188" y="267.4166564941406" width="316.26666259765625" height="13.916666984558105" fill="black" rx="2"/></mask></defs>
-<rect width="640" height="280" fill="#061018" style="fill:rgb(6, 16, 24);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-
-<text x="320" y="22" fill="#ccc" font-size="14" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)" style="fill:rgb(204, 204, 204);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:14px;font-weight:700;text-anchor:middle;dominant-baseline:auto">Ambient Light</text>
-<text x="320" y="37" fill="#3a4a5a" font-size="9" font-family="monospace" text-anchor="middle" style="fill:rgb(58, 74, 90);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">base illumination applied to all surfaces equally, regardless of orientation</text>
-
-<line x1="213" y1="42" x2="213" y2="268" stroke="#0c1a26" stroke-width="1.5" mask="url(#imagine-text-gaps-2vose8)" style="fill:rgb(0, 0, 0);stroke:rgb(12, 26, 38);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="427" y1="42" x2="427" y2="268" stroke="#0c1a26" stroke-width="1.5" mask="url(#imagine-text-gaps-2vose8)" style="fill:rgb(0, 0, 0);stroke:rgb(12, 26, 38);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="5" y1="220" x2="208" y2="220" stroke="#0c1a26" stroke-width="1" style="fill:rgb(0, 0, 0);stroke:rgb(12, 26, 38);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="218" y1="220" x2="422" y2="220" stroke="#0c1a26" stroke-width="1" style="fill:rgb(0, 0, 0);stroke:rgb(12, 26, 38);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="432" y1="220" x2="635" y2="220" stroke="#0c1a26" stroke-width="1" style="fill:rgb(0, 0, 0);stroke:rgb(12, 26, 38);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-
-<!-- PANEL 1: No ambient -->
-<circle cx="30" cy="57" r="7" fill="rgba(255,102,0,0.45)" stroke="#FF6600" stroke-width="1.5" style="fill:rgba(255, 102, 0, 0.45);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="30" y1="48" x2="30" y2="42" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="37" y1="51" x2="42" y2="46" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="23" y1="51" x2="18" y2="46" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<polygon points="22,82 102,70 102,200 22,212" fill="rgba(48,160,80,0.68)" stroke="#30a050" stroke-width="1.5" style="fill:rgba(48, 160, 80, 0.68);stroke:rgb(48, 160, 80);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<polygon points="102,70 168,85 168,215 102,200" fill="rgba(5,10,7,0.97)" stroke="#1c2820" stroke-width="1.5" style="fill:rgba(5, 10, 7, 0.97);stroke:rgb(28, 40, 32);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="95" y="234" fill="rgba(208,64,64,0.75)" font-size="9" font-family="monospace" text-anchor="middle" style="fill:rgba(208, 64, 64, 0.75);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Color(0, 0, 0)</text>
-<text x="95" y="249" fill="rgba(208,64,64,0.9)" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)" style="fill:rgba(208, 64, 64, 0.9);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:700;text-anchor:middle;dominant-baseline:auto">✗ pure black</text>
-<text x="95" y="262" fill="#3a4a5a" font-size="8" font-family="monospace" text-anchor="middle" style="fill:rgb(58, 74, 90);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:middle;dominant-baseline:auto">harsh shadows, no depth</text>
-
-<!-- PANEL 2: Default ambient (50,50,50) -->
-<circle cx="243" cy="57" r="7" fill="rgba(255,102,0,0.45)" stroke="#FF6600" stroke-width="1.5" style="fill:rgba(255, 102, 0, 0.45);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="243" y1="48" x2="243" y2="42" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="250" y1="51" x2="255" y2="46" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="236" y1="51" x2="231" y2="46" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<polygon points="235,82 315,70 315,200 235,212" fill="rgba(48,160,80,0.68)" stroke="#30a050" stroke-width="1.5" style="fill:rgba(48, 160, 80, 0.68);stroke:rgb(48, 160, 80);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<polygon points="315,70 381,85 381,215 315,200" fill="rgba(40,75,46,0.75)" stroke="#285c30" stroke-width="1" style="fill:rgba(40, 75, 46, 0.75);stroke:rgb(40, 92, 48);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="308" y="234" fill="#39FF14" font-size="9" font-family="monospace" text-anchor="middle" style="fill:rgb(57, 255, 20);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Color(50, 50, 50)</text>
-<text x="308" y="249" fill="#39FF14" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)" style="fill:rgb(57, 255, 20);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:700;text-anchor:middle;dominant-baseline:auto">✓ balanced</text>
-<text x="308" y="262" fill="#3a4a5a" font-size="8" font-family="monospace" text-anchor="middle" style="fill:rgb(58, 74, 90);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:middle;dominant-baseline:auto">depth preserved</text>
-
-<!-- PANEL 3: Too much ambient (150,150,150) -->
-<circle cx="457" cy="57" r="7" fill="rgba(255,102,0,0.45)" stroke="#FF6600" stroke-width="1.5" style="fill:rgba(255, 102, 0, 0.45);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="457" y1="48" x2="457" y2="42" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="464" y1="51" x2="469" y2="46" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<line x1="450" y1="51" x2="445" y2="46" stroke="#FF6600" stroke-width="1.1" opacity="0.6" style="fill:rgb(0, 0, 0);stroke:rgb(255, 102, 0);color:rgb(251, 251, 254);stroke-width:1.1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:0.6;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<polygon points="449,82 529,70 529,200 449,212" fill="rgba(48,160,80,0.68)" stroke="#30a050" stroke-width="1.5" style="fill:rgba(48, 160, 80, 0.68);stroke:rgb(48, 160, 80);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<polygon points="529,70 595,85 595,215 529,200" fill="rgba(85,145,92,0.72)" stroke="#30a050" stroke-width="1.5" style="fill:rgba(85, 145, 92, 0.72);stroke:rgb(48, 160, 80);color:rgb(251, 251, 254);stroke-width:1.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="522" y="234" fill="#FF6600" font-size="9" font-family="monospace" text-anchor="middle" style="fill:rgb(255, 102, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:9px;font-weight:400;text-anchor:middle;dominant-baseline:auto">Color(150, 150, 150)</text>
-<text x="522" y="249" fill="#FF6600" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)" style="fill:rgb(255, 102, 0);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:10px;font-weight:700;text-anchor:middle;dominant-baseline:auto">✗ too flat</text>
-<text x="522" y="262" fill="#3a4a5a" font-size="8" font-family="monospace" text-anchor="middle" style="fill:rgb(58, 74, 90);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:middle;dominant-baseline:auto">no depth contrast</text>
-
-<line x1="20" y1="268" x2="620" y2="268" stroke="#0c1a26" stroke-width="1" mask="url(#imagine-text-gaps-2vose8)" style="fill:rgb(0, 0, 0);stroke:rgb(12, 26, 38);color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:&quot;Anthropic Sans&quot;, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
-<text x="320" y="277" fill="#2a3a4a" font-size="8" font-family="monospace" text-anchor="middle" style="fill:rgb(42, 58, 74);stroke:none;color:rgb(251, 251, 254);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:monospace;font-size:8px;font-weight:400;text-anchor:middle;dominant-baseline:auto">lightingManager.setAmbientLight(new Color(50, 50, 50))  ←  default</text>
-</svg>
\ No newline at end of file
diff --git a/doc/Shading/Distance attenuation.svg b/doc/Shading/Distance attenuation.svg
deleted file mode 100644 (file)
index 2edf492..0000000
+++ /dev/null
@@ -1,91 +0,0 @@
-<svg viewBox="0 0 640 295" width="640" height="295" xmlns="http://www.w3.org/2000/svg">
-<defs>
-  <filter id="glows"><feGaussianBlur stdDeviation="1.5" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-  <marker id="ax" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="5" markerHeight="5" orient="auto">
-    <path d="M 0 0 L 8 4 L 0 8 z" fill="#3a5060"/>
-  </marker>
-</defs>
-<rect width="640" height="295" fill="#061018"/>
-
-<text x="320" y="22" fill="#ccc" font-size="14" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)">Distance Attenuation</text>
-<text x="320" y="37" fill="#3a4a5a" font-size="9" font-family="monospace" text-anchor="middle">light intensity falls off with distance from source</text>
-
-<line x1="400" y1="45" x2="400" y2="282" stroke="#0c1a26" stroke-width="1.5"/>
-
-<!-- Light source -->
-<circle cx="52" cy="110" r="22" fill="rgba(255,102,0,0.06)"/>
-<circle cx="52" cy="110" r="14" fill="rgba(255,102,0,0.2)" stroke="rgba(255,102,0,0.45)" stroke-width="1.5"/>
-<circle cx="52" cy="110" r="6" fill="rgba(255,102,0,0.7)" stroke="#FF6600" stroke-width="1.5"/>
-<line x1="52" y1="92" x2="52" y2="84" stroke="#FF6600" stroke-width="1.1" opacity="0.55"/>
-<line x1="65" y1="97" x2="72" y2="90" stroke="#FF6600" stroke-width="1.1" opacity="0.55"/>
-<line x1="39" y1="97" x2="32" y2="90" stroke="#FF6600" stroke-width="1.1" opacity="0.55"/>
-<line x1="70" y1="110" x2="78" y2="110" stroke="#FF6600" stroke-width="1.1" opacity="0.55"/>
-<line x1="65" y1="123" x2="72" y2="130" stroke="#FF6600" stroke-width="1.1" opacity="0.55"/>
-<text x="52" y="82" fill="#FF6600" font-size="9" font-weight="700" font-family="monospace" text-anchor="middle">Light</text>
-
-<line x1="52" y1="155" x2="380" y2="155" stroke="#1a2a3a" stroke-width="1" stroke-dasharray="4 3"/>
-
-<!-- Surface d=100, att=0.99 -->
-<line x1="65" y1="110" x2="126" y2="110" stroke="#FF6600" stroke-width="1" stroke-dasharray="4 2" opacity="0.40"/>
-<polygon points="128,77 163,77 158,148 133,148" fill="rgba(48,160,80,0.70)" stroke="#30a050" stroke-width="1.5"/>
-<text x="145" y="65" fill="#30a050" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)">0.99</text>
-<line x1="145" y1="152" x2="145" y2="160" stroke="#2a3a4a" stroke-width="1"/>
-<text x="145" y="172" fill="#555" font-size="8" font-family="monospace" text-anchor="middle">d = 100</text>
-
-<!-- Surface d=300, att=0.52 -->
-<line x1="65" y1="110" x2="223" y2="110" stroke="#FF6600" stroke-width="1" stroke-dasharray="4 2" opacity="0.22"/>
-<polygon points="225,77 259,77 255,148 229,148" fill="rgba(48,160,80,0.36)" stroke="rgba(48,160,80,0.65)" stroke-width="1.2"/>
-<text x="242" y="65" fill="rgba(48,160,80,0.8)" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">0.52</text>
-<line x1="242" y1="152" x2="242" y2="160" stroke="#2a3a4a" stroke-width="1"/>
-<text x="242" y="172" fill="#555" font-size="8" font-family="monospace" text-anchor="middle">d = 300</text>
-
-<!-- Surface d=500, att=0.29 -->
-<line x1="65" y1="110" x2="316" y2="110" stroke="#FF6600" stroke-width="1" stroke-dasharray="4 2" opacity="0.10"/>
-<polygon points="318,77 352,77 349,148 321,148" fill="rgba(48,160,80,0.18)" stroke="rgba(48,160,80,0.38)" stroke-width="1"/>
-<text x="335" y="65" fill="rgba(48,160,80,0.55)" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">0.29</text>
-<line x1="335" y1="152" x2="335" y2="160" stroke="#2a3a4a" stroke-width="1"/>
-<text x="335" y="172" fill="#555" font-size="8" font-family="monospace" text-anchor="middle">d = 500</text>
-
-<text x="195" y="192" fill="#2a3a4a" font-size="8" font-family="monospace" text-anchor="middle">← attenuation factor shown above each surface →</text>
-
-<!-- Chart -->
-<text x="513" y="57" fill="#aaa" font-size="9" font-weight="700" font-family="monospace" text-anchor="middle">attenuation vs distance</text>
-<line x1="415" y1="210" x2="630" y2="210" stroke="#2a3a4a" stroke-width="1" marker-end="url(#ax)"/>
-<line x1="415" y1="210" x2="415" y2="67" stroke="#2a3a4a" stroke-width="1" marker-end="url(#ax)"/>
-<text x="622" y="222" fill="#3a5060" font-size="8" font-family="monospace">d</text>
-<text x="408" y="65" fill="#3a5060" font-size="8" font-family="monospace" text-anchor="end">att</text>
-<line x1="411" y1="210" x2="419" y2="210" stroke="#2a3a4a" stroke-width="1"/>
-<text x="408" y="213" fill="#3a5060" font-size="7" font-family="monospace" text-anchor="end">0</text>
-<line x1="411" y1="138" x2="419" y2="138" stroke="#2a3a4a" stroke-width="1"/>
-<line x1="415" y1="138" x2="625" y2="138" stroke="#0d1e2e" stroke-width="1"/>
-<text x="408" y="141" fill="#3a5060" font-size="7" font-family="monospace" text-anchor="end">0.5</text>
-<line x1="411" y1="67" x2="419" y2="67" stroke="#2a3a4a" stroke-width="1"/>
-<line x1="415" y1="67" x2="625" y2="67" stroke="#0d1e2e" stroke-width="1"/>
-<text x="408" y="70" fill="#3a5060" font-size="7" font-family="monospace" text-anchor="end">1.0</text>
-<line x1="450" y1="206" x2="450" y2="214" stroke="#2a3a4a" stroke-width="1"/>
-<line x1="450" y1="67" x2="450" y2="210" stroke="#0d1e2e" stroke-width="1" stroke-dasharray="2 4"/>
-<text x="450" y="222" fill="#3a5060" font-size="7" font-family="monospace" text-anchor="middle">100</text>
-<line x1="520" y1="206" x2="520" y2="214" stroke="#2a3a4a" stroke-width="1"/>
-<line x1="520" y1="67" x2="520" y2="210" stroke="#0d1e2e" stroke-width="1" stroke-dasharray="2 4"/>
-<text x="520" y="222" fill="#3a5060" font-size="7" font-family="monospace" text-anchor="middle">300</text>
-<line x1="590" y1="206" x2="590" y2="214" stroke="#2a3a4a" stroke-width="1"/>
-<line x1="590" y1="67" x2="590" y2="210" stroke="#0d1e2e" stroke-width="1" stroke-dasharray="2 4"/>
-<text x="590" y="222" fill="#3a5060" font-size="7" font-family="monospace" text-anchor="middle">500</text>
-<path d="M 415,67 C 430,67 440,68 450,68 C 468,68 492,100 520,136 C 548,170 575,175 625,178"
-      fill="none" stroke="#FF6600" stroke-width="2" opacity="0.85"/>
-<circle cx="415" cy="67" r="3" fill="#FF6600" opacity="0.70"/>
-<circle cx="450" cy="68" r="3" fill="#FF6600" opacity="0.85"/>
-<circle cx="520" cy="136" r="3" fill="#FF6600" opacity="0.85"/>
-<circle cx="590" cy="169" r="3" fill="#FF6600" opacity="0.85"/>
-<text x="453" y="62" fill="rgba(255,102,0,0.75)" font-size="7" font-family="monospace">0.99</text>
-<text x="523" y="131" fill="rgba(255,102,0,0.75)" font-size="7" font-family="monospace">0.52</text>
-<text x="593" y="164" fill="rgba(255,102,0,0.75)" font-size="7" font-family="monospace">0.29</text>
-
-<!-- Formula box -->
-<rect x="405" y="230" width="225" height="46" rx="4" fill="rgba(32,112,192,0.08)" stroke="#2070c0" stroke-width="1.5"/>
-<text x="517" y="249" fill="#2070c0" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">attenuation =</text>
-<text x="517" y="267" fill="#2070c0" font-size="11" font-family="monospace" text-anchor="middle">1 / (1 + 0.0001 · d²)</text>
-
-<line x1="20" y1="282" x2="620" y2="282" stroke="#0c1a26" stroke-width="1"/>
-<text x="320" y="291" fill="#2a3a4a" font-size="8" font-family="monospace" text-anchor="middle">coefficient 0.0001 was tuned for typical scene scales in Aukio 3D</text>
-</svg>
diff --git a/doc/Shading/Lambert cosine law.svg b/doc/Shading/Lambert cosine law.svg
deleted file mode 100644 (file)
index 1f4e216..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
-<defs>
-  <filter id="glow"><feGaussianBlur stdDeviation="2" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-  <filter id="glows"><feGaussianBlur stdDeviation="1.5" result="blur"/><feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
-  <marker id="an" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#30a050"/></marker>
-  <marker id="al" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" fill="#FF6600"/></marker>
-</defs>
-<rect width="640" height="480" fill="#061018"/>
-<text x="320" y="30" fill="#ccc" font-size="17" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)">Lambert Cosine Law</text>
-<text x="320" y="48" fill="#3a4a5a" font-size="10" font-family="monospace" text-anchor="middle">how surface orientation determines light intensity</text>
-<line x1="385" y1="58" x2="385" y2="305" stroke="#0c1a26" stroke-width="1.5"/>
-<line x1="20" y1="308" x2="620" y2="308" stroke="#0c1a26" stroke-width="1.5"/>
-<polygon points="60,275 280,255 295,165 75,185" fill="none" stroke="rgba(48,160,80,0.15)" stroke-width="8"/>
-<polygon points="60,275 280,255 295,165 75,185" fill="rgba(48,160,80,0.13)" stroke="#30a050" stroke-width="2"/>
-<circle cx="178" cy="220" r="4" fill="#30a050"/>
-<line x1="178" y1="220" x2="148" y2="75" stroke="#30a050" stroke-width="2.5" marker-end="url(#an)"/>
-<text x="128" y="70" fill="#30a050" font-size="18" font-weight="700" font-family="monospace" filter="url(#glow)">N̂</text>
-<text x="130" y="84" fill="#aaa" font-size="9" font-family="monospace">normal</text>
-<circle cx="345" cy="85" r="28" fill="rgba(255,102,0,0.06)"/>
-<circle cx="345" cy="85" r="17" fill="rgba(255,102,0,0.2)" stroke="rgba(255,102,0,0.45)" stroke-width="1.5"/>
-<circle cx="345" cy="85" r="7" fill="rgba(255,102,0,0.7)" stroke="#FF6600" stroke-width="1.5"/>
-<line x1="345" y1="62" x2="345" y2="52" stroke="#FF6600" stroke-width="1.2" opacity="0.55"/>
-<line x1="362" y1="67" x2="370" y2="60" stroke="#FF6600" stroke-width="1.2" opacity="0.55"/>
-<line x1="328" y1="67" x2="320" y2="60" stroke="#FF6600" stroke-width="1.2" opacity="0.55"/>
-<line x1="368" y1="85" x2="377" y2="85" stroke="#FF6600" stroke-width="1.2" opacity="0.55"/>
-<line x1="322" y1="85" x2="313" y2="85" stroke="#FF6600" stroke-width="1.2" opacity="0.55"/>
-<text x="370" y="89" fill="#FF6600" font-size="11" font-weight="700" font-family="monospace" text-anchor="start">Light</text>
-<line x1="333" y1="97" x2="185" y2="215" stroke="#FF6600" stroke-width="2" stroke-dasharray="6 3" marker-end="url(#al)"/>
-<text x="274" y="147" fill="#FF6600" font-size="15" font-weight="700" font-family="monospace" filter="url(#glow)">L̂</text>
-<path d="M 167,166 A 55,55 0 0,1 221,185" fill="none" stroke="#b09020" stroke-width="2"/>
-<text x="213" y="160" fill="#b09020" font-size="14" font-weight="700" font-family="monospace" filter="url(#glows)">θ</text>
-<text x="178" y="244" fill="rgba(48,160,80,0.6)" font-size="10" font-family="monospace" text-anchor="middle">surface polygon</text>
-<rect x="398" y="68" width="218" height="105" rx="4" fill="rgba(32,112,192,0.08)" stroke="#2070c0" stroke-width="1.5"/>
-<text x="507" y="93" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">brightness =</text>
-<text x="507" y="118" fill="#2070c0" font-size="16" font-weight="700" font-family="monospace" text-anchor="middle" filter="url(#glows)">dot( N̂ , L̂ )</text>
-<line x1="408" y1="127" x2="606" y2="127" stroke="#2070c0" stroke-width="1" opacity="0.3"/>
-<text x="507" y="152" fill="#2070c0" font-size="13" font-family="monospace" text-anchor="middle">= cos( θ )</text>
-<rect x="398" y="183" width="218" height="115" rx="4" fill="rgba(80,96,192,0.05)" stroke="rgba(80,96,192,0.4)" stroke-width="1"/>
-<text x="412" y="204" fill="#666" font-size="10" font-family="monospace">θ = 0°</text>
-<rect x="458" y="193" width="80" height="13" rx="2" fill="rgba(57,255,20,0.15)" stroke="#39FF14" stroke-width="1"/>
-<rect x="458" y="193" width="80" height="13" rx="2" fill="rgba(57,255,20,0.7)"/>
-<text x="548" y="204" fill="#39FF14" font-size="10" font-weight="700" font-family="monospace">1.00</text>
-<text x="412" y="225" fill="#666" font-size="10" font-family="monospace">θ = 45°</text>
-<rect x="458" y="214" width="80" height="13" rx="2" fill="rgba(48,160,80,0.08)" stroke="#30a050" stroke-width="1"/>
-<rect x="458" y="214" width="57" height="13" rx="2" fill="rgba(48,160,80,0.55)"/>
-<text x="548" y="225" fill="#bbb" font-size="10" font-family="monospace">0.71</text>
-<text x="412" y="246" fill="#666" font-size="10" font-family="monospace">θ = 90°</text>
-<rect x="458" y="235" width="80" height="13" rx="2" fill="rgba(48,160,80,0.04)" stroke="rgba(48,160,80,0.2)" stroke-width="1"/>
-<text x="548" y="246" fill="#555" font-size="10" font-family="monospace">0.00</text>
-<line x1="408" y1="255" x2="606" y2="255" stroke="rgba(80,96,192,0.3)" stroke-width="1"/>
-<text x="412" y="271" fill="#555" font-size="10" font-family="monospace">θ &gt; 90°</text>
-<text x="460" y="271" fill="rgba(208,64,64,0.75)" font-size="10" font-family="monospace">back-face → skip</text>
-<text x="412" y="287" fill="#3a4a5a" font-size="9" font-family="monospace">dot &lt; 0  →  no contribution</text>
-<text x="320" y="326" fill="#2a3a4a" font-size="10" font-family="monospace" text-anchor="middle">— angle examples —</text>
-<g transform="translate(40,338)">
-  <rect x="10" y="78" width="100" height="13" rx="2" fill="rgba(48,160,80,0.25)" stroke="#30a050" stroke-width="1.5"/>
-  <line x1="60" y1="78" x2="60" y2="32" stroke="#30a050" stroke-width="2" marker-end="url(#an)"/>
-  <circle cx="60" cy="13" r="7" fill="rgba(255,102,0,0.5)" stroke="#FF6600" stroke-width="1.5"/>
-  <line x1="60" y1="20" x2="60" y2="34" stroke="#FF6600" stroke-width="2" marker-end="url(#al)"/>
-  <text x="60" y="103" fill="#39FF14" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">θ = 0°</text>
-  <rect x="10" y="112" width="100" height="11" rx="2" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1"/>
-  <rect x="10" y="112" width="100" height="11" rx="2" fill="#30a050"/>
-  <text x="60" y="121" fill="#061018" font-size="8" font-weight="700" font-family="monospace" text-anchor="middle">100%</text>
-</g>
-<g transform="translate(250,338)">
-  <rect x="10" y="78" width="100" height="13" rx="2" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
-  <line x1="60" y1="78" x2="60" y2="32" stroke="#30a050" stroke-width="2" marker-end="url(#an)"/>
-  <circle cx="104" cy="34" r="7" fill="rgba(255,102,0,0.5)" stroke="#FF6600" stroke-width="1.5"/>
-  <line x1="99" y1="39" x2="68" y2="70" stroke="#FF6600" stroke-width="2" marker-end="url(#al)"/>
-  <path d="M 60,50 A 28,28 0 0,1 80,58" fill="none" stroke="#b09020" stroke-width="1.5"/>
-  <text x="83" y="51" fill="#b09020" font-size="9" font-weight="700" font-family="monospace">45°</text>
-  <text x="60" y="103" fill="#bbb" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">θ = 45°</text>
-  <rect x="10" y="112" width="100" height="11" rx="2" fill="rgba(48,160,80,0.08)" stroke="#30a050" stroke-width="1"/>
-  <rect x="10" y="112" width="71" height="11" rx="2" fill="rgba(48,160,80,0.55)"/>
-  <text x="60" y="121" fill="#ccc" font-size="8" font-weight="700" font-family="monospace" text-anchor="middle">71%</text>
-</g>
-<g transform="translate(462,338)">
-  <rect x="10" y="78" width="100" height="13" rx="2" fill="rgba(208,64,64,0.06)" stroke="rgba(208,64,64,0.4)" stroke-width="1.5" stroke-dasharray="4 3"/>
-  <line x1="60" y1="78" x2="60" y2="32" stroke="#30a050" stroke-width="2" marker-end="url(#an)"/>
-  <circle cx="122" cy="78" r="7" fill="rgba(255,102,0,0.5)" stroke="#FF6600" stroke-width="1.5"/>
-  <line x1="115" y1="78" x2="76" y2="78" stroke="#FF6600" stroke-width="2" marker-end="url(#al)"/>
-  <path d="M 60,50 A 28,28 0 0,1 88,78" fill="none" stroke="#b09020" stroke-width="1.5"/>
-  <text x="80" y="57" fill="#b09020" font-size="9" font-weight="700" font-family="monospace">90°</text>
-  <text x="60" y="103" fill="rgba(208,64,64,0.8)" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">θ = 90°</text>
-  <rect x="10" y="112" width="100" height="11" rx="2" fill="rgba(208,64,64,0.05)" stroke="rgba(208,64,64,0.4)" stroke-width="1" stroke-dasharray="3 2"/>
-  <text x="60" y="121" fill="rgba(208,64,64,0.7)" font-size="8" font-weight="700" font-family="monospace" text-anchor="middle">0%  (skip)</text>
-</g>
-<line x1="40" y1="472" x2="62" y2="472" stroke="#30a050" stroke-width="2"/>
-<text x="68" y="476" fill="#30a050" font-size="9" font-family="monospace">N̂  surface normal</text>
-<line x1="250" y1="472" x2="272" y2="472" stroke="#FF6600" stroke-width="2" stroke-dasharray="5 2"/>
-<text x="278" y="476" fill="#FF6600" font-size="9" font-family="monospace">L̂  light direction</text>
-</svg>
diff --git a/doc/Shading/Shaded sphere.png b/doc/Shading/Shaded sphere.png
deleted file mode 100644 (file)
index fbc6487..0000000
Binary files a/doc/Shading/Shaded sphere.png and /dev/null differ
diff --git a/doc/Shading/Shading pipeline.svg b/doc/Shading/Shading pipeline.svg
deleted file mode 100644 (file)
index a58c431..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-<svg viewBox="0 0 620 80" width="620" height="80" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <marker id="arrowhead2" viewBox="0 0 10 10" refX="9" refY="5"
-            markerWidth="6" markerHeight="6" orient="auto">
-      <path d="M 0 0 L 10 5 L 0 10 z" fill="#30a050"/>
-    </marker>
-  </defs>
-  <rect width="620" height="80" fill="#061018"/>
-
-  <!-- Transform phase (where shading happens) -->
-  <rect x="140" y="25" width="90" height="30" rx="3" fill="rgba(176,144,32,0.15)" stroke="#b09020" stroke-width="2"/>
-  <text x="185" y="43" fill="#b09020" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Transform</text>
-  <text x="185" y="67" fill="#b09020" font-size="8" font-family="monospace" text-anchor="middle">compute lighting</text>
-
-  <!-- Other phases -->
-  <rect x="15" y="25" width="90" height="30" rx="3" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
-  <text x="60" y="43" fill="#30a050" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Shapes</text>
-
-  <rect x="265" y="25" width="70" height="30" rx="3" fill="rgba(200,80,140,0.15)" stroke="#c05088" stroke-width="1.5"/>
-  <text x="300" y="43" fill="#c05088" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Sort</text>
-
-  <rect x="365" y="25" width="80" height="30" rx="3" fill="rgba(255,102,0,0.15)" stroke="#FF6600" stroke-width="1.5"/>
-  <text x="405" y="43" fill="#FF6600" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Paint</text>
-  <text x="405" y="67" fill="#bbb" font-size="8" font-family="monospace" text-anchor="middle">use cached color</text>
-
-  <rect x="480" y="25" width="60" height="30" rx="3" fill="rgba(57,255,20,0.15)" stroke="#39FF14" stroke-width="1.5"/>
-  <text x="510" y="43" fill="#39FF14" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Blit</text>
-
-  <!-- Arrows -->
-  <line x1="105" y1="40" x2="135" y2="40" stroke="#30a050" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
-  <line x1="230" y1="40" x2="260" y2="40" stroke="#2070c0" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
-  <line x1="335" y1="40" x2="360" y2="40" stroke="#c05088" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
-  <line x1="445" y1="40" x2="475" y2="40" stroke="#FF6600" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
-  <line x1="540" y1="40" x2="565" y2="40" stroke="#39FF14" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
-</svg>
diff --git a/doc/Shading/index.org b/doc/Shading/index.org
deleted file mode 100644 (file)
index fdf95a0..0000000
+++ /dev/null
@@ -1,266 +0,0 @@
-#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
-#+TITLE: Shading & Lighting - Aukio 3D
-#+LANGUAGE: en
-#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
-#+LATEX_HEADER: \usepackage{parskip}
-#+LATEX_HEADER: \usepackage[none]{hyphenat}
-
-#+OPTIONS: H:20 num:20
-#+OPTIONS: author:nil
-
-#+HTML_HEAD: <link rel="stylesheet" href="../style.css"/>
-
-[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
-
-* Overview
-:PROPERTIES:
-:CUSTOM_ID: shading-lighting
-:END:
-
-#+attr_html: :class responsive-img
-#+attr_latex: :width 1000px
-[[file:Shaded sphere.png]]
-
-*Aukio 3D* implements *flat shading* using the [[https://en.wikipedia.org/wiki/Lambert%27s_cosine_law][Lambert cosine
-law]]. Each polygon receives a single color based on its orientation
-relative to light sources. This is a simple yet effective lighting
-model that gives 3D objects depth and realism.
-
-** The Lighting Model: Lambert Cosine Law
-:PROPERTIES:
-:CUSTOM_ID: lambert-cosine-law
-:END:
-
-#+INCLUDE: "Lambert cosine law.svg" export html
-
-The *Lambert cosine law* determines how much light a surface receives
-based on its orientation. A surface facing directly toward a light source
-receives maximum illumination; as it tilts away, the illumination decreases
-proportionally until it reaches zero when perpendicular to the light
-direction. This fundamental principle creates the visual cues that make 3D
-objects appear solid and dimensional rather than flat.
-
-The engine implements this law through the dot product of two vectors. The
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] computes a unit vector pointing from the polygon's center
-to each [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]], then calculates the dot product with the surface
-normal. When the dot product equals 1.0, the surface faces the light
-directly and receives full brightness. At 0.71 (a 45-degree angle), it
-receives about 71% illumination. At zero or below, the surface faces away
-from the light and receives no direct contribution from that source. The
-implementation in [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager.computeLighting()]] explicitly checks for
-positive dot products before adding light contributions, ensuring that
-back-facing surfaces skip unnecessary calculations.
-
-The surface normal itself is computed by [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]], which takes
-the first three vertices of a polygon and calculates their cross product to
-find the perpendicular direction. This normal, along with the polygon's
-center point calculated by [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]], is passed to the lighting manager
-during the [[file:../Rendering loop/][transform phase]] of the rendering loop. The transform phase runs
-in parallel, but each polygon is transformed by exactly one worker per
-pass, so its cached =shadedColor= field has a single writer — the
-result is reused allocation-free during the subsequent multi-threaded
-paint phase. See the
-[[file:../index.org::#normal-vector][Normal Vector]] section for more details on how normals are computed and used
-throughout the engine.
-
-* Light Sources
-:PROPERTIES:
-:CUSTOM_ID: light-sources
-:END:
-
-Each light source has three properties:
-
-| Property   | Description                          |
-|------------+--------------------------------------|
-| Position   | 3D world coordinates of the light    |
-| Color      | RGB color of emitted light           |
-| Intensity  | Brightness multiplier (1.0 = normal) |
-
-#+BEGIN_SRC java
-import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource;
-import eu.svjatoslav.aukio.e3d.geometry.Point3D;
-import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
-
-// Create a bright yellow light to the right
-LightSource rightLight = new LightSource(
-    new Point3D(200, -100, 0),  // position: right, above, at viewer level
-    Color.YELLOW,               // color
-    2.0                         // intensity: extra bright
-);
-
-// Create a dim blue light from the left
-LightSource leftLight = new LightSource(
-    new Point3D(-150, 50, 100),
-    Color.BLUE,
-    0.5                         // intensity: dim
-);
-#+END_SRC
-
-Multiple light sources add their contributions together, allowing for
-complex lighting setups like the screenshot above showing a sphere lit
-by two lights from the right.
-
-** Distance Attenuation
-:PROPERTIES:
-:CUSTOM_ID: distance-attenuation
-:END:
-
-#+INCLUDE: "Distance attenuation.svg" export html
-
-Light intensity decreases with distance using a *simplified inverse
-square law*:
-
-#+BEGIN_SRC
-attenuation = 1.0 / (1.0 + 0.0001 * distance²)
-#+END_SRC
-
-- At distance 0: attenuation = 1.0 (full intensity)
-- At distance 100: attenuation ≈ 0.99 (almost full)
-- At distance 300: attenuation ≈ 0.52 (half intensity)
-- At distance 500: attenuation ≈ 0.29 (about 30%)
-
-This simplified formula prevents harsh cutoffs while still providing
-distance-based dimming. The =0.0001= coefficient was tuned for typical
-scene scales in Aukio 3D.
-
-* Ambient Light
-:PROPERTIES:
-:CUSTOM_ID: ambient-light
-:END:
-
-#+INCLUDE: "Ambient light comparison.svg" export html
-
-*Ambient light* provides base illumination that affects all surfaces
-equally, regardless of orientation. Without ambient light, surfaces not
-directly facing a light source would be pure black.
-
-- Default ambient: =Color(50, 50, 50)= (dim gray) — set by the ViewPanel
-  constructor; a standalone =new LightingManager()= starts at
-  =Color(10, 10, 10)=
-- Configurable via =lightingManager.setAmbientLight()=
-- Too much ambient: flat appearance (no contrast)
-- Too little ambient: harsh shadows (pure black areas)
-
-#+BEGIN_SRC java
-// Increase ambient for softer shadows
-viewPanel.getLightingManager().setAmbientLight(new Color(80, 80, 80));
-
-// Reduce ambient for dramatic contrast
-viewPanel.getLightingManager().setAmbientLight(new Color(20, 20, 20));
-#+END_SRC
-
-* Using Shading in Your Scene
-:PROPERTIES:
-:CUSTOM_ID: using-shading
-:END:
-
-**Adding light sources:**
-
-#+BEGIN_SRC java
-import eu.svjatoslav.aukio.e3d.gui.ViewPanel;
-import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource;
-
-ViewPanel viewPanel = new ViewPanel();
-
-// Get the lighting manager
-LightingManager lighting = viewPanel.getLightingManager();
-
-// Add light sources
-lighting.addLight(new LightSource(
-    new Point3D(200, -100, 0),  // right side, above
-    Color.YELLOW,
-    1.5                         // bright
-));
-
-lighting.addLight(new LightSource(
-    new Point3D(-100, 0, 200),  // left side, further away
-    new Color(255, 200, 150),   // warm white
-    1.0
-));
-
-// Configure ambient light
-lighting.setAmbientLight(new Color(40, 40, 40));
-#+END_SRC
-
-**Enabling shading on shapes:**
-
-#+BEGIN_SRC java
-import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonRectangularBox;
-
-// Create a shaded box
-SolidPolygonRectangularBox box = new SolidPolygonRectangularBox(
-    new Point3D(-50, -50, 100),  // min corner
-    new Point3D(50, 50, 200),   // max corner
-    Color.RED
-);
-
-// Enable shading on the box and all its sub-polygons
-box.setShadingEnabled(true);
-
-// Also enable backface culling for closed meshes
-box.setBackfaceCulling(true);
-
-// Add to scene
-viewPanel.getRootShapeCollection().addShape(box);
-#+END_SRC
-
-Shading propagates through composite shapes — calling
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setShadingEnabled(boolean)][setShadingEnabled(true)]] on a composite enables shading for all its
-sub-polygons.
-
-** Related Classes
-:PROPERTIES:
-:CUSTOM_ID: related-classes
-:END:
-
-| Class | Purpose |
-|-------+---------|
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] | Manages light sources and computes shading |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]] | Individual light with position, color, intensity |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Polygon shape with shading support |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] | Composite shape with shading propagation |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html][ViewPanel]] | Provides access to LightingManager |
-* Implementation details
-:PROPERTIES:
-:CUSTOM_ID: implementation-details
-:END:
-
-#+INCLUDE: "Shading pipeline.svg" export html
-
-Lighting is computed during *Phase 1* (transform phase) of the
-[[file:../Rendering loop/][rendering loop]]:
-
-1. Each shaded polygon calculates its center point and surface normal
-2. [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] computes lighting from all sources
-3. Result stored in reusable =shadedColor= field
-4. During *Phase 4* (paint), the cached color is used directly
-
-**Why during transform phase?**
-
-- Lighting computed *once per polygon per pass* — not per pixel
-- Each polygon is transformed by a single worker, so its cached result
-  has exactly one writer even though the transform phase runs in parallel
-- Result reused during multi-threaded paint phase — efficient
-
-** Performance Characteristics
-:PROPERTIES:
-:CUSTOM_ID: performance
-:END:
-
-| Aspect       | Cost                          |
-|--------------+-------------------------------|
-| Computation  | Per polygon, not per pixel    |
-| Phase        | Parallel transform (single writer per polygon) |
-| Allocation   | Zero (reuses Color instance)  |
-| Cache        | One shadedColor per polygon   |
-
-The shading implementation is optimized for CPU rendering:
-
-- *Flat shading*: One lighting calculation per polygon (N-vertex polygon = 1 calculation)
-- *Reusable Color*: Result stored in existing field, no allocation during render
-- *Thread-safe*: One writer per polygon per pass, so no synchronization needed
-- *Pre-computed*: All paint workers (tile grid, ~75% of CPU cores by default) read the same cached result
-
-This approach trades visual fidelity (no per-pixel lighting) for
-performance — essential for software rendering where per-pixel lighting
-would be prohibitively expensive.
diff --git a/doc/Stereoscopic rendering/Stereo geometry.svg b/doc/Stereoscopic rendering/Stereo geometry.svg
deleted file mode 100644 (file)
index 2f62d50..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-<svg viewBox="0 0 640 460" width="640" height="460" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="2" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-  </defs>
-
-  <!-- background -->
-  <rect width="640" height="460" fill="#061018"/>
-
-  <!-- faint grid -->
-  <g stroke="#1a3a4a" stroke-width="0.5">
-    <line x1="60" y1="60" x2="600" y2="60"/>
-    <line x1="60" y1="130" x2="600" y2="130"/>
-    <line x1="60" y1="200" x2="600" y2="200"/>
-    <line x1="60" y1="270" x2="600" y2="270"/>
-    <line x1="60" y1="340" x2="600" y2="340"/>
-    <line x1="60" y1="410" x2="600" y2="410"/>
-  </g>
-
-  <text x="320" y="34" fill="#40b0d0" font-size="17" font-family="monospace" text-anchor="middle" filter="url(#glow)">Two parallel cameras, one screen</text>
-  <text x="320" y="52" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">top-down view of the scene (z grows downward = into the scene)</text>
-
-  <!-- projection plane line (screen) -->
-  <line x1="100" y1="140" x2="560" y2="140" stroke="#c05088" stroke-width="2" filter="url(#glow)"/>
-  <text x="560" y="130" fill="#c05088" font-size="10" font-family="monospace" text-anchor="end">screen plane (per eye)</text>
-
-  <!-- world objects -->
-  <circle cx="330" cy="240" r="8" fill="#FF8833" filter="url(#glow)"/>
-  <text x="330" y="262" fill="#FF8833" font-size="10" font-family="monospace" text-anchor="middle">near object</text>
-  <circle cx="330" cy="380" r="8" fill="#2070c0" filter="url(#glow)"/>
-  <text x="330" y="402" fill="#2070c0" font-size="10" font-family="monospace" text-anchor="middle">far object</text>
-
-  <!-- eyes -->
-  <circle cx="240" cy="70" r="7" fill="#39FF14" filter="url(#glow)"/>
-  <text x="222" y="74" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="end">left eye</text>
-  <text x="222" y="88" fill="#39FF14" font-size="9" font-family="monospace" text-anchor="end">x - IPD/2</text>
-  <circle cx="420" cy="70" r="7" fill="#40b0d0" filter="url(#glow)"/>
-  <text x="438" y="74" fill="#40b0d0" font-size="11" font-family="monospace">right eye</text>
-  <text x="438" y="88" fill="#40b0d0" font-size="9" font-family="monospace">x + IPD/2</text>
-
-  <!-- IPD brace -->
-  <line x1="240" y1="98" x2="420" y2="98" stroke="#b09020" stroke-width="1.5" filter="url(#glow)"/>
-  <line x1="240" y1="92" x2="240" y2="104" stroke="#b09020" stroke-width="1.5"/>
-  <line x1="420" y1="92" x2="420" y2="104" stroke="#b09020" stroke-width="1.5"/>
-  <text x="330" y="92" fill="#b09020" font-size="11" font-family="monospace" text-anchor="middle" filter="url(#glow)">IPD = 6.5 units (cm)</text>
-
-  <!-- rays: left eye -->
-  <line x1="240" y1="70" x2="330" y2="240" stroke="#39FF14" stroke-width="1.5" stroke-opacity="0.8"/>
-  <line x1="240" y1="70" x2="330" y2="380" stroke="#39FF14" stroke-width="1" stroke-opacity="0.45" stroke-dasharray="5 3"/>
-  <!-- rays: right eye -->
-  <line x1="420" y1="70" x2="330" y2="240" stroke="#40b0d0" stroke-width="1.5" stroke-opacity="0.8"/>
-  <line x1="420" y1="70" x2="330" y2="380" stroke="#40b0d0" stroke-width="1" stroke-opacity="0.45" stroke-dasharray="5 3"/>
-
-  <!-- projections of the near object on the screen plane -->
-  <!-- left eye: ray from (240,70) to (330,240); at y=140: t=(140-70)/(240-70)=0.4118, x=240+0.4118*90=277 -->
-  <circle cx="277" cy="140" r="4" fill="#FF8833" stroke="#39FF14" stroke-width="1.5" filter="url(#glow)"/>
-  <!-- right eye: ray from (420,70) to (330,240); at y=140: x=420-0.4118*90=383 -->
-  <circle cx="383" cy="140" r="4" fill="#FF8833" stroke="#40b0d0" stroke-width="1.5" filter="url(#glow)"/>
-  <!-- projections of the far object -->
-  <!-- left: (240,70)->(330,380); t=(140-70)/(380-70)=0.2258; x=240+0.2258*90=260 -->
-  <circle cx="260" cy="140" r="3.5" fill="#2070c0" stroke="#39FF14" stroke-width="1.5"/>
-  <!-- right: x=420-0.2258*90=400 -->
-  <circle cx="400" cy="140" r="3.5" fill="#2070c0" stroke="#40b0d0" stroke-width="1.5"/>
-
-  <!-- disparity braces on the screen plane -->
-  <line x1="277" y1="152" x2="383" y2="152" stroke="#FF8833" stroke-width="1.5" filter="url(#glow)"/>
-  <line x1="277" y1="147" x2="277" y2="157" stroke="#FF8833" stroke-width="1.5"/>
-  <line x1="383" y1="147" x2="383" y2="157" stroke="#FF8833" stroke-width="1.5"/>
-  <text x="330" y="168" fill="#FF8833" font-size="10" font-family="monospace" text-anchor="middle">large disparity = close</text>
-  <line x1="260" y1="126" x2="400" y2="126" stroke="#2070c0" stroke-width="1" stroke-opacity="0.9"/>
-  <text x="330" y="120" fill="#2070c0" font-size="10" font-family="monospace" text-anchor="middle">small disparity = far</text>
-
-  <text x="60" y="446" fill="#999" font-size="10" font-family="monospace">cameras stay PARALLEL (no toe-in) - depth comes purely from the x offset</text>
-</svg>
diff --git a/doc/Stereoscopic rendering/Stereo per eye.svg b/doc/Stereoscopic rendering/Stereo per eye.svg
deleted file mode 100644 (file)
index 5fc9cfd..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-<svg viewBox="0 0 640 300" width="640" height="300" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="1.5" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-  </defs>
-
-  <!-- background -->
-  <rect width="640" height="300" fill="#061018"/>
-
-  <text x="320" y="30" fill="#40b0d0" font-size="15" font-family="monospace" text-anchor="middle" filter="url(#glow)">What changes per eye</text>
-
-  <!-- table-ish rows -->
-  <g font-family="monospace">
-    <!-- header -->
-    <text x="60" y="62" fill="#999" font-size="10">concern</text>
-    <text x="330" y="62" fill="#999" font-size="10">per-eye behavior</text>
-    <line x1="55" y1="70" x2="600" y2="70" stroke="#1a3a4a" stroke-width="1"/>
-
-    <text x="60" y="92" fill="#c05088" font-size="10">camera</text>
-    <text x="330" y="92" fill="#40b0d0" font-size="10">translation.x += &#177;IPD/2 (restored after the pass)</text>
-    <line x1="55" y1="100" x2="600" y2="100" stroke="#1a3a4a" stroke-width="0.5"/>
-
-    <text x="60" y="122" fill="#c05088" font-size="10">projection</text>
-    <text x="330" y="122" fill="#40b0d0" font-size="10">scale = eyeWidth/3; x += stereoViewportOffsetX</text>
-    <line x1="55" y1="130" x2="600" y2="130" stroke="#1a3a4a" stroke-width="0.5"/>
-
-    <text x="60" y="152" fill="#c05088" font-size="10">frustum culling</text>
-    <text x="330" y="152" fill="#40b0d0" font-size="10">built from stereoViewportWidth - narrower FOV per eye</text>
-    <line x1="55" y1="160" x2="600" y2="160" stroke="#1a3a4a" stroke-width="0.5"/>
-
-    <text x="60" y="182" fill="#c05088" font-size="10">painting</text>
-    <text x="330" y="182" fill="#40b0d0" font-size="10">clipped to [renderMinX, renderMaxX) = the eye's half</text>
-    <line x1="55" y1="190" x2="600" y2="190" stroke="#1a3a4a" stroke-width="0.5"/>
-
-    <text x="60" y="212" fill="#c05088" font-size="10">mouse picking</text>
-    <text x="330" y="212" fill="#40b0d0" font-size="10">hits combined only for the eye containing the cursor</text>
-    <line x1="55" y1="220" x2="600" y2="220" stroke="#1a3a4a" stroke-width="0.5"/>
-
-    <text x="60" y="242" fill="#c05088" font-size="10">HUD / overlays</text>
-    <text x="330" y="242" fill="#40b0d0" font-size="10">drawn once, spanning the full frame (zero disparity)</text>
-  </g>
-
-  <text x="60" y="282" fill="#999" font-size="9" font-family="monospace">everything else - geometry, textures, lightmaps, GI - is shared: the scene is identical, only the viewpoint moves</text>
-</svg>
diff --git a/doc/Stereoscopic rendering/Stereo pipeline.svg b/doc/Stereoscopic rendering/Stereo pipeline.svg
deleted file mode 100644 (file)
index 895e1fc..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <filter id="glow">
-      <feGaussianBlur stdDeviation="2" result="blur"/>
-      <feMerge>
-        <feMergeNode in="blur"/>
-        <feMergeNode in="SourceGraphic"/>
-      </feMerge>
-    </filter>
-    <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
-      <path d="M 0 0 L 10 5 L 0 10 z" fill="#40b0d0"/>
-    </marker>
-    <marker id="arrowG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
-      <path d="M 0 0 L 10 5 L 0 10 z" fill="#39FF14"/>
-    </marker>
-  </defs>
-
-  <!-- background -->
-  <rect width="640" height="480" fill="#061018"/>
-
-  <text x="320" y="32" fill="#40b0d0" font-size="17" font-family="monospace" text-anchor="middle" filter="url(#glow)">One frame = two passes</text>
-  <text x="320" y="50" fill="#999" font-size="10" font-family="monospace" text-anchor="middle">the triple-buffered pipeline runs the same phases twice, once per eye</text>
-
-  <!-- camera box -->
-  <rect x="230" y="70" width="180" height="44" rx="5" fill="rgba(176,144,32,0.08)" stroke="#b09020" stroke-width="1.5"/>
-  <text x="320" y="88" fill="#b09020" font-size="11" font-family="monospace" text-anchor="middle">camera</text>
-  <text x="320" y="104" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">translation.x nudged +/- IPD/2</text>
-
-  <!-- left pass lane -->
-  <rect x="40" y="150" width="250" height="60" rx="5" fill="rgba(57,255,20,0.06)" stroke="#39FF14" stroke-width="1.5" filter="url(#glow)"/>
-  <text x="165" y="172" fill="#39FF14" font-size="12" font-family="monospace" text-anchor="middle">pass LEFT</text>
-  <text x="165" y="188" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">transform &#8594; sort &#8594; tile-bin</text>
-  <text x="165" y="201" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">viewport [0, w/2)</text>
-
-  <!-- right pass lane -->
-  <rect x="350" y="150" width="250" height="60" rx="5" fill="rgba(64,176,208,0.06)" stroke="#40b0d0" stroke-width="1.5" filter="url(#glow)"/>
-  <text x="475" y="172" fill="#40b0d0" font-size="12" font-family="monospace" text-anchor="middle">pass RIGHT</text>
-  <text x="475" y="188" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">transform &#8594; sort &#8594; tile-bin</text>
-  <text x="475" y="201" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">viewport [w/2, w)</text>
-
-  <!-- arrows from camera -->
-  <line x1="285" y1="114" x2="180" y2="148" stroke="#39FF14" stroke-width="1.5" marker-end="url(#arrowG)"/>
-  <line x1="355" y1="114" x2="460" y2="148" stroke="#40b0d0" stroke-width="1.5" marker-end="url(#arrow)"/>
-
-  <!-- per-pass context note -->
-  <rect x="130" y="236" width="380" height="52" rx="5" fill="rgba(192,80,136,0.06)" stroke="#c05088" stroke-width="1.5"/>
-  <text x="320" y="256" fill="#c05088" font-size="10" font-family="monospace" text-anchor="middle">each pass owns a RenderingContext copy</text>
-  <text x="320" y="272" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">stereoEye, stereoViewportWidth/OffsetX, renderMinX..renderMaxX</text>
-
-  <line x1="165" y1="210" x2="250" y2="234" stroke="#39FF14" stroke-width="1.2" marker-end="url(#arrowG)"/>
-  <line x1="475" y1="210" x2="390" y2="234" stroke="#40b0d0" stroke-width="1.2" marker-end="url(#arrow)"/>
-
-  <!-- shared frame buffer -->
-  <rect x="70" y="330" width="500" height="80" rx="5" fill="rgba(255,255,255,0.03)" stroke="#b09020" stroke-width="1.5" filter="url(#glow)"/>
-  <rect x="70" y="330" width="250" height="80" fill="rgba(57,255,20,0.07)"/>
-  <rect x="320" y="330" width="250" height="80" fill="rgba(64,176,208,0.07)"/>
-  <line x1="320" y1="330" x2="320" y2="410" stroke="#c05088" stroke-width="2" stroke-dasharray="6 4"/>
-  <text x="195" y="365" fill="#39FF14" font-size="11" font-family="monospace" text-anchor="middle">left eye pixels</text>
-  <text x="195" y="382" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">painting clipped to left half</text>
-  <text x="445" y="365" fill="#40b0d0" font-size="11" font-family="monospace" text-anchor="middle">right eye pixels</text>
-  <text x="445" y="382" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">painting clipped to right half</text>
-  <text x="320" y="428" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">ONE shared frame buffer &#8594; one blit to screen (side-by-side image)</text>
-
-  <line x1="250" y1="288" x2="195" y2="328" stroke="#39FF14" stroke-width="1.2" marker-end="url(#arrowG)"/>
-  <line x1="390" y1="288" x2="445" y2="328" stroke="#40b0d0" stroke-width="1.2" marker-end="url(#arrow)"/>
-
-  <text x="60" y="464" fill="#999" font-size="9" font-family="monospace">vertex buffers, aggregators and paint slots still cycle through 3 slots - the two passes overlap freely</text>
-</svg>
diff --git a/doc/Stereoscopic rendering/index.org b/doc/Stereoscopic rendering/index.org
deleted file mode 100644 (file)
index f1ceee8..0000000
+++ /dev/null
@@ -1,190 +0,0 @@
-#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
-#+TITLE: Stereoscopic Rendering - Aukio 3D
-#+LANGUAGE: en
-#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
-#+LATEX_HEADER: \usepackage{parskip}
-#+LATEX_HEADER: \setlength{\parindent}{15pt}
-#+LATEX_HEADER: \usepackage{palatino}
-#+LATEX_HEADER: \usepackage{charter}
-#+HTML_HEAD: <style type="text/css">body { max-width: 100%;}</style>
-
-[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]]
-
-* What stereo rendering adds
-:PROPERTIES:
-:CUSTOM_ID: what-stereo-adds
-:END:
-
-A single rendered image is flat: the brain infers depth only from
-monocular cues (occlusion, shading, perspective, motion parallax while
-you move). *Stereoscopic rendering* adds the strongest depth cue of
-all — /binocular disparity/: your two eyes see slightly different
-images, and the visual cortex turns the difference into a direct
-sensation of depth.
-
-Aukio 3D implements the simplest and most portable form: *side-by-side
-stereo*. Every frame renders the scene twice — once from the left eye
-position, once from the right — into the left and right halves of the
-same image. A VR headset, 3D TV, or a pair of XR glasses in
-side-by-side mode feeds each half to the corresponding eye, and the
-scene gains real volume.
-
-#+CAPTION: A side-by-side stereoscopic frame of the House demo, rendered headlessly with [[file:../index.org::#snapshot][the Snapshot tool]]. Left half: left eye. Right half: right eye. Compare the dark cube and the doorway between the halves — the horizontal shift is the disparity your brain reads as depth.
-[[file:stereo-side-by-side.png]]
-
-* The geometry: two parallel cameras
-:PROPERTIES:
-:CUSTOM_ID: geometry
-:END:
-
-The two eye cameras are identical to the mono camera except for one
-thing: the left eye's position is shifted by -IPD/2 and the right
-eye's by +IPD/2 along the *world* X axis (the offset is applied to the
-camera translation's =x= component directly, then restored). IPD
-(inter-pupillary distance) defaults to *6.5 world units* — the House
-demo treats 1 unit as 1 cm, and 6.5 cm is the median human IPD.
-
-Both cameras look in exactly the same direction (*parallel cameras*,
-no toe-in). Objects at different depths then land at different
-horizontal offsets between the two images — that offset is the
-disparity:
-
-#+CAPTION: Top-down view: the two eye positions and how a near and a far object project onto the screen plane. The near object separates much more between the eyes than the far one.
-[[file:Stereo geometry.svg]]
-
-- near object -> large disparity -> feels close,
-- far object -> small disparity -> feels far,
-- object at infinity -> zero disparity.
-
-Larger IPD exaggerates disparity (stronger but potentially straining
-depth); smaller IPD flattens the scene. =+=/=-= keys adjust it live in
-0.5-unit steps while stereo is active.
-
-* One frame = two passes
-:PROPERTIES:
-:CUSTOM_ID: two-passes
-:END:
-
-Stereo does not add a second pipeline — it runs the existing
-triple-buffered pipeline *twice per frame*. The render thread in
-=ViewPanel.renderFrame()= executes two render passes back to back:
-
-#+CAPTION: Per frame, the camera is nudged left, a full transform/sort/bin pass runs for the left viewport, then the camera is nudged right and a second pass runs for the right viewport. Both paint into one shared frame buffer, clipped to their half.
-[[file:Stereo pipeline.svg]]
-
-1. *Pass LEFT:* camera translation.x is temporarily decreased by
-   IPD/2, the scene is transformed, depth-sorted and tile-binned into a
-   per-pass =RenderingContext= copy whose viewport is the left half of
-   the frame (=[0, width/2)=), and the paint continuation is submitted
-   to the worker pool.
-2. *Pass RIGHT:* the same with +IPD/2 and the right viewport
-   (=[width/2, width)=). The camera offset is always restored in a
-   =finally= block, so the camera never drifts.
-3. The two paints write into *one shared frame buffer* — each clipped
-   to its half — and the completed side-by-side image is blitted to
-   the screen in one go.
-
-The triple-buffer machinery (3 vertex slots, 3 aggregator slots, 3
-framebuffers) does not change: a *pass* takes the slot =passCounter %
-3=, so left and right passes of the same frame simply occupy
-consecutive slots and overlap exactly like consecutive mono frames do.
-Workers flow from one pass's tiles straight into the next pass's tiles
-with no idle gap.
-
-* What adapts per eye
-:PROPERTIES:
-:CUSTOM_ID: per-eye
-:END:
-
-The scene itself — geometry, textures, lightmaps, global illumination
-— is shared and identical for both eyes. Only the *viewpoint* moves,
-so only view-dependent stages differ per pass:
-
-#+CAPTION: The per-eye surface area of the engine. Everything not listed here is eye-independent.
-[[file:Stereo per eye.svg]]
-
-- *Projection:* =Vertex= projects with =projectionScale = eyeWidth/3=
-  (per-eye horizontal FOV) and adds =stereoViewportOffsetX= so the
-  projected image lands in the correct half of the buffer. The same
-  offset is applied for near-plane-clip vertices created directly in
-  camera space.
-- *Frustum culling:* the frustum is rebuilt per pass from
-  =stereoViewportWidth=, so each eye culls against its own (narrower)
-  view volume — nothing leaks in from the other eye's half.
-- *Painting:* every painter clips X to =[renderMinX, renderMaxX)=,
-  which the pass set to its viewport. No eye can paint into the other
-  half, even if a polygon crosses the center line.
-- *Mouse picking:* in stereo each eye shows the same object at a
-  different screen X, so a hit can only be resolved against one eye.
-  =ViewPanel= combines mouse results only for the pass whose viewport
-  actually contains the cursor.
-- *HUD/overlays:* developer tools, crosshair and text are drawn once
-  over the finished frame, at zero disparity — they sit on the screen
-  surface, not in the world.
-
-* Enabling stereo
-:PROPERTIES:
-:CUSTOM_ID: enabling
-:END:
-
-#+BEGIN_SRC java
-ViewPanel viewPanel = ...;
-
-// Side-by-side stereo on:
-viewPanel.setStereoModeEnabled(true);
-
-// Optional: match the viewer (default 6.5 world units):
-viewPanel.setStereoIPD(6.5);
-#+END_SRC
-
-In the demos, *SHIFT+F11* toggles stereo and fullscreen together (XR
-glasses want both); plain *F11* remains fullscreen-only. With stereo
-active, *+* and *-* adjust the IPD in 0.5-unit steps (clamped at 0.5)
-so the viewer can tune comfort at runtime.
-
-#+CAPTION: The same scene rendered as a normal mono frame (for comparison with the pair above). Notice there is no horizontal offset to read depth from — the picture is flat.
-[[file:mono-comparison.png]]
-
-* Performance and limitations
-:PROPERTIES:
-:CUSTOM_ID: limitations
-:END:
-
-- Stereo *doubles the per-frame transform, sort and paint work* — two
-  full passes instead of one. The pipeline overlaps them the same way
-  it overlaps consecutive mono frames, so throughput drops less than
-  2x on a multi-core machine, but expect a real cost.
-- Each eye gets *half the horizontal resolution* of the panel. On a
-  1920x1080 fullscreen window each eye sees 960x1080 — pixels are
-  shared, not duplicated.
-- IPD is in *world units*: 6.5 only means "6.5 cm" if the scene is
-  modeled at 1 unit = 1 cm. In a scene with a different scale, divide
-  or multiply accordingly — or just tune with =+=/=-= until the depth
-  feels right.
-- The eye offset is applied along the *world X axis*, not the camera's
-  right vector: it is exactly correct when the camera faces along Z
-  (yaw = 0) and degrades as you turn — at yaw = 90° the eyes would be
-  offset front-to-back instead of side-to-side. For a fixed-viewing-
-  direction demo this is fine; a fully rotational stereo camera would
-  need to apply the IPD along the rotated right vector.
-- Side-by-side is a *display format*, not a headset driver: the engine
-  produces the image; an XR viewer, 3D TV or video player is
-  responsible for delivering the halves to the eyes.
-- Global illumination is unaffected: lightmaps live on the surfaces,
-  so both eyes sample the same converged lighting for free.
-
-* Related Classes
-:PROPERTIES:
-:CUSTOM_ID: related-classes
-:END:
-
-| Class                | Role in stereo rendering                                        |
-|----------------------+-----------------------------------------------------------------|
-| =ViewPanel=          | owns stereoModeEnabled/stereoIPD; runs the two passes per frame |
-| =StereoEye=          | NONE / LEFT / RIGHT tag carried by each pass context            |
-| =RenderingContext=   | per-eye viewport fields: stereoViewportWidth/OffsetX, renderMin/MaxX |
-| =Vertex=             | per-eye projection: scale from eye width + viewport X offset    |
-| =ShapeCollection=    | rebuilds the frustum per pass from the eye's viewport width     |
-| =InputManager=       | SHIFT+F11 stereo toggle, +/- live IPD adjustment                |
-
-[[file:../index.html#outline-container-understanding-3d-engine][Back to main documentation]]
diff --git a/doc/Stereoscopic rendering/mono-comparison.png b/doc/Stereoscopic rendering/mono-comparison.png
deleted file mode 100644 (file)
index 3b70ee0..0000000
Binary files a/doc/Stereoscopic rendering/mono-comparison.png and /dev/null differ
diff --git a/doc/Stereoscopic rendering/stereo-side-by-side.png b/doc/Stereoscopic rendering/stereo-side-by-side.png
deleted file mode 100644 (file)
index 12fa4fc..0000000
Binary files a/doc/Stereoscopic rendering/stereo-side-by-side.png and /dev/null differ
diff --git a/doc/Winding order.svg b/doc/Winding order.svg
deleted file mode 100644 (file)
index d82048e..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-<svg viewBox="0 0 640 480" width="640" height="480" xmlns="http://www.w3.org/2000/svg">
-  <defs>
-    <marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5"
-            markerWidth="8" markerHeight="8" orient="auto-start-reverse">
-      <path d="M 0 0 L 10 5 L 0 10 z" fill="#30a050"/>
-    </marker>
-    <marker id="arrow-red" viewBox="0 0 10 10" refX="10" refY="5"
-            markerWidth="8" markerHeight="8" orient="auto-start-reverse">
-      <path d="M 0 0 L 10 5 L 0 10 z" fill="rgba(208,64,64,0.5)"/>
-    </marker>
-  </defs>
-  <rect width="640" height="480" fill="#061018"/>
-
-  <!-- Green front-face triangle: V1=top, V2=bottom-left, V3=bottom-right -->
-  <polygon points="160,100 260,360 60,360" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="3"/>
-  <!-- CCW arrow: arc from near V1, curves LEFT and DOWN toward V2 -->
-  <path d="M140,144 A 104,104 0 0,0 74,310" fill="none" stroke="#30a050" stroke-width="3" stroke-dasharray="8 4" marker-end="url(#arrow-green)"/>
-  <text x="68" y="240" fill="#30a050" font-size="20" font-weight="700" font-family="monospace">CCW</text>
-  <circle cx="160" cy="100" r="6" fill="#30a050"/>
-  <circle cx="60" cy="360" r="6" fill="#30a050"/>
-  <circle cx="260" cy="360" r="6" fill="#30a050"/>
-  <text x="156" y="88" fill="#aaa" font-size="18" font-family="monospace">V₁</text>
-  <text x="28" y="396" fill="#aaa" font-size="18" font-family="monospace">V₂</text>
-  <text x="264" y="396" fill="#aaa" font-size="18" font-family="monospace">V₃</text>
-  <text x="72" y="440" fill="#30a050" font-size="22" font-weight="700" font-family="monospace">FRONT FACE ✓</text>
-  <!-- Red back-face triangle -->
-  <polygon points="480,100 580,360 380,360" fill="rgba(208,64,64,0.06)" stroke="rgba(208,64,64,0.3)" stroke-width="3" stroke-dasharray="12 6"/>
-  <!-- CW arrow: arc from near V1, curves RIGHT and DOWN -->
-  <path d="M500,144 A 104,104 0 0,1 566,310" fill="none" stroke="rgba(208,64,64,0.5)" stroke-width="3" stroke-dasharray="8 4" marker-end="url(#arrow-red)"/>
-  <text x="536" y="240" fill="rgba(208,64,64,0.6)" font-size="20" font-weight="700" font-family="monospace">CW</text>
-  <line x1="456" y1="216" x2="504" y2="264" stroke="rgba(208,64,64,0.4)" stroke-width="6"/>
-  <line x1="504" y1="216" x2="456" y2="264" stroke="rgba(208,64,64,0.4)" stroke-width="6"/>
-  <text x="372" y="440" fill="rgba(208,64,64,0.7)" font-size="22" font-weight="700" font-family="monospace">BACK FACE ✗</text>
-  <text x="390" y="468" fill="#aaa" font-size="18" font-family="monospace">(culled — not drawn)</text>
-</svg>
diff --git a/doc/export-docs.sh b/doc/export-docs.sh
deleted file mode 100755 (executable)
index d55e8f2..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/bin/bash
-# export-docs.sh — export all org-mode documentation pages to HTML.
-#
-# Exports every doc/**/index.org (and doc/index.org) with the darksun
-# theme, using the user's Emacs configuration. Run from anywhere:
-#
-#   doc/export-docs.sh            # export all pages
-#   doc/export-docs.sh --check    # export, then render every page with
-#                                 # headless Chrome to /tmp/doc-check-*.png
-#                                 # for visual inspection
-#
-# Requires: emacs (with ~/.emacs providing the org HTML setup),
-#           google-chrome (only for --check).
-
-set -euo pipefail
-DOC_DIR="$(cd "$(dirname "$0")" && pwd)"
-
-mapfile -t PAGES < <(find "$DOC_DIR" -name index.org | sort)
-
-echo "Exporting ${#PAGES[@]} pages..."
-for page in "${PAGES[@]}"; do
-    rel="${page#"$DOC_DIR"/}"
-    if emacs --batch -l ~/.emacs --visit="$page" \
-            --funcall=org-html-export-to-html --kill 2>&1 \
-            | grep -qi "aborted\|unable to resolve link"; then
-        echo "FAIL $rel"
-        exit 1
-    fi
-    echo "  ok $rel"
-done
-
-if [[ "${1:-}" == "--check" ]]; then
-    echo "Rendering pages for visual check..."
-    for page in "${PAGES[@]}"; do
-        rel="${page#"$DOC_DIR"/}"
-        html="${page%.org}.html"
-        out="/tmp/doc-check-$(echo "$rel" | tr '/ ' '__').png"
-        google-chrome --headless --disable-gpu --hide-scrollbars \
-            --virtual-time-budget=8000 --window-size=1100,2000 \
-            --screenshot="$out" "file://$html" 2>/dev/null
-        echo "  shot $out"
-    done
-fi
-
-echo "Done."
diff --git a/doc/index.org b/doc/index.org
deleted file mode 100644 (file)
index 4068d09..0000000
+++ /dev/null
@@ -1,1224 +0,0 @@
-#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
-#+TITLE: Aukio 3D - Realtime 3D engine
-#+LANGUAGE: en
-#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
-#+LATEX_HEADER: \usepackage{parskip}
-#+LATEX_HEADER: \usepackage[none]{hyphenat}
-
-#+OPTIONS: H:20 num:20
-#+OPTIONS: author:nil
-
-#+HTML_HEAD: <link rel="stylesheet" href="style.css"/>
-
-* Introduction
-:PROPERTIES:
-:CUSTOM_ID: overview
-:ID:       a31a1f4d-5368-4fd9-aaf8-fa6d81851187
-:END:
-
-[[file:Example.png]]
-
-*Aukio 3D* is a realtime 3D rendering engine written in pure Java. It
-runs entirely on the CPU — no GPU required, no OpenGL, no Vulkan, no
-native libraries. Just Java.
-
-The motivation is simple: GPU-based 3D is a minefield of accidental
-complexity. Drivers are buggy or missing entirely. Features you need
-aren't supported on your target hardware. You run out of GPU RAM. You
-wrestle with platform-specific interop layers, shader compilation
-quirks, and dependency hell. Every GPU API comes with its own
-ecosystem of pain — version mismatches, incomplete implementations,
-vendor-specific workarounds. I want a library that "just works".
-
-*Aukio 3D* takes a different path. By rendering everything in software
-on the CPU, the entire GPU problem space simply disappears. You add a
-Maven dependency, write some Java, and you have a 3D scene. It runs
-wherever Java runs.
-
-This approach is quite practical for many use-cases. Modern systems
-ship with many CPU cores, and those with unified memory architectures
-offer high bandwidth between CPU and RAM. Software rendering that once
-seemed wasteful is now a reasonable choice where you need good-enough
-performance without the overhead of a full GPU pipeline. Java's JIT
-compiler helps too, optimizing hot rendering paths at runtime.
-
-Beyond convenience, CPU rendering gives you complete control. You own
-every pixel. You can freely experiment with custom rendering
-algorithms, optimization strategies, and visual effects without being
-constrained by what a GPU API exposes. Instead of brute-forcing
-everything through a fixed GPU pipeline, you can implement clever,
-application-specific optimizations.
-
-*Aukio 3D* is part of the larger [[https://www3.svjatoslav.eu/projects/aukio/][Aukio project]], with the long-term goal
-of providing a platform for 3D user interfaces and interactive data
-visualization. It can also be used as a standalone 3D engine in any
-Java project. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/][demos]] for examples of what it can do today.
-
-*Major features:*
-** Global Illumination
-:PROPERTIES:
-:CUSTOM_ID: global-illumination
-:END:
-
-#+attr_html: :width 600px
-#+attr_latex: :width 600px
-[[file:Global illumination/Global illumination.png]]
-
-On top of flat shading, the engine computes progressive *global
-illumination* on background CPU threads: real shadows, smooth light
-falloff inside polygons (per-texel lightmaps), and indirect bounce
-light — while the render loop itself never traces a single ray.
-
-Read more about [[file:Global illumination/][global illumination]].
-
-** Side-by-side stereoscopic rendering support
-:PROPERTIES:
-:CUSTOM_ID: stereoscopic
-:END:
-
-#+attr_html: :width 600px
-#+attr_latex: :width 600px
-[[file:Stereoscopic rendering/stereo-side-by-side.png]]
-
-The engine can render every frame twice — once per eye — into the left
-and right halves of the same image, for XR glasses and 3D displays.
-The cameras stay parallel and are offset by a configurable IPD
-(inter-pupillary distance). Two render passes share one triple-buffered
-pipeline, each clipped to its half of the frame buffer; per-eye
-projection, frustum culling and mouse picking adapt automatically.
-
-See [[file:Stereoscopic%20rendering/][Stereoscopic rendering]] for the
-geometry, pipeline and tuning.
-
-** Constructive Solid Geometry
-
-#+attr_html: :width 600px
-#+attr_latex: :width 600px
-[[file:CSG/CSG demo.png]]
-
-*Aukio 3D* allows performing boolean operations against geometry shapes.
-So one can subtract, unionize or intersect shapes.
-
-To understand CSG boolean operations, read more about [[file:CSG/][Constructive
-Solid Geometry]].
-
-** SDF textures for sharp text
-:PROPERTIES:
-:CUSTOM_ID: sdf-text
-:END:
-
-[[file:SDF textures/sdf-angled.png]]
-
-Text and vector-art surfaces do not store coverage; they store a
-*signed distance field* — per texel, the distance to the nearest glyph
-edge. The rasterizer re-derives coverage per screen pixel from that
-smooth field, so text stays sharp at any zoom and fades to clean gray
-under minification, all without a mipmap chain.
-
-See [[file:SDF%20textures/][SDF textures]] for the glyph pipeline, the
-render path, analytic minification and tuning knobs.
-
-* How take engine into use
-:PROPERTIES:
-:CUSTOM_ID: taking-engine-into-use
-:END:
-
-Add the *Aukio 3D* dependency to your Maven project:
-
-#+BEGIN_SRC xml
-<dependencies>
-    <dependency>
-        <groupId>eu.svjatoslav</groupId>
-        <artifactId>aukio-3d</artifactId>
-        <version>1.4</version>
-    </dependency>
-</dependencies>
-#+END_SRC
-
-Also add the repository (the library is not on Maven Central):
-
-#+BEGIN_SRC xml
-<repositories>
-    <repository>
-        <id>svjatoslav.eu</id>
-        <name>Svjatoslav repository</name>
-        <url>https://www3.svjatoslav.eu/maven/</url>
-    </repository>
-</repositories>
-#+END_SRC
-
-- Library requires Java 21 or newer.
-
-- Study the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/][demo applications]] for practical examples. Start with the
-  [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#minimal-example][minimal example]] to see the basic boilerplate needed to render a 3D
-  scene.
-
-- Study [[#understanding-3d-engine][how Aukio 3D engine works]].
-- Read online [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/][JavaDoc]].
-- See [[https://www3.svjatoslav.eu/projects/aukio-3d/graphs/][*Aukio 3D* class diagrams]]. (Diagrams were generated by using
-  [[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility)
-
-* Essential theory
-:PROPERTIES:
-:CUSTOM_ID: understanding-3d-engine
-:ID:       4b6c1355-0afe-40c6-86c3-14bf8a11a8d0
-:END:
-** Coordinate System (X, Y, Z)
-:PROPERTIES:
-:CUSTOM_ID: coordinate-system
-:END:
-
-#+INCLUDE: "Coordinate system.svg" export html
-
-*Aukio 3D* uses a **left-handed coordinate system with X pointing right
-and Y pointing down**, matching standard 2D screen coordinates. This
-coordinate system should feel intuitive for people with preexisting 2D
-graphics background.
-
-| Axis | Direction                          | Meaning                                   |
-|------+------------------------------------+-------------------------------------------|
-| X    | Horizontal, positive = RIGHT       | Objects with larger X appear to the right |
-| Y    | Vertical, positive = DOWN          | Lower Y = higher visually (up)            |
-| Z    | Depth, positive = away from viewer | Negative Z = closer to camera             |
-
-*Practical Examples*
-
-- A point at =(0, 0, 0)= is at the origin.
-- A point at =(100, 50, 200)= is: 100 units right, 50 units down
-  visually, 200 units away from the camera.
-- To place object A "above" object B, give A a **smaller Y value**
-  than B.
-
-Coordinates in this system are stored using the
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point3D.html][Point3D]] class — a mutable container with public =x=, =y=, =z= fields
-supporting vector operations like distance, rotation, and translation.
-Vertices (see [[#vertex][below]]) are positioned within this coordinate system.
-
-The [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#coordinate-system][aukio-3d-demos]] project includes an interactive
-coordinate system reference showing X, Y, Z axes as colored arrows
-with a grid plane for spatial context.
-
-** Point3D and Vertex
-:PROPERTIES:
-:CUSTOM_ID: vertex
-:END:
-
-#+INCLUDE: "Point3D vertex.svg" export html
-
-Every 3D object is built from *vertices* — corner points that define
-the shape's geometry. A triangle has 3 vertices, a cube has 8, and
-complex meshes have thousands. The engine uses two related classes to
-represent points in 3D space, each serving a different purpose.
-
-
-
-*** Point3D — Raw Coordinates
-
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point3D.html][Point3D]] is the fundamental coordinate type throughout the engine. It
-stores a position or vector with three public fields: =x=, =y=, =z=.
-The class provides vector math operations: distance calculation,
-rotation, translation, scaling, dot/cross products, and interpolation. Methods follow a fluent API convention where mutating
-operations (like =add=, =multiply=) return =this= for chaining, while
-non-mutating variants (like =withAdded=, =withMultiplied=) return new
-instances.
-
-Use =Point3D= for:
-- Storing positions, vectors, or any raw 3D coordinate
-- Distance and angle calculations between points
-- Vector math (dot product, cross product, normalization)
-- Rotating or translating positions before shape construction
-
-#+BEGIN_SRC java
-Point3D p1 = new Point3D(100, 50, 200);
-Point3D p2 = new Point3D(0, 0, 100);
-double distance = p1.getDistanceTo(p2);                  // Euclidean distance
-Point3D direction = p1.withSubtracted(p2).unit();        // New point: unit vector from p2 to p1
-p1.rotate(new Point3D(0,0,0), Math.PI/4, 0);             // Rotate p1 in place, 45° in XZ plane
-#+END_SRC
-
-*** Vertex — Rendering-Ready Coordinates
-
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] wraps a =Point3D= and adds the coordinate spaces needed during
-rendering. As a shape transforms through the render pipeline, each
-vertex tracks its position in multiple spaces:
-
-| Field                  | Purpose                                                |
-|------------------------+--------------------------------------------------------|
-| =coordinate=           | Original position in local/model space                 |
-| =transformedCoordinate(ctx)=  | Position relative to camera (after transform stack) |
-| =onScreenCoordinate(ctx)=     | 2D screen pixels (after perspective projection)     |
-| =textureCoordinate=    | Optional UV coords in pixel units (not normalized)     |
-| =normal=               | Optional normal vector for CSG polygon splitting       |
-
-=transformedCoordinate= and =onScreenCoordinate= are accessor methods,
-not plain fields: each vertex carries three slots for each, one per
-pipeline projection slot, and the accessor picks the slot of the
-context's current render pass. This is what lets the triple-buffered
-pipeline transform the next frame while previous frames are still
-being painted (see [[file:Rendering loop/index.org::#software-pipeline][software pipeline]]).
-
-During rendering, the vertex is transformed through all spaces: first
-applying the
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/TransformStack.html][TransformStack]] to get the camera-relative coordinate, then
-projecting to 2D. Results are cached per frame per slot to avoid
-recomputing for vertices shared across multiple shapes.
-
-Use =Vertex= when:
-- Constructing triangles, polygons, or textured shapes
-- Your geometry needs texture UV coordinates
-- You're performing CSG boolean operations (requires =normal=)
-
-#+BEGIN_SRC java
-// Create a textured triangle (texture coordinates use pixel units)
-// For a 256x256 texture: (0,0)=top-left, (256,256)=bottom-right
-Vertex v1 = new Vertex(new Point3D(0, 0, 100), new Point2D(0, 0));
-Vertex v2 = new Vertex(new Point3D(100, 0, 100), new Point2D(256, 0));
-Vertex v3 = new Vertex(new Point3D(50, 100, 100), new Point2D(128, 256));
-TexturedTriangle triangle = new TexturedTriangle(v1, v2, v3, texture);
-#+END_SRC
-
-*** When to Use Each
-
-| Use Point3D                          | Use Vertex                                    |
-|--------------------------------------+-----------------------------------------------|
-| Positioning shapes, cameras, lights  | Building triangles and polygons               |
-| Vector math (distances, directions)  | Texture-mapped geometry                      |
-| Rotating or translating positions    | CSG operations                                |
-| Temporary calculations               | Shapes that render through transform pipeline |
-
-For simple shapes without textures, you can pass raw =Point3D=
-coordinates directly to constructors — the shape will internally wrap
-them in =Vertex= objects. The [[#coordinate-system][coordinate system]] above defines the
-meaning of all =x=, =y=, =z= values in both classes.
-
-** Edge
-:PROPERTIES:
-:CUSTOM_ID: edge
-:END:
-
-#+INCLUDE: "Edge.svg" export html
-
-An *edge* is a straight line segment connecting two [[#vertex][vertices]]. Edges
-form the wireframe skeleton of a 3D model — the structural framework
-visible when surfaces are not rendered. A triangle has 3 edges, a cube
-has 12 edges, and complex meshes have thousands.
-
-In *Aukio 3D*, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class implements edges as renderable shapes. Each
-Line connects two [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] endpoints and stores two properties: a
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#width][width]] in world units (adjusted for perspective during rendering) and a
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#color][color]] with alpha transparency. The rendering algorithm switches
-between two modes based on the projected screen width: thin lines below
-the threshold are drawn as single pixels with alpha-adjusted coloring,
-while thicker lines are rendered as filled rectangles with perspective-correct
-edge fading using four [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineInterpolator.html][LineInterpolator]] scanline boundaries.
-
-Wireframe shapes are composite objects built from multiple Line instances.
-For example, [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.html][WireframeBox]] creates 12 Line objects — four edges parallel to
-each axis — using a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineAppearance.html][LineAppearance]] factory to ensure consistent styling across
-all edges. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.html][WireframeCube]] convenience subclass provides a center-point
-constructor. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual comparison of
-wireframe (edges only) versus solid polygon (surfaces with lighting)
-rendering modes.
-
-** Face (Triangle)
-:PROPERTIES:
-:CUSTOM_ID: face-triangle
-:END:
-
-#+INCLUDE: "Face triangle.svg" export html
-
-A *face* is a flat surface enclosed by edges — the visible skin of a 3D
-object. While faces can theoretically have any number of sides, 3D
-engines standardize on *triangles* because three points always define a
-flat plane. A quad (4 vertices) or pentagon (5 vertices) might be
-non-planar depending on vertex positions, causing rendering artifacts.
-Triangles avoid this problem entirely.
-
-*** SolidPolygon — Solid-Color Faces
-
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] is the primary face type, supporting any number of vertices
-(3 or more). Triangles render directly via scanline rasterization.
-N-vertex polygons (quads, pentagons, etc.) are triangulated using fan
-decomposition — a quad becomes 2 triangles, a pentagon 3 — but only
-when the polygon lives inside an
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]
-(the scene graph root is one): the composite triangulates while
-building its render list. A standalone SolidPolygon with more than 3
-vertices cannot be painted directly and throws IllegalStateException.
-
-Each SolidPolygon stores a single fill color with optional alpha
-transparency. When shading is enabled, the lighting manager computes
-the polygon's illumination once during the transform phase, then
-applies the shaded color during painting. Backface culling (see
-[[#winding-order-backface-culling][Winding Order & Backface Culling]]) can be enabled per-polygon, or
-applied recursively to an entire composite shape via
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] — this propagates the
-setting to all SolidPolygon and TexturedTriangle sub-shapes, including
-nested composites.
-
-#+BEGIN_SRC java
-// Create a red triangle
-SolidPolygon triangle = SolidPolygon.triangle(
-    new Point3D(0, 0, 100),
-    new Point3D(50, 0, 100),
-    new Point3D(25, 50, 100),
-    Color.RED
-);
-
-// Create a blue quad (internally triangulated)
-SolidPolygon quad = SolidPolygon.quad(
-    new Point3D(-50, -50, 100),
-    new Point3D(50, -50, 100),
-    new Point3D(50, 50, 100),
-    new Point3D(-50, 50, 100),
-    Color.BLUE
-);
-
-// Enable lighting and culling for a closed mesh
-quad.setShadingEnabled(true);
-quad.setBackfaceCulling(true);
-
-// Add to the scene — the root composite triangulates the quad
-viewPanel.getRootShapeCollection().addShape(quad);
-#+END_SRC
-
-*** TexturedTriangle — UV-Mapped Faces
-
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] renders faces with image textures mapped via UV
-coordinates. Each of the three [[#vertex][vertices]] stores a =textureCoordinate=
-(a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point2D.html][Point2D]] with U and V values in *pixel units* matching the texture
-dimensions). For a 256×256 texture, coordinates range from (0,0) at the
-top-left corner to (256,256) at the bottom-right. During rasterization, the
-engine interpolates these UV coordinates across the triangle's surface,
-sampling the texture at each pixel. When mipmaps are used, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html#multiplicationFactor][multiplicationFactor]]
-scales coordinates to match the selected mipmap resolution.
-
-The texture system supports mipmaps — pre-scaled versions of the texture
-selected based on the triangle's screen size to reduce aliasing artifacts
-on distant surfaces. Texture coordinates are mapped with
-perspective-correct interpolation inside the scanline rasterizer, so
-large triangles at steep angles render without distortion — see the
-[[file:Perspective correct textures/][perspective-correct textures]] page.
-
-#+BEGIN_SRC java
-// Create a 256x256 texture
-Texture texture = new Texture(256, 256, 2);  // width, height, maxUpscale
-
-// Create a textured triangle with UV coordinates in pixel units
-Vertex v1 = new Vertex(new Point3D(0, 0, 100), new Point2D(0, 0));      // top-left
-Vertex v2 = new Vertex(new Point3D(100, 0, 100), new Point2D(256, 0));  // top-right
-Vertex v3 = new Vertex(new Point3D(50, 100, 100), new Point2D(128, 256)); // bottom-center
-
-TexturedTriangle triangle = new TexturedTriangle(v1, v2, v3, texture);
-triangle.setBackfaceCulling(true);
-#+END_SRC
-
-Both SolidPolygon and TexturedTriangle extend
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]], which handles vertex transformation and depth
-sorting. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual comparison of solid
-versus textured polygon rendering.
-
-** Normal Vector
-:PROPERTIES:
-:CUSTOM_ID: normal-vector
-:END:
-
-#+INCLUDE: "Normal vector.svg" export html
-
-A *normal* is a vector perpendicular to a surface. It tells the
-renderer which direction a face is pointing. Normals are critical for
-*lighting* — the angle between the light direction and the normal
-determines how bright a surface appears.
-
-**Use cases:**
-
-| Use case             | API                                          | Computation                | Location          |
-|----------------------+----------------------------------------------+----------------------------+-------------------|
-| BSP/CSG operations   | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#getPlane()][SolidPolygon.getPlane()]] → [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#normal][Plane.normal]]       | Lazy-cached once           | =Plane=           |
-| Per-frame shading    | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]] → =cachedNormal= field | Recomputed every frame     | =SolidPolygon=    |
-| Lighting calculation | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html#computeLighting()][LightingManager.computeLighting()]]            | Uses normal via =dot(L,N)= | =LightingManager= |
-
-**Implementation notes:**
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]]: shared zero-allocation helper for computing normals from three points
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Plane]]: stores normals in Hesse normal form (normal + distance) for BSP spatial partitioning
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html#normal][Vertex.normal]]: optional field for CSG polygon splitting (not used for rendering)
-
-** Mesh
-:PROPERTIES:
-:CUSTOM_ID: mesh
-:END:
-
-#+INCLUDE: "Mesh.svg" export html
-
-A *mesh* is a collection of vertices, edges, and faces that together
-define the shape of a 3D object. Even curved surfaces like spheres are
-approximated by many small triangles — more triangles means a smoother
-appearance. A cube has 8 vertices forming 12 triangular faces, while a
-smooth sphere requires hundreds or thousands of triangles depending on
-the desired quality.
-
-In *Aukio 3D*, meshes are built through composition rather than
-monolithic vertex/index buffers. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]] class is
-the foundation for primitive shapes — each instance stores its own
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html#vertices][List&lt;Vertex&gt;]] directly. This includes [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] (N-vertex
-convex polygons, not limited to triangles), [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]]
-(UV-mapped triangles), and [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] (wireframe edges). The
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] class groups multiple shapes into a single
-object with its own position, rotation, and transform — useful for
-complex models that move or rotate together.
-
-Complex meshes are constructed procedurally by adding primitive shapes
-during initialization. For example, [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.html][SolidPolygonSphere]] generates
-triangles using a latitude-longitude grid: with 16 segments, it
-creates 960 SolidPolygon triangles by looping through
-rings and sectors, calling [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#addShape(eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape)][addShape()]] for each. The generic
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.html][SolidPolygonMesh]] accepts any list of triangles, allowing custom
-geometry from procedural generation or external sources.
-
-During rendering, several automatic optimizations occur. N-vertex
-polygons (quads, pentagons, etc.) are triangulated using fan
-triangulation inside the composite's render-list builder, converting
-an N-vertex polygon into N-2 triangles. Textured triangles render with
-perspective-correct texture mapping (see [[file:Perspective correct textures/][perspective-correct textures]]).
-Composites perform view frustum culling to skip rendering when entirely
-off-screen. Sub-shapes can be organized into named groups via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/SubShape.html][SubShape]]
-wrappers, allowing [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#showGroup(java.lang.String)][showGroup()]] and [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#hideGroup(java.lang.String)][hideGroup()]] to toggle visibility of
-entire sections. Composite shapes also support CSG boolean operations
-— see the [[file:CSG/][Constructive Solid Geometry]] documentation for union,
-subtract, and intersect operations.
-
-The [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] showcases all primitive shapes available in
-*Aukio 3D*, rendered in both wireframe mode (edges only via
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.html][WireframeBox]] and similar) and solid polygon mode (filled surfaces with
-dynamic lighting).
-
-** Working with Colors
-:PROPERTIES:
-:CUSTOM_ID: working-with-colors
-:ID:       f2c9642a-a093-444f-8992-76c97ff28c16
-:END:
-
-Aukio 3D uses its own [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html][Color class]] instead of [[https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/Color.html][java.awt.Color]]. This
-custom implementation is designed specifically for the engine's
-software rasterizer, where avoiding object allocation during rendering
-is critical for performance. When rendering thousands of polygons per
-frame, creating new Color instances for each one would generate
-excessive garbage and trigger frequent garbage collection
-pauses. Instead, the engine's Color class uses mutable fields that can
-be reused across frames.
-
-The class stores RGBA components as public integer fields in the range
-0–255. This format matches the engine's pixel buffer layout and avoids
-costly float-to-int conversions during rasterization. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#r][r]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#g][g]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#b][b]], and
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#a][a]] fields are accessible directly, allowing lighting calculations and
-alpha blending to modify colors in-place without allocating new
-objects. For example, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] class maintains a reusable
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][shadedColor]] field that gets updated during each frame's lighting
-calculation instead of creating a new Color instance per polygon.
-
-Color provides several constructors for different input formats. The
-most common approach is using hex strings via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#hex(java.lang.String)][Color.hex(String)]] or the
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(java.lang.String)][String constructor]], which support formats like ="F80"= (3-digit RGB),
-="FF8800"= (6-digit RGB), ="F808"= (4-digit RGBA), and ="FF8800CC"=
-(8-digit RGBA). You can also create colors from integer RGBA
-components (0–255) using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(int,int,int,int)][new Color(r, g, b, a)]], from floating-point
-components (0.0–1.0) via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(double,double,double,double)][new Color(double r, double g, double b,
-double a)]], or from a packed RGB integer like =0xFF8800= using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(int)][new
-Color(int rgb)]]. The class also provides predefined constants for
-common colors: [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#RED][Color.RED]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#GREEN][Color.GREEN]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#BLUE][Color.BLUE]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#YELLOW][Color.YELLOW]],
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#CYAN][Color.CYAN]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#MAGENTA][Color.MAGENTA]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#WHITE][Color.WHITE]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#BLACK][Color.BLACK]], and
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#TRANSPARENT][Color.TRANSPARENT]].
-
-The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#set(int,int,int,int)][set(int r, int g, int b, int a)]] method modifies a Color in-place
-and returns =this= for method chaining, which is essential for
-performance during rendering. For example, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]]
-calculates lighting contributions from all light sources and stores
-the final shaded color directly into a reusable Color instance via
-=set()=, avoiding any allocation. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#toAwtColor()][toAwtColor()]] method converts a
-Aukio 3D Color to a java.awt.Color when needed for Java2D graphics
-operations, caching the result to avoid repeated conversion. The
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#toInt()][toInt()]] method packs the color into an ARGB integer suitable for the
-engine's pixel buffer, used during rasterization to write pixels
-directly.
-
-#+BEGIN_SRC java
-import eu.svjatoslav.aukio.e3d.renderer.raster.Color;
-import static eu.svjatoslav.aukio.e3d.renderer.raster.Color.hex;
-
-// Using predefined color constants
-Color red = Color.RED;
-Color transparent = Color.TRANSPARENT;
-
-// Create from hex string (recommended for clarity)
-Color orange = hex("FF8800");           // RGB, fully opaque
-Color semiTransparent = hex("FF880080"); // RGBA, 50% transparent
-
-// Create from integer components (0-255)
-Color custom = new Color(255, 128, 64, 200);
-
-// Create from packed RGB integer
-Color packed = new Color(0xFF8800);
-
-// Modify existing color in-place (no allocation)
-Color reusable = new Color();
-reusable.set(100, 200, 50, 255);
-
-// Convert to AWT color for Java2D operations
-java.awt.Color awtColor = custom.toAwtColor();
-
-// Use in lighting calculations (LightingManager modifies in-place)
-// See the Shading & Lighting documentation for details
-#+END_SRC
-
-The alpha component controls transparency during rendering. A value of
-0 makes the color fully transparent, while 255 makes it fully
-opaque. The rasterizer implements alpha blending during the paint
-phase: when drawing a semi-transparent pixel, the engine blends the
-source color with the existing background pixel proportionally based
-on the alpha value. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html#drawPixel(int,int\[\],int)][TextureBitmap.drawPixel()]] method handles this
-blending, multiplying source colors by alpha and background colors by
-=(255 - alpha)=, then combining them. You can test whether a color is
-fully transparent using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#isTransparent()][isTransparent()]], which returns true when alpha
-equals zero.
-
-For lighting calculations, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]] class uses Color to
-represent the color and intensity of emitted light. Multiple light
-sources contribute to the final shaded color of each polygon, as
-described in the [[file:Shading/index.org::#shading-lighting][Shading & Lighting]] documentation. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html#setAmbientLight(eu.svjatoslav.aukio.e3d.renderer.raster.Color)][ambient light]]
-provides base illumination that affects all surfaces equally,
-regardless of orientation. Colors are also used for wireframe
-rendering via the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class, where the color field determines the
-line's appearance.
-
-** Shading & Lighting
-
-#+attr_html: :width 600px
-#+attr_latex: :width 600px
-[[file:Shading/Shaded%20sphere.png]]
-
-
-*Aukio 3D* implements *flat shading* — one normal per polygon,
-computed from the first three vertices. Each polygon receives a single
-color based on its orientation relative to light sources.
-
-To understand lighting and shading, read more about [[file:Shading/][shading & lighting]].
-
-* 3D engine internals
-** Main render loop
-
-The rendering loop is the heart of the engine, continuously generating
-frames at a target rate (typically 60 FPS). Each frame transforms 3D
-shapes through a multi-stage pipeline before displaying them on screen.
-
-#+INCLUDE: "Rendering loop/Render pipeline.svg" export html
-
-The render loop runs on a dedicated background daemon thread managed by
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html][ViewPanel]], which can optionally sleep between frames to maintain a
-target FPS or run unlimited for benchmarking.
-
-For a detailed walkthrough of each phase with diagrams and code
-examples, see the dedicated page: [[file:Rendering loop/][Rendering loop]].
-
-** Near-Plane Clipping
-:PROPERTIES:
-:CUSTOM_ID: near-plane-clipping
-:END:
-
-Individual polygons that straddle the camera's near plane are not
-dropped wholesale: the vertex loop is clipped against the plane, new
-intersection vertices are generated with 3D-interpolated UVs and
-normals, and the clipped polygon — a triangle can become a quad,
-painted as a triangle fan — renders normally. Only polygons entirely
-behind the near plane are culled. This keeps floor and wall tiles
-visible when the camera brushes against them.
-
-#+INCLUDE: "Near plane clip/Near plane straddle.svg" export html
-
-Read more about [[file:Near plane clip/][near-plane clipping]].
-
-** Depth buffer
-:PROPERTIES:
-:CUSTOM_ID: depth-buffer
-:END:
-
-Visibility is resolved per pixel by a depth buffer: every triangle
-interpolates =1/z= across its spans and wins a pixel only where it is
-nearer than the surface already there. Opaque geometry — textured
-triangles, solid polygons — paints front-to-back with depth writes;
-translucent geometry paints back-to-front with depth tests but no
-writes, so it never occludes. Lines and billboards stay painter-ordered
-overlays by design.
-
-See [[file:Depth%20buffer/][Depth buffer]] for the full treatment.
-
-** Frustum & View Frustum Culling
-
-*Aukio 3D* implements view frustum culling.
-
-#+INCLUDE: "Frustum culling/Frustum diagram.svg" export html
-
-To understand frustum culling and object-level visibility
-optimization, read more about [[file:Frustum culling/][frustum & view frustum culling.]]
-
-** Winding Order & Backface Culling
-:PROPERTIES:
-:CUSTOM_ID: winding-order-backface-culling
-:END:
-
-#+INCLUDE: "Winding order.svg" export html
-
-The order in which a triangle's vertices are listed determines its
-*winding order*. In *Aukio 3D*, screen coordinates have Y-axis pointing
-*down*, which inverts the apparent winding direction compared to
-standard mathematical convention (Y-up). *Counter-clockwise (CCW)* in
-screen space means front-facing. *Backface culling* skips rendering
-triangles that face away from the camera — a major performance
-optimization.
-
-- CCW winding (in screen space) → front face (visible)
-- CW winding (in screen space) → back face (culled)
-- When viewing a polygon from outside: define vertices in *counter-clockwise* order as seen from the camera
-- Saves ~50% of triangle rendering
-- Implementation uses signed area: =signedArea < 0= means front-facing
-  (in Y-down screen coordinates, negative signed area corresponds to
-  visually CCW winding)
-
-In *Aukio 3D*, backface culling is *optional* and disabled by default. Enable it per-shape:
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#setBackfaceCulling(boolean)][SolidPolygon.setBackfaceCulling(true)]]
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html#setBackfaceCulling(boolean)][TexturedTriangle.setBackfaceCulling(true)]]
-- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] (applies to all
-  sub-shapes)
-
-See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#winding-order][Winding Order demo]] for an interactive visualization.
-
-** Perspective correct textures
-
-#+attr_html: :class responsive-img
-#+attr_latex: :width 1000px
-[[file:Perspective correct textures/Affine distortion.png]]
-
-*Aukio 3D* tries to do perspective-correct texture rendering. Read more
-about [[file:Perspective correct textures/][perspective-correct texture implementation]].
-
-* Developer tools
-:PROPERTIES:
-:CUSTOM_ID: developer-tools
-:ID:       8c5e2a1f-9d3b-4f6a-b8e7-1c4d5f7a9b2e
-:END:
-
-Press *F12* anywhere in the application to open the Developer Tools
-panel:
-
-#+attr_html: :class responsive-img
-#+attr_latex: :width 1000px
-[[file:Developer tools/Developer tools.png]]
-
-This debugging interface helps you understand what the engine is doing
-internally and diagnose rendering issues. Pressing F12 again closes
-the panel.
-
-** Diagnostic toggles
-:PROPERTIES:
-:CUSTOM_ID: diagnostic-toggles
-:END:
-
-*** Show polygon borders
-:PROPERTIES:
-:CUSTOM_ID: show-polygon-borders
-:END:
-
-When enabled, each [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] draws yellow outlines around its
-three edges after rendering its texture content. This overlays the
-triangle mesh onto the final image:
-
-#+attr_html: :class responsive-img
-[[file:Developer tools/Render polygon borders.png]]
-
-Use this visualization when investigating:
-
-- Mesh structure: see the actual triangles as the rasterizer receives
-  them
-- Geometry bugs: spot T-junction gaps and overlapping geometry
-- Texture distortion: compare triangle shapes against visible warping
-
-*** Render alternate segments (overdraw debug)
-:PROPERTIES:
-:CUSTOM_ID: render-alternate-segments
-:END:
-
-Renders only even-numbered paint tiles while leaving odd-numbered ones
-black. (The screen is divided into a grid of rectangular tiles for
-parallel rendering — see [[file:Rendering loop/index.org::#phase-4-clear-paint-tiles][the rendering loop documentation]].
-"Segments" is the older name for tiles.)
-
-#+attr_html: :class responsive-img
-[[file:Developer tools/Render alternative segments.png]]
-
-This toggle helps detect overdraw: threads writing outside their
-allocated tile. If you see rendering artifacts in the black tiles, a
-paint task is writing pixels outside its assigned area — a clear sign
-of a bug.
-
-*** Show segment boundaries
-:PROPERTIES:
-:CUSTOM_ID: show-segment-boundaries
-:END:
-
-Draws red lines along the paint tile boundaries, making it easy to see
-exactly where each tile's rendered area begins and ends. In stereo
-mode each eye's viewport gets its own grid:
-
-#+attr_html: :class responsive-img
-[[file:Developer tools/Show segment boundaries.png]]
-
-Useful for:
-
-- Verifying the tile grid division
-- Debugging tile-boundary rendering issues (e.g. clipped text or
-  missing slivers at tile edges)
-- Understanding the parallel rendering architecture visually
-
-** Camera position
-:PROPERTIES:
-:CUSTOM_ID: camera-position
-:END:
-
-Displays the current camera coordinates and orientation in real-time:
-
-| Parameter | Description                              |
-|-----------+------------------------------------------|
-| x, y, z   | Camera position in 3D world space        |
-| yaw       | Rotation around the Y axis (left/right)  |
-| pitch     | Rotation around the X axis (up/down)     |
-| roll      | Rotation around the Z axis (tilt)        |
-
-The *Copy* button copies the full camera position string to the
-clipboard in a format ready to paste into bug reports or configuration
-files.
-
-Use this for:
-- Reporting exact camera positions when filing bugs
-- Saving interesting viewpoints for later reference
-- Understanding camera movement during navigation
-- Sharing specific views with other developers
-
-Example copied format:
-#+BEGIN_EXAMPLE
-500.00, -300.00, -800.00, 0.60, -0.50, -0.00
-#+END_EXAMPLE
-
-The six numbers map 1:1 onto
-[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Transform.html#set(double,double,double,double,double,double)][Transform.set(x, y, z, yaw, pitch, roll)]],
-so a copied viewpoint can be restored at startup — this is how the
-demo applications freeze a good camera position into code:
-
-#+BEGIN_SRC java
-// Camera position captured via Developer Tools -> Copy
-viewPanel.getCamera().getTransform().set(
-        130.66, -65.49, -248.18,   // x, y, z
-        -0.06, -0.36, -0.00);      // yaw, pitch, roll
-#+END_SRC
-
-** Frustum culling statistics
-:PROPERTIES:
-:CUSTOM_ID: frustum-culling-statistics
-:END:
-
-Shows real-time statistics about composite shape frustum culling
-efficiency (see the dedicated [[file:Frustum culling/][frustum culling]] page for how
-culling itself works):
-
-| Statistic | Description                                              |
-|-----------+----------------------------------------------------------|
-| Total     | Number of composite shapes tested against the frustum    |
-| Culled    | Number of composites rejected (outside view frustum)     |
-| Culled %  | Percentage of composites that were culled (0-100%)       |
-
-*How to interpret the numbers:*
-
-- *High cull % (60-90%)*: Excellent — most objects are being correctly culled
-- *Medium cull % (20-60%)*: Moderate — some optimization benefit
-- *Low cull % (0-20%)*: Limited benefit — either all objects are visible, or scene needs restructuring
-
-*Example:*
-#+BEGIN_EXAMPLE
-Total: 473  Culled: 425 (89.9%)
-#+END_EXAMPLE
-
-This means 473 composite shapes were tested, 425 were outside the view
-and skipped entirely, and only 48 composites (with all their children)
-actually needed to be rendered. This is excellent culling efficiency.
-
-The statistics update every 200ms while the panel is open. Note that
-the root composite is never frustum-tested (it's always rendered), so
-the "Total" count excludes it.
-
-** Render threads
-:PROPERTIES:
-:CUSTOM_ID: render-threads
-:END:
-
-Shows the number of active render threads versus available CPU cores.
-The engine defaults to 75% of available threads (at most cores − 1, so
-one thread always stays free for the rest of the system). The count is
-changeable at runtime via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#setNumRenderThreads(int)][ViewPanel.setNumRenderThreads(int)]];
-the worker pool is recreated lazily on the next frame.
-
-** Frame rate
-:PROPERTIES:
-:CUSTOM_ID: frame-rate
-:END:
-
-Shows the current target FPS and the measured production rate (frames
-completed per second, averaged over a ~500 ms window). The measured
-number counts produced frames regardless of how quickly the display
-path presents them — see [[file:Rendering loop/index.org::#frame-rate-control][frame rate control]].
-
-The *Unlock FPS* toggle switches to unlimited (benchmark) mode: the
-engine renders continuously as fast as possible, even when the scene
-is static. Toggling off restores the previously locked target rate.
-
-** Thread activity timeline
-:PROPERTIES:
-:CUSTOM_ID: thread-activity-timeline
-:END:
-
-A per-thread occupancy view — the software-renderer equivalent of a
-GPU frame profiler. Each thread gets a row (the render thread and
-present thread on top, then one row per worker), time runs along the X
-axis, and each colored block is one recorded work interval. Idle time
-is black.
-
-#+attr_html: :class responsive-img
-[[file:Developer tools/Thread timeline.png]]
-
-Press *Record* to start capturing. The colors encode both the task
-kind and which frame the task belongs to — transform, paint, and
-binning come in three frame-parity variants (f0/f1/f2), so you can see
-up to three frames in flight simultaneously. Additional colors mark
-render-thread orchestration, blocked time, blits, and the sort/drain
-sub-phases.
-
-Navigation: mouse wheel scrolls, Ctrl+wheel zooms, and a scrollbar
-moves along the captured range.
-
-The legend colors, exactly as the timeline paints them:
-
-| Color | Legend label | What it shows |
-|-------+--------------+---------------|
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#2ECC40;border:1px solid #444;vertical-align:middle"></span>@@ =#2ECC40= | =transform f0= | Vertex transform chunk task, frame slot 0 |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#B8D900;border:1px solid #444;vertical-align:middle"></span>@@ =#B8D900= | =transform f1= | Vertex transform chunk task, frame slot 1 |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#6B8E23;border:1px solid #444;vertical-align:middle"></span>@@ =#6B8E23= | =transform f2= | Vertex transform chunk task, frame slot 2 |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#0074D9;border:1px solid #444;vertical-align:middle"></span>@@ =#0074D9= | =paint f0= | Paint tile task (clear + rasterize one tile), frame slot 0 |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#F012BE;border:1px solid #444;vertical-align:middle"></span>@@ =#F012BE= | =paint f1= | Paint tile task, frame slot 1 |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#B10DC9;border:1px solid #444;vertical-align:middle"></span>@@ =#B10DC9= | =paint f2= | Paint tile task, frame slot 2 |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#39CCCC;border:1px solid #444;vertical-align:middle"></span>@@ =#39CCCC= | =bin f0= | Tile binning task (assign sorted shapes to tiles), frame slot 0 |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#008B8B;border:1px solid #444;vertical-align:middle"></span>@@ =#008B8B= | =bin f1= | Tile binning task, frame slot 1 |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#007070;border:1px solid #444;vertical-align:middle"></span>@@ =#007070= | =bin f2= | Tile binning task, frame slot 2 |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#A0A0A0;border:1px solid #444;vertical-align:middle"></span>@@ =#A0A0A0= | =render serial= | Render thread orchestration: tree walk and pass submission |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#8B0000;border:1px solid #444;vertical-align:middle"></span>@@ =#8B0000= | =blocked= | Render thread waiting for an older paint pass or the present gate |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#FFFFFF;border:1px solid #444;vertical-align:middle"></span>@@ =#FFFFFF= | =blit= | Present thread copying a finished frame to the screen |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#FF851B;border:1px solid #444;vertical-align:middle"></span>@@ =#FF851B= | =sort+bin= | A pass's async continuation as a whole: drain, sort, bin, submit paint |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#8B4513;border:1px solid #444;vertical-align:middle"></span>@@ =#8B4513= | =drain= | Continuation sub-phase: await and merge parallel transform chunks |
-| @@html:<span style="display:inline-block;width:2.2em;height:0.9em;background:#FFD700;border:1px solid #444;vertical-align:middle"></span>@@ =#FFD700= | =sort= | Continuation sub-phase: depth sort of the pass's shapes |
-
-The f0/f1/f2 suffixes are the frame's projection slot (frame number
-modulo 3) — the triple-buffering from the [[file:Rendering loop/index.org::#software-pipeline][software pipeline]].
-When the pipeline is healthy you see interleaved colors from two or
-three frames on the worker rows at once: paint tasks of an older frame
-overlapping transform and binning of the newer one. Wide =blocked=
-spans on the render row, or worker rows with black gaps, mean the
-pipeline is starved rather than busy.
-
-Recording is cheap but not free (~100 ns per task; a single volatile
-read when disabled), so leave it off during benchmarking runs. The
-captured intervals live in a fixed-size ring buffer — long recordings
-keep only the most recent history.
-
-What to look for:
-
-- *Solidly packed worker rows* mean the pipeline is keeping all cores
-  busy — the design goal (see [[file:Rendering loop/index.org::#software-pipeline][software pipeline]])
-- *Long "blocked" spans on the render row* mean the render thread is
-  waiting for paint passes — workers are the bottleneck
-- *Long "blit" spans on the present row* mean the display path
-  (X server) is the bottleneck; excess frames are being dropped from
-  the presentation mailbox
-
-** Live log viewer
-:PROPERTIES:
-:CUSTOM_ID: live-log-viewer
-:END:
-
-The scrollable text area shows captured debug output in real-time:
-- Green text on black background for readability
-- Auto-scrolls to show latest entries
-- Updates every 200ms while panel is open
-- Captures logs even when panel is closed (replays when reopened)
-
-Use the *Clear Logs* button to reset the log buffer for fresh
-diagnostic captures.
-
-** Headless & agentic tooling
-:PROPERTIES:
-:CUSTOM_ID: headless-agentic
-:END:
-
-For windowless rendering, pixel assertions, golden-image regression
-tests and scene dumps — built for automated verification and AI agents —
-see [[#agentic-development][agentic development tools]].
-
-* Agentic development
-:PROPERTIES:
-:CUSTOM_ID: agentic-development
-:END:
-
-*Aukio 3D* provides good support for automated AI coding agents
-(for example OpenCode, Hermes Agent, etc..).
-
-Thanks to facilities is =eu.svjatoslav.aukio.e3d.headless= package, an
-AI agent can render any scene from any pose, assert what got painted,
-compare against committed reference images, and dump the full scene
-state for a bug report — all without a window, a display, or the
-render thread.
-
-** One pipeline, two drivers
-:PROPERTIES:
-:CUSTOM_ID: one-pipeline
-:END:
-
-The key design decision: the headless path drives *the very same
-transform &rarr; sort &rarr; paint pipeline* the on-screen ViewPanel
-uses. Nothing is reimplemented, so a passing headless test proves the
-real render works, and a bug reproduced headlessly is the real bug.
-
-#+INCLUDE: "Agentic development/Headless lanes.svg" export html
-
-Making this possible required three small engine changes:
-
-- ~ShapeCollection.transformShapes(Camera, RenderingContext)~ — the
-  transform phase now accepts a camera directly; the ViewPanel variant
-  just forwards its camera. Headless code never touches Swing.
-- ~RenderingContext.getImage()~ — hands out the backing BufferedImage
-  the rasterizer paints into.
-- ~GlobalIllumination.isRunning()~ / ~isConverged()~ /
-  ~getWorkItemCount()~ — GI state became inspectable.
-
-** Snapshot: render without a window
-:PROPERTIES:
-:CUSTOM_ID: snapshot
-:END:
-
-#+BEGIN_SRC java
-import eu.svjatoslav.aukio.e3d.headless.Snapshot;
-
-ShapeCollection scene = new ShapeCollection();
-scene.addShape(myShape);
-LightingManager lighting = new LightingManager();
-lighting.setAmbientLight(Color.hex("181818"));
-
-// One call: build context, transform, sort, paint, return the image.
-BufferedImage image = Snapshot.render(scene, lighting,
-        "290.31, -35.59, -2.10, -0.58, -0.15, 0.0", 640, 480);
-Snapshot.save(image, "/tmp/snapshot.png");
-#+END_SRC
-
-The pose string is the *same "x, y, z, yaw, pitch, roll" format the
-demos print* and users quote in bug reports — paste the pose, reproduce
-the exact view. ~Snapshot.cameraFromPose()~ and ~Snapshot.poseString()~
-convert in both directions.
-
-For tests that need to detect *unpainted* pixels (holes), the
-~renderInto()~ variant fills the background with a caller-chosen
-sentinel color first, so "nothing was painted here" is unambiguous even
-in a pitch-black scene:
-
-#+BEGIN_SRC java
-RenderingContext ctx = new RenderingContext(640, 480, 1);
-ctx.lightingManager = lighting;
-Snapshot.renderInto(scene, camera, ctx, 0x00010203); // sentinel
-#+END_SRC
-
-A real headless render — the House demo from a bug-report pose:
-
-#+attr_html: :class responsive-img
-#+attr_latex: :width 640px
-[[file:Agentic development/snapshot-example.png]]
-
-** PixelAssertions: did this region get painted?
-:PROPERTIES:
-:CUSTOM_ID: pixel-assertions
-:END:
-
-The recurring debugging question — "did the floor actually render, or
-did clipping eat it?" — becomes a library call:
-
-#+BEGIN_SRC java
-import eu.svjatoslav.aukio.e3d.headless.PixelAssertions;
-
-// Fraction of a relative rectangle still equal to the background:
-double holes = PixelAssertions.unpaintedFraction(image, 0,
-        0.15, 0.45, 0.85, 1.0);   // lower-center band
-if (holes > 0.05)
-    throw new AssertionError("floor has holes: " + holes);
-
-long red = PixelAssertions.countColor(image, 0xFF0000);   // flat-color tests
-String grid = PixelAssertions.dumpPixelGrid(image, 320, 240, 3, 8); // hex dump
-#+END_SRC
-
-#+INCLUDE: "Agentic development/Pixel assertion.svg" export html
-
-** GoldenImage: compare against a reference
-:PROPERTIES:
-:CUSTOM_ID: golden-image
-:END:
-
-A pixel counts as different when any RGB channel drifts more than a
-per-channel tolerance; the comparison fails when the fraction of
-differing pixels exceeds a threshold. Deterministic flat-shaded renders
-can use tight tolerances (4, 0.005); noisier paths relax them.
-
-#+BEGIN_SRC java
-import eu.svjatoslav.aukio.e3d.headless.GoldenImage;
-
-GoldenImage.Result r = GoldenImage.compare(actual,
-        new File("goldens/house-flat.png"), 4, 0.005);
-if (!r.passed)
-    GoldenImage.saveDiff(actual, goldenFile, "/tmp/diff.png"); // red = differs
-#+END_SRC
-
-#+INCLUDE: "Agentic development/Golden workflow.svg" export html
-
-There is also a CLI for shell scripts — exit 0 = match, 1 = differ:
-
-#+BEGIN_SRC bash
-java eu.svjatoslav.aukio.e3d.headless.GoldenImage actual.png golden.png 4 0.005
-#+END_SRC
-
-A real diff: the house rendered with the living-room lamp removed,
-compared against the golden. The red region is exactly the room that
-lost its light:
-
-#+attr_html: :class responsive-img
-#+attr_latex: :width 640px
-[[file:Agentic development/diff-example.png]]
-
-** SceneDump: the reproducible bug report
-:PROPERTIES:
-:CUSTOM_ID: scene-dump
-:END:
-
-One call produces everything needed to reproduce what a frame shows:
-
-#+BEGIN_SRC java
-System.out.println(SceneDump.dump(scene, lighting, camera, gi));
-#+END_SRC
-
-#+BEGIN_EXAMPLE
-== SceneDump ==
-shapes: 5 top-level, 546 queued for rendering
-lights: 4 (ambient #181818)
-  [0] pos=(-800.0, -240.0, 0.0) color=FFD890 intensity=6.0
-  [1] pos=(0.0, -240.0, 0.0) color=D8E4FF intensity=5.0
-  [2] pos=(800.0, -240.0, 0.0) color=FFB060 intensity=6.0
-  [3] pos=(250.0, -60.0, -250.0) color=60FF90 intensity=2.0
-camera: 290.31, -35.59, -2.10, -0.58, -0.15, -0.00
-GI: running, 152034 work items, converged
-#+END_EXAMPLE
-
-The camera line is a pose string — it feeds straight back into
-~Snapshot.render()~.
-
-** HouseGoldens: ready-made regression tests
-:PROPERTIES:
-:CUSTOM_ID: house-goldens
-:END:
-
-The aukio-3d-demos repo contains a working example of all of the above:
-~eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens~ renders the
-House demo at two poses (the default view and the near-plane straddle
-bug pose), compares both against committed goldens, and independently
-asserts the floor has no holes.
-
-#+BEGIN_SRC bash
-cd aukio-3d-demos
-mvn clean package
-mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt
-java -cp "target/classes:$(cat cp.txt)" \
-  eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens           # verify
-java -cp "target/classes:$(cat cp.txt)" \
-  eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens --update  # regenerate goldens
-#+END_SRC
-
-Exit code 0 = all pass, 1 = any mismatch (with a diff PNG in /tmp).
-Demos that want the same treatment expose their scene construction:
-~HouseDemo.buildHouse()~, ~addFurniture()~ and ~addLights()~ are public
-for exactly this reason.
-
-** export-docs.sh: regenerate the documentation
-:PROPERTIES:
-:CUSTOM_ID: export-docs
-:END:
-
-All engine documentation (the pages you are reading) lives as org-mode
-files under =doc/=. One script exports every page to HTML with the
-darksun theme:
-
-#+BEGIN_SRC bash
-doc/export-docs.sh            # export all pages
-doc/export-docs.sh --check    # + render every page with headless Chrome
-                              #   to /tmp/doc-check-*.png for visual review
-#+END_SRC
-
-The =--check= mode is how an agent verifies its own documentation: SVG
-label collisions, broken image links and table breakage all show up in
-the rendered screenshots.
-
-** A typical agent session
-:PROPERTIES:
-:CUSTOM_ID: typical-session
-:END:
-
-#+BEGIN_EXAMPLE
-1. Reproduce:   Snapshot.render(scene, lighting, bugReportPose, 640, 480)
-2. Inspect:     SceneDump.dump(...) + view the PNG
-3. Fix the engine
-4. Verify:      PixelAssertions.unpaintedFraction(...) == 0
-5. Regression:  HouseGoldens  (must stay ALL PASS)
-6. Document:    edit doc pages, export-docs.sh --check, review shots
-#+END_EXAMPLE
-
-** Related Classes
-:PROPERTIES:
-:CUSTOM_ID: related-classes
-:END:
-
-| Class           | Purpose                                            |
-|-----------------+----------------------------------------------------|
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/Snapshot.html][Snapshot]]         | Windowless render facade + pose string conversion  |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/PixelAssertions.html][PixelAssertions]]  | Painted-region / color-count / hex-grid assertions |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/GoldenImage.html][GoldenImage]]      | Golden-PNG comparison, diff writer, CLI            |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/SceneDump.html][SceneDump]]        | Scene state as a reproducible text block           |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.html][ShapeCollection]]  | ~transformShapes(Camera, ...)~ headless overload   |
-| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] | ~getImage()~ exposes the painted frame             |
-
-* Source code
-:PROPERTIES:
-:CUSTOM_ID: source-code
-:ID:       978b7ea2-e246-45d0-be76-4d561308e9f3
-:END:
-
-*This program is free software: released under Creative Commons Zero
-(CC0) license*
-
-*Program author:*
-- Svjatoslav Agejenko
-- Homepage: https://svjatoslav.eu
-- Email: mailto://svjatoslav@svjatoslav.eu
-- See also: [[https://www.svjatoslav.eu/projects/][Other software projects hosted at svjatoslav.eu]]
-
-*Getting the source code:*
-- [[https://www2.svjatoslav.eu/gitweb/?p=aukio-3d.git;a=snapshot;h=HEAD;sf=tgz][Download latest source code snapshot in TAR GZ format]]
-- [[https://www2.svjatoslav.eu/gitweb/?p=aukio-3d.git;a=summary][Browse Git repository online]]
-- Clone Git repository using command:
-  : git clone https://www3.svjatoslav.eu/git/aukio-3d.git
diff --git a/doc/style.css b/doc/style.css
deleted file mode 100644 (file)
index 3403e0e..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-.flex-center {
-  display: flex;
-  justify-content: center;
-}
-
-.flex-center video {
-  width: min(90%, 1000px);
-  height: auto;
-}
-
-.responsive-img {
-  width: min(100%, 1000px);
-  height: auto;
-}
-
-/* === SVG diagram theme === */
-svg > rect:first-child {
-  fill: #061018;
-}
-
-svg text[fill="#666"],
-svg text[fill="#999"] {
-  fill: #aaa !important;
-}
-
-svg line[stroke="#ccc"] {
-  stroke: #445566 !important;
-}
-
-svg {
-  background-color: #061018;
-  border-radius: 8px;
-  display: block;
-  margin: 0 auto;
-}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 3cfe059..2eefa3c 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>eu.svjatoslav</groupId>
     <artifactId>aukio-3d</artifactId>
-    <version>1.5-SNAPSHOT</version>
+    <version>1.0.0-SNAPSHOT</version>
     <name>Aukio 3D</name>
     <description>3D engine</description>