From: Svjatoslav Agejenko Date: Sat, 19 Sep 2026 21:41:33 +0000 (+0300) Subject: Rename doc/ to Documentation/ and unify version at 1.0.0-SNAPSHOT X-Git-Tag: aukio-3d-1.0.0~11 X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=0345b176c73408bdabe79e664d902cefa0a93325;p=aukio-3d.git Rename doc/ to Documentation/ and unify version at 1.0.0-SNAPSHOT - Documentation/ picks up the whole former doc/ tree (org sources, diagrams, screenshots); export-docs.sh comments follow - .gitignore, AGENTS.org and Tools/Update web site updated to the new layout (export scan, javadoc staging, graphs, upload) - pom.xml: 1.5-SNAPSHOT -> 1.0.0-SNAPSHOT; correct to the current hosting URL --- diff --git a/.gitignore b/.gitignore index 31378ad..319eac6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ /.classpath /.project /.settings/ -/doc/graphs/ -/doc/apidocs/ +/Documentation/graphs/ +/Documentation/apidocs/ /*.iml *.html diff --git a/AGENTS.org b/AGENTS.org index 7dc28c0..2dc0fb8 100644 --- a/AGENTS.org +++ b/AGENTS.org @@ -447,8 +447,8 @@ java -cp "target/classes:$(cat cp.txt)" \ eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens --update # regenerate goldens # Regenerate all HTML documentation (org -> HTML, darksun theme) -doc/export-docs.sh # export -doc/export-docs.sh --check # export + headless-Chrome screenshots to /tmp +Documentation/export-docs.sh # export +Documentation/export-docs.sh --check # export + headless-Chrome screenshots to /tmp #+end_src Test files: ~src/test/java/~ (JUnit 4) @@ -558,18 +558,18 @@ rebuild the exact demo scene headlessly. | Path | Topic | |----------------------------------------------+---------------------------------------------------------------------| -| ~doc/index.org~ | Main: coordinate system, shapes, CSG, developer tools | -| ~doc/Rendering loop/index.org~ | 5-phase pipeline, multi-threaded paint | -| ~doc/Shading/index.org~ | Lambert shading, lights, distance attenuation | -| ~doc/CSG/index.org~ | Boolean ops via BSP trees | -| ~doc/Frustum culling/index.org~ | View frustum culling | -| ~doc/Near plane clip/index.org~ | Near-plane polygon clipping (straddling geometry) | -| ~doc/Global illumination/index.org~ | Progressive GI: lightmaps, bounces, convergence | -| ~doc/Perspective correct textures/index.org~ | Texture mapping math | -| ~doc/Agentic development/index.org~ | Stub: headless-toolkit docs moved to ~doc/index.org~ :: Agentic development | -| ~doc/Stereoscopic rendering/index.org~ | Side-by-side stereo: two passes, per-eye viewports, IPD | -| ~doc/BSP-tree painter's algorithm/index.org~ | BSP compile + rank traversal fixing average-Z sort | -| ~doc/SDF textures/index.org~ | SDF text: glyph fields, coverage window, TextCanvas | - -Regenerate all HTML: ~doc/export-docs.sh~ (add ~--check~ for rendered +| ~Documentation/index.org~ | Main: coordinate system, shapes, CSG, developer tools | +| ~Documentation/Rendering loop/index.org~ | 5-phase pipeline, multi-threaded paint | +| ~Documentation/Shading/index.org~ | Lambert shading, lights, distance attenuation | +| ~Documentation/CSG/index.org~ | Boolean ops via BSP trees | +| ~Documentation/Frustum culling/index.org~ | View frustum culling | +| ~Documentation/Near plane clip/index.org~ | Near-plane polygon clipping (straddling geometry) | +| ~Documentation/Global illumination/index.org~ | Progressive GI: lightmaps, bounces, convergence | +| ~Documentation/Perspective correct textures/index.org~ | Texture mapping math | +| ~Documentation/Agentic development/index.org~ | Stub: headless-toolkit docs moved to ~Documentation/index.org~ :: Agentic development | +| ~Documentation/Stereoscopic rendering/index.org~ | Side-by-side stereo: two passes, per-eye viewports, IPD | +| ~Documentation/BSP-tree painter's algorithm/index.org~ | BSP compile + rank traversal fixing average-Z sort | +| ~Documentation/SDF textures/index.org~ | SDF text: glyph fields, coverage window, TextCanvas | + +Regenerate all HTML: ~Documentation/export-docs.sh~ (add ~--check~ for rendered screenshots of every page). diff --git a/Documentation/Agentic development/Golden workflow.svg b/Documentation/Agentic development/Golden workflow.svg new file mode 100644 index 0000000..aa0e7fb --- /dev/null +++ b/Documentation/Agentic development/Golden workflow.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Snapshot.render + scene + pose + + + + + + goldens/*.png + committed reference + + + + + GoldenImage + .compare + + + + + PASS + exit 0 + + + + + FAIL, exit 1 + + diff PNG to /tmp + + + + + bug? fix code + intended? run + --update + + + + regenerates reference + + tolerance: per-channel delta + max differing-pixel fraction — shading is deterministic, keep both tight + diff --git a/Documentation/Agentic development/Headless lanes.svg b/Documentation/Agentic development/Headless lanes.svg new file mode 100644 index 0000000..1303a80 --- /dev/null +++ b/Documentation/Agentic development/Headless lanes.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ViewPanel + window + render thread + camera from user input + + + + Snapshot.render() + no window, no display + camera from pose string + + + + SAME pipeline + transform + ↓ + sort by Z + ↓ + paint + + + + screen + BufferStrategy + + + BufferedImage + → PNG (Snapshot.save) + → PixelAssertions + → GoldenImage + + + + + + + + + + + identical results + headless rendering drives the very same code the window uses — a test render IS the real render + diff --git a/Documentation/Agentic development/Pixel assertion.svg b/Documentation/Agentic development/Pixel assertion.svg new file mode 100644 index 0000000..a9218f6 --- /dev/null +++ b/Documentation/Agentic development/Pixel assertion.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rendered frame (640×480) + + + + + + painted + painted + + + + region (0.15, 0.45) → (0.85, 1.0) + + + + hole + + + + PixelAssertions + unpaintedFraction(img, + bg=0x000000, + 0.15, 0.45, + 0.85, 1.0) + counts pixels that still + equal the background + → 0.017 > 0.01 FAIL + + + + + sentinel background: "nothing rendered here" is unambiguous, even in dark scenes + diff --git a/Documentation/Agentic development/diff-example.png b/Documentation/Agentic development/diff-example.png new file mode 100644 index 0000000..941d5ca Binary files /dev/null and b/Documentation/Agentic development/diff-example.png differ diff --git a/Documentation/Agentic development/snapshot-example.png b/Documentation/Agentic development/snapshot-example.png new file mode 100644 index 0000000..92ea03c Binary files /dev/null and b/Documentation/Agentic development/snapshot-example.png differ diff --git a/Documentation/CSG/BSP tree.svg b/Documentation/CSG/BSP tree.svg new file mode 100644 index 0000000..eb89c2c --- /dev/null +++ b/Documentation/CSG/BSP tree.svg @@ -0,0 +1,45 @@ + + + + + + + + + Plane P₁ + + + + + + + + + front + back + + + + Front (P₂) + + + + Back (P₃) + + + + + + + + + + + leaf + + leaf + + + Each plane divides space into front (normal side) and back (opposite) + Polygons are classified and split at each partitioning plane + diff --git a/Documentation/CSG/CSG demo.png b/Documentation/CSG/CSG demo.png new file mode 100644 index 0000000..2275350 Binary files /dev/null and b/Documentation/CSG/CSG demo.png differ diff --git a/Documentation/CSG/CSG intersect.svg b/Documentation/CSG/CSG intersect.svg new file mode 100644 index 0000000..a912f81 --- /dev/null +++ b/Documentation/CSG/CSG intersect.svg @@ -0,0 +1,41 @@ + + + + + + + + + + Input + + A + + B + + + ∩ + intersect + + + + + + + Result: A ∩ B + + + + + + + + + + + + Find overlap + between both + Only shared volume + remains + diff --git a/Documentation/CSG/CSG operations.svg b/Documentation/CSG/CSG operations.svg new file mode 100644 index 0000000..3f73cbe --- /dev/null +++ b/Documentation/CSG/CSG operations.svg @@ -0,0 +1,37 @@ + + + + + + + + + Input + + A + + B + + + − + subtract + + + + + + + Result: A − B + + + + + + cavity + + + B is the "cutter" + carves out of A + Cube with cavity + interior faces visible + diff --git a/Documentation/CSG/CSG union.svg b/Documentation/CSG/CSG union.svg new file mode 100644 index 0000000..f1eedec --- /dev/null +++ b/Documentation/CSG/CSG union.svg @@ -0,0 +1,38 @@ + + + + + + + + Input + + A + + B + + + + + union + + + + + + + Result: A + B + + + + + + removed + + + Keeps all geometry + from both shapes + Single combined volume + interior faces removed + diff --git a/Documentation/CSG/Polygon clipping.svg b/Documentation/CSG/Polygon clipping.svg new file mode 100644 index 0000000..41de628 --- /dev/null +++ b/Documentation/CSG/Polygon clipping.svg @@ -0,0 +1,39 @@ + + + + + + + + Polygon crosses plane + + + plane + + + + + → + split + + + Split into fragments + + + + + front + + + + back + + + + + + new edge + + + Spanning polygons are split; each fragment goes to its respective subtree + diff --git a/Documentation/CSG/index.org b/Documentation/CSG/index.org new file mode 100644 index 0000000..ebdbad6 --- /dev/null +++ b/Documentation/CSG/index.org @@ -0,0 +1,284 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Constructive Solid Geometry - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* What is CSG? +:PROPERTIES: +:CUSTOM_ID: what-is-csg +:ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +:END: + +*Constructive Solid Geometry* (CSG) is a modeling technique that builds +complex 3D shapes by combining simpler primitives using boolean +operations. Instead of manually creating every vertex and face, you +define shapes as the result of operations like "merge these two cubes" +or "carve a hole using this sphere." + +CSG is particularly powerful for: +- *Procedural modeling* — generate complex geometry algorithmically +- *CAD/CAM applications* — define parts as combinations of primitives +- *Game development* — create architectural elements, holes, cavities +- *Rapid prototyping* — iterate on designs by adjusting operations + +The three fundamental CSG operations are: + +| Operation | Symbol | Result | +|-------------+--------+-------------------------------------------| +| Subtract | A - B | A with B carved out (holes, cavities) | +| Union | A + B | Combined volume (both shapes merged) | +| Intersect | A ∩ B | Volume where both overlap | + +See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#csg-demo][CSG demo]] for an interactive visualization. + +* The Three Operations +:PROPERTIES: +:CUSTOM_ID: the-three-operations +:END: + +#+attr_html: :class responsive-img +#+attr_latex: :width 1000px +[[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#csg-demo][file:CSG%20demo.png]] + +The screenshot above shows all three operations displayed left to right: +subtract (green cube with spherical cavity), union (merged green and +orange shapes), and intersect (only the overlapping region in blue). + +The diagrams below use the same green cube (A) and orange sphere (B) as +the screenshot above. Each operation transforms these inputs differently, +producing the results shown from left to right in the image. + +** Subtract (A - B) +:PROPERTIES: +:CUSTOM_ID: subtract-operation +:END: + +#+INCLUDE: "CSG operations.svg" export html + +*Subtract* removes the orange sphere (B) from the green cube (A), carving +out a cavity. The diagram shows B acting as a "cutter" — where it overlaps +A, a hole is created. Interior faces *are preserved* and become visible, +allowing you to see inside the carved-out space (shown as the orange dashed +curve in the result). + +This matches the leftmost shape in the screenshot: a green cube with a +visible spherical hollow inside, showing the interior surfaces created by +the subtraction. + +This operation is ideal for creating: +- Holes and tunnels +- Carved-out spaces +- Hollow objects + +#+BEGIN_SRC java +SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN); +SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE); + +cube.subtract(sphere); // cube now has a spherical cavity +#+END_SRC + +** Union (A + B) +:PROPERTIES: +:CUSTOM_ID: union-operation +:END: + +#+INCLUDE: "CSG union.svg" export html + +*Union* merges the green cube (A) and orange sphere (B) into one continuous +volume. The diagram shows both shapes combining — the interior seam (where +they overlap) is removed, creating a single solid surface with no internal +boundaries (indicated by the dashed blue line labeled "removed"). + +This corresponds to the center shape in the screenshot: both green and +orange colors present but seamlessly joined, forming one unified object. + +#+BEGIN_SRC java +SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN); +SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE); + +cube.union(sphere); // cube now contains the merged result +#+END_SRC + +** Intersect (A ∩ B) +:PROPERTIES: +:CUSTOM_ID: intersect-operation +:END: + +#+INCLUDE: "CSG intersect.svg" export html + +*Intersect* keeps only the volume where the green cube (A) and orange +sphere (B) overlap — the region that is inside *both* shapes +simultaneously. The diagram shows this as the blue-shaded area: the +portion of the sphere that fits within the cube boundaries. Everything +else is discarded. + +This is the rightmost shape in the screenshot: only the overlapping +portion remains, showing which parts of space were occupied by both the +cube and sphere at the same time. + +This operation is useful for: +- Creating shapes constrained by multiple boundaries +- Finding collision regions +- Trimming geometry to fit within bounds + +#+BEGIN_SRC java +SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN); +SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE); + +cube.intersect(sphere); // only the overlapping region remains +#+END_SRC + +* BSP Tree Algorithm +:PROPERTIES: +:CUSTOM_ID: bsp-tree-algorithm +:END: + +CSG boolean operations are implemented using *Binary Space Partitioning* +(BSP) trees. A BSP tree recursively divides 3D space using planes, +creating a hierarchical structure that enables efficient polygon clipping +and spatial queries. + +** BSP Tree Structure +:PROPERTIES: +:CUSTOM_ID: bsp-tree-structure +:END: + +#+INCLUDE: "BSP tree.svg" export html + +Each BSP node contains: +- A *partitioning plane* that divides space into two half-spaces +- *Polygons* that lie exactly on this plane (coplanar) +- *Front* subtree — polygons on the same side as the plane's normal +- *Back* subtree — polygons on the opposite side + +** Key BSP Operations +:PROPERTIES: +:CUSTOM_ID: key-bsp-operations +:END: + +The BSP tree provides three core operations that enable CSG: + +| Operation | Description | +|----------------+--------------------------------------------------| +| =invert()= | Flip all normals, swap front/back children | +| =clipTo(tree)= | Remove polygons inside the other tree's solid | +| =addPolygons()= | Insert new polygons, splitting at planes | + +*Invert* is fundamental to CSG. By flipping inside/outside, we can +transform subtraction and intersection into variations of clipping: + +- **Subtract** = invert A, clip against B, add B's clipped parts, invert back +- **Intersect** = invert A, clip B against A, invert B, clip A against B, combine, invert A back + +** Polygon Clipping +:PROPERTIES: +:CUSTOM_ID: polygon-clipping +:END: + +When a polygon crosses a partitioning plane, it's *split* into two +fragments: + +#+INCLUDE: "Polygon clipping.svg" export html + +This recursive splitting ensures that all polygons are cleanly classified +as entirely in front, entirely behind, or exactly on a plane — never +"spanning" across. + +* Using CSG in Aukio 3D +:PROPERTIES: +:CUSTOM_ID: using-csg-in-aukio-3d +:END: + +CSG operations are methods on [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]. They modify the +shape *in-place* — the result replaces the original geometry. + +** Basic Usage +:PROPERTIES: +:CUSTOM_ID: basic-usage +:END: + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.*; + +// Create two shapes +SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 100, Color.GREEN); +SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 70, 12, Color.ORANGE); + +// Perform CSG operations (in-place modification) +cube.subtract(sphere); // Cube with spherical cavity +// or +cube.union(sphere); // Merged shape +// or +cube.intersect(sphere); // Only overlapping region + +// Add to scene +shapes.addShape(cube.setBackfaceCulling(true)); +#+END_SRC + +** Child Handling Behavior +:PROPERTIES: +:CUSTOM_ID: child-handling +:END: + +CSG operations only affect *SolidPolygon* geometry. Other children are +preserved as objects: + +| Child Type | Union | Subtract | Intersect | +|-----------------------+------------------+------------------+------------------| +| SolidPolygon (this) | Replaced with result | Replaced with result | Replaced with result | +| SolidPolygon (other) | Merged into result | Discarded (cutter) | Discarded | +| Line, TextCanvas (this) | Preserved | Preserved | Preserved | +| Line, TextCanvas (other) | Merged into this shape | Discarded | Discarded | +| Nested composite | Preserved as object — but see below | same | same | + +*Nested composites are not CSG-safe.* Polygon extraction recurses into +them, so their SolidPolygons are included in the BSP result — while the +nested composite object itself is also preserved, duplicating that +geometry in the render. Apply CSG to flat composites, or extract the +nested polygons first. + +This allows you to attach labels, decorations, or wireframe overlays to +shapes without them being affected by CSG operations (for union, the +other shape's decorations are copied over too). + +** Important Notes +:PROPERTIES: +:CUSTOM_ID: important-notes +:END: + +1. *Shapes are modified in-place*. The original geometry is replaced. + Clone shapes beforehand if you need to preserve the originals. + +2. *CSG works on SolidPolygon children only*. TexturedTriangle and other + shape types are not processed. + +3. *Result quality depends on mesh density*. Low-polygon inputs may + produce visible artifacts at intersection boundaries. Use higher + subdivision counts for smoother results. + +4. *Backface culling is recommended*. CSG results often have internal + faces from the cutting operation. Enable culling to hide backfaces: + =shape.setBackfaceCulling(true)= + +* Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|--------------------------+------------------------------------------------------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/BspTree.html][BspTree]] | BSP tree for spatial partitioning and CSG operations | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Plane]] | Partitioning plane used by BSP nodes | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Polygon shape processed by CSG | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] | Base class with union/subtract/intersect methods | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.html][SolidPolygonMesh]] | Custom polygon mesh for arbitrary geometry | +| SolidPolygon* primitives | See [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery]] for all available shapes | diff --git a/Documentation/Coordinate system.svg b/Documentation/Coordinate system.svg new file mode 100644 index 0000000..4497bf3 --- /dev/null +++ b/Documentation/Coordinate system.svg @@ -0,0 +1,18 @@ + + + + + + X + right (+) / left (-) + + + Y + down (+) / up (-) + + + Z + away (+) / towards (-) + Origin + (0, 0, 0) + \ No newline at end of file diff --git a/Documentation/Depth buffer/index.org b/Documentation/Depth buffer/index.org new file mode 100644 index 0000000..2f2c75d --- /dev/null +++ b/Documentation/Depth buffer/index.org @@ -0,0 +1,130 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Depth Buffer - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \setlength{\parindent}{15pt} +#+LATEX_HEADER: \usepackage{palatino} +#+LATEX_HEADER: \usepackage{charter} +#+HTML_HEAD: + +[[file:../index.html#outline-container-depth-buffer][<- Back to index]] + +* Per-pixel visibility +:PROPERTIES: +:CUSTOM_ID: per-pixel +:END: + +The engine resolves visibility with a depth buffer, not paint order. +Every rasterized triangle carries a per-pixel depth quantity =zw = +1/z= (camera-space), interpolated linearly across each span — =1/z= is +affine in screen space, so it rides the same edge interpolators as the +texture gradients. A fragment wins a pixel only where + +#+BEGIN_EXAMPLE +zw > stored - margin * zw^2 (margin = 0 by default) +#+END_EXAMPLE + +Default margin 0 is *strict depth*: the nearer fragment always wins, +regardless of paint order, so overlapping depth ranges — a floor tile +extending under furniture, a wall seen through a doorway — come out +correct per pixel. A nonzero margin (= RenderingContext.DEPTH_MARGIN_DZ =, +=-Daukio.zbuffer.margin=, world units) re-opens a tolerance window +*behind* the stored depth for near-coplanar pairs; any nonzero window +re-imports per-triangle sort errors into per-pixel occlusion, which is +why the default is strict. + +The depth buffer is allocated once per frame context +(=RenderingContext.depth=, float per pixel) and cleared per tile +together with the pixel buffer. + +* Two passes +:PROPERTIES: +:CUSTOM_ID: two-passes +:END: + +=RenderAggregator.paintSorted= paints the sorted queue in two passes: + +1. *Opaque pass* — opaque-class triangles, iterated front-to-back (the + queue is back-to-front, so reversed), depth test + depth write. + Front-to-back order is a pure performance hint: hidden fragments + die on the depth test *before* the texture fetch (early-z). +2. *Alpha pass* — alpha-class triangles (translucent solid polygons, + alpha-carrying textures, SDF text), iterated back-to-front in queue + order, depth test but *no depth write*. Translucency never + occludes, and overlapping translucent surfaces keep painter-coherent + mutual order. + +A shape's class comes from its paint color or texture: solid polygons +with =alpha = 255= are opaque, anything translucent is alpha-class; +textured triangles are alpha-class when the texture has alpha or is an +SDF mask. + +* Which shapes carry depth +:PROPERTIES: +:CUSTOM_ID: shapes +:END: + +- =TexturedTriangle= — opaque or alpha class by texture. +- =SolidPolygon= — depth-tested since 2026-09-17; opaque when its + (possibly shaded) color is fully opaque, translucent otherwise. + Near-plane-clipped quads fan-triangulate with depth like any other + triangles. +- =LightmappedTriangle= — a textured triangle, so the same rules. +- =Line=, =Billboard=, =GlowingPoint= — no depth by design: they are + 2D overlays (wireframes, markers, sprites) and always paint on top, + in the alpha pass. + +Because every occluder writes depth, scene code no longer needs any +ordering structure: composites just fan-triangulate their polygons. +(=LightmappedCompositeShape= exists only to wrap polygons as lightmap +carriers for the GI system, not to order them.) + +* Hi-Z occlusion pyramid +:PROPERTIES: +:CUSTOM_ID: hi-z +:END: + +After each successful paint, =ViewPanel= builds a Hi-Z pyramid from +the depth buffer (gui/HiZPyramid): 8-pixel tiles pooled upward, +each tile storing the *minimum* =zw= (farthest written depth — +max-pooling would store the nearest occluder and wrongly cull geometry +visible between near gaps). Next frame, =TriangleMeshBlock= projects +its world AABB's 8 corners and, when the nearest corner is still +behind the pyramid's stored depth, skips the whole block before any +per-triangle work. + +The test is conservative by construction (min-pooling plus sky pixels +at =-inf=), so it never culls visible geometry; wrong culls under +camera motion self-heal in one frame. Knobs: =-Daukio.hiz.margin=0.02=, +kill switch =-Daukio.hiz=false=. Headless snapshots never build the +pyramid, so golden renders are structurally unaffected. Stereo skips +the test (the pyramid is mono). + +* Determinism and depth dumps +:PROPERTIES: +:CUSTOM_ID: determinism +:END: + +The renderer is bit-deterministic: same scene and camera give +bit-identical pixels across runs, which is what the golden-image +regression tests compare. =Snapshot= (the headless toolkit) supports +=-Daukio.zbuffer.dumpDepth=path.png= to write a grayscale depth map +alongside the color image — useful when hunting depth-window bugs +(bisect those with =-Daukio.zbuffer.margin=0=). + +* Related classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Role | +|----------------------+----------------------------------------------------------| +| =RenderingContext= | =depth= buffer, =depthPass=, =DEPTH_MARGIN_DZ= constant | +| =RenderAggregator= | =paintSorted= two-pass driver, queue sort | +| =TexturedTriangle= | Z span writers (perspective and affine) | +| =SolidPolygon= | flat-color Z span writer, two-pass classification | +| =HiZPyramid= | temporal whole-block occlusion culling | +| =ViewPanel= | per-tile depth clear, pyramid rebuild after paint | + +[[file:../index.html#outline-container-depth-buffer][Back to main documentation]] diff --git a/Documentation/Developer tools/Developer tools.png b/Documentation/Developer tools/Developer tools.png new file mode 100644 index 0000000..825b0de Binary files /dev/null and b/Documentation/Developer tools/Developer tools.png differ diff --git a/Documentation/Developer tools/Render alternative segments.png b/Documentation/Developer tools/Render alternative segments.png new file mode 100644 index 0000000..e2bd569 Binary files /dev/null and b/Documentation/Developer tools/Render alternative segments.png differ diff --git a/Documentation/Developer tools/Render polygon borders.png b/Documentation/Developer tools/Render polygon borders.png new file mode 100644 index 0000000..5ec2182 Binary files /dev/null and b/Documentation/Developer tools/Render polygon borders.png differ diff --git a/Documentation/Developer tools/Show segment boundaries.png b/Documentation/Developer tools/Show segment boundaries.png new file mode 100644 index 0000000..01a1978 Binary files /dev/null and b/Documentation/Developer tools/Show segment boundaries.png differ diff --git a/Documentation/Developer tools/Thread timeline.png b/Documentation/Developer tools/Thread timeline.png new file mode 100644 index 0000000..dd1d378 Binary files /dev/null and b/Documentation/Developer tools/Thread timeline.png differ diff --git a/Documentation/Edge.svg b/Documentation/Edge.svg new file mode 100644 index 0000000..e9af1cf --- /dev/null +++ b/Documentation/Edge.svg @@ -0,0 +1,12 @@ + + + + + + + + V₁ + V₂ + V₃ + edge + diff --git a/Documentation/Example.png b/Documentation/Example.png new file mode 100644 index 0000000..7094240 Binary files /dev/null and b/Documentation/Example.png differ diff --git a/Documentation/Face triangle.svg b/Documentation/Face triangle.svg new file mode 100644 index 0000000..509c841 --- /dev/null +++ b/Documentation/Face triangle.svg @@ -0,0 +1,14 @@ + + + + + + + + + + V₁ + V₂ + V₃ + FACE + diff --git a/Documentation/Frustum culling/Frustum diagram.svg b/Documentation/Frustum culling/Frustum diagram.svg new file mode 100644 index 0000000..b59d4a8 --- /dev/null +++ b/Documentation/Frustum culling/Frustum diagram.svg @@ -0,0 +1,58 @@ + + + + + + + + + +Z + (view direction) + + + + Camera + + + + + + + + + + + + + + Near + + + + Far + + + + + + visible region + + + Top plane + Bottom plane + + + + ✓ + rendered + + + + ✗ + culled + + + + ✗ + culled + diff --git a/Documentation/Frustum culling/P-vertex AABB.svg b/Documentation/Frustum culling/P-vertex AABB.svg new file mode 100644 index 0000000..a3acfb8 --- /dev/null +++ b/Documentation/Frustum culling/P-vertex AABB.svg @@ -0,0 +1,38 @@ + + + + + + + + P-vertex: corner most aligned with plane normal + If P is behind the plane → entire AABB is outside + + + + Plane + + + inside frustum + + outside frustum + + + + + N + + + + inside + + + P + + + + outside + + + P + diff --git a/Documentation/Frustum culling/index.org b/Documentation/Frustum culling/index.org new file mode 100644 index 0000000..9c4a941 --- /dev/null +++ b/Documentation/Frustum culling/index.org @@ -0,0 +1,177 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Frustum & View Frustum Culling - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* Frustum & View Frustum Culling +:PROPERTIES: +:CUSTOM_ID: frustum-view-frustum-culling +:END: + +#+INCLUDE: "Frustum diagram.svg" export html + +The *view frustum* is a truncated pyramid-shaped volume that represents +everything the camera can see. Objects completely outside this volume are +skipped during rendering — a powerful optimization called *frustum culling*. + +** The Six Frustum Planes +:PROPERTIES: +:CUSTOM_ID: frustum-planes +:END: + +The frustum is defined by six clipping planes: + +| Plane | Purpose | +|---------+--------------------------------------------| +| Left | Left edge of viewport | +| Right | Right edge of viewport | +| Top | Top edge of viewport (smaller Y in Y-down) | +| Bottom | Bottom edge of viewport (larger Y) | +| Near | Closest visible distance from camera | +| Far | Farthest visible distance from camera | + +Each plane divides 3D space into "inside" (visible) and "outside" +(culled). An object must pass all six plane tests to be considered +potentially visible. + +** Frustum Culling vs Backface Culling +:PROPERTIES: +:CUSTOM_ID: frustum-vs-backface-culling +:END: + +These are complementary optimizations at different levels: + +| Optimization | Level | What it skips | +|-----------------+--------------+----------------------------------| +| Frustum culling | Object level | Entire composite shapes + children | +| Backface culling | Polygon level | Individual triangles facing away | + +*Frustum culling* happens first during the transform phase — entire +object trees are skipped with a single bounding box test. *Backface +culling* happens later during rasterization — individual triangles +are checked before being drawn. + +For best performance, use both: organize your scene with composite +shapes for effective frustum culling, and enable backface culling on +closed meshes. + +* How Frustum Culling Works in Aukio 3D +:PROPERTIES: +:CUSTOM_ID: frustum-culling-implementation +:END: + +Frustum culling is applied automatically to all [[../index.org#mesh][composite shapes]] +during Phase 1 (transform) of the [[../Rendering loop/][rendering loop]]: + +1. *Update frustum*: Compute 6 planes from camera FOV and viewport size +2. *For each composite shape*: + - Get its [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Box.html][Axis-Aligned Bounding Box (AABB)]] + - Transform all 8 corners to view space + - Test against frustum using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Frustum.html#intersectsAABB][intersectsAABB()]] + - If outside: skip the entire composite and all children + - If inside: continue transforming children + +(The root composite itself is never tested — it is always rendered.) + +** The AABB Intersection Algorithm +:PROPERTIES: +:CUSTOM_ID: aabb-intersection-algorithm +:END: + +The intersection test uses an optimized "P-vertex" approach: + +#+INCLUDE: "P-vertex AABB.svg" export html + +For each plane, instead of testing all 8 corners of the bounding box, +we test only the *P-vertex* — the corner most aligned with the plane +normal. If this "best" corner is behind the plane, the entire box must +be outside the frustum. + +- Plane normal points *into* the frustum (toward visible region) +- P-vertex: select corner based on normal direction + - If normal.x > 0 → use maxX (rightmost corner) + - If normal.x < 0 → use minX (leftmost corner) + - Same logic for Y and Z +- Test: =dot(normal, P-vertex) < distance= → outside + +This reduces from 48 tests (8 corners × 6 planes) to just 6 tests per +object. + +* Performance Benefits +:PROPERTIES: +:CUSTOM_ID: frustum-performance +:END: + +Frustum culling can dramatically improve performance for large scenes: + +- *High cull % (60-90%)*: Excellent — most objects skipped entirely +- *Medium cull % (20-60%)*: Moderate benefit +- *Low cull % (0-20%)*: Limited benefit — most objects visible + +A composite shape that is culled skips: +- Transforming all its children +- Computing bounding boxes for children +- All polygon-level operations (backface culling, rasterization) + +Open Developer Tools (F12) to see real-time [[../index.org#frustum-culling-statistics][frustum culling statistics]]. + +* Scene Design for Effective Culling +:PROPERTIES: +:CUSTOM_ID: frustum-scene-design +:END: + +Frustum culling works best when you organize your scene into +well-defined composite shapes: + +#+BEGIN_SRC java +// Good: Each building is a separate composite +AbstractCompositeShape cityBlock = new AbstractCompositeShape(); +for (Building building : buildings) { + AbstractCompositeShape buildingComposite = new AbstractCompositeShape(); + buildingComposite.addShape(buildingWalls); + buildingComposite.addShape(buildingRoof); + buildingComposite.addShape(buildingInterior); + cityBlock.addShape(buildingComposite); +} + +// Less effective: Everything in one giant composite +AbstractCompositeShape allObjects = new AbstractCompositeShape(); +allObjects.addShape(building1Walls); +allObjects.addShape(building1Roof); +allObjects.addShape(building2Walls); +// ... hundreds of shapes directly in root +#+END_SRC + +*Best practices:* + +- Use composites to group objects that occupy a bounded region of space +- Keep bounding boxes tight (don't add distant objects to the same composite) +- Nest composites hierarchically for multi-level culling (city → block → building) +- Call [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.html#invalidateBounds()][invalidateBounds()]] after moving shapes — the bounding box is + recomputed lazily on next use + +* Technical Details +:PROPERTIES: +:CUSTOM_ID: frustum-technical-details +:END: + +The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Frustum.html][Frustum]] class: + +- Computes planes in *view space* (camera at origin, looking along +Z) +- FOV derived from =projectionScale = width / 3= (≈112° horizontal FOV) +- Default clip distances: Near = 1.0, Far = 10000.0 +- Planes stored in [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Hesse normal form]]: (normal vector, distance) + +The frustum is updated once per render pass in +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.html][ShapeCollection.transformShapesBegin()]], from the camera state and +the stereo viewport width — in stereo mode each eye gets its own +frustum, so the update runs twice per frame. diff --git a/Documentation/Global illumination/Bounce estimator.svg b/Documentation/Global illumination/Bounce estimator.svg new file mode 100644 index 0000000..0d1972e --- /dev/null +++ b/Documentation/Global illumination/Bounce estimator.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + surface + + + + + + + P + + normal + + + + cosine-weighted + hemisphere + + + + + + + + + Q + + + + + light + + + + shadow ray: clear + + + + + + occluder + blocked + + + + at Q: direct light (cached shadow bits) + + Q's current indirect estimate + + + + P's target = (albedo / π) x (direct + indirect) + blended in with an exponential moving average + + one sample per texel per visit: one shadow ray + one bounce ray — bounce light ripples deeper every sweep + diff --git a/Documentation/Global illumination/GI pipeline.svg b/Documentation/Global illumination/GI pipeline.svg new file mode 100644 index 0000000..0f63023 --- /dev/null +++ b/Documentation/Global illumination/GI pipeline.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + scene snapshot + triangles + lights + + BVH + + + GI workers + Monte Carlo + sweeps + + + lightmaps + per-texel indirect + + shadow bits + + + composite + baseColor x light + double-buffered + swap + + + painter + plain texture + lookup + + + + + + + + + + bounce reads last sweep's estimate + + zero ray casting + on render threads + diff --git a/Documentation/Global illumination/Global illumination.png b/Documentation/Global illumination/Global illumination.png new file mode 100644 index 0000000..9909193 Binary files /dev/null and b/Documentation/Global illumination/Global illumination.png differ diff --git a/Documentation/Global illumination/Lightmap mapping.svg b/Documentation/Global illumination/Lightmap mapping.svg new file mode 100644 index 0000000..2d82fea --- /dev/null +++ b/Documentation/Global illumination/Lightmap mapping.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + invalid half (u+v > 1) + filled from neighbors so sampling + near the hypotenuse stays clean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + one texel = one surface + patch, sampled at center + + + + a (u=0, v=0) + + b (u=1, v=0) + + c (u=0, v=1) + + + + e1 = b − a + + e2 = c − a + + + + world(u,v) = a + e1·u + e2·v + texels = edge / unitsPerTexel + + every texel owns a fixed patch of the triangle — shadows and gradients live INSIDE the surface + diff --git a/Documentation/Global illumination/gi-converged.png b/Documentation/Global illumination/gi-converged.png new file mode 100644 index 0000000..1f81bfd Binary files /dev/null and b/Documentation/Global illumination/gi-converged.png differ diff --git a/Documentation/Global illumination/gi-flat.png b/Documentation/Global illumination/gi-flat.png new file mode 100644 index 0000000..f135bd2 Binary files /dev/null and b/Documentation/Global illumination/gi-flat.png differ diff --git a/Documentation/Global illumination/gi-start.png b/Documentation/Global illumination/gi-start.png new file mode 100644 index 0000000..6f7f3f0 Binary files /dev/null and b/Documentation/Global illumination/gi-start.png differ diff --git a/Documentation/Global illumination/index.org b/Documentation/Global illumination/index.org new file mode 100644 index 0000000..9568956 --- /dev/null +++ b/Documentation/Global illumination/index.org @@ -0,0 +1,251 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Global Illumination - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* What global illumination adds +:PROPERTIES: +:CUSTOM_ID: what-gi-adds +:END: + +Plain per-polygon shading lights every polygon with one flat color: +no shadows, and surfaces that receive no direct light stay uniformly +dark. A room corner reads as a flat silhouette instead of a corner. + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:gi-flat.png]] + +*Aukio 3D* can optionally compute *global illumination* progressively +on background CPU threads: shadows appear, light pools under lamps +with smooth falloff, and colored light *bleeds* — a red sofa tints the +floor next to it red. All of it converges gradually over the first +seconds of a scene, then idles. + +Same camera, same house: flat shading (top) versus converged GI +(below). Note the soft shadow of the partition wall, the lamp glow on +the ceiling, and the subtle color variation across the floor. + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:gi-converged.png]] + +* The big idea: GI off the render path +:PROPERTIES: +:CUSTOM_ID: off-the-render-path +:END: + +Ray tracing is far too slow to run per frame in a software renderer, +so it doesn't: *the render loop never traces a single ray.* Painting a +lightmapped triangle is an ordinary texture lookup, exactly as fast as +any textured polygon. + +All the expensive work happens on dedicated low-priority worker threads +that continuously refine per-surface lighting values. Whenever the +values have improved enough, the workers regenerate each triangle's +*composite texture* (baseColor x total lighting) into a back buffer +and swap it in atomically — painters never see a half-updated texture. + +#+INCLUDE: "GI pipeline.svg" export html + +Because painters only read finished textures, frame rate is completely +decoupled from GI quality: you can crank lightmap resolution up and the +only cost is CPU time on the worker threads, not frame time. + +* Lightmaps: a texture per triangle +:PROPERTIES: +:CUSTOM_ID: lightmaps +:END: + +Flat shading can only color a polygon uniformly — shadows and gradients +need resolution *inside* the polygon. Every lightmapped triangle +therefore owns a small generated texture, its *lightmap*, whose texels +map onto the triangle surface by an affine rule: + +#+INCLUDE: "Lightmap mapping.svg" export html + +The triangle's UVs are pinned to (0,0), (1,0), (0,1), so the valid +texel region is the half where u+v <= 1; the other half of the square +texture is flood-filled from valid neighbors so that nearest sampling +near the hypotenuse never picks up garbage. + +Each texel stores two things, both written only by GI threads: + +- *Indirect irradiance* (RGB floats) — the accumulated bounced light. +- *Per-light visibility bits* — whether the last shadow ray from this + texel reached each lamp (cached so later queries cost nothing). + +Resolution is set in world units per texel +(=LightmappedCompositeShape.setLightmapUnitsPerTexel()=, default 12): a 100-unit +wall cell gets an 8x8 lightmap. Halving the units quadruples the +tracing work. + +*What you trace is what you see:* the composite texture the painter +samples is the lightmap itself, at native texel resolution — there is +no upscaling step. Shadow-edge smoothness comes from tracing at finer +resolution, never from interpolation. (An earlier bilinear-upscaling +pass produced visibly artificial results and was removed; finer texels +cost more CPU on the GI threads but look right.) + +* One sample: a shadow ray and a bounce ray +:PROPERTIES: +:CUSTOM_ID: one-sample +:END: + +The work list is flat: one item per lightmap texel (and one per plain +polygon, see below). Worker threads walk it round-robin, and every +visit to a texel casts exactly two rays from the texel's world +position, nudged slightly off the surface along the normal: + +1. *Shadow ray* toward a lamp. Answers visible/occluded, cached in the + texel's visibility bits. On a texel's *first* visit all lamps are + tested at once, so direct light and hard shadows appear after a + single sweep instead of trickling in lamp by lamp; later visits + re-test one lamp at a time, round-robin. +2. *Bounce ray* in a random cosine-weighted direction around the + normal. Wherever it lands (point Q), the sample reads Q's *direct* + lighting — using Q's cached shadow bits, no new shadow rays — plus + Q's *current indirect estimate*, and blends the sum into the texel's + own indirect value. + +#+INCLUDE: "Bounce estimator.svg" export html + +Reading Q's current indirect estimate instead of recursing is what +makes bounce light propagate: sweep 1 learns "Q is directly lit", +sweep 2 learns "P sees a lit Q", sweep 3 learns "R sees a lit P"... +Light ripples one surface deeper with every sweep, with no recursion +limit and no exponential ray explosion. The =1/pi= diffuse gain keeps +the feedback loop from diverging: without it the indirect term +amplifies itself and the scene saturates to white. + +* Progressive convergence +:PROPERTIES: +:CUSTOM_ID: convergence +:END: + +Monte Carlo samples are noisy, so blending happens at *two nested +levels*, both exponential moving averages: + +1. *Inner, per sample*: each texel blends every new bounce-ray result + into its indirect estimate with a constant weight (=alpha = 0.15=, + mode =fixed=). Every ray hit stays equally intensive forever — an + unlit area fades to darkness at the same rate a lit area brightens. + (The old =adaptive= mode, which decays alpha with sample count, is + still available; see the knobs below.) +2. *Outer, per composite update*: the value that reaches the screen is + a second EMA over the COMPLETE sum =ambient + direct + indirect=. + The texture can only move =e3d.gi.compositeAlpha= (default 0.2) of + the remaining distance per 500 ms update — so direct light, shadows + and bounce light all glide in together over a few seconds, and no + single-frame jump is possible by construction. + +The world starts at a *uniform medium irradiance* +(=e3d.gi.initialIrradiance=, default 128): the scene is visible from +the very first frame, then lit areas brighten and unlit areas sink to +darkness as the workers sweep — lights and shadows gradually become +distinguished instead of the old pitch-black start with a sudden flash +once the first sweep landed: + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:gi-start.png]] + +Consequences of the design: + +- *Hysteresis is free*: when a lamp moves or geometry changes, old + light fades out gradually instead of popping — the same pair of EMAs + that accumulates light also drains it. +- *Convergence detection*: per-sample deltas are pure noise (and with a + constant alpha they never settle), so the system watches the average + per-texel movement of the on-screen estimate; after five composite + updates below =e3d.gi.calmThreshold= (default 1.0 light unit) the + workers drop to a low duty cycle (~50% of sweep time, capped) instead + of burning CPU. Any scene or light change rebuilds the snapshot and + restarts full-speed tracing. +- *Despeckle*: at composite time the indirect channel is blended 50/50 + with the mean of its valid 4-neighbors, killing single-texel Monte + Carlo spikes without blurring real gradients. +- *Composite cadence*: textures regenerate at most every 500 ms — one + atomic swap per triangle, invisible to painters. + +* Plain polygons get GI too +:PROPERTIES: +:CUSTOM_ID: plain-polygons +:END: + +Surfaces that are not lightmapped (ordinary =SolidPolygon= with shading +enabled) still benefit, at per-polygon resolution, through the +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.html][GiLightProvider]] +interface that =GlobalIllumination= installs into the +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]]: + +- =isLightVisible(polygon, light)= answers from cached shadow bits — + direct-light shadows fade in on flat-shaded geometry. +- =addIndirectLight(polygon, baseColor, result)= adds the polygon's + bounced-light estimate into the flat-shaded color. + +Both are called from parallel render-pool threads, so they only read +volatile caches — never trace. + +* Enabling GI +:PROPERTIES: +:CUSTOM_ID: enabling +:END: + +#+BEGIN_SRC java +// Per-texel lightmaps on composite geometry (the House demo setup): +LightmappedCompositeShape house = new LightmappedCompositeShape(); +house.setLightmappingEnabled(true); +house.setLightmapUnitsPerTexel(3.0); // fine texels: quality from traced rays + +// Start the workers (2 threads by default; more converge faster): +viewPanel.enableGlobalIllumination(4); +#+END_SRC + +Tuning knobs (system properties): + +| Property | Default | Effect | +|---------------------------+---------+-----------------------------------------| +| =e3d.gi.alphaMode= | fixed | =adaptive= decays the inner EMA alpha with sample count | +| =e3d.gi.alphaFloor= | 0.08 | adaptive-mode floor; higher adapts faster but noisier | +| =e3d.gi.compositeAlpha= | 0.2 | outer EMA: fade speed of the on-screen estimate per 500 ms update | +| =e3d.gi.initialIrradiance=| 128 | uniform medium start (0..255 light units) | +| =e3d.gi.calmThreshold= | 1.0 | convergence: avg estimate movement (light units) | +| =e3d.gi.despeckle= | true | neighbor-smoothing of indirect at composite time | +| =e3d.gi.debug= | false | sweep statistics to stdout | +| =e3d.gi.dumpLightmaps= | (unset) | dump composite lightmaps as PNGs to the given dir | + +* Limitations +:PROPERTIES: +:CUSTOM_ID: limitations +:END: + +- *Diffuse light only* — no specular bounce, no caustics. +- Polygon vertices are traced in composite-local space; scenes that put + non-identity transforms on composites are traced incorrectly. +- The bounce estimate is one ray deep per sample — correctness comes + from sweep-over-sweep propagation, so deeply indirect corners take + several sweeps to brighten. + +* Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|----------------------+---------------------------------------------------------------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/GlobalIllumination.html][GlobalIllumination]] | Progressive tracer: sweeps, EMA convergence, composite swaps | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/Lightmap.html][Lightmap]] | Per-triangle texel state + double-buffered composite textures | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.html][LightmappedTriangle]] | Textured triangle whose texture is the GI composite | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/TriangleBvh.html][TriangleBvh]] | BVH over world triangles: nearest-hit and any-hit ray queries (Möller–Trumbore) | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.html][GiLightProvider]] | Cache-read interface feeding the flat-shading path | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightmappedCompositeShape.html][LightmappedCompositeShape]] | Wraps polygons into lightmapped triangles | diff --git a/Documentation/Mesh.svg b/Documentation/Mesh.svg new file mode 100644 index 0000000..8a20f46 --- /dev/null +++ b/Documentation/Mesh.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + triangulated + section + + diff --git a/Documentation/Near plane clip/Clip algorithm.svg b/Documentation/Near plane clip/Clip algorithm.svg new file mode 100644 index 0000000..1d09a7e --- /dev/null +++ b/Documentation/Near plane clip/Clip algorithm.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + behind (z ≤ near) + in front (z > near) + + + + near plane + + + + + + + + + + + + + + v0 + + v1 + + v2 + + + + p′ + + p″ + + + t = 0.5 along v0 → v2 + + + + + t = (near − z1) / (z2 − z1) + p = p1 + t·(p2 − p1) + uv = uv1 + t·(uv2 − uv1) + + every edge crossing the plane spawns an interpolated vertex; + in-front vertices pass through unchanged + diff --git a/Documentation/Near plane clip/Fan triangulation.svg b/Documentation/Near plane clip/Fan triangulation.svg new file mode 100644 index 0000000..94a8656 --- /dev/null +++ b/Documentation/Near plane clip/Fan triangulation.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + v0 + + v1 + + p″ + + p′ + + + T1 = (v0, v1, p″) + T2 = (v0, p″, p′) + + + a triangle cut once + becomes a quad; + the rasterizer paints it + as a 2-triangle fan + sharing v0 + diff --git a/Documentation/Near plane clip/Near plane straddle.svg b/Documentation/Near plane clip/Near plane straddle.svg new file mode 100644 index 0000000..aa2c9a6 --- /dev/null +++ b/Documentation/Near plane clip/Near plane straddle.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + z (depth) → + x ↓ + + + + + camera + + + + + near plane z = 1 + + + + + + + floor tiles + + + + + + kept fragment + cut away + + + old: one vertex behind ⇒ + whole tile dropped + + + + new: clip at the plane, + paint the surviving fragment + + diff --git a/Documentation/Near plane clip/index.org b/Documentation/Near plane clip/index.org new file mode 100644 index 0000000..0c59a70 --- /dev/null +++ b/Documentation/Near plane clip/index.org @@ -0,0 +1,151 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Near-Plane Clipping - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* The problem +:PROPERTIES: +:CUSTOM_ID: the-problem +:END: + +When the camera brushes against geometry — a floor tile under your +feet, a wall you lean into — part of a polygon can end up *behind* the +viewer while the rest stays in front. Perspective projection divides by +depth (=screenX = x / z=), so a vertex at z ≤ 0 has no meaningful screen +position at all. + +The naive way out — dropping any polygon that has even one vertex +behind the camera — makes whole tiles vanish exactly when they are +closest and largest on screen. Walking through the House demo, floor +tiles blinked out of existence at the bottom of the frame: + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:near-clip-before.png]] + +*Aukio 3D* instead *clips the polygon against the near plane* and +renders the surviving fragment. The same frame with clipping enabled — +the floor is solid to the bottom edge: + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:near-clip-after.png]] + +Think of the camera plane as the edge of a table and the polygon as a +sheet of paper partly hanging off it. Dropping the polygon means +throwing away the whole sheet. Clipping takes scissors, cuts the sheet +along the table edge, and keeps the part that lies on the table. + +#+INCLUDE: "Near plane straddle.svg" export html + +* Why not just clamp z? +:PROPERTIES: +:CUSTOM_ID: why-not-clamp-z +:END: + +A tempting one-liner is to force every vertex to =z = max(z, epsilon)= +and project anyway. It fails geometrically: a vertex at z = −50 clamped +to z = 0.01 projects to a screen coordinate thousands of pixels away, +*in the wrong direction* — the sign flip of the division mirrors it +through the camera. The polygon smears into giant streaks across the +frame instead of ending cleanly at the screen edge. + +Clipping produces the geometrically correct cut: the polygon's new edge +lies exactly on the near plane, and everything the rasterizer receives +has z > 0. + +* How the clipping works +:PROPERTIES: +:CUSTOM_ID: how-it-works +:END: + +Clipping happens in *camera space*, after the transform stack has moved +vertices relative to the viewer but *before* the perspective divide. +The vertex loop is walked edge by edge (Sutherland-Hodgman style) +against the plane =z = nearPlaneDistance=: + +1. An in-front vertex passes through unchanged. +2. An edge that crosses the plane spawns a new vertex at the + intersection, with position, UV and normal all interpolated with the + same parameter =t=. +3. A behind-plane vertex is skipped. + +#+INCLUDE: "Clip algorithm.svg" export html + +Interpolating UVs linearly along the 3D edge is exactly right for the +perspective-correct texture mapper: the intersection vertex is a real +point on the original edge, so its texture coordinate is the same blend +of the endpoints' UVs. Textured fragments therefore show the correct +texels right up to the cut, with no seam. + +Only a polygon with *all* vertices behind the plane is culled — the +legitimate version of the old behavior. + +* From clipped loop to pixels +:PROPERTIES: +:CUSTOM_ID: from-clip-to-pixels +:END: + +A convex N-gon crossing the plane clips to a single contiguous loop of +at most N+1 vertices. For the triangle-based rasterizers this means a +triangle can become a *quad*, which is painted as a two-triangle fan +sharing the first vertex — exact, because the clip of a convex polygon +stays convex: + +#+INCLUDE: "Fan triangulation.svg" export html + +Shape support: + +| Shape | Behavior when straddling | +|--------------------+---------------------------------------------------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Clipped loop painted as triangle fan | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | Fan-painted with interpolated UVs (also inherited by lightmapped GI fragments) | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] | Shortened to the in-front endpoint + intersection | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.html][Billboard]] | Single anchor point: culled when behind, as before | + +Implementation notes: + +- Clipped output is stored *per pipeline slot* on the shape + (=clippedVertices(ctx)=), so the triple-buffered pipeline can + transform frame N+1 while frame N is still painting. +- Depth sorting and tile binning use the clipped vertices' average Z + and screen bounds — a clipped tile sorts as the fragment it became, + not as the polygon that reached behind you. +- New intersection vertices exist only in camera space; they are + projected directly via + [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html#setCameraSpaceCoordinate(double,double,double,eu.svjatoslav.aukio.e3d.gui.RenderingContext)][Vertex.setCameraSpaceCoordinate()]], + bypassing the transform stack. + +* Configuration +:PROPERTIES: +:CUSTOM_ID: configuration +:END: + +The near plane distance is a per-context knob, in world units: + +#+BEGIN_SRC java +// Default is 1.0; smaller values let the camera press closer to +// geometry before the scissors bite, at the cost of larger projected +// coordinates for clipped fragments. +viewPanel.getRenderingContext().nearPlaneDistance = 0.5; +#+END_SRC + +* Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|---------------------------+----------------------------------------------------------------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]] | Vertex-loop clipping in =transform()=, per-slot clip storage | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] | Camera-space projection for generated intersection vertices | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] | Carries =nearPlaneDistance= | diff --git a/Documentation/Near plane clip/near-clip-after.png b/Documentation/Near plane clip/near-clip-after.png new file mode 100644 index 0000000..f135bd2 Binary files /dev/null and b/Documentation/Near plane clip/near-clip-after.png differ diff --git a/Documentation/Near plane clip/near-clip-before.png b/Documentation/Near plane clip/near-clip-before.png new file mode 100644 index 0000000..6b27c44 Binary files /dev/null and b/Documentation/Near plane clip/near-clip-before.png differ diff --git a/Documentation/Normal vector.svg b/Documentation/Normal vector.svg new file mode 100644 index 0000000..016136e --- /dev/null +++ b/Documentation/Normal vector.svg @@ -0,0 +1,18 @@ + + + + + + + + + N̂ + unit normal + (perpendicular + to surface) + + + Light + + L · N = brightness + diff --git a/Documentation/Perspective correct textures/Adaptive interval.svg b/Documentation/Perspective correct textures/Adaptive interval.svg new file mode 100644 index 0000000..924bc5c --- /dev/null +++ b/Documentation/Perspective correct textures/Adaptive interval.svg @@ -0,0 +1,59 @@ + + + + + + perspective curvature → rises toward the far end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 16 px + 8 px + 4 px + 2 px + + one scanline, 100 px → + grazing-angle floor, far side + diff --git a/Documentation/Perspective correct textures/Affine distortion.png b/Documentation/Perspective correct textures/Affine distortion.png new file mode 100644 index 0000000..8d3722b Binary files /dev/null and b/Documentation/Perspective correct textures/Affine distortion.png differ diff --git a/Documentation/Perspective correct textures/Scanline correction.svg b/Documentation/Perspective correct textures/Scanline correction.svg new file mode 100644 index 0000000..cc5fc8d --- /dev/null +++ b/Documentation/Perspective correct textures/Scanline correction.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + screen pixel → + texel u ↑ + + + + + + + + + + + + + + + + + + + + exact perspective + + corrected every 16 px + + plain affine + diff --git a/Documentation/Perspective correct textures/index.org b/Documentation/Perspective correct textures/index.org new file mode 100644 index 0000000..3438f01 --- /dev/null +++ b/Documentation/Perspective correct textures/index.org @@ -0,0 +1,177 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Perspective-Correct Textures - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* The problem +:PROPERTIES: +:CUSTOM_ID: introduction +:ID: a2b3c4d5-e6f7-8901-bcde-f23456789012 +:END: + +When a textured polygon is rendered at an angle to the viewer, naive +linear interpolation of texture coordinates produces visible +distortion. + +Consider a large textured floor extending toward the horizon. Without +perspective correction, the texture appears to "swim" or distort +because the texture coordinates are interpolated linearly across +screen space, not accounting for depth. + +#+attr_html: :class responsive-img +#+attr_latex: :width 1000px +[[file:Affine distortion.png]] + +The *Aukio 3D* engine solves this with *subdivided perspective +correction* inside the scanline rasterizer — the same technique Quake +used. + +* How perspective correction works +:PROPERTIES: +:CUSTOM_ID: how-perspective-correction-works +:END: + +Texture coordinates (u, v) are not linear in screen space, so they +cannot simply be stepped per pixel. But divide them by depth and they +become linear: *(u/z, v/z, 1/z) all interpolate linearly* across the +triangle in screen space. + +The rasterizer exploits this: + +1. Compute (u/z, v/z, 1/z) at each vertex +2. Interpolate all three across the scanline with plain additions +3. Every N pixels, recover the exact texture coordinate with one + division: =u = (u/z) / (1/z)= +4. Between correction points, step u/v affinely toward the next exact + point + +#+INCLUDE: "Scanline correction.svg" export html + +The orange polyline hugs the exact green curve: within each 16-pixel +block it is a straight line, but every block starts exactly on the +curve. The dashed pink line is plain affine interpolation — visibly +wrong everywhere except the endpoints. + +Think of it like walking with a map that is slightly distorted: you +walk in a straight line, but every 16 steps you check a landmark and +correct your course. The correction (a division) costs something, so +you do it every N pixels instead of every pixel — the divide cost is +amortized to 1/16th of a per-pixel-correct rasterizer. + +#+BEGIN_SRC java +// Per scanline (simplified from drawHorizontalLinePerspective): +while (done < span) { + // Advance (u/z, v/z, 1/z) to the end of this block + su += dsu * block; sv += dsv * block; sw += dsw * block; + double txNext = su / sw; // one reciprocal = exact texture position + double tyNext = sv / sw; + + // Step affinely through the block + double txStep = (txNext - tx) / block; + double tyStep = (tyNext - ty) / block; + for (int i = 0; i < block; i++) { + plot(x++, texture.sample(tx, ty)); + tx += txStep; ty += tyStep; + } + done += block; +} +#+END_SRC + +** Adaptive correction interval + +Quake used a fixed 16-pixel interval. This engine keeps 16 as the +default but *shrinks the interval when the error bound demands it*. + +The error of affine stepping within a block grows with both the +texture gradient (texels per pixel) and the perspective curvature +(how fast 1/z changes across the span). Each scanline computes + +#+BEGIN_EXAMPLE +error(texels) ≈ texelRate · interval² · k / 2 +#+END_EXAMPLE + +where =k = |d(1/z)| / min(1/z)= is the per-pixel relative depth change, +and picks the largest power-of-two interval from the ladder 16, 8, 4, +2, 1 that keeps the bound under half a texel. Flat, gently angled +spans keep the fast 16-pixel cadence; a floor tile seen at a grazing +angle drops to shorter intervals exactly where the curvature is high. + +#+INCLUDE: "Adaptive interval.svg" export html + +** When affine is good enough + +For small or nearly flat triangles, plain affine mapping is already +within half a texel of exact perspective, so the perspective setup is +skipped entirely. The test compares the texture range the triangle +covers against its depth variation: + +#+BEGIN_EXAMPLE +affine is sufficient when texelSpan · (zMax/zMin − 1) < 2 +#+END_EXAMPLE + +Note the criterion is the *texel* span, not the pixel size — a tiny +on-screen triangle can still map many texels into few pixels. Distant +clusters of small triangles (a common case) all render through the +cheaper affine path. + +Triangles straddling the near plane (any vertex closer than z = 0.001) +also fall back to affine, because 1/z interpolation is invalid there. + +** Toggling the correction + +Perspective correction can be switched off globally for A/B comparison +or debugging: + +#+BEGIN_SRC java +TexturedTriangle.setPerspectiveCorrectionEnabled(false); // plain affine everywhere +#+END_SRC + +With correction disabled, large triangles at steep angles visibly warp +— useful for demonstrating what the correction actually buys. + +* Mipmap selection +:PROPERTIES: +:CUSTOM_ID: mipmap-selection +:END: + +Perspective correction fixes *where* a texel is sampled; mipmapping +decides *which resolution* to sample from. Each triangle estimates its +screen-pixels-per-texel ratio from edge lengths: + +#+BEGIN_EXAMPLE +scaleFactor = (sum of screen edge lengths) / (sum of UV edge lengths) · 1.2 +#+END_EXAMPLE + +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html#getMipmapForScale(double)][Texture.getMipmapForScale()]] +then picks the lazily-generated mipmap level closest to that scale: +halved resolutions when the texture is minified, doubled when strongly +magnified. Sampling a smaller mipmap under minification both speeds up +rendering (better cache behavior) and reduces aliasing. + +* Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|-------------------------------+--------------------------------------------------------------------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | Textured triangle with perspective-correct and SDF rendering paths | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PerspectiveBorderInterpolator.html][PerspectiveBorderInterpolator]] | Edge walker interpolating (u/z, v/z, 1/z) along triangle borders | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.html][PolygonBorderInterpolator]] | Edge walker for plain affine mapping | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html][Texture]] | Mipmap container; also carries the SDF mask and color layers | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html][TextureBitmap]] | Raw pixel array for one mipmap level | + +*See also:* + +- [[file:../SDF textures/][SDF textures]] — signed-distance-field glyph rendering, the + alternative sampling path in =TexturedTriangle= that reuses the same + perspective-correct interpolation for crisp text at any angle. diff --git a/Documentation/Point3D vertex.svg b/Documentation/Point3D vertex.svg new file mode 100644 index 0000000..0954bac --- /dev/null +++ b/Documentation/Point3D vertex.svg @@ -0,0 +1,105 @@ + + + + + + + +Point3D +raw coordinates + + + + + + +(x, y, z) + + + + + +.getDistanceTo() + + + + +.rotate() + + + + +.add() +.subtract() + + + + +.crossProduct() + + + + +.unit() +.dot() + + + + +.multiply() + + +Mutable, fluent API +Positions, vectors, math + + +Vertex +rendering-ready wrapper + + + + + + + +coordinate +Point3D + + + +wraps + + + +transformedCoordinate + + + +onScreenCoordinate + + + +textureCoordinate +UV + + + +normal +for CSG + + +local +camera +space +2D +pixels + + + +local +screen + + + +Tracks position across coordinate spaces + diff --git a/Documentation/Rendering loop/CPU scheduling.png b/Documentation/Rendering loop/CPU scheduling.png new file mode 100644 index 0000000..12aa23d Binary files /dev/null and b/Documentation/Rendering loop/CPU scheduling.png differ diff --git a/Documentation/Rendering loop/Double buffering.svg b/Documentation/Rendering loop/Double buffering.svg new file mode 100644 index 0000000..141dad6 --- /dev/null +++ b/Documentation/Rendering loop/Double buffering.svg @@ -0,0 +1,47 @@ + + + + + Without double-buffering + + + display shows partial update + + + + old frame + + + + ← tear + + + + new frame + + + With double-buffering + + + + Back buffer + (draw here) + + + + + + + + + swap + + + + Front buffer + (displayed) + + + complete + frame + diff --git a/Documentation/Rendering loop/Paint tiles.svg b/Documentation/Rendering loop/Paint tiles.svg new file mode 100644 index 0000000..e27d5f8 --- /dev/null +++ b/Documentation/Rendering loop/Paint tiles.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + one shape + + ~10 tiles per thread; threads steal pending + tiles — no fixed thread↔tile assignment + diff --git a/Documentation/Rendering loop/Painter's algorithm.svg b/Documentation/Rendering loop/Painter's algorithm.svg new file mode 100644 index 0000000..7727fc2 --- /dev/null +++ b/Documentation/Rendering loop/Painter's algorithm.svg @@ -0,0 +1,13 @@ + + + + + + Far (Z=500) — painted first + + + Medium (Z=300) — painted second + + + Near (Z=100) — painted last + diff --git a/Documentation/Rendering loop/Render pipeline.svg b/Documentation/Rendering loop/Render pipeline.svg new file mode 100644 index 0000000..927e357 --- /dev/null +++ b/Documentation/Rendering loop/Render pipeline.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + Shapes + + + Transform + + + Sort + + + Bin + + + Paint + + + Present + + + Screen + + + + + + + + + + + 3D vertices + world→screen + back-to-front + per tile + tile grid + own thread + diff --git a/Documentation/Rendering loop/index.org b/Documentation/Rendering loop/index.org new file mode 100644 index 0000000..c7fe2e0 --- /dev/null +++ b/Documentation/Rendering loop/index.org @@ -0,0 +1,523 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Rendering Loop - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* Rendering loop +:PROPERTIES: +:CUSTOM_ID: rendering-loop +:ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +:END: + +The rendering loop is the heart of the engine, continuously generating +frames on a dedicated background thread. It orchestrates the entire +rendering pipeline from 3D world space to pixels on screen. + +** What is a render loop? +:PROPERTIES: +:CUSTOM_ID: what-is-a-render-loop +:END: + +A *render loop* is a continuous process that generates visual frames +from 3D data. Think of it like a movie camera: each "frame" captures +the current state of the 3D world and converts it into a 2D image that +can be displayed on screen. + +The process transforms shapes through multiple coordinate systems: + +#+INCLUDE: "Render pipeline.svg" export html + +Each step has a specific purpose: + +| Step | Input | Output | Purpose | +|-----------+-------+--------+---------| +| Shapes | 3D [[file:../index.org::#vertex][vertices]], [[file:../index.org::#mesh][meshes]] | Scene data | Objects waiting to be drawn | +| Transform | World coordinates | Screen coordinates | Convert 3D positions to where they appear on screen (see [[file:../index.org::#coordinate-system][coordinate system]]); cull shapes outside the view frustum. Parallel: heavy subtrees fork onto the worker pool | +| Sort | Unordered shapes | Ordered by depth | Ensure correct visibility (far objects painted first). Parallel merge sort for large scenes | +| Bin | Sorted shapes | Per-tile shape lists | Each paint tile iterates only shapes that can touch it. Parallel over the worker pool | +| Paint | Per-tile shape lists | Pixels in buffer | Tiles split the screen into independent work units so clearing and rasterization run in parallel across CPU cores | +| Present | Pixel buffer | Screen image | Hand the completed frame to a dedicated thread that copies it to the display | + +This pipeline runs repeatedly, targeting 60 frames per second by +default. Even if nothing moves, the loop continues running—but the +engine [[#frame-listeners][skips unnecessary work]] when the scene is static. + +The steps above describe one frame *logically*, in the order data flows +through it. In execution the engine is a software pipeline: transform +of the next frame already runs while the previous frame is still being +painted, and presentation happens on its own thread. See +[[#software-pipeline][Software pipeline]]. + +** Main loop structure +:PROPERTIES: +:CUSTOM_ID: main-loop-structure +:END: + +The engine runs two dedicated daemon threads: + +- =e3d-render= — produces frames. It runs continuously: + +#+BEGIN_SRC java +while (renderThreadRunning) { + ensureThatViewIsUpToDate(); // Produce one frame (or skip) + maintainTargetFps(); // Sleep if ahead of schedule +} +#+END_SRC + +- =e3d-present= — presents frames. It takes completed frames from a + mailbox and performs all display-path work (the multi-megabyte + =drawImage=, =BufferStrategy.show()= and the X server round-trip), so + the render thread never blocks on the display. + +Both threads are daemons, so they stop automatically when the JVM +exits. You can stop them explicitly with [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#stop()][ViewPanel.stop()]]. + +** Frame rate control +:PROPERTIES: +:CUSTOM_ID: frame-rate-control +:END: + +The engine supports two modes: + +- *Target FPS mode*: Set with [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#setFrameRate(int)][setFrameRate(int)]]. + The engine tries to maintain the target rate by sleeping between frames. + + - *When rendering is slower than target*: No sleeping occurs. The engine + runs at maximum hardware speed. Missed frames are skipped, not + rendered later — the timing simply resets to current time. + + - *When rendering is faster than target*: The thread sleeps to limit FPS + to the target rate, avoiding unnecessary CPU usage. + + For example, with a 60 FPS target: + - If a complex scene takes 30ms per frame, you get ~33 FPS (hardware limit) + - If the scene later simplifies to 10ms per frame, you get exactly + 60 FPS (throttled by sleeping) + +- *Unlimited mode*: Set =setFrameRate(0)= or negative. No sleeping — + renders as fast as possible, and frames are produced even when the + scene reports no changes, so the measured rate reflects maximum + achievable throughput. Useful for benchmarking. + +*Production vs presentation.* These are measured separately: + +- *Production rate* (=getMeasuredFPS()=) counts frames the pipeline + completes per second. This is the benchmark number. +- *Presentation rate* is how fast frames actually reach the screen. In + capped-FPS mode the present thread is paced to 60 blits per second + (override with =-Daukio3d.presentRate=N=); the cap exists because the + X server also dispatches input, and flooding it with blits causes + desktop-wide mouse/keyboard jitter. When production outruns + presentation, stale frames are dropped from the mailbox instead of + piling up latency. In unlimited (benchmark) mode presentation pacing + is disabled entirely, so it cannot throttle production. + +* Software pipeline +:PROPERTIES: +:CUSTOM_ID: software-pipeline +:END: + +The phases below are described per frame, but consecutive frames +*overlap*. The engine triple-buffers everything a frame writes: + +- 3 framebuffers (each with its own =RenderingContext=) +- 3 projection buffer slots (per-vertex screen state) +- 3 render aggregators (transform output, sort/bin state) + +A render pass P (one per frame, or one per eye in stereo) transforms +into slot P mod 3, so it only conflicts with the paint of pass P-3. +Before each transform the render thread *flushes* completed paint +passes (mouse hits, frame deposit) and blocks only if paint P-3 is +still running — which steady-state worker throughput prevents. Workers +finishing one pass's tiles flow straight into the next pass's queued +tiles with no idle gap. + +Completed frames go to a *presentation mailbox* that keeps only the +newest frame: if the display path is slower than production, stale +frames are dropped (and their buffers released) instead of +accumulating latency — swapchain "mailbox mode". + +A per-buffer *present gate* guarantees painting frame F+3 never +overwrites a buffer the present thread is still blitting frame F from. + +The goal of all this overlap is throughput: keep every CPU core busy, +all the time. No phase waits for another phase of the same frame when +it could already be working on the next one. The Developer Tools +thread-activity timeline shows it working — all 18 worker rows packed +solid with paint, bin and sort tasks from up to three frames at once, +while the render thread (top row) and present thread tick along above +them: + +#+attr_html: :class responsive-img +[[file:CPU scheduling.png]] + +The pipeline can be disabled with =-Daukio3d.pipeline=false=, restoring +strictly sequential phase order (each paint pass is awaited +immediately). This is a kill switch for benchmarking and regression +hunting. + +* Rendering phases +:PROPERTIES: +:CUSTOM_ID: rendering-phases +:END: + +Each frame goes through 6 phases. Phases 2–4 run inside an +asynchronous continuation on the shared worker pool, and phases of +consecutive frames overlap as described in [[#software-pipeline][Software pipeline]]. + +** Phase 1: Transform shapes +:PROPERTIES: +:CUSTOM_ID: phase-1-transform-shapes +:END: + +All shapes are transformed from world space to screen space: + +1. Build camera-relative transform (inverse of camera position/rotation) +2. Update the view frustum from camera state and viewport dimensions +3. Walk the scene tree: + - Cull composite shapes whose bounding box misses the frustum + - Apply camera transform + - Project 3D → 2D (perspective projection) + - Calculate depth for sorting + - Queue for rendering + +*What is coordinate transformation?* + +Every shape exists in "world space" — its own position in the 3D world. +To render it, we must convert to "screen space" — where it appears on +your monitor. This involves: + +- *Translation*: Move coordinates relative to camera position +- *Rotation*: Rotate coordinates based on camera orientation +- *Projection*: Convert 3D (x, y, z) to 2D (x, y) screen pixels + +Objects further away appear smaller (perspective). The [[file:../index.org::#coordinate-system][coordinate system]] +uses Y-down to match screen conventions, making projection straightforward. + +The transform is *parallel and non-blocking*: composites with enough +children fork their render lists into chunk tasks on the shared worker +pool (at any nesting level), and the render thread returns without +waiting. The chunk tasks are drained and merged on a worker thread +inside the paint continuation, while the render thread is already +walking the next pass. + +*Frustum culling* happens here: composites test their bounding box +against the frustum and skip invisible subtrees entirely, saving both +transform and paint work. Per-frame culling statistics are collected +for the developer tools panel. + +** Phase 2: Sort shapes by depth +:PROPERTIES: +:CUSTOM_ID: phase-2-sort-shapes +:END: + +Shapes are sorted by depth in descending order (farthest first), with +the shape id as a deterministic tiebreaker: + +#+BEGIN_SRC java +// ShapesZIndexComparator: descending Z, ties broken by shape id +if (z1 < z2) return 1; // z1 is nearer -> sort after z2 +else if (z1 > z2) return -1; // z1 is farther -> sort before z2 +return Integer.compare(o1.shapeId, o2.shapeId); +#+END_SRC + +Above 8192 queued shapes the sort runs as an instrumented parallel +merge sort on the shared worker pool; below that it is single-threaded. + +*Why sort back-to-front?* + +This implements the *painter's algorithm* — like painting a landscape: +first paint the sky (farthest), then mountains, then trees, then the +foreground. Each layer covers what's behind it. + +#+INCLUDE: "Painter's algorithm.svg" export html + +Without sorting, nearby objects might be painted first and then covered +by distant ones, causing visual errors. This is especially important for +*transparent objects* — you need to see through the near ones to what's +behind. + +The Z value represents distance from the camera after transformation. +Larger values = further away. The id tiebreaker keeps the order +deterministic frame-to-frame, which tiled rendering relies on: every +tile paints its shapes in the same global (Z, id) order. + +** Phase 3: Bin shapes into tiles +:PROPERTIES: +:CUSTOM_ID: phase-3-bin-shapes-into-tiles +:END: + +The sorted queue is binned per paint tile by screen-space overlap: +each tile's bin lists only the shapes whose vertex bounds (plus a +paint margin) can touch that tile. A shape overlapping several tiles +is added to each of their bins. + +This means a paint thread iterates a short local list instead of the +whole scene, and it is what makes the tile grid scale: refining the +grid shrinks each bin instead of just subdividing the clearing work. + +Binning is parallelized over the shared worker pool. + +** Phase 4: Clear and paint tiles (multi-threaded) +:PROPERTIES: +:CUSTOM_ID: phase-4-clear-paint-tiles +:END: + +The viewport is divided into a grid of rectangular *tiles* — roughly +10 tiles per render thread, split into near-squares (square tiles +minimize boundary crossings, i.e. how many tiles each shape overlaps). +These are *not* horizontal bands: each tile has both X and Y bounds. + +#+INCLUDE: "Paint tiles.svg" export html + +Painting is work-stolen, not pre-assigned. All tile tasks go onto a +shared =ForkJoinPool= (sized to 75% of CPU threads by default, at most +cores − 1, so one thread stays free for the rest of the system; +changeable at runtime via =setNumRenderThreads(int)=). A worker that +finishes a cheap tile immediately pulls the next queued task — another +tile (of this or an adjacent frame's pass), a transform chunk, a sort +piece — so cores never idle behind a busy thread. + +Each tile task: + +1. *Clear tile*: fill its rectangle with background color +2. *Paint shapes*: rasterize the tile's bin, back-to-front, clipping + at tile bounds + +Both operations happen within the same task, so clearing always +completes before painting on that tile. Parallel clearing across +disjoint tiles maximizes memory bandwidth utilization. + +Each tile renders through a =SegmentRenderingContext= — a view of the +frame context carrying the tile's X/Y bounds and a =Graphics2D= +pre-clipped to the tile rectangle for thread-safe text and +anti-aliased drawing. (The class name predates the tile grid; a +"segment" is now a tile.) Mouse hit detection happens during painting, +before clipping. + +A =CountDownLatch= tracks completion of all the pass's tiles — but the +render thread does *not* wait for it here. The latch is awaited one +pass later, during the flush (see [[#phase-5-flush-completed-passes][Phase 5]]). + +** Phase 5: Flush completed passes +:PROPERTIES: +:CUSTOM_ID: phase-5-flush-completed-passes +:END: + +Before each new transform, the render thread flushes paint passes that +have completed. For each flushed pass: + +1. Await its tile latch (blocks only when correctness demands it — + transform of pass P may not start before paint of pass P-3 finished) +2. *Combine mouse results*: during painting, each tile tracked which + shape is under the mouse cursor. Since all tiles paint the same + back-to-front order, they should all report the same hit; the first + non-null result wins: + +#+BEGIN_SRC java +for (SegmentRenderingContext ctx : segmentContexts) { + if (ctx.getSegmentMouseHit() != null) { + context.setCurrentObjectUnderMouseCursor(ctx.getSegmentMouseHit()); + return; + } +} +#+END_SRC + + In stereo mode this only runs for the eye whose viewport actually + contains the cursor — each eye sees a different camera position, so + combining for the wrong eye would overwrite a valid hit with null. +3. If this pass completed a frame, deposit the frame into the + presentation mailbox (see [[#software-pipeline][Software pipeline]]). The render + thread never blocks on the display. + +Passes that finished painting are flushed without any blocking, so +completed frames reach the mailbox as early as possible. + +** Phase 6: Present frame +:PROPERTIES: +:CUSTOM_ID: phase-6-present-frame +:END: + +The =e3d-present= thread takes the newest mailbox frame (dropping any +unshown older frame) and copies its =BufferedImage= to the screen using +[[https://cr.openjdk.org/~iris/se/17/latestSpec/api/java.desktop/java/awt/image/BufferStrategy.html][BufferStrategy]] for tear-free page-flipping: + +#+BEGIN_SRC java +do { + Graphics2D g = bufferStrategy.getDrawGraphics(); + g.drawImage(context.bufferedImage, 0, 0, null); + g.dispose(); +} while (bufferStrategy.contentsRestored()); + +// framebuffer released for reuse here +bufferStrategy.show(); +Toolkit.getDefaultToolkit().sync(); +#+END_SRC + +The frame's buffer is released for reuse right after the =drawImage= +loop — =show()= and =sync()= touch only the BufferStrategy's own back +buffer and the X connection, and at high resolutions they cost more +than the draw itself, so the next frame's painters don't wait for them. + +*What is double-buffering?* + +Without double-buffering, the screen updates while pixels are being +written. This causes *screen tearing* — visible horizontal splits where +the top of the frame shows old content while the bottom shows new. + +#+INCLUDE: "Double buffering.svg" export html + +Double-buffering uses two pixel buffers: +- *Back buffer*: Where rendering happens (offscreen, invisible) +- *Front buffer*: What's currently displayed on screen + +When rendering completes, the buffers *swap* in one atomic operation. +The viewer always sees complete frames, never partial updates. + +The =do-while= loop handles the case where the OS recreates the back +buffer (common during window resizing). Since our offscreen +=BufferedImage= still has the correct pixels, we only need to re-blit, +not re-render. + +* Frame listeners and smart repaint skipping +:PROPERTIES: +:CUSTOM_ID: frame-listeners +:ID: e360a877-cca6-4cba-a9a4-ea40b0f1a183 +:END: + +A *FrameListener* is a callback that runs custom logic before each potential +frame. Think of it as your "per-frame hook" — the engine calls all registered +listeners, giving them a chance to update animations, physics, or game logic. + +** Registering a frame listener + +Use [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#addFrameListener(eu.svjatoslav.aukio.e3d.gui.FrameListener)][addFrameListener()]] to register your callback: + +#+BEGIN_SRC java +// This is how you register a frame listener +viewPanel.addFrameListener((panel, deltaMs) -> { + // Example: simple animation listener + double rotationSpeed = 1.0; // radians per second + shape.rotate(rotationSpeed * deltaMs / 1000.0); // Framerate-independent rotation + return true; // Request repaint (shape moved) +}); +#+END_SRC + +The listener receives two parameters: +- =panel=: The ViewPanel that's rendering +- =deltaMs=: Milliseconds since last frame (for framerate-independent animation) + +The return value controls whether the frame gets rendered: +- =true=: "Something changed — repaint the screen" +- =false=: "Nothing changed — can skip this frame" + +** Frame skipping optimization + +The engine avoids unnecessary rendering. A frame is skipped when: +- *All listeners return false* (nothing changed in your scene) +- *Camera did not move* (built-in Camera listener returns false once + the camera comes to rest) +- *No resize or repaint requests* + +This means a static scene with no animations consumes almost zero CPU. +The render thread keeps running (checking for changes), but actual pixel +rendering is skipped entirely. Skipped frames still flush any pending +paint passes from earlier frames, so in-flight frames always reach the +screen. + +Two exceptions force a frame regardless of listeners: +- *Unlimited (benchmark) mode* (=targetFPS <= 0=) renders continuously, + so the measured rate reflects maximum throughput +- An explicit repaint request (resize, stereo toggle, + =repaintDuringNextViewUpdate()=, etc.) + +#+BEGIN_SRC java +// Example: listener that only requests repaint when needed +viewPanel.addFrameListener((panel, deltaMs) -> { + if (gameState.hasUpdates()) { + gameState.processUpdates(); + return true; // Only repaint when game state actually changed + } + return false; // Skip frame — nothing to update +}); +#+END_SRC + +** Built-in listeners + +The engine registers these listeners by default: +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/Camera.html][Camera]] — applies movement velocity and friction each frame, and + returns true when the camera actually moved (more than a small + threshold), i.e. while the user is actively navigating +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.html][InputManager]] — processes mouse/keyboard events + +When the camera stops moving and you release all keys, the Camera listener +returns false. If your custom listeners also return false, the frame is +skipped until something changes. + +* Rendering context +:PROPERTIES: +:CUSTOM_ID: rendering-context +:END: + +The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] holds all state for rendering into one framebuffer: +the pixel buffer, projection parameters, and per-frame bookkeeping. + +| Field | Purpose | +|-------+---------| +| =pixels[]= | Raw pixel buffer (int[] in RGB format) | +| =bufferedImage= | Java2D wrapper around pixels | +| =graphics= | Graphics2D for text, lines, shapes | +| =width=, =height= | Full framebuffer dimensions | +| =centerCoordinate= | Screen center of the active viewport (for projection) | +| =projectionScale= | Perspective scale factor, derived from viewport width. Mutable: each stereo eye sets its own | +| =renderMinX=, =renderMaxX= | X bounds of the active viewport or tile | +| =renderMinY=, =renderMaxY= | Y bounds (full height on the frame context, tile bounds on segment views) | +| =stereoEye=, =stereoViewportWidth=, =stereoViewportOffsetX= | Which eye this pass renders and where its viewport sits in the buffer | +| =tilesX=, =tilesY=, =viewportCount=, =numRenderSegments= | Tile grid geometry (segments = tilesX × tilesY × viewports) | +| =frustum= | View frustum for culling, rebuilt each pass from camera state | +| =frameNumber= | Per-context frame counter | +| =transformCycleId= | Globally unique transform-cycle id, safe key for per-cycle memoization | +| =vertexSlot= | Projection buffer slot (0–2) this pass transforms into | + +** Triple-buffered frame contexts + +The engine keeps /three/ frame contexts, cycled by frame parity. While +frame N is still being painted from one buffer, frame N+1 already +transforms into the next — paint threads never idle waiting for the +transform phase, and vice versa. A per-buffer *present gate* prevents +painting frame F+3 into a buffer the present thread is still blitting +frame F from. + +All three contexts are recreated together when the window is resized, +when the tile grid changes (render thread count), or when stereo mode +is toggled. Otherwise they are reused — =prepareForNewFrameRendering()= +just resets per-frame state like mouse tracking. + +** Per-pass copies + +Each render pass (one per eye in stereo) works on a private /copy/ of +the frame context. The copy shares the pixel buffer, graphics and +services, but owns the projection fields (center, scale, viewport, +vertex slot), so the next pass's setup cannot disturb a pass whose +transform or paint is still in flight. + +Consequence for engine code: per-frame mutable state must be allocated +eagerly on the frame context. Anything created lazily inside a pass +lands on the throwaway copy and is lost. + +** Tile segment views + +Each paint tile gets a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/SegmentRenderingContext.html][SegmentRenderingContext]], +a view that shares the framebuffer with its parent but carries its own +X/Y tile bounds and a pre-clipped =Graphics2D= for thread-safe text and +shape drawing. Mouse hits are tracked per tile and combined after all +tiles finish painting. diff --git a/Documentation/SDF textures/SDF concept.svg b/Documentation/SDF textures/SDF concept.svg new file mode 100644 index 0000000..bc1b750 --- /dev/null +++ b/Documentation/SDF textures/SDF concept.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + Why a distance field, not a bitmap + one glyph edge, magnified 8x - stored coverage vs re-derived coverage + + + BITMAP coverage + what you store is what you get + + + + + + + + + + + + + + + + + + + + + + + + + + + + texel grid IS the resolution limit: + edges stair-step, curves become blocks + + + SDF: distance to edge + a smooth field - the edge is re-derived per pixel + + + + + + + + + + + + + + + + edge recovered at + display resolution + + + d < 0: inside ink + d > 0: outside + d = 0: the edge + + + + coverage = (127.5 - d) * aaK + 128 + aaK scales the gradient window to the pixel footprint - the same 16x32 texel field + serves a 4-pixel label and a full-screen billboard + diff --git a/Documentation/SDF textures/SDF glyph pipeline.svg b/Documentation/SDF textures/SDF glyph pipeline.svg new file mode 100644 index 0000000..09473d6 --- /dev/null +++ b/Documentation/SDF textures/SDF glyph pipeline.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + Glyph field generation (SdfGlyphCache) + once per character, then cached - stamping is a block copy + + + + 1. rasterize glyph + Liberation Mono Bold, AA on + 64x128 px (4x supersample) + font auto-sized to fit cell + advance 0.6em (Courier-compat) + + + + + + 2. distance transform + exact Euclidean EDT + (Felzenszwalb-Huttenlocher, + two separable 1-D passes) + dOut to ink, dIn to background + + + + + + 3. sign, clamp, average + signed = dOut - dIn + clamp to +/- 2 texels spread + average FIELD down to 16x32 + (averaging the field, not coverage, + preserves the edge position) + + + + + + + mask encoding (per texel, 0..255) + 0 = deep inside ink 127.5 = the edge 255 = far outside + + + + + + + + + + + + + + + + + TextCanvas.putChar + stamps the cached 16x32 mask + into the cell position of sdfMask + + + three texture layers + sdfMask: glyph SHAPES (bilinear) + sdfForeground + primary: colors + + why sans-serif bold: Courier's serifs and hairline strokes decay into unresolvable noise + when the distance field is minified - uniform sturdy strokes survive + + + + NO mipmaps on the mask: the edge gradient spans ~2 texels, + half-res masks melt glyph edges - minification is analytic instead + diff --git a/Documentation/SDF textures/SDF minification.svg b/Documentation/SDF textures/SDF minification.svg new file mode 100644 index 0000000..95cce51 --- /dev/null +++ b/Documentation/SDF textures/SDF minification.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + Minification: analytic coverage window + one screen pixel covering many texels still resolves the edge correctly + + + + a screen pixel on the texture + + + + + + + + + + + + + + + + + + + footprint: many texels per pixel + a bitmap would average to mush or alias; + the field still knows where the edge is + + + + per-axis footprint from UV gradients + footX = |dUV/dx|, footY = |dUV/dy| + window follows the SHARPEST axis + + + + + aaK = 2*spread / texelsPerPixel + widens the coverage window as pixels grow + + + + + perceptual corrections (minified text + else reads as gray haze): + SHARPEN x2: sub-pixel window kills halo + coverage gamma < 1: stem darkening + + + + result: graceful degradation + magnified: edges re-derived at display resolution - razor sharp + minified: coverage fades smoothly to clean gray, no crawling aliases + angled: the uncompressed axis keeps its sharpness + diff --git a/Documentation/SDF textures/glyph-sdf-S.png b/Documentation/SDF textures/glyph-sdf-S.png new file mode 100644 index 0000000..ecc1de5 Binary files /dev/null and b/Documentation/SDF textures/glyph-sdf-S.png differ diff --git a/Documentation/SDF textures/index.org b/Documentation/SDF textures/index.org new file mode 100644 index 0000000..ff4cf78 --- /dev/null +++ b/Documentation/SDF textures/index.org @@ -0,0 +1,252 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: SDF Textures - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* What SDF textures are +:PROPERTIES: +:CUSTOM_ID: what-sdf-is +:END: + +A regular texture stores *coverage*: each texel says "this much ink +here". That is a photocopy of the glyph — resample it (magnify, minify, +view at an angle) and the stored pixels blur or alias, because the +information about /where the edge is/ was thrown away when the glyph +was rasterized. + +A *signed distance field* (SDF) texture stores something smarter: per +texel, the *distance to the nearest edge* — negative inside the ink, +positive outside, zero exactly on the boundary. The rasterizer then +re-derives coverage per screen pixel from this smooth field. The edge +position survives resampling because the field around it is linear — +bilinear interpolation of a linear ramp is exact. + +#+ATTR_HTML: :width 640 +[[file:SDF concept.svg]] + +In Aukio 3D the mask is a grayscale field in =texture.sdfMask=: + +- =0= — deep inside the ink +- =127.5= — exactly on the edge +- =255= — far outside any glyph + +The gradient spans only =SPREAD_TEXELS = 2.0= texels around the edge — +that narrow band is all the rasterizer needs. + +Here is a real field, dumped straight from =SdfGlyphCache= (glyph "S", +16x32 texels, upscaled 12x with nearest so you can see the texels): + +[[file:glyph-sdf-S.png]] + +Dark inside the strokes, bright outside, and a smooth gray ramp exactly +two texels wide around the contour. + +* Generating glyph fields +:PROPERTIES: +:CUSTOM_ID: glyph-pipeline +:END: + +=SdfGlyphCache= generates each character's distance field once and +caches it in a =ConcurrentHashMap=; stamping a glyph into a canvas is +then just a block copy. + +#+ATTR_HTML: :width 640 +[[file:SDF glyph pipeline.svg]] + +The steps: + +1. *Rasterize* the glyph with AWT at 4x the cell size (64x128 pixels) + with anti-aliasing on, using Liberation Mono Bold (metric-compatible + with Courier New, so the cell grid is unchanged). The font size is + auto-shrunk until the widest glyph fits the scratch without clipping + — a clipped glyph would corrupt the distance field at the cell edge. +2. *Distance transform*: an exact Euclidean distance transform + (Felzenszwalb & Huttenlocher, two separable 1-D passes over parabola + envelopes) is run twice — once for distance to nearest ink pixel, + once for distance to nearest background pixel. +3. *Sign, clamp, average*: signed distance = dOut - dIn, clamped to + +/-2 texels of spread, then the *field* (not coverage) is averaged + down to the 16x32 cell resolution. Averaging the field preserves the + edge position; averaging coverage would not. + +The font choice matters: Courier's serifs and hairline strokes decay +into unresolvable noise when the field is minified. A uniform-stroke +bold sans-serif survives. + +* The rendering path +:PROPERTIES: +:CUSTOM_ID: render-path +:END: + +When =texture.isSdf()= is true (an =sdfMask= is attached), +=TexturedTriangle.paintSdf= takes over. Three layers are involved: + +| Layer | Contents | Sampling | +|------------------+-----------------------------+----------| +| =sdfMask= | glyph shapes (the field) | bilinear | +| =sdfForeground= | ink color, flat per cell | nearest | +| =primaryBitmap= | background color, per cell | nearest | + +Per screen pixel: + +1. Sample the mask bilinearly (fixed-point) -> distance =d=. +2. Convert to coverage: =cov = (127.5 - d) * aaK + 128=, clamped to + [0, 256]. =aaK= scales the 2-texel gradient window to the current + pixel footprint (see next section). +3. Blend: =pixel = bg * (1 - cov) + fg * cov=. + +Perspective-correct interpolation applies to SDF triangles exactly as +it does to regular textured triangles — same affine-sufficiency test, +same subdivided correction. See +[[file:../Perspective correct textures/index.org][Perspective-correct +textures]]; only the per-pixel sampling differs. + +* Minification without mipmaps +:PROPERTIES: +:CUSTOM_ID: minification +:END: + +*There is deliberately no mipmap chain for SDF layers.* A distance +field's edge gradient spans ~2 texels; a half-resolution mask melts the +glyph edges. Worse, the two triangles of a rectangle cross mip +thresholds at slightly different distances, producing a hard diagonal +quality split and sudden blur steps while dollying (observed in +practice). + +Minification is instead handled *analytically*: the coverage window is +widened by the screen-space pixel footprint, giving area-correct +coverage straight from the primary field. + +#+ATTR_HTML: :width 640 +[[file:SDF minification.svg]] + +The footprint is computed per axis from the screen-space UV gradients — +=text on an angled plane is minified mostly along one axis=, and an +isotropic average would blur the axis that still has resolution to +spare. The coverage window follows the sharpest axis. + +Area-correct coverage alone reads as a low-contrast gray haze, so two +perceptual corrections (A/B-tuned on far + angled text) kick in under +minification: + +- *Sharpening* (=SDF_SHARPEN=, default 2): narrows the coverage window + below one pixel — kills the haze halo at the cost of slight shimmer. +- *Coverage gamma* (< 1, automatic from the footprint): darkens stems + like a small-size font rasterizer, keeping thin strokes present. + +Real output, rendered headlessly through the [[file:../index.org::#snapshot][Snapshot tool]]: + +Magnified — edges re-derived at display resolution, razor sharp: + +[[file:sdf-near.png]] + +At moderate distance: + +[[file:sdf-mid.png]] + +Far away — small but clean, fading to gray instead of disintegrating +into aliases (right: 4x nearest zoom of the center): + +[[file:sdf-far.png]] + +[[file:sdf-far-zoom.png]] + +At an oblique angle — foreshortened along one axis, still sharp along +the other: + +[[file:sdf-angled.png]] + +* Using it +:PROPERTIES: +:CUSTOM_ID: using-sdf +:END: + +*TextCanvas* is the main entry point: a textured rectangle carrying a +character grid in 3D space. World cell size 8x16 units, texture cell +16x32 texels (2 texels per world unit). + +#+BEGIN_SRC java +Transform location = new Transform(new Point3D(0, 0, 500)); +TextCanvas canvas = new TextCanvas(location, "Hello, World!", + Color.WHITE, Color.BLACK); +shapeCollection.addShape(canvas); + +// blank canvas + cursor writing +TextCanvas blank = new TextCanvas(location, new TextPointer(10, 40), + Color.GREEN, Color.BLACK); +blank.locate(0, 0); +blank.print("Line 1"); +blank.locate(1, 0); +blank.print("Line 2"); +blank.setForegroundColor(Color.RED); // affects subsequent writes +blank.setTextColor(Color.CYAN); // recolors existing ink only +#+END_SRC + +Colors are per-cell: each =putChar= fills the cell's rectangle in the +background and foreground layers, so one canvas can hold many colors. + +*ForwardOrientedTextBlock* renders the same pipeline onto a billboard +that always faces the camera — for labels that must stay readable from +any angle: + +#+BEGIN_SRC java +ForwardOrientedTextBlock label = new ForwardOrientedTextBlock( + new Point3D(0, -50, 300), 1.0, 2, "Hello, World!", Color.RED); +shapeCollection.addShape(label); +#+END_SRC + +Real use in the demos: the life demo's help panel (=life_demo/Main.java= +=createHelpPanel()=) and the axis labels in =CoordinateSystemDemo=. + +* Tuning knobs +:PROPERTIES: +:CUSTOM_ID: tuning +:END: + +JVM properties (A/B tuning knobs in =TexturedTriangle=): + +| Property | Default | Effect | +|-------------------+---------+-------------------------------------------| +| =e3d.sdf.gamma= | 0 (auto) | fixed coverage gamma; auto derives from footprint | +| =e3d.sdf.sharpen= | 2 | coverage window narrowing; 1 = pixel-exact | +| =e3d.sdf.debug= | false | prints per-triangle footprints and path decisions to stderr | + +* Limitations +:PROPERTIES: +:CUSTOM_ID: limitations +:END: + +- *Fixed cell grid*: TextCanvas is monospace by construction (16x32 + texel cells). Proportional fonts would need a different stamping + scheme. +- *ASCII-oriented cache*: =SdfGlyphCache= measures printable ASCII + (33..126) when sizing the font; exotic glyphs may fit worse. +- *Under extreme minification* text fades to gray by design — that is + the correct physical answer (a sub-pixel glyph has no shape left), + but it means distant labels are decorative, not readable. +- *Bandwidth under minification*: sampling the primary field (no mip + chain) costs more bandwidth per pixel. Text surfaces are small, so + this is the right trade — do not attach SDF masks to huge surfaces. + +* Related classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Role | +|-----------------------------+--------------------------------------------------| +| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.html][TextCanvas]] | Character grid surface in 3D; owns the layers | +| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/SdfGlyphCache.html][SdfGlyphCache]] | Per-glyph field generation + cache (EDT inside) | +| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.html][ForwardOrientedTextBlock]] | Camera-facing text billboard | +| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | =paintSdf= — the scanline path | +| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html][Texture]] | =sdfMask=, =sdfForeground=, =sdfSpreadTexels= | + +*See also:* + +- [[file:../Perspective correct textures/][Perspective-correct textures]] — the scanline texture-mapping path + that SDF rendering builds on; both paths live in =TexturedTriangle= + and share the same interpolated UVs. + diff --git a/Documentation/SDF textures/sdf-angled.png b/Documentation/SDF textures/sdf-angled.png new file mode 100644 index 0000000..34814bf Binary files /dev/null and b/Documentation/SDF textures/sdf-angled.png differ diff --git a/Documentation/SDF textures/sdf-far-zoom.png b/Documentation/SDF textures/sdf-far-zoom.png new file mode 100644 index 0000000..f0c0a44 Binary files /dev/null and b/Documentation/SDF textures/sdf-far-zoom.png differ diff --git a/Documentation/SDF textures/sdf-far.png b/Documentation/SDF textures/sdf-far.png new file mode 100644 index 0000000..64898a2 Binary files /dev/null and b/Documentation/SDF textures/sdf-far.png differ diff --git a/Documentation/SDF textures/sdf-mid.png b/Documentation/SDF textures/sdf-mid.png new file mode 100644 index 0000000..ba8ed3e Binary files /dev/null and b/Documentation/SDF textures/sdf-mid.png differ diff --git a/Documentation/SDF textures/sdf-near.png b/Documentation/SDF textures/sdf-near.png new file mode 100644 index 0000000..1d493a1 Binary files /dev/null and b/Documentation/SDF textures/sdf-near.png differ diff --git a/Documentation/Shading/Ambient light comparison.svg b/Documentation/Shading/Ambient light comparison.svg new file mode 100644 index 0000000..ce07a00 --- /dev/null +++ b/Documentation/Shading/Ambient light comparison.svg @@ -0,0 +1,51 @@ + + + + + + +Ambient Light +base illumination applied to all surfaces equally, regardless of orientation + + + + + + + + + + + + + + +Color(0, 0, 0) +✗ pure black +harsh shadows, no depth + + + + + + + + +Color(50, 50, 50) +✓ balanced +depth preserved + + + + + + + + +Color(150, 150, 150) +✗ too flat +no depth contrast + + +lightingManager.setAmbientLight(new Color(50, 50, 50)) ← default + \ No newline at end of file diff --git a/Documentation/Shading/Distance attenuation.svg b/Documentation/Shading/Distance attenuation.svg new file mode 100644 index 0000000..2edf492 --- /dev/null +++ b/Documentation/Shading/Distance attenuation.svg @@ -0,0 +1,91 @@ + + + + + + + + + +Distance Attenuation +light intensity falls off with distance from source + + + + + + + + + + + + +Light + + + + + + +0.99 + +d = 100 + + + + +0.52 + +d = 300 + + + + +0.29 + +d = 500 + +← attenuation factor shown above each surface → + + +attenuation vs distance + + +d +att + +0 + + +0.5 + + +1.0 + + +100 + + +300 + + +500 + + + + + +0.99 +0.52 +0.29 + + + +attenuation = +1 / (1 + 0.0001 · d²) + + +coefficient 0.0001 was tuned for typical scene scales in Aukio 3D + diff --git a/Documentation/Shading/Lambert cosine law.svg b/Documentation/Shading/Lambert cosine law.svg new file mode 100644 index 0000000..1f4e216 --- /dev/null +++ b/Documentation/Shading/Lambert cosine law.svg @@ -0,0 +1,92 @@ + + + + + + + + +Lambert Cosine Law +how surface orientation determines light intensity + + + + + + +N̂ +normal + + + + + + + + +Light + +L̂ + +θ +surface polygon + +brightness = +dot( N̂ , L̂ ) + += cos( θ ) + +θ = 0° + + +1.00 +θ = 45° + + +0.71 +θ = 90° + +0.00 + +θ > 90° +back-face → skip +dot < 0 → no contribution +— angle examples — + + + + + + θ = 0° + + + 100% + + + + + + + + 45° + θ = 45° + + + 71% + + + + + + + + 90° + θ = 90° + + 0% (skip) + + +N̂ surface normal + +L̂ light direction + diff --git a/Documentation/Shading/Shaded sphere.png b/Documentation/Shading/Shaded sphere.png new file mode 100644 index 0000000..fbc6487 Binary files /dev/null and b/Documentation/Shading/Shaded sphere.png differ diff --git a/Documentation/Shading/Shading pipeline.svg b/Documentation/Shading/Shading pipeline.svg new file mode 100644 index 0000000..a58c431 --- /dev/null +++ b/Documentation/Shading/Shading pipeline.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + Transform + compute lighting + + + + Shapes + + + Sort + + + Paint + use cached color + + + Blit + + + + + + + + diff --git a/Documentation/Shading/index.org b/Documentation/Shading/index.org new file mode 100644 index 0000000..fdf95a0 --- /dev/null +++ b/Documentation/Shading/index.org @@ -0,0 +1,266 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Shading & Lighting - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* Overview +:PROPERTIES: +:CUSTOM_ID: shading-lighting +:END: + +#+attr_html: :class responsive-img +#+attr_latex: :width 1000px +[[file:Shaded sphere.png]] + +*Aukio 3D* implements *flat shading* using the [[https://en.wikipedia.org/wiki/Lambert%27s_cosine_law][Lambert cosine +law]]. Each polygon receives a single color based on its orientation +relative to light sources. This is a simple yet effective lighting +model that gives 3D objects depth and realism. + +** The Lighting Model: Lambert Cosine Law +:PROPERTIES: +:CUSTOM_ID: lambert-cosine-law +:END: + +#+INCLUDE: "Lambert cosine law.svg" export html + +The *Lambert cosine law* determines how much light a surface receives +based on its orientation. A surface facing directly toward a light source +receives maximum illumination; as it tilts away, the illumination decreases +proportionally until it reaches zero when perpendicular to the light +direction. This fundamental principle creates the visual cues that make 3D +objects appear solid and dimensional rather than flat. + +The engine implements this law through the dot product of two vectors. The +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] computes a unit vector pointing from the polygon's center +to each [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]], then calculates the dot product with the surface +normal. When the dot product equals 1.0, the surface faces the light +directly and receives full brightness. At 0.71 (a 45-degree angle), it +receives about 71% illumination. At zero or below, the surface faces away +from the light and receives no direct contribution from that source. The +implementation in [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager.computeLighting()]] explicitly checks for +positive dot products before adding light contributions, ensuring that +back-facing surfaces skip unnecessary calculations. + +The surface normal itself is computed by [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]], which takes +the first three vertices of a polygon and calculates their cross product to +find the perpendicular direction. This normal, along with the polygon's +center point calculated by [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]], is passed to the lighting manager +during the [[file:../Rendering loop/][transform phase]] of the rendering loop. The transform phase runs +in parallel, but each polygon is transformed by exactly one worker per +pass, so its cached =shadedColor= field has a single writer — the +result is reused allocation-free during the subsequent multi-threaded +paint phase. See the +[[file:../index.org::#normal-vector][Normal Vector]] section for more details on how normals are computed and used +throughout the engine. + +* Light Sources +:PROPERTIES: +:CUSTOM_ID: light-sources +:END: + +Each light source has three properties: + +| Property | Description | +|------------+--------------------------------------| +| Position | 3D world coordinates of the light | +| Color | RGB color of emitted light | +| Intensity | Brightness multiplier (1.0 = normal) | + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; + +// Create a bright yellow light to the right +LightSource rightLight = new LightSource( + new Point3D(200, -100, 0), // position: right, above, at viewer level + Color.YELLOW, // color + 2.0 // intensity: extra bright +); + +// Create a dim blue light from the left +LightSource leftLight = new LightSource( + new Point3D(-150, 50, 100), + Color.BLUE, + 0.5 // intensity: dim +); +#+END_SRC + +Multiple light sources add their contributions together, allowing for +complex lighting setups like the screenshot above showing a sphere lit +by two lights from the right. + +** Distance Attenuation +:PROPERTIES: +:CUSTOM_ID: distance-attenuation +:END: + +#+INCLUDE: "Distance attenuation.svg" export html + +Light intensity decreases with distance using a *simplified inverse +square law*: + +#+BEGIN_SRC +attenuation = 1.0 / (1.0 + 0.0001 * distance²) +#+END_SRC + +- At distance 0: attenuation = 1.0 (full intensity) +- At distance 100: attenuation ≈ 0.99 (almost full) +- At distance 300: attenuation ≈ 0.52 (half intensity) +- At distance 500: attenuation ≈ 0.29 (about 30%) + +This simplified formula prevents harsh cutoffs while still providing +distance-based dimming. The =0.0001= coefficient was tuned for typical +scene scales in Aukio 3D. + +* Ambient Light +:PROPERTIES: +:CUSTOM_ID: ambient-light +:END: + +#+INCLUDE: "Ambient light comparison.svg" export html + +*Ambient light* provides base illumination that affects all surfaces +equally, regardless of orientation. Without ambient light, surfaces not +directly facing a light source would be pure black. + +- Default ambient: =Color(50, 50, 50)= (dim gray) — set by the ViewPanel + constructor; a standalone =new LightingManager()= starts at + =Color(10, 10, 10)= +- Configurable via =lightingManager.setAmbientLight()= +- Too much ambient: flat appearance (no contrast) +- Too little ambient: harsh shadows (pure black areas) + +#+BEGIN_SRC java +// Increase ambient for softer shadows +viewPanel.getLightingManager().setAmbientLight(new Color(80, 80, 80)); + +// Reduce ambient for dramatic contrast +viewPanel.getLightingManager().setAmbientLight(new Color(20, 20, 20)); +#+END_SRC + +* Using Shading in Your Scene +:PROPERTIES: +:CUSTOM_ID: using-shading +:END: + +**Adding light sources:** + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource; + +ViewPanel viewPanel = new ViewPanel(); + +// Get the lighting manager +LightingManager lighting = viewPanel.getLightingManager(); + +// Add light sources +lighting.addLight(new LightSource( + new Point3D(200, -100, 0), // right side, above + Color.YELLOW, + 1.5 // bright +)); + +lighting.addLight(new LightSource( + new Point3D(-100, 0, 200), // left side, further away + new Color(255, 200, 150), // warm white + 1.0 +)); + +// Configure ambient light +lighting.setAmbientLight(new Color(40, 40, 40)); +#+END_SRC + +**Enabling shading on shapes:** + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonRectangularBox; + +// Create a shaded box +SolidPolygonRectangularBox box = new SolidPolygonRectangularBox( + new Point3D(-50, -50, 100), // min corner + new Point3D(50, 50, 200), // max corner + Color.RED +); + +// Enable shading on the box and all its sub-polygons +box.setShadingEnabled(true); + +// Also enable backface culling for closed meshes +box.setBackfaceCulling(true); + +// Add to scene +viewPanel.getRootShapeCollection().addShape(box); +#+END_SRC + +Shading propagates through composite shapes — calling +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setShadingEnabled(boolean)][setShadingEnabled(true)]] on a composite enables shading for all its +sub-polygons. + +** Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|-------+---------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] | Manages light sources and computes shading | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]] | Individual light with position, color, intensity | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Polygon shape with shading support | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] | Composite shape with shading propagation | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html][ViewPanel]] | Provides access to LightingManager | +* Implementation details +:PROPERTIES: +:CUSTOM_ID: implementation-details +:END: + +#+INCLUDE: "Shading pipeline.svg" export html + +Lighting is computed during *Phase 1* (transform phase) of the +[[file:../Rendering loop/][rendering loop]]: + +1. Each shaded polygon calculates its center point and surface normal +2. [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] computes lighting from all sources +3. Result stored in reusable =shadedColor= field +4. During *Phase 4* (paint), the cached color is used directly + +**Why during transform phase?** + +- Lighting computed *once per polygon per pass* — not per pixel +- Each polygon is transformed by a single worker, so its cached result + has exactly one writer even though the transform phase runs in parallel +- Result reused during multi-threaded paint phase — efficient + +** Performance Characteristics +:PROPERTIES: +:CUSTOM_ID: performance +:END: + +| Aspect | Cost | +|--------------+-------------------------------| +| Computation | Per polygon, not per pixel | +| Phase | Parallel transform (single writer per polygon) | +| Allocation | Zero (reuses Color instance) | +| Cache | One shadedColor per polygon | + +The shading implementation is optimized for CPU rendering: + +- *Flat shading*: One lighting calculation per polygon (N-vertex polygon = 1 calculation) +- *Reusable Color*: Result stored in existing field, no allocation during render +- *Thread-safe*: One writer per polygon per pass, so no synchronization needed +- *Pre-computed*: All paint workers (tile grid, ~75% of CPU cores by default) read the same cached result + +This approach trades visual fidelity (no per-pixel lighting) for +performance — essential for software rendering where per-pixel lighting +would be prohibitively expensive. diff --git a/Documentation/Stereoscopic rendering/Stereo geometry.svg b/Documentation/Stereoscopic rendering/Stereo geometry.svg new file mode 100644 index 0000000..2f62d50 --- /dev/null +++ b/Documentation/Stereoscopic rendering/Stereo geometry.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Two parallel cameras, one screen + top-down view of the scene (z grows downward = into the scene) + + + + screen plane (per eye) + + + + near object + + far object + + + + left eye + x - IPD/2 + + right eye + x + IPD/2 + + + + + + IPD = 6.5 units (cm) + + + + + + + + + + + + + + + + + + + + + + + + large disparity = close + + small disparity = far + + cameras stay PARALLEL (no toe-in) - depth comes purely from the x offset + diff --git a/Documentation/Stereoscopic rendering/Stereo per eye.svg b/Documentation/Stereoscopic rendering/Stereo per eye.svg new file mode 100644 index 0000000..5fc9cfd --- /dev/null +++ b/Documentation/Stereoscopic rendering/Stereo per eye.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + What changes per eye + + + + + concern + per-eye behavior + + + camera + translation.x += ±IPD/2 (restored after the pass) + + + projection + scale = eyeWidth/3; x += stereoViewportOffsetX + + + frustum culling + built from stereoViewportWidth - narrower FOV per eye + + + painting + clipped to [renderMinX, renderMaxX) = the eye's half + + + mouse picking + hits combined only for the eye containing the cursor + + + HUD / overlays + drawn once, spanning the full frame (zero disparity) + + + everything else - geometry, textures, lightmaps, GI - is shared: the scene is identical, only the viewpoint moves + diff --git a/Documentation/Stereoscopic rendering/Stereo pipeline.svg b/Documentation/Stereoscopic rendering/Stereo pipeline.svg new file mode 100644 index 0000000..895e1fc --- /dev/null +++ b/Documentation/Stereoscopic rendering/Stereo pipeline.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + One frame = two passes + the triple-buffered pipeline runs the same phases twice, once per eye + + + + camera + translation.x nudged +/- IPD/2 + + + + pass LEFT + transform → sort → tile-bin + viewport [0, w/2) + + + + pass RIGHT + transform → sort → tile-bin + viewport [w/2, w) + + + + + + + + each pass owns a RenderingContext copy + stereoEye, stereoViewportWidth/OffsetX, renderMinX..renderMaxX + + + + + + + + + + left eye pixels + painting clipped to left half + right eye pixels + painting clipped to right half + ONE shared frame buffer → one blit to screen (side-by-side image) + + + + + vertex buffers, aggregators and paint slots still cycle through 3 slots - the two passes overlap freely + diff --git a/Documentation/Stereoscopic rendering/index.org b/Documentation/Stereoscopic rendering/index.org new file mode 100644 index 0000000..f1ceee8 --- /dev/null +++ b/Documentation/Stereoscopic rendering/index.org @@ -0,0 +1,190 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Stereoscopic Rendering - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \setlength{\parindent}{15pt} +#+LATEX_HEADER: \usepackage{palatino} +#+LATEX_HEADER: \usepackage{charter} +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* What stereo rendering adds +:PROPERTIES: +:CUSTOM_ID: what-stereo-adds +:END: + +A single rendered image is flat: the brain infers depth only from +monocular cues (occlusion, shading, perspective, motion parallax while +you move). *Stereoscopic rendering* adds the strongest depth cue of +all — /binocular disparity/: your two eyes see slightly different +images, and the visual cortex turns the difference into a direct +sensation of depth. + +Aukio 3D implements the simplest and most portable form: *side-by-side +stereo*. Every frame renders the scene twice — once from the left eye +position, once from the right — into the left and right halves of the +same image. A VR headset, 3D TV, or a pair of XR glasses in +side-by-side mode feeds each half to the corresponding eye, and the +scene gains real volume. + +#+CAPTION: A side-by-side stereoscopic frame of the House demo, rendered headlessly with [[file:../index.org::#snapshot][the Snapshot tool]]. Left half: left eye. Right half: right eye. Compare the dark cube and the doorway between the halves — the horizontal shift is the disparity your brain reads as depth. +[[file:stereo-side-by-side.png]] + +* The geometry: two parallel cameras +:PROPERTIES: +:CUSTOM_ID: geometry +:END: + +The two eye cameras are identical to the mono camera except for one +thing: the left eye's position is shifted by -IPD/2 and the right +eye's by +IPD/2 along the *world* X axis (the offset is applied to the +camera translation's =x= component directly, then restored). IPD +(inter-pupillary distance) defaults to *6.5 world units* — the House +demo treats 1 unit as 1 cm, and 6.5 cm is the median human IPD. + +Both cameras look in exactly the same direction (*parallel cameras*, +no toe-in). Objects at different depths then land at different +horizontal offsets between the two images — that offset is the +disparity: + +#+CAPTION: Top-down view: the two eye positions and how a near and a far object project onto the screen plane. The near object separates much more between the eyes than the far one. +[[file:Stereo geometry.svg]] + +- near object -> large disparity -> feels close, +- far object -> small disparity -> feels far, +- object at infinity -> zero disparity. + +Larger IPD exaggerates disparity (stronger but potentially straining +depth); smaller IPD flattens the scene. =+=/=-= keys adjust it live in +0.5-unit steps while stereo is active. + +* One frame = two passes +:PROPERTIES: +:CUSTOM_ID: two-passes +:END: + +Stereo does not add a second pipeline — it runs the existing +triple-buffered pipeline *twice per frame*. The render thread in +=ViewPanel.renderFrame()= executes two render passes back to back: + +#+CAPTION: Per frame, the camera is nudged left, a full transform/sort/bin pass runs for the left viewport, then the camera is nudged right and a second pass runs for the right viewport. Both paint into one shared frame buffer, clipped to their half. +[[file:Stereo pipeline.svg]] + +1. *Pass LEFT:* camera translation.x is temporarily decreased by + IPD/2, the scene is transformed, depth-sorted and tile-binned into a + per-pass =RenderingContext= copy whose viewport is the left half of + the frame (=[0, width/2)=), and the paint continuation is submitted + to the worker pool. +2. *Pass RIGHT:* the same with +IPD/2 and the right viewport + (=[width/2, width)=). The camera offset is always restored in a + =finally= block, so the camera never drifts. +3. The two paints write into *one shared frame buffer* — each clipped + to its half — and the completed side-by-side image is blitted to + the screen in one go. + +The triple-buffer machinery (3 vertex slots, 3 aggregator slots, 3 +framebuffers) does not change: a *pass* takes the slot =passCounter % +3=, so left and right passes of the same frame simply occupy +consecutive slots and overlap exactly like consecutive mono frames do. +Workers flow from one pass's tiles straight into the next pass's tiles +with no idle gap. + +* What adapts per eye +:PROPERTIES: +:CUSTOM_ID: per-eye +:END: + +The scene itself — geometry, textures, lightmaps, global illumination +— is shared and identical for both eyes. Only the *viewpoint* moves, +so only view-dependent stages differ per pass: + +#+CAPTION: The per-eye surface area of the engine. Everything not listed here is eye-independent. +[[file:Stereo per eye.svg]] + +- *Projection:* =Vertex= projects with =projectionScale = eyeWidth/3= + (per-eye horizontal FOV) and adds =stereoViewportOffsetX= so the + projected image lands in the correct half of the buffer. The same + offset is applied for near-plane-clip vertices created directly in + camera space. +- *Frustum culling:* the frustum is rebuilt per pass from + =stereoViewportWidth=, so each eye culls against its own (narrower) + view volume — nothing leaks in from the other eye's half. +- *Painting:* every painter clips X to =[renderMinX, renderMaxX)=, + which the pass set to its viewport. No eye can paint into the other + half, even if a polygon crosses the center line. +- *Mouse picking:* in stereo each eye shows the same object at a + different screen X, so a hit can only be resolved against one eye. + =ViewPanel= combines mouse results only for the pass whose viewport + actually contains the cursor. +- *HUD/overlays:* developer tools, crosshair and text are drawn once + over the finished frame, at zero disparity — they sit on the screen + surface, not in the world. + +* Enabling stereo +:PROPERTIES: +:CUSTOM_ID: enabling +:END: + +#+BEGIN_SRC java +ViewPanel viewPanel = ...; + +// Side-by-side stereo on: +viewPanel.setStereoModeEnabled(true); + +// Optional: match the viewer (default 6.5 world units): +viewPanel.setStereoIPD(6.5); +#+END_SRC + +In the demos, *SHIFT+F11* toggles stereo and fullscreen together (XR +glasses want both); plain *F11* remains fullscreen-only. With stereo +active, *+* and *-* adjust the IPD in 0.5-unit steps (clamped at 0.5) +so the viewer can tune comfort at runtime. + +#+CAPTION: The same scene rendered as a normal mono frame (for comparison with the pair above). Notice there is no horizontal offset to read depth from — the picture is flat. +[[file:mono-comparison.png]] + +* Performance and limitations +:PROPERTIES: +:CUSTOM_ID: limitations +:END: + +- Stereo *doubles the per-frame transform, sort and paint work* — two + full passes instead of one. The pipeline overlaps them the same way + it overlaps consecutive mono frames, so throughput drops less than + 2x on a multi-core machine, but expect a real cost. +- Each eye gets *half the horizontal resolution* of the panel. On a + 1920x1080 fullscreen window each eye sees 960x1080 — pixels are + shared, not duplicated. +- IPD is in *world units*: 6.5 only means "6.5 cm" if the scene is + modeled at 1 unit = 1 cm. In a scene with a different scale, divide + or multiply accordingly — or just tune with =+=/=-= until the depth + feels right. +- The eye offset is applied along the *world X axis*, not the camera's + right vector: it is exactly correct when the camera faces along Z + (yaw = 0) and degrades as you turn — at yaw = 90° the eyes would be + offset front-to-back instead of side-to-side. For a fixed-viewing- + direction demo this is fine; a fully rotational stereo camera would + need to apply the IPD along the rotated right vector. +- Side-by-side is a *display format*, not a headset driver: the engine + produces the image; an XR viewer, 3D TV or video player is + responsible for delivering the halves to the eyes. +- Global illumination is unaffected: lightmaps live on the surfaces, + so both eyes sample the same converged lighting for free. + +* Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Role in stereo rendering | +|----------------------+-----------------------------------------------------------------| +| =ViewPanel= | owns stereoModeEnabled/stereoIPD; runs the two passes per frame | +| =StereoEye= | NONE / LEFT / RIGHT tag carried by each pass context | +| =RenderingContext= | per-eye viewport fields: stereoViewportWidth/OffsetX, renderMin/MaxX | +| =Vertex= | per-eye projection: scale from eye width + viewport X offset | +| =ShapeCollection= | rebuilds the frustum per pass from the eye's viewport width | +| =InputManager= | SHIFT+F11 stereo toggle, +/- live IPD adjustment | + +[[file:../index.html#outline-container-understanding-3d-engine][Back to main documentation]] diff --git a/Documentation/Stereoscopic rendering/mono-comparison.png b/Documentation/Stereoscopic rendering/mono-comparison.png new file mode 100644 index 0000000..3b70ee0 Binary files /dev/null and b/Documentation/Stereoscopic rendering/mono-comparison.png differ diff --git a/Documentation/Stereoscopic rendering/stereo-side-by-side.png b/Documentation/Stereoscopic rendering/stereo-side-by-side.png new file mode 100644 index 0000000..12fa4fc Binary files /dev/null and b/Documentation/Stereoscopic rendering/stereo-side-by-side.png differ diff --git a/Documentation/Winding order.svg b/Documentation/Winding order.svg new file mode 100644 index 0000000..d82048e --- /dev/null +++ b/Documentation/Winding order.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + CCW + + + + V₁ + V₂ + V₃ + FRONT FACE ✓ + + + + + CW + + + BACK FACE ✗ + (culled — not drawn) + diff --git a/Documentation/export-docs.sh b/Documentation/export-docs.sh new file mode 100755 index 0000000..7ef274d --- /dev/null +++ b/Documentation/export-docs.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# export-docs.sh — export all org-mode documentation pages to HTML. +# +# Exports every Documentation/**/index.org (and Documentation/index.org) +# with the darksun theme, using the user's Emacs configuration. Run from +# anywhere: +# +# Documentation/export-docs.sh # export all pages +# Documentation/export-docs.sh --check # export, then render every page with +# # headless Chrome to /tmp/doc-check-*.png +# # for visual inspection +# +# Requires: emacs (with ~/.emacs providing the org HTML setup), +# google-chrome (only for --check). + +set -euo pipefail +DOC_DIR="$(cd "$(dirname "$0")" && pwd)" + +mapfile -t PAGES < <(find "$DOC_DIR" -name index.org | sort) + +echo "Exporting ${#PAGES[@]} pages..." +for page in "${PAGES[@]}"; do + rel="${page#"$DOC_DIR"/}" + if emacs --batch -l ~/.emacs --visit="$page" \ + --funcall=org-html-export-to-html --kill 2>&1 \ + | grep -qi "aborted\|unable to resolve link"; then + echo "FAIL $rel" + exit 1 + fi + echo " ok $rel" +done + +if [[ "${1:-}" == "--check" ]]; then + echo "Rendering pages for visual check..." + for page in "${PAGES[@]}"; do + rel="${page#"$DOC_DIR"/}" + html="${page%.org}.html" + out="/tmp/doc-check-$(echo "$rel" | tr '/ ' '__').png" + google-chrome --headless --disable-gpu --hide-scrollbars \ + --virtual-time-budget=8000 --window-size=1100,2000 \ + --screenshot="$out" "file://$html" 2>/dev/null + echo " shot $out" + done +fi + +echo "Done." diff --git a/Documentation/index.org b/Documentation/index.org new file mode 100644 index 0000000..4068d09 --- /dev/null +++ b/Documentation/index.org @@ -0,0 +1,1224 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Aukio 3D - Realtime 3D engine +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \usepackage[none]{hyphenat} + +#+OPTIONS: H:20 num:20 +#+OPTIONS: author:nil + +#+HTML_HEAD: + +* Introduction +:PROPERTIES: +:CUSTOM_ID: overview +:ID: a31a1f4d-5368-4fd9-aaf8-fa6d81851187 +:END: + +[[file:Example.png]] + +*Aukio 3D* is a realtime 3D rendering engine written in pure Java. It +runs entirely on the CPU — no GPU required, no OpenGL, no Vulkan, no +native libraries. Just Java. + +The motivation is simple: GPU-based 3D is a minefield of accidental +complexity. Drivers are buggy or missing entirely. Features you need +aren't supported on your target hardware. You run out of GPU RAM. You +wrestle with platform-specific interop layers, shader compilation +quirks, and dependency hell. Every GPU API comes with its own +ecosystem of pain — version mismatches, incomplete implementations, +vendor-specific workarounds. I want a library that "just works". + +*Aukio 3D* takes a different path. By rendering everything in software +on the CPU, the entire GPU problem space simply disappears. You add a +Maven dependency, write some Java, and you have a 3D scene. It runs +wherever Java runs. + +This approach is quite practical for many use-cases. Modern systems +ship with many CPU cores, and those with unified memory architectures +offer high bandwidth between CPU and RAM. Software rendering that once +seemed wasteful is now a reasonable choice where you need good-enough +performance without the overhead of a full GPU pipeline. Java's JIT +compiler helps too, optimizing hot rendering paths at runtime. + +Beyond convenience, CPU rendering gives you complete control. You own +every pixel. You can freely experiment with custom rendering +algorithms, optimization strategies, and visual effects without being +constrained by what a GPU API exposes. Instead of brute-forcing +everything through a fixed GPU pipeline, you can implement clever, +application-specific optimizations. + +*Aukio 3D* is part of the larger [[https://www3.svjatoslav.eu/projects/aukio/][Aukio project]], with the long-term goal +of providing a platform for 3D user interfaces and interactive data +visualization. It can also be used as a standalone 3D engine in any +Java project. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/][demos]] for examples of what it can do today. + +*Major features:* +** Global Illumination +:PROPERTIES: +:CUSTOM_ID: global-illumination +:END: + +#+attr_html: :width 600px +#+attr_latex: :width 600px +[[file:Global illumination/Global illumination.png]] + +On top of flat shading, the engine computes progressive *global +illumination* on background CPU threads: real shadows, smooth light +falloff inside polygons (per-texel lightmaps), and indirect bounce +light — while the render loop itself never traces a single ray. + +Read more about [[file:Global illumination/][global illumination]]. + +** Side-by-side stereoscopic rendering support +:PROPERTIES: +:CUSTOM_ID: stereoscopic +:END: + +#+attr_html: :width 600px +#+attr_latex: :width 600px +[[file:Stereoscopic rendering/stereo-side-by-side.png]] + +The engine can render every frame twice — once per eye — into the left +and right halves of the same image, for XR glasses and 3D displays. +The cameras stay parallel and are offset by a configurable IPD +(inter-pupillary distance). Two render passes share one triple-buffered +pipeline, each clipped to its half of the frame buffer; per-eye +projection, frustum culling and mouse picking adapt automatically. + +See [[file:Stereoscopic%20rendering/][Stereoscopic rendering]] for the +geometry, pipeline and tuning. + +** Constructive Solid Geometry + +#+attr_html: :width 600px +#+attr_latex: :width 600px +[[file:CSG/CSG demo.png]] + +*Aukio 3D* allows performing boolean operations against geometry shapes. +So one can subtract, unionize or intersect shapes. + +To understand CSG boolean operations, read more about [[file:CSG/][Constructive +Solid Geometry]]. + +** SDF textures for sharp text +:PROPERTIES: +:CUSTOM_ID: sdf-text +:END: + +[[file:SDF textures/sdf-angled.png]] + +Text and vector-art surfaces do not store coverage; they store a +*signed distance field* — per texel, the distance to the nearest glyph +edge. The rasterizer re-derives coverage per screen pixel from that +smooth field, so text stays sharp at any zoom and fades to clean gray +under minification, all without a mipmap chain. + +See [[file:SDF%20textures/][SDF textures]] for the glyph pipeline, the +render path, analytic minification and tuning knobs. + +* How take engine into use +:PROPERTIES: +:CUSTOM_ID: taking-engine-into-use +:END: + +Add the *Aukio 3D* dependency to your Maven project: + +#+BEGIN_SRC xml + + + eu.svjatoslav + aukio-3d + 1.4 + + +#+END_SRC + +Also add the repository (the library is not on Maven Central): + +#+BEGIN_SRC xml + + + svjatoslav.eu + Svjatoslav repository + https://www3.svjatoslav.eu/maven/ + + +#+END_SRC + +- Library requires Java 21 or newer. + +- Study the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/][demo applications]] for practical examples. Start with the + [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#minimal-example][minimal example]] to see the basic boilerplate needed to render a 3D + scene. + +- Study [[#understanding-3d-engine][how Aukio 3D engine works]]. +- Read online [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/][JavaDoc]]. +- See [[https://www3.svjatoslav.eu/projects/aukio-3d/graphs/][*Aukio 3D* class diagrams]]. (Diagrams were generated by using + [[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility) + +* Essential theory +:PROPERTIES: +:CUSTOM_ID: understanding-3d-engine +:ID: 4b6c1355-0afe-40c6-86c3-14bf8a11a8d0 +:END: +** Coordinate System (X, Y, Z) +:PROPERTIES: +:CUSTOM_ID: coordinate-system +:END: + +#+INCLUDE: "Coordinate system.svg" export html + +*Aukio 3D* uses a **left-handed coordinate system with X pointing right +and Y pointing down**, matching standard 2D screen coordinates. This +coordinate system should feel intuitive for people with preexisting 2D +graphics background. + +| Axis | Direction | Meaning | +|------+------------------------------------+-------------------------------------------| +| X | Horizontal, positive = RIGHT | Objects with larger X appear to the right | +| Y | Vertical, positive = DOWN | Lower Y = higher visually (up) | +| Z | Depth, positive = away from viewer | Negative Z = closer to camera | + +*Practical Examples* + +- A point at =(0, 0, 0)= is at the origin. +- A point at =(100, 50, 200)= is: 100 units right, 50 units down + visually, 200 units away from the camera. +- To place object A "above" object B, give A a **smaller Y value** + than B. + +Coordinates in this system are stored using the +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point3D.html][Point3D]] class — a mutable container with public =x=, =y=, =z= fields +supporting vector operations like distance, rotation, and translation. +Vertices (see [[#vertex][below]]) are positioned within this coordinate system. + +The [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#coordinate-system][aukio-3d-demos]] project includes an interactive +coordinate system reference showing X, Y, Z axes as colored arrows +with a grid plane for spatial context. + +** Point3D and Vertex +:PROPERTIES: +:CUSTOM_ID: vertex +:END: + +#+INCLUDE: "Point3D vertex.svg" export html + +Every 3D object is built from *vertices* — corner points that define +the shape's geometry. A triangle has 3 vertices, a cube has 8, and +complex meshes have thousands. The engine uses two related classes to +represent points in 3D space, each serving a different purpose. + + + +*** Point3D — Raw Coordinates + +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point3D.html][Point3D]] is the fundamental coordinate type throughout the engine. It +stores a position or vector with three public fields: =x=, =y=, =z=. +The class provides vector math operations: distance calculation, +rotation, translation, scaling, dot/cross products, and interpolation. Methods follow a fluent API convention where mutating +operations (like =add=, =multiply=) return =this= for chaining, while +non-mutating variants (like =withAdded=, =withMultiplied=) return new +instances. + +Use =Point3D= for: +- Storing positions, vectors, or any raw 3D coordinate +- Distance and angle calculations between points +- Vector math (dot product, cross product, normalization) +- Rotating or translating positions before shape construction + +#+BEGIN_SRC java +Point3D p1 = new Point3D(100, 50, 200); +Point3D p2 = new Point3D(0, 0, 100); +double distance = p1.getDistanceTo(p2); // Euclidean distance +Point3D direction = p1.withSubtracted(p2).unit(); // New point: unit vector from p2 to p1 +p1.rotate(new Point3D(0,0,0), Math.PI/4, 0); // Rotate p1 in place, 45° in XZ plane +#+END_SRC + +*** Vertex — Rendering-Ready Coordinates + +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] wraps a =Point3D= and adds the coordinate spaces needed during +rendering. As a shape transforms through the render pipeline, each +vertex tracks its position in multiple spaces: + +| Field | Purpose | +|------------------------+--------------------------------------------------------| +| =coordinate= | Original position in local/model space | +| =transformedCoordinate(ctx)= | Position relative to camera (after transform stack) | +| =onScreenCoordinate(ctx)= | 2D screen pixels (after perspective projection) | +| =textureCoordinate= | Optional UV coords in pixel units (not normalized) | +| =normal= | Optional normal vector for CSG polygon splitting | + +=transformedCoordinate= and =onScreenCoordinate= are accessor methods, +not plain fields: each vertex carries three slots for each, one per +pipeline projection slot, and the accessor picks the slot of the +context's current render pass. This is what lets the triple-buffered +pipeline transform the next frame while previous frames are still +being painted (see [[file:Rendering loop/index.org::#software-pipeline][software pipeline]]). + +During rendering, the vertex is transformed through all spaces: first +applying the +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/TransformStack.html][TransformStack]] to get the camera-relative coordinate, then +projecting to 2D. Results are cached per frame per slot to avoid +recomputing for vertices shared across multiple shapes. + +Use =Vertex= when: +- Constructing triangles, polygons, or textured shapes +- Your geometry needs texture UV coordinates +- You're performing CSG boolean operations (requires =normal=) + +#+BEGIN_SRC java +// Create a textured triangle (texture coordinates use pixel units) +// For a 256x256 texture: (0,0)=top-left, (256,256)=bottom-right +Vertex v1 = new Vertex(new Point3D(0, 0, 100), new Point2D(0, 0)); +Vertex v2 = new Vertex(new Point3D(100, 0, 100), new Point2D(256, 0)); +Vertex v3 = new Vertex(new Point3D(50, 100, 100), new Point2D(128, 256)); +TexturedTriangle triangle = new TexturedTriangle(v1, v2, v3, texture); +#+END_SRC + +*** When to Use Each + +| Use Point3D | Use Vertex | +|--------------------------------------+-----------------------------------------------| +| Positioning shapes, cameras, lights | Building triangles and polygons | +| Vector math (distances, directions) | Texture-mapped geometry | +| Rotating or translating positions | CSG operations | +| Temporary calculations | Shapes that render through transform pipeline | + +For simple shapes without textures, you can pass raw =Point3D= +coordinates directly to constructors — the shape will internally wrap +them in =Vertex= objects. The [[#coordinate-system][coordinate system]] above defines the +meaning of all =x=, =y=, =z= values in both classes. + +** Edge +:PROPERTIES: +:CUSTOM_ID: edge +:END: + +#+INCLUDE: "Edge.svg" export html + +An *edge* is a straight line segment connecting two [[#vertex][vertices]]. Edges +form the wireframe skeleton of a 3D model — the structural framework +visible when surfaces are not rendered. A triangle has 3 edges, a cube +has 12 edges, and complex meshes have thousands. + +In *Aukio 3D*, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class implements edges as renderable shapes. Each +Line connects two [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] endpoints and stores two properties: a +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#width][width]] in world units (adjusted for perspective during rendering) and a +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#color][color]] with alpha transparency. The rendering algorithm switches +between two modes based on the projected screen width: thin lines below +the threshold are drawn as single pixels with alpha-adjusted coloring, +while thicker lines are rendered as filled rectangles with perspective-correct +edge fading using four [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineInterpolator.html][LineInterpolator]] scanline boundaries. + +Wireframe shapes are composite objects built from multiple Line instances. +For example, [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.html][WireframeBox]] creates 12 Line objects — four edges parallel to +each axis — using a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineAppearance.html][LineAppearance]] factory to ensure consistent styling across +all edges. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.html][WireframeCube]] convenience subclass provides a center-point +constructor. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual comparison of +wireframe (edges only) versus solid polygon (surfaces with lighting) +rendering modes. + +** Face (Triangle) +:PROPERTIES: +:CUSTOM_ID: face-triangle +:END: + +#+INCLUDE: "Face triangle.svg" export html + +A *face* is a flat surface enclosed by edges — the visible skin of a 3D +object. While faces can theoretically have any number of sides, 3D +engines standardize on *triangles* because three points always define a +flat plane. A quad (4 vertices) or pentagon (5 vertices) might be +non-planar depending on vertex positions, causing rendering artifacts. +Triangles avoid this problem entirely. + +*** SolidPolygon — Solid-Color Faces + +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] is the primary face type, supporting any number of vertices +(3 or more). Triangles render directly via scanline rasterization. +N-vertex polygons (quads, pentagons, etc.) are triangulated using fan +decomposition — a quad becomes 2 triangles, a pentagon 3 — but only +when the polygon lives inside an +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] +(the scene graph root is one): the composite triangulates while +building its render list. A standalone SolidPolygon with more than 3 +vertices cannot be painted directly and throws IllegalStateException. + +Each SolidPolygon stores a single fill color with optional alpha +transparency. When shading is enabled, the lighting manager computes +the polygon's illumination once during the transform phase, then +applies the shaded color during painting. Backface culling (see +[[#winding-order-backface-culling][Winding Order & Backface Culling]]) can be enabled per-polygon, or +applied recursively to an entire composite shape via +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] — this propagates the +setting to all SolidPolygon and TexturedTriangle sub-shapes, including +nested composites. + +#+BEGIN_SRC java +// Create a red triangle +SolidPolygon triangle = SolidPolygon.triangle( + new Point3D(0, 0, 100), + new Point3D(50, 0, 100), + new Point3D(25, 50, 100), + Color.RED +); + +// Create a blue quad (internally triangulated) +SolidPolygon quad = SolidPolygon.quad( + new Point3D(-50, -50, 100), + new Point3D(50, -50, 100), + new Point3D(50, 50, 100), + new Point3D(-50, 50, 100), + Color.BLUE +); + +// Enable lighting and culling for a closed mesh +quad.setShadingEnabled(true); +quad.setBackfaceCulling(true); + +// Add to the scene — the root composite triangulates the quad +viewPanel.getRootShapeCollection().addShape(quad); +#+END_SRC + +*** TexturedTriangle — UV-Mapped Faces + +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] renders faces with image textures mapped via UV +coordinates. Each of the three [[#vertex][vertices]] stores a =textureCoordinate= +(a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point2D.html][Point2D]] with U and V values in *pixel units* matching the texture +dimensions). For a 256×256 texture, coordinates range from (0,0) at the +top-left corner to (256,256) at the bottom-right. During rasterization, the +engine interpolates these UV coordinates across the triangle's surface, +sampling the texture at each pixel. When mipmaps are used, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html#multiplicationFactor][multiplicationFactor]] +scales coordinates to match the selected mipmap resolution. + +The texture system supports mipmaps — pre-scaled versions of the texture +selected based on the triangle's screen size to reduce aliasing artifacts +on distant surfaces. Texture coordinates are mapped with +perspective-correct interpolation inside the scanline rasterizer, so +large triangles at steep angles render without distortion — see the +[[file:Perspective correct textures/][perspective-correct textures]] page. + +#+BEGIN_SRC java +// Create a 256x256 texture +Texture texture = new Texture(256, 256, 2); // width, height, maxUpscale + +// Create a textured triangle with UV coordinates in pixel units +Vertex v1 = new Vertex(new Point3D(0, 0, 100), new Point2D(0, 0)); // top-left +Vertex v2 = new Vertex(new Point3D(100, 0, 100), new Point2D(256, 0)); // top-right +Vertex v3 = new Vertex(new Point3D(50, 100, 100), new Point2D(128, 256)); // bottom-center + +TexturedTriangle triangle = new TexturedTriangle(v1, v2, v3, texture); +triangle.setBackfaceCulling(true); +#+END_SRC + +Both SolidPolygon and TexturedTriangle extend +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]], which handles vertex transformation and depth +sorting. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual comparison of solid +versus textured polygon rendering. + +** Normal Vector +:PROPERTIES: +:CUSTOM_ID: normal-vector +:END: + +#+INCLUDE: "Normal vector.svg" export html + +A *normal* is a vector perpendicular to a surface. It tells the +renderer which direction a face is pointing. Normals are critical for +*lighting* — the angle between the light direction and the normal +determines how bright a surface appears. + +**Use cases:** + +| Use case | API | Computation | Location | +|----------------------+----------------------------------------------+----------------------------+-------------------| +| BSP/CSG operations | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#getPlane()][SolidPolygon.getPlane()]] → [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#normal][Plane.normal]] | Lazy-cached once | =Plane= | +| Per-frame shading | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]] → =cachedNormal= field | Recomputed every frame | =SolidPolygon= | +| Lighting calculation | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html#computeLighting()][LightingManager.computeLighting()]] | Uses normal via =dot(L,N)= | =LightingManager= | + +**Implementation notes:** +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]]: shared zero-allocation helper for computing normals from three points +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Plane]]: stores normals in Hesse normal form (normal + distance) for BSP spatial partitioning +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html#normal][Vertex.normal]]: optional field for CSG polygon splitting (not used for rendering) + +** Mesh +:PROPERTIES: +:CUSTOM_ID: mesh +:END: + +#+INCLUDE: "Mesh.svg" export html + +A *mesh* is a collection of vertices, edges, and faces that together +define the shape of a 3D object. Even curved surfaces like spheres are +approximated by many small triangles — more triangles means a smoother +appearance. A cube has 8 vertices forming 12 triangular faces, while a +smooth sphere requires hundreds or thousands of triangles depending on +the desired quality. + +In *Aukio 3D*, meshes are built through composition rather than +monolithic vertex/index buffers. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]] class is +the foundation for primitive shapes — each instance stores its own +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html#vertices][List<Vertex>]] directly. This includes [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] (N-vertex +convex polygons, not limited to triangles), [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] +(UV-mapped triangles), and [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] (wireframe edges). The +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] class groups multiple shapes into a single +object with its own position, rotation, and transform — useful for +complex models that move or rotate together. + +Complex meshes are constructed procedurally by adding primitive shapes +during initialization. For example, [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.html][SolidPolygonSphere]] generates +triangles using a latitude-longitude grid: with 16 segments, it +creates 960 SolidPolygon triangles by looping through +rings and sectors, calling [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#addShape(eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape)][addShape()]] for each. The generic +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.html][SolidPolygonMesh]] accepts any list of triangles, allowing custom +geometry from procedural generation or external sources. + +During rendering, several automatic optimizations occur. N-vertex +polygons (quads, pentagons, etc.) are triangulated using fan +triangulation inside the composite's render-list builder, converting +an N-vertex polygon into N-2 triangles. Textured triangles render with +perspective-correct texture mapping (see [[file:Perspective correct textures/][perspective-correct textures]]). +Composites perform view frustum culling to skip rendering when entirely +off-screen. Sub-shapes can be organized into named groups via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/SubShape.html][SubShape]] +wrappers, allowing [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#showGroup(java.lang.String)][showGroup()]] and [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#hideGroup(java.lang.String)][hideGroup()]] to toggle visibility of +entire sections. Composite shapes also support CSG boolean operations +— see the [[file:CSG/][Constructive Solid Geometry]] documentation for union, +subtract, and intersect operations. + +The [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] showcases all primitive shapes available in +*Aukio 3D*, rendered in both wireframe mode (edges only via +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.html][WireframeBox]] and similar) and solid polygon mode (filled surfaces with +dynamic lighting). + +** Working with Colors +:PROPERTIES: +:CUSTOM_ID: working-with-colors +:ID: f2c9642a-a093-444f-8992-76c97ff28c16 +:END: + +Aukio 3D uses its own [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html][Color class]] instead of [[https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/Color.html][java.awt.Color]]. This +custom implementation is designed specifically for the engine's +software rasterizer, where avoiding object allocation during rendering +is critical for performance. When rendering thousands of polygons per +frame, creating new Color instances for each one would generate +excessive garbage and trigger frequent garbage collection +pauses. Instead, the engine's Color class uses mutable fields that can +be reused across frames. + +The class stores RGBA components as public integer fields in the range +0–255. This format matches the engine's pixel buffer layout and avoids +costly float-to-int conversions during rasterization. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#r][r]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#g][g]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#b][b]], and +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#a][a]] fields are accessible directly, allowing lighting calculations and +alpha blending to modify colors in-place without allocating new +objects. For example, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] class maintains a reusable +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][shadedColor]] field that gets updated during each frame's lighting +calculation instead of creating a new Color instance per polygon. + +Color provides several constructors for different input formats. The +most common approach is using hex strings via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#hex(java.lang.String)][Color.hex(String)]] or the +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(java.lang.String)][String constructor]], which support formats like ="F80"= (3-digit RGB), +="FF8800"= (6-digit RGB), ="F808"= (4-digit RGBA), and ="FF8800CC"= +(8-digit RGBA). You can also create colors from integer RGBA +components (0–255) using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(int,int,int,int)][new Color(r, g, b, a)]], from floating-point +components (0.0–1.0) via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(double,double,double,double)][new Color(double r, double g, double b, +double a)]], or from a packed RGB integer like =0xFF8800= using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(int)][new +Color(int rgb)]]. The class also provides predefined constants for +common colors: [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#RED][Color.RED]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#GREEN][Color.GREEN]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#BLUE][Color.BLUE]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#YELLOW][Color.YELLOW]], +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#CYAN][Color.CYAN]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#MAGENTA][Color.MAGENTA]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#WHITE][Color.WHITE]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#BLACK][Color.BLACK]], and +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#TRANSPARENT][Color.TRANSPARENT]]. + +The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#set(int,int,int,int)][set(int r, int g, int b, int a)]] method modifies a Color in-place +and returns =this= for method chaining, which is essential for +performance during rendering. For example, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] +calculates lighting contributions from all light sources and stores +the final shaded color directly into a reusable Color instance via +=set()=, avoiding any allocation. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#toAwtColor()][toAwtColor()]] method converts a +Aukio 3D Color to a java.awt.Color when needed for Java2D graphics +operations, caching the result to avoid repeated conversion. The +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#toInt()][toInt()]] method packs the color into an ARGB integer suitable for the +engine's pixel buffer, used during rasterization to write pixels +directly. + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import static eu.svjatoslav.aukio.e3d.renderer.raster.Color.hex; + +// Using predefined color constants +Color red = Color.RED; +Color transparent = Color.TRANSPARENT; + +// Create from hex string (recommended for clarity) +Color orange = hex("FF8800"); // RGB, fully opaque +Color semiTransparent = hex("FF880080"); // RGBA, 50% transparent + +// Create from integer components (0-255) +Color custom = new Color(255, 128, 64, 200); + +// Create from packed RGB integer +Color packed = new Color(0xFF8800); + +// Modify existing color in-place (no allocation) +Color reusable = new Color(); +reusable.set(100, 200, 50, 255); + +// Convert to AWT color for Java2D operations +java.awt.Color awtColor = custom.toAwtColor(); + +// Use in lighting calculations (LightingManager modifies in-place) +// See the Shading & Lighting documentation for details +#+END_SRC + +The alpha component controls transparency during rendering. A value of +0 makes the color fully transparent, while 255 makes it fully +opaque. The rasterizer implements alpha blending during the paint +phase: when drawing a semi-transparent pixel, the engine blends the +source color with the existing background pixel proportionally based +on the alpha value. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html#drawPixel(int,int\[\],int)][TextureBitmap.drawPixel()]] method handles this +blending, multiplying source colors by alpha and background colors by +=(255 - alpha)=, then combining them. You can test whether a color is +fully transparent using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#isTransparent()][isTransparent()]], which returns true when alpha +equals zero. + +For lighting calculations, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]] class uses Color to +represent the color and intensity of emitted light. Multiple light +sources contribute to the final shaded color of each polygon, as +described in the [[file:Shading/index.org::#shading-lighting][Shading & Lighting]] documentation. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html#setAmbientLight(eu.svjatoslav.aukio.e3d.renderer.raster.Color)][ambient light]] +provides base illumination that affects all surfaces equally, +regardless of orientation. Colors are also used for wireframe +rendering via the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class, where the color field determines the +line's appearance. + +** Shading & Lighting + +#+attr_html: :width 600px +#+attr_latex: :width 600px +[[file:Shading/Shaded%20sphere.png]] + + +*Aukio 3D* implements *flat shading* — one normal per polygon, +computed from the first three vertices. Each polygon receives a single +color based on its orientation relative to light sources. + +To understand lighting and shading, read more about [[file:Shading/][shading & lighting]]. + +* 3D engine internals +** Main render loop + +The rendering loop is the heart of the engine, continuously generating +frames at a target rate (typically 60 FPS). Each frame transforms 3D +shapes through a multi-stage pipeline before displaying them on screen. + +#+INCLUDE: "Rendering loop/Render pipeline.svg" export html + +The render loop runs on a dedicated background daemon thread managed by +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html][ViewPanel]], which can optionally sleep between frames to maintain a +target FPS or run unlimited for benchmarking. + +For a detailed walkthrough of each phase with diagrams and code +examples, see the dedicated page: [[file:Rendering loop/][Rendering loop]]. + +** Near-Plane Clipping +:PROPERTIES: +:CUSTOM_ID: near-plane-clipping +:END: + +Individual polygons that straddle the camera's near plane are not +dropped wholesale: the vertex loop is clipped against the plane, new +intersection vertices are generated with 3D-interpolated UVs and +normals, and the clipped polygon — a triangle can become a quad, +painted as a triangle fan — renders normally. Only polygons entirely +behind the near plane are culled. This keeps floor and wall tiles +visible when the camera brushes against them. + +#+INCLUDE: "Near plane clip/Near plane straddle.svg" export html + +Read more about [[file:Near plane clip/][near-plane clipping]]. + +** Depth buffer +:PROPERTIES: +:CUSTOM_ID: depth-buffer +:END: + +Visibility is resolved per pixel by a depth buffer: every triangle +interpolates =1/z= across its spans and wins a pixel only where it is +nearer than the surface already there. Opaque geometry — textured +triangles, solid polygons — paints front-to-back with depth writes; +translucent geometry paints back-to-front with depth tests but no +writes, so it never occludes. Lines and billboards stay painter-ordered +overlays by design. + +See [[file:Depth%20buffer/][Depth buffer]] for the full treatment. + +** Frustum & View Frustum Culling + +*Aukio 3D* implements view frustum culling. + +#+INCLUDE: "Frustum culling/Frustum diagram.svg" export html + +To understand frustum culling and object-level visibility +optimization, read more about [[file:Frustum culling/][frustum & view frustum culling.]] + +** Winding Order & Backface Culling +:PROPERTIES: +:CUSTOM_ID: winding-order-backface-culling +:END: + +#+INCLUDE: "Winding order.svg" export html + +The order in which a triangle's vertices are listed determines its +*winding order*. In *Aukio 3D*, screen coordinates have Y-axis pointing +*down*, which inverts the apparent winding direction compared to +standard mathematical convention (Y-up). *Counter-clockwise (CCW)* in +screen space means front-facing. *Backface culling* skips rendering +triangles that face away from the camera — a major performance +optimization. + +- CCW winding (in screen space) → front face (visible) +- CW winding (in screen space) → back face (culled) +- When viewing a polygon from outside: define vertices in *counter-clockwise* order as seen from the camera +- Saves ~50% of triangle rendering +- Implementation uses signed area: =signedArea < 0= means front-facing + (in Y-down screen coordinates, negative signed area corresponds to + visually CCW winding) + +In *Aukio 3D*, backface culling is *optional* and disabled by default. Enable it per-shape: +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#setBackfaceCulling(boolean)][SolidPolygon.setBackfaceCulling(true)]] +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html#setBackfaceCulling(boolean)][TexturedTriangle.setBackfaceCulling(true)]] +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] (applies to all + sub-shapes) + +See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#winding-order][Winding Order demo]] for an interactive visualization. + +** Perspective correct textures + +#+attr_html: :class responsive-img +#+attr_latex: :width 1000px +[[file:Perspective correct textures/Affine distortion.png]] + +*Aukio 3D* tries to do perspective-correct texture rendering. Read more +about [[file:Perspective correct textures/][perspective-correct texture implementation]]. + +* Developer tools +:PROPERTIES: +:CUSTOM_ID: developer-tools +:ID: 8c5e2a1f-9d3b-4f6a-b8e7-1c4d5f7a9b2e +:END: + +Press *F12* anywhere in the application to open the Developer Tools +panel: + +#+attr_html: :class responsive-img +#+attr_latex: :width 1000px +[[file:Developer tools/Developer tools.png]] + +This debugging interface helps you understand what the engine is doing +internally and diagnose rendering issues. Pressing F12 again closes +the panel. + +** Diagnostic toggles +:PROPERTIES: +:CUSTOM_ID: diagnostic-toggles +:END: + +*** Show polygon borders +:PROPERTIES: +:CUSTOM_ID: show-polygon-borders +:END: + +When enabled, each [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] draws yellow outlines around its +three edges after rendering its texture content. This overlays the +triangle mesh onto the final image: + +#+attr_html: :class responsive-img +[[file:Developer tools/Render polygon borders.png]] + +Use this visualization when investigating: + +- Mesh structure: see the actual triangles as the rasterizer receives + them +- Geometry bugs: spot T-junction gaps and overlapping geometry +- Texture distortion: compare triangle shapes against visible warping + +*** Render alternate segments (overdraw debug) +:PROPERTIES: +:CUSTOM_ID: render-alternate-segments +:END: + +Renders only even-numbered paint tiles while leaving odd-numbered ones +black. (The screen is divided into a grid of rectangular tiles for +parallel rendering — see [[file:Rendering loop/index.org::#phase-4-clear-paint-tiles][the rendering loop documentation]]. +"Segments" is the older name for tiles.) + +#+attr_html: :class responsive-img +[[file:Developer tools/Render alternative segments.png]] + +This toggle helps detect overdraw: threads writing outside their +allocated tile. If you see rendering artifacts in the black tiles, a +paint task is writing pixels outside its assigned area — a clear sign +of a bug. + +*** Show segment boundaries +:PROPERTIES: +:CUSTOM_ID: show-segment-boundaries +:END: + +Draws red lines along the paint tile boundaries, making it easy to see +exactly where each tile's rendered area begins and ends. In stereo +mode each eye's viewport gets its own grid: + +#+attr_html: :class responsive-img +[[file:Developer tools/Show segment boundaries.png]] + +Useful for: + +- Verifying the tile grid division +- Debugging tile-boundary rendering issues (e.g. clipped text or + missing slivers at tile edges) +- Understanding the parallel rendering architecture visually + +** Camera position +:PROPERTIES: +:CUSTOM_ID: camera-position +:END: + +Displays the current camera coordinates and orientation in real-time: + +| Parameter | Description | +|-----------+------------------------------------------| +| x, y, z | Camera position in 3D world space | +| yaw | Rotation around the Y axis (left/right) | +| pitch | Rotation around the X axis (up/down) | +| roll | Rotation around the Z axis (tilt) | + +The *Copy* button copies the full camera position string to the +clipboard in a format ready to paste into bug reports or configuration +files. + +Use this for: +- Reporting exact camera positions when filing bugs +- Saving interesting viewpoints for later reference +- Understanding camera movement during navigation +- Sharing specific views with other developers + +Example copied format: +#+BEGIN_EXAMPLE +500.00, -300.00, -800.00, 0.60, -0.50, -0.00 +#+END_EXAMPLE + +The six numbers map 1:1 onto +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Transform.html#set(double,double,double,double,double,double)][Transform.set(x, y, z, yaw, pitch, roll)]], +so a copied viewpoint can be restored at startup — this is how the +demo applications freeze a good camera position into code: + +#+BEGIN_SRC java +// Camera position captured via Developer Tools -> Copy +viewPanel.getCamera().getTransform().set( + 130.66, -65.49, -248.18, // x, y, z + -0.06, -0.36, -0.00); // yaw, pitch, roll +#+END_SRC + +** Frustum culling statistics +:PROPERTIES: +:CUSTOM_ID: frustum-culling-statistics +:END: + +Shows real-time statistics about composite shape frustum culling +efficiency (see the dedicated [[file:Frustum culling/][frustum culling]] page for how +culling itself works): + +| Statistic | Description | +|-----------+----------------------------------------------------------| +| Total | Number of composite shapes tested against the frustum | +| Culled | Number of composites rejected (outside view frustum) | +| Culled % | Percentage of composites that were culled (0-100%) | + +*How to interpret the numbers:* + +- *High cull % (60-90%)*: Excellent — most objects are being correctly culled +- *Medium cull % (20-60%)*: Moderate — some optimization benefit +- *Low cull % (0-20%)*: Limited benefit — either all objects are visible, or scene needs restructuring + +*Example:* +#+BEGIN_EXAMPLE +Total: 473 Culled: 425 (89.9%) +#+END_EXAMPLE + +This means 473 composite shapes were tested, 425 were outside the view +and skipped entirely, and only 48 composites (with all their children) +actually needed to be rendered. This is excellent culling efficiency. + +The statistics update every 200ms while the panel is open. Note that +the root composite is never frustum-tested (it's always rendered), so +the "Total" count excludes it. + +** Render threads +:PROPERTIES: +:CUSTOM_ID: render-threads +:END: + +Shows the number of active render threads versus available CPU cores. +The engine defaults to 75% of available threads (at most cores − 1, so +one thread always stays free for the rest of the system). The count is +changeable at runtime via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#setNumRenderThreads(int)][ViewPanel.setNumRenderThreads(int)]]; +the worker pool is recreated lazily on the next frame. + +** Frame rate +:PROPERTIES: +:CUSTOM_ID: frame-rate +:END: + +Shows the current target FPS and the measured production rate (frames +completed per second, averaged over a ~500 ms window). The measured +number counts produced frames regardless of how quickly the display +path presents them — see [[file:Rendering loop/index.org::#frame-rate-control][frame rate control]]. + +The *Unlock FPS* toggle switches to unlimited (benchmark) mode: the +engine renders continuously as fast as possible, even when the scene +is static. Toggling off restores the previously locked target rate. + +** Thread activity timeline +:PROPERTIES: +:CUSTOM_ID: thread-activity-timeline +:END: + +A per-thread occupancy view — the software-renderer equivalent of a +GPU frame profiler. Each thread gets a row (the render thread and +present thread on top, then one row per worker), time runs along the X +axis, and each colored block is one recorded work interval. Idle time +is black. + +#+attr_html: :class responsive-img +[[file:Developer tools/Thread timeline.png]] + +Press *Record* to start capturing. The colors encode both the task +kind and which frame the task belongs to — transform, paint, and +binning come in three frame-parity variants (f0/f1/f2), so you can see +up to three frames in flight simultaneously. Additional colors mark +render-thread orchestration, blocked time, blits, and the sort/drain +sub-phases. + +Navigation: mouse wheel scrolls, Ctrl+wheel zooms, and a scrollbar +moves along the captured range. + +The legend colors, exactly as the timeline paints them: + +| Color | Legend label | What it shows | +|-------+--------------+---------------| +| @@html:@@ =#2ECC40= | =transform f0= | Vertex transform chunk task, frame slot 0 | +| @@html:@@ =#B8D900= | =transform f1= | Vertex transform chunk task, frame slot 1 | +| @@html:@@ =#6B8E23= | =transform f2= | Vertex transform chunk task, frame slot 2 | +| @@html:@@ =#0074D9= | =paint f0= | Paint tile task (clear + rasterize one tile), frame slot 0 | +| @@html:@@ =#F012BE= | =paint f1= | Paint tile task, frame slot 1 | +| @@html:@@ =#B10DC9= | =paint f2= | Paint tile task, frame slot 2 | +| @@html:@@ =#39CCCC= | =bin f0= | Tile binning task (assign sorted shapes to tiles), frame slot 0 | +| @@html:@@ =#008B8B= | =bin f1= | Tile binning task, frame slot 1 | +| @@html:@@ =#007070= | =bin f2= | Tile binning task, frame slot 2 | +| @@html:@@ =#A0A0A0= | =render serial= | Render thread orchestration: tree walk and pass submission | +| @@html:@@ =#8B0000= | =blocked= | Render thread waiting for an older paint pass or the present gate | +| @@html:@@ =#FFFFFF= | =blit= | Present thread copying a finished frame to the screen | +| @@html:@@ =#FF851B= | =sort+bin= | A pass's async continuation as a whole: drain, sort, bin, submit paint | +| @@html:@@ =#8B4513= | =drain= | Continuation sub-phase: await and merge parallel transform chunks | +| @@html:@@ =#FFD700= | =sort= | Continuation sub-phase: depth sort of the pass's shapes | + +The f0/f1/f2 suffixes are the frame's projection slot (frame number +modulo 3) — the triple-buffering from the [[file:Rendering loop/index.org::#software-pipeline][software pipeline]]. +When the pipeline is healthy you see interleaved colors from two or +three frames on the worker rows at once: paint tasks of an older frame +overlapping transform and binning of the newer one. Wide =blocked= +spans on the render row, or worker rows with black gaps, mean the +pipeline is starved rather than busy. + +Recording is cheap but not free (~100 ns per task; a single volatile +read when disabled), so leave it off during benchmarking runs. The +captured intervals live in a fixed-size ring buffer — long recordings +keep only the most recent history. + +What to look for: + +- *Solidly packed worker rows* mean the pipeline is keeping all cores + busy — the design goal (see [[file:Rendering loop/index.org::#software-pipeline][software pipeline]]) +- *Long "blocked" spans on the render row* mean the render thread is + waiting for paint passes — workers are the bottleneck +- *Long "blit" spans on the present row* mean the display path + (X server) is the bottleneck; excess frames are being dropped from + the presentation mailbox + +** Live log viewer +:PROPERTIES: +:CUSTOM_ID: live-log-viewer +:END: + +The scrollable text area shows captured debug output in real-time: +- Green text on black background for readability +- Auto-scrolls to show latest entries +- Updates every 200ms while panel is open +- Captures logs even when panel is closed (replays when reopened) + +Use the *Clear Logs* button to reset the log buffer for fresh +diagnostic captures. + +** Headless & agentic tooling +:PROPERTIES: +:CUSTOM_ID: headless-agentic +:END: + +For windowless rendering, pixel assertions, golden-image regression +tests and scene dumps — built for automated verification and AI agents — +see [[#agentic-development][agentic development tools]]. + +* Agentic development +:PROPERTIES: +:CUSTOM_ID: agentic-development +:END: + +*Aukio 3D* provides good support for automated AI coding agents +(for example OpenCode, Hermes Agent, etc..). + +Thanks to facilities is =eu.svjatoslav.aukio.e3d.headless= package, an +AI agent can render any scene from any pose, assert what got painted, +compare against committed reference images, and dump the full scene +state for a bug report — all without a window, a display, or the +render thread. + +** One pipeline, two drivers +:PROPERTIES: +:CUSTOM_ID: one-pipeline +:END: + +The key design decision: the headless path drives *the very same +transform → sort → paint pipeline* the on-screen ViewPanel +uses. Nothing is reimplemented, so a passing headless test proves the +real render works, and a bug reproduced headlessly is the real bug. + +#+INCLUDE: "Agentic development/Headless lanes.svg" export html + +Making this possible required three small engine changes: + +- ~ShapeCollection.transformShapes(Camera, RenderingContext)~ — the + transform phase now accepts a camera directly; the ViewPanel variant + just forwards its camera. Headless code never touches Swing. +- ~RenderingContext.getImage()~ — hands out the backing BufferedImage + the rasterizer paints into. +- ~GlobalIllumination.isRunning()~ / ~isConverged()~ / + ~getWorkItemCount()~ — GI state became inspectable. + +** Snapshot: render without a window +:PROPERTIES: +:CUSTOM_ID: snapshot +:END: + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.headless.Snapshot; + +ShapeCollection scene = new ShapeCollection(); +scene.addShape(myShape); +LightingManager lighting = new LightingManager(); +lighting.setAmbientLight(Color.hex("181818")); + +// One call: build context, transform, sort, paint, return the image. +BufferedImage image = Snapshot.render(scene, lighting, + "290.31, -35.59, -2.10, -0.58, -0.15, 0.0", 640, 480); +Snapshot.save(image, "/tmp/snapshot.png"); +#+END_SRC + +The pose string is the *same "x, y, z, yaw, pitch, roll" format the +demos print* and users quote in bug reports — paste the pose, reproduce +the exact view. ~Snapshot.cameraFromPose()~ and ~Snapshot.poseString()~ +convert in both directions. + +For tests that need to detect *unpainted* pixels (holes), the +~renderInto()~ variant fills the background with a caller-chosen +sentinel color first, so "nothing was painted here" is unambiguous even +in a pitch-black scene: + +#+BEGIN_SRC java +RenderingContext ctx = new RenderingContext(640, 480, 1); +ctx.lightingManager = lighting; +Snapshot.renderInto(scene, camera, ctx, 0x00010203); // sentinel +#+END_SRC + +A real headless render — the House demo from a bug-report pose: + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:Agentic development/snapshot-example.png]] + +** PixelAssertions: did this region get painted? +:PROPERTIES: +:CUSTOM_ID: pixel-assertions +:END: + +The recurring debugging question — "did the floor actually render, or +did clipping eat it?" — becomes a library call: + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.headless.PixelAssertions; + +// Fraction of a relative rectangle still equal to the background: +double holes = PixelAssertions.unpaintedFraction(image, 0, + 0.15, 0.45, 0.85, 1.0); // lower-center band +if (holes > 0.05) + throw new AssertionError("floor has holes: " + holes); + +long red = PixelAssertions.countColor(image, 0xFF0000); // flat-color tests +String grid = PixelAssertions.dumpPixelGrid(image, 320, 240, 3, 8); // hex dump +#+END_SRC + +#+INCLUDE: "Agentic development/Pixel assertion.svg" export html + +** GoldenImage: compare against a reference +:PROPERTIES: +:CUSTOM_ID: golden-image +:END: + +A pixel counts as different when any RGB channel drifts more than a +per-channel tolerance; the comparison fails when the fraction of +differing pixels exceeds a threshold. Deterministic flat-shaded renders +can use tight tolerances (4, 0.005); noisier paths relax them. + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.headless.GoldenImage; + +GoldenImage.Result r = GoldenImage.compare(actual, + new File("goldens/house-flat.png"), 4, 0.005); +if (!r.passed) + GoldenImage.saveDiff(actual, goldenFile, "/tmp/diff.png"); // red = differs +#+END_SRC + +#+INCLUDE: "Agentic development/Golden workflow.svg" export html + +There is also a CLI for shell scripts — exit 0 = match, 1 = differ: + +#+BEGIN_SRC bash +java eu.svjatoslav.aukio.e3d.headless.GoldenImage actual.png golden.png 4 0.005 +#+END_SRC + +A real diff: the house rendered with the living-room lamp removed, +compared against the golden. The red region is exactly the room that +lost its light: + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:Agentic development/diff-example.png]] + +** SceneDump: the reproducible bug report +:PROPERTIES: +:CUSTOM_ID: scene-dump +:END: + +One call produces everything needed to reproduce what a frame shows: + +#+BEGIN_SRC java +System.out.println(SceneDump.dump(scene, lighting, camera, gi)); +#+END_SRC + +#+BEGIN_EXAMPLE +== SceneDump == +shapes: 5 top-level, 546 queued for rendering +lights: 4 (ambient #181818) + [0] pos=(-800.0, -240.0, 0.0) color=FFD890 intensity=6.0 + [1] pos=(0.0, -240.0, 0.0) color=D8E4FF intensity=5.0 + [2] pos=(800.0, -240.0, 0.0) color=FFB060 intensity=6.0 + [3] pos=(250.0, -60.0, -250.0) color=60FF90 intensity=2.0 +camera: 290.31, -35.59, -2.10, -0.58, -0.15, -0.00 +GI: running, 152034 work items, converged +#+END_EXAMPLE + +The camera line is a pose string — it feeds straight back into +~Snapshot.render()~. + +** HouseGoldens: ready-made regression tests +:PROPERTIES: +:CUSTOM_ID: house-goldens +:END: + +The aukio-3d-demos repo contains a working example of all of the above: +~eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens~ renders the +House demo at two poses (the default view and the near-plane straddle +bug pose), compares both against committed goldens, and independently +asserts the floor has no holes. + +#+BEGIN_SRC bash +cd aukio-3d-demos +mvn clean package +mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt +java -cp "target/classes:$(cat cp.txt)" \ + eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens # verify +java -cp "target/classes:$(cat cp.txt)" \ + eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens --update # regenerate goldens +#+END_SRC + +Exit code 0 = all pass, 1 = any mismatch (with a diff PNG in /tmp). +Demos that want the same treatment expose their scene construction: +~HouseDemo.buildHouse()~, ~addFurniture()~ and ~addLights()~ are public +for exactly this reason. + +** export-docs.sh: regenerate the documentation +:PROPERTIES: +:CUSTOM_ID: export-docs +:END: + +All engine documentation (the pages you are reading) lives as org-mode +files under =doc/=. One script exports every page to HTML with the +darksun theme: + +#+BEGIN_SRC bash +doc/export-docs.sh # export all pages +doc/export-docs.sh --check # + render every page with headless Chrome + # to /tmp/doc-check-*.png for visual review +#+END_SRC + +The =--check= mode is how an agent verifies its own documentation: SVG +label collisions, broken image links and table breakage all show up in +the rendered screenshots. + +** A typical agent session +:PROPERTIES: +:CUSTOM_ID: typical-session +:END: + +#+BEGIN_EXAMPLE +1. Reproduce: Snapshot.render(scene, lighting, bugReportPose, 640, 480) +2. Inspect: SceneDump.dump(...) + view the PNG +3. Fix the engine +4. Verify: PixelAssertions.unpaintedFraction(...) == 0 +5. Regression: HouseGoldens (must stay ALL PASS) +6. Document: edit doc pages, export-docs.sh --check, review shots +#+END_EXAMPLE + +** Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|-----------------+----------------------------------------------------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/Snapshot.html][Snapshot]] | Windowless render facade + pose string conversion | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/PixelAssertions.html][PixelAssertions]] | Painted-region / color-count / hex-grid assertions | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/GoldenImage.html][GoldenImage]] | Golden-PNG comparison, diff writer, CLI | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/SceneDump.html][SceneDump]] | Scene state as a reproducible text block | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.html][ShapeCollection]] | ~transformShapes(Camera, ...)~ headless overload | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] | ~getImage()~ exposes the painted frame | + +* Source code +:PROPERTIES: +:CUSTOM_ID: source-code +:ID: 978b7ea2-e246-45d0-be76-4d561308e9f3 +:END: + +*This program is free software: released under Creative Commons Zero +(CC0) license* + +*Program author:* +- Svjatoslav Agejenko +- Homepage: https://svjatoslav.eu +- Email: mailto://svjatoslav@svjatoslav.eu +- See also: [[https://www.svjatoslav.eu/projects/][Other software projects hosted at svjatoslav.eu]] + +*Getting the source code:* +- [[https://www2.svjatoslav.eu/gitweb/?p=aukio-3d.git;a=snapshot;h=HEAD;sf=tgz][Download latest source code snapshot in TAR GZ format]] +- [[https://www2.svjatoslav.eu/gitweb/?p=aukio-3d.git;a=summary][Browse Git repository online]] +- Clone Git repository using command: + : git clone https://www3.svjatoslav.eu/git/aukio-3d.git diff --git a/Documentation/style.css b/Documentation/style.css new file mode 100644 index 0000000..3403e0e --- /dev/null +++ b/Documentation/style.css @@ -0,0 +1,35 @@ +.flex-center { + display: flex; + justify-content: center; +} + +.flex-center video { + width: min(90%, 1000px); + height: auto; +} + +.responsive-img { + width: min(100%, 1000px); + height: auto; +} + +/* === SVG diagram theme === */ +svg > rect:first-child { + fill: #061018; +} + +svg text[fill="#666"], +svg text[fill="#999"] { + fill: #aaa !important; +} + +svg line[stroke="#ccc"] { + stroke: #445566 !important; +} + +svg { + background-color: #061018; + border-radius: 8px; + display: block; + margin: 0 auto; +} \ No newline at end of file diff --git a/Tools/Update web site b/Tools/Update web site index e80b377..a38f227 100755 --- a/Tools/Update web site +++ b/Tools/Update web site @@ -26,10 +26,10 @@ export_org_to_html() { } export_org_files_to_html() { - echo "🔍 Searching for .org files in doc/ ..." + echo "🔍 Searching for .org files in Documentation/ ..." echo "=======================================" - mapfile -t ORG_FILES < <(find doc -type f -name "*.org" | sort) + mapfile -t ORG_FILES < <(find Documentation -type f -name "*.org" | sort) if [ ${#ORG_FILES[@]} -eq 0 ]; then echo "❌ No .org files found!" @@ -61,22 +61,22 @@ export_org_files_to_html() { } build_visualization_graphs() { - rm -rf doc/graphs/ - mkdir -p doc/graphs/ + rm -rf Documentation/graphs/ + mkdir -p Documentation/graphs/ - javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d doc/graphs/ -n "All classes" -t png -ho - javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d doc/graphs/ -n "GUI" -t png -w "eu.svjatoslav.aukio.e3d.gui.*" -ho - javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d doc/graphs/ -n "Raster engine" -t png -w "eu.svjatoslav.aukio.e3d.renderer.raster.*" -ho + javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d Documentation/graphs/ -n "All classes" -t png -ho + javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d Documentation/graphs/ -n "GUI" -t png -w "eu.svjatoslav.aukio.e3d.gui.*" -ho + javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d Documentation/graphs/ -n "Raster engine" -t png -w "eu.svjatoslav.aukio.e3d.renderer.raster.*" -ho - meviz index -w doc/graphs/ -t "Aukio 3D classes" + meviz index -w Documentation/graphs/ -t "Aukio 3D classes" } # Build project jar file and JavaDocs mvn clean package # Put generated JavaDoc HTML files to documentation directory -rm -rf doc/apidocs/ -cp -r target/apidocs/ doc/ +rm -rf Documentation/apidocs/ +cp -r target/apidocs/ Documentation/ # Publish Emacs org-mode files into HTML format export_org_files_to_html @@ -87,7 +87,7 @@ build_visualization_graphs ## Upload assembled documentation to server echo "📤 Uploading to server..." -rsync -avz --delete -e 'ssh -p 10006' doc/ \ +rsync -avz --delete -e 'ssh -p 10006' Documentation/ \ n0@www3.svjatoslav.eu:/mnt/big/projects/aukio-3d/ if [ $? -eq 0 ]; then diff --git a/doc/Agentic development/Golden workflow.svg b/doc/Agentic development/Golden workflow.svg deleted file mode 100644 index aa0e7fb..0000000 --- a/doc/Agentic development/Golden workflow.svg +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Snapshot.render - scene + pose - - - - - - goldens/*.png - committed reference - - - - - GoldenImage - .compare - - - - - PASS - exit 0 - - - - - FAIL, exit 1 - + diff PNG to /tmp - - - - - bug? fix code - intended? run - --update - - - - regenerates reference - - tolerance: per-channel delta + max differing-pixel fraction — shading is deterministic, keep both tight - diff --git a/doc/Agentic development/Headless lanes.svg b/doc/Agentic development/Headless lanes.svg deleted file mode 100644 index 1303a80..0000000 --- a/doc/Agentic development/Headless lanes.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ViewPanel - window + render thread - camera from user input - - - - Snapshot.render() - no window, no display - camera from pose string - - - - SAME pipeline - transform - ↓ - sort by Z - ↓ - paint - - - - screen - BufferStrategy - - - BufferedImage - → PNG (Snapshot.save) - → PixelAssertions - → GoldenImage - - - - - - - - - - - identical results - headless rendering drives the very same code the window uses — a test render IS the real render - diff --git a/doc/Agentic development/Pixel assertion.svg b/doc/Agentic development/Pixel assertion.svg deleted file mode 100644 index a9218f6..0000000 --- a/doc/Agentic development/Pixel assertion.svg +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - rendered frame (640×480) - - - - - - painted - painted - - - - region (0.15, 0.45) → (0.85, 1.0) - - - - hole - - - - PixelAssertions - unpaintedFraction(img, - bg=0x000000, - 0.15, 0.45, - 0.85, 1.0) - counts pixels that still - equal the background - → 0.017 > 0.01 FAIL - - - - - sentinel background: "nothing rendered here" is unambiguous, even in dark scenes - diff --git a/doc/Agentic development/diff-example.png b/doc/Agentic development/diff-example.png deleted file mode 100644 index 941d5ca..0000000 Binary files a/doc/Agentic development/diff-example.png and /dev/null differ diff --git a/doc/Agentic development/snapshot-example.png b/doc/Agentic development/snapshot-example.png deleted file mode 100644 index 92ea03c..0000000 Binary files a/doc/Agentic development/snapshot-example.png and /dev/null differ diff --git a/doc/CSG/BSP tree.svg b/doc/CSG/BSP tree.svg deleted file mode 100644 index eb89c2c..0000000 --- a/doc/CSG/BSP tree.svg +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - Plane P₁ - - - - - - - - - front - back - - - - Front (P₂) - - - - Back (P₃) - - - - - - - - - - - leaf - - leaf - - - Each plane divides space into front (normal side) and back (opposite) - Polygons are classified and split at each partitioning plane - diff --git a/doc/CSG/CSG demo.png b/doc/CSG/CSG demo.png deleted file mode 100644 index 2275350..0000000 Binary files a/doc/CSG/CSG demo.png and /dev/null differ diff --git a/doc/CSG/CSG intersect.svg b/doc/CSG/CSG intersect.svg deleted file mode 100644 index a912f81..0000000 --- a/doc/CSG/CSG intersect.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - Input - - A - - B - - - ∩ - intersect - - - - - - - Result: A ∩ B - - - - - - - - - - - - Find overlap - between both - Only shared volume - remains - diff --git a/doc/CSG/CSG operations.svg b/doc/CSG/CSG operations.svg deleted file mode 100644 index 3f73cbe..0000000 --- a/doc/CSG/CSG operations.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - Input - - A - - B - - - − - subtract - - - - - - - Result: A − B - - - - - - cavity - - - B is the "cutter" - carves out of A - Cube with cavity - interior faces visible - diff --git a/doc/CSG/CSG union.svg b/doc/CSG/CSG union.svg deleted file mode 100644 index f1eedec..0000000 --- a/doc/CSG/CSG union.svg +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - Input - - A - - B - - - + - union - - - - - - - Result: A + B - - - - - - removed - - - Keeps all geometry - from both shapes - Single combined volume - interior faces removed - diff --git a/doc/CSG/Polygon clipping.svg b/doc/CSG/Polygon clipping.svg deleted file mode 100644 index 41de628..0000000 --- a/doc/CSG/Polygon clipping.svg +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - Polygon crosses plane - - - plane - - - - - → - split - - - Split into fragments - - - - - front - - - - back - - - - - - new edge - - - Spanning polygons are split; each fragment goes to its respective subtree - diff --git a/doc/CSG/index.org b/doc/CSG/index.org deleted file mode 100644 index ebdbad6..0000000 --- a/doc/CSG/index.org +++ /dev/null @@ -1,284 +0,0 @@ -#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme -#+TITLE: Constructive Solid Geometry - Aukio 3D -#+LANGUAGE: en -#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} -#+LATEX_HEADER: \usepackage{parskip} -#+LATEX_HEADER: \usepackage[none]{hyphenat} - -#+OPTIONS: H:20 num:20 -#+OPTIONS: author:nil - -#+HTML_HEAD: - -[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] - -* What is CSG? -:PROPERTIES: -:CUSTOM_ID: what-is-csg -:ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 -:END: - -*Constructive Solid Geometry* (CSG) is a modeling technique that builds -complex 3D shapes by combining simpler primitives using boolean -operations. Instead of manually creating every vertex and face, you -define shapes as the result of operations like "merge these two cubes" -or "carve a hole using this sphere." - -CSG is particularly powerful for: -- *Procedural modeling* — generate complex geometry algorithmically -- *CAD/CAM applications* — define parts as combinations of primitives -- *Game development* — create architectural elements, holes, cavities -- *Rapid prototyping* — iterate on designs by adjusting operations - -The three fundamental CSG operations are: - -| Operation | Symbol | Result | -|-------------+--------+-------------------------------------------| -| Subtract | A - B | A with B carved out (holes, cavities) | -| Union | A + B | Combined volume (both shapes merged) | -| Intersect | A ∩ B | Volume where both overlap | - -See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#csg-demo][CSG demo]] for an interactive visualization. - -* The Three Operations -:PROPERTIES: -:CUSTOM_ID: the-three-operations -:END: - -#+attr_html: :class responsive-img -#+attr_latex: :width 1000px -[[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#csg-demo][file:CSG%20demo.png]] - -The screenshot above shows all three operations displayed left to right: -subtract (green cube with spherical cavity), union (merged green and -orange shapes), and intersect (only the overlapping region in blue). - -The diagrams below use the same green cube (A) and orange sphere (B) as -the screenshot above. Each operation transforms these inputs differently, -producing the results shown from left to right in the image. - -** Subtract (A - B) -:PROPERTIES: -:CUSTOM_ID: subtract-operation -:END: - -#+INCLUDE: "CSG operations.svg" export html - -*Subtract* removes the orange sphere (B) from the green cube (A), carving -out a cavity. The diagram shows B acting as a "cutter" — where it overlaps -A, a hole is created. Interior faces *are preserved* and become visible, -allowing you to see inside the carved-out space (shown as the orange dashed -curve in the result). - -This matches the leftmost shape in the screenshot: a green cube with a -visible spherical hollow inside, showing the interior surfaces created by -the subtraction. - -This operation is ideal for creating: -- Holes and tunnels -- Carved-out spaces -- Hollow objects - -#+BEGIN_SRC java -SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN); -SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE); - -cube.subtract(sphere); // cube now has a spherical cavity -#+END_SRC - -** Union (A + B) -:PROPERTIES: -:CUSTOM_ID: union-operation -:END: - -#+INCLUDE: "CSG union.svg" export html - -*Union* merges the green cube (A) and orange sphere (B) into one continuous -volume. The diagram shows both shapes combining — the interior seam (where -they overlap) is removed, creating a single solid surface with no internal -boundaries (indicated by the dashed blue line labeled "removed"). - -This corresponds to the center shape in the screenshot: both green and -orange colors present but seamlessly joined, forming one unified object. - -#+BEGIN_SRC java -SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN); -SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE); - -cube.union(sphere); // cube now contains the merged result -#+END_SRC - -** Intersect (A ∩ B) -:PROPERTIES: -:CUSTOM_ID: intersect-operation -:END: - -#+INCLUDE: "CSG intersect.svg" export html - -*Intersect* keeps only the volume where the green cube (A) and orange -sphere (B) overlap — the region that is inside *both* shapes -simultaneously. The diagram shows this as the blue-shaded area: the -portion of the sphere that fits within the cube boundaries. Everything -else is discarded. - -This is the rightmost shape in the screenshot: only the overlapping -portion remains, showing which parts of space were occupied by both the -cube and sphere at the same time. - -This operation is useful for: -- Creating shapes constrained by multiple boundaries -- Finding collision regions -- Trimming geometry to fit within bounds - -#+BEGIN_SRC java -SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN); -SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE); - -cube.intersect(sphere); // only the overlapping region remains -#+END_SRC - -* BSP Tree Algorithm -:PROPERTIES: -:CUSTOM_ID: bsp-tree-algorithm -:END: - -CSG boolean operations are implemented using *Binary Space Partitioning* -(BSP) trees. A BSP tree recursively divides 3D space using planes, -creating a hierarchical structure that enables efficient polygon clipping -and spatial queries. - -** BSP Tree Structure -:PROPERTIES: -:CUSTOM_ID: bsp-tree-structure -:END: - -#+INCLUDE: "BSP tree.svg" export html - -Each BSP node contains: -- A *partitioning plane* that divides space into two half-spaces -- *Polygons* that lie exactly on this plane (coplanar) -- *Front* subtree — polygons on the same side as the plane's normal -- *Back* subtree — polygons on the opposite side - -** Key BSP Operations -:PROPERTIES: -:CUSTOM_ID: key-bsp-operations -:END: - -The BSP tree provides three core operations that enable CSG: - -| Operation | Description | -|----------------+--------------------------------------------------| -| =invert()= | Flip all normals, swap front/back children | -| =clipTo(tree)= | Remove polygons inside the other tree's solid | -| =addPolygons()= | Insert new polygons, splitting at planes | - -*Invert* is fundamental to CSG. By flipping inside/outside, we can -transform subtraction and intersection into variations of clipping: - -- **Subtract** = invert A, clip against B, add B's clipped parts, invert back -- **Intersect** = invert A, clip B against A, invert B, clip A against B, combine, invert A back - -** Polygon Clipping -:PROPERTIES: -:CUSTOM_ID: polygon-clipping -:END: - -When a polygon crosses a partitioning plane, it's *split* into two -fragments: - -#+INCLUDE: "Polygon clipping.svg" export html - -This recursive splitting ensures that all polygons are cleanly classified -as entirely in front, entirely behind, or exactly on a plane — never -"spanning" across. - -* Using CSG in Aukio 3D -:PROPERTIES: -:CUSTOM_ID: using-csg-in-aukio-3d -:END: - -CSG operations are methods on [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]. They modify the -shape *in-place* — the result replaces the original geometry. - -** Basic Usage -:PROPERTIES: -:CUSTOM_ID: basic-usage -:END: - -#+BEGIN_SRC java -import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.*; - -// Create two shapes -SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 100, Color.GREEN); -SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 70, 12, Color.ORANGE); - -// Perform CSG operations (in-place modification) -cube.subtract(sphere); // Cube with spherical cavity -// or -cube.union(sphere); // Merged shape -// or -cube.intersect(sphere); // Only overlapping region - -// Add to scene -shapes.addShape(cube.setBackfaceCulling(true)); -#+END_SRC - -** Child Handling Behavior -:PROPERTIES: -:CUSTOM_ID: child-handling -:END: - -CSG operations only affect *SolidPolygon* geometry. Other children are -preserved as objects: - -| Child Type | Union | Subtract | Intersect | -|-----------------------+------------------+------------------+------------------| -| SolidPolygon (this) | Replaced with result | Replaced with result | Replaced with result | -| SolidPolygon (other) | Merged into result | Discarded (cutter) | Discarded | -| Line, TextCanvas (this) | Preserved | Preserved | Preserved | -| Line, TextCanvas (other) | Merged into this shape | Discarded | Discarded | -| Nested composite | Preserved as object — but see below | same | same | - -*Nested composites are not CSG-safe.* Polygon extraction recurses into -them, so their SolidPolygons are included in the BSP result — while the -nested composite object itself is also preserved, duplicating that -geometry in the render. Apply CSG to flat composites, or extract the -nested polygons first. - -This allows you to attach labels, decorations, or wireframe overlays to -shapes without them being affected by CSG operations (for union, the -other shape's decorations are copied over too). - -** Important Notes -:PROPERTIES: -:CUSTOM_ID: important-notes -:END: - -1. *Shapes are modified in-place*. The original geometry is replaced. - Clone shapes beforehand if you need to preserve the originals. - -2. *CSG works on SolidPolygon children only*. TexturedTriangle and other - shape types are not processed. - -3. *Result quality depends on mesh density*. Low-polygon inputs may - produce visible artifacts at intersection boundaries. Use higher - subdivision counts for smoother results. - -4. *Backface culling is recommended*. CSG results often have internal - faces from the cutting operation. Enable culling to hide backfaces: - =shape.setBackfaceCulling(true)= - -* Related Classes -:PROPERTIES: -:CUSTOM_ID: related-classes -:END: - -| Class | Purpose | -|--------------------------+------------------------------------------------------| -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/BspTree.html][BspTree]] | BSP tree for spatial partitioning and CSG operations | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Plane]] | Partitioning plane used by BSP nodes | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Polygon shape processed by CSG | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] | Base class with union/subtract/intersect methods | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.html][SolidPolygonMesh]] | Custom polygon mesh for arbitrary geometry | -| SolidPolygon* primitives | See [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery]] for all available shapes | diff --git a/doc/Coordinate system.svg b/doc/Coordinate system.svg deleted file mode 100644 index 4497bf3..0000000 --- a/doc/Coordinate system.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - X - right (+) / left (-) - - - Y - down (+) / up (-) - - - Z - away (+) / towards (-) - Origin - (0, 0, 0) - \ No newline at end of file diff --git a/doc/Depth buffer/index.org b/doc/Depth buffer/index.org deleted file mode 100644 index 2f2c75d..0000000 --- a/doc/Depth buffer/index.org +++ /dev/null @@ -1,130 +0,0 @@ -#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme -#+TITLE: Depth Buffer - Aukio 3D -#+LANGUAGE: en -#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} -#+LATEX_HEADER: \usepackage{parskip} -#+LATEX_HEADER: \setlength{\parindent}{15pt} -#+LATEX_HEADER: \usepackage{palatino} -#+LATEX_HEADER: \usepackage{charter} -#+HTML_HEAD: - -[[file:../index.html#outline-container-depth-buffer][<- Back to index]] - -* Per-pixel visibility -:PROPERTIES: -:CUSTOM_ID: per-pixel -:END: - -The engine resolves visibility with a depth buffer, not paint order. -Every rasterized triangle carries a per-pixel depth quantity =zw = -1/z= (camera-space), interpolated linearly across each span — =1/z= is -affine in screen space, so it rides the same edge interpolators as the -texture gradients. A fragment wins a pixel only where - -#+BEGIN_EXAMPLE -zw > stored - margin * zw^2 (margin = 0 by default) -#+END_EXAMPLE - -Default margin 0 is *strict depth*: the nearer fragment always wins, -regardless of paint order, so overlapping depth ranges — a floor tile -extending under furniture, a wall seen through a doorway — come out -correct per pixel. A nonzero margin (= RenderingContext.DEPTH_MARGIN_DZ =, -=-Daukio.zbuffer.margin=, world units) re-opens a tolerance window -*behind* the stored depth for near-coplanar pairs; any nonzero window -re-imports per-triangle sort errors into per-pixel occlusion, which is -why the default is strict. - -The depth buffer is allocated once per frame context -(=RenderingContext.depth=, float per pixel) and cleared per tile -together with the pixel buffer. - -* Two passes -:PROPERTIES: -:CUSTOM_ID: two-passes -:END: - -=RenderAggregator.paintSorted= paints the sorted queue in two passes: - -1. *Opaque pass* — opaque-class triangles, iterated front-to-back (the - queue is back-to-front, so reversed), depth test + depth write. - Front-to-back order is a pure performance hint: hidden fragments - die on the depth test *before* the texture fetch (early-z). -2. *Alpha pass* — alpha-class triangles (translucent solid polygons, - alpha-carrying textures, SDF text), iterated back-to-front in queue - order, depth test but *no depth write*. Translucency never - occludes, and overlapping translucent surfaces keep painter-coherent - mutual order. - -A shape's class comes from its paint color or texture: solid polygons -with =alpha = 255= are opaque, anything translucent is alpha-class; -textured triangles are alpha-class when the texture has alpha or is an -SDF mask. - -* Which shapes carry depth -:PROPERTIES: -:CUSTOM_ID: shapes -:END: - -- =TexturedTriangle= — opaque or alpha class by texture. -- =SolidPolygon= — depth-tested since 2026-09-17; opaque when its - (possibly shaded) color is fully opaque, translucent otherwise. - Near-plane-clipped quads fan-triangulate with depth like any other - triangles. -- =LightmappedTriangle= — a textured triangle, so the same rules. -- =Line=, =Billboard=, =GlowingPoint= — no depth by design: they are - 2D overlays (wireframes, markers, sprites) and always paint on top, - in the alpha pass. - -Because every occluder writes depth, scene code no longer needs any -ordering structure: composites just fan-triangulate their polygons. -(=LightmappedCompositeShape= exists only to wrap polygons as lightmap -carriers for the GI system, not to order them.) - -* Hi-Z occlusion pyramid -:PROPERTIES: -:CUSTOM_ID: hi-z -:END: - -After each successful paint, =ViewPanel= builds a Hi-Z pyramid from -the depth buffer (gui/HiZPyramid): 8-pixel tiles pooled upward, -each tile storing the *minimum* =zw= (farthest written depth — -max-pooling would store the nearest occluder and wrongly cull geometry -visible between near gaps). Next frame, =TriangleMeshBlock= projects -its world AABB's 8 corners and, when the nearest corner is still -behind the pyramid's stored depth, skips the whole block before any -per-triangle work. - -The test is conservative by construction (min-pooling plus sky pixels -at =-inf=), so it never culls visible geometry; wrong culls under -camera motion self-heal in one frame. Knobs: =-Daukio.hiz.margin=0.02=, -kill switch =-Daukio.hiz=false=. Headless snapshots never build the -pyramid, so golden renders are structurally unaffected. Stereo skips -the test (the pyramid is mono). - -* Determinism and depth dumps -:PROPERTIES: -:CUSTOM_ID: determinism -:END: - -The renderer is bit-deterministic: same scene and camera give -bit-identical pixels across runs, which is what the golden-image -regression tests compare. =Snapshot= (the headless toolkit) supports -=-Daukio.zbuffer.dumpDepth=path.png= to write a grayscale depth map -alongside the color image — useful when hunting depth-window bugs -(bisect those with =-Daukio.zbuffer.margin=0=). - -* Related classes -:PROPERTIES: -:CUSTOM_ID: related-classes -:END: - -| Class | Role | -|----------------------+----------------------------------------------------------| -| =RenderingContext= | =depth= buffer, =depthPass=, =DEPTH_MARGIN_DZ= constant | -| =RenderAggregator= | =paintSorted= two-pass driver, queue sort | -| =TexturedTriangle= | Z span writers (perspective and affine) | -| =SolidPolygon= | flat-color Z span writer, two-pass classification | -| =HiZPyramid= | temporal whole-block occlusion culling | -| =ViewPanel= | per-tile depth clear, pyramid rebuild after paint | - -[[file:../index.html#outline-container-depth-buffer][Back to main documentation]] diff --git a/doc/Developer tools/Developer tools.png b/doc/Developer tools/Developer tools.png deleted file mode 100644 index 825b0de..0000000 Binary files a/doc/Developer tools/Developer tools.png and /dev/null differ diff --git a/doc/Developer tools/Render alternative segments.png b/doc/Developer tools/Render alternative segments.png deleted file mode 100644 index e2bd569..0000000 Binary files a/doc/Developer tools/Render alternative segments.png and /dev/null differ diff --git a/doc/Developer tools/Render polygon borders.png b/doc/Developer tools/Render polygon borders.png deleted file mode 100644 index 5ec2182..0000000 Binary files a/doc/Developer tools/Render polygon borders.png and /dev/null differ diff --git a/doc/Developer tools/Show segment boundaries.png b/doc/Developer tools/Show segment boundaries.png deleted file mode 100644 index 01a1978..0000000 Binary files a/doc/Developer tools/Show segment boundaries.png and /dev/null differ diff --git a/doc/Developer tools/Thread timeline.png b/doc/Developer tools/Thread timeline.png deleted file mode 100644 index dd1d378..0000000 Binary files a/doc/Developer tools/Thread timeline.png and /dev/null differ diff --git a/doc/Edge.svg b/doc/Edge.svg deleted file mode 100644 index e9af1cf..0000000 --- a/doc/Edge.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - V₁ - V₂ - V₃ - edge - diff --git a/doc/Example.png b/doc/Example.png deleted file mode 100644 index 7094240..0000000 Binary files a/doc/Example.png and /dev/null differ diff --git a/doc/Face triangle.svg b/doc/Face triangle.svg deleted file mode 100644 index 509c841..0000000 --- a/doc/Face triangle.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - V₁ - V₂ - V₃ - FACE - diff --git a/doc/Frustum culling/Frustum diagram.svg b/doc/Frustum culling/Frustum diagram.svg deleted file mode 100644 index b59d4a8..0000000 --- a/doc/Frustum culling/Frustum diagram.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - +Z - (view direction) - - - - Camera - - - - - - - - - - - - - - Near - - - - Far - - - - - - visible region - - - Top plane - Bottom plane - - - - ✓ - rendered - - - - ✗ - culled - - - - ✗ - culled - diff --git a/doc/Frustum culling/P-vertex AABB.svg b/doc/Frustum culling/P-vertex AABB.svg deleted file mode 100644 index a3acfb8..0000000 --- a/doc/Frustum culling/P-vertex AABB.svg +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - P-vertex: corner most aligned with plane normal - If P is behind the plane → entire AABB is outside - - - - Plane - - - inside frustum - - outside frustum - - - - - N - - - - inside - - - P - - - - outside - - - P - diff --git a/doc/Frustum culling/index.org b/doc/Frustum culling/index.org deleted file mode 100644 index 9c4a941..0000000 --- a/doc/Frustum culling/index.org +++ /dev/null @@ -1,177 +0,0 @@ -#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme -#+TITLE: Frustum & View Frustum Culling - Aukio 3D -#+LANGUAGE: en -#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} -#+LATEX_HEADER: \usepackage{parskip} -#+LATEX_HEADER: \usepackage[none]{hyphenat} - -#+OPTIONS: H:20 num:20 -#+OPTIONS: author:nil - -#+HTML_HEAD: - -[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] - -* Frustum & View Frustum Culling -:PROPERTIES: -:CUSTOM_ID: frustum-view-frustum-culling -:END: - -#+INCLUDE: "Frustum diagram.svg" export html - -The *view frustum* is a truncated pyramid-shaped volume that represents -everything the camera can see. Objects completely outside this volume are -skipped during rendering — a powerful optimization called *frustum culling*. - -** The Six Frustum Planes -:PROPERTIES: -:CUSTOM_ID: frustum-planes -:END: - -The frustum is defined by six clipping planes: - -| Plane | Purpose | -|---------+--------------------------------------------| -| Left | Left edge of viewport | -| Right | Right edge of viewport | -| Top | Top edge of viewport (smaller Y in Y-down) | -| Bottom | Bottom edge of viewport (larger Y) | -| Near | Closest visible distance from camera | -| Far | Farthest visible distance from camera | - -Each plane divides 3D space into "inside" (visible) and "outside" -(culled). An object must pass all six plane tests to be considered -potentially visible. - -** Frustum Culling vs Backface Culling -:PROPERTIES: -:CUSTOM_ID: frustum-vs-backface-culling -:END: - -These are complementary optimizations at different levels: - -| Optimization | Level | What it skips | -|-----------------+--------------+----------------------------------| -| Frustum culling | Object level | Entire composite shapes + children | -| Backface culling | Polygon level | Individual triangles facing away | - -*Frustum culling* happens first during the transform phase — entire -object trees are skipped with a single bounding box test. *Backface -culling* happens later during rasterization — individual triangles -are checked before being drawn. - -For best performance, use both: organize your scene with composite -shapes for effective frustum culling, and enable backface culling on -closed meshes. - -* How Frustum Culling Works in Aukio 3D -:PROPERTIES: -:CUSTOM_ID: frustum-culling-implementation -:END: - -Frustum culling is applied automatically to all [[../index.org#mesh][composite shapes]] -during Phase 1 (transform) of the [[../Rendering loop/][rendering loop]]: - -1. *Update frustum*: Compute 6 planes from camera FOV and viewport size -2. *For each composite shape*: - - Get its [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Box.html][Axis-Aligned Bounding Box (AABB)]] - - Transform all 8 corners to view space - - Test against frustum using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Frustum.html#intersectsAABB][intersectsAABB()]] - - If outside: skip the entire composite and all children - - If inside: continue transforming children - -(The root composite itself is never tested — it is always rendered.) - -** The AABB Intersection Algorithm -:PROPERTIES: -:CUSTOM_ID: aabb-intersection-algorithm -:END: - -The intersection test uses an optimized "P-vertex" approach: - -#+INCLUDE: "P-vertex AABB.svg" export html - -For each plane, instead of testing all 8 corners of the bounding box, -we test only the *P-vertex* — the corner most aligned with the plane -normal. If this "best" corner is behind the plane, the entire box must -be outside the frustum. - -- Plane normal points *into* the frustum (toward visible region) -- P-vertex: select corner based on normal direction - - If normal.x > 0 → use maxX (rightmost corner) - - If normal.x < 0 → use minX (leftmost corner) - - Same logic for Y and Z -- Test: =dot(normal, P-vertex) < distance= → outside - -This reduces from 48 tests (8 corners × 6 planes) to just 6 tests per -object. - -* Performance Benefits -:PROPERTIES: -:CUSTOM_ID: frustum-performance -:END: - -Frustum culling can dramatically improve performance for large scenes: - -- *High cull % (60-90%)*: Excellent — most objects skipped entirely -- *Medium cull % (20-60%)*: Moderate benefit -- *Low cull % (0-20%)*: Limited benefit — most objects visible - -A composite shape that is culled skips: -- Transforming all its children -- Computing bounding boxes for children -- All polygon-level operations (backface culling, rasterization) - -Open Developer Tools (F12) to see real-time [[../index.org#frustum-culling-statistics][frustum culling statistics]]. - -* Scene Design for Effective Culling -:PROPERTIES: -:CUSTOM_ID: frustum-scene-design -:END: - -Frustum culling works best when you organize your scene into -well-defined composite shapes: - -#+BEGIN_SRC java -// Good: Each building is a separate composite -AbstractCompositeShape cityBlock = new AbstractCompositeShape(); -for (Building building : buildings) { - AbstractCompositeShape buildingComposite = new AbstractCompositeShape(); - buildingComposite.addShape(buildingWalls); - buildingComposite.addShape(buildingRoof); - buildingComposite.addShape(buildingInterior); - cityBlock.addShape(buildingComposite); -} - -// Less effective: Everything in one giant composite -AbstractCompositeShape allObjects = new AbstractCompositeShape(); -allObjects.addShape(building1Walls); -allObjects.addShape(building1Roof); -allObjects.addShape(building2Walls); -// ... hundreds of shapes directly in root -#+END_SRC - -*Best practices:* - -- Use composites to group objects that occupy a bounded region of space -- Keep bounding boxes tight (don't add distant objects to the same composite) -- Nest composites hierarchically for multi-level culling (city → block → building) -- Call [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.html#invalidateBounds()][invalidateBounds()]] after moving shapes — the bounding box is - recomputed lazily on next use - -* Technical Details -:PROPERTIES: -:CUSTOM_ID: frustum-technical-details -:END: - -The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Frustum.html][Frustum]] class: - -- Computes planes in *view space* (camera at origin, looking along +Z) -- FOV derived from =projectionScale = width / 3= (≈112° horizontal FOV) -- Default clip distances: Near = 1.0, Far = 10000.0 -- Planes stored in [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Hesse normal form]]: (normal vector, distance) - -The frustum is updated once per render pass in -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.html][ShapeCollection.transformShapesBegin()]], from the camera state and -the stereo viewport width — in stereo mode each eye gets its own -frustum, so the update runs twice per frame. diff --git a/doc/Global illumination/Bounce estimator.svg b/doc/Global illumination/Bounce estimator.svg deleted file mode 100644 index 0d1972e..0000000 --- a/doc/Global illumination/Bounce estimator.svg +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - surface - - - - - - - P - - normal - - - - cosine-weighted - hemisphere - - - - - - - - - Q - - - - - light - - - - shadow ray: clear - - - - - - occluder - blocked - - - - at Q: direct light (cached shadow bits) - + Q's current indirect estimate - - - - P's target = (albedo / π) x (direct + indirect) - blended in with an exponential moving average - - one sample per texel per visit: one shadow ray + one bounce ray — bounce light ripples deeper every sweep - diff --git a/doc/Global illumination/GI pipeline.svg b/doc/Global illumination/GI pipeline.svg deleted file mode 100644 index 0f63023..0000000 --- a/doc/Global illumination/GI pipeline.svg +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - scene snapshot - triangles + lights - + BVH - - - GI workers - Monte Carlo - sweeps - - - lightmaps - per-texel indirect - + shadow bits - - - composite - baseColor x light - double-buffered - swap - - - painter - plain texture - lookup - - - - - - - - - - bounce reads last sweep's estimate - - zero ray casting - on render threads - diff --git a/doc/Global illumination/Global illumination.png b/doc/Global illumination/Global illumination.png deleted file mode 100644 index 9909193..0000000 Binary files a/doc/Global illumination/Global illumination.png and /dev/null differ diff --git a/doc/Global illumination/Lightmap mapping.svg b/doc/Global illumination/Lightmap mapping.svg deleted file mode 100644 index 2d82fea..0000000 --- a/doc/Global illumination/Lightmap mapping.svg +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - invalid half (u+v > 1) - filled from neighbors so sampling - near the hypotenuse stays clean - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - one texel = one surface - patch, sampled at center - - - - a (u=0, v=0) - - b (u=1, v=0) - - c (u=0, v=1) - - - - e1 = b − a - - e2 = c − a - - - - world(u,v) = a + e1·u + e2·v - texels = edge / unitsPerTexel - - every texel owns a fixed patch of the triangle — shadows and gradients live INSIDE the surface - diff --git a/doc/Global illumination/gi-converged.png b/doc/Global illumination/gi-converged.png deleted file mode 100644 index 1f81bfd..0000000 Binary files a/doc/Global illumination/gi-converged.png and /dev/null differ diff --git a/doc/Global illumination/gi-flat.png b/doc/Global illumination/gi-flat.png deleted file mode 100644 index f135bd2..0000000 Binary files a/doc/Global illumination/gi-flat.png and /dev/null differ diff --git a/doc/Global illumination/gi-start.png b/doc/Global illumination/gi-start.png deleted file mode 100644 index 6f7f3f0..0000000 Binary files a/doc/Global illumination/gi-start.png and /dev/null differ diff --git a/doc/Global illumination/index.org b/doc/Global illumination/index.org deleted file mode 100644 index 9568956..0000000 --- a/doc/Global illumination/index.org +++ /dev/null @@ -1,251 +0,0 @@ -#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme -#+TITLE: Global Illumination - Aukio 3D -#+LANGUAGE: en -#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} -#+LATEX_HEADER: \usepackage{parskip} -#+LATEX_HEADER: \usepackage[none]{hyphenat} - -#+OPTIONS: H:20 num:20 -#+OPTIONS: author:nil - -#+HTML_HEAD: - -[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] - -* What global illumination adds -:PROPERTIES: -:CUSTOM_ID: what-gi-adds -:END: - -Plain per-polygon shading lights every polygon with one flat color: -no shadows, and surfaces that receive no direct light stay uniformly -dark. A room corner reads as a flat silhouette instead of a corner. - -#+attr_html: :class responsive-img -#+attr_latex: :width 640px -[[file:gi-flat.png]] - -*Aukio 3D* can optionally compute *global illumination* progressively -on background CPU threads: shadows appear, light pools under lamps -with smooth falloff, and colored light *bleeds* — a red sofa tints the -floor next to it red. All of it converges gradually over the first -seconds of a scene, then idles. - -Same camera, same house: flat shading (top) versus converged GI -(below). Note the soft shadow of the partition wall, the lamp glow on -the ceiling, and the subtle color variation across the floor. - -#+attr_html: :class responsive-img -#+attr_latex: :width 640px -[[file:gi-converged.png]] - -* The big idea: GI off the render path -:PROPERTIES: -:CUSTOM_ID: off-the-render-path -:END: - -Ray tracing is far too slow to run per frame in a software renderer, -so it doesn't: *the render loop never traces a single ray.* Painting a -lightmapped triangle is an ordinary texture lookup, exactly as fast as -any textured polygon. - -All the expensive work happens on dedicated low-priority worker threads -that continuously refine per-surface lighting values. Whenever the -values have improved enough, the workers regenerate each triangle's -*composite texture* (baseColor x total lighting) into a back buffer -and swap it in atomically — painters never see a half-updated texture. - -#+INCLUDE: "GI pipeline.svg" export html - -Because painters only read finished textures, frame rate is completely -decoupled from GI quality: you can crank lightmap resolution up and the -only cost is CPU time on the worker threads, not frame time. - -* Lightmaps: a texture per triangle -:PROPERTIES: -:CUSTOM_ID: lightmaps -:END: - -Flat shading can only color a polygon uniformly — shadows and gradients -need resolution *inside* the polygon. Every lightmapped triangle -therefore owns a small generated texture, its *lightmap*, whose texels -map onto the triangle surface by an affine rule: - -#+INCLUDE: "Lightmap mapping.svg" export html - -The triangle's UVs are pinned to (0,0), (1,0), (0,1), so the valid -texel region is the half where u+v <= 1; the other half of the square -texture is flood-filled from valid neighbors so that nearest sampling -near the hypotenuse never picks up garbage. - -Each texel stores two things, both written only by GI threads: - -- *Indirect irradiance* (RGB floats) — the accumulated bounced light. -- *Per-light visibility bits* — whether the last shadow ray from this - texel reached each lamp (cached so later queries cost nothing). - -Resolution is set in world units per texel -(=LightmappedCompositeShape.setLightmapUnitsPerTexel()=, default 12): a 100-unit -wall cell gets an 8x8 lightmap. Halving the units quadruples the -tracing work. - -*What you trace is what you see:* the composite texture the painter -samples is the lightmap itself, at native texel resolution — there is -no upscaling step. Shadow-edge smoothness comes from tracing at finer -resolution, never from interpolation. (An earlier bilinear-upscaling -pass produced visibly artificial results and was removed; finer texels -cost more CPU on the GI threads but look right.) - -* One sample: a shadow ray and a bounce ray -:PROPERTIES: -:CUSTOM_ID: one-sample -:END: - -The work list is flat: one item per lightmap texel (and one per plain -polygon, see below). Worker threads walk it round-robin, and every -visit to a texel casts exactly two rays from the texel's world -position, nudged slightly off the surface along the normal: - -1. *Shadow ray* toward a lamp. Answers visible/occluded, cached in the - texel's visibility bits. On a texel's *first* visit all lamps are - tested at once, so direct light and hard shadows appear after a - single sweep instead of trickling in lamp by lamp; later visits - re-test one lamp at a time, round-robin. -2. *Bounce ray* in a random cosine-weighted direction around the - normal. Wherever it lands (point Q), the sample reads Q's *direct* - lighting — using Q's cached shadow bits, no new shadow rays — plus - Q's *current indirect estimate*, and blends the sum into the texel's - own indirect value. - -#+INCLUDE: "Bounce estimator.svg" export html - -Reading Q's current indirect estimate instead of recursing is what -makes bounce light propagate: sweep 1 learns "Q is directly lit", -sweep 2 learns "P sees a lit Q", sweep 3 learns "R sees a lit P"... -Light ripples one surface deeper with every sweep, with no recursion -limit and no exponential ray explosion. The =1/pi= diffuse gain keeps -the feedback loop from diverging: without it the indirect term -amplifies itself and the scene saturates to white. - -* Progressive convergence -:PROPERTIES: -:CUSTOM_ID: convergence -:END: - -Monte Carlo samples are noisy, so blending happens at *two nested -levels*, both exponential moving averages: - -1. *Inner, per sample*: each texel blends every new bounce-ray result - into its indirect estimate with a constant weight (=alpha = 0.15=, - mode =fixed=). Every ray hit stays equally intensive forever — an - unlit area fades to darkness at the same rate a lit area brightens. - (The old =adaptive= mode, which decays alpha with sample count, is - still available; see the knobs below.) -2. *Outer, per composite update*: the value that reaches the screen is - a second EMA over the COMPLETE sum =ambient + direct + indirect=. - The texture can only move =e3d.gi.compositeAlpha= (default 0.2) of - the remaining distance per 500 ms update — so direct light, shadows - and bounce light all glide in together over a few seconds, and no - single-frame jump is possible by construction. - -The world starts at a *uniform medium irradiance* -(=e3d.gi.initialIrradiance=, default 128): the scene is visible from -the very first frame, then lit areas brighten and unlit areas sink to -darkness as the workers sweep — lights and shadows gradually become -distinguished instead of the old pitch-black start with a sudden flash -once the first sweep landed: - -#+attr_html: :class responsive-img -#+attr_latex: :width 640px -[[file:gi-start.png]] - -Consequences of the design: - -- *Hysteresis is free*: when a lamp moves or geometry changes, old - light fades out gradually instead of popping — the same pair of EMAs - that accumulates light also drains it. -- *Convergence detection*: per-sample deltas are pure noise (and with a - constant alpha they never settle), so the system watches the average - per-texel movement of the on-screen estimate; after five composite - updates below =e3d.gi.calmThreshold= (default 1.0 light unit) the - workers drop to a low duty cycle (~50% of sweep time, capped) instead - of burning CPU. Any scene or light change rebuilds the snapshot and - restarts full-speed tracing. -- *Despeckle*: at composite time the indirect channel is blended 50/50 - with the mean of its valid 4-neighbors, killing single-texel Monte - Carlo spikes without blurring real gradients. -- *Composite cadence*: textures regenerate at most every 500 ms — one - atomic swap per triangle, invisible to painters. - -* Plain polygons get GI too -:PROPERTIES: -:CUSTOM_ID: plain-polygons -:END: - -Surfaces that are not lightmapped (ordinary =SolidPolygon= with shading -enabled) still benefit, at per-polygon resolution, through the -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.html][GiLightProvider]] -interface that =GlobalIllumination= installs into the -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]]: - -- =isLightVisible(polygon, light)= answers from cached shadow bits — - direct-light shadows fade in on flat-shaded geometry. -- =addIndirectLight(polygon, baseColor, result)= adds the polygon's - bounced-light estimate into the flat-shaded color. - -Both are called from parallel render-pool threads, so they only read -volatile caches — never trace. - -* Enabling GI -:PROPERTIES: -:CUSTOM_ID: enabling -:END: - -#+BEGIN_SRC java -// Per-texel lightmaps on composite geometry (the House demo setup): -LightmappedCompositeShape house = new LightmappedCompositeShape(); -house.setLightmappingEnabled(true); -house.setLightmapUnitsPerTexel(3.0); // fine texels: quality from traced rays - -// Start the workers (2 threads by default; more converge faster): -viewPanel.enableGlobalIllumination(4); -#+END_SRC - -Tuning knobs (system properties): - -| Property | Default | Effect | -|---------------------------+---------+-----------------------------------------| -| =e3d.gi.alphaMode= | fixed | =adaptive= decays the inner EMA alpha with sample count | -| =e3d.gi.alphaFloor= | 0.08 | adaptive-mode floor; higher adapts faster but noisier | -| =e3d.gi.compositeAlpha= | 0.2 | outer EMA: fade speed of the on-screen estimate per 500 ms update | -| =e3d.gi.initialIrradiance=| 128 | uniform medium start (0..255 light units) | -| =e3d.gi.calmThreshold= | 1.0 | convergence: avg estimate movement (light units) | -| =e3d.gi.despeckle= | true | neighbor-smoothing of indirect at composite time | -| =e3d.gi.debug= | false | sweep statistics to stdout | -| =e3d.gi.dumpLightmaps= | (unset) | dump composite lightmaps as PNGs to the given dir | - -* Limitations -:PROPERTIES: -:CUSTOM_ID: limitations -:END: - -- *Diffuse light only* — no specular bounce, no caustics. -- Polygon vertices are traced in composite-local space; scenes that put - non-identity transforms on composites are traced incorrectly. -- The bounce estimate is one ray deep per sample — correctness comes - from sweep-over-sweep propagation, so deeply indirect corners take - several sweeps to brighten. - -* Related Classes -:PROPERTIES: -:CUSTOM_ID: related-classes -:END: - -| Class | Purpose | -|----------------------+---------------------------------------------------------------| -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/GlobalIllumination.html][GlobalIllumination]] | Progressive tracer: sweeps, EMA convergence, composite swaps | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/Lightmap.html][Lightmap]] | Per-triangle texel state + double-buffered composite textures | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.html][LightmappedTriangle]] | Textured triangle whose texture is the GI composite | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/TriangleBvh.html][TriangleBvh]] | BVH over world triangles: nearest-hit and any-hit ray queries (Möller–Trumbore) | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.html][GiLightProvider]] | Cache-read interface feeding the flat-shading path | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightmappedCompositeShape.html][LightmappedCompositeShape]] | Wraps polygons into lightmapped triangles | diff --git a/doc/Mesh.svg b/doc/Mesh.svg deleted file mode 100644 index 8a20f46..0000000 --- a/doc/Mesh.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - triangulated - section - - diff --git a/doc/Near plane clip/Clip algorithm.svg b/doc/Near plane clip/Clip algorithm.svg deleted file mode 100644 index 1d09a7e..0000000 --- a/doc/Near plane clip/Clip algorithm.svg +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - behind (z ≤ near) - in front (z > near) - - - - near plane - - - - - - - - - - - - - - v0 - - v1 - - v2 - - - - p′ - - p″ - - - t = 0.5 along v0 → v2 - - - - - t = (near − z1) / (z2 − z1) - p = p1 + t·(p2 − p1) - uv = uv1 + t·(uv2 − uv1) - - every edge crossing the plane spawns an interpolated vertex; - in-front vertices pass through unchanged - diff --git a/doc/Near plane clip/Fan triangulation.svg b/doc/Near plane clip/Fan triangulation.svg deleted file mode 100644 index 94a8656..0000000 --- a/doc/Near plane clip/Fan triangulation.svg +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - v0 - - v1 - - p″ - - p′ - - - T1 = (v0, v1, p″) - T2 = (v0, p″, p′) - - - a triangle cut once - becomes a quad; - the rasterizer paints it - as a 2-triangle fan - sharing v0 - diff --git a/doc/Near plane clip/Near plane straddle.svg b/doc/Near plane clip/Near plane straddle.svg deleted file mode 100644 index aa2c9a6..0000000 --- a/doc/Near plane clip/Near plane straddle.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - z (depth) → - x ↓ - - - - - camera - - - - - near plane z = 1 - - - - - - - floor tiles - - - - - - kept fragment - cut away - - - old: one vertex behind ⇒ - whole tile dropped - - - - new: clip at the plane, - paint the surviving fragment - - diff --git a/doc/Near plane clip/index.org b/doc/Near plane clip/index.org deleted file mode 100644 index 0c59a70..0000000 --- a/doc/Near plane clip/index.org +++ /dev/null @@ -1,151 +0,0 @@ -#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme -#+TITLE: Near-Plane Clipping - Aukio 3D -#+LANGUAGE: en -#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} -#+LATEX_HEADER: \usepackage{parskip} -#+LATEX_HEADER: \usepackage[none]{hyphenat} - -#+OPTIONS: H:20 num:20 -#+OPTIONS: author:nil - -#+HTML_HEAD: - -[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] - -* The problem -:PROPERTIES: -:CUSTOM_ID: the-problem -:END: - -When the camera brushes against geometry — a floor tile under your -feet, a wall you lean into — part of a polygon can end up *behind* the -viewer while the rest stays in front. Perspective projection divides by -depth (=screenX = x / z=), so a vertex at z ≤ 0 has no meaningful screen -position at all. - -The naive way out — dropping any polygon that has even one vertex -behind the camera — makes whole tiles vanish exactly when they are -closest and largest on screen. Walking through the House demo, floor -tiles blinked out of existence at the bottom of the frame: - -#+attr_html: :class responsive-img -#+attr_latex: :width 640px -[[file:near-clip-before.png]] - -*Aukio 3D* instead *clips the polygon against the near plane* and -renders the surviving fragment. The same frame with clipping enabled — -the floor is solid to the bottom edge: - -#+attr_html: :class responsive-img -#+attr_latex: :width 640px -[[file:near-clip-after.png]] - -Think of the camera plane as the edge of a table and the polygon as a -sheet of paper partly hanging off it. Dropping the polygon means -throwing away the whole sheet. Clipping takes scissors, cuts the sheet -along the table edge, and keeps the part that lies on the table. - -#+INCLUDE: "Near plane straddle.svg" export html - -* Why not just clamp z? -:PROPERTIES: -:CUSTOM_ID: why-not-clamp-z -:END: - -A tempting one-liner is to force every vertex to =z = max(z, epsilon)= -and project anyway. It fails geometrically: a vertex at z = −50 clamped -to z = 0.01 projects to a screen coordinate thousands of pixels away, -*in the wrong direction* — the sign flip of the division mirrors it -through the camera. The polygon smears into giant streaks across the -frame instead of ending cleanly at the screen edge. - -Clipping produces the geometrically correct cut: the polygon's new edge -lies exactly on the near plane, and everything the rasterizer receives -has z > 0. - -* How the clipping works -:PROPERTIES: -:CUSTOM_ID: how-it-works -:END: - -Clipping happens in *camera space*, after the transform stack has moved -vertices relative to the viewer but *before* the perspective divide. -The vertex loop is walked edge by edge (Sutherland-Hodgman style) -against the plane =z = nearPlaneDistance=: - -1. An in-front vertex passes through unchanged. -2. An edge that crosses the plane spawns a new vertex at the - intersection, with position, UV and normal all interpolated with the - same parameter =t=. -3. A behind-plane vertex is skipped. - -#+INCLUDE: "Clip algorithm.svg" export html - -Interpolating UVs linearly along the 3D edge is exactly right for the -perspective-correct texture mapper: the intersection vertex is a real -point on the original edge, so its texture coordinate is the same blend -of the endpoints' UVs. Textured fragments therefore show the correct -texels right up to the cut, with no seam. - -Only a polygon with *all* vertices behind the plane is culled — the -legitimate version of the old behavior. - -* From clipped loop to pixels -:PROPERTIES: -:CUSTOM_ID: from-clip-to-pixels -:END: - -A convex N-gon crossing the plane clips to a single contiguous loop of -at most N+1 vertices. For the triangle-based rasterizers this means a -triangle can become a *quad*, which is painted as a two-triangle fan -sharing the first vertex — exact, because the clip of a convex polygon -stays convex: - -#+INCLUDE: "Fan triangulation.svg" export html - -Shape support: - -| Shape | Behavior when straddling | -|--------------------+---------------------------------------------------| -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Clipped loop painted as triangle fan | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | Fan-painted with interpolated UVs (also inherited by lightmapped GI fragments) | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] | Shortened to the in-front endpoint + intersection | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.html][Billboard]] | Single anchor point: culled when behind, as before | - -Implementation notes: - -- Clipped output is stored *per pipeline slot* on the shape - (=clippedVertices(ctx)=), so the triple-buffered pipeline can - transform frame N+1 while frame N is still painting. -- Depth sorting and tile binning use the clipped vertices' average Z - and screen bounds — a clipped tile sorts as the fragment it became, - not as the polygon that reached behind you. -- New intersection vertices exist only in camera space; they are - projected directly via - [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html#setCameraSpaceCoordinate(double,double,double,eu.svjatoslav.aukio.e3d.gui.RenderingContext)][Vertex.setCameraSpaceCoordinate()]], - bypassing the transform stack. - -* Configuration -:PROPERTIES: -:CUSTOM_ID: configuration -:END: - -The near plane distance is a per-context knob, in world units: - -#+BEGIN_SRC java -// Default is 1.0; smaller values let the camera press closer to -// geometry before the scissors bite, at the cost of larger projected -// coordinates for clipped fragments. -viewPanel.getRenderingContext().nearPlaneDistance = 0.5; -#+END_SRC - -* Related Classes -:PROPERTIES: -:CUSTOM_ID: related-classes -:END: - -| Class | Purpose | -|---------------------------+----------------------------------------------------------------| -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]] | Vertex-loop clipping in =transform()=, per-slot clip storage | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] | Camera-space projection for generated intersection vertices | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] | Carries =nearPlaneDistance= | diff --git a/doc/Near plane clip/near-clip-after.png b/doc/Near plane clip/near-clip-after.png deleted file mode 100644 index f135bd2..0000000 Binary files a/doc/Near plane clip/near-clip-after.png and /dev/null differ diff --git a/doc/Near plane clip/near-clip-before.png b/doc/Near plane clip/near-clip-before.png deleted file mode 100644 index 6b27c44..0000000 Binary files a/doc/Near plane clip/near-clip-before.png and /dev/null differ diff --git a/doc/Normal vector.svg b/doc/Normal vector.svg deleted file mode 100644 index 016136e..0000000 --- a/doc/Normal vector.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - N̂ - unit normal - (perpendicular - to surface) - - - Light - - L · N = brightness - diff --git a/doc/Perspective correct textures/Adaptive interval.svg b/doc/Perspective correct textures/Adaptive interval.svg deleted file mode 100644 index 924bc5c..0000000 --- a/doc/Perspective correct textures/Adaptive interval.svg +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - perspective curvature → rises toward the far end - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 16 px - 8 px - 4 px - 2 px - - one scanline, 100 px → - grazing-angle floor, far side - diff --git a/doc/Perspective correct textures/Affine distortion.png b/doc/Perspective correct textures/Affine distortion.png deleted file mode 100644 index 8d3722b..0000000 Binary files a/doc/Perspective correct textures/Affine distortion.png and /dev/null differ diff --git a/doc/Perspective correct textures/Scanline correction.svg b/doc/Perspective correct textures/Scanline correction.svg deleted file mode 100644 index cc5fc8d..0000000 --- a/doc/Perspective correct textures/Scanline correction.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - screen pixel → - texel u ↑ - - - - - - - - - - - - - - - - - - - - exact perspective - - corrected every 16 px - - plain affine - diff --git a/doc/Perspective correct textures/index.org b/doc/Perspective correct textures/index.org deleted file mode 100644 index 3438f01..0000000 --- a/doc/Perspective correct textures/index.org +++ /dev/null @@ -1,177 +0,0 @@ -#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme -#+TITLE: Perspective-Correct Textures - Aukio 3D -#+LANGUAGE: en -#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} -#+LATEX_HEADER: \usepackage{parskip} -#+LATEX_HEADER: \usepackage[none]{hyphenat} - -#+OPTIONS: H:20 num:20 -#+OPTIONS: author:nil - -#+HTML_HEAD: - -[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] - -* The problem -:PROPERTIES: -:CUSTOM_ID: introduction -:ID: a2b3c4d5-e6f7-8901-bcde-f23456789012 -:END: - -When a textured polygon is rendered at an angle to the viewer, naive -linear interpolation of texture coordinates produces visible -distortion. - -Consider a large textured floor extending toward the horizon. Without -perspective correction, the texture appears to "swim" or distort -because the texture coordinates are interpolated linearly across -screen space, not accounting for depth. - -#+attr_html: :class responsive-img -#+attr_latex: :width 1000px -[[file:Affine distortion.png]] - -The *Aukio 3D* engine solves this with *subdivided perspective -correction* inside the scanline rasterizer — the same technique Quake -used. - -* How perspective correction works -:PROPERTIES: -:CUSTOM_ID: how-perspective-correction-works -:END: - -Texture coordinates (u, v) are not linear in screen space, so they -cannot simply be stepped per pixel. But divide them by depth and they -become linear: *(u/z, v/z, 1/z) all interpolate linearly* across the -triangle in screen space. - -The rasterizer exploits this: - -1. Compute (u/z, v/z, 1/z) at each vertex -2. Interpolate all three across the scanline with plain additions -3. Every N pixels, recover the exact texture coordinate with one - division: =u = (u/z) / (1/z)= -4. Between correction points, step u/v affinely toward the next exact - point - -#+INCLUDE: "Scanline correction.svg" export html - -The orange polyline hugs the exact green curve: within each 16-pixel -block it is a straight line, but every block starts exactly on the -curve. The dashed pink line is plain affine interpolation — visibly -wrong everywhere except the endpoints. - -Think of it like walking with a map that is slightly distorted: you -walk in a straight line, but every 16 steps you check a landmark and -correct your course. The correction (a division) costs something, so -you do it every N pixels instead of every pixel — the divide cost is -amortized to 1/16th of a per-pixel-correct rasterizer. - -#+BEGIN_SRC java -// Per scanline (simplified from drawHorizontalLinePerspective): -while (done < span) { - // Advance (u/z, v/z, 1/z) to the end of this block - su += dsu * block; sv += dsv * block; sw += dsw * block; - double txNext = su / sw; // one reciprocal = exact texture position - double tyNext = sv / sw; - - // Step affinely through the block - double txStep = (txNext - tx) / block; - double tyStep = (tyNext - ty) / block; - for (int i = 0; i < block; i++) { - plot(x++, texture.sample(tx, ty)); - tx += txStep; ty += tyStep; - } - done += block; -} -#+END_SRC - -** Adaptive correction interval - -Quake used a fixed 16-pixel interval. This engine keeps 16 as the -default but *shrinks the interval when the error bound demands it*. - -The error of affine stepping within a block grows with both the -texture gradient (texels per pixel) and the perspective curvature -(how fast 1/z changes across the span). Each scanline computes - -#+BEGIN_EXAMPLE -error(texels) ≈ texelRate · interval² · k / 2 -#+END_EXAMPLE - -where =k = |d(1/z)| / min(1/z)= is the per-pixel relative depth change, -and picks the largest power-of-two interval from the ladder 16, 8, 4, -2, 1 that keeps the bound under half a texel. Flat, gently angled -spans keep the fast 16-pixel cadence; a floor tile seen at a grazing -angle drops to shorter intervals exactly where the curvature is high. - -#+INCLUDE: "Adaptive interval.svg" export html - -** When affine is good enough - -For small or nearly flat triangles, plain affine mapping is already -within half a texel of exact perspective, so the perspective setup is -skipped entirely. The test compares the texture range the triangle -covers against its depth variation: - -#+BEGIN_EXAMPLE -affine is sufficient when texelSpan · (zMax/zMin − 1) < 2 -#+END_EXAMPLE - -Note the criterion is the *texel* span, not the pixel size — a tiny -on-screen triangle can still map many texels into few pixels. Distant -clusters of small triangles (a common case) all render through the -cheaper affine path. - -Triangles straddling the near plane (any vertex closer than z = 0.001) -also fall back to affine, because 1/z interpolation is invalid there. - -** Toggling the correction - -Perspective correction can be switched off globally for A/B comparison -or debugging: - -#+BEGIN_SRC java -TexturedTriangle.setPerspectiveCorrectionEnabled(false); // plain affine everywhere -#+END_SRC - -With correction disabled, large triangles at steep angles visibly warp -— useful for demonstrating what the correction actually buys. - -* Mipmap selection -:PROPERTIES: -:CUSTOM_ID: mipmap-selection -:END: - -Perspective correction fixes *where* a texel is sampled; mipmapping -decides *which resolution* to sample from. Each triangle estimates its -screen-pixels-per-texel ratio from edge lengths: - -#+BEGIN_EXAMPLE -scaleFactor = (sum of screen edge lengths) / (sum of UV edge lengths) · 1.2 -#+END_EXAMPLE - -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html#getMipmapForScale(double)][Texture.getMipmapForScale()]] -then picks the lazily-generated mipmap level closest to that scale: -halved resolutions when the texture is minified, doubled when strongly -magnified. Sampling a smaller mipmap under minification both speeds up -rendering (better cache behavior) and reduces aliasing. - -* Related Classes -:PROPERTIES: -:CUSTOM_ID: related-classes -:END: - -| Class | Purpose | -|-------------------------------+--------------------------------------------------------------------| -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | Textured triangle with perspective-correct and SDF rendering paths | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PerspectiveBorderInterpolator.html][PerspectiveBorderInterpolator]] | Edge walker interpolating (u/z, v/z, 1/z) along triangle borders | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.html][PolygonBorderInterpolator]] | Edge walker for plain affine mapping | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html][Texture]] | Mipmap container; also carries the SDF mask and color layers | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html][TextureBitmap]] | Raw pixel array for one mipmap level | - -*See also:* - -- [[file:../SDF textures/][SDF textures]] — signed-distance-field glyph rendering, the - alternative sampling path in =TexturedTriangle= that reuses the same - perspective-correct interpolation for crisp text at any angle. diff --git a/doc/Point3D vertex.svg b/doc/Point3D vertex.svg deleted file mode 100644 index 0954bac..0000000 --- a/doc/Point3D vertex.svg +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - -Point3D -raw coordinates - - - - - - -(x, y, z) - - - - - -.getDistanceTo() - - - - -.rotate() - - - - -.add() -.subtract() - - - - -.crossProduct() - - - - -.unit() -.dot() - - - - -.multiply() - - -Mutable, fluent API -Positions, vectors, math - - -Vertex -rendering-ready wrapper - - - - - - - -coordinate -Point3D - - - -wraps - - - -transformedCoordinate - - - -onScreenCoordinate - - - -textureCoordinate -UV - - - -normal -for CSG - - -local -camera -space -2D -pixels - - - -local -screen - - - -Tracks position across coordinate spaces - diff --git a/doc/Rendering loop/CPU scheduling.png b/doc/Rendering loop/CPU scheduling.png deleted file mode 100644 index 12aa23d..0000000 Binary files a/doc/Rendering loop/CPU scheduling.png and /dev/null differ diff --git a/doc/Rendering loop/Double buffering.svg b/doc/Rendering loop/Double buffering.svg deleted file mode 100644 index 141dad6..0000000 --- a/doc/Rendering loop/Double buffering.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - Without double-buffering - - - display shows partial update - - - - old frame - - - - ← tear - - - - new frame - - - With double-buffering - - - - Back buffer - (draw here) - - - - - - - - - swap - - - - Front buffer - (displayed) - - - complete - frame - diff --git a/doc/Rendering loop/Paint tiles.svg b/doc/Rendering loop/Paint tiles.svg deleted file mode 100644 index e27d5f8..0000000 --- a/doc/Rendering loop/Paint tiles.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - one shape - - ~10 tiles per thread; threads steal pending - tiles — no fixed thread↔tile assignment - diff --git a/doc/Rendering loop/Painter's algorithm.svg b/doc/Rendering loop/Painter's algorithm.svg deleted file mode 100644 index 7727fc2..0000000 --- a/doc/Rendering loop/Painter's algorithm.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - Far (Z=500) — painted first - - - Medium (Z=300) — painted second - - - Near (Z=100) — painted last - diff --git a/doc/Rendering loop/Render pipeline.svg b/doc/Rendering loop/Render pipeline.svg deleted file mode 100644 index 927e357..0000000 --- a/doc/Rendering loop/Render pipeline.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - Shapes - - - Transform - - - Sort - - - Bin - - - Paint - - - Present - - - Screen - - - - - - - - - - - 3D vertices - world→screen - back-to-front - per tile - tile grid - own thread - diff --git a/doc/Rendering loop/index.org b/doc/Rendering loop/index.org deleted file mode 100644 index c7fe2e0..0000000 --- a/doc/Rendering loop/index.org +++ /dev/null @@ -1,523 +0,0 @@ -#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme -#+TITLE: Rendering Loop - Aukio 3D -#+LANGUAGE: en -#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} -#+LATEX_HEADER: \usepackage{parskip} -#+LATEX_HEADER: \usepackage[none]{hyphenat} - -#+OPTIONS: H:20 num:20 -#+OPTIONS: author:nil - -#+HTML_HEAD: - -[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] - -* Rendering loop -:PROPERTIES: -:CUSTOM_ID: rendering-loop -:ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 -:END: - -The rendering loop is the heart of the engine, continuously generating -frames on a dedicated background thread. It orchestrates the entire -rendering pipeline from 3D world space to pixels on screen. - -** What is a render loop? -:PROPERTIES: -:CUSTOM_ID: what-is-a-render-loop -:END: - -A *render loop* is a continuous process that generates visual frames -from 3D data. Think of it like a movie camera: each "frame" captures -the current state of the 3D world and converts it into a 2D image that -can be displayed on screen. - -The process transforms shapes through multiple coordinate systems: - -#+INCLUDE: "Render pipeline.svg" export html - -Each step has a specific purpose: - -| Step | Input | Output | Purpose | -|-----------+-------+--------+---------| -| Shapes | 3D [[file:../index.org::#vertex][vertices]], [[file:../index.org::#mesh][meshes]] | Scene data | Objects waiting to be drawn | -| Transform | World coordinates | Screen coordinates | Convert 3D positions to where they appear on screen (see [[file:../index.org::#coordinate-system][coordinate system]]); cull shapes outside the view frustum. Parallel: heavy subtrees fork onto the worker pool | -| Sort | Unordered shapes | Ordered by depth | Ensure correct visibility (far objects painted first). Parallel merge sort for large scenes | -| Bin | Sorted shapes | Per-tile shape lists | Each paint tile iterates only shapes that can touch it. Parallel over the worker pool | -| Paint | Per-tile shape lists | Pixels in buffer | Tiles split the screen into independent work units so clearing and rasterization run in parallel across CPU cores | -| Present | Pixel buffer | Screen image | Hand the completed frame to a dedicated thread that copies it to the display | - -This pipeline runs repeatedly, targeting 60 frames per second by -default. Even if nothing moves, the loop continues running—but the -engine [[#frame-listeners][skips unnecessary work]] when the scene is static. - -The steps above describe one frame *logically*, in the order data flows -through it. In execution the engine is a software pipeline: transform -of the next frame already runs while the previous frame is still being -painted, and presentation happens on its own thread. See -[[#software-pipeline][Software pipeline]]. - -** Main loop structure -:PROPERTIES: -:CUSTOM_ID: main-loop-structure -:END: - -The engine runs two dedicated daemon threads: - -- =e3d-render= — produces frames. It runs continuously: - -#+BEGIN_SRC java -while (renderThreadRunning) { - ensureThatViewIsUpToDate(); // Produce one frame (or skip) - maintainTargetFps(); // Sleep if ahead of schedule -} -#+END_SRC - -- =e3d-present= — presents frames. It takes completed frames from a - mailbox and performs all display-path work (the multi-megabyte - =drawImage=, =BufferStrategy.show()= and the X server round-trip), so - the render thread never blocks on the display. - -Both threads are daemons, so they stop automatically when the JVM -exits. You can stop them explicitly with [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#stop()][ViewPanel.stop()]]. - -** Frame rate control -:PROPERTIES: -:CUSTOM_ID: frame-rate-control -:END: - -The engine supports two modes: - -- *Target FPS mode*: Set with [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#setFrameRate(int)][setFrameRate(int)]]. - The engine tries to maintain the target rate by sleeping between frames. - - - *When rendering is slower than target*: No sleeping occurs. The engine - runs at maximum hardware speed. Missed frames are skipped, not - rendered later — the timing simply resets to current time. - - - *When rendering is faster than target*: The thread sleeps to limit FPS - to the target rate, avoiding unnecessary CPU usage. - - For example, with a 60 FPS target: - - If a complex scene takes 30ms per frame, you get ~33 FPS (hardware limit) - - If the scene later simplifies to 10ms per frame, you get exactly - 60 FPS (throttled by sleeping) - -- *Unlimited mode*: Set =setFrameRate(0)= or negative. No sleeping — - renders as fast as possible, and frames are produced even when the - scene reports no changes, so the measured rate reflects maximum - achievable throughput. Useful for benchmarking. - -*Production vs presentation.* These are measured separately: - -- *Production rate* (=getMeasuredFPS()=) counts frames the pipeline - completes per second. This is the benchmark number. -- *Presentation rate* is how fast frames actually reach the screen. In - capped-FPS mode the present thread is paced to 60 blits per second - (override with =-Daukio3d.presentRate=N=); the cap exists because the - X server also dispatches input, and flooding it with blits causes - desktop-wide mouse/keyboard jitter. When production outruns - presentation, stale frames are dropped from the mailbox instead of - piling up latency. In unlimited (benchmark) mode presentation pacing - is disabled entirely, so it cannot throttle production. - -* Software pipeline -:PROPERTIES: -:CUSTOM_ID: software-pipeline -:END: - -The phases below are described per frame, but consecutive frames -*overlap*. The engine triple-buffers everything a frame writes: - -- 3 framebuffers (each with its own =RenderingContext=) -- 3 projection buffer slots (per-vertex screen state) -- 3 render aggregators (transform output, sort/bin state) - -A render pass P (one per frame, or one per eye in stereo) transforms -into slot P mod 3, so it only conflicts with the paint of pass P-3. -Before each transform the render thread *flushes* completed paint -passes (mouse hits, frame deposit) and blocks only if paint P-3 is -still running — which steady-state worker throughput prevents. Workers -finishing one pass's tiles flow straight into the next pass's queued -tiles with no idle gap. - -Completed frames go to a *presentation mailbox* that keeps only the -newest frame: if the display path is slower than production, stale -frames are dropped (and their buffers released) instead of -accumulating latency — swapchain "mailbox mode". - -A per-buffer *present gate* guarantees painting frame F+3 never -overwrites a buffer the present thread is still blitting frame F from. - -The goal of all this overlap is throughput: keep every CPU core busy, -all the time. No phase waits for another phase of the same frame when -it could already be working on the next one. The Developer Tools -thread-activity timeline shows it working — all 18 worker rows packed -solid with paint, bin and sort tasks from up to three frames at once, -while the render thread (top row) and present thread tick along above -them: - -#+attr_html: :class responsive-img -[[file:CPU scheduling.png]] - -The pipeline can be disabled with =-Daukio3d.pipeline=false=, restoring -strictly sequential phase order (each paint pass is awaited -immediately). This is a kill switch for benchmarking and regression -hunting. - -* Rendering phases -:PROPERTIES: -:CUSTOM_ID: rendering-phases -:END: - -Each frame goes through 6 phases. Phases 2–4 run inside an -asynchronous continuation on the shared worker pool, and phases of -consecutive frames overlap as described in [[#software-pipeline][Software pipeline]]. - -** Phase 1: Transform shapes -:PROPERTIES: -:CUSTOM_ID: phase-1-transform-shapes -:END: - -All shapes are transformed from world space to screen space: - -1. Build camera-relative transform (inverse of camera position/rotation) -2. Update the view frustum from camera state and viewport dimensions -3. Walk the scene tree: - - Cull composite shapes whose bounding box misses the frustum - - Apply camera transform - - Project 3D → 2D (perspective projection) - - Calculate depth for sorting - - Queue for rendering - -*What is coordinate transformation?* - -Every shape exists in "world space" — its own position in the 3D world. -To render it, we must convert to "screen space" — where it appears on -your monitor. This involves: - -- *Translation*: Move coordinates relative to camera position -- *Rotation*: Rotate coordinates based on camera orientation -- *Projection*: Convert 3D (x, y, z) to 2D (x, y) screen pixels - -Objects further away appear smaller (perspective). The [[file:../index.org::#coordinate-system][coordinate system]] -uses Y-down to match screen conventions, making projection straightforward. - -The transform is *parallel and non-blocking*: composites with enough -children fork their render lists into chunk tasks on the shared worker -pool (at any nesting level), and the render thread returns without -waiting. The chunk tasks are drained and merged on a worker thread -inside the paint continuation, while the render thread is already -walking the next pass. - -*Frustum culling* happens here: composites test their bounding box -against the frustum and skip invisible subtrees entirely, saving both -transform and paint work. Per-frame culling statistics are collected -for the developer tools panel. - -** Phase 2: Sort shapes by depth -:PROPERTIES: -:CUSTOM_ID: phase-2-sort-shapes -:END: - -Shapes are sorted by depth in descending order (farthest first), with -the shape id as a deterministic tiebreaker: - -#+BEGIN_SRC java -// ShapesZIndexComparator: descending Z, ties broken by shape id -if (z1 < z2) return 1; // z1 is nearer -> sort after z2 -else if (z1 > z2) return -1; // z1 is farther -> sort before z2 -return Integer.compare(o1.shapeId, o2.shapeId); -#+END_SRC - -Above 8192 queued shapes the sort runs as an instrumented parallel -merge sort on the shared worker pool; below that it is single-threaded. - -*Why sort back-to-front?* - -This implements the *painter's algorithm* — like painting a landscape: -first paint the sky (farthest), then mountains, then trees, then the -foreground. Each layer covers what's behind it. - -#+INCLUDE: "Painter's algorithm.svg" export html - -Without sorting, nearby objects might be painted first and then covered -by distant ones, causing visual errors. This is especially important for -*transparent objects* — you need to see through the near ones to what's -behind. - -The Z value represents distance from the camera after transformation. -Larger values = further away. The id tiebreaker keeps the order -deterministic frame-to-frame, which tiled rendering relies on: every -tile paints its shapes in the same global (Z, id) order. - -** Phase 3: Bin shapes into tiles -:PROPERTIES: -:CUSTOM_ID: phase-3-bin-shapes-into-tiles -:END: - -The sorted queue is binned per paint tile by screen-space overlap: -each tile's bin lists only the shapes whose vertex bounds (plus a -paint margin) can touch that tile. A shape overlapping several tiles -is added to each of their bins. - -This means a paint thread iterates a short local list instead of the -whole scene, and it is what makes the tile grid scale: refining the -grid shrinks each bin instead of just subdividing the clearing work. - -Binning is parallelized over the shared worker pool. - -** Phase 4: Clear and paint tiles (multi-threaded) -:PROPERTIES: -:CUSTOM_ID: phase-4-clear-paint-tiles -:END: - -The viewport is divided into a grid of rectangular *tiles* — roughly -10 tiles per render thread, split into near-squares (square tiles -minimize boundary crossings, i.e. how many tiles each shape overlaps). -These are *not* horizontal bands: each tile has both X and Y bounds. - -#+INCLUDE: "Paint tiles.svg" export html - -Painting is work-stolen, not pre-assigned. All tile tasks go onto a -shared =ForkJoinPool= (sized to 75% of CPU threads by default, at most -cores − 1, so one thread stays free for the rest of the system; -changeable at runtime via =setNumRenderThreads(int)=). A worker that -finishes a cheap tile immediately pulls the next queued task — another -tile (of this or an adjacent frame's pass), a transform chunk, a sort -piece — so cores never idle behind a busy thread. - -Each tile task: - -1. *Clear tile*: fill its rectangle with background color -2. *Paint shapes*: rasterize the tile's bin, back-to-front, clipping - at tile bounds - -Both operations happen within the same task, so clearing always -completes before painting on that tile. Parallel clearing across -disjoint tiles maximizes memory bandwidth utilization. - -Each tile renders through a =SegmentRenderingContext= — a view of the -frame context carrying the tile's X/Y bounds and a =Graphics2D= -pre-clipped to the tile rectangle for thread-safe text and -anti-aliased drawing. (The class name predates the tile grid; a -"segment" is now a tile.) Mouse hit detection happens during painting, -before clipping. - -A =CountDownLatch= tracks completion of all the pass's tiles — but the -render thread does *not* wait for it here. The latch is awaited one -pass later, during the flush (see [[#phase-5-flush-completed-passes][Phase 5]]). - -** Phase 5: Flush completed passes -:PROPERTIES: -:CUSTOM_ID: phase-5-flush-completed-passes -:END: - -Before each new transform, the render thread flushes paint passes that -have completed. For each flushed pass: - -1. Await its tile latch (blocks only when correctness demands it — - transform of pass P may not start before paint of pass P-3 finished) -2. *Combine mouse results*: during painting, each tile tracked which - shape is under the mouse cursor. Since all tiles paint the same - back-to-front order, they should all report the same hit; the first - non-null result wins: - -#+BEGIN_SRC java -for (SegmentRenderingContext ctx : segmentContexts) { - if (ctx.getSegmentMouseHit() != null) { - context.setCurrentObjectUnderMouseCursor(ctx.getSegmentMouseHit()); - return; - } -} -#+END_SRC - - In stereo mode this only runs for the eye whose viewport actually - contains the cursor — each eye sees a different camera position, so - combining for the wrong eye would overwrite a valid hit with null. -3. If this pass completed a frame, deposit the frame into the - presentation mailbox (see [[#software-pipeline][Software pipeline]]). The render - thread never blocks on the display. - -Passes that finished painting are flushed without any blocking, so -completed frames reach the mailbox as early as possible. - -** Phase 6: Present frame -:PROPERTIES: -:CUSTOM_ID: phase-6-present-frame -:END: - -The =e3d-present= thread takes the newest mailbox frame (dropping any -unshown older frame) and copies its =BufferedImage= to the screen using -[[https://cr.openjdk.org/~iris/se/17/latestSpec/api/java.desktop/java/awt/image/BufferStrategy.html][BufferStrategy]] for tear-free page-flipping: - -#+BEGIN_SRC java -do { - Graphics2D g = bufferStrategy.getDrawGraphics(); - g.drawImage(context.bufferedImage, 0, 0, null); - g.dispose(); -} while (bufferStrategy.contentsRestored()); - -// framebuffer released for reuse here -bufferStrategy.show(); -Toolkit.getDefaultToolkit().sync(); -#+END_SRC - -The frame's buffer is released for reuse right after the =drawImage= -loop — =show()= and =sync()= touch only the BufferStrategy's own back -buffer and the X connection, and at high resolutions they cost more -than the draw itself, so the next frame's painters don't wait for them. - -*What is double-buffering?* - -Without double-buffering, the screen updates while pixels are being -written. This causes *screen tearing* — visible horizontal splits where -the top of the frame shows old content while the bottom shows new. - -#+INCLUDE: "Double buffering.svg" export html - -Double-buffering uses two pixel buffers: -- *Back buffer*: Where rendering happens (offscreen, invisible) -- *Front buffer*: What's currently displayed on screen - -When rendering completes, the buffers *swap* in one atomic operation. -The viewer always sees complete frames, never partial updates. - -The =do-while= loop handles the case where the OS recreates the back -buffer (common during window resizing). Since our offscreen -=BufferedImage= still has the correct pixels, we only need to re-blit, -not re-render. - -* Frame listeners and smart repaint skipping -:PROPERTIES: -:CUSTOM_ID: frame-listeners -:ID: e360a877-cca6-4cba-a9a4-ea40b0f1a183 -:END: - -A *FrameListener* is a callback that runs custom logic before each potential -frame. Think of it as your "per-frame hook" — the engine calls all registered -listeners, giving them a chance to update animations, physics, or game logic. - -** Registering a frame listener - -Use [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#addFrameListener(eu.svjatoslav.aukio.e3d.gui.FrameListener)][addFrameListener()]] to register your callback: - -#+BEGIN_SRC java -// This is how you register a frame listener -viewPanel.addFrameListener((panel, deltaMs) -> { - // Example: simple animation listener - double rotationSpeed = 1.0; // radians per second - shape.rotate(rotationSpeed * deltaMs / 1000.0); // Framerate-independent rotation - return true; // Request repaint (shape moved) -}); -#+END_SRC - -The listener receives two parameters: -- =panel=: The ViewPanel that's rendering -- =deltaMs=: Milliseconds since last frame (for framerate-independent animation) - -The return value controls whether the frame gets rendered: -- =true=: "Something changed — repaint the screen" -- =false=: "Nothing changed — can skip this frame" - -** Frame skipping optimization - -The engine avoids unnecessary rendering. A frame is skipped when: -- *All listeners return false* (nothing changed in your scene) -- *Camera did not move* (built-in Camera listener returns false once - the camera comes to rest) -- *No resize or repaint requests* - -This means a static scene with no animations consumes almost zero CPU. -The render thread keeps running (checking for changes), but actual pixel -rendering is skipped entirely. Skipped frames still flush any pending -paint passes from earlier frames, so in-flight frames always reach the -screen. - -Two exceptions force a frame regardless of listeners: -- *Unlimited (benchmark) mode* (=targetFPS <= 0=) renders continuously, - so the measured rate reflects maximum throughput -- An explicit repaint request (resize, stereo toggle, - =repaintDuringNextViewUpdate()=, etc.) - -#+BEGIN_SRC java -// Example: listener that only requests repaint when needed -viewPanel.addFrameListener((panel, deltaMs) -> { - if (gameState.hasUpdates()) { - gameState.processUpdates(); - return true; // Only repaint when game state actually changed - } - return false; // Skip frame — nothing to update -}); -#+END_SRC - -** Built-in listeners - -The engine registers these listeners by default: -- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/Camera.html][Camera]] — applies movement velocity and friction each frame, and - returns true when the camera actually moved (more than a small - threshold), i.e. while the user is actively navigating -- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.html][InputManager]] — processes mouse/keyboard events - -When the camera stops moving and you release all keys, the Camera listener -returns false. If your custom listeners also return false, the frame is -skipped until something changes. - -* Rendering context -:PROPERTIES: -:CUSTOM_ID: rendering-context -:END: - -The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] holds all state for rendering into one framebuffer: -the pixel buffer, projection parameters, and per-frame bookkeeping. - -| Field | Purpose | -|-------+---------| -| =pixels[]= | Raw pixel buffer (int[] in RGB format) | -| =bufferedImage= | Java2D wrapper around pixels | -| =graphics= | Graphics2D for text, lines, shapes | -| =width=, =height= | Full framebuffer dimensions | -| =centerCoordinate= | Screen center of the active viewport (for projection) | -| =projectionScale= | Perspective scale factor, derived from viewport width. Mutable: each stereo eye sets its own | -| =renderMinX=, =renderMaxX= | X bounds of the active viewport or tile | -| =renderMinY=, =renderMaxY= | Y bounds (full height on the frame context, tile bounds on segment views) | -| =stereoEye=, =stereoViewportWidth=, =stereoViewportOffsetX= | Which eye this pass renders and where its viewport sits in the buffer | -| =tilesX=, =tilesY=, =viewportCount=, =numRenderSegments= | Tile grid geometry (segments = tilesX × tilesY × viewports) | -| =frustum= | View frustum for culling, rebuilt each pass from camera state | -| =frameNumber= | Per-context frame counter | -| =transformCycleId= | Globally unique transform-cycle id, safe key for per-cycle memoization | -| =vertexSlot= | Projection buffer slot (0–2) this pass transforms into | - -** Triple-buffered frame contexts - -The engine keeps /three/ frame contexts, cycled by frame parity. While -frame N is still being painted from one buffer, frame N+1 already -transforms into the next — paint threads never idle waiting for the -transform phase, and vice versa. A per-buffer *present gate* prevents -painting frame F+3 into a buffer the present thread is still blitting -frame F from. - -All three contexts are recreated together when the window is resized, -when the tile grid changes (render thread count), or when stereo mode -is toggled. Otherwise they are reused — =prepareForNewFrameRendering()= -just resets per-frame state like mouse tracking. - -** Per-pass copies - -Each render pass (one per eye in stereo) works on a private /copy/ of -the frame context. The copy shares the pixel buffer, graphics and -services, but owns the projection fields (center, scale, viewport, -vertex slot), so the next pass's setup cannot disturb a pass whose -transform or paint is still in flight. - -Consequence for engine code: per-frame mutable state must be allocated -eagerly on the frame context. Anything created lazily inside a pass -lands on the throwaway copy and is lost. - -** Tile segment views - -Each paint tile gets a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/SegmentRenderingContext.html][SegmentRenderingContext]], -a view that shares the framebuffer with its parent but carries its own -X/Y tile bounds and a pre-clipped =Graphics2D= for thread-safe text and -shape drawing. Mouse hits are tracked per tile and combined after all -tiles finish painting. diff --git a/doc/SDF textures/SDF concept.svg b/doc/SDF textures/SDF concept.svg deleted file mode 100644 index bc1b750..0000000 --- a/doc/SDF textures/SDF concept.svg +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - Why a distance field, not a bitmap - one glyph edge, magnified 8x - stored coverage vs re-derived coverage - - - BITMAP coverage - what you store is what you get - - - - - - - - - - - - - - - - - - - - - - - - - - - - texel grid IS the resolution limit: - edges stair-step, curves become blocks - - - SDF: distance to edge - a smooth field - the edge is re-derived per pixel - - - - - - - - - - - - - - - - edge recovered at - display resolution - - - d < 0: inside ink - d > 0: outside - d = 0: the edge - - - - coverage = (127.5 - d) * aaK + 128 - aaK scales the gradient window to the pixel footprint - the same 16x32 texel field - serves a 4-pixel label and a full-screen billboard - diff --git a/doc/SDF textures/SDF glyph pipeline.svg b/doc/SDF textures/SDF glyph pipeline.svg deleted file mode 100644 index 09473d6..0000000 --- a/doc/SDF textures/SDF glyph pipeline.svg +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - Glyph field generation (SdfGlyphCache) - once per character, then cached - stamping is a block copy - - - - 1. rasterize glyph - Liberation Mono Bold, AA on - 64x128 px (4x supersample) - font auto-sized to fit cell - advance 0.6em (Courier-compat) - - - - - - 2. distance transform - exact Euclidean EDT - (Felzenszwalb-Huttenlocher, - two separable 1-D passes) - dOut to ink, dIn to background - - - - - - 3. sign, clamp, average - signed = dOut - dIn - clamp to +/- 2 texels spread - average FIELD down to 16x32 - (averaging the field, not coverage, - preserves the edge position) - - - - - - - mask encoding (per texel, 0..255) - 0 = deep inside ink 127.5 = the edge 255 = far outside - - - - - - - - - - - - - - - - - TextCanvas.putChar - stamps the cached 16x32 mask - into the cell position of sdfMask - - - three texture layers - sdfMask: glyph SHAPES (bilinear) - sdfForeground + primary: colors - - why sans-serif bold: Courier's serifs and hairline strokes decay into unresolvable noise - when the distance field is minified - uniform sturdy strokes survive - - - - NO mipmaps on the mask: the edge gradient spans ~2 texels, - half-res masks melt glyph edges - minification is analytic instead - diff --git a/doc/SDF textures/SDF minification.svg b/doc/SDF textures/SDF minification.svg deleted file mode 100644 index 95cce51..0000000 --- a/doc/SDF textures/SDF minification.svg +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - Minification: analytic coverage window - one screen pixel covering many texels still resolves the edge correctly - - - - a screen pixel on the texture - - - - - - - - - - - - - - - - - - - footprint: many texels per pixel - a bitmap would average to mush or alias; - the field still knows where the edge is - - - - per-axis footprint from UV gradients - footX = |dUV/dx|, footY = |dUV/dy| - window follows the SHARPEST axis - - - - - aaK = 2*spread / texelsPerPixel - widens the coverage window as pixels grow - - - - - perceptual corrections (minified text - else reads as gray haze): - SHARPEN x2: sub-pixel window kills halo - coverage gamma < 1: stem darkening - - - - result: graceful degradation - magnified: edges re-derived at display resolution - razor sharp - minified: coverage fades smoothly to clean gray, no crawling aliases - angled: the uncompressed axis keeps its sharpness - diff --git a/doc/SDF textures/glyph-sdf-S.png b/doc/SDF textures/glyph-sdf-S.png deleted file mode 100644 index ecc1de5..0000000 Binary files a/doc/SDF textures/glyph-sdf-S.png and /dev/null differ diff --git a/doc/SDF textures/index.org b/doc/SDF textures/index.org deleted file mode 100644 index ff4cf78..0000000 --- a/doc/SDF textures/index.org +++ /dev/null @@ -1,252 +0,0 @@ -#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme -#+TITLE: SDF Textures - Aukio 3D -#+LANGUAGE: en -#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} -#+LATEX_HEADER: \usepackage{parskip} - -[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] - -* What SDF textures are -:PROPERTIES: -:CUSTOM_ID: what-sdf-is -:END: - -A regular texture stores *coverage*: each texel says "this much ink -here". That is a photocopy of the glyph — resample it (magnify, minify, -view at an angle) and the stored pixels blur or alias, because the -information about /where the edge is/ was thrown away when the glyph -was rasterized. - -A *signed distance field* (SDF) texture stores something smarter: per -texel, the *distance to the nearest edge* — negative inside the ink, -positive outside, zero exactly on the boundary. The rasterizer then -re-derives coverage per screen pixel from this smooth field. The edge -position survives resampling because the field around it is linear — -bilinear interpolation of a linear ramp is exact. - -#+ATTR_HTML: :width 640 -[[file:SDF concept.svg]] - -In Aukio 3D the mask is a grayscale field in =texture.sdfMask=: - -- =0= — deep inside the ink -- =127.5= — exactly on the edge -- =255= — far outside any glyph - -The gradient spans only =SPREAD_TEXELS = 2.0= texels around the edge — -that narrow band is all the rasterizer needs. - -Here is a real field, dumped straight from =SdfGlyphCache= (glyph "S", -16x32 texels, upscaled 12x with nearest so you can see the texels): - -[[file:glyph-sdf-S.png]] - -Dark inside the strokes, bright outside, and a smooth gray ramp exactly -two texels wide around the contour. - -* Generating glyph fields -:PROPERTIES: -:CUSTOM_ID: glyph-pipeline -:END: - -=SdfGlyphCache= generates each character's distance field once and -caches it in a =ConcurrentHashMap=; stamping a glyph into a canvas is -then just a block copy. - -#+ATTR_HTML: :width 640 -[[file:SDF glyph pipeline.svg]] - -The steps: - -1. *Rasterize* the glyph with AWT at 4x the cell size (64x128 pixels) - with anti-aliasing on, using Liberation Mono Bold (metric-compatible - with Courier New, so the cell grid is unchanged). The font size is - auto-shrunk until the widest glyph fits the scratch without clipping - — a clipped glyph would corrupt the distance field at the cell edge. -2. *Distance transform*: an exact Euclidean distance transform - (Felzenszwalb & Huttenlocher, two separable 1-D passes over parabola - envelopes) is run twice — once for distance to nearest ink pixel, - once for distance to nearest background pixel. -3. *Sign, clamp, average*: signed distance = dOut - dIn, clamped to - +/-2 texels of spread, then the *field* (not coverage) is averaged - down to the 16x32 cell resolution. Averaging the field preserves the - edge position; averaging coverage would not. - -The font choice matters: Courier's serifs and hairline strokes decay -into unresolvable noise when the field is minified. A uniform-stroke -bold sans-serif survives. - -* The rendering path -:PROPERTIES: -:CUSTOM_ID: render-path -:END: - -When =texture.isSdf()= is true (an =sdfMask= is attached), -=TexturedTriangle.paintSdf= takes over. Three layers are involved: - -| Layer | Contents | Sampling | -|------------------+-----------------------------+----------| -| =sdfMask= | glyph shapes (the field) | bilinear | -| =sdfForeground= | ink color, flat per cell | nearest | -| =primaryBitmap= | background color, per cell | nearest | - -Per screen pixel: - -1. Sample the mask bilinearly (fixed-point) -> distance =d=. -2. Convert to coverage: =cov = (127.5 - d) * aaK + 128=, clamped to - [0, 256]. =aaK= scales the 2-texel gradient window to the current - pixel footprint (see next section). -3. Blend: =pixel = bg * (1 - cov) + fg * cov=. - -Perspective-correct interpolation applies to SDF triangles exactly as -it does to regular textured triangles — same affine-sufficiency test, -same subdivided correction. See -[[file:../Perspective correct textures/index.org][Perspective-correct -textures]]; only the per-pixel sampling differs. - -* Minification without mipmaps -:PROPERTIES: -:CUSTOM_ID: minification -:END: - -*There is deliberately no mipmap chain for SDF layers.* A distance -field's edge gradient spans ~2 texels; a half-resolution mask melts the -glyph edges. Worse, the two triangles of a rectangle cross mip -thresholds at slightly different distances, producing a hard diagonal -quality split and sudden blur steps while dollying (observed in -practice). - -Minification is instead handled *analytically*: the coverage window is -widened by the screen-space pixel footprint, giving area-correct -coverage straight from the primary field. - -#+ATTR_HTML: :width 640 -[[file:SDF minification.svg]] - -The footprint is computed per axis from the screen-space UV gradients — -=text on an angled plane is minified mostly along one axis=, and an -isotropic average would blur the axis that still has resolution to -spare. The coverage window follows the sharpest axis. - -Area-correct coverage alone reads as a low-contrast gray haze, so two -perceptual corrections (A/B-tuned on far + angled text) kick in under -minification: - -- *Sharpening* (=SDF_SHARPEN=, default 2): narrows the coverage window - below one pixel — kills the haze halo at the cost of slight shimmer. -- *Coverage gamma* (< 1, automatic from the footprint): darkens stems - like a small-size font rasterizer, keeping thin strokes present. - -Real output, rendered headlessly through the [[file:../index.org::#snapshot][Snapshot tool]]: - -Magnified — edges re-derived at display resolution, razor sharp: - -[[file:sdf-near.png]] - -At moderate distance: - -[[file:sdf-mid.png]] - -Far away — small but clean, fading to gray instead of disintegrating -into aliases (right: 4x nearest zoom of the center): - -[[file:sdf-far.png]] - -[[file:sdf-far-zoom.png]] - -At an oblique angle — foreshortened along one axis, still sharp along -the other: - -[[file:sdf-angled.png]] - -* Using it -:PROPERTIES: -:CUSTOM_ID: using-sdf -:END: - -*TextCanvas* is the main entry point: a textured rectangle carrying a -character grid in 3D space. World cell size 8x16 units, texture cell -16x32 texels (2 texels per world unit). - -#+BEGIN_SRC java -Transform location = new Transform(new Point3D(0, 0, 500)); -TextCanvas canvas = new TextCanvas(location, "Hello, World!", - Color.WHITE, Color.BLACK); -shapeCollection.addShape(canvas); - -// blank canvas + cursor writing -TextCanvas blank = new TextCanvas(location, new TextPointer(10, 40), - Color.GREEN, Color.BLACK); -blank.locate(0, 0); -blank.print("Line 1"); -blank.locate(1, 0); -blank.print("Line 2"); -blank.setForegroundColor(Color.RED); // affects subsequent writes -blank.setTextColor(Color.CYAN); // recolors existing ink only -#+END_SRC - -Colors are per-cell: each =putChar= fills the cell's rectangle in the -background and foreground layers, so one canvas can hold many colors. - -*ForwardOrientedTextBlock* renders the same pipeline onto a billboard -that always faces the camera — for labels that must stay readable from -any angle: - -#+BEGIN_SRC java -ForwardOrientedTextBlock label = new ForwardOrientedTextBlock( - new Point3D(0, -50, 300), 1.0, 2, "Hello, World!", Color.RED); -shapeCollection.addShape(label); -#+END_SRC - -Real use in the demos: the life demo's help panel (=life_demo/Main.java= -=createHelpPanel()=) and the axis labels in =CoordinateSystemDemo=. - -* Tuning knobs -:PROPERTIES: -:CUSTOM_ID: tuning -:END: - -JVM properties (A/B tuning knobs in =TexturedTriangle=): - -| Property | Default | Effect | -|-------------------+---------+-------------------------------------------| -| =e3d.sdf.gamma= | 0 (auto) | fixed coverage gamma; auto derives from footprint | -| =e3d.sdf.sharpen= | 2 | coverage window narrowing; 1 = pixel-exact | -| =e3d.sdf.debug= | false | prints per-triangle footprints and path decisions to stderr | - -* Limitations -:PROPERTIES: -:CUSTOM_ID: limitations -:END: - -- *Fixed cell grid*: TextCanvas is monospace by construction (16x32 - texel cells). Proportional fonts would need a different stamping - scheme. -- *ASCII-oriented cache*: =SdfGlyphCache= measures printable ASCII - (33..126) when sizing the font; exotic glyphs may fit worse. -- *Under extreme minification* text fades to gray by design — that is - the correct physical answer (a sub-pixel glyph has no shape left), - but it means distant labels are decorative, not readable. -- *Bandwidth under minification*: sampling the primary field (no mip - chain) costs more bandwidth per pixel. Text surfaces are small, so - this is the right trade — do not attach SDF masks to huge surfaces. - -* Related classes -:PROPERTIES: -:CUSTOM_ID: related-classes -:END: - -| Class | Role | -|-----------------------------+--------------------------------------------------| -| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.html][TextCanvas]] | Character grid surface in 3D; owns the layers | -| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/SdfGlyphCache.html][SdfGlyphCache]] | Per-glyph field generation + cache (EDT inside) | -| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.html][ForwardOrientedTextBlock]] | Camera-facing text billboard | -| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | =paintSdf= — the scanline path | -| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html][Texture]] | =sdfMask=, =sdfForeground=, =sdfSpreadTexels= | - -*See also:* - -- [[file:../Perspective correct textures/][Perspective-correct textures]] — the scanline texture-mapping path - that SDF rendering builds on; both paths live in =TexturedTriangle= - and share the same interpolated UVs. - diff --git a/doc/SDF textures/sdf-angled.png b/doc/SDF textures/sdf-angled.png deleted file mode 100644 index 34814bf..0000000 Binary files a/doc/SDF textures/sdf-angled.png and /dev/null differ diff --git a/doc/SDF textures/sdf-far-zoom.png b/doc/SDF textures/sdf-far-zoom.png deleted file mode 100644 index f0c0a44..0000000 Binary files a/doc/SDF textures/sdf-far-zoom.png and /dev/null differ diff --git a/doc/SDF textures/sdf-far.png b/doc/SDF textures/sdf-far.png deleted file mode 100644 index 64898a2..0000000 Binary files a/doc/SDF textures/sdf-far.png and /dev/null differ diff --git a/doc/SDF textures/sdf-mid.png b/doc/SDF textures/sdf-mid.png deleted file mode 100644 index ba8ed3e..0000000 Binary files a/doc/SDF textures/sdf-mid.png and /dev/null differ diff --git a/doc/SDF textures/sdf-near.png b/doc/SDF textures/sdf-near.png deleted file mode 100644 index 1d493a1..0000000 Binary files a/doc/SDF textures/sdf-near.png and /dev/null differ diff --git a/doc/Shading/Ambient light comparison.svg b/doc/Shading/Ambient light comparison.svg deleted file mode 100644 index ce07a00..0000000 --- a/doc/Shading/Ambient light comparison.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - -Ambient Light -base illumination applied to all surfaces equally, regardless of orientation - - - - - - - - - - - - - - -Color(0, 0, 0) -✗ pure black -harsh shadows, no depth - - - - - - - - -Color(50, 50, 50) -✓ balanced -depth preserved - - - - - - - - -Color(150, 150, 150) -✗ too flat -no depth contrast - - -lightingManager.setAmbientLight(new Color(50, 50, 50)) ← default - \ No newline at end of file diff --git a/doc/Shading/Distance attenuation.svg b/doc/Shading/Distance attenuation.svg deleted file mode 100644 index 2edf492..0000000 --- a/doc/Shading/Distance attenuation.svg +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - -Distance Attenuation -light intensity falls off with distance from source - - - - - - - - - - - - -Light - - - - - - -0.99 - -d = 100 - - - - -0.52 - -d = 300 - - - - -0.29 - -d = 500 - -← attenuation factor shown above each surface → - - -attenuation vs distance - - -d -att - -0 - - -0.5 - - -1.0 - - -100 - - -300 - - -500 - - - - - -0.99 -0.52 -0.29 - - - -attenuation = -1 / (1 + 0.0001 · d²) - - -coefficient 0.0001 was tuned for typical scene scales in Aukio 3D - diff --git a/doc/Shading/Lambert cosine law.svg b/doc/Shading/Lambert cosine law.svg deleted file mode 100644 index 1f4e216..0000000 --- a/doc/Shading/Lambert cosine law.svg +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - -Lambert Cosine Law -how surface orientation determines light intensity - - - - - - -N̂ -normal - - - - - - - - -Light - -L̂ - -θ -surface polygon - -brightness = -dot( N̂ , L̂ ) - -= cos( θ ) - -θ = 0° - - -1.00 -θ = 45° - - -0.71 -θ = 90° - -0.00 - -θ > 90° -back-face → skip -dot < 0 → no contribution -— angle examples — - - - - - - θ = 0° - - - 100% - - - - - - - - 45° - θ = 45° - - - 71% - - - - - - - - 90° - θ = 90° - - 0% (skip) - - -N̂ surface normal - -L̂ light direction - diff --git a/doc/Shading/Shaded sphere.png b/doc/Shading/Shaded sphere.png deleted file mode 100644 index fbc6487..0000000 Binary files a/doc/Shading/Shaded sphere.png and /dev/null differ diff --git a/doc/Shading/Shading pipeline.svg b/doc/Shading/Shading pipeline.svg deleted file mode 100644 index a58c431..0000000 --- a/doc/Shading/Shading pipeline.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - Transform - compute lighting - - - - Shapes - - - Sort - - - Paint - use cached color - - - Blit - - - - - - - - diff --git a/doc/Shading/index.org b/doc/Shading/index.org deleted file mode 100644 index fdf95a0..0000000 --- a/doc/Shading/index.org +++ /dev/null @@ -1,266 +0,0 @@ -#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme -#+TITLE: Shading & Lighting - Aukio 3D -#+LANGUAGE: en -#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} -#+LATEX_HEADER: \usepackage{parskip} -#+LATEX_HEADER: \usepackage[none]{hyphenat} - -#+OPTIONS: H:20 num:20 -#+OPTIONS: author:nil - -#+HTML_HEAD: - -[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] - -* Overview -:PROPERTIES: -:CUSTOM_ID: shading-lighting -:END: - -#+attr_html: :class responsive-img -#+attr_latex: :width 1000px -[[file:Shaded sphere.png]] - -*Aukio 3D* implements *flat shading* using the [[https://en.wikipedia.org/wiki/Lambert%27s_cosine_law][Lambert cosine -law]]. Each polygon receives a single color based on its orientation -relative to light sources. This is a simple yet effective lighting -model that gives 3D objects depth and realism. - -** The Lighting Model: Lambert Cosine Law -:PROPERTIES: -:CUSTOM_ID: lambert-cosine-law -:END: - -#+INCLUDE: "Lambert cosine law.svg" export html - -The *Lambert cosine law* determines how much light a surface receives -based on its orientation. A surface facing directly toward a light source -receives maximum illumination; as it tilts away, the illumination decreases -proportionally until it reaches zero when perpendicular to the light -direction. This fundamental principle creates the visual cues that make 3D -objects appear solid and dimensional rather than flat. - -The engine implements this law through the dot product of two vectors. The -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] computes a unit vector pointing from the polygon's center -to each [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]], then calculates the dot product with the surface -normal. When the dot product equals 1.0, the surface faces the light -directly and receives full brightness. At 0.71 (a 45-degree angle), it -receives about 71% illumination. At zero or below, the surface faces away -from the light and receives no direct contribution from that source. The -implementation in [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager.computeLighting()]] explicitly checks for -positive dot products before adding light contributions, ensuring that -back-facing surfaces skip unnecessary calculations. - -The surface normal itself is computed by [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]], which takes -the first three vertices of a polygon and calculates their cross product to -find the perpendicular direction. This normal, along with the polygon's -center point calculated by [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]], is passed to the lighting manager -during the [[file:../Rendering loop/][transform phase]] of the rendering loop. The transform phase runs -in parallel, but each polygon is transformed by exactly one worker per -pass, so its cached =shadedColor= field has a single writer — the -result is reused allocation-free during the subsequent multi-threaded -paint phase. See the -[[file:../index.org::#normal-vector][Normal Vector]] section for more details on how normals are computed and used -throughout the engine. - -* Light Sources -:PROPERTIES: -:CUSTOM_ID: light-sources -:END: - -Each light source has three properties: - -| Property | Description | -|------------+--------------------------------------| -| Position | 3D world coordinates of the light | -| Color | RGB color of emitted light | -| Intensity | Brightness multiplier (1.0 = normal) | - -#+BEGIN_SRC java -import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource; -import eu.svjatoslav.aukio.e3d.geometry.Point3D; -import eu.svjatoslav.aukio.e3d.renderer.raster.Color; - -// Create a bright yellow light to the right -LightSource rightLight = new LightSource( - new Point3D(200, -100, 0), // position: right, above, at viewer level - Color.YELLOW, // color - 2.0 // intensity: extra bright -); - -// Create a dim blue light from the left -LightSource leftLight = new LightSource( - new Point3D(-150, 50, 100), - Color.BLUE, - 0.5 // intensity: dim -); -#+END_SRC - -Multiple light sources add their contributions together, allowing for -complex lighting setups like the screenshot above showing a sphere lit -by two lights from the right. - -** Distance Attenuation -:PROPERTIES: -:CUSTOM_ID: distance-attenuation -:END: - -#+INCLUDE: "Distance attenuation.svg" export html - -Light intensity decreases with distance using a *simplified inverse -square law*: - -#+BEGIN_SRC -attenuation = 1.0 / (1.0 + 0.0001 * distance²) -#+END_SRC - -- At distance 0: attenuation = 1.0 (full intensity) -- At distance 100: attenuation ≈ 0.99 (almost full) -- At distance 300: attenuation ≈ 0.52 (half intensity) -- At distance 500: attenuation ≈ 0.29 (about 30%) - -This simplified formula prevents harsh cutoffs while still providing -distance-based dimming. The =0.0001= coefficient was tuned for typical -scene scales in Aukio 3D. - -* Ambient Light -:PROPERTIES: -:CUSTOM_ID: ambient-light -:END: - -#+INCLUDE: "Ambient light comparison.svg" export html - -*Ambient light* provides base illumination that affects all surfaces -equally, regardless of orientation. Without ambient light, surfaces not -directly facing a light source would be pure black. - -- Default ambient: =Color(50, 50, 50)= (dim gray) — set by the ViewPanel - constructor; a standalone =new LightingManager()= starts at - =Color(10, 10, 10)= -- Configurable via =lightingManager.setAmbientLight()= -- Too much ambient: flat appearance (no contrast) -- Too little ambient: harsh shadows (pure black areas) - -#+BEGIN_SRC java -// Increase ambient for softer shadows -viewPanel.getLightingManager().setAmbientLight(new Color(80, 80, 80)); - -// Reduce ambient for dramatic contrast -viewPanel.getLightingManager().setAmbientLight(new Color(20, 20, 20)); -#+END_SRC - -* Using Shading in Your Scene -:PROPERTIES: -:CUSTOM_ID: using-shading -:END: - -**Adding light sources:** - -#+BEGIN_SRC java -import eu.svjatoslav.aukio.e3d.gui.ViewPanel; -import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource; - -ViewPanel viewPanel = new ViewPanel(); - -// Get the lighting manager -LightingManager lighting = viewPanel.getLightingManager(); - -// Add light sources -lighting.addLight(new LightSource( - new Point3D(200, -100, 0), // right side, above - Color.YELLOW, - 1.5 // bright -)); - -lighting.addLight(new LightSource( - new Point3D(-100, 0, 200), // left side, further away - new Color(255, 200, 150), // warm white - 1.0 -)); - -// Configure ambient light -lighting.setAmbientLight(new Color(40, 40, 40)); -#+END_SRC - -**Enabling shading on shapes:** - -#+BEGIN_SRC java -import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonRectangularBox; - -// Create a shaded box -SolidPolygonRectangularBox box = new SolidPolygonRectangularBox( - new Point3D(-50, -50, 100), // min corner - new Point3D(50, 50, 200), // max corner - Color.RED -); - -// Enable shading on the box and all its sub-polygons -box.setShadingEnabled(true); - -// Also enable backface culling for closed meshes -box.setBackfaceCulling(true); - -// Add to scene -viewPanel.getRootShapeCollection().addShape(box); -#+END_SRC - -Shading propagates through composite shapes — calling -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setShadingEnabled(boolean)][setShadingEnabled(true)]] on a composite enables shading for all its -sub-polygons. - -** Related Classes -:PROPERTIES: -:CUSTOM_ID: related-classes -:END: - -| Class | Purpose | -|-------+---------| -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] | Manages light sources and computes shading | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]] | Individual light with position, color, intensity | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Polygon shape with shading support | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] | Composite shape with shading propagation | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html][ViewPanel]] | Provides access to LightingManager | -* Implementation details -:PROPERTIES: -:CUSTOM_ID: implementation-details -:END: - -#+INCLUDE: "Shading pipeline.svg" export html - -Lighting is computed during *Phase 1* (transform phase) of the -[[file:../Rendering loop/][rendering loop]]: - -1. Each shaded polygon calculates its center point and surface normal -2. [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] computes lighting from all sources -3. Result stored in reusable =shadedColor= field -4. During *Phase 4* (paint), the cached color is used directly - -**Why during transform phase?** - -- Lighting computed *once per polygon per pass* — not per pixel -- Each polygon is transformed by a single worker, so its cached result - has exactly one writer even though the transform phase runs in parallel -- Result reused during multi-threaded paint phase — efficient - -** Performance Characteristics -:PROPERTIES: -:CUSTOM_ID: performance -:END: - -| Aspect | Cost | -|--------------+-------------------------------| -| Computation | Per polygon, not per pixel | -| Phase | Parallel transform (single writer per polygon) | -| Allocation | Zero (reuses Color instance) | -| Cache | One shadedColor per polygon | - -The shading implementation is optimized for CPU rendering: - -- *Flat shading*: One lighting calculation per polygon (N-vertex polygon = 1 calculation) -- *Reusable Color*: Result stored in existing field, no allocation during render -- *Thread-safe*: One writer per polygon per pass, so no synchronization needed -- *Pre-computed*: All paint workers (tile grid, ~75% of CPU cores by default) read the same cached result - -This approach trades visual fidelity (no per-pixel lighting) for -performance — essential for software rendering where per-pixel lighting -would be prohibitively expensive. diff --git a/doc/Stereoscopic rendering/Stereo geometry.svg b/doc/Stereoscopic rendering/Stereo geometry.svg deleted file mode 100644 index 2f62d50..0000000 --- a/doc/Stereoscopic rendering/Stereo geometry.svg +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - Two parallel cameras, one screen - top-down view of the scene (z grows downward = into the scene) - - - - screen plane (per eye) - - - - near object - - far object - - - - left eye - x - IPD/2 - - right eye - x + IPD/2 - - - - - - IPD = 6.5 units (cm) - - - - - - - - - - - - - - - - - - - - - - - - large disparity = close - - small disparity = far - - cameras stay PARALLEL (no toe-in) - depth comes purely from the x offset - diff --git a/doc/Stereoscopic rendering/Stereo per eye.svg b/doc/Stereoscopic rendering/Stereo per eye.svg deleted file mode 100644 index 5fc9cfd..0000000 --- a/doc/Stereoscopic rendering/Stereo per eye.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - What changes per eye - - - - - concern - per-eye behavior - - - camera - translation.x += ±IPD/2 (restored after the pass) - - - projection - scale = eyeWidth/3; x += stereoViewportOffsetX - - - frustum culling - built from stereoViewportWidth - narrower FOV per eye - - - painting - clipped to [renderMinX, renderMaxX) = the eye's half - - - mouse picking - hits combined only for the eye containing the cursor - - - HUD / overlays - drawn once, spanning the full frame (zero disparity) - - - everything else - geometry, textures, lightmaps, GI - is shared: the scene is identical, only the viewpoint moves - diff --git a/doc/Stereoscopic rendering/Stereo pipeline.svg b/doc/Stereoscopic rendering/Stereo pipeline.svg deleted file mode 100644 index 895e1fc..0000000 --- a/doc/Stereoscopic rendering/Stereo pipeline.svg +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - One frame = two passes - the triple-buffered pipeline runs the same phases twice, once per eye - - - - camera - translation.x nudged +/- IPD/2 - - - - pass LEFT - transform → sort → tile-bin - viewport [0, w/2) - - - - pass RIGHT - transform → sort → tile-bin - viewport [w/2, w) - - - - - - - - each pass owns a RenderingContext copy - stereoEye, stereoViewportWidth/OffsetX, renderMinX..renderMaxX - - - - - - - - - - left eye pixels - painting clipped to left half - right eye pixels - painting clipped to right half - ONE shared frame buffer → one blit to screen (side-by-side image) - - - - - vertex buffers, aggregators and paint slots still cycle through 3 slots - the two passes overlap freely - diff --git a/doc/Stereoscopic rendering/index.org b/doc/Stereoscopic rendering/index.org deleted file mode 100644 index f1ceee8..0000000 --- a/doc/Stereoscopic rendering/index.org +++ /dev/null @@ -1,190 +0,0 @@ -#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme -#+TITLE: Stereoscopic Rendering - Aukio 3D -#+LANGUAGE: en -#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} -#+LATEX_HEADER: \usepackage{parskip} -#+LATEX_HEADER: \setlength{\parindent}{15pt} -#+LATEX_HEADER: \usepackage{palatino} -#+LATEX_HEADER: \usepackage{charter} -#+HTML_HEAD: - -[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] - -* What stereo rendering adds -:PROPERTIES: -:CUSTOM_ID: what-stereo-adds -:END: - -A single rendered image is flat: the brain infers depth only from -monocular cues (occlusion, shading, perspective, motion parallax while -you move). *Stereoscopic rendering* adds the strongest depth cue of -all — /binocular disparity/: your two eyes see slightly different -images, and the visual cortex turns the difference into a direct -sensation of depth. - -Aukio 3D implements the simplest and most portable form: *side-by-side -stereo*. Every frame renders the scene twice — once from the left eye -position, once from the right — into the left and right halves of the -same image. A VR headset, 3D TV, or a pair of XR glasses in -side-by-side mode feeds each half to the corresponding eye, and the -scene gains real volume. - -#+CAPTION: A side-by-side stereoscopic frame of the House demo, rendered headlessly with [[file:../index.org::#snapshot][the Snapshot tool]]. Left half: left eye. Right half: right eye. Compare the dark cube and the doorway between the halves — the horizontal shift is the disparity your brain reads as depth. -[[file:stereo-side-by-side.png]] - -* The geometry: two parallel cameras -:PROPERTIES: -:CUSTOM_ID: geometry -:END: - -The two eye cameras are identical to the mono camera except for one -thing: the left eye's position is shifted by -IPD/2 and the right -eye's by +IPD/2 along the *world* X axis (the offset is applied to the -camera translation's =x= component directly, then restored). IPD -(inter-pupillary distance) defaults to *6.5 world units* — the House -demo treats 1 unit as 1 cm, and 6.5 cm is the median human IPD. - -Both cameras look in exactly the same direction (*parallel cameras*, -no toe-in). Objects at different depths then land at different -horizontal offsets between the two images — that offset is the -disparity: - -#+CAPTION: Top-down view: the two eye positions and how a near and a far object project onto the screen plane. The near object separates much more between the eyes than the far one. -[[file:Stereo geometry.svg]] - -- near object -> large disparity -> feels close, -- far object -> small disparity -> feels far, -- object at infinity -> zero disparity. - -Larger IPD exaggerates disparity (stronger but potentially straining -depth); smaller IPD flattens the scene. =+=/=-= keys adjust it live in -0.5-unit steps while stereo is active. - -* One frame = two passes -:PROPERTIES: -:CUSTOM_ID: two-passes -:END: - -Stereo does not add a second pipeline — it runs the existing -triple-buffered pipeline *twice per frame*. The render thread in -=ViewPanel.renderFrame()= executes two render passes back to back: - -#+CAPTION: Per frame, the camera is nudged left, a full transform/sort/bin pass runs for the left viewport, then the camera is nudged right and a second pass runs for the right viewport. Both paint into one shared frame buffer, clipped to their half. -[[file:Stereo pipeline.svg]] - -1. *Pass LEFT:* camera translation.x is temporarily decreased by - IPD/2, the scene is transformed, depth-sorted and tile-binned into a - per-pass =RenderingContext= copy whose viewport is the left half of - the frame (=[0, width/2)=), and the paint continuation is submitted - to the worker pool. -2. *Pass RIGHT:* the same with +IPD/2 and the right viewport - (=[width/2, width)=). The camera offset is always restored in a - =finally= block, so the camera never drifts. -3. The two paints write into *one shared frame buffer* — each clipped - to its half — and the completed side-by-side image is blitted to - the screen in one go. - -The triple-buffer machinery (3 vertex slots, 3 aggregator slots, 3 -framebuffers) does not change: a *pass* takes the slot =passCounter % -3=, so left and right passes of the same frame simply occupy -consecutive slots and overlap exactly like consecutive mono frames do. -Workers flow from one pass's tiles straight into the next pass's tiles -with no idle gap. - -* What adapts per eye -:PROPERTIES: -:CUSTOM_ID: per-eye -:END: - -The scene itself — geometry, textures, lightmaps, global illumination -— is shared and identical for both eyes. Only the *viewpoint* moves, -so only view-dependent stages differ per pass: - -#+CAPTION: The per-eye surface area of the engine. Everything not listed here is eye-independent. -[[file:Stereo per eye.svg]] - -- *Projection:* =Vertex= projects with =projectionScale = eyeWidth/3= - (per-eye horizontal FOV) and adds =stereoViewportOffsetX= so the - projected image lands in the correct half of the buffer. The same - offset is applied for near-plane-clip vertices created directly in - camera space. -- *Frustum culling:* the frustum is rebuilt per pass from - =stereoViewportWidth=, so each eye culls against its own (narrower) - view volume — nothing leaks in from the other eye's half. -- *Painting:* every painter clips X to =[renderMinX, renderMaxX)=, - which the pass set to its viewport. No eye can paint into the other - half, even if a polygon crosses the center line. -- *Mouse picking:* in stereo each eye shows the same object at a - different screen X, so a hit can only be resolved against one eye. - =ViewPanel= combines mouse results only for the pass whose viewport - actually contains the cursor. -- *HUD/overlays:* developer tools, crosshair and text are drawn once - over the finished frame, at zero disparity — they sit on the screen - surface, not in the world. - -* Enabling stereo -:PROPERTIES: -:CUSTOM_ID: enabling -:END: - -#+BEGIN_SRC java -ViewPanel viewPanel = ...; - -// Side-by-side stereo on: -viewPanel.setStereoModeEnabled(true); - -// Optional: match the viewer (default 6.5 world units): -viewPanel.setStereoIPD(6.5); -#+END_SRC - -In the demos, *SHIFT+F11* toggles stereo and fullscreen together (XR -glasses want both); plain *F11* remains fullscreen-only. With stereo -active, *+* and *-* adjust the IPD in 0.5-unit steps (clamped at 0.5) -so the viewer can tune comfort at runtime. - -#+CAPTION: The same scene rendered as a normal mono frame (for comparison with the pair above). Notice there is no horizontal offset to read depth from — the picture is flat. -[[file:mono-comparison.png]] - -* Performance and limitations -:PROPERTIES: -:CUSTOM_ID: limitations -:END: - -- Stereo *doubles the per-frame transform, sort and paint work* — two - full passes instead of one. The pipeline overlaps them the same way - it overlaps consecutive mono frames, so throughput drops less than - 2x on a multi-core machine, but expect a real cost. -- Each eye gets *half the horizontal resolution* of the panel. On a - 1920x1080 fullscreen window each eye sees 960x1080 — pixels are - shared, not duplicated. -- IPD is in *world units*: 6.5 only means "6.5 cm" if the scene is - modeled at 1 unit = 1 cm. In a scene with a different scale, divide - or multiply accordingly — or just tune with =+=/=-= until the depth - feels right. -- The eye offset is applied along the *world X axis*, not the camera's - right vector: it is exactly correct when the camera faces along Z - (yaw = 0) and degrades as you turn — at yaw = 90° the eyes would be - offset front-to-back instead of side-to-side. For a fixed-viewing- - direction demo this is fine; a fully rotational stereo camera would - need to apply the IPD along the rotated right vector. -- Side-by-side is a *display format*, not a headset driver: the engine - produces the image; an XR viewer, 3D TV or video player is - responsible for delivering the halves to the eyes. -- Global illumination is unaffected: lightmaps live on the surfaces, - so both eyes sample the same converged lighting for free. - -* Related Classes -:PROPERTIES: -:CUSTOM_ID: related-classes -:END: - -| Class | Role in stereo rendering | -|----------------------+-----------------------------------------------------------------| -| =ViewPanel= | owns stereoModeEnabled/stereoIPD; runs the two passes per frame | -| =StereoEye= | NONE / LEFT / RIGHT tag carried by each pass context | -| =RenderingContext= | per-eye viewport fields: stereoViewportWidth/OffsetX, renderMin/MaxX | -| =Vertex= | per-eye projection: scale from eye width + viewport X offset | -| =ShapeCollection= | rebuilds the frustum per pass from the eye's viewport width | -| =InputManager= | SHIFT+F11 stereo toggle, +/- live IPD adjustment | - -[[file:../index.html#outline-container-understanding-3d-engine][Back to main documentation]] diff --git a/doc/Stereoscopic rendering/mono-comparison.png b/doc/Stereoscopic rendering/mono-comparison.png deleted file mode 100644 index 3b70ee0..0000000 Binary files a/doc/Stereoscopic rendering/mono-comparison.png and /dev/null differ diff --git a/doc/Stereoscopic rendering/stereo-side-by-side.png b/doc/Stereoscopic rendering/stereo-side-by-side.png deleted file mode 100644 index 12fa4fc..0000000 Binary files a/doc/Stereoscopic rendering/stereo-side-by-side.png and /dev/null differ diff --git a/doc/Winding order.svg b/doc/Winding order.svg deleted file mode 100644 index d82048e..0000000 --- a/doc/Winding order.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - CCW - - - - V₁ - V₂ - V₃ - FRONT FACE ✓ - - - - - CW - - - BACK FACE ✗ - (culled — not drawn) - diff --git a/doc/export-docs.sh b/doc/export-docs.sh deleted file mode 100755 index d55e8f2..0000000 --- a/doc/export-docs.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash -# export-docs.sh — export all org-mode documentation pages to HTML. -# -# Exports every doc/**/index.org (and doc/index.org) with the darksun -# theme, using the user's Emacs configuration. Run from anywhere: -# -# doc/export-docs.sh # export all pages -# doc/export-docs.sh --check # export, then render every page with -# # headless Chrome to /tmp/doc-check-*.png -# # for visual inspection -# -# Requires: emacs (with ~/.emacs providing the org HTML setup), -# google-chrome (only for --check). - -set -euo pipefail -DOC_DIR="$(cd "$(dirname "$0")" && pwd)" - -mapfile -t PAGES < <(find "$DOC_DIR" -name index.org | sort) - -echo "Exporting ${#PAGES[@]} pages..." -for page in "${PAGES[@]}"; do - rel="${page#"$DOC_DIR"/}" - if emacs --batch -l ~/.emacs --visit="$page" \ - --funcall=org-html-export-to-html --kill 2>&1 \ - | grep -qi "aborted\|unable to resolve link"; then - echo "FAIL $rel" - exit 1 - fi - echo " ok $rel" -done - -if [[ "${1:-}" == "--check" ]]; then - echo "Rendering pages for visual check..." - for page in "${PAGES[@]}"; do - rel="${page#"$DOC_DIR"/}" - html="${page%.org}.html" - out="/tmp/doc-check-$(echo "$rel" | tr '/ ' '__').png" - google-chrome --headless --disable-gpu --hide-scrollbars \ - --virtual-time-budget=8000 --window-size=1100,2000 \ - --screenshot="$out" "file://$html" 2>/dev/null - echo " shot $out" - done -fi - -echo "Done." diff --git a/doc/index.org b/doc/index.org deleted file mode 100644 index 4068d09..0000000 --- a/doc/index.org +++ /dev/null @@ -1,1224 +0,0 @@ -#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme -#+TITLE: Aukio 3D - Realtime 3D engine -#+LANGUAGE: en -#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} -#+LATEX_HEADER: \usepackage{parskip} -#+LATEX_HEADER: \usepackage[none]{hyphenat} - -#+OPTIONS: H:20 num:20 -#+OPTIONS: author:nil - -#+HTML_HEAD: - -* Introduction -:PROPERTIES: -:CUSTOM_ID: overview -:ID: a31a1f4d-5368-4fd9-aaf8-fa6d81851187 -:END: - -[[file:Example.png]] - -*Aukio 3D* is a realtime 3D rendering engine written in pure Java. It -runs entirely on the CPU — no GPU required, no OpenGL, no Vulkan, no -native libraries. Just Java. - -The motivation is simple: GPU-based 3D is a minefield of accidental -complexity. Drivers are buggy or missing entirely. Features you need -aren't supported on your target hardware. You run out of GPU RAM. You -wrestle with platform-specific interop layers, shader compilation -quirks, and dependency hell. Every GPU API comes with its own -ecosystem of pain — version mismatches, incomplete implementations, -vendor-specific workarounds. I want a library that "just works". - -*Aukio 3D* takes a different path. By rendering everything in software -on the CPU, the entire GPU problem space simply disappears. You add a -Maven dependency, write some Java, and you have a 3D scene. It runs -wherever Java runs. - -This approach is quite practical for many use-cases. Modern systems -ship with many CPU cores, and those with unified memory architectures -offer high bandwidth between CPU and RAM. Software rendering that once -seemed wasteful is now a reasonable choice where you need good-enough -performance without the overhead of a full GPU pipeline. Java's JIT -compiler helps too, optimizing hot rendering paths at runtime. - -Beyond convenience, CPU rendering gives you complete control. You own -every pixel. You can freely experiment with custom rendering -algorithms, optimization strategies, and visual effects without being -constrained by what a GPU API exposes. Instead of brute-forcing -everything through a fixed GPU pipeline, you can implement clever, -application-specific optimizations. - -*Aukio 3D* is part of the larger [[https://www3.svjatoslav.eu/projects/aukio/][Aukio project]], with the long-term goal -of providing a platform for 3D user interfaces and interactive data -visualization. It can also be used as a standalone 3D engine in any -Java project. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/][demos]] for examples of what it can do today. - -*Major features:* -** Global Illumination -:PROPERTIES: -:CUSTOM_ID: global-illumination -:END: - -#+attr_html: :width 600px -#+attr_latex: :width 600px -[[file:Global illumination/Global illumination.png]] - -On top of flat shading, the engine computes progressive *global -illumination* on background CPU threads: real shadows, smooth light -falloff inside polygons (per-texel lightmaps), and indirect bounce -light — while the render loop itself never traces a single ray. - -Read more about [[file:Global illumination/][global illumination]]. - -** Side-by-side stereoscopic rendering support -:PROPERTIES: -:CUSTOM_ID: stereoscopic -:END: - -#+attr_html: :width 600px -#+attr_latex: :width 600px -[[file:Stereoscopic rendering/stereo-side-by-side.png]] - -The engine can render every frame twice — once per eye — into the left -and right halves of the same image, for XR glasses and 3D displays. -The cameras stay parallel and are offset by a configurable IPD -(inter-pupillary distance). Two render passes share one triple-buffered -pipeline, each clipped to its half of the frame buffer; per-eye -projection, frustum culling and mouse picking adapt automatically. - -See [[file:Stereoscopic%20rendering/][Stereoscopic rendering]] for the -geometry, pipeline and tuning. - -** Constructive Solid Geometry - -#+attr_html: :width 600px -#+attr_latex: :width 600px -[[file:CSG/CSG demo.png]] - -*Aukio 3D* allows performing boolean operations against geometry shapes. -So one can subtract, unionize or intersect shapes. - -To understand CSG boolean operations, read more about [[file:CSG/][Constructive -Solid Geometry]]. - -** SDF textures for sharp text -:PROPERTIES: -:CUSTOM_ID: sdf-text -:END: - -[[file:SDF textures/sdf-angled.png]] - -Text and vector-art surfaces do not store coverage; they store a -*signed distance field* — per texel, the distance to the nearest glyph -edge. The rasterizer re-derives coverage per screen pixel from that -smooth field, so text stays sharp at any zoom and fades to clean gray -under minification, all without a mipmap chain. - -See [[file:SDF%20textures/][SDF textures]] for the glyph pipeline, the -render path, analytic minification and tuning knobs. - -* How take engine into use -:PROPERTIES: -:CUSTOM_ID: taking-engine-into-use -:END: - -Add the *Aukio 3D* dependency to your Maven project: - -#+BEGIN_SRC xml - - - eu.svjatoslav - aukio-3d - 1.4 - - -#+END_SRC - -Also add the repository (the library is not on Maven Central): - -#+BEGIN_SRC xml - - - svjatoslav.eu - Svjatoslav repository - https://www3.svjatoslav.eu/maven/ - - -#+END_SRC - -- Library requires Java 21 or newer. - -- Study the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/][demo applications]] for practical examples. Start with the - [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#minimal-example][minimal example]] to see the basic boilerplate needed to render a 3D - scene. - -- Study [[#understanding-3d-engine][how Aukio 3D engine works]]. -- Read online [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/][JavaDoc]]. -- See [[https://www3.svjatoslav.eu/projects/aukio-3d/graphs/][*Aukio 3D* class diagrams]]. (Diagrams were generated by using - [[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility) - -* Essential theory -:PROPERTIES: -:CUSTOM_ID: understanding-3d-engine -:ID: 4b6c1355-0afe-40c6-86c3-14bf8a11a8d0 -:END: -** Coordinate System (X, Y, Z) -:PROPERTIES: -:CUSTOM_ID: coordinate-system -:END: - -#+INCLUDE: "Coordinate system.svg" export html - -*Aukio 3D* uses a **left-handed coordinate system with X pointing right -and Y pointing down**, matching standard 2D screen coordinates. This -coordinate system should feel intuitive for people with preexisting 2D -graphics background. - -| Axis | Direction | Meaning | -|------+------------------------------------+-------------------------------------------| -| X | Horizontal, positive = RIGHT | Objects with larger X appear to the right | -| Y | Vertical, positive = DOWN | Lower Y = higher visually (up) | -| Z | Depth, positive = away from viewer | Negative Z = closer to camera | - -*Practical Examples* - -- A point at =(0, 0, 0)= is at the origin. -- A point at =(100, 50, 200)= is: 100 units right, 50 units down - visually, 200 units away from the camera. -- To place object A "above" object B, give A a **smaller Y value** - than B. - -Coordinates in this system are stored using the -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point3D.html][Point3D]] class — a mutable container with public =x=, =y=, =z= fields -supporting vector operations like distance, rotation, and translation. -Vertices (see [[#vertex][below]]) are positioned within this coordinate system. - -The [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#coordinate-system][aukio-3d-demos]] project includes an interactive -coordinate system reference showing X, Y, Z axes as colored arrows -with a grid plane for spatial context. - -** Point3D and Vertex -:PROPERTIES: -:CUSTOM_ID: vertex -:END: - -#+INCLUDE: "Point3D vertex.svg" export html - -Every 3D object is built from *vertices* — corner points that define -the shape's geometry. A triangle has 3 vertices, a cube has 8, and -complex meshes have thousands. The engine uses two related classes to -represent points in 3D space, each serving a different purpose. - - - -*** Point3D — Raw Coordinates - -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point3D.html][Point3D]] is the fundamental coordinate type throughout the engine. It -stores a position or vector with three public fields: =x=, =y=, =z=. -The class provides vector math operations: distance calculation, -rotation, translation, scaling, dot/cross products, and interpolation. Methods follow a fluent API convention where mutating -operations (like =add=, =multiply=) return =this= for chaining, while -non-mutating variants (like =withAdded=, =withMultiplied=) return new -instances. - -Use =Point3D= for: -- Storing positions, vectors, or any raw 3D coordinate -- Distance and angle calculations between points -- Vector math (dot product, cross product, normalization) -- Rotating or translating positions before shape construction - -#+BEGIN_SRC java -Point3D p1 = new Point3D(100, 50, 200); -Point3D p2 = new Point3D(0, 0, 100); -double distance = p1.getDistanceTo(p2); // Euclidean distance -Point3D direction = p1.withSubtracted(p2).unit(); // New point: unit vector from p2 to p1 -p1.rotate(new Point3D(0,0,0), Math.PI/4, 0); // Rotate p1 in place, 45° in XZ plane -#+END_SRC - -*** Vertex — Rendering-Ready Coordinates - -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] wraps a =Point3D= and adds the coordinate spaces needed during -rendering. As a shape transforms through the render pipeline, each -vertex tracks its position in multiple spaces: - -| Field | Purpose | -|------------------------+--------------------------------------------------------| -| =coordinate= | Original position in local/model space | -| =transformedCoordinate(ctx)= | Position relative to camera (after transform stack) | -| =onScreenCoordinate(ctx)= | 2D screen pixels (after perspective projection) | -| =textureCoordinate= | Optional UV coords in pixel units (not normalized) | -| =normal= | Optional normal vector for CSG polygon splitting | - -=transformedCoordinate= and =onScreenCoordinate= are accessor methods, -not plain fields: each vertex carries three slots for each, one per -pipeline projection slot, and the accessor picks the slot of the -context's current render pass. This is what lets the triple-buffered -pipeline transform the next frame while previous frames are still -being painted (see [[file:Rendering loop/index.org::#software-pipeline][software pipeline]]). - -During rendering, the vertex is transformed through all spaces: first -applying the -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/TransformStack.html][TransformStack]] to get the camera-relative coordinate, then -projecting to 2D. Results are cached per frame per slot to avoid -recomputing for vertices shared across multiple shapes. - -Use =Vertex= when: -- Constructing triangles, polygons, or textured shapes -- Your geometry needs texture UV coordinates -- You're performing CSG boolean operations (requires =normal=) - -#+BEGIN_SRC java -// Create a textured triangle (texture coordinates use pixel units) -// For a 256x256 texture: (0,0)=top-left, (256,256)=bottom-right -Vertex v1 = new Vertex(new Point3D(0, 0, 100), new Point2D(0, 0)); -Vertex v2 = new Vertex(new Point3D(100, 0, 100), new Point2D(256, 0)); -Vertex v3 = new Vertex(new Point3D(50, 100, 100), new Point2D(128, 256)); -TexturedTriangle triangle = new TexturedTriangle(v1, v2, v3, texture); -#+END_SRC - -*** When to Use Each - -| Use Point3D | Use Vertex | -|--------------------------------------+-----------------------------------------------| -| Positioning shapes, cameras, lights | Building triangles and polygons | -| Vector math (distances, directions) | Texture-mapped geometry | -| Rotating or translating positions | CSG operations | -| Temporary calculations | Shapes that render through transform pipeline | - -For simple shapes without textures, you can pass raw =Point3D= -coordinates directly to constructors — the shape will internally wrap -them in =Vertex= objects. The [[#coordinate-system][coordinate system]] above defines the -meaning of all =x=, =y=, =z= values in both classes. - -** Edge -:PROPERTIES: -:CUSTOM_ID: edge -:END: - -#+INCLUDE: "Edge.svg" export html - -An *edge* is a straight line segment connecting two [[#vertex][vertices]]. Edges -form the wireframe skeleton of a 3D model — the structural framework -visible when surfaces are not rendered. A triangle has 3 edges, a cube -has 12 edges, and complex meshes have thousands. - -In *Aukio 3D*, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class implements edges as renderable shapes. Each -Line connects two [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] endpoints and stores two properties: a -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#width][width]] in world units (adjusted for perspective during rendering) and a -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#color][color]] with alpha transparency. The rendering algorithm switches -between two modes based on the projected screen width: thin lines below -the threshold are drawn as single pixels with alpha-adjusted coloring, -while thicker lines are rendered as filled rectangles with perspective-correct -edge fading using four [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineInterpolator.html][LineInterpolator]] scanline boundaries. - -Wireframe shapes are composite objects built from multiple Line instances. -For example, [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.html][WireframeBox]] creates 12 Line objects — four edges parallel to -each axis — using a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineAppearance.html][LineAppearance]] factory to ensure consistent styling across -all edges. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.html][WireframeCube]] convenience subclass provides a center-point -constructor. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual comparison of -wireframe (edges only) versus solid polygon (surfaces with lighting) -rendering modes. - -** Face (Triangle) -:PROPERTIES: -:CUSTOM_ID: face-triangle -:END: - -#+INCLUDE: "Face triangle.svg" export html - -A *face* is a flat surface enclosed by edges — the visible skin of a 3D -object. While faces can theoretically have any number of sides, 3D -engines standardize on *triangles* because three points always define a -flat plane. A quad (4 vertices) or pentagon (5 vertices) might be -non-planar depending on vertex positions, causing rendering artifacts. -Triangles avoid this problem entirely. - -*** SolidPolygon — Solid-Color Faces - -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] is the primary face type, supporting any number of vertices -(3 or more). Triangles render directly via scanline rasterization. -N-vertex polygons (quads, pentagons, etc.) are triangulated using fan -decomposition — a quad becomes 2 triangles, a pentagon 3 — but only -when the polygon lives inside an -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] -(the scene graph root is one): the composite triangulates while -building its render list. A standalone SolidPolygon with more than 3 -vertices cannot be painted directly and throws IllegalStateException. - -Each SolidPolygon stores a single fill color with optional alpha -transparency. When shading is enabled, the lighting manager computes -the polygon's illumination once during the transform phase, then -applies the shaded color during painting. Backface culling (see -[[#winding-order-backface-culling][Winding Order & Backface Culling]]) can be enabled per-polygon, or -applied recursively to an entire composite shape via -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] — this propagates the -setting to all SolidPolygon and TexturedTriangle sub-shapes, including -nested composites. - -#+BEGIN_SRC java -// Create a red triangle -SolidPolygon triangle = SolidPolygon.triangle( - new Point3D(0, 0, 100), - new Point3D(50, 0, 100), - new Point3D(25, 50, 100), - Color.RED -); - -// Create a blue quad (internally triangulated) -SolidPolygon quad = SolidPolygon.quad( - new Point3D(-50, -50, 100), - new Point3D(50, -50, 100), - new Point3D(50, 50, 100), - new Point3D(-50, 50, 100), - Color.BLUE -); - -// Enable lighting and culling for a closed mesh -quad.setShadingEnabled(true); -quad.setBackfaceCulling(true); - -// Add to the scene — the root composite triangulates the quad -viewPanel.getRootShapeCollection().addShape(quad); -#+END_SRC - -*** TexturedTriangle — UV-Mapped Faces - -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] renders faces with image textures mapped via UV -coordinates. Each of the three [[#vertex][vertices]] stores a =textureCoordinate= -(a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point2D.html][Point2D]] with U and V values in *pixel units* matching the texture -dimensions). For a 256×256 texture, coordinates range from (0,0) at the -top-left corner to (256,256) at the bottom-right. During rasterization, the -engine interpolates these UV coordinates across the triangle's surface, -sampling the texture at each pixel. When mipmaps are used, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html#multiplicationFactor][multiplicationFactor]] -scales coordinates to match the selected mipmap resolution. - -The texture system supports mipmaps — pre-scaled versions of the texture -selected based on the triangle's screen size to reduce aliasing artifacts -on distant surfaces. Texture coordinates are mapped with -perspective-correct interpolation inside the scanline rasterizer, so -large triangles at steep angles render without distortion — see the -[[file:Perspective correct textures/][perspective-correct textures]] page. - -#+BEGIN_SRC java -// Create a 256x256 texture -Texture texture = new Texture(256, 256, 2); // width, height, maxUpscale - -// Create a textured triangle with UV coordinates in pixel units -Vertex v1 = new Vertex(new Point3D(0, 0, 100), new Point2D(0, 0)); // top-left -Vertex v2 = new Vertex(new Point3D(100, 0, 100), new Point2D(256, 0)); // top-right -Vertex v3 = new Vertex(new Point3D(50, 100, 100), new Point2D(128, 256)); // bottom-center - -TexturedTriangle triangle = new TexturedTriangle(v1, v2, v3, texture); -triangle.setBackfaceCulling(true); -#+END_SRC - -Both SolidPolygon and TexturedTriangle extend -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]], which handles vertex transformation and depth -sorting. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual comparison of solid -versus textured polygon rendering. - -** Normal Vector -:PROPERTIES: -:CUSTOM_ID: normal-vector -:END: - -#+INCLUDE: "Normal vector.svg" export html - -A *normal* is a vector perpendicular to a surface. It tells the -renderer which direction a face is pointing. Normals are critical for -*lighting* — the angle between the light direction and the normal -determines how bright a surface appears. - -**Use cases:** - -| Use case | API | Computation | Location | -|----------------------+----------------------------------------------+----------------------------+-------------------| -| BSP/CSG operations | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#getPlane()][SolidPolygon.getPlane()]] → [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#normal][Plane.normal]] | Lazy-cached once | =Plane= | -| Per-frame shading | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]] → =cachedNormal= field | Recomputed every frame | =SolidPolygon= | -| Lighting calculation | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html#computeLighting()][LightingManager.computeLighting()]] | Uses normal via =dot(L,N)= | =LightingManager= | - -**Implementation notes:** -- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]]: shared zero-allocation helper for computing normals from three points -- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Plane]]: stores normals in Hesse normal form (normal + distance) for BSP spatial partitioning -- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html#normal][Vertex.normal]]: optional field for CSG polygon splitting (not used for rendering) - -** Mesh -:PROPERTIES: -:CUSTOM_ID: mesh -:END: - -#+INCLUDE: "Mesh.svg" export html - -A *mesh* is a collection of vertices, edges, and faces that together -define the shape of a 3D object. Even curved surfaces like spheres are -approximated by many small triangles — more triangles means a smoother -appearance. A cube has 8 vertices forming 12 triangular faces, while a -smooth sphere requires hundreds or thousands of triangles depending on -the desired quality. - -In *Aukio 3D*, meshes are built through composition rather than -monolithic vertex/index buffers. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]] class is -the foundation for primitive shapes — each instance stores its own -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html#vertices][List<Vertex>]] directly. This includes [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] (N-vertex -convex polygons, not limited to triangles), [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] -(UV-mapped triangles), and [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] (wireframe edges). The -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] class groups multiple shapes into a single -object with its own position, rotation, and transform — useful for -complex models that move or rotate together. - -Complex meshes are constructed procedurally by adding primitive shapes -during initialization. For example, [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.html][SolidPolygonSphere]] generates -triangles using a latitude-longitude grid: with 16 segments, it -creates 960 SolidPolygon triangles by looping through -rings and sectors, calling [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#addShape(eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape)][addShape()]] for each. The generic -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.html][SolidPolygonMesh]] accepts any list of triangles, allowing custom -geometry from procedural generation or external sources. - -During rendering, several automatic optimizations occur. N-vertex -polygons (quads, pentagons, etc.) are triangulated using fan -triangulation inside the composite's render-list builder, converting -an N-vertex polygon into N-2 triangles. Textured triangles render with -perspective-correct texture mapping (see [[file:Perspective correct textures/][perspective-correct textures]]). -Composites perform view frustum culling to skip rendering when entirely -off-screen. Sub-shapes can be organized into named groups via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/SubShape.html][SubShape]] -wrappers, allowing [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#showGroup(java.lang.String)][showGroup()]] and [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#hideGroup(java.lang.String)][hideGroup()]] to toggle visibility of -entire sections. Composite shapes also support CSG boolean operations -— see the [[file:CSG/][Constructive Solid Geometry]] documentation for union, -subtract, and intersect operations. - -The [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] showcases all primitive shapes available in -*Aukio 3D*, rendered in both wireframe mode (edges only via -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.html][WireframeBox]] and similar) and solid polygon mode (filled surfaces with -dynamic lighting). - -** Working with Colors -:PROPERTIES: -:CUSTOM_ID: working-with-colors -:ID: f2c9642a-a093-444f-8992-76c97ff28c16 -:END: - -Aukio 3D uses its own [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html][Color class]] instead of [[https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/Color.html][java.awt.Color]]. This -custom implementation is designed specifically for the engine's -software rasterizer, where avoiding object allocation during rendering -is critical for performance. When rendering thousands of polygons per -frame, creating new Color instances for each one would generate -excessive garbage and trigger frequent garbage collection -pauses. Instead, the engine's Color class uses mutable fields that can -be reused across frames. - -The class stores RGBA components as public integer fields in the range -0–255. This format matches the engine's pixel buffer layout and avoids -costly float-to-int conversions during rasterization. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#r][r]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#g][g]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#b][b]], and -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#a][a]] fields are accessible directly, allowing lighting calculations and -alpha blending to modify colors in-place without allocating new -objects. For example, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] class maintains a reusable -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][shadedColor]] field that gets updated during each frame's lighting -calculation instead of creating a new Color instance per polygon. - -Color provides several constructors for different input formats. The -most common approach is using hex strings via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#hex(java.lang.String)][Color.hex(String)]] or the -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(java.lang.String)][String constructor]], which support formats like ="F80"= (3-digit RGB), -="FF8800"= (6-digit RGB), ="F808"= (4-digit RGBA), and ="FF8800CC"= -(8-digit RGBA). You can also create colors from integer RGBA -components (0–255) using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(int,int,int,int)][new Color(r, g, b, a)]], from floating-point -components (0.0–1.0) via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(double,double,double,double)][new Color(double r, double g, double b, -double a)]], or from a packed RGB integer like =0xFF8800= using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(int)][new -Color(int rgb)]]. The class also provides predefined constants for -common colors: [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#RED][Color.RED]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#GREEN][Color.GREEN]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#BLUE][Color.BLUE]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#YELLOW][Color.YELLOW]], -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#CYAN][Color.CYAN]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#MAGENTA][Color.MAGENTA]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#WHITE][Color.WHITE]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#BLACK][Color.BLACK]], and -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#TRANSPARENT][Color.TRANSPARENT]]. - -The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#set(int,int,int,int)][set(int r, int g, int b, int a)]] method modifies a Color in-place -and returns =this= for method chaining, which is essential for -performance during rendering. For example, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] -calculates lighting contributions from all light sources and stores -the final shaded color directly into a reusable Color instance via -=set()=, avoiding any allocation. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#toAwtColor()][toAwtColor()]] method converts a -Aukio 3D Color to a java.awt.Color when needed for Java2D graphics -operations, caching the result to avoid repeated conversion. The -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#toInt()][toInt()]] method packs the color into an ARGB integer suitable for the -engine's pixel buffer, used during rasterization to write pixels -directly. - -#+BEGIN_SRC java -import eu.svjatoslav.aukio.e3d.renderer.raster.Color; -import static eu.svjatoslav.aukio.e3d.renderer.raster.Color.hex; - -// Using predefined color constants -Color red = Color.RED; -Color transparent = Color.TRANSPARENT; - -// Create from hex string (recommended for clarity) -Color orange = hex("FF8800"); // RGB, fully opaque -Color semiTransparent = hex("FF880080"); // RGBA, 50% transparent - -// Create from integer components (0-255) -Color custom = new Color(255, 128, 64, 200); - -// Create from packed RGB integer -Color packed = new Color(0xFF8800); - -// Modify existing color in-place (no allocation) -Color reusable = new Color(); -reusable.set(100, 200, 50, 255); - -// Convert to AWT color for Java2D operations -java.awt.Color awtColor = custom.toAwtColor(); - -// Use in lighting calculations (LightingManager modifies in-place) -// See the Shading & Lighting documentation for details -#+END_SRC - -The alpha component controls transparency during rendering. A value of -0 makes the color fully transparent, while 255 makes it fully -opaque. The rasterizer implements alpha blending during the paint -phase: when drawing a semi-transparent pixel, the engine blends the -source color with the existing background pixel proportionally based -on the alpha value. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html#drawPixel(int,int\[\],int)][TextureBitmap.drawPixel()]] method handles this -blending, multiplying source colors by alpha and background colors by -=(255 - alpha)=, then combining them. You can test whether a color is -fully transparent using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#isTransparent()][isTransparent()]], which returns true when alpha -equals zero. - -For lighting calculations, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]] class uses Color to -represent the color and intensity of emitted light. Multiple light -sources contribute to the final shaded color of each polygon, as -described in the [[file:Shading/index.org::#shading-lighting][Shading & Lighting]] documentation. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html#setAmbientLight(eu.svjatoslav.aukio.e3d.renderer.raster.Color)][ambient light]] -provides base illumination that affects all surfaces equally, -regardless of orientation. Colors are also used for wireframe -rendering via the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class, where the color field determines the -line's appearance. - -** Shading & Lighting - -#+attr_html: :width 600px -#+attr_latex: :width 600px -[[file:Shading/Shaded%20sphere.png]] - - -*Aukio 3D* implements *flat shading* — one normal per polygon, -computed from the first three vertices. Each polygon receives a single -color based on its orientation relative to light sources. - -To understand lighting and shading, read more about [[file:Shading/][shading & lighting]]. - -* 3D engine internals -** Main render loop - -The rendering loop is the heart of the engine, continuously generating -frames at a target rate (typically 60 FPS). Each frame transforms 3D -shapes through a multi-stage pipeline before displaying them on screen. - -#+INCLUDE: "Rendering loop/Render pipeline.svg" export html - -The render loop runs on a dedicated background daemon thread managed by -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html][ViewPanel]], which can optionally sleep between frames to maintain a -target FPS or run unlimited for benchmarking. - -For a detailed walkthrough of each phase with diagrams and code -examples, see the dedicated page: [[file:Rendering loop/][Rendering loop]]. - -** Near-Plane Clipping -:PROPERTIES: -:CUSTOM_ID: near-plane-clipping -:END: - -Individual polygons that straddle the camera's near plane are not -dropped wholesale: the vertex loop is clipped against the plane, new -intersection vertices are generated with 3D-interpolated UVs and -normals, and the clipped polygon — a triangle can become a quad, -painted as a triangle fan — renders normally. Only polygons entirely -behind the near plane are culled. This keeps floor and wall tiles -visible when the camera brushes against them. - -#+INCLUDE: "Near plane clip/Near plane straddle.svg" export html - -Read more about [[file:Near plane clip/][near-plane clipping]]. - -** Depth buffer -:PROPERTIES: -:CUSTOM_ID: depth-buffer -:END: - -Visibility is resolved per pixel by a depth buffer: every triangle -interpolates =1/z= across its spans and wins a pixel only where it is -nearer than the surface already there. Opaque geometry — textured -triangles, solid polygons — paints front-to-back with depth writes; -translucent geometry paints back-to-front with depth tests but no -writes, so it never occludes. Lines and billboards stay painter-ordered -overlays by design. - -See [[file:Depth%20buffer/][Depth buffer]] for the full treatment. - -** Frustum & View Frustum Culling - -*Aukio 3D* implements view frustum culling. - -#+INCLUDE: "Frustum culling/Frustum diagram.svg" export html - -To understand frustum culling and object-level visibility -optimization, read more about [[file:Frustum culling/][frustum & view frustum culling.]] - -** Winding Order & Backface Culling -:PROPERTIES: -:CUSTOM_ID: winding-order-backface-culling -:END: - -#+INCLUDE: "Winding order.svg" export html - -The order in which a triangle's vertices are listed determines its -*winding order*. In *Aukio 3D*, screen coordinates have Y-axis pointing -*down*, which inverts the apparent winding direction compared to -standard mathematical convention (Y-up). *Counter-clockwise (CCW)* in -screen space means front-facing. *Backface culling* skips rendering -triangles that face away from the camera — a major performance -optimization. - -- CCW winding (in screen space) → front face (visible) -- CW winding (in screen space) → back face (culled) -- When viewing a polygon from outside: define vertices in *counter-clockwise* order as seen from the camera -- Saves ~50% of triangle rendering -- Implementation uses signed area: =signedArea < 0= means front-facing - (in Y-down screen coordinates, negative signed area corresponds to - visually CCW winding) - -In *Aukio 3D*, backface culling is *optional* and disabled by default. Enable it per-shape: -- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#setBackfaceCulling(boolean)][SolidPolygon.setBackfaceCulling(true)]] -- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html#setBackfaceCulling(boolean)][TexturedTriangle.setBackfaceCulling(true)]] -- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] (applies to all - sub-shapes) - -See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#winding-order][Winding Order demo]] for an interactive visualization. - -** Perspective correct textures - -#+attr_html: :class responsive-img -#+attr_latex: :width 1000px -[[file:Perspective correct textures/Affine distortion.png]] - -*Aukio 3D* tries to do perspective-correct texture rendering. Read more -about [[file:Perspective correct textures/][perspective-correct texture implementation]]. - -* Developer tools -:PROPERTIES: -:CUSTOM_ID: developer-tools -:ID: 8c5e2a1f-9d3b-4f6a-b8e7-1c4d5f7a9b2e -:END: - -Press *F12* anywhere in the application to open the Developer Tools -panel: - -#+attr_html: :class responsive-img -#+attr_latex: :width 1000px -[[file:Developer tools/Developer tools.png]] - -This debugging interface helps you understand what the engine is doing -internally and diagnose rendering issues. Pressing F12 again closes -the panel. - -** Diagnostic toggles -:PROPERTIES: -:CUSTOM_ID: diagnostic-toggles -:END: - -*** Show polygon borders -:PROPERTIES: -:CUSTOM_ID: show-polygon-borders -:END: - -When enabled, each [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] draws yellow outlines around its -three edges after rendering its texture content. This overlays the -triangle mesh onto the final image: - -#+attr_html: :class responsive-img -[[file:Developer tools/Render polygon borders.png]] - -Use this visualization when investigating: - -- Mesh structure: see the actual triangles as the rasterizer receives - them -- Geometry bugs: spot T-junction gaps and overlapping geometry -- Texture distortion: compare triangle shapes against visible warping - -*** Render alternate segments (overdraw debug) -:PROPERTIES: -:CUSTOM_ID: render-alternate-segments -:END: - -Renders only even-numbered paint tiles while leaving odd-numbered ones -black. (The screen is divided into a grid of rectangular tiles for -parallel rendering — see [[file:Rendering loop/index.org::#phase-4-clear-paint-tiles][the rendering loop documentation]]. -"Segments" is the older name for tiles.) - -#+attr_html: :class responsive-img -[[file:Developer tools/Render alternative segments.png]] - -This toggle helps detect overdraw: threads writing outside their -allocated tile. If you see rendering artifacts in the black tiles, a -paint task is writing pixels outside its assigned area — a clear sign -of a bug. - -*** Show segment boundaries -:PROPERTIES: -:CUSTOM_ID: show-segment-boundaries -:END: - -Draws red lines along the paint tile boundaries, making it easy to see -exactly where each tile's rendered area begins and ends. In stereo -mode each eye's viewport gets its own grid: - -#+attr_html: :class responsive-img -[[file:Developer tools/Show segment boundaries.png]] - -Useful for: - -- Verifying the tile grid division -- Debugging tile-boundary rendering issues (e.g. clipped text or - missing slivers at tile edges) -- Understanding the parallel rendering architecture visually - -** Camera position -:PROPERTIES: -:CUSTOM_ID: camera-position -:END: - -Displays the current camera coordinates and orientation in real-time: - -| Parameter | Description | -|-----------+------------------------------------------| -| x, y, z | Camera position in 3D world space | -| yaw | Rotation around the Y axis (left/right) | -| pitch | Rotation around the X axis (up/down) | -| roll | Rotation around the Z axis (tilt) | - -The *Copy* button copies the full camera position string to the -clipboard in a format ready to paste into bug reports or configuration -files. - -Use this for: -- Reporting exact camera positions when filing bugs -- Saving interesting viewpoints for later reference -- Understanding camera movement during navigation -- Sharing specific views with other developers - -Example copied format: -#+BEGIN_EXAMPLE -500.00, -300.00, -800.00, 0.60, -0.50, -0.00 -#+END_EXAMPLE - -The six numbers map 1:1 onto -[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Transform.html#set(double,double,double,double,double,double)][Transform.set(x, y, z, yaw, pitch, roll)]], -so a copied viewpoint can be restored at startup — this is how the -demo applications freeze a good camera position into code: - -#+BEGIN_SRC java -// Camera position captured via Developer Tools -> Copy -viewPanel.getCamera().getTransform().set( - 130.66, -65.49, -248.18, // x, y, z - -0.06, -0.36, -0.00); // yaw, pitch, roll -#+END_SRC - -** Frustum culling statistics -:PROPERTIES: -:CUSTOM_ID: frustum-culling-statistics -:END: - -Shows real-time statistics about composite shape frustum culling -efficiency (see the dedicated [[file:Frustum culling/][frustum culling]] page for how -culling itself works): - -| Statistic | Description | -|-----------+----------------------------------------------------------| -| Total | Number of composite shapes tested against the frustum | -| Culled | Number of composites rejected (outside view frustum) | -| Culled % | Percentage of composites that were culled (0-100%) | - -*How to interpret the numbers:* - -- *High cull % (60-90%)*: Excellent — most objects are being correctly culled -- *Medium cull % (20-60%)*: Moderate — some optimization benefit -- *Low cull % (0-20%)*: Limited benefit — either all objects are visible, or scene needs restructuring - -*Example:* -#+BEGIN_EXAMPLE -Total: 473 Culled: 425 (89.9%) -#+END_EXAMPLE - -This means 473 composite shapes were tested, 425 were outside the view -and skipped entirely, and only 48 composites (with all their children) -actually needed to be rendered. This is excellent culling efficiency. - -The statistics update every 200ms while the panel is open. Note that -the root composite is never frustum-tested (it's always rendered), so -the "Total" count excludes it. - -** Render threads -:PROPERTIES: -:CUSTOM_ID: render-threads -:END: - -Shows the number of active render threads versus available CPU cores. -The engine defaults to 75% of available threads (at most cores − 1, so -one thread always stays free for the rest of the system). The count is -changeable at runtime via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#setNumRenderThreads(int)][ViewPanel.setNumRenderThreads(int)]]; -the worker pool is recreated lazily on the next frame. - -** Frame rate -:PROPERTIES: -:CUSTOM_ID: frame-rate -:END: - -Shows the current target FPS and the measured production rate (frames -completed per second, averaged over a ~500 ms window). The measured -number counts produced frames regardless of how quickly the display -path presents them — see [[file:Rendering loop/index.org::#frame-rate-control][frame rate control]]. - -The *Unlock FPS* toggle switches to unlimited (benchmark) mode: the -engine renders continuously as fast as possible, even when the scene -is static. Toggling off restores the previously locked target rate. - -** Thread activity timeline -:PROPERTIES: -:CUSTOM_ID: thread-activity-timeline -:END: - -A per-thread occupancy view — the software-renderer equivalent of a -GPU frame profiler. Each thread gets a row (the render thread and -present thread on top, then one row per worker), time runs along the X -axis, and each colored block is one recorded work interval. Idle time -is black. - -#+attr_html: :class responsive-img -[[file:Developer tools/Thread timeline.png]] - -Press *Record* to start capturing. The colors encode both the task -kind and which frame the task belongs to — transform, paint, and -binning come in three frame-parity variants (f0/f1/f2), so you can see -up to three frames in flight simultaneously. Additional colors mark -render-thread orchestration, blocked time, blits, and the sort/drain -sub-phases. - -Navigation: mouse wheel scrolls, Ctrl+wheel zooms, and a scrollbar -moves along the captured range. - -The legend colors, exactly as the timeline paints them: - -| Color | Legend label | What it shows | -|-------+--------------+---------------| -| @@html:@@ =#2ECC40= | =transform f0= | Vertex transform chunk task, frame slot 0 | -| @@html:@@ =#B8D900= | =transform f1= | Vertex transform chunk task, frame slot 1 | -| @@html:@@ =#6B8E23= | =transform f2= | Vertex transform chunk task, frame slot 2 | -| @@html:@@ =#0074D9= | =paint f0= | Paint tile task (clear + rasterize one tile), frame slot 0 | -| @@html:@@ =#F012BE= | =paint f1= | Paint tile task, frame slot 1 | -| @@html:@@ =#B10DC9= | =paint f2= | Paint tile task, frame slot 2 | -| @@html:@@ =#39CCCC= | =bin f0= | Tile binning task (assign sorted shapes to tiles), frame slot 0 | -| @@html:@@ =#008B8B= | =bin f1= | Tile binning task, frame slot 1 | -| @@html:@@ =#007070= | =bin f2= | Tile binning task, frame slot 2 | -| @@html:@@ =#A0A0A0= | =render serial= | Render thread orchestration: tree walk and pass submission | -| @@html:@@ =#8B0000= | =blocked= | Render thread waiting for an older paint pass or the present gate | -| @@html:@@ =#FFFFFF= | =blit= | Present thread copying a finished frame to the screen | -| @@html:@@ =#FF851B= | =sort+bin= | A pass's async continuation as a whole: drain, sort, bin, submit paint | -| @@html:@@ =#8B4513= | =drain= | Continuation sub-phase: await and merge parallel transform chunks | -| @@html:@@ =#FFD700= | =sort= | Continuation sub-phase: depth sort of the pass's shapes | - -The f0/f1/f2 suffixes are the frame's projection slot (frame number -modulo 3) — the triple-buffering from the [[file:Rendering loop/index.org::#software-pipeline][software pipeline]]. -When the pipeline is healthy you see interleaved colors from two or -three frames on the worker rows at once: paint tasks of an older frame -overlapping transform and binning of the newer one. Wide =blocked= -spans on the render row, or worker rows with black gaps, mean the -pipeline is starved rather than busy. - -Recording is cheap but not free (~100 ns per task; a single volatile -read when disabled), so leave it off during benchmarking runs. The -captured intervals live in a fixed-size ring buffer — long recordings -keep only the most recent history. - -What to look for: - -- *Solidly packed worker rows* mean the pipeline is keeping all cores - busy — the design goal (see [[file:Rendering loop/index.org::#software-pipeline][software pipeline]]) -- *Long "blocked" spans on the render row* mean the render thread is - waiting for paint passes — workers are the bottleneck -- *Long "blit" spans on the present row* mean the display path - (X server) is the bottleneck; excess frames are being dropped from - the presentation mailbox - -** Live log viewer -:PROPERTIES: -:CUSTOM_ID: live-log-viewer -:END: - -The scrollable text area shows captured debug output in real-time: -- Green text on black background for readability -- Auto-scrolls to show latest entries -- Updates every 200ms while panel is open -- Captures logs even when panel is closed (replays when reopened) - -Use the *Clear Logs* button to reset the log buffer for fresh -diagnostic captures. - -** Headless & agentic tooling -:PROPERTIES: -:CUSTOM_ID: headless-agentic -:END: - -For windowless rendering, pixel assertions, golden-image regression -tests and scene dumps — built for automated verification and AI agents — -see [[#agentic-development][agentic development tools]]. - -* Agentic development -:PROPERTIES: -:CUSTOM_ID: agentic-development -:END: - -*Aukio 3D* provides good support for automated AI coding agents -(for example OpenCode, Hermes Agent, etc..). - -Thanks to facilities is =eu.svjatoslav.aukio.e3d.headless= package, an -AI agent can render any scene from any pose, assert what got painted, -compare against committed reference images, and dump the full scene -state for a bug report — all without a window, a display, or the -render thread. - -** One pipeline, two drivers -:PROPERTIES: -:CUSTOM_ID: one-pipeline -:END: - -The key design decision: the headless path drives *the very same -transform → sort → paint pipeline* the on-screen ViewPanel -uses. Nothing is reimplemented, so a passing headless test proves the -real render works, and a bug reproduced headlessly is the real bug. - -#+INCLUDE: "Agentic development/Headless lanes.svg" export html - -Making this possible required three small engine changes: - -- ~ShapeCollection.transformShapes(Camera, RenderingContext)~ — the - transform phase now accepts a camera directly; the ViewPanel variant - just forwards its camera. Headless code never touches Swing. -- ~RenderingContext.getImage()~ — hands out the backing BufferedImage - the rasterizer paints into. -- ~GlobalIllumination.isRunning()~ / ~isConverged()~ / - ~getWorkItemCount()~ — GI state became inspectable. - -** Snapshot: render without a window -:PROPERTIES: -:CUSTOM_ID: snapshot -:END: - -#+BEGIN_SRC java -import eu.svjatoslav.aukio.e3d.headless.Snapshot; - -ShapeCollection scene = new ShapeCollection(); -scene.addShape(myShape); -LightingManager lighting = new LightingManager(); -lighting.setAmbientLight(Color.hex("181818")); - -// One call: build context, transform, sort, paint, return the image. -BufferedImage image = Snapshot.render(scene, lighting, - "290.31, -35.59, -2.10, -0.58, -0.15, 0.0", 640, 480); -Snapshot.save(image, "/tmp/snapshot.png"); -#+END_SRC - -The pose string is the *same "x, y, z, yaw, pitch, roll" format the -demos print* and users quote in bug reports — paste the pose, reproduce -the exact view. ~Snapshot.cameraFromPose()~ and ~Snapshot.poseString()~ -convert in both directions. - -For tests that need to detect *unpainted* pixels (holes), the -~renderInto()~ variant fills the background with a caller-chosen -sentinel color first, so "nothing was painted here" is unambiguous even -in a pitch-black scene: - -#+BEGIN_SRC java -RenderingContext ctx = new RenderingContext(640, 480, 1); -ctx.lightingManager = lighting; -Snapshot.renderInto(scene, camera, ctx, 0x00010203); // sentinel -#+END_SRC - -A real headless render — the House demo from a bug-report pose: - -#+attr_html: :class responsive-img -#+attr_latex: :width 640px -[[file:Agentic development/snapshot-example.png]] - -** PixelAssertions: did this region get painted? -:PROPERTIES: -:CUSTOM_ID: pixel-assertions -:END: - -The recurring debugging question — "did the floor actually render, or -did clipping eat it?" — becomes a library call: - -#+BEGIN_SRC java -import eu.svjatoslav.aukio.e3d.headless.PixelAssertions; - -// Fraction of a relative rectangle still equal to the background: -double holes = PixelAssertions.unpaintedFraction(image, 0, - 0.15, 0.45, 0.85, 1.0); // lower-center band -if (holes > 0.05) - throw new AssertionError("floor has holes: " + holes); - -long red = PixelAssertions.countColor(image, 0xFF0000); // flat-color tests -String grid = PixelAssertions.dumpPixelGrid(image, 320, 240, 3, 8); // hex dump -#+END_SRC - -#+INCLUDE: "Agentic development/Pixel assertion.svg" export html - -** GoldenImage: compare against a reference -:PROPERTIES: -:CUSTOM_ID: golden-image -:END: - -A pixel counts as different when any RGB channel drifts more than a -per-channel tolerance; the comparison fails when the fraction of -differing pixels exceeds a threshold. Deterministic flat-shaded renders -can use tight tolerances (4, 0.005); noisier paths relax them. - -#+BEGIN_SRC java -import eu.svjatoslav.aukio.e3d.headless.GoldenImage; - -GoldenImage.Result r = GoldenImage.compare(actual, - new File("goldens/house-flat.png"), 4, 0.005); -if (!r.passed) - GoldenImage.saveDiff(actual, goldenFile, "/tmp/diff.png"); // red = differs -#+END_SRC - -#+INCLUDE: "Agentic development/Golden workflow.svg" export html - -There is also a CLI for shell scripts — exit 0 = match, 1 = differ: - -#+BEGIN_SRC bash -java eu.svjatoslav.aukio.e3d.headless.GoldenImage actual.png golden.png 4 0.005 -#+END_SRC - -A real diff: the house rendered with the living-room lamp removed, -compared against the golden. The red region is exactly the room that -lost its light: - -#+attr_html: :class responsive-img -#+attr_latex: :width 640px -[[file:Agentic development/diff-example.png]] - -** SceneDump: the reproducible bug report -:PROPERTIES: -:CUSTOM_ID: scene-dump -:END: - -One call produces everything needed to reproduce what a frame shows: - -#+BEGIN_SRC java -System.out.println(SceneDump.dump(scene, lighting, camera, gi)); -#+END_SRC - -#+BEGIN_EXAMPLE -== SceneDump == -shapes: 5 top-level, 546 queued for rendering -lights: 4 (ambient #181818) - [0] pos=(-800.0, -240.0, 0.0) color=FFD890 intensity=6.0 - [1] pos=(0.0, -240.0, 0.0) color=D8E4FF intensity=5.0 - [2] pos=(800.0, -240.0, 0.0) color=FFB060 intensity=6.0 - [3] pos=(250.0, -60.0, -250.0) color=60FF90 intensity=2.0 -camera: 290.31, -35.59, -2.10, -0.58, -0.15, -0.00 -GI: running, 152034 work items, converged -#+END_EXAMPLE - -The camera line is a pose string — it feeds straight back into -~Snapshot.render()~. - -** HouseGoldens: ready-made regression tests -:PROPERTIES: -:CUSTOM_ID: house-goldens -:END: - -The aukio-3d-demos repo contains a working example of all of the above: -~eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens~ renders the -House demo at two poses (the default view and the near-plane straddle -bug pose), compares both against committed goldens, and independently -asserts the floor has no holes. - -#+BEGIN_SRC bash -cd aukio-3d-demos -mvn clean package -mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt -java -cp "target/classes:$(cat cp.txt)" \ - eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens # verify -java -cp "target/classes:$(cat cp.txt)" \ - eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens --update # regenerate goldens -#+END_SRC - -Exit code 0 = all pass, 1 = any mismatch (with a diff PNG in /tmp). -Demos that want the same treatment expose their scene construction: -~HouseDemo.buildHouse()~, ~addFurniture()~ and ~addLights()~ are public -for exactly this reason. - -** export-docs.sh: regenerate the documentation -:PROPERTIES: -:CUSTOM_ID: export-docs -:END: - -All engine documentation (the pages you are reading) lives as org-mode -files under =doc/=. One script exports every page to HTML with the -darksun theme: - -#+BEGIN_SRC bash -doc/export-docs.sh # export all pages -doc/export-docs.sh --check # + render every page with headless Chrome - # to /tmp/doc-check-*.png for visual review -#+END_SRC - -The =--check= mode is how an agent verifies its own documentation: SVG -label collisions, broken image links and table breakage all show up in -the rendered screenshots. - -** A typical agent session -:PROPERTIES: -:CUSTOM_ID: typical-session -:END: - -#+BEGIN_EXAMPLE -1. Reproduce: Snapshot.render(scene, lighting, bugReportPose, 640, 480) -2. Inspect: SceneDump.dump(...) + view the PNG -3. Fix the engine -4. Verify: PixelAssertions.unpaintedFraction(...) == 0 -5. Regression: HouseGoldens (must stay ALL PASS) -6. Document: edit doc pages, export-docs.sh --check, review shots -#+END_EXAMPLE - -** Related Classes -:PROPERTIES: -:CUSTOM_ID: related-classes -:END: - -| Class | Purpose | -|-----------------+----------------------------------------------------| -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/Snapshot.html][Snapshot]] | Windowless render facade + pose string conversion | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/PixelAssertions.html][PixelAssertions]] | Painted-region / color-count / hex-grid assertions | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/GoldenImage.html][GoldenImage]] | Golden-PNG comparison, diff writer, CLI | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/SceneDump.html][SceneDump]] | Scene state as a reproducible text block | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.html][ShapeCollection]] | ~transformShapes(Camera, ...)~ headless overload | -| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] | ~getImage()~ exposes the painted frame | - -* Source code -:PROPERTIES: -:CUSTOM_ID: source-code -:ID: 978b7ea2-e246-45d0-be76-4d561308e9f3 -:END: - -*This program is free software: released under Creative Commons Zero -(CC0) license* - -*Program author:* -- Svjatoslav Agejenko -- Homepage: https://svjatoslav.eu -- Email: mailto://svjatoslav@svjatoslav.eu -- See also: [[https://www.svjatoslav.eu/projects/][Other software projects hosted at svjatoslav.eu]] - -*Getting the source code:* -- [[https://www2.svjatoslav.eu/gitweb/?p=aukio-3d.git;a=snapshot;h=HEAD;sf=tgz][Download latest source code snapshot in TAR GZ format]] -- [[https://www2.svjatoslav.eu/gitweb/?p=aukio-3d.git;a=summary][Browse Git repository online]] -- Clone Git repository using command: - : git clone https://www3.svjatoslav.eu/git/aukio-3d.git diff --git a/doc/style.css b/doc/style.css deleted file mode 100644 index 3403e0e..0000000 --- a/doc/style.css +++ /dev/null @@ -1,35 +0,0 @@ -.flex-center { - display: flex; - justify-content: center; -} - -.flex-center video { - width: min(90%, 1000px); - height: auto; -} - -.responsive-img { - width: min(100%, 1000px); - height: auto; -} - -/* === SVG diagram theme === */ -svg > rect:first-child { - fill: #061018; -} - -svg text[fill="#666"], -svg text[fill="#999"] { - fill: #aaa !important; -} - -svg line[stroke="#ccc"] { - stroke: #445566 !important; -} - -svg { - background-color: #061018; - border-radius: 8px; - display: block; - margin: 0 auto; -} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3cfe059..2eefa3c 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 eu.svjatoslav aukio-3d - 1.5-SNAPSHOT + 1.0.0-SNAPSHOT Aukio 3D 3D engine