From: Svjatoslav Agejenko Date: Mon, 6 Apr 2026 13:52:00 +0000 (+0300) Subject: docs(rendering): document render pipeline, lighting, and optimize normals X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=a4dd962eb49179a0b31548f876091ac1e580813e;p=sixth-3d.git docs(rendering): document render pipeline, lighting, and optimize normals Add comprehensive documentation for the rendering pipeline with SVG diagrams showing the Shapes→Transform→Sort→Paint→Blit flow. Document double-buffering, painter's algorithm, and frame listeners. Add shading & lighting guide covering Lambert cosine law, light sources, ambient light, and distance attenuation with visual diagrams. Refactor Plane to expose computeNormal() for zero-allocation normal computation, and move canvas clearing into parallel paint phase for better memory bandwidth utilization. --- diff --git a/TODO.org b/TODO.org index 51448e8..ca72dc7 100644 --- a/TODO.org +++ b/TODO.org @@ -72,7 +72,6 @@ the sweet spot. Now system will need to compute each unique point in 3D only once. Polygons can share coordinates. - * Features :PROPERTIES: :CUSTOM_ID: features diff --git a/doc/index.org b/doc/index.org index 1fbf08f..7d8863f 100644 --- a/doc/index.org +++ b/doc/index.org @@ -152,7 +152,52 @@ Also add the repository (the library is not on Maven Central): ** Main render loop -- To understand main render loop, see dedicated page: [[file:rendering-loop/][Rendering loop]] +#+BEGIN_EXPORT html + + + + + + + + + + + Shapes + + + Transform + + + Sort + + + Paint + + + Blit + + + Screen + + + + + + + + + + 3D vertices + world→screen + back-to-front + 8 threads + copy buffer + +#+END_EXPORT + +To understand main render loop, see dedicated page: [[file:rendering-loop/][Rendering loop]] ** Coordinate System (X, Y, Z) :PROPERTIES: @@ -328,11 +373,31 @@ 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. -- *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: diff --git a/doc/rendering-loop/index.org b/doc/rendering-loop/index.org index 1f09cac..2a06c19 100644 --- a/doc/rendering-loop/index.org +++ b/doc/rendering-loop/index.org @@ -37,6 +37,77 @@ 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: + +#+BEGIN_EXPORT html + + + + + + + + + + + Shapes + + + Transform + + + Sort + + + Paint + + + Blit + + + Screen + + + + + + + + + + 3D vertices + world→screen + back-to-front + 8 threads + copy buffer + +#+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 @@ -61,57 +132,34 @@ You can stop it explicitly with [[https://www3.svjatoslav.eu/projects/sixth-3d/a 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: @@ -123,11 +171,24 @@ 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: @@ -136,15 +197,47 @@ 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 + + + + + + Far (Z=500) — painted first + + + Medium (Z=300) — painted second + + + Near (Z=100) — painted last + +#+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 @@ -170,6 +263,7 @@ The screen is divided into 8 horizontal segments, each rendered by a separate th 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) @@ -180,14 +274,18 @@ A =CountDownLatch= waits for all 8 threads to complete before proceeding. 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) { @@ -198,13 +296,13 @@ 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 { @@ -217,25 +315,138 @@ bufferStrategy.show(); 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 + + + + + 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 + +#+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: @@ -249,7 +460,7 @@ The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e | =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 | diff --git a/doc/shading/Shaded sphere.png b/doc/shading/Shaded sphere.png new file mode 100644 index 0000000..fbc6487 Binary files /dev/null and b/doc/shading/Shaded sphere.png differ diff --git a/doc/shading/index.org b/doc/shading/index.org new file mode 100644 index 0000000..62f6d62 --- /dev/null +++ b/doc/shading/index.org @@ -0,0 +1,415 @@ +#+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 + +#+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 + + + + + + + + + + + + + + + N̂ + normal + + + + Light + + + + L̂ + + + + θ + + + + brightness = + dot(N̂, L̂) + = cos(θ) + + + + + brightness + ← angle determines intensity + +#+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 + + + + + + no ambient + (pure black) + + + → + + + + ambient + + + + ambient provides base illumination + +#+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 + + + + + + Light + + + + d = 100 + d = 250 + + + + bright + + + + medium + + + + dim + + + + attenuation = + 1 / (1 + 0.0001·d²) + + + Simplified inverse square law avoids harsh cutoffs + +#+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 + + + + + + + + + + + Transform + compute lighting + + + + Shapes + + + Sort + + + Paint + use cached color + + + Blit + + + + + + + + +#+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 diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Plane.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Plane.java index 48d526e..b208c87 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Plane.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Plane.java @@ -49,27 +49,70 @@ public class Plane { this.distance = distance; } + /** + * Computes the unit normal vector for a triangle defined by three points. + * + *

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.

+ * + *

The normal is computed as the cross product of two edge vectors (b-a and c-a), + * then normalized to unit length.

+ * + * @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. * + *

Uses {@link #computeNormal} for the normal calculation, then computes + * the signed distance from origin using the dot product.

+ * * @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)); } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java index 1405aca..fe77551 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java @@ -383,11 +383,10 @@ public class ViewPanel extends Canvas { // === 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]; @@ -408,6 +407,7 @@ public class ViewPanel extends Canvas { renderExecutor.submit(() -> { try { + clearSegmentPixels(segmentContexts[segmentIndex]); rootShapeCollection.paintShapes(segmentContexts[segmentIndex]); } finally { latch.countDown(); @@ -474,25 +474,18 @@ public class ViewPanel extends Canvas { } } - 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) { diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java index 1e5033a..4aa3632 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java @@ -107,10 +107,6 @@ public class SolidPolygon extends AbstractCoordinateShape { *

Lazy-computed on first call to {@link #getPlane()}.

*/ private Plane plane; - /** - * Flag indicating whether the plane has been computed. - */ - private boolean planeComputed = false; /** * The fill color of this polygon. */ @@ -502,13 +498,12 @@ public class SolidPolygon extends AbstractCoordinateShape { * @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; } @@ -524,12 +519,8 @@ public class SolidPolygon extends AbstractCoordinateShape { */ 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(); } /** @@ -550,45 +541,6 @@ public class SolidPolygon extends AbstractCoordinateShape { 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. * @@ -766,7 +718,13 @@ public class SolidPolygon extends AbstractCoordinateShape { // 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); }