- A text editor component rendered in 3D space
- Human input device (HID) tracking for mouse and keyboard
+# Documentation
+
+Extensive documentation is available in Org mode format under `doc/`:
+
+| Path | Topic |
+|----------------------------------------------|-------------------------------------------------------------------------------------------------|
+| `doc/index.org` | Main documentation: engine intro, coordinate system, shapes, CSG overview, developer tools |
+| `doc/rendering-loop/index.org` | Rendering pipeline: 5 phases (transform→sort→paint→blit), multi-threaded paint, frame listeners |
+| `doc/shading/index.org` | Lighting: Lambert cosine law, light sources, ambient light, distance attenuation |
+| `doc/csg/index.org` | Boolean operations: subtract/union/intersect via BSP trees, polygon clipping |
+| `doc/perspective-correct-textures/index.org` | Tessellation for perspective-correct texture mapping (adaptive subdivision) |
+| `doc/frustum-culling/index.org` | View frustum culling: 6 planes, AABB intersection tests, scene design tips |
+| `TODO.org` | Project roadmap: planned features, performance improvements, demo ideas |
+
# Repository Structure
src/main/java/eu/svjatoslav/sixth/e3d/
--- /dev/null
+<svg viewBox="0 0 320 260" width="320" height="260">
+ <rect width="320" height="260" fill="#061018"/>
+ <circle cx="140" cy="130" r="5" fill="rgba(0,0,0,0.1)" stroke="rgba(0,0,0,0.2)" stroke-width="1"/>
+ <line x1="140" y1="130" x2="280" y2="130" stroke="#d04040" stroke-width="2.5"/>
+ <polygon points="280,130 270,125 270,135" fill="#d04040"/>
+ <text x="284" y="134" fill="#d04040" font-size="14" font-weight="700" font-family="monospace">X</text>
+ <text x="200" y="152" fill="#bbb" font-size="9" font-family="monospace">right (+) / left (-)</text>
+ <line x1="140" y1="130" x2="140" y2="240" stroke="#30a050" stroke-width="2.5"/>
+ <polygon points="140,240 135,230 145,230" fill="#30a050"/>
+ <text x="146" y="252" fill="#30a050" font-size="14" font-weight="700" font-family="monospace">Y</text>
+ <text x="146" y="228" fill="#bbb" font-size="9" font-family="monospace">down (+) / up (-)</text>
+ <line x1="140" y1="130" x2="60" y2="70" stroke="#2070c0" stroke-width="2.5"/>
+ <polygon points="60,70 70,72 66,82" fill="#2070c0"/>
+ <text x="42" y="62" fill="#2070c0" font-size="14" font-weight="700" font-family="monospace">Z</text>
+ <text x="60" y="56" fill="#bbb" font-size="9" font-family="monospace">away (+) / towards (-)</text>
+ <text x="150" y="102" fill="#aaa" font-size="11" font-weight="600" font-family="monospace">Origin</text>
+ <text x="147" y="115" fill="#bbb" font-size="9" font-family="monospace">(0, 0, 0)</text>
+</svg>
\ No newline at end of file
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
--- /dev/null
+<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>
:CUSTOM_ID: subtract-operation
:END:
-#+BEGIN_EXPORT html
-<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>
-#+END_EXPORT
+#+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
:CUSTOM_ID: union-operation
:END:
-#+BEGIN_EXPORT html
-<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="#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.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="#aaa" 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="#aaa" font-size="9" font-family="monospace" text-anchor="middle">Keeps all geometry</text>
- <text x="110" y="155" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">from both shapes</text>
- <text x="440" y="142" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">Single combined volume</text>
- <text x="440" y="155" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">interior faces removed</text>
-</svg>
-#+END_EXPORT
+#+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
:CUSTOM_ID: intersect-operation
:END:
-#+BEGIN_EXPORT html
-<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="#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.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="#aaa" 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="#aaa" font-size="9" font-family="monospace" text-anchor="middle">Find overlap</text>
- <text x="110" y="155" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">between both</text>
- <text x="440" y="142" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">Only shared volume</text>
- <text x="440" y="155" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">remains</text>
-</svg>
-#+END_EXPORT
+#+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
:CUSTOM_ID: bsp-tree-structure
:END:
-#+BEGIN_EXPORT html
-<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="#666" stroke-width="1" stroke-dasharray="3 2"/>
- <text x="85" y="145" fill="#888" 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="#666" stroke-width="1" stroke-dasharray="3 2"/>
- <text x="235" y="145" fill="#888" font-size="10" font-family="monospace" text-anchor="middle">leaf</text>
-
- <!-- ── Explanation ── -->
- <text x="310" y="185" fill="#aaa" 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="#aaa" font-size="9" font-family="monospace" text-anchor="middle">Polygons are classified and split at each partitioning plane</text>
-</svg>
-#+END_EXPORT
+#+INCLUDE: "bsp-tree.svg" export html
Each BSP node contains:
- A *partitioning plane* that divides space into two half-spaces
When a polygon crosses a partitioning plane, it's *split* into two
fragments:
-#+BEGIN_EXPORT html
-<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="#aaa" 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="#aaa" 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="#aaa" font-size="9" font-family="monospace" text-anchor="middle">Spanning polygons are split; each fragment goes to its respective subtree</text>
-</svg>
-#+END_EXPORT
+#+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
--- /dev/null
+<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>
--- /dev/null
+<svg viewBox="0 0 320 240" width="320" height="240">
+ <rect width="320" height="240" fill="#061018"/>
+ <polygon points="160,50 80,190 240,190" fill="rgba(100,100,200,0.04)" stroke="rgba(100,100,200,0.2)" stroke-width="1"/>
+ <line x1="160" y1="50" x2="240" y2="190" stroke="#5060c0" stroke-width="3" stroke-linecap="round"/>
+ <circle cx="160" cy="50" r="5" fill="#5060c0"/>
+ <circle cx="80" cy="190" r="4" fill="rgba(80,96,192,0.5)"/>
+ <circle cx="240" cy="190" r="5" fill="#5060c0"/>
+ <text x="150" y="40" fill="#aaa" font-size="10" font-family="monospace">V₁</text>
+ <text x="246" y="194" fill="#aaa" font-size="10" font-family="monospace">V₂</text>
+ <text x="60" y="200" fill="#bbb" font-size="10" font-family="monospace">V₃</text>
+ <text x="210" y="110" fill="#5060c0" font-size="12" font-weight="700" font-family="monospace" transform="rotate(30 210 110)">edge</text>
+</svg>
--- /dev/null
+<svg viewBox="0 0 320 240" width="320" height="240">
+ <rect width="320" height="240" fill="#061018"/>
+ <polygon points="160,40 60,200 260,200" fill="rgba(200,80,140,0.15)" stroke="#c05088" stroke-width="1.5"/>
+ <line x1="100" y1="140" x2="220" y2="140" stroke="rgba(200,80,140,0.1)" stroke-width="0.5"/>
+ <line x1="120" y1="160" x2="200" y2="160" stroke="rgba(200,80,140,0.08)" stroke-width="0.5"/>
+ <line x1="82" y1="180" x2="238" y2="180" stroke="rgba(200,80,140,0.06)" stroke-width="0.5"/>
+ <circle cx="160" cy="40" r="4" fill="#c05088"/>
+ <circle cx="60" cy="200" r="4" fill="#c05088"/>
+ <circle cx="260" cy="200" r="4" fill="#c05088"/>
+ <text x="148" y="30" fill="#c05088" font-size="10" font-weight="700" font-family="monospace">V₁</text>
+ <text x="38" y="210" fill="#c05088" font-size="10" font-weight="700" font-family="monospace">V₂</text>
+ <text x="266" y="210" fill="#c05088" font-size="10" font-weight="700" font-family="monospace">V₃</text>
+ <text x="132" y="150" fill="rgba(192,80,136,0.5)" font-size="14" font-weight="700" font-family="monospace">FACE</text>
+</svg>
--- /dev/null
+<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>
:CUSTOM_ID: frustum-view-frustum-culling
:END:
-#+BEGIN_EXPORT html
-<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>
-#+END_EXPORT
+#+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
The intersection test uses an optimized "P-vertex" approach:
-#+BEGIN_EXPORT html
-<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="#999" 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>
-#+END_EXPORT
+#+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
--- /dev/null
+<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>
** Main render loop
-#+BEGIN_EXPORT html
-<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="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="140" y="25" width="90" height="30" rx="3" fill="rgba(32,112,192,0.15)" stroke="#2070c0" stroke-width="1.5"/>
- <text x="185" y="43" fill="#2070c0" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Transform</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>
-
- <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>
-
- <rect x="570" y="25" width="35" height="30" rx="3" fill="rgba(100,100,100,0.2)" stroke="#aaa" stroke-width="1.5"/>
- <text x="587" y="43" fill="#aaa" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Screen</text>
-
- <!-- Arrows -->
- <line x1="105" y1="40" x2="135" y2="40" stroke="#30a050" stroke-width="1.5" marker-end="url(#arrowhead)"/>
- <line x1="230" y1="40" x2="260" y2="40" stroke="#2070c0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
- <line x1="335" y1="40" x2="360" y2="40" stroke="#c05088" stroke-width="1.5" marker-end="url(#arrowhead)"/>
- <line x1="445" y1="40" x2="475" y2="40" stroke="#FF6600" stroke-width="1.5" marker-end="url(#arrowhead)"/>
- <line x1="540" y1="40" x2="565" y2="40" stroke="#39FF14" stroke-width="1.5" marker-end="url(#arrowhead)"/>
-
- <!-- Labels below -->
- <text x="60" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">3D vertices</text>
- <text x="185" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">world→screen</text>
- <text x="300" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">back-to-front</text>
- <text x="405" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">8 threads</text>
- <text x="510" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">copy buffer</text>
-</svg>
-#+END_EXPORT
-
-To understand main render loop, see dedicated page: [[file:rendering-loop/][Rendering 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/sixth-3d/apidocs/eu/svjatoslav/sixth/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]].
** Coordinate System (X, Y, Z)
:PROPERTIES:
:CUSTOM_ID: coordinate-system
:END:
-#+BEGIN_EXPORT html
-<svg viewBox="0 0 320 260" width="320" height="260">
- <rect width="320" height="260" fill="#f8f8f8"/>
- <circle cx="140" cy="130" r="5" fill="rgba(0,0,0,0.1)" stroke="rgba(0,0,0,0.2)" stroke-width="1"/>
- <line x1="140" y1="130" x2="280" y2="130" stroke="#d04040" stroke-width="2.5"/>
- <polygon points="280,130 270,125 270,135" fill="#d04040"/>
- <text x="284" y="134" fill="#d04040" font-size="14" font-weight="700" font-family="monospace">X</text>
- <text x="200" y="152" fill="#999" font-size="9" font-family="monospace">right (+) / left (-)</text>
- <line x1="140" y1="130" x2="140" y2="240" stroke="#30a050" stroke-width="2.5"/>
- <polygon points="140,240 135,230 145,230" fill="#30a050"/>
- <text x="146" y="252" fill="#30a050" font-size="14" font-weight="700" font-family="monospace">Y</text>
- <text x="146" y="228" fill="#999" font-size="9" font-family="monospace">down (+) / up (-)</text>
- <line x1="140" y1="130" x2="60" y2="70" stroke="#2070c0" stroke-width="2.5"/>
- <polygon points="60,70 70,72 66,82" fill="#2070c0"/>
- <text x="42" y="62" fill="#2070c0" font-size="14" font-weight="700" font-family="monospace">Z</text>
- <text x="60" y="56" fill="#999" font-size="9" font-family="monospace">away (+) / towards (-)</text>
- <text x="150" y="102" fill="#666" font-size="11" font-weight="600" font-family="monospace">Origin</text>
- <text x="147" y="115" fill="#999" font-size="9" font-family="monospace">(0, 0, 0)</text>
-</svg>
-#+END_EXPORT
+#+INCLUDE: "coordinate-system.svg" export html
*Sixth 3D* uses a **left-handed coordinate system with X pointing right
and Y pointing down**, matching standard 2D screen coordinates. This
- 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/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d-demos/#coordinate-system][sixth-3d-demos]] project includes an interactive
coordinate system reference showing X, Y, Z axes as colored arrows
with a grid plane for spatial context.
-** Vertex
+** Point3D and Vertex
:PROPERTIES:
:CUSTOM_ID: vertex
:END:
-#+BEGIN_EXPORT html
-<svg viewBox="0 0 320 240" width="320" height="240">
- <rect width="320" height="240" fill="#f8f8f8"/>
- <line x1="80" y1="180" x2="260" y2="180" stroke="#ccc" stroke-width="1"/>
- <line x1="80" y1="180" x2="80" y2="40" stroke="#ccc" stroke-width="1"/>
- <circle cx="190" cy="100" r="20" fill="rgba(56,140,248,0.08)" stroke="none"/>
- <circle cx="190" cy="100" r="10" fill="rgba(56,140,248,0.15)" stroke="none"/>
- <circle cx="190" cy="100" r="4" fill="#2070c0"/>
- <line x1="190" y1="100" x2="190" y2="180" stroke="#2070c0" stroke-width="1" stroke-dasharray="4 3" opacity="0.4"/>
- <line x1="190" y1="100" x2="80" y2="100" stroke="#2070c0" stroke-width="1" stroke-dasharray="4 3" opacity="0.4"/>
- <text x="196" y="94" fill="#2070c0" font-size="13" font-weight="700" font-family="monospace">V</text>
- <text x="200" y="108" fill="#666" font-size="10" font-family="monospace">(x, y, z)</text>
- <text x="186" y="196" fill="#999" font-size="9" font-family="monospace">x</text>
- <text x="64" y="100" fill="#999" font-size="9" font-family="monospace">y</text>
-</svg>
-#+END_EXPORT
-
-A *vertex* is a single point in 3D space, defined by three
-coordinates: *x*, *y*, and *z*. Every 3D object is ultimately built
-from vertices. A vertex can also carry additional data beyond
-position.
-
-- Position: =(x, y, z)=
-- Can also store: color, texture UV, normal vector
-- A triangle = 3 vertices, a cube = 8 vertices
-- Vertex maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Point3D.html][Point3D]] class in *Sixth 3D* engine.
+#+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/sixth-3d/apidocs/eu/svjatoslav/sixth/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.subtract(p2).unit(); // Unit vector from p2 to p1
+Point3D rotated = p1.rotate(new Point3D(0,0,0), Math.PI/4, 0); // Rotate 45° in XZ plane
+#+END_SRC
+
+*** Vertex — Rendering-Ready Coordinates
+
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/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=| Position relative to camera (after transform stack) |
+| =onScreenCoordinate= | 2D screen pixels (after perspective projection) |
+| =textureCoordinate= | Optional UV coords in pixel units (not normalized) |
+| =normal= | Optional normal vector for CSG polygon splitting |
+
+During rendering, =calculateLocationRelativeToViewer()= transforms the
+vertex through all spaces: first applying the
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/math/TransformStack.html][TransformStack]] to get =transformedCoordinate=, then projecting to 2D
+for =onScreenCoordinate=. Results are cached per-frame 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:
-#+BEGIN_EXPORT html
-<svg viewBox="0 0 320 240" width="320" height="240">
- <rect width="320" height="240" fill="#f8f8f8"/>
- <polygon points="160,50 80,190 240,190" fill="rgba(100,100,200,0.04)" stroke="rgba(100,100,200,0.2)" stroke-width="1"/>
- <line x1="160" y1="50" x2="240" y2="190" stroke="#5060c0" stroke-width="3" stroke-linecap="round"/>
- <circle cx="160" cy="50" r="5" fill="#5060c0"/>
- <circle cx="80" cy="190" r="4" fill="rgba(80,96,192,0.5)"/>
- <circle cx="240" cy="190" r="5" fill="#5060c0"/>
- <text x="150" y="40" fill="#666" font-size="10" font-family="monospace">V₁</text>
- <text x="246" y="194" fill="#666" font-size="10" font-family="monospace">V₂</text>
- <text x="60" y="200" fill="#999" font-size="10" font-family="monospace">V₃</text>
- <text x="210" y="110" fill="#5060c0" font-size="12" font-weight="700" font-family="monospace" transform="rotate(30 210 110)">edge</text>
-</svg>
-#+END_EXPORT
-
-An *edge* is a straight line segment connecting two vertices. Edges
-define the wireframe skeleton of a 3D model. In rendering, edges
-themselves are rarely drawn — they exist implicitly as boundaries of
-faces.
-
-- Edge = line from V₁ to V₂
-- A triangle has 3 edges
-- A cube has 12 edges
-- Wireframe mode renders edges visibly
-- Edge is related to and can be represented by the [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class in Sixth
- 3D engine.
+#+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 *Sixth 3D*, the [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class implements edges as renderable shapes. Each
+Line connects two [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/math/Vertex.html][Vertex]] endpoints and stores two properties: a
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/LineAppearance.html][LineAppearance]] factory to ensure consistent styling across
+all edges. The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.html][WireframeCube]] convenience subclass provides a center-point
+constructor. See the [[https://www3.svjatoslav.eu/projects/sixth-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:
-#+BEGIN_EXPORT html
-<svg viewBox="0 0 320 240" width="320" height="240">
- <rect width="320" height="240" fill="#f8f8f8"/>
- <polygon points="160,40 60,200 260,200" fill="rgba(200,80,140,0.15)" stroke="#c05088" stroke-width="1.5"/>
- <line x1="100" y1="140" x2="220" y2="140" stroke="rgba(200,80,140,0.1)" stroke-width="0.5"/>
- <line x1="120" y1="160" x2="200" y2="160" stroke="rgba(200,80,140,0.08)" stroke-width="0.5"/>
- <line x1="82" y1="180" x2="238" y2="180" stroke="rgba(200,80,140,0.06)" stroke-width="0.5"/>
- <circle cx="160" cy="40" r="4" fill="#c05088"/>
- <circle cx="60" cy="200" r="4" fill="#c05088"/>
- <circle cx="260" cy="200" r="4" fill="#c05088"/>
- <text x="148" y="30" fill="#c05088" font-size="10" font-weight="700" font-family="monospace">V₁</text>
- <text x="38" y="210" fill="#c05088" font-size="10" font-weight="700" font-family="monospace">V₂</text>
- <text x="266" y="210" fill="#c05088" font-size="10" font-weight="700" font-family="monospace">V₃</text>
- <text x="132" y="150" fill="rgba(192,80,136,0.5)" font-size="14" font-weight="700" font-family="monospace">FACE</text>
-</svg>
-#+END_EXPORT
-
-A *face* is a flat surface enclosed by edges. In most 3D engines, the fundamental face is a *triangle* — defined by exactly 3 vertices. Triangles are preferred because they are always planar (flat) and trivially simple to rasterize.
-
-- Triangle = 3 vertices + 3 edges
-- Always guaranteed to be coplanar
-- Quads (4 vertices) = 2 triangles
-- Complex shapes = many triangles (a "mesh")
-- Face maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidTriangle.html][SolidTriangle]], [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]], or [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] in *Sixth 3D*.
+#+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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] is the primary face type, supporting any number of vertices
+(3 or more). For triangles, it renders directly via scanline rasterization.
+For N-vertex polygons (quads, pentagons, etc.), the engine automatically
+triangulates using fan decomposition before rendering — a quad becomes 2
+triangles, a pentagon becomes 3.
+
+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/sixth-3d/apidocs/eu/svjatoslav/sixth/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);
+#+END_SRC
+
+*** TexturedTriangle — UV-Mapped Faces
+
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/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. For large triangles that would show perspective
+distortion, the engine can tessellate the triangle into smaller pieces
+for more accurate rendering.
+
+#+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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]], which handles vertex transformation and depth
+sorting. See the [[https://www3.svjatoslav.eu/projects/sixth-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:
-#+BEGIN_EXPORT html
-<svg viewBox="0 0 320 260" width="320" height="260">
- <rect width="320" height="260" fill="#f8f8f8"/>
- <polygon points="60,200 160,180 260,200 160,220" fill="rgba(180,150,30,0.1)" stroke="rgba(180,150,30,0.4)" stroke-width="1"/>
- <line x1="90" y1="198" x2="230" y2="198" stroke="rgba(180,150,30,0.08)" stroke-width="0.5"/>
- <line x1="110" y1="194" x2="210" y2="194" stroke="rgba(180,150,30,0.06)" stroke-width="0.5"/>
- <line x1="160" y1="198" x2="160" y2="60" stroke="#b09020" stroke-width="2.5"/>
- <polygon points="160,60 155,72 165,72" fill="#b09020"/>
- <path d="M160,198 L160,178 L170,180" fill="none" stroke="rgba(180,150,30,0.5)" stroke-width="1"/>
- <text x="168" y="56" fill="#b09020" font-size="13" font-weight="700" font-family="monospace">N̂</text>
- <text x="168" y="72" fill="#999" font-size="9" font-family="monospace">unit normal</text>
- <text x="168" y="86" fill="#999" font-size="9" font-family="monospace">(perpendicular</text>
- <text x="168" y="98" fill="#999" font-size="9" font-family="monospace"> to surface)</text>
- <circle cx="70" cy="60" r="14" fill="rgba(180,150,30,0.08)" stroke="rgba(180,150,30,0.3)" stroke-width="1"/>
- <circle cx="70" cy="60" r="4" fill="rgba(180,150,30,0.6)"/>
- <text x="56" y="42" fill="#999" font-size="9" font-family="monospace">Light</text>
- <line x1="80" y1="68" x2="150" y2="170" stroke="rgba(180,150,30,0.2)" stroke-width="1" stroke-dasharray="4 3"/>
- <text x="82" y="142" fill="rgba(180,150,30,0.5)" font-size="9" font-family="monospace">L · N = brightness</text>
-</svg>
-#+END_EXPORT
+#+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
:CUSTOM_ID: mesh
:END:
-#+BEGIN_EXPORT html
-<svg viewBox="0 0 320 240" width="320" height="240">
- <rect width="320" height="240" fill="#f8f8f8"/>
- <ellipse cx="160" cy="120" rx="90" ry="90" fill="none" stroke="rgba(80,96,192,0.1)" stroke-width="0.5"/>
- <ellipse cx="160" cy="120" rx="90" ry="20" fill="none" stroke="rgba(80,96,192,0.25)" stroke-width="0.8"/>
- <ellipse cx="160" cy="90" rx="75" ry="16" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="0.6"/>
- <ellipse cx="160" cy="150" rx="75" ry="16" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="0.6"/>
- <ellipse cx="160" cy="60" rx="45" ry="10" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="0.5"/>
- <ellipse cx="160" cy="180" rx="45" ry="10" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="0.5"/>
- <ellipse cx="160" cy="120" rx="20" ry="90" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="0.6"/>
- <ellipse cx="160" cy="120" rx="55" ry="90" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="0.5"/>
- <polygon points="160,30 185,58 140,55" fill="rgba(80,96,192,0.15)" stroke="#5060c0" stroke-width="1"/>
- <polygon points="185,58 205,88 160,82" fill="rgba(80,96,192,0.1)" stroke="#5060c0" stroke-width="0.8"/>
- <polygon points="160,82 185,58 140,55" fill="rgba(80,96,192,0.07)" stroke="rgba(80,96,192,0.5)" stroke-width="0.6"/>
- <circle cx="160" cy="30" r="2.5" fill="#5060c0"/>
- <circle cx="185" cy="58" r="2.5" fill="#5060c0"/>
- <circle cx="140" cy="55" r="2.5" fill="#5060c0"/>
- <circle cx="205" cy="88" r="2.5" fill="#5060c0"/>
- <circle cx="160" cy="82" r="2.5" fill="#5060c0"/>
- <text x="218" y="70" fill="#5060c0" font-size="10" font-weight="600" font-family="monospace">triangulated</text>
- <text x="218" y="82" fill="#5060c0" font-size="10" font-weight="600" font-family="monospace">section</text>
- <line x1="206" y1="75" x2="214" y2="75" stroke="#5060c0" stroke-width="0.8"/>
-</svg>
-#+END_EXPORT
-
-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.
-
-- Mesh data = vertex array + index array
-- Index array avoids duplicating shared vertices
-- Cube: 8 vertices, 12 triangles
-- Smooth sphere: hundreds–thousands of triangles
-- =vertices[] + indices[]= → efficient storage
-- In *Sixth 3D* engine:
- - [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]]: base class for single shapes with vertices (triangles, lines). Use when creating one primitive.
- - [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]: groups multiple shapes into one object. Use for complex models that move/rotate together.
-
-See the [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual showcase of
-all primitive shapes available in *Sixth 3D*, rendered in both
-wireframe and solid polygon styles with dynamic lighting.
+#+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 *Sixth 3D*, meshes are built through composition rather than
+monolithic vertex/index buffers. The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]] class is
+the foundation for primitive shapes — each instance stores its own
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.html#vertices][List<Vertex>]] directly. This includes [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] (N-vertex
+convex polygons, not limited to triangles), [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]]
+(UV-mapped triangles), and [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] (wireframe edges). The
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.html][SolidPolygonSphere]] generates
+triangles using a latitude-longitude grid: with 16 segments, it
+creates approximately 900 SolidPolygon triangles by looping through
+rings and sectors, calling [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#addShape(eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape)][addShape()]] for each. The generic
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/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 in [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#retessellate(eu.svjatoslav.sixth.e3d.gui.RenderingContext)][AbstractCompositeShape.retessellate()]], converting an
+N-vertex polygon into N-2 triangles. Textured polygons undergo
+level-of-detail tessellation — distant triangles are subdivided into
+smaller pieces for perspective-correct texture mapping. 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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.html][SubShape]]
+wrappers, allowing [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#hideGroup(java.lang.String)][showGroup()]] and [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d-demos/#shape-gallery][Shape Gallery demo]] showcases all primitive shapes available in
+*Sixth 3D*, rendered in both wireframe mode (edges only via
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.html][WireframeBox]] and similar) and solid polygon mode (filled surfaces with
+dynamic lighting).
** Perspective correct textures
*Sixth 3D* implements view frustum culling.
-#+BEGIN_EXPORT html
-<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>
-#+END_EXPORT
+#+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.]]
*Sixth 3D* allows performing boolean operations against geometry shapes.
So one can subtract, unionize or intersect shapes.
-#+BEGIN_EXPORT html
-<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>
-#+END_EXPORT
+#+INCLUDE: "csg/csg-operations.svg" export html
To understand CSG boolean operations, read more about [[file:csg/][Constructive
Solid Geometry]].
:CUSTOM_ID: winding-order-backface-culling
:END:
-#+BEGIN_EXPORT html
-<svg viewBox="0 0 320 240" width="320" height="240">
- <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="320" height="240" fill="#061018"/>
- <!-- Green front-face triangle: V1=top, V2=bottom-left, V3=bottom-right -->
- <polygon points="80,50 130,180 30,180" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
- <!-- CCW arrow: arc from near V1, curves LEFT and DOWN toward V2 -->
- <path d="M70,72 A 52,52 0 0,0 37,155" fill="none" stroke="#30a050" stroke-width="1.5" stroke-dasharray="4 2" marker-end="url(#arrow-green)"/>
- <text x="34" y="120" fill="#30a050" font-size="10" font-weight="700" font-family="monospace">CCW</text>
- <circle cx="80" cy="50" r="3" fill="#30a050"/>
- <circle cx="30" cy="180" r="3" fill="#30a050"/>
- <circle cx="130" cy="180" r="3" fill="#30a050"/>
- <text x="78" y="44" fill="#aaa" font-size="9" font-family="monospace">V₁</text>
- <text x="14" y="198" fill="#aaa" font-size="9" font-family="monospace">V₂</text>
- <text x="132" y="198" fill="#aaa" font-size="9" font-family="monospace">V₃</text>
- <text x="36" y="220" fill="#30a050" font-size="11" font-weight="700" font-family="monospace">FRONT FACE ✓</text>
- <!-- Red back-face triangle -->
- <polygon points="240,50 290,180 190,180" fill="rgba(208,64,64,0.06)" stroke="rgba(208,64,64,0.3)" stroke-width="1.5" stroke-dasharray="6 3"/>
- <!-- CW arrow: arc from near V1, curves RIGHT and DOWN -->
- <path d="M250,72 A 52,52 0 0,1 283,155" fill="none" stroke="rgba(208,64,64,0.5)" stroke-width="1.5" stroke-dasharray="4 2" marker-end="url(#arrow-red)"/>
- <text x="268" y="120" fill="rgba(208,64,64,0.6)" font-size="10" font-weight="700" font-family="monospace">CW</text>
- <line x1="228" y1="108" x2="252" y2="132" stroke="rgba(208,64,64,0.4)" stroke-width="3"/>
- <line x1="252" y1="108" x2="228" y2="132" stroke="rgba(208,64,64,0.4)" stroke-width="3"/>
- <text x="186" y="220" fill="rgba(208,64,64,0.7)" font-size="11" font-weight="700" font-family="monospace">BACK FACE ✗</text>
- <text x="195" y="234" fill="#aaa" font-size="9" font-family="monospace">(culled — not drawn)</text>
-</svg>
-#+END_EXPORT
+#+INCLUDE: "winding-order.svg" export html
The order in which a triangle's vertices are listed determines its
*winding order*. In *Sixth 3D*, screen coordinates have Y-axis pointing
:ID: f2c9642a-a093-444f-8992-76c97ff28c16
:END:
-*Sixth 3D* uses its own Color class (not java.awt.Color):
+Sixth 3D uses its own [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#r][r]], [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#g][g]], [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#b][b]], and
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] class maintains a reusable
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#shadedColor][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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#hex(java.lang.String)][Color.hex(String)]] or the
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#RED][Color.RED]], [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#GREEN][Color.GREEN]], [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#BLUE][Color.BLUE]], [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#YELLOW][Color.YELLOW]],
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#CYAN][Color.CYAN]], [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#MAGENTA][Color.MAGENTA]], [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#WHITE][Color.WHITE]], [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#BLACK][Color.BLACK]], and
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#TRANSPARENT][Color.TRANSPARENT]].
+
+The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#toAwtColor()][toAwtColor()]] method converts a
+Sixth 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/sixth-3d/apidocs/eu/svjatoslav/sixth/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.sixth.e3d.renderer.raster.Color;
+import static eu.svjatoslav.sixth.e3d.renderer.raster.Color.hex;
-// Using predefined colors
+// Using predefined color constants
Color red = Color.RED;
-Color green = Color.GREEN;
-Color blue = Color.BLUE;
+Color transparent = Color.TRANSPARENT;
-// Create custom color (R, G, B, A)
-Color custom = new Color(255, 128, 64, 200); // semi-transparent orange
+// Create from hex string (recommended for clarity)
+Color orange = hex("FF8800"); // RGB, fully opaque
+Color semiTransparent = hex("FF880080"); // RGBA, 50% transparent
-// Or use hex string
-Color hex = new Color("FF8040CC"); // same orange with alpha
+// 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/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/Color.html#isTransparent()][isTransparent()]], which returns true when alpha
+equals zero.
+
+For lighting calculations, the [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.html#setAmbientLight(eu.svjatoslav.sixth.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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class, where the color field determines the
+line's appearance.
+
* Developer tools
:PROPERTIES:
:CUSTOM_ID: developer-tools
--- /dev/null
+<svg viewBox="0 0 320 240" width="320" height="240">
+ <rect width="320" height="240" fill="#061018"/>
+ <ellipse cx="160" cy="120" rx="90" ry="90" fill="none" stroke="rgba(80,96,192,0.1)" stroke-width="0.5"/>
+ <ellipse cx="160" cy="120" rx="90" ry="20" fill="none" stroke="rgba(80,96,192,0.25)" stroke-width="0.8"/>
+ <ellipse cx="160" cy="90" rx="75" ry="16" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="0.6"/>
+ <ellipse cx="160" cy="150" rx="75" ry="16" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="0.6"/>
+ <ellipse cx="160" cy="60" rx="45" ry="10" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="0.5"/>
+ <ellipse cx="160" cy="180" rx="45" ry="10" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="0.5"/>
+ <ellipse cx="160" cy="120" rx="20" ry="90" fill="none" stroke="rgba(80,96,192,0.2)" stroke-width="0.6"/>
+ <ellipse cx="160" cy="120" rx="55" ry="90" fill="none" stroke="rgba(80,96,192,0.15)" stroke-width="0.5"/>
+ <polygon points="160,30 185,58 140,55" fill="rgba(80,96,192,0.15)" stroke="#5060c0" stroke-width="1"/>
+ <polygon points="185,58 205,88 160,82" fill="rgba(80,96,192,0.1)" stroke="#5060c0" stroke-width="0.8"/>
+ <polygon points="160,82 185,58 140,55" fill="rgba(80,96,192,0.07)" stroke="rgba(80,96,192,0.5)" stroke-width="0.6"/>
+ <circle cx="160" cy="30" r="2.5" fill="#5060c0"/>
+ <circle cx="185" cy="58" r="2.5" fill="#5060c0"/>
+ <circle cx="140" cy="55" r="2.5" fill="#5060c0"/>
+ <circle cx="205" cy="88" r="2.5" fill="#5060c0"/>
+ <circle cx="160" cy="82" r="2.5" fill="#5060c0"/>
+ <text x="218" y="70" fill="#5060c0" font-size="10" font-weight="600" font-family="monospace">triangulated</text>
+ <text x="218" y="82" fill="#5060c0" font-size="10" font-weight="600" font-family="monospace">section</text>
+ <line x1="206" y1="75" x2="214" y2="75" stroke="#5060c0" stroke-width="0.8"/>
+</svg>
--- /dev/null
+<svg viewBox="0 0 320 260" width="320" height="260">
+ <rect width="320" height="260" fill="#061018"/>
+ <polygon points="60,200 160,180 260,200 160,220" fill="rgba(180,150,30,0.1)" stroke="rgba(180,150,30,0.4)" stroke-width="1"/>
+ <line x1="90" y1="198" x2="230" y2="198" stroke="rgba(180,150,30,0.08)" stroke-width="0.5"/>
+ <line x1="110" y1="194" x2="210" y2="194" stroke="rgba(180,150,30,0.06)" stroke-width="0.5"/>
+ <line x1="160" y1="198" x2="160" y2="60" stroke="#b09020" stroke-width="2.5"/>
+ <polygon points="160,60 155,72 165,72" fill="#b09020"/>
+ <path d="M160,198 L160,178 L170,180" fill="none" stroke="rgba(180,150,30,0.5)" stroke-width="1"/>
+ <text x="168" y="56" fill="#b09020" font-size="13" font-weight="700" font-family="monospace">N̂</text>
+ <text x="168" y="72" fill="#bbb" font-size="9" font-family="monospace">unit normal</text>
+ <text x="168" y="86" fill="#bbb" font-size="9" font-family="monospace">(perpendicular</text>
+ <text x="168" y="98" fill="#bbb" font-size="9" font-family="monospace"> to surface)</text>
+ <circle cx="70" cy="60" r="14" fill="rgba(180,150,30,0.08)" stroke="rgba(180,150,30,0.3)" stroke-width="1"/>
+ <circle cx="70" cy="60" r="4" fill="rgba(180,150,30,0.6)"/>
+ <text x="56" y="42" fill="#bbb" font-size="9" font-family="monospace">Light</text>
+ <line x1="80" y1="68" x2="150" y2="170" stroke="rgba(180,150,30,0.2)" stroke-width="1" stroke-dasharray="4 3"/>
+ <text x="82" y="142" fill="rgba(180,150,30,0.5)" font-size="9" font-family="monospace">L · N = brightness</text>
+</svg>
}
#+END_SRC
-#+BEGIN_EXPORT html
-<svg viewBox="0 0 520 300" width="1000" height="600">
- <defs>
- <marker id="arrow-cyan" viewBox="0 0 10 10" refX="10" 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="520" height="300" fill="#061018"/>
-
- <!-- Step 1: original triangle -->
- <text x="80" y="22" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">1. Original</text>
- <polygon points="80,40 20,170 140,170"
- fill="rgba(32,112,192,0.12)" stroke="#2070c0" stroke-width="1.5"/>
- <circle cx="80" cy="40" r="3" fill="#2070c0"/>
- <circle cx="20" cy="170" r="3" fill="#2070c0"/>
- <circle cx="140" cy="170" r="3" fill="#2070c0"/>
- <text x="66" y="36" fill="#aaa" font-size="9" font-family="monospace">A</text>
- <text x="6" y="184" fill="#aaa" font-size="9" font-family="monospace">B</text>
- <text x="144" y="184" fill="#aaa" font-size="9" font-family="monospace">C</text>
-
- <!-- Longest edge highlight -->
- <line x1="20" y1="170" x2="140" y2="170" stroke="#40b0d0" stroke-width="2.5"/>
- <text x="80" y="192" fill="#40b0d0" font-size="9" font-weight="700" font-family="monospace" text-anchor="middle">longest edge</text>
-
- <!-- Arrow to step 2 -->
- <line x1="156" y1="105" x2="178" y2="105" stroke="#40b0d0" stroke-width="1.2" marker-end="url(#arrow-cyan)"/>
-
- <!-- Step 2: first split -->
- <text x="270" y="22" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">2. Split</text>
-
- <!-- Sub-triangle left -->
- <polygon points="270,40 210,170 270,170"
- fill="rgba(32,112,192,0.10)" stroke="#2070c0" stroke-width="1"/>
- <!-- Sub-triangle right -->
- <polygon points="270,40 270,170 330,170"
- fill="rgba(48,160,80,0.10)" stroke="#30a050" stroke-width="1"/>
-
- <circle cx="270" cy="40" r="3" fill="#2070c0"/>
- <circle cx="210" cy="170" r="3" fill="#2070c0"/>
- <circle cx="330" cy="170" r="3" fill="#2070c0"/>
-
- <!-- Midpoint -->
- <circle cx="270" cy="170" r="4" fill="#40b0d0"/>
- <text x="250" y="192" fill="#40b0d0" font-size="9" font-weight="700" font-family="monospace" text-anchor="middle">M</text>
- <text x="250" y="204" fill="#aaa" font-size="8" font-family="monospace" text-anchor="middle">midpoint</text>
-
- <!-- Split line -->
- <line x1="270" y1="40" x2="270" y2="170" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="4 3"/>
-
- <!-- Arrow to step 3 -->
- <line x1="346" y1="105" x2="368" y2="105" stroke="#40b0d0" stroke-width="1.2" marker-end="url(#arrow-cyan)"/>
-
- <!-- Step 3: fully subdivided -->
- <text x="440" y="22" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">3. Recurse</text>
-
- <!-- Four sub-triangles (A=440,40 B=380,170 C=500,170 M=mid(BC)=440,170 P=mid(AB)=410,105 Q=mid(AC)=470,105) -->
- <polygon points="440,40 410,105 440,170"
- fill="rgba(32,112,192,0.12)" stroke="#2070c0" stroke-width="0.8"/>
- <polygon points="440,40 470,105 440,170"
- fill="rgba(48,160,80,0.10)" stroke="#30a050" stroke-width="0.8"/>
- <polygon points="410,105 380,170 440,170"
- fill="rgba(176,144,32,0.10)" stroke="#b09020" stroke-width="0.8"/>
- <polygon points="470,105 500,170 440,170"
- fill="rgba(192,80,136,0.10)" stroke="#c05088" stroke-width="0.8"/>
-
- <!-- Split lines -->
- <line x1="440" y1="40" x2="440" y2="170" stroke="#40b0d0" stroke-width="1" stroke-dasharray="3 2"/>
- <line x1="410" y1="105" x2="440" y2="170" stroke="rgba(64,176,208,0.4)" stroke-width="0.8" stroke-dasharray="3 2"/>
- <line x1="470" y1="105" x2="440" y2="170" stroke="rgba(64,176,208,0.4)" stroke-width="0.8" stroke-dasharray="3 2"/>
-
- <!-- Original vertices -->
- <circle cx="440" cy="40" r="2.5" fill="#2070c0"/>
- <circle cx="380" cy="170" r="2.5" fill="#2070c0"/>
- <circle cx="500" cy="170" r="2.5" fill="#2070c0"/>
- <!-- Midpoints -->
- <circle cx="440" cy="170" r="3" fill="#40b0d0"/>
- <circle cx="410" cy="105" r="3" fill="#40b0d0"/>
- <circle cx="470" cy="105" r="3" fill="#40b0d0"/>
-
- <!-- Annotation -->
- <text x="260" y="240" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">Each split halves the longest edge at its midpoint.</text>
- <text x="260" y="256" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">Recursion stops when all edges < maxDistance.</text>
-
- <!-- Legend -->
- <circle cx="160" cy="280" r="3" fill="#40b0d0"/>
- <text x="170" y="284" fill="#40b0d0" font-size="9" font-family="monospace">midpoint (3D + UV averaged)</text>
-</svg>
-#+END_EXPORT
+#+INCLUDE: "triangle-tessellation.svg" export html
The midpoint is computed by averaging both 3D coordinates *and* texture
coordinates.
--- /dev/null
+<svg viewBox="0 0 520 300" width="1000" height="600">
+ <defs>
+ <marker id="arrow-cyan" viewBox="0 0 10 10" refX="10" 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="520" height="300" fill="#061018"/>
+
+ <!-- Step 1: original triangle -->
+ <text x="80" y="22" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">1. Original</text>
+ <polygon points="80,40 20,170 140,170"
+ fill="rgba(32,112,192,0.12)" stroke="#2070c0" stroke-width="1.5"/>
+ <circle cx="80" cy="40" r="3" fill="#2070c0"/>
+ <circle cx="20" cy="170" r="3" fill="#2070c0"/>
+ <circle cx="140" cy="170" r="3" fill="#2070c0"/>
+ <text x="66" y="36" fill="#ccc" font-size="9" font-family="monospace">A</text>
+ <text x="6" y="184" fill="#ccc" font-size="9" font-family="monospace">B</text>
+ <text x="144" y="184" fill="#ccc" font-size="9" font-family="monospace">C</text>
+
+ <!-- Longest edge highlight -->
+ <line x1="20" y1="170" x2="140" y2="170" stroke="#40b0d0" stroke-width="2.5"/>
+ <text x="80" y="192" fill="#40b0d0" font-size="9" font-weight="700" font-family="monospace" text-anchor="middle">longest edge</text>
+
+ <!-- Arrow to step 2 -->
+ <line x1="156" y1="105" x2="178" y2="105" stroke="#40b0d0" stroke-width="1.2" marker-end="url(#arrow-cyan)"/>
+
+ <!-- Step 2: first split -->
+ <text x="270" y="22" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">2. Split</text>
+
+ <!-- Sub-triangle left -->
+ <polygon points="270,40 210,170 270,170"
+ fill="rgba(32,112,192,0.10)" stroke="#2070c0" stroke-width="1"/>
+ <!-- Sub-triangle right -->
+ <polygon points="270,40 270,170 330,170"
+ fill="rgba(48,160,80,0.10)" stroke="#30a050" stroke-width="1"/>
+
+ <circle cx="270" cy="40" r="3" fill="#2070c0"/>
+ <circle cx="210" cy="170" r="3" fill="#2070c0"/>
+ <circle cx="330" cy="170" r="3" fill="#2070c0"/>
+
+ <!-- Midpoint -->
+ <circle cx="270" cy="170" r="4" fill="#40b0d0"/>
+ <text x="250" y="192" fill="#40b0d0" font-size="9" font-weight="700" font-family="monospace" text-anchor="middle">M</text>
+ <text x="250" y="204" fill="#ccc" font-size="8" font-family="monospace" text-anchor="middle">midpoint</text>
+
+ <!-- Split line -->
+ <line x1="270" y1="40" x2="270" y2="170" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="4 3"/>
+
+ <!-- Arrow to step 3 -->
+ <line x1="346" y1="105" x2="368" y2="105" stroke="#40b0d0" stroke-width="1.2" marker-end="url(#arrow-cyan)"/>
+
+ <!-- Step 3: fully subdivided -->
+ <text x="440" y="22" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">3. Recurse</text>
+
+ <!-- Four sub-triangles (A=440,40 B=380,170 C=500,170 M=mid(BC)=440,170 P=mid(AB)=410,105 Q=mid(AC)=470,105) -->
+ <polygon points="440,40 410,105 440,170"
+ fill="rgba(32,112,192,0.12)" stroke="#2070c0" stroke-width="0.8"/>
+ <polygon points="440,40 470,105 440,170"
+ fill="rgba(48,160,80,0.10)" stroke="#30a050" stroke-width="0.8"/>
+ <polygon points="410,105 380,170 440,170"
+ fill="rgba(176,144,32,0.10)" stroke="#b09020" stroke-width="0.8"/>
+ <polygon points="470,105 500,170 440,170"
+ fill="rgba(192,80,136,0.10)" stroke="#c05088" stroke-width="0.8"/>
+
+ <!-- Split lines -->
+ <line x1="440" y1="40" x2="440" y2="170" stroke="#40b0d0" stroke-width="1" stroke-dasharray="3 2"/>
+ <line x1="410" y1="105" x2="440" y2="170" stroke="rgba(64,176,208,0.4)" stroke-width="0.8" stroke-dasharray="3 2"/>
+ <line x1="470" y1="105" x2="440" y2="170" stroke="rgba(64,176,208,0.4)" stroke-width="0.8" stroke-dasharray="3 2"/>
+
+ <!-- Original vertices -->
+ <circle cx="440" cy="40" r="2.5" fill="#2070c0"/>
+ <circle cx="380" cy="170" r="2.5" fill="#2070c0"/>
+ <circle cx="500" cy="170" r="2.5" fill="#2070c0"/>
+ <!-- Midpoints -->
+ <circle cx="440" cy="170" r="3" fill="#40b0d0"/>
+ <circle cx="410" cy="105" r="3" fill="#40b0d0"/>
+ <circle cx="470" cy="105" r="3" fill="#40b0d0"/>
+
+ <!-- Annotation -->
+ <text x="260" y="240" fill="#ccc" font-size="10" font-family="monospace" text-anchor="middle">Each split halves the longest edge at its midpoint.</text>
+ <text x="260" y="256" fill="#ccc" font-size="10" font-family="monospace" text-anchor="middle">Recursion stops when all edges < maxDistance.</text>
+
+ <!-- Legend -->
+ <circle cx="160" cy="280" r="3" fill="#40b0d0"/>
+ <text x="170" y="284" fill="#40b0d0" font-size="9" font-family="monospace">midpoint (3D + UV averaged)</text>
+</svg>
--- /dev/null
+<svg width="100%" viewBox="0 0 680 350" xmlns="http://www.w3.org/2000/svg"><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>
+<rect width="680" height="340" rx="8" 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
+
+<!-- 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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:"Anthropic Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", 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>
--- /dev/null
+<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>
The process transforms shapes through multiple coordinate systems:
-#+BEGIN_EXPORT html
-<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="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="140" y="25" width="90" height="30" rx="3" fill="rgba(32,112,192,0.15)" stroke="#2070c0" stroke-width="1.5"/>
- <text x="185" y="43" fill="#2070c0" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Transform</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>
-
- <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>
-
- <rect x="570" y="25" width="35" height="30" rx="3" fill="rgba(100,100,100,0.2)" stroke="#aaa" stroke-width="1.5"/>
- <text x="587" y="43" fill="#aaa" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Screen</text>
-
- <!-- Arrows -->
- <line x1="105" y1="40" x2="135" y2="40" stroke="#30a050" stroke-width="1.5" marker-end="url(#arrowhead)"/>
- <line x1="230" y1="40" x2="260" y2="40" stroke="#2070c0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
- <line x1="335" y1="40" x2="360" y2="40" stroke="#c05088" stroke-width="1.5" marker-end="url(#arrowhead)"/>
- <line x1="445" y1="40" x2="475" y2="40" stroke="#FF6600" stroke-width="1.5" marker-end="url(#arrowhead)"/>
- <line x1="540" y1="40" x2="565" y2="40" stroke="#39FF14" stroke-width="1.5" marker-end="url(#arrowhead)"/>
-
- <!-- Labels below -->
- <text x="60" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">3D vertices</text>
- <text x="185" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">world→screen</text>
- <text x="300" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">back-to-front</text>
- <text x="405" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">8 threads</text>
- <text x="510" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">copy buffer</text>
-</svg>
-#+END_EXPORT
+#+INCLUDE: "render-pipeline.svg" export html
Each step has a specific purpose:
first paint the sky (farthest), then mountains, then trees, then the
foreground. Each layer covers what's behind it.
-#+BEGIN_EXPORT html
-
-<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>
-#+END_EXPORT
+#+INCLUDE: "painters-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
Both operations happen within the same thread task, ensuring clearing completes before painting begins. This provides greater RAM bandwidth utilization compared to single-threaded clearing.
-#+BEGIN_EXPORT html
-<svg viewBox="0 0 400 200" width="400" height="200">
- <rect width="400" height="200" fill="#061018"/>
- <rect x="10" y="5" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
- <rect x="10" y="30" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
- <rect x="10" y="55" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
- <rect x="10" y="80" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
- <rect x="10" y="105" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
- <rect x="10" y="130" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
- <rect x="10" y="155" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
- <rect x="10" y="180" width="380" height="15" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
- <text x="20" y="21" fill="#30a050" font-size="11" font-family="monospace">Segment 0 (Thread 0)</text>
- <text x="20" y="46" fill="#30a050" font-size="11" font-family="monospace">Segment 1 (Thread 1)</text>
- <text x="20" y="71" fill="#30a050" font-size="11" font-family="monospace">Segment 2 (Thread 2)</text>
- <text x="20" y="96" fill="#30a050" font-size="11" font-family="monospace">Segment 3 (Thread 3)</text>
- <text x="20" y="121" fill="#30a050" font-size="11" font-family="monospace">Segment 4 (Thread 4)</text>
- <text x="20" y="146" fill="#30a050" font-size="11" font-family="monospace">Segment 5 (Thread 5)</text>
- <text x="20" y="171" fill="#30a050" font-size="11" font-family="monospace">Segment 6 (Thread 6)</text>
- <text x="20" y="193" fill="#30a050" font-size="11" font-family="monospace">Segment 7 (Thread 7)</text>
-</svg>
-#+END_EXPORT
+#+INCLUDE: "paint-segments.svg" export html
Each thread:
- Gets a =SegmentRenderingContext= with Y-bounds (minY, maxY)
written. This causes *screen tearing* — visible horizontal splits where
the top of the frame shows old content while the bottom shows new.
-#+BEGIN_EXPORT html
-<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="#666" 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="#555" 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="#555" 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>
-#+END_EXPORT
+#+INCLUDE: "double-buffering.svg" export html
Double-buffering uses two pixel buffers:
- *Back buffer*: Where rendering happens (offscreen, invisible)
--- /dev/null
+<svg viewBox="0 0 400 200" width="400" height="200">
+ <rect width="400" height="200" fill="#061018"/>
+ <rect x="10" y="5" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="30" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="55" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="80" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="105" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="130" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="155" width="380" height="22" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <rect x="10" y="180" width="380" height="15" fill="#1a3a4a" stroke="#30a050" stroke-width="1"/>
+ <text x="20" y="21" fill="#30a050" font-size="11" font-family="monospace">Segment 0 (Thread 0)</text>
+ <text x="20" y="46" fill="#30a050" font-size="11" font-family="monospace">Segment 1 (Thread 1)</text>
+ <text x="20" y="71" fill="#30a050" font-size="11" font-family="monospace">Segment 2 (Thread 2)</text>
+ <text x="20" y="96" fill="#30a050" font-size="11" font-family="monospace">Segment 3 (Thread 3)</text>
+ <text x="20" y="121" fill="#30a050" font-size="11" font-family="monospace">Segment 4 (Thread 4)</text>
+ <text x="20" y="146" fill="#30a050" font-size="11" font-family="monospace">Segment 5 (Thread 5)</text>
+ <text x="20" y="171" fill="#30a050" font-size="11" font-family="monospace">Segment 6 (Thread 6)</text>
+ <text x="20" y="193" fill="#30a050" font-size="11" font-family="monospace">Segment 7 (Thread 7)</text>
+</svg>
--- /dev/null
+
+<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>
--- /dev/null
+<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="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="140" y="25" width="90" height="30" rx="3" fill="rgba(32,112,192,0.15)" stroke="#2070c0" stroke-width="1.5"/>
+ <text x="185" y="43" fill="#2070c0" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Transform</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>
+
+ <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>
+
+ <rect x="570" y="25" width="35" height="30" rx="3" fill="rgba(100,100,100,0.2)" stroke="#aaa" stroke-width="1.5"/>
+ <text x="587" y="43" fill="#aaa" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Screen</text>
+
+ <!-- Arrows -->
+ <line x1="105" y1="40" x2="135" y2="40" stroke="#30a050" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+ <line x1="230" y1="40" x2="260" y2="40" stroke="#2070c0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+ <line x1="335" y1="40" x2="360" y2="40" stroke="#c05088" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+ <line x1="445" y1="40" x2="475" y2="40" stroke="#FF6600" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+ <line x1="540" y1="40" x2="565" y2="40" stroke="#39FF14" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+
+ <!-- Labels below -->
+ <text x="60" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">3D vertices</text>
+ <text x="185" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">world→screen</text>
+ <text x="300" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">back-to-front</text>
+ <text x="405" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">8 threads</text>
+ <text x="510" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">copy buffer</text>
+</svg>
--- /dev/null
+<svg viewBox="0 0 320 140" width="320" height="140" xmlns="http://www.w3.org/2000/svg">
+ <rect width="320" height="140" fill="#061018"/>
+
+ <!-- Dark polygon without ambient -->
+ <polygon points="40,120 140,120 120,40 80,40"
+ fill="rgba(10,10,10,0.8)" stroke="#555" stroke-width="1"/>
+ <text x="85" y="70" fill="#aaa" font-size="9" font-family="monospace" text-anchor="middle">no ambient</text>
+ <text x="85" y="82" fill="#aaa" font-size="8" font-family="monospace" text-anchor="middle">(pure black)</text>
+
+ <!-- Arrow -->
+ <text x="165" y="75" fill="#30a050" font-size="16" font-family="monospace">→</text>
+
+ <!-- Lit polygon with ambient -->
+ <polygon points="180,120 280,120 260,40 220,40"
+ fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1"/>
+ <text x="225" y="70" fill="#30a050" font-size="9" font-family="monospace" text-anchor="middle">ambient</text>
+ <text x="225" y="82" fill="#30a050" font-size="8" font-family="monospace" text-anchor="middle="(50,50,50)"></text>
+
+ <!-- Formula -->
+ <text x="160" y="135" fill="#bbb" font-size="9" font-family="monospace" text-anchor="middle">ambient provides base illumination</text>
+</svg>
--- /dev/null
+<svg viewBox="0 0 520 180" width="520" height="180" xmlns="http://www.w3.org/2000/svg">
+ <rect width="520" height="180" fill="#061018"/>
+
+ <!-- Light source -->
+ <circle cx="80" cy="90" r="10" fill="rgba(255,102,0,0.3)" stroke="#FF6600" stroke-width="2"/>
+ <text x="92" y="94" fill="#FF6600" font-size="11" font-weight="700" font-family="monospace">Light</text>
+
+ <!-- Distance markers -->
+ <line x1="80" y1="110" x2="450" y2="110" stroke="#555" stroke-width="1" stroke-dasharray="4 3"/>
+ <text x="180" y="125" fill="#aaa" font-size="8" font-family="monospace" text-anchor="middle">d = 100</text>
+ <text x="330" y="125" fill="#aaa" font-size="8" font-family="monospace" text-anchor="middle">d = 250</text>
+
+ <!-- Polygon at close distance -->
+ <polygon points="160,80 200,80 190,50 170,50"
+ fill="rgba(48,160,80,0.25)" stroke="#30a050" stroke-width="1.5"/>
+ <text x="175" y="72" fill="#30a050" font-size="8" font-weight="600" font-family="monospace">bright</text>
+
+ <!-- Polygon at medium distance -->
+ <polygon points="300,80 340,80 330,50 310,50"
+ fill="rgba(48,160,80,0.12)" stroke="#30a050" stroke-width="1"/>
+ <text x="315" y="72" fill="#30a050" font-size="8" font-family="monospace">medium</text>
+
+ <!-- Polygon at far distance -->
+ <polygon points="420,80 450,80 445,55 425,55"
+ fill="rgba(48,160,80,0.06)" stroke="rgba(48,160,80,0.4)" stroke-width="0.8"/>
+ <text x="433" y="72" fill="rgba(48,160,80,0.6)" font-size="8" font-family="monospace">dim</text>
+
+ <!-- Formula -->
+ <rect x="380" y="135" width="120" height="35" rx="3" fill="rgba(32,112,192,0.1)" stroke="#2070c0" stroke-width="1"/>
+ <text x="390" y="155" fill="#2070c0" font-size="9" font-family="monospace">attenuation =</text>
+ <text x="390" y="167" fill="#2070c0" font-size="9" font-family="monospace">1 / (1 + 0.0001·d²)</text>
+
+ <!-- Explanation -->
+ <text x="260" y="175" fill="#bbb" font-size="9" font-family="monospace" text-anchor="middle">Simplified inverse square law avoids harsh cutoffs</text>
+</svg>
:CUSTOM_ID: lambert-cosine-law
:END:
-#+BEGIN_EXPORT html
-<svg viewBox="0 0 520 240" width="520" height="240" xmlns="http://www.w3.org/2000/svg">
- <defs>
- <marker id="arrow-light" viewBox="0 0 10 10" refX="10" refY="5"
- markerWidth="7" markerHeight="7" orient="auto-start-reverse">
- <path d="M 0 0 L 10 5 L 0 10 z" fill="#FF6600"/>
- </marker>
- </defs>
- <rect width="520" height="240" fill="#061018"/>
-
- <!-- Polygon surface -->
- <polygon points="80,180 220,180 180,60 120,60"
- fill="rgba(48,160,80,0.12)" stroke="#30a050" stroke-width="1.5"/>
-
- <!-- Normal vector (perpendicular to surface) -->
- <line x1="150" y1="120" x2="150" y2="40" stroke="#30a050" stroke-width="2.5"/>
- <polygon points="150,40 145,52 155,52" fill="#30a050"/>
- <text x="158" y="38" fill="#30a050" font-size="12" font-weight="700" font-family="monospace">N̂</text>
- <text x="158" y="50" fill="#999" font-size="9" font-family="monospace">normal</text>
-
- <!-- Light source -->
- <circle cx="350" cy="80" r="12" fill="rgba(255,102,0,0.25)" stroke="#FF6600" stroke-width="2"/>
- <text x="368" y="84" fill="#FF6600" font-size="11" font-weight="700" font-family="monospace">Light</text>
-
- <!-- Light direction vector -->
- <line x1="350" y1="80" x2="170" y2="120" stroke="#FF6600" stroke-width="2" stroke-dasharray="5 3" marker-end="url(#arrow-light)"/>
- <text x="250" y="92" fill="#FF6600" font-size="10" font-family="monospace" text-anchor="middle">L̂</text>
-
- <!-- Angle arc between normal and light direction -->
- <path d="M150,60 A60,60 0 0,1 180,80" fill="none" stroke="#b09020" stroke-width="1.5"/>
- <text x="182" y="75" fill="#b09020" font-size="10" font-weight="700" font-family="monospace">θ</text>
-
- <!-- Formula box -->
- <rect x="380" y="140" width="120" height="80" rx="4" fill="rgba(32,112,192,0.1)" stroke="#2070c0" stroke-width="1"/>
- <text x="390" y="165" fill="#2070c0" font-size="11" font-weight="700" font-family="monospace">brightness =</text>
- <text x="390" y="185" fill="#2070c0" font-size="11" font-family="monospace">dot(N̂, L̂)</text>
- <text x="390" y="205" fill="#999" font-size="9" font-family="monospace">= cos(θ)</text>
-
- <!-- Brightness indicator -->
- <rect x="80" y="200" width="140" height="20" rx="2" fill="rgba(48,160,80,0.3)" stroke="#30a050" stroke-width="1"/>
- <rect x="80" y="200" width="85" height="20" rx="2" fill="#30a050"/>
- <text x="85" y="214" fill="#fff" font-size="9" font-weight="600" font-family="monospace">brightness</text>
- <text x="232" y="214" fill="#999" font-size="9" font-family="monospace">← angle determines intensity</text>
-</svg>
-#+END_EXPORT
+#+INCLUDE: "lambert-cosine-law.svg" export html
The *Lambert cosine law* states that the brightness of a surface depends
on the angle between its normal vector and the light direction:
:CUSTOM_ID: ambient-light
:END:
-#+BEGIN_EXPORT html
-<svg viewBox="0 0 320 140" width="320" height="140" xmlns="http://www.w3.org/2000/svg">
- <rect width="320" height="140" fill="#061018"/>
-
- <!-- Dark polygon without ambient -->
- <polygon points="40,120 140,120 120,40 80,40"
- fill="rgba(10,10,10,0.8)" stroke="#555" stroke-width="1"/>
- <text x="85" y="70" fill="#666" font-size="9" font-family="monospace" text-anchor="middle">no ambient</text>
- <text x="85" y="82" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">(pure black)</text>
-
- <!-- Arrow -->
- <text x="165" y="75" fill="#30a050" font-size="16" font-family="monospace">→</text>
-
- <!-- Lit polygon with ambient -->
- <polygon points="180,120 280,120 260,40 220,40"
- fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1"/>
- <text x="225" y="70" fill="#30a050" font-size="9" font-family="monospace" text-anchor="middle">ambient</text>
- <text x="225" y="82" fill="#30a050" font-size="8" font-family="monospace" text-anchor="middle="(50,50,50)"></text>
-
- <!-- Formula -->
- <text x="160" y="135" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">ambient provides base illumination</text>
-</svg>
-#+END_EXPORT
+#+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
:CUSTOM_ID: distance-attenuation
:END:
-#+BEGIN_EXPORT html
-<svg viewBox="0 0 520 180" width="520" height="180" xmlns="http://www.w3.org/2000/svg">
- <rect width="520" height="180" fill="#061018"/>
-
- <!-- Light source -->
- <circle cx="80" cy="90" r="10" fill="rgba(255,102,0,0.3)" stroke="#FF6600" stroke-width="2"/>
- <text x="92" y="94" fill="#FF6600" font-size="11" font-weight="700" font-family="monospace">Light</text>
-
- <!-- Distance markers -->
- <line x1="80" y1="110" x2="450" y2="110" stroke="#555" stroke-width="1" stroke-dasharray="4 3"/>
- <text x="180" y="125" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">d = 100</text>
- <text x="330" y="125" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">d = 250</text>
-
- <!-- Polygon at close distance -->
- <polygon points="160,80 200,80 190,50 170,50"
- fill="rgba(48,160,80,0.25)" stroke="#30a050" stroke-width="1.5"/>
- <text x="175" y="72" fill="#30a050" font-size="8" font-weight="600" font-family="monospace">bright</text>
-
- <!-- Polygon at medium distance -->
- <polygon points="300,80 340,80 330,50 310,50"
- fill="rgba(48,160,80,0.12)" stroke="#30a050" stroke-width="1"/>
- <text x="315" y="72" fill="#30a050" font-size="8" font-family="monospace">medium</text>
-
- <!-- Polygon at far distance -->
- <polygon points="420,80 450,80 445,55 425,55"
- fill="rgba(48,160,80,0.06)" stroke="rgba(48,160,80,0.4)" stroke-width="0.8"/>
- <text x="433" y="72" fill="rgba(48,160,80,0.6)" font-size="8" font-family="monospace">dim</text>
-
- <!-- Formula -->
- <rect x="380" y="135" width="120" height="35" rx="3" fill="rgba(32,112,192,0.1)" stroke="#2070c0" stroke-width="1"/>
- <text x="390" y="155" fill="#2070c0" font-size="9" font-family="monospace">attenuation =</text>
- <text x="390" y="167" fill="#2070c0" font-size="9" font-family="monospace">1 / (1 + 0.0001·d²)</text>
-
- <!-- Explanation -->
- <text x="260" y="175" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">Simplified inverse square law avoids harsh cutoffs</text>
-</svg>
-#+END_EXPORT
+#+INCLUDE: "distance-attenuation.svg" export html
Light intensity decreases with distance using a *simplified inverse
square law*:
:CUSTOM_ID: render-pipeline-integration
:END:
-#+BEGIN_EXPORT html
-<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="#999" 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>
-#+END_EXPORT
+#+INCLUDE: "shading-pipeline.svg" export html
Lighting is computed during *Phase 2* (transform phase) of the
[[file:../rendering-loop/][rendering loop]]:
--- /dev/null
+<svg viewBox="0 0 520 240" width="520" height="240" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <marker id="arrow-light" viewBox="0 0 10 10" refX="10" refY="5"
+ markerWidth="7" markerHeight="7" orient="auto-start-reverse">
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#FF6600"/>
+ </marker>
+ </defs>
+ <rect width="520" height="240" fill="#061018"/>
+
+ <!-- Polygon surface -->
+ <polygon points="80,180 220,180 180,60 120,60"
+ fill="rgba(48,160,80,0.12)" stroke="#30a050" stroke-width="1.5"/>
+
+ <!-- Normal vector (perpendicular to surface) -->
+ <line x1="150" y1="120" x2="150" y2="40" stroke="#30a050" stroke-width="2.5"/>
+ <polygon points="150,40 145,52 155,52" fill="#30a050"/>
+ <text x="158" y="38" fill="#30a050" font-size="12" font-weight="700" font-family="monospace">N̂</text>
+ <text x="158" y="50" fill="#bbb" font-size="9" font-family="monospace">normal</text>
+
+ <!-- Light source -->
+ <circle cx="350" cy="80" r="12" fill="rgba(255,102,0,0.25)" stroke="#FF6600" stroke-width="2"/>
+ <text x="368" y="84" fill="#FF6600" font-size="11" font-weight="700" font-family="monospace">Light</text>
+
+ <!-- Light direction vector -->
+ <line x1="350" y1="80" x2="170" y2="120" stroke="#FF6600" stroke-width="2" stroke-dasharray="5 3" marker-end="url(#arrow-light)"/>
+ <text x="250" y="92" fill="#FF6600" font-size="10" font-family="monospace" text-anchor="middle">L̂</text>
+
+ <!-- Angle arc between normal and light direction -->
+ <path d="M150,60 A60,60 0 0,1 180,80" fill="none" stroke="#b09020" stroke-width="1.5"/>
+ <text x="182" y="75" fill="#b09020" font-size="10" font-weight="700" font-family="monospace">θ</text>
+
+ <!-- Formula box -->
+ <rect x="380" y="140" width="120" height="80" rx="4" fill="rgba(32,112,192,0.1)" stroke="#2070c0" stroke-width="1"/>
+ <text x="390" y="165" fill="#2070c0" font-size="11" font-weight="700" font-family="monospace">brightness =</text>
+ <text x="390" y="185" fill="#2070c0" font-size="11" font-family="monospace">dot(N̂, L̂)</text>
+ <text x="390" y="205" fill="#bbb" font-size="9" font-family="monospace">= cos(θ)</text>
+
+ <!-- Brightness indicator -->
+ <rect x="80" y="200" width="140" height="20" rx="2" fill="rgba(48,160,80,0.3)" stroke="#30a050" stroke-width="1"/>
+ <rect x="80" y="200" width="85" height="20" rx="2" fill="#30a050"/>
+ <text x="85" y="214" fill="#fff" font-size="9" font-weight="600" font-family="monospace">brightness</text>
+ <text x="232" y="214" fill="#bbb" font-size="9" font-family="monospace">← angle determines intensity</text>
+</svg>
--- /dev/null
+<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>
--- /dev/null
+<svg viewBox="0 0 320 240" width="320" height="240">
+ <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="320" height="240" fill="#061018"/>
+ <!-- Green front-face triangle: V1=top, V2=bottom-left, V3=bottom-right -->
+ <polygon points="80,50 130,180 30,180" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+ <!-- CCW arrow: arc from near V1, curves LEFT and DOWN toward V2 -->
+ <path d="M70,72 A 52,52 0 0,0 37,155" fill="none" stroke="#30a050" stroke-width="1.5" stroke-dasharray="4 2" marker-end="url(#arrow-green)"/>
+ <text x="34" y="120" fill="#30a050" font-size="10" font-weight="700" font-family="monospace">CCW</text>
+ <circle cx="80" cy="50" r="3" fill="#30a050"/>
+ <circle cx="30" cy="180" r="3" fill="#30a050"/>
+ <circle cx="130" cy="180" r="3" fill="#30a050"/>
+ <text x="78" y="44" fill="#aaa" font-size="9" font-family="monospace">V₁</text>
+ <text x="14" y="198" fill="#aaa" font-size="9" font-family="monospace">V₂</text>
+ <text x="132" y="198" fill="#aaa" font-size="9" font-family="monospace">V₃</text>
+ <text x="36" y="220" fill="#30a050" font-size="11" font-weight="700" font-family="monospace">FRONT FACE ✓</text>
+ <!-- Red back-face triangle -->
+ <polygon points="240,50 290,180 190,180" fill="rgba(208,64,64,0.06)" stroke="rgba(208,64,64,0.3)" stroke-width="1.5" stroke-dasharray="6 3"/>
+ <!-- CW arrow: arc from near V1, curves RIGHT and DOWN -->
+ <path d="M250,72 A 52,52 0 0,1 283,155" fill="none" stroke="rgba(208,64,64,0.5)" stroke-width="1.5" stroke-dasharray="4 2" marker-end="url(#arrow-red)"/>
+ <text x="268" y="120" fill="rgba(208,64,64,0.6)" font-size="10" font-weight="700" font-family="monospace">CW</text>
+ <line x1="228" y1="108" x2="252" y2="132" stroke="rgba(208,64,64,0.4)" stroke-width="3"/>
+ <line x1="252" y1="108" x2="228" y2="132" stroke="rgba(208,64,64,0.4)" stroke-width="3"/>
+ <text x="186" y="220" fill="rgba(208,64,64,0.7)" font-size="11" font-weight="700" font-family="monospace">BACK FACE ✗</text>
+ <text x="195" y="234" fill="#aaa" font-size="9" font-family="monospace">(culled — not drawn)</text>
+</svg>
* @param t the interpolation parameter (0 to 1)
* @return a new Point3D representing the interpolated position
*/
- public Point3D lerp(final Point3D other, final double t) {
+ public Point3D interpolate(final Point3D other, final double t) {
return new Point3D(
x + (other.x - x) * t,
y + (other.y - y) * t,
*/
public Vertex interpolate(final Vertex other, final double t) {
final Vertex result = new Vertex(
- coordinate.lerp(other.coordinate, t),
+ coordinate.interpolate(other.coordinate, t),
(textureCoordinate != null && other.textureCoordinate != null)
? new Point2D(
textureCoordinate.x + (other.textureCoordinate.x - textureCoordinate.x) * t,
: null
);
if (normal != null && other.normal != null) {
- result.normal = normal.lerp(other.normal, t);
+ result.normal = normal.interpolate(other.normal, t);
}
return result;
}