Now system will need to compute each unique point in 3D only
once. Polygons can share coordinates.
-
* Features
:PROPERTIES:
:CUSTOM_ID: features
** Main render loop
-- To understand main render loop, see dedicated page: [[file:rendering-loop/][Rendering loop]]
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 620 80" width="620" height="80" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <marker id="arrowhead" viewBox="0 0 10 10" refX="9" refY="5"
+ markerWidth="6" markerHeight="6" orient="auto">
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#30a050"/>
+ </marker>
+ </defs>
+ <rect width="620" height="80" fill="#061018"/>
+
+ <!-- Boxes -->
+ <rect x="15" y="25" width="90" height="30" rx="3" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+ <text x="60" y="43" fill="#30a050" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Shapes</text>
+
+ <rect x="140" y="25" width="90" height="30" rx="3" fill="rgba(32,112,192,0.15)" stroke="#2070c0" stroke-width="1.5"/>
+ <text x="185" y="43" fill="#2070c0" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Transform</text>
+
+ <rect x="265" y="25" width="70" height="30" rx="3" fill="rgba(200,80,140,0.15)" stroke="#c05088" stroke-width="1.5"/>
+ <text x="300" y="43" fill="#c05088" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Sort</text>
+
+ <rect x="365" y="25" width="80" height="30" rx="3" fill="rgba(255,102,0,0.15)" stroke="#FF6600" stroke-width="1.5"/>
+ <text x="405" y="43" fill="#FF6600" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Paint</text>
+
+ <rect x="480" y="25" width="60" height="30" rx="3" fill="rgba(57,255,20,0.15)" stroke="#39FF14" stroke-width="1.5"/>
+ <text x="510" y="43" fill="#39FF14" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Blit</text>
+
+ <rect x="570" y="25" width="35" height="30" rx="3" fill="rgba(100,100,100,0.2)" stroke="#aaa" stroke-width="1.5"/>
+ <text x="587" y="43" fill="#aaa" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Screen</text>
+
+ <!-- Arrows -->
+ <line x1="105" y1="40" x2="135" y2="40" stroke="#30a050" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+ <line x1="230" y1="40" x2="260" y2="40" stroke="#2070c0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+ <line x1="335" y1="40" x2="360" y2="40" stroke="#c05088" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+ <line x1="445" y1="40" x2="475" y2="40" stroke="#FF6600" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+ <line x1="540" y1="40" x2="565" y2="40" stroke="#39FF14" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+
+ <!-- Labels below -->
+ <text x="60" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">3D vertices</text>
+ <text x="185" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">world→screen</text>
+ <text x="300" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">back-to-front</text>
+ <text x="405" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">8 threads</text>
+ <text x="510" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">copy buffer</text>
+</svg>
+#+END_EXPORT
+
+To understand main render loop, see dedicated page: [[file:rendering-loop/][Rendering loop]]
** Coordinate System (X, Y, Z)
:PROPERTIES:
*lighting* — the angle between the light direction and the normal
determines how bright a surface appears.
-- *Face normal*: one normal per triangle
-- *Vertex normal*: one normal per vertex (averaged from adjacent faces for smooth shading)
-- =dot(L, N)= → surface brightness
-- Flat shading → face normals
-- Gouraud/Phong → vertex normals + interpolation
+**Use cases:**
+
+| Use case | API | Computation | Location |
+|----------------------+----------------------------------------------+----------------------------+-------------------|
+| BSP/CSG operations | [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#getPlane()][SolidPolygon.getPlane()]] → [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Plane.html#normal][Plane.normal]] | Lazy-cached once | =Plane= |
+| Per-frame shading | [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]] → =cachedNormal= field | Recomputed every frame | =SolidPolygon= |
+| Lighting calculation | [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.html#computeLighting()][LightingManager.computeLighting()]] | Uses normal via =dot(L,N)= | =LightingManager= |
+
+**Implementation notes:**
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]]: shared zero-allocation helper for computing normals from three points
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Plane.html][Plane]]: stores normals in Hesse normal form (normal + distance) for BSP spatial partitioning
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/math/Vertex.html#normal][Vertex.normal]]: optional field for CSG polygon splitting (not used for rendering)
+
+** Shading & Lighting
+
+#+attr_html: :width 600px
+#+attr_latex: :width 600px
+[[file:shading/Shaded%20sphere.png]]
+
+
+*Sixth 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]].
** Mesh
:PROPERTIES:
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:
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 620 80" width="620" height="80" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <marker id="arrowhead" viewBox="0 0 10 10" refX="9" refY="5"
+ markerWidth="6" markerHeight="6" orient="auto">
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#30a050"/>
+ </marker>
+ </defs>
+ <rect width="620" height="80" fill="#061018"/>
+
+ <!-- Boxes -->
+ <rect x="15" y="25" width="90" height="30" rx="3" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+ <text x="60" y="43" fill="#30a050" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Shapes</text>
+
+ <rect x="140" y="25" width="90" height="30" rx="3" fill="rgba(32,112,192,0.15)" stroke="#2070c0" stroke-width="1.5"/>
+ <text x="185" y="43" fill="#2070c0" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Transform</text>
+
+ <rect x="265" y="25" width="70" height="30" rx="3" fill="rgba(200,80,140,0.15)" stroke="#c05088" stroke-width="1.5"/>
+ <text x="300" y="43" fill="#c05088" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Sort</text>
+
+ <rect x="365" y="25" width="80" height="30" rx="3" fill="rgba(255,102,0,0.15)" stroke="#FF6600" stroke-width="1.5"/>
+ <text x="405" y="43" fill="#FF6600" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Paint</text>
+
+ <rect x="480" y="25" width="60" height="30" rx="3" fill="rgba(57,255,20,0.15)" stroke="#39FF14" stroke-width="1.5"/>
+ <text x="510" y="43" fill="#39FF14" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Blit</text>
+
+ <rect x="570" y="25" width="35" height="30" rx="3" fill="rgba(100,100,100,0.2)" stroke="#aaa" stroke-width="1.5"/>
+ <text x="587" y="43" fill="#aaa" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Screen</text>
+
+ <!-- Arrows -->
+ <line x1="105" y1="40" x2="135" y2="40" stroke="#30a050" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+ <line x1="230" y1="40" x2="260" y2="40" stroke="#2070c0" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+ <line x1="335" y1="40" x2="360" y2="40" stroke="#c05088" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+ <line x1="445" y1="40" x2="475" y2="40" stroke="#FF6600" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+ <line x1="540" y1="40" x2="565" y2="40" stroke="#39FF14" stroke-width="1.5" marker-end="url(#arrowhead)"/>
+
+ <!-- Labels below -->
+ <text x="60" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">3D vertices</text>
+ <text x="185" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">world→screen</text>
+ <text x="300" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">back-to-front</text>
+ <text x="405" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">8 threads</text>
+ <text x="510" y="67" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">copy buffer</text>
+</svg>
+#+END_EXPORT
+
+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]]) |
+| Sort | Unordered shapes | Ordered by depth | Ensure correct visibility (far objects painted first) |
+| Paint | Sorted shapes | Pixels in buffer | Clear segments (parallel) and rasterize triangles into pixel array |
+| Blit | Pixel buffer | Screen image | Copy completed frame to display |
+
+This pipeline runs repeatedly, typically 60 times per second (60 FPS).
+Even if nothing moves, the loop continues running—but the engine [[#frame-listeners][skips
+unnecessary work]] when the scene is static.
+
** Main loop structure
:PROPERTIES:
:CUSTOM_ID: main-loop-structure
The engine supports two modes:
-- *Target FPS mode*: Set with [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html#setFrameRate(int)][setFrameRate(int)]]. The thread sleeps
- between frames to maintain the target rate. If rendering takes
- longer than the frame interval, the engine catches up naturally
- without sleeping.
+- *Target FPS mode*: Set with [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html#setFrameRate(int)][setFrameRate(int)]].
+ The engine tries to maintain the target rate by sleeping between frames.
-- *Unlimited mode*: Set =setFrameRate(0)= or negative. No sleeping —
- renders as fast as possible. Useful for benchmarking.
+ - *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.
-** Frame listeners
-:PROPERTIES:
-:CUSTOM_ID: frame-listeners
-:END:
+ - *When rendering is faster than target*: The thread sleeps to limit FPS
+ to the target rate, avoiding unnecessary CPU usage.
-Before each frame, the engine notifies all registered [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/FrameListener.html][FrameListener]]s:
-
-#+BEGIN_SRC java
-viewPanel.addFrameListener((panel, deltaMs) -> {
- // Update animations, physics, game logic
- shape.rotate(0.01);
- return true; // true = force repaint
-});
-#+END_SRC
+ 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)
-Frame listeners can trigger repaints by returning =true=. Built-in listeners include:
-- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/Camera.html][Camera]] — handles keyboard/mouse navigation
-- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.html][InputManager]] — processes input events
+- *Unlimited mode*: Set =setFrameRate(0)= or negative. No sleeping —
+ renders as fast as possible. Useful for benchmarking.
* Rendering phases
:PROPERTIES:
:CUSTOM_ID: rendering-phases
:END:
-Each frame goes through 6 phases. Open the Developer Tools panel (F12)
-to see these phases logged in real-time:
+Each frame goes through 5 phases:
-** Phase 1: Clear canvas
+** Phase 1: Transform shapes
:PROPERTIES:
-:CUSTOM_ID: phase-1-clear-canvas
-:END:
-
-The pixel buffer is filled with the background color (default: black).
-
-#+BEGIN_SRC java
-Arrays.fill(pixels, 0, width * height, backgroundColorRgb);
-#+END_SRC
-
-This is a simple =Arrays.fill= operation — very fast, single-threaded.
-
-** Phase 2: Transform shapes
-:PROPERTIES:
-:CUSTOM_ID: phase-2-transform-shapes
+:CUSTOM_ID: phase-1-transform-shapes
:END:
All shapes are transformed from world space to screen space:
- Calculate =onScreenZ= for depth 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.
+
This is single-threaded but very fast — just math, no pixel operations.
-** Phase 3: Sort shapes
+** Phase 2: Sort shapes
:PROPERTIES:
-:CUSTOM_ID: phase-3-sort-shapes
+:CUSTOM_ID: phase-2-sort-shapes
:END:
Shapes are sorted by =onScreenZ= (depth) in descending order:
Collections.sort(queuedShapes, (a, b) -> Double.compare(b.onScreenZ, a.onScreenZ));
#+END_SRC
-Back-to-front sorting is essential for correct transparency and
-occlusion. Shapes further from the camera are painted first.
+*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.
+
+#+BEGIN_EXPORT html
+
+<svg viewBox="0 0 520 180" width="520" height="180" xmlns="http://www.w3.org/2000/svg">
+ <rect width="520" height="180" fill="#061018"/>
+ <!-- Far -->
+ <rect x="30" y="15" width="440" height="150" fill="rgba(48,160,80,0.06)" stroke="rgba(48,160,80,0.35)" stroke-width="1.5"/>
+ <text x="250" y="38" fill="rgba(48,160,80,0.7)" font-size="11" font-family="monospace" text-anchor="middle">Far (Z=500) — painted first</text>
+ <!-- Medium -->
+ <rect x="70" y="48" width="360" height="105" fill="rgba(32,112,192,0.10)" stroke="rgba(32,112,192,0.5)" stroke-width="1.5"/>
+ <text x="250" y="80" fill="rgba(32,112,192,0.85)" font-size="11" font-family="monospace" text-anchor="middle">Medium (Z=300) — painted second</text>
+ <!-- Near -->
+ <rect x="115" y="90" width="270" height="55" fill="rgba(200,80,140,0.18)" stroke="#c05088" stroke-width="2"/>
+ <text x="250" y="123" fill="#c05088" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">Near (Z=100) — painted last</text>
+</svg>
+#+END_EXPORT
+
+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.
-** Phase 4: Paint shapes (multi-threaded)
+The =onScreenZ= value represents distance from the camera after
+transformation. Larger values = further away.
+
+** Phase 3: Clear and paint segments (multi-threaded)
:PROPERTIES:
-:CUSTOM_ID: phase-4-paint-shapes
+:CUSTOM_ID: phase-3-clear-paint-segments
:END:
-The screen is divided into 8 horizontal segments, each rendered by a separate thread:
+The screen is divided into 8 horizontal segments, each processed by a separate thread. Each thread performs two operations:
+
+1. *Clear segment*: Fill its Y-range with background color
+2. *Paint shapes*: Render all shapes within that Y-range
+
+Both operations happen within the same thread task, ensuring clearing completes before painting begins. This provides greater RAM bandwidth utilization compared to single-threaded clearing.
#+BEGIN_EXPORT html
<svg viewBox="0 0 400 200" width="400" height="200">
Each thread:
- Gets a =SegmentRenderingContext= with Y-bounds (minY, maxY)
+- Clears its segment to background color (parallel memory fill)
- Iterates all shapes and paints pixels within its Y-range
- Clips triangles/lines at segment boundaries
- Detects mouse hits (before clipping)
The fixed thread pool (=Executors.newFixedThreadPool(8)=) avoids the
overhead of creating threads per frame.
-** Phase 5: Combine mouse results
+**Why parallel clearing?** Each thread clears its own memory region
+(disjoint Y-ranges), avoiding synchronization overhead while maximizing
+memory bandwidth utilization on multi-core systems.
+
+** Phase 4: Combine mouse results
:PROPERTIES:
-:CUSTOM_ID: phase-5-combine-mouse-results
+:CUSTOM_ID: phase-4-combine-mouse-results
:END:
During painting, each segment tracks which shape is under the mouse cursor.
Since all segments paint the same shapes (just different Y-ranges), they
-should all report the same hit. Phase 5 takes the first non-null result:
+should all report the same hit. Phase 4 takes the first non-null result:
#+BEGIN_SRC java
for (SegmentRenderingContext ctx : segmentContexts) {
}
#+END_SRC
-** Phase 6: Blit to screen
+** Phase 5: Blit to screen
:PROPERTIES:
-:CUSTOM_ID: phase-6-blit-to-screen
+:CUSTOM_ID: phase-5-blit-to-screen
:END:
The rendered =BufferedImage= is copied to the screen using
-[[https://docs.oracle.com/javase/21/docs/api/java/awt/image/BufferStrategy.html][BufferStrategy]] for tear-free page-flipping:
+[[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 {
Toolkit.getDefaultToolkit().sync();
#+END_SRC
+*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.
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 520 180" width="520" height="180" xmlns="http://www.w3.org/2000/svg">
+ <rect width="520" height="180" fill="#061018"/>
+
+ <!-- LEFT: Tearing -->
+ <text x="125" y="18" fill="#d04040" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">Without double-buffering</text>
+
+ <rect x="25" y="28" width="200" height="140" stroke="rgba(100,100,100,0.4)" stroke-width="1" fill="none" rx="3"/>
+ <text x="125" y="44" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">display shows partial update</text>
+
+ <!-- Old frame top half -->
+ <rect x="45" y="52" width="160" height="45" fill="rgba(208,64,64,0.12)" stroke="rgba(208,64,64,0.4)" stroke-width="1"/>
+ <text x="125" y="79" fill="rgba(208,64,64,0.6)" font-size="9" font-family="monospace" text-anchor="middle">old frame</text>
+
+ <!-- Tear line -->
+ <line x1="45" y1="97" x2="205" y2="97" stroke="#d04040" stroke-width="2" stroke-dasharray="4,3"/>
+ <text x="228" y="100" fill="#d04040" font-size="8" font-family="monospace">← tear</text>
+
+ <!-- New frame bottom half -->
+ <rect x="45" y="97" width="160" height="55" fill="rgba(48,160,80,0.1)" stroke="rgba(48,160,80,0.4)" stroke-width="1"/>
+ <text x="125" y="130" fill="rgba(48,160,80,0.6)" font-size="9" font-family="monospace" text-anchor="middle">new frame</text>
+
+ <!-- RIGHT: Double-buffered -->
+ <text x="400" y="18" fill="#30a050" font-size="11" font-weight="700" font-family="monospace" text-anchor="middle">With double-buffering</text>
+
+ <!-- Back buffer -->
+ <rect x="295" y="35" width="90" height="130" fill="rgba(32,112,192,0.08)" stroke="rgba(32,112,192,0.5)" stroke-width="1.5" rx="2"/>
+ <text x="340" y="58" fill="#2070c0" font-size="9" font-family="monospace" text-anchor="middle">Back buffer</text>
+ <text x="340" y="72" fill="#555" font-size="7" font-family="monospace" text-anchor="middle">(draw here)</text>
+ <!-- Scribble lines to suggest "being drawn" -->
+ <line x1="310" y1="90" x2="365" y2="90" stroke="rgba(32,112,192,0.25)" stroke-width="1"/>
+ <line x1="310" y1="100" x2="355" y2="100" stroke="rgba(32,112,192,0.2)" stroke-width="1"/>
+ <line x1="310" y1="110" x2="345" y2="110" stroke="rgba(32,112,192,0.15)" stroke-width="1"/>
+
+ <!-- Swap arrow -->
+ <line x1="390" y1="100" x2="415" y2="100" stroke="#30a050" stroke-width="1.5"/>
+ <polygon points="415,96 423,100 415,104" fill="#30a050"/>
+ <text x="407" y="90" fill="#30a050" font-size="7" font-family="monospace" text-anchor="middle">swap</text>
+
+ <!-- Front buffer -->
+ <rect x="428" y="35" width="80" height="130" fill="rgba(48,160,80,0.1)" stroke="#30a050" stroke-width="1.5" rx="2"/>
+ <text x="468" y="58" fill="#30a050" font-size="9" font-family="monospace" text-anchor="middle">Front buffer</text>
+ <text x="468" y="72" fill="#555" font-size="7" font-family="monospace" text-anchor="middle">(displayed)</text>
+ <!-- Solid fill to suggest complete frame -->
+ <rect x="440" y="85" width="56" height="65" fill="rgba(48,160,80,0.08)" rx="1"/>
+ <text x="468" y="122" fill="rgba(48,160,80,0.5)" font-size="8" font-family="monospace" text-anchor="middle">complete</text>
+ <text x="468" y="133" fill="rgba(48,160,80,0.5)" font-size="8" font-family="monospace" text-anchor="middle">frame</text>
+</svg>
+#+END_EXPORT
+
+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.
-* Smart repaint skipping
+
+* Frame listeners and smart repaint skipping
:PROPERTIES:
-:CUSTOM_ID: smart-repaint-skipping
+:CUSTOM_ID: frame-listeners
+:ID: e360a877-cca6-4cba-a9a4-ea40b0f1a183
:END:
-The engine avoids unnecessary rendering:
+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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html#addFrameListener(eu.svjatoslav.sixth.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 velocity is zero* (built-in Camera listener returns false when not moving)
+- *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.
+
+#+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
-- =viewRepaintNeeded= flag: Set to =true= only when something changes
-- Frame listeners can return =false= to skip repaint
-- Resizing, component events, and explicit =repaintDuringNextViewUpdate()=
- calls set the flag
+The engine registers these listeners by default:
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/Camera.html][Camera]] — returns true when user is actively navigating (velocity > 0)
+- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.html][InputManager]] — processes mouse/keyboard events
-This means a static scene consumes almost zero CPU — the render thread
-just spins checking the flag.
+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:
| =pixels[]= | Raw pixel buffer (int[] in RGB format) |
| =bufferedImage= | Java2D wrapper around pixels |
| =graphics= | Graphics2D for text, lines, shapes |
-| =width=, =height= | Screen dimensions |
+| =width=, =height= | Rendering area dimensions |
| =centerCoordinate= | Screen center (for projection) |
| =projectionScale= | Perspective scale factor |
| =frameNumber= | Monotonically increasing frame counter |
--- /dev/null
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Shading & Lighting - Sixth 3D
+#+LANGUAGE: en
+#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry}
+#+LATEX_HEADER: \usepackage{parskip}
+#+LATEX_HEADER: \usepackage[none]{hyphenat}
+
+#+OPTIONS: H:20 num:20
+#+OPTIONS: author:nil
+
+#+begin_export html
+<style>
+ .flex-center {
+ display: flex;
+ justify-content: center;
+ }
+ .flex-center video {
+ width: min(90%, 1000px);
+ height: auto;
+ }
+ .responsive-img {
+ width: min(100%, 1000px);
+ height: auto;
+ }
+
+ /* === SVG diagram theme === */
+ svg > rect:first-child {
+ fill: #061018;
+ }
+
+ /* Lighten axis/helper labels that were dark-on-light */
+ svg text[fill="#666"],
+ svg text[fill="#999"] {
+ fill: #aaa !important;
+ }
+
+ /* Lighten dashed axis lines */
+ svg line[stroke="#ccc"] {
+ stroke: #445566 !important;
+ }
+
+</style>
+#+end_export
+
+[[file:../index.org][Back to main documentation]]
+
+* Shading & Lighting
+:PROPERTIES:
+:CUSTOM_ID: shading-lighting
+:END:
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 1000px
+[[file:Shaded sphere.png]]
+
+*Sixth 3D* implements *flat shading* using the 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:
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 520 240" width="520" height="240" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <marker id="arrow-light" viewBox="0 0 10 10" refX="10" refY="5"
+ markerWidth="7" markerHeight="7" orient="auto-start-reverse">
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#FF6600"/>
+ </marker>
+ </defs>
+ <rect width="520" height="240" fill="#061018"/>
+
+ <!-- Polygon surface -->
+ <polygon points="80,180 220,180 180,60 120,60"
+ fill="rgba(48,160,80,0.12)" stroke="#30a050" stroke-width="1.5"/>
+
+ <!-- Normal vector (perpendicular to surface) -->
+ <line x1="150" y1="120" x2="150" y2="40" stroke="#30a050" stroke-width="2.5"/>
+ <polygon points="150,40 145,52 155,52" fill="#30a050"/>
+ <text x="158" y="38" fill="#30a050" font-size="12" font-weight="700" font-family="monospace">N̂</text>
+ <text x="158" y="50" fill="#999" font-size="9" font-family="monospace">normal</text>
+
+ <!-- Light source -->
+ <circle cx="350" cy="80" r="12" fill="rgba(255,102,0,0.25)" stroke="#FF6600" stroke-width="2"/>
+ <text x="368" y="84" fill="#FF6600" font-size="11" font-weight="700" font-family="monospace">Light</text>
+
+ <!-- Light direction vector -->
+ <line x1="350" y1="80" x2="170" y2="120" stroke="#FF6600" stroke-width="2" stroke-dasharray="5 3" marker-end="url(#arrow-light)"/>
+ <text x="250" y="92" fill="#FF6600" font-size="10" font-family="monospace" text-anchor="middle">L̂</text>
+
+ <!-- Angle arc between normal and light direction -->
+ <path d="M150,60 A60,60 0 0,1 180,80" fill="none" stroke="#b09020" stroke-width="1.5"/>
+ <text x="182" y="75" fill="#b09020" font-size="10" font-weight="700" font-family="monospace">θ</text>
+
+ <!-- Formula box -->
+ <rect x="380" y="140" width="120" height="80" rx="4" fill="rgba(32,112,192,0.1)" stroke="#2070c0" stroke-width="1"/>
+ <text x="390" y="165" fill="#2070c0" font-size="11" font-weight="700" font-family="monospace">brightness =</text>
+ <text x="390" y="185" fill="#2070c0" font-size="11" font-family="monospace">dot(N̂, L̂)</text>
+ <text x="390" y="205" fill="#999" font-size="9" font-family="monospace">= cos(θ)</text>
+
+ <!-- Brightness indicator -->
+ <rect x="80" y="200" width="140" height="20" rx="2" fill="rgba(48,160,80,0.3)" stroke="#30a050" stroke-width="1"/>
+ <rect x="80" y="200" width="85" height="20" rx="2" fill="#30a050"/>
+ <text x="85" y="214" fill="#fff" font-size="9" font-weight="600" font-family="monospace">brightness</text>
+ <text x="232" y="214" fill="#999" font-size="9" font-family="monospace">← angle determines intensity</text>
+</svg>
+#+END_EXPORT
+
+The *Lambert cosine law* states that the brightness of a surface depends
+on the angle between its normal vector and the light direction:
+
+- Surface facing the light (θ = 0°): maximum brightness (=cos(0°) = 1.0=)
+- Surface at 45° angle: moderate brightness (=cos(45°) = 0.71=)
+- Surface perpendicular to light (θ = 90°): no direct light (=cos(90°) = 0.0=)
+
+This is computed as the *dot product* of the unit normal vector (N̂) and
+unit light direction vector (L̂). The dot product automatically gives the
+cosine of the angle between them.
+
+** 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.sixth.e3d.renderer.raster.lighting.LightSource;
+import eu.svjatoslav.sixth.e3d.geometry.Point3D;
+import eu.svjatoslav.sixth.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.
+
+** Ambient Light
+:PROPERTIES:
+:CUSTOM_ID: ambient-light
+:END:
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 320 140" width="320" height="140" xmlns="http://www.w3.org/2000/svg">
+ <rect width="320" height="140" fill="#061018"/>
+
+ <!-- Dark polygon without ambient -->
+ <polygon points="40,120 140,120 120,40 80,40"
+ fill="rgba(10,10,10,0.8)" stroke="#555" stroke-width="1"/>
+ <text x="85" y="70" fill="#666" font-size="9" font-family="monospace" text-anchor="middle">no ambient</text>
+ <text x="85" y="82" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">(pure black)</text>
+
+ <!-- Arrow -->
+ <text x="165" y="75" fill="#30a050" font-size="16" font-family="monospace">→</text>
+
+ <!-- Lit polygon with ambient -->
+ <polygon points="180,120 280,120 260,40 220,40"
+ fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1"/>
+ <text x="225" y="70" fill="#30a050" font-size="9" font-family="monospace" text-anchor="middle">ambient</text>
+ <text x="225" y="82" fill="#30a050" font-size="8" font-family="monospace" text-anchor="middle="(50,50,50)"></text>
+
+ <!-- Formula -->
+ <text x="160" y="135" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">ambient provides base illumination</text>
+</svg>
+#+END_EXPORT
+
+*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)
+- 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
+
+** Distance Attenuation
+:PROPERTIES:
+:CUSTOM_ID: distance-attenuation
+:END:
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 520 180" width="520" height="180" xmlns="http://www.w3.org/2000/svg">
+ <rect width="520" height="180" fill="#061018"/>
+
+ <!-- Light source -->
+ <circle cx="80" cy="90" r="10" fill="rgba(255,102,0,0.3)" stroke="#FF6600" stroke-width="2"/>
+ <text x="92" y="94" fill="#FF6600" font-size="11" font-weight="700" font-family="monospace">Light</text>
+
+ <!-- Distance markers -->
+ <line x1="80" y1="110" x2="450" y2="110" stroke="#555" stroke-width="1" stroke-dasharray="4 3"/>
+ <text x="180" y="125" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">d = 100</text>
+ <text x="330" y="125" fill="#666" font-size="8" font-family="monospace" text-anchor="middle">d = 250</text>
+
+ <!-- Polygon at close distance -->
+ <polygon points="160,80 200,80 190,50 170,50"
+ fill="rgba(48,160,80,0.25)" stroke="#30a050" stroke-width="1.5"/>
+ <text x="175" y="72" fill="#30a050" font-size="8" font-weight="600" font-family="monospace">bright</text>
+
+ <!-- Polygon at medium distance -->
+ <polygon points="300,80 340,80 330,50 310,50"
+ fill="rgba(48,160,80,0.12)" stroke="#30a050" stroke-width="1"/>
+ <text x="315" y="72" fill="#30a050" font-size="8" font-family="monospace">medium</text>
+
+ <!-- Polygon at far distance -->
+ <polygon points="420,80 450,80 445,55 425,55"
+ fill="rgba(48,160,80,0.06)" stroke="rgba(48,160,80,0.4)" stroke-width="0.8"/>
+ <text x="433" y="72" fill="rgba(48,160,80,0.6)" font-size="8" font-family="monospace">dim</text>
+
+ <!-- Formula -->
+ <rect x="380" y="135" width="120" height="35" rx="3" fill="rgba(32,112,192,0.1)" stroke="#2070c0" stroke-width="1"/>
+ <text x="390" y="155" fill="#2070c0" font-size="9" font-family="monospace">attenuation =</text>
+ <text x="390" y="167" fill="#2070c0" font-size="9" font-family="monospace">1 / (1 + 0.0001·d²)</text>
+
+ <!-- Explanation -->
+ <text x="260" y="175" fill="#999" font-size="9" font-family="monospace" text-anchor="middle">Simplified inverse square law avoids harsh cutoffs</text>
+</svg>
+#+END_EXPORT
+
+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 Sixth 3D.
+
+** Integration with the Render Pipeline
+:PROPERTIES:
+:CUSTOM_ID: render-pipeline-integration
+:END:
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 620 80" width="620" height="80" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <marker id="arrowhead2" viewBox="0 0 10 10" refX="9" refY="5"
+ markerWidth="6" markerHeight="6" orient="auto">
+ <path d="M 0 0 L 10 5 L 0 10 z" fill="#30a050"/>
+ </marker>
+ </defs>
+ <rect width="620" height="80" fill="#061018"/>
+
+ <!-- Transform phase (where shading happens) -->
+ <rect x="140" y="25" width="90" height="30" rx="3" fill="rgba(176,144,32,0.15)" stroke="#b09020" stroke-width="2"/>
+ <text x="185" y="43" fill="#b09020" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Transform</text>
+ <text x="185" y="67" fill="#b09020" font-size="8" font-family="monospace" text-anchor="middle">compute lighting</text>
+
+ <!-- Other phases -->
+ <rect x="15" y="25" width="90" height="30" rx="3" fill="rgba(48,160,80,0.15)" stroke="#30a050" stroke-width="1.5"/>
+ <text x="60" y="43" fill="#30a050" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Shapes</text>
+
+ <rect x="265" y="25" width="70" height="30" rx="3" fill="rgba(200,80,140,0.15)" stroke="#c05088" stroke-width="1.5"/>
+ <text x="300" y="43" fill="#c05088" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Sort</text>
+
+ <rect x="365" y="25" width="80" height="30" rx="3" fill="rgba(255,102,0,0.15)" stroke="#FF6600" stroke-width="1.5"/>
+ <text x="405" y="43" fill="#FF6600" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Paint</text>
+ <text x="405" y="67" fill="#999" font-size="8" font-family="monospace" text-anchor="middle">use cached color</text>
+
+ <rect x="480" y="25" width="60" height="30" rx="3" fill="rgba(57,255,20,0.15)" stroke="#39FF14" stroke-width="1.5"/>
+ <text x="510" y="43" fill="#39FF14" font-size="10" font-weight="700" font-family="monospace" text-anchor="middle">Blit</text>
+
+ <!-- Arrows -->
+ <line x1="105" y1="40" x2="135" y2="40" stroke="#30a050" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
+ <line x1="230" y1="40" x2="260" y2="40" stroke="#2070c0" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
+ <line x1="335" y1="40" x2="360" y2="40" stroke="#c05088" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
+ <line x1="445" y1="40" x2="475" y2="40" stroke="#FF6600" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
+ <line x1="540" y1="40" x2="565" y2="40" stroke="#39FF14" stroke-width="1.5" marker-end="url(#arrowhead2)"/>
+</svg>
+#+END_EXPORT
+
+Lighting is computed during *Phase 2* (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/sixth-3d/apidocs/eu/svjatoslav/sixth/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?**
+
+- Transform phase is *single-threaded* — no race conditions
+- Lighting computed *once per polygon per frame* — not per pixel
+- Result reused during multi-threaded paint phase — efficient
+
+** Using Shading in Your Scene
+:PROPERTIES:
+:CUSTOM_ID: using-shading
+:END:
+
+**Adding light sources:**
+
+#+BEGIN_SRC java
+import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
+import eu.svjatoslav.sixth.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.sixth.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/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setShadingEnabled(boolean)][setShadingEnabled(true)]] on a composite enables shading for all its
+sub-polygons.
+
+** Performance Characteristics
+:PROPERTIES:
+:CUSTOM_ID: performance
+:END:
+
+| Aspect | Cost |
+|--------------+-------------------------------|
+| Computation | Per polygon, not per pixel |
+| Phase | Single-threaded (transform) |
+| 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*: Single-threaded transform phase avoids synchronization
+- *Pre-computed*: All 8 paint threads use 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.
+
+** Related Classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class | Purpose |
+|-------+---------|
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] | Manages light sources and computes shading |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/lighting/LightSource.html][LightSource]] | Individual light with position, color, intensity |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Polygon shape with shading support |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] | Composite shape with shading propagation |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html][ViewPanel]] | Provides access to LightingManager |
\ No newline at end of file
this.distance = distance;
}
+ /**
+ * Computes the unit normal vector for a triangle defined by three points.
+ *
+ * <p>Zero-allocation method: fills the result point instead of creating a new one.
+ * This is the shared implementation used by both {@link #fromPoints} and
+ * {@link SolidPolygon} for shading calculations.</p>
+ *
+ * <p>The normal is computed as the cross product of two edge vectors (b-a and c-a),
+ * then normalized to unit length.</p>
+ *
+ * @param a first point (base point for edge vectors)
+ * @param b second point
+ * @param c third point
+ * @param result Point3D to receive the unit normal vector (modified in place)
+ * @return true if normal computed successfully, false if points are collinear
+ * (cross product magnitude less than EPSILON)
+ */
+ public static boolean computeNormal(final Point3D a, final Point3D b,
+ final Point3D c, final Point3D result) {
+ // Edge vectors from a to b and a to c
+ final double ax = b.x - a.x;
+ final double ay = b.y - a.y;
+ final double az = b.z - a.z;
+
+ final double bx = c.x - a.x;
+ final double by = c.y - a.y;
+ final double bz = c.z - a.z;
+
+ // Cross product: (edge1 × edge2)
+ double nx = ay * bz - az * by;
+ double ny = az * bx - ax * bz;
+ double nz = ax * by - ay * bx;
+
+ // Normalize
+ final double length = Math.sqrt(nx * nx + ny * ny + nz * nz);
+ if (length < EPSILON) {
+ result.x = result.y = result.z = 0;
+ return false;
+ }
+
+ result.x = nx / length;
+ result.y = ny / length;
+ result.z = nz / length;
+ return true;
+ }
+
/**
* Creates a plane from three non-collinear points.
*
+ * <p>Uses {@link #computeNormal} for the normal calculation, then computes
+ * the signed distance from origin using the dot product.</p>
+ *
* @param a the first point on the plane
* @param b the second point on the plane
* @param c the third point on the plane
* @return a new Plane passing through the three points
+ * @throws ArithmeticException if the points are collinear (cannot define a plane)
*/
public static Plane fromPoints(final Point3D a, final Point3D b, final Point3D c) {
- final Point3D edge1 = b.withSubtracted(a);
- final Point3D edge2 = c.withSubtracted(a);
-
- final Point3D cross = edge1.cross(edge2);
-
- if (cross.getVectorLength() < EPSILON) {
+ final Point3D n = new Point3D();
+ if (!computeNormal(a, b, c, n)) {
throw new ArithmeticException(
"Cannot create plane from collinear points: cross product is zero");
}
-
- final Point3D n = cross.unit();
-
return new Plane(n, n.dot(a));
}
// === Render ONCE to offscreen buffer ===
// The offscreen bufferedImage is unaffected by BufferStrategy contentsRestored(),
// so we only need to render once, then retry the blit if needed.
- clearCanvasAllSegments();
rootShapeCollection.transformShapes(this, renderingContext);
rootShapeCollection.sortShapes();
- // Phase 4: Paint segments in parallel
+ // Phase 4: Clear and paint segments in parallel
final int height = renderingContext.height;
final int segmentHeight = height / RenderingContext.NUM_RENDER_SEGMENTS;
final SegmentRenderingContext[] segmentContexts = new SegmentRenderingContext[RenderingContext.NUM_RENDER_SEGMENTS];
renderExecutor.submit(() -> {
try {
+ clearSegmentPixels(segmentContexts[segmentIndex]);
rootShapeCollection.paintShapes(segmentContexts[segmentIndex]);
} finally {
latch.countDown();
}
}
- private void clearCanvasAllSegments() {
+ /**
+ * Clears a single segment's pixel area to the background color.
+ * Called by each render thread before painting shapes.
+ *
+ * @param ctx the segment rendering context with Y-bounds
+ */
+ private void clearSegmentPixels(final SegmentRenderingContext ctx) {
final int rgb = (backgroundColor.r << 16) | (backgroundColor.g << 8) | backgroundColor.b;
final int width = renderingContext.width;
- final int height = renderingContext.height;
final int[] pixels = renderingContext.pixels;
-
- if (developerTools.renderAlternateSegments) {
- // Clear only even segments (0, 2, 4, 6), leave odd segments with previous frame content
- // This helps visualize what content would be overdrawn by the missing segment renders
- final int segmentHeight = height / RenderingContext.NUM_RENDER_SEGMENTS;
- for (int seg = 0; seg < RenderingContext.NUM_RENDER_SEGMENTS; seg += 2) {
- final int minY = seg * segmentHeight;
- final int maxY = (seg == RenderingContext.NUM_RENDER_SEGMENTS - 1) ? height : (seg + 1) * segmentHeight;
- Arrays.fill(pixels, minY * width, maxY * width, rgb);
- }
- // Odd segments intentionally NOT cleared - retain previous frame's rendered content
- } else {
- Arrays.fill(pixels, 0, width * height, rgb);
- }
+
+ Arrays.fill(pixels, ctx.renderMinY * width, ctx.renderMaxY * width, rgb);
}
private void combineMouseResults(final SegmentRenderingContext[] segmentContexts) {
* <p>Lazy-computed on first call to {@link #getPlane()}.</p>
*/
private Plane plane;
- /**
- * Flag indicating whether the plane has been computed.
- */
- private boolean planeComputed = false;
/**
* The fill color of this polygon.
*/
* @return the Plane containing this polygon
*/
public Plane getPlane() {
- if (!planeComputed) {
+ if (plane == null) {
plane = Plane.fromPoints(
vertices.get(0).coordinate,
vertices.get(1).coordinate,
vertices.get(2).coordinate
);
- planeComputed = true;
}
return plane;
}
*/
public void flip() {
Collections.reverse(vertices);
- for (final Vertex v : vertices) {
- v.flip();
- }
- if (planeComputed) {
- plane.flip();
- }
+ for (final Vertex vertex : vertices) vertex.flip();
+ if (plane != null) plane.flip();
}
/**
return clone;
}
- /**
- * Calculates the unit normal vector of this polygon.
- *
- * @param result the point to store the normal vector in
- */
- private void calculateNormal(final Point3D result) {
- if (vertices.size() < 3) {
- result.x = result.y = result.z = 0;
- return;
- }
-
- final Point3D v0 = vertices.get(0).coordinate;
- final Point3D v1 = vertices.get(1).coordinate;
- final Point3D v2 = vertices.get(2).coordinate;
-
- final double ax = v1.x - v0.x;
- final double ay = v1.y - v0.y;
- final double az = v1.z - v0.z;
-
- final double bx = v2.x - v0.x;
- final double by = v2.y - v0.y;
- final double bz = v2.z - v0.z;
-
- double nx = ay * bz - az * by;
- double ny = az * bx - ax * bz;
- double nz = ax * by - ay * bx;
-
- final double length = Math.sqrt(nx * nx + ny * ny + nz * nz);
- if (length > 0.0001) {
- nx /= length;
- ny /= length;
- nz /= length;
- }
-
- result.x = nx;
- result.y = ny;
- result.z = nz;
- }
-
/**
* Calculates the centroid (geometric center) of this polygon.
*
// Compute lighting once during transform phase (single-threaded)
if (shadingEnabled && renderingContext.lightingManager != null) {
calculateCenter(cachedCenter);
- calculateNormal(cachedNormal);
+ // Compute normal from first 3 vertices
+ Plane.computeNormal(
+ vertices.get(0).coordinate,
+ vertices.get(1).coordinate,
+ vertices.get(2).coordinate,
+ cachedNormal
+ );
renderingContext.lightingManager.computeLighting(
cachedCenter, cachedNormal, color, shadedColor);
}