--- /dev/null
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Constructive Solid Geometry - Sixth 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+begin_export html
+<style>
+ .flex-center {
+ display: flex;
+ justify-content: center;
+ }
+ .flex-center video {
+ width: min(90%, 1000px);
+ height: auto;
+ }
+ .responsive-img {
+ width: min(100%, 1000px);
+ height: auto;
+ }
+
+ /* === SVG diagram theme === */
+ svg > rect:first-child {
+ fill: #061018;
+ }
+
+ /* Lighten axis/helper labels that were dark-on-light */
+ svg text[fill="#666"],
+ svg text[fill="#999"] {
+ fill: #aaa !important;
+ }
+
+ /* Lighten dashed axis lines */
+ svg line[stroke="#ccc"] {
+ stroke: #445566 !important;
+ }
+
+</style>
+#+end_export
+
+[[file:../index.org][Back to main documentation]]
+
+* What is CSG?
+:PROPERTIES:
+:CUSTOM_ID: what-is-csg
+:ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
+:END:
+
+*Constructive Solid Geometry* (CSG) is a modeling technique that builds
+complex 3D shapes by combining simpler primitives using boolean
+operations. Instead of manually creating every vertex and face, you
+define shapes as the result of operations like "merge these two cubes"
+or "carve a hole using this sphere."
+
+CSG is particularly powerful for:
+- *Procedural modeling* — generate complex geometry algorithmically
+- *CAD/CAM applications* — define parts as combinations of primitives
+- *Game development* — create architectural elements, holes, cavities
+- *Rapid prototyping* — iterate on designs by adjusting operations
+
+The three fundamental CSG operations are:
+
+| Operation | Symbol | Result |
+|-------------+--------+-------------------------------------------|
+| Subtract | A - B | A with B carved out (holes, cavities) |
+| Union | A + B | Combined volume (both shapes merged) |
+| Intersect | A ∩ B | Volume where both overlap |
+
+See the [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/#csg-demo][CSG demo]] for an interactive visualization.
+
+* The Three Operations
+:PROPERTIES:
+:CUSTOM_ID: the-three-operations
+:END:
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 1000px
+[[https://www3.svjatoslav.eu/projects/sixth-3d-demos/#csg-demo][file:CSG%20demo.png]]
+
+The screenshot above shows all three operations displayed left to right:
+subtract (green cube with spherical cavity), union (merged green and
+orange shapes), and intersect (only the overlapping region in blue).
+
+The diagrams below use the same green cube (A) and orange sphere (B) as
+the screenshot above. Each operation transforms these inputs differently,
+producing the results shown from left to right in the image.
+
+** Subtract (A - B)
+:PROPERTIES:
+:CUSTOM_ID: subtract-operation
+:END:
+
+#+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
+
+*Subtract* removes the orange sphere (B) from the green cube (A), carving
+out a cavity. The diagram shows B acting as a "cutter" — where it overlaps
+A, a hole is created. Interior faces *are preserved* and become visible,
+allowing you to see inside the carved-out space (shown as the orange dashed
+curve in the result).
+
+This matches the leftmost shape in the screenshot: a green cube with a
+visible spherical hollow inside, showing the interior surfaces created by
+the subtraction.
+
+This operation is ideal for creating:
+- Holes and tunnels
+- Carved-out spaces
+- Hollow objects
+
+#+BEGIN_SRC java
+SolidPolygonCube cube = new SolidPolygonCube(origin(), 80, Color.GREEN);
+SolidPolygonSphere sphere = new SolidPolygonSphere(origin(), 60, 8, Color.ORANGE);
+
+cube.subtract(sphere); // cube now has a spherical cavity
+#+END_SRC
+
+** Union (A + B)
+:PROPERTIES:
+:CUSTOM_ID: union-operation
+:END:
+
+#+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
+
+*Union* merges the green cube (A) and orange sphere (B) into one continuous
+volume. The diagram shows both shapes combining — the interior seam (where
+they overlap) is removed, creating a single solid surface with no internal
+boundaries (indicated by the dashed blue line labeled "removed").
+
+This corresponds to the center shape in the screenshot: both green and
+orange colors present but seamlessly joined, forming one unified object.
+
+#+BEGIN_SRC java
+SolidPolygonCube cube = new SolidPolygonCube(origin(), 80, Color.GREEN);
+SolidPolygonSphere sphere = new SolidPolygonSphere(origin(), 60, 8, Color.ORANGE);
+
+cube.union(sphere); // cube now contains the merged result
+#+END_SRC
+
+** Intersect (A ∩ B)
+:PROPERTIES:
+:CUSTOM_ID: intersect-operation
+:END:
+
+#+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
+
+*Intersect* keeps only the volume where the green cube (A) and orange
+sphere (B) overlap — the region that is inside *both* shapes
+simultaneously. The diagram shows this as the blue-shaded area: the
+portion of the sphere that fits within the cube boundaries. Everything
+else is discarded.
+
+This is the rightmost shape in the screenshot: only the overlapping
+portion remains, showing which parts of space were occupied by both the
+cube and sphere at the same time.
+
+This operation is useful for:
+- Creating shapes constrained by multiple boundaries
+- Finding collision regions
+- Trimming geometry to fit within bounds
+
+#+BEGIN_SRC java
+SolidPolygonCube cube = new SolidPolygonCube(origin(), 80, Color.GREEN);
+SolidPolygonSphere sphere = new SolidPolygonSphere(origin(), 60, 8, Color.ORANGE);
+
+cube.intersect(sphere); // only the overlapping region remains
+#+END_SRC
+
+* BSP Tree Algorithm
+:PROPERTIES:
+:CUSTOM_ID: bsp-tree-algorithm
+:END:
+
+CSG boolean operations are implemented using *Binary Space Partitioning*
+(BSP) trees. A BSP tree recursively divides 3D space using planes,
+creating a hierarchical structure that enables efficient polygon clipping
+and spatial queries.
+
+** BSP Tree Structure
+:PROPERTIES:
+:CUSTOM_ID: bsp-tree-structure
+:END:
+
+#+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
+
+Each BSP node contains:
+- A *partitioning plane* that divides space into two half-spaces
+- *Polygons* that lie exactly on this plane (coplanar)
+- *Front* subtree — polygons on the same side as the plane's normal
+- *Back* subtree — polygons on the opposite side
+
+** Key BSP Operations
+:PROPERTIES:
+:CUSTOM_ID: key-bsp-operations
+:END:
+
+The BSP tree provides three core operations that enable CSG:
+
+| Operation | Description |
+|----------------+--------------------------------------------------|
+| =invert()= | Flip all normals, swap front/back children |
+| =clipTo(tree)= | Remove polygons inside the other tree's solid |
+| =addPolygons()= | Insert new polygons, splitting at planes |
+
+*Invert* is fundamental to CSG. By flipping inside/outside, we can
+transform subtraction and intersection into variations of clipping:
+
+- **Subtract** = invert A, clip against B, add B's clipped parts, invert back
+- **Intersect** = invert A, clip B against A, invert both, combine, invert back
+
+** Polygon Clipping
+:PROPERTIES:
+:CUSTOM_ID: polygon-clipping
+:END:
+
+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
+
+This recursive splitting ensures that all polygons are cleanly classified
+as entirely in front, entirely behind, or exactly on a plane — never
+"spanning" across.
+
+* Using CSG in Sixth 3D
+:PROPERTIES:
+:CUSTOM_ID: using-csg-in-sixth-3d
+:END:
+
+CSG operations are methods on [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]. They modify the
+shape *in-place* — the result replaces the original geometry.
+
+** Basic Usage
+:PROPERTIES:
+:CUSTOM_ID: basic-usage
+:END:
+
+#+BEGIN_SRC java
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.*;
+
+// Create two shapes
+SolidPolygonCube cube = new SolidPolygonCube(origin(), 100, Color.GREEN);
+SolidPolygonSphere sphere = new SolidPolygonSphere(origin(), 70, 12, Color.ORANGE);
+
+// Perform CSG operations (in-place modification)
+cube.subtract(sphere); // Cube with spherical cavity
+// or
+cube.union(sphere); // Merged shape
+// or
+cube.intersect(sphere); // Only overlapping region
+
+// Add to scene
+shapes.addShape(cube.setBackfaceCulling(true));
+#+END_SRC
+
+** Child Handling Behavior
+:PROPERTIES:
+:CUSTOM_ID: child-handling
+:END:
+
+CSG operations only affect *SolidPolygon* children. Other children are
+preserved:
+
+| Child Type | Union | Subtract | Intersect |
+|-----------------------+------------------+------------------+------------------|
+| SolidPolygon (this) | Replaced with result | Replaced with result | Replaced with result |
+| SolidPolygon (other) | Merged into result | Discarded (cutter) | Discarded |
+| Line, TextCanvas | Preserved | Preserved | Preserved |
+| Nested composite | Preserved unchanged | Preserved unchanged | Preserved unchanged |
+
+This allows you to attach labels, decorations, or wireframe overlays to
+shapes without them being affected by CSG operations.
+
+** Important Notes
+:PROPERTIES:
+:CUSTOM_ID: important-notes
+:END:
+
+1. *Shapes are modified in-place*. The original geometry is replaced.
+ Clone shapes beforehand if you need to preserve the originals.
+
+2. *CSG works on SolidPolygon children only*. TexturedTriangle and other
+ shape types are not processed.
+
+3. *Result quality depends on mesh density*. Low-polygon inputs may
+ produce visible artifacts at intersection boundaries. Use higher
+ subdivision counts for smoother results.
+
+4. *Backface culling is recommended*. CSG results often have internal
+ faces from the cutting operation. Enable culling to hide backfaces:
+ =shape.setBackfaceCulling(true)=
+
+* Related Classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class | Purpose |
+|--------------------------+------------------------------------------------------|
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/BspTree.html][BspTree]] | BSP tree for spatial partitioning and CSG operations |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Plane.html][Plane]] | Partitioning plane used by BSP nodes |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Polygon shape processed by CSG |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] | Base class with union/subtract/intersect methods |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.html][SolidPolygonMesh]] | Custom polygon mesh for arbitrary geometry |
+| SolidPolygon* primitives | See [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/#shape-gallery][Shape Gallery]] for all available shapes |
--- /dev/null
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Frustum & View Frustum Culling - Sixth 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+begin_export html
+<style>
+ .flex-center {
+ display: flex;
+ justify-content: center;
+ }
+ .flex-center video {
+ width: min(90%, 1000px);
+ height: auto;
+ }
+ .responsive-img {
+ width: min(100%, 1000px);
+ height: auto;
+ }
+
+ /* === SVG diagram theme === */
+ svg > rect:first-child {
+ fill: #061018;
+ }
+
+ /* Lighten axis/helper labels that were dark-on-light */
+ svg text[fill="#666"],
+ svg text[fill="#999"] {
+ fill: #aaa !important;
+ }
+
+ /* Lighten dashed axis lines */
+ svg line[stroke="#ccc"] {
+ stroke: #445566 !important;
+ }
+
+</style>
+#+end_export
+
+[[file:../index.org][Back to main documentation]]
+
+* Frustum & View Frustum Culling
+:PROPERTIES:
+: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
+
+The *view frustum* is a truncated pyramid-shaped volume that represents
+everything the camera can see. Objects completely outside this volume are
+skipped during rendering — a powerful optimization called *frustum culling*.
+
+** The Six Frustum Planes
+:PROPERTIES:
+:CUSTOM_ID: frustum-planes
+:END:
+
+The frustum is defined by six clipping planes:
+
+| Plane | Purpose |
+|---------+--------------------------------------------|
+| Left | Left edge of viewport |
+| Right | Right edge of viewport |
+| Top | Top edge of viewport (smaller Y in Y-down) |
+| Bottom | Bottom edge of viewport (larger Y) |
+| Near | Closest visible distance from camera |
+| Far | Farthest visible distance from camera |
+
+Each plane divides 3D space into "inside" (visible) and "outside"
+(culled). An object must pass all six plane tests to be considered
+potentially visible.
+
+** Frustum Culling vs Backface Culling
+:PROPERTIES:
+:CUSTOM_ID: frustum-vs-backface-culling
+:END:
+
+These are complementary optimizations at different levels:
+
+| Optimization | Level | What it skips |
+|-----------------+--------------+----------------------------------|
+| Frustum culling | Object level | Entire composite shapes + children |
+| Backface culling | Polygon level | Individual triangles facing away |
+
+*Frustum culling* happens first during the transform phase — entire
+object trees are skipped with a single bounding box test. *Backface
+culling* happens later during rasterization — individual triangles
+are checked before being drawn.
+
+For best performance, use both: organize your scene with composite
+shapes for effective frustum culling, and enable backface culling on
+closed meshes.
+
+* How Frustum Culling Works in Sixth 3D
+:PROPERTIES:
+:CUSTOM_ID: frustum-culling-implementation
+:END:
+
+Frustum culling is applied automatically to all [[../index.org#mesh][composite shapes]]
+during Phase 2 of the [[../rendering-loop/][rendering loop]]:
+
+1. *Update frustum*: Compute 6 planes from camera FOV and viewport size
+2. *For each composite shape*:
+ - Get its [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Box.html][Axis-Aligned Bounding Box (AABB)]]
+ - Transform all 8 corners to view space
+ - Test against frustum using [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Frustum.html#intersectsAABB][intersectsAABB()]]
+ - If outside: skip the entire composite and all children
+ - If inside: continue transforming children
+
+** The AABB Intersection Algorithm
+:PROPERTIES:
+:CUSTOM_ID: aabb-intersection-algorithm
+:END:
+
+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
+
+For each plane, instead of testing all 8 corners of the bounding box,
+we test only the *P-vertex* — the corner most aligned with the plane
+normal. If this "best" corner is behind the plane, the entire box must
+be outside the frustum.
+
+- Plane normal points *into* the frustum (toward visible region)
+- P-vertex: select corner based on normal direction
+ - If normal.x > 0 → use maxX (rightmost corner)
+ - If normal.x < 0 → use minX (leftmost corner)
+ - Same logic for Y and Z
+- Test: =dot(normal, P-vertex) < distance= → outside
+
+This reduces from 48 tests (8 corners × 6 planes) to just 6 tests per
+object.
+
+* Performance Benefits
+:PROPERTIES:
+:CUSTOM_ID: frustum-performance
+:END:
+
+Frustum culling can dramatically improve performance for large scenes:
+
+- *High cull % (60-90%)*: Excellent — most objects skipped entirely
+- *Medium cull % (20-60%)*: Moderate benefit
+- *Low cull % (0-20%)*: Limited benefit — most objects visible
+
+A composite shape that is culled skips:
+- Transforming all its children
+- Computing bounding boxes for children
+- All polygon-level operations (backface culling, rasterization)
+
+Open Developer Tools (F12) to see real-time [[../index.org#frustum-culling-statistics][frustum culling statistics]].
+
+* Scene Design for Effective Culling
+:PROPERTIES:
+:CUSTOM_ID: frustum-scene-design
+:END:
+
+Frustum culling works best when you organize your scene into
+well-defined composite shapes:
+
+#+BEGIN_SRC java
+// Good: Each building is a separate composite
+AbstractCompositeShape cityBlock = new AbstractCompositeShape();
+for (Building building : buildings) {
+ AbstractCompositeShape buildingComposite = new AbstractCompositeShape();
+ buildingComposite.add(buildingWalls);
+ buildingComposite.add(buildingRoof);
+ buildingComposite.add(buildingInterior);
+ cityBlock.add(buildingComposite);
+}
+
+// Less effective: Everything in one giant composite
+AbstractCompositeShape allObjects = new AbstractCompositeShape();
+allObjects.add(building1Walls);
+allObjects.add(building1Roof);
+allObjects.add(building2Walls);
+// ... hundreds of shapes directly in root
+#+END_SRC
+
+*Best practices:*
+
+- Use composites to group objects that occupy a bounded region of space
+- Keep bounding boxes tight (don't add distant objects to the same composite)
+- Nest composites hierarchically for multi-level culling (city → block → building)
+- Call [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractShape.html#updateBoundingBox()][updateBoundingBox()]] after moving shapes
+
+* Technical Details
+:PROPERTIES:
+:CUSTOM_ID: frustum-technical-details
+:END:
+
+The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Frustum.html][Frustum]] class:
+
+- Computes planes in *view space* (camera at origin, looking along +Z)
+- FOV derived from =projectionScale = width / 3= (≈112° horizontal FOV)
+- Default clip distances: Near = 1.0, Far = 10000.0
+- Planes stored in [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Plane.html][Hesse normal form]]: (normal vector, distance)
+
+The frustum is updated once per frame in
+[[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.html][ShapeCollection.transformAllShapes()]] before processing composite
+shapes.
ecosystem of pain — version mismatches, incomplete implementations,
vendor-specific workarounds. I want a library that "just works".
-Sixth 3D takes a different path. By rendering everything in software
+*Sixth 3D* takes a different path. By rendering everything in software
on the CPU, the entire GPU problem space simply disappears. You add a
Maven dependency, write some Java, and you have a 3D scene. It runs
wherever Java runs.
everything through a fixed GPU pipeline, you can implement clever,
application-specific optimizations.
-Sixth 3D is part of the larger [[https://www3.svjatoslav.eu/projects/sixth/][Sixth project]], with the long-term goal
+*Sixth 3D* is part of the larger [[https://www3.svjatoslav.eu/projects/sixth/][Sixth project]], with the long-term goal
of providing a platform for 3D user interfaces and interactive data
visualization. It can also be used as a standalone 3D engine in any
Java project. See the [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demos]] for examples of what it can do today.
:ID: 4b6c1355-0afe-40c6-86c3-14bf8a11a8d0
:END:
-- To understand main render loop, see dedicated page: [[file:rendering-loop.org][Rendering loop]]
-
-- To understand perspective-correct texture mapping, see dedicated
- page: [[file:perspective-correct-textures/][Perspective-correct textures]]
-
- Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]] for practical examples. Start with [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/#minimal-example][minimal
example]].
+** Main render loop
+
+- To understand main render loop, see dedicated page: [[file:rendering-loop/][Rendering loop]]
+
** Coordinate System (X, Y, Z)
:PROPERTIES:
:CUSTOM_ID: coordinate-system
</svg>
#+END_EXPORT
-Sixth 3D uses a **left-handed coordinate system with X pointing right
+*Sixth 3D* uses a **left-handed coordinate system with X pointing right
and Y pointing down**, matching standard 2D screen coordinates. This
coordinate system should feel intuitive for people with preexisting 2D
graphics background.
- 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.
+- Vertex maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Point3D.html][Point3D]] class in *Sixth 3D* engine.
** Edge
:PROPERTIES:
- 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.
+- 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*.
** Normal Vector
:PROPERTIES:
- Cube: 8 vertices, 12 triangles
- Smooth sphere: hundreds–thousands of triangles
- =vertices[] + indices[]= → efficient storage
-- In Sixth 3D engine:
+- 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
+all primitive shapes available in *Sixth 3D*, rendered in both
wireframe and solid polygon styles with dynamic lighting.
+** Perspective correct textures
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 1000px
+[[file:perspective-correct-textures/Affine distortion.png]]
+
+*Sixth 3D* tries to do perspective-correct texture rendering. Read more
+about [[file:perspective-correct-textures/][perspective-correct texture implementation]].
+
+** Frustum & View Frustum Culling
+
+*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
+
+To understand frustum culling and object-level visibility
+optimization, read more about [[file:frustum-culling/][frustum & view frustum culling.]]
+
+** Constructive Solid Geometry
+
+*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
+
+To understand CSG boolean operations, read more about [[file:csg/][Constructive
+Solid Geometry]].
+
** Winding Order & Backface Culling
:PROPERTIES:
:CUSTOM_ID: winding-order-backface-culling
#+END_EXPORT
The order in which a triangle's vertices are listed determines its
-*winding order*. In Sixth 3D, screen coordinates have Y-axis pointing
+*winding order*. In *Sixth 3D*, screen coordinates have Y-axis pointing
*down*, which inverts the apparent winding direction compared to
standard mathematical convention (Y-up). *Counter-clockwise (CCW)* in
screen space means front-facing. *Backface culling* skips rendering
(in Y-down screen coordinates, negative signed area corresponds to
visually CCW winding)
-In Sixth 3D, backface culling is *optional* and disabled by default. Enable it per-shape:
-- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidTriangle.html#setBackfaceCulling(boolean)][SolidTriangle.setBackfaceCulling(true)]]
+In *Sixth 3D*, backface culling is *optional* and disabled by default. Enable it per-shape:
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#setBackfaceCulling(boolean)][SolidPolygon.setBackfaceCulling(true)]]
- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html#setBackfaceCulling(boolean)][TexturedTriangle.setBackfaceCulling(true)]]
- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] (applies to all
sub-shapes)
+See the [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/#winding-order][Winding Order demo]] for an interactive visualization.
+
** Working with Colors
:PROPERTIES:
:CUSTOM_ID: working-with-colors
:ID: f2c9642a-a093-444f-8992-76c97ff28c16
:END:
-Sixth 3D uses its own Color class (not java.awt.Color):
+*Sixth 3D* uses its own Color class (not java.awt.Color):
#+BEGIN_SRC java
import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
:END:
- Study how [[id:4b6c1355-0afe-40c6-86c3-14bf8a11a8d0][scene definition]] works.
-- Understand [[file:rendering-loop.org][main rendering loop]].
+- Understand [[file:rendering-loop/][main rendering loop]].
- Read online [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/][JavaDoc]].
-- See [[https://www3.svjatoslav.eu/projects/sixth-3d/graphs/][Sixth 3D class diagrams]]. (Diagrams were generated by using
+- See [[https://www3.svjatoslav.eu/projects/sixth-3d/graphs/][*Sixth 3D* class diagrams]]. (Diagrams were generated by using
[[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility)
- Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]].
#+attr_latex: :width 1000px
[[file:Affine distortion.png]]
-The Sixth 3D engine solves this through *adaptive polygon tessellation*.
+The *Sixth 3D* engine solves this through *adaptive polygon tessellation*.
Instead of computing true perspective-correct interpolation per pixel
(which is expensive), the engine subdivides large triangles into
smaller pieces. Each sub-triangle is rendered with simple affine
+++ /dev/null
-#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
-#+TITLE: Rendering Loop - Sixth 3D
-#+LANGUAGE: en
-#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
-#+LATEX_HEADER: \usepackage{parskip}
-#+LATEX_HEADER: \usepackage[none]{hyphenat}
-
-#+OPTIONS: H:20 num:20
-#+OPTIONS: author:nil
-
-#+begin_export html
-<style>
- .flex-center {
- display: flex;
- justify-content: center;
- }
- .flex-center video {
- width: min(90%, 1000px);
- height: auto;
- }
- .responsive-img {
- width: min(100%, 1000px);
- height: auto;
- }
-</style>
-#+end_export
-
-[[file:index.org][Back to main documentation]]
-
-* Rendering loop
-:PROPERTIES:
-:CUSTOM_ID: rendering-loop
-:ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
-:END:
-
-The rendering loop is the heart of the engine, continuously generating
-frames on a dedicated background thread. It orchestrates the entire
-rendering pipeline from 3D world space to pixels on screen.
-
-** Main loop structure
-:PROPERTIES:
-:CUSTOM_ID: main-loop-structure
-:END:
-
-The render thread runs continuously in a dedicated daemon thread:
-
-#+BEGIN_SRC java
-while (renderThreadRunning) {
- ensureThatViewIsUpToDate(); // Render one frame
- maintainTargetFps(); // Sleep if needed
-}
-#+END_SRC
-
-The thread is a daemon, so it automatically stops when the JVM exits.
-You can stop it explicitly with [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html#stop()][ViewPanel.stop()]].
-
-** Frame rate control
-:PROPERTIES:
-:CUSTOM_ID: frame-rate-control
-:END:
-
-The engine supports two modes:
-
-- *Target FPS mode*: Set with [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html#setFrameRate(int)][setFrameRate(int)]]. The thread sleeps
- between frames to maintain the target rate. If rendering takes
- longer than the frame interval, the engine catches up naturally
- without sleeping.
-
-- *Unlimited mode*: Set =setFrameRate(0)= or negative. No sleeping —
- renders as fast as possible. Useful for benchmarking.
-
-** Frame listeners
-:PROPERTIES:
-:CUSTOM_ID: frame-listeners
-:END:
-
-Before each frame, the engine notifies all registered [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/FrameListener.html][FrameListener]]s:
-
-#+BEGIN_SRC java
-viewPanel.addFrameListener((panel, deltaMs) -> {
- // Update animations, physics, game logic
- shape.rotate(0.01);
- return true; // true = force repaint
-});
-#+END_SRC
-
-Frame listeners can trigger repaints by returning =true=. Built-in listeners include:
-- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/Camera.html][Camera]] — handles keyboard/mouse navigation
-- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.html][InputManager]] — processes input events
-
-* Rendering phases
-:PROPERTIES:
-:CUSTOM_ID: rendering-phases
-:END:
-
-Each frame goes through 6 phases. Open the Developer Tools panel (F12)
-to see these phases logged in real-time:
-
-** Phase 1: Clear canvas
-:PROPERTIES:
-:CUSTOM_ID: phase-1-clear-canvas
-:END:
-
-The pixel buffer is filled with the background color (default: black).
-
-#+BEGIN_SRC java
-Arrays.fill(pixels, 0, width * height, backgroundColorRgb);
-#+END_SRC
-
-This is a simple =Arrays.fill= operation — very fast, single-threaded.
-
-** Phase 2: Transform shapes
-:PROPERTIES:
-:CUSTOM_ID: phase-2-transform-shapes
-:END:
-
-All shapes are transformed from world space to screen space:
-
-1. Build camera-relative transform (inverse of camera position/rotation)
-2. For each shape:
- - Apply camera transform
- - Project 3D → 2D (perspective projection)
- - Calculate =onScreenZ= for depth sorting
- - Queue for rendering
-
-This is single-threaded but very fast — just math, no pixel operations.
-
-** Phase 3: Sort shapes
-:PROPERTIES:
-:CUSTOM_ID: phase-3-sort-shapes
-:END:
-
-Shapes are sorted by =onScreenZ= (depth) in descending order:
-
-#+BEGIN_SRC java
-Collections.sort(queuedShapes, (a, b) -> Double.compare(b.onScreenZ, a.onScreenZ));
-#+END_SRC
-
-Back-to-front sorting is essential for correct transparency and
-occlusion. Shapes further from the camera are painted first.
-
-** Phase 4: Paint shapes (multi-threaded)
-:PROPERTIES:
-:CUSTOM_ID: phase-4-paint-shapes
-:END:
-
-The screen is divided into 8 horizontal segments, each rendered by a separate thread:
-
-#+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
-
-Each thread:
-- Gets a =SegmentRenderingContext= with Y-bounds (minY, maxY)
-- Iterates all shapes and paints pixels within its Y-range
-- Clips triangles/lines at segment boundaries
-- Detects mouse hits (before clipping)
-
-A =CountDownLatch= waits for all 8 threads to complete before proceeding.
-
-**Why 8 segments?** This matches the typical core count of modern CPUs.
-The fixed thread pool (=Executors.newFixedThreadPool(8)=) avoids the
-overhead of creating threads per frame.
-
-** Phase 5: Combine mouse results
-:PROPERTIES:
-:CUSTOM_ID: phase-5-combine-mouse-results
-:END:
-
-During painting, each segment tracks which shape is under the mouse cursor.
-Since all segments paint the same shapes (just different Y-ranges), they
-should all report the same hit. Phase 5 takes the first non-null result:
-
-#+BEGIN_SRC java
-for (SegmentRenderingContext ctx : segmentContexts) {
- if (ctx.getSegmentMouseHit() != null) {
- renderingContext.setCurrentObjectUnderMouseCursor(ctx.getSegmentMouseHit());
- break;
- }
-}
-#+END_SRC
-
-** Phase 6: Blit to screen
-:PROPERTIES:
-:CUSTOM_ID: phase-6-blit-to-screen
-:END:
-
-The rendered =BufferedImage= is copied to the screen using
-[[https://docs.oracle.com/javase/21/docs/api/java/awt/image/BufferStrategy.html][BufferStrategy]] for tear-free page-flipping:
-
-#+BEGIN_SRC java
-do {
- Graphics2D g = bufferStrategy.getDrawGraphics();
- g.drawImage(renderingContext.bufferedImage, 0, 0, null);
- g.dispose();
-} while (bufferStrategy.contentsRestored());
-
-bufferStrategy.show();
-Toolkit.getDefaultToolkit().sync();
-#+END_SRC
-
-The =do-while= loop handles the case where the OS recreates the back
-buffer (common during window resizing). Since our offscreen
-=BufferedImage= still has the correct pixels, we only need to re-blit,
-not re-render.
-
-* Smart repaint skipping
-:PROPERTIES:
-:CUSTOM_ID: smart-repaint-skipping
-:END:
-
-The engine avoids unnecessary rendering:
-
-- =viewRepaintNeeded= flag: Set to =true= only when something changes
-- Frame listeners can return =false= to skip repaint
-- Resizing, component events, and explicit =repaintDuringNextViewUpdate()=
- calls set the flag
-
-This means a static scene consumes almost zero CPU — the render thread
-just spins checking the flag.
-
-* Rendering context
-:PROPERTIES:
-:CUSTOM_ID: rendering-context
-:END:
-
-The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/RenderingContext.html][RenderingContext]] holds all state for a single frame:
-
-| Field | Purpose |
-|-------+---------|
-| =pixels[]= | Raw pixel buffer (int[] in RGB format) |
-| =bufferedImage= | Java2D wrapper around pixels |
-| =graphics= | Graphics2D for text, lines, shapes |
-| =width=, =height= | Screen dimensions |
-| =centerCoordinate= | Screen center (for projection) |
-| =projectionScale= | Perspective scale factor |
-| =frameNumber= | Monotonically increasing frame counter |
-
-A new context is created when the window is resized. Otherwise, the
-same context is reused — =prepareForNewFrameRendering()= just resets
-per-frame state like mouse tracking.
--- /dev/null
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Rendering Loop - Sixth 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+begin_export html
+<style>
+ .flex-center {
+ display: flex;
+ justify-content: center;
+ }
+ .flex-center video {
+ width: min(90%, 1000px);
+ height: auto;
+ }
+ .responsive-img {
+ width: min(100%, 1000px);
+ height: auto;
+ }
+</style>
+#+end_export
+
+[[file:../index.org][Back to main documentation]]
+
+* Rendering loop
+:PROPERTIES:
+:CUSTOM_ID: rendering-loop
+:ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
+:END:
+
+The rendering loop is the heart of the engine, continuously generating
+frames on a dedicated background thread. It orchestrates the entire
+rendering pipeline from 3D world space to pixels on screen.
+
+** Main loop structure
+:PROPERTIES:
+:CUSTOM_ID: main-loop-structure
+:END:
+
+The render thread runs continuously in a dedicated daemon thread:
+
+#+BEGIN_SRC java
+while (renderThreadRunning) {
+ ensureThatViewIsUpToDate(); // Render one frame
+ maintainTargetFps(); // Sleep if needed
+}
+#+END_SRC
+
+The thread is a daemon, so it automatically stops when the JVM exits.
+You can stop it explicitly with [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html#stop()][ViewPanel.stop()]].
+
+** Frame rate control
+:PROPERTIES:
+:CUSTOM_ID: frame-rate-control
+:END:
+
+The engine supports two modes:
+
+- *Target FPS mode*: Set with [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html#setFrameRate(int)][setFrameRate(int)]]. The thread sleeps
+ between frames to maintain the target rate. If rendering takes
+ longer than the frame interval, the engine catches up naturally
+ without sleeping.
+
+- *Unlimited mode*: Set =setFrameRate(0)= or negative. No sleeping —
+ renders as fast as possible. Useful for benchmarking.
+
+** Frame listeners
+:PROPERTIES:
+:CUSTOM_ID: frame-listeners
+:END:
+
+Before each frame, the engine notifies all registered [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/FrameListener.html][FrameListener]]s:
+
+#+BEGIN_SRC java
+viewPanel.addFrameListener((panel, deltaMs) -> {
+ // Update animations, physics, game logic
+ shape.rotate(0.01);
+ return true; // true = force repaint
+});
+#+END_SRC
+
+Frame listeners can trigger repaints by returning =true=. Built-in listeners include:
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/Camera.html][Camera]] — handles keyboard/mouse navigation
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.html][InputManager]] — processes input events
+
+* Rendering phases
+:PROPERTIES:
+:CUSTOM_ID: rendering-phases
+:END:
+
+Each frame goes through 6 phases. Open the Developer Tools panel (F12)
+to see these phases logged in real-time:
+
+** Phase 1: Clear canvas
+:PROPERTIES:
+:CUSTOM_ID: phase-1-clear-canvas
+:END:
+
+The pixel buffer is filled with the background color (default: black).
+
+#+BEGIN_SRC java
+Arrays.fill(pixels, 0, width * height, backgroundColorRgb);
+#+END_SRC
+
+This is a simple =Arrays.fill= operation — very fast, single-threaded.
+
+** Phase 2: Transform shapes
+:PROPERTIES:
+:CUSTOM_ID: phase-2-transform-shapes
+:END:
+
+All shapes are transformed from world space to screen space:
+
+1. Build camera-relative transform (inverse of camera position/rotation)
+2. For each shape:
+ - Apply camera transform
+ - Project 3D → 2D (perspective projection)
+ - Calculate =onScreenZ= for depth sorting
+ - Queue for rendering
+
+This is single-threaded but very fast — just math, no pixel operations.
+
+** Phase 3: Sort shapes
+:PROPERTIES:
+:CUSTOM_ID: phase-3-sort-shapes
+:END:
+
+Shapes are sorted by =onScreenZ= (depth) in descending order:
+
+#+BEGIN_SRC java
+Collections.sort(queuedShapes, (a, b) -> Double.compare(b.onScreenZ, a.onScreenZ));
+#+END_SRC
+
+Back-to-front sorting is essential for correct transparency and
+occlusion. Shapes further from the camera are painted first.
+
+** Phase 4: Paint shapes (multi-threaded)
+:PROPERTIES:
+:CUSTOM_ID: phase-4-paint-shapes
+:END:
+
+The screen is divided into 8 horizontal segments, each rendered by a separate thread:
+
+#+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
+
+Each thread:
+- Gets a =SegmentRenderingContext= with Y-bounds (minY, maxY)
+- Iterates all shapes and paints pixels within its Y-range
+- Clips triangles/lines at segment boundaries
+- Detects mouse hits (before clipping)
+
+A =CountDownLatch= waits for all 8 threads to complete before proceeding.
+
+**Why 8 segments?** This matches the typical core count of modern CPUs.
+The fixed thread pool (=Executors.newFixedThreadPool(8)=) avoids the
+overhead of creating threads per frame.
+
+** Phase 5: Combine mouse results
+:PROPERTIES:
+:CUSTOM_ID: phase-5-combine-mouse-results
+:END:
+
+During painting, each segment tracks which shape is under the mouse cursor.
+Since all segments paint the same shapes (just different Y-ranges), they
+should all report the same hit. Phase 5 takes the first non-null result:
+
+#+BEGIN_SRC java
+for (SegmentRenderingContext ctx : segmentContexts) {
+ if (ctx.getSegmentMouseHit() != null) {
+ renderingContext.setCurrentObjectUnderMouseCursor(ctx.getSegmentMouseHit());
+ break;
+ }
+}
+#+END_SRC
+
+** Phase 6: Blit to screen
+:PROPERTIES:
+:CUSTOM_ID: phase-6-blit-to-screen
+:END:
+
+The rendered =BufferedImage= is copied to the screen using
+[[https://docs.oracle.com/javase/21/docs/api/java/awt/image/BufferStrategy.html][BufferStrategy]] for tear-free page-flipping:
+
+#+BEGIN_SRC java
+do {
+ Graphics2D g = bufferStrategy.getDrawGraphics();
+ g.drawImage(renderingContext.bufferedImage, 0, 0, null);
+ g.dispose();
+} while (bufferStrategy.contentsRestored());
+
+bufferStrategy.show();
+Toolkit.getDefaultToolkit().sync();
+#+END_SRC
+
+The =do-while= loop handles the case where the OS recreates the back
+buffer (common during window resizing). Since our offscreen
+=BufferedImage= still has the correct pixels, we only need to re-blit,
+not re-render.
+
+* Smart repaint skipping
+:PROPERTIES:
+:CUSTOM_ID: smart-repaint-skipping
+:END:
+
+The engine avoids unnecessary rendering:
+
+- =viewRepaintNeeded= flag: Set to =true= only when something changes
+- Frame listeners can return =false= to skip repaint
+- Resizing, component events, and explicit =repaintDuringNextViewUpdate()=
+ calls set the flag
+
+This means a static scene consumes almost zero CPU — the render thread
+just spins checking the flag.
+
+* Rendering context
+:PROPERTIES:
+:CUSTOM_ID: rendering-context
+:END:
+
+The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/RenderingContext.html][RenderingContext]] holds all state for a single frame:
+
+| Field | Purpose |
+|-------+---------|
+| =pixels[]= | Raw pixel buffer (int[] in RGB format) |
+| =bufferedImage= | Java2D wrapper around pixels |
+| =graphics= | Graphics2D for text, lines, shapes |
+| =width=, =height= | Screen dimensions |
+| =centerCoordinate= | Screen center (for projection) |
+| =projectionScale= | Perspective scale factor |
+| =frameNumber= | Monotonically increasing frame counter |
+
+A new context is created when the window is resized. Otherwise, the
+same context is reused — =prepareForNewFrameRendering()= just resets
+per-frame state like mouse tracking.