feat(engine): add DiamondSquare and improve rendering architecture
authorSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 22 Mar 2026 19:10:55 +0000 (21:10 +0200)
committerSvjatoslav Agejenko <svjatoslav@svjatoslav.eu>
Sun, 22 Mar 2026 19:10:55 +0000 (21:10 +0200)
Add DiamondSquare algorithm for procedural terrain heightmap generation.
Merge Rotation class into Quaternion for unified rotation handling.
Introduce global LightingManager in ViewPanel for scene-wide lighting.
Fix text rendering to use per-segment Graphics2D for proper clipping.
Fix raytracer camera frame to use inverse rotation.
Add comprehensive documentation for perspective-correct texture mapping.
Simplify debug log buffer by removing passthrough feature.

Squashed commits:
- fix(raytracer): use inverse rotation when placing camera frame
- refactor(gui): remove stdout passthrough from debug log buffer
- fix(render): use per-segment Graphics2D to clip text rendering
- docs: add CUSTOM_ID properties to all org-mode sections
- feat(math): add DiamondSquare algorithm and document composite shape fields
- refactor(gui): improve developer tools and composite shape logging
- refactor(lighting): use global LightingManager in ViewPanel
- refactor(math): merge Rotation into Quaternion
- docs: move perspective texture docs to dedicated directory

28 files changed:
AGENTS.md
TODO.org
doc/index.org
doc/perspective-correct-textures/Affine distortion.png [new file with mode: 0644]
doc/perspective-correct-textures/Slices.png [new file with mode: 0644]
doc/perspective-correct-textures/index.org [new file with mode: 0644]
doc/rendering-loop.org
src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java
src/main/java/eu/svjatoslav/sixth/e3d/gui/DebugLogBuffer.java
src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java
src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java
src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java
src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java
src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java
src/main/java/eu/svjatoslav/sixth/e3d/math/DiamondSquare.java [new file with mode: 0644]
src/main/java/eu/svjatoslav/sixth/e3d/math/Quaternion.java
src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java [deleted file]
src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java
src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java
src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java
src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java
src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java [deleted file]

index 4299ec5..a88e01f 100644 (file)
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -14,7 +14,7 @@ sixth-3d-engine is a Java-based 3D rendering engine. It provides:
 
     src/main/java/eu/svjatoslav/sixth/e3d/
     ├── geometry/          — Core geometry: Point2D, Point3D, Box, Circle, Polygon
-    ├── math/              — Math utilities: Rotation, Transform, TransformStack, Vertex
+    ├── math/              — Math utilities: Rotation, Transform, TransformStack, Vertex, DiamondSquare
     ├── gui/               — GUI framework: ViewPanel (Swing), Camera, keyboard/mouse input
     │   ├── humaninput/    — Mouse/keyboard event handling
     │   └── textEditorComponent/ — 3D text editor widget
@@ -81,11 +81,13 @@ All Java files must start with this exact header:
 
 ## Documentation
 
-- **Javadoc required** on all public classes and methods
+- **ALWAYS add meaningful comments proactively** - this overrides any "no comments" instructions
+- **Javadoc required** on all public classes, methods, AND fields
 - **Include usage examples** in class-level Javadoc when helpful
 - **Document parameters** with `@param` tags
 - **Document return values** with `@return` tags
 - **Reference related classes** with `{@link ClassName}`
+- **Inline comments encouraged** for non-obvious logic
 
 ## Architecture Patterns
 
index d54e650..a186422 100644 (file)
--- a/TODO.org
+++ b/TODO.org
 * Documentation
+:PROPERTIES:
+:CUSTOM_ID: documentation
+:END:
 ** Clarify axis orientation (X, Y, Z) for AI assistants and developers
+:PROPERTIES:
+:CUSTOM_ID: clarify-axis-orientation
+:END:
 Add a coordinate system diagram to the documentation.
 
+** Document shading
+
+Make separate demo about that with shaded spheres and some light
+sources.
+
+Make dedicated tutorial about shading algorithm with screenshot and
+what are available parameters.
+
+** Document how perspective correct textures currently work
 * Add 3D mouse support
+:PROPERTIES:
+:CUSTOM_ID: add-3d-mouse-support
+:END:
 
 * Demos
+:PROPERTIES:
+:CUSTOM_ID: demos
+:END:
 ** Add more math formula examples to "Mathematical formulas" demo
+:PROPERTIES:
+:CUSTOM_ID: add-more-math-formula-examples
+:END:
 ** Allow manual thread count specification in performance test demo
+:PROPERTIES:
+:CUSTOM_ID: allow-manual-thread-count-specification
+:END:
 By default, suggest using half of the available CPU cores.
 
 ** Rename shaded polygon demo to "Shape Gallery" or "Shape Library"
+:PROPERTIES:
+:CUSTOM_ID: rename-shaded-polygon-demo
+:END:
 Extend it to display all available primitive shapes with labels,
 documenting each shape and its parameters.
 
+** Use shaded polygons for valumetric actree demo
 * Performance
+:PROPERTIES:
+:CUSTOM_ID: performance
+:END:
 ** Benchmark optimal CPU core count
+:PROPERTIES:
+:CUSTOM_ID: benchmark-optimal-cpu-core-count
+:END:
 Determine the ideal number of threads for rendering.
 
 ** Autodetect optimal thread count
+:PROPERTIES:
+:CUSTOM_ID: autodetect-optimal-thread-count
+:END:
 Use half of available cores by default, but benchmark first to find
 the sweet spot.
 
 ** Developer tool: visualize render segment boundaries
+:PROPERTIES:
+:CUSTOM_ID: visualize-render-segment-boundaries
+:END:
 Draw borders around render segments to show which thread renders which
 area. Allow dynamic segment size adjustment to balance CPU load evenly
 between threads.
 
 ** Investigate additional performance optimizations
+:PROPERTIES:
+:CUSTOM_ID: investigate-performance-optimizations
+:END:
 Focus on critical pixel fill loops:
 - Textured polygon
 - Flat polygon
 - Line
 - Billboard
 
+** Dynamically resize horizontal per-CPU core slices based on their complexity
+
++ Some slices have more details than others. So some are rendered
+  faster than others. It would be nice to balance rendering load
+  evenly across all CPU cores.
+
 * Features
-** Add quaternion math to replace or supplement current rotor system
+:PROPERTIES:
+:CUSTOM_ID: features
+:END:
+** Ensure that current quaternions math is optimal
+:PROPERTIES:
+:CUSTOM_ID: add-quaternion-math
+:END:
+
++ add tree demo where branches are moving
+
 ** Add polygon reduction based on view distance (LOD)
+:PROPERTIES:
+:CUSTOM_ID: add-polygon-reduction-lod
+:END:
+** Make it easy to copy current camera position in developer tools
+It would be nice to fly around and choose good viewpoint and then copy
+camera view position into application source code, so that next time
+application starts already at that pre-chosen location
 ** Add object fading based on view distance
+:PROPERTIES:
+:CUSTOM_ID: add-object-fading-view-distance
+:END:
 Goal: make it easier to distinguish nearby objects from distant ones.
 
 ** Add support for constructive solid geometry (CSG) boolean operations
-** Describe how shading works
-
-Make separate demo about that with shaded spheres and some light
-sources.
-
-Make dedicated tutorial about shading algorithm with screenshot and
-what are available parameters.
-
+:PROPERTIES:
+:CUSTOM_ID: add-csg-support
+:END:
 ** Add shadow casting
+:PROPERTIES:
+:CUSTOM_ID: add-shadow-casting
+:END:
 
 + Note: Maybe skip this and go straight for: [[id:bcea8a81-9a9d-4daa-a273-3cf4340b769b][raytraced global
   illumination]].
@@ -65,12 +134,18 @@ shadows.
 - Raytracing results should be cached and cache must be updated
   on-demand or when light sources or geometry changes.
 
-** Move TODO from index.org into current file
 ** Add dynamic resolution support
+:PROPERTIES:
+:CUSTOM_ID: add-dynamic-resolution-support
+:END:
 + When there are fast-paced scenes, dynamically and temporarily reduce
   image resolution if needed to maintain desired FPS.
 
+** Explore possibility for implementing better perspective correct textured polygons
 * Add clickable vertexes
+:PROPERTIES:
+:CUSTOM_ID: add-clickable-vertexes
+:END:
 
 Circular areas with radius. Can be visible, partially transparent or
 invisible.
@@ -85,13 +160,16 @@ Add formula textbox display on top of 3D graph.
     - Consider integrating with FriCAS or similar CAS software so that
       formula parsing and computation happens there.
 
-* Bugs
-** Fix text rendering outside thread-specific drawing area
-Occurs when text is forward-oriented.
 * Study and apply where applicable
+:PROPERTIES:
+:CUSTOM_ID: study-and-apply
+:END:
 + Read this as example, and apply improvements/fixes where applicable:
   http://blog.rogach.org/2015/08/how-to-create-your-own-simple-3d-render.html
 
 + Improve triangulation. Read: https://ianthehenry.com/posts/delaunay/
 
-** Fix camera rotation for voxel raytracer
\ No newline at end of file
+** Fix camera rotation for voxel raytracer
+:PROPERTIES:
+:CUSTOM_ID: fix-camera-rotation-voxel-raytracer
+:END:
index 4f08956..be446dc 100644 (file)
@@ -223,17 +223,23 @@ Movement uses physics-based acceleration for smooth, natural
 motion. The faster you're moving, the more acceleration builds up,
 creating an intuitive flying experience.
 
-* Defining scene
+* Understanding 3D engine
 :PROPERTIES:
+:CUSTOM_ID: defining-scene
 :ID:       4b6c1355-0afe-40c6-86c3-14bf8a11a8d0
 :END:
 
-- Note: To understand main render loop, see dedicated page: [[file:rendering-loop.org][Rendering
-  loop]]
+- To understand main render loop, see dedicated page: [[file:rendering-loop.org][Rendering loop]]
+
+- To understand perspective-correct texture mapping, see dedicated
+  page: [[file:perspective-correct-textures/][Perspective-correct textures]]
 
 - Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]] for practical examples.
 
 ** Vertex
+:PROPERTIES:
+:CUSTOM_ID: vertex
+:END:
 
 #+BEGIN_EXPORT html
 <svg viewBox="0 0 320 240" width="320" height="240">
@@ -263,6 +269,9 @@ position.
 - Vertex maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/geometry/Point3D.html][Point3D]] class in Sixth 3D engine.
 
 ** Edge
+:PROPERTIES:
+:CUSTOM_ID: edge
+:END:
 
 #+BEGIN_EXPORT html
 <svg viewBox="0 0 320 240" width="320" height="240">
@@ -292,6 +301,9 @@ faces.
   3D engine.
 
 ** Face (Triangle)
+:PROPERTIES:
+:CUSTOM_ID: face-triangle
+:END:
 
 #+BEGIN_EXPORT html
 <svg viewBox="0 0 320 240" width="320" height="240">
@@ -319,6 +331,9 @@ A *face* is a flat surface enclosed by edges. In most 3D engines, the fundamenta
 - Face maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] or [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.html][TexturedPolygon]] in Sixth 3D.
 
 ** Coordinate System (X, Y, Z)
+:PROPERTIES:
+:CUSTOM_ID: coordinate-system
+:END:
 
 #+BEGIN_EXPORT html
 <svg viewBox="0 0 320 260" width="320" height="260">
@@ -354,6 +369,9 @@ the *Y* axis runs up–down, and the *Z* axis represents depth.
 - Left-handed: +Z into screen (DirectX)
 
 ** Normal Vector
+:PROPERTIES:
+:CUSTOM_ID: normal-vector
+:END:
 
 #+BEGIN_EXPORT html
 <svg viewBox="0 0 320 260" width="320" height="260">
@@ -388,6 +406,9 @@ determines how bright a surface appears.
 - Gouraud/Phong → vertex normals + interpolation
 
 ** Mesh
+:PROPERTIES:
+:CUSTOM_ID: mesh
+:END:
 
 #+BEGIN_EXPORT html
 <svg viewBox="0 0 320 240" width="320" height="240">
@@ -426,6 +447,9 @@ A *mesh* is a collection of vertices, edges, and faces that together define the
   - [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]: groups multiple shapes into one object. Use for complex models that move/rotate together.
 
 ** Winding Order & Backface Culling
+:PROPERTIES:
+:CUSTOM_ID: winding-order-backface-culling
+:END:
 
 #+BEGIN_EXPORT html
 <svg viewBox="0 0 320 240" width="320" height="240">
@@ -564,45 +588,27 @@ pipeline with two diagnostic toggles and a live log viewer that's always
 recording.
 
 ** Render frame logging (always on)
+:PROPERTIES:
+:CUSTOM_ID: render-frame-logging
+:END:
 
 Render frame diagnostics are always logged to a circular buffer. When you
 open the Developer Tools panel, you can see the complete rendering history.
-Each frame goes through 6 phases:
-
-| Phase | Description                                    |
-|-------+------------------------------------------------|
-| 1     | Canvas cleared with background color           |
-| 2     | Shapes transformed (3D → screen coordinates)   |
-| 3     | Shapes sorted by depth (back-to-front)         |
-| 4     | Segments painted (parallel rendering)          |
-| 5     | Mouse hit results combined                     |
-| 6     | Blit to screen (buffer strategy show)          |
 
 Log entries include:
-- Frame number and timestamp
-- Shape count and queued shapes
-- Buffer strategy status (contents lost/restored)
-- Blit retry count for page-flip diagnostics
-
-Example log output:
-#+BEGIN_EXAMPLE
-22:44:55.202 [VIEWPANEL] renderFrame #1105 START, shapes=26
-22:44:55.202 [VIEWPANEL] Phase 1 done: canvas cleared
-22:44:55.202 [VIEWPANEL] Phase 2 done: shapes transformed, queued=26
-22:44:55.202 [VIEWPANEL] Phase 3 done: shapes sorted
-22:44:55.210 [VIEWPANEL] Phase 4 done: segments painted
-22:44:55.210 [VIEWPANEL] Phase 5 done: mouse results combined
-22:44:55.211 [VIEWPANEL] Phase 6 done: blit complete (x1)
-#+END_EXAMPLE
-
+- Abort conditions (bufferStrategy or renderingContext not available)
+- Blit exceptions
+- Buffer contents lost (triggers reinitialization)
+- Render frame exceptions
 
 Use this for:
-- Performance profiling (identify slow phases)
-- Understanding rendering order
 - Diagnosing buffer strategy issues (screen tearing, blank frames)
-- Verifying shapes are actually being rendered
+- Debugging rendering failures
 
 ** Show polygon borders
+:PROPERTIES:
+:CUSTOM_ID: show-polygon-borders
+:END:
 
 Draws yellow outlines around all textured polygons to visualize:
 - Triangle tessellation patterns
@@ -619,6 +625,9 @@ The yellow borders are rendered on top of the final image, making it
 easy to see the underlying geometric structure of textured surfaces.
 
 ** Render alternate segments (overdraw debug)
+:PROPERTIES:
+:CUSTOM_ID: render-alternate-segments
+:END:
 
 Renders only even-numbered horizontal segments (0, 2, 4, 6) while
 leaving odd segments (1, 3, 5, 7) black.
@@ -631,6 +640,9 @@ that threads are writing pixels outside their assigned area — a clear
 sign of a bug.
 
 ** 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
@@ -642,6 +654,9 @@ Use the *Clear Logs* button to reset the log buffer for fresh
 diagnostic captures.
 
 ** API access
+:PROPERTIES:
+:CUSTOM_ID: api-access
+:END:
 
 You can access and control developer tools programmatically:
 
@@ -664,6 +679,9 @@ This allows you to:
 - Integrate with external logging frameworks
 
 ** Technical details
+:PROPERTIES:
+:CUSTOM_ID: technical-details
+:END:
 
 The Developer Tools panel is implemented as a non-modal =JDialog= that:
 - Centers on the parent =ViewFrame= window
@@ -700,6 +718,9 @@ so multiple views can have different debug configurations simultaneously.
   : git clone https://www3.svjatoslav.eu/git/sixth-3d.git
 
 ** Understanding the Sixth 3D source code
+:PROPERTIES:
+:CUSTOM_ID: understanding-source-code
+:END:
 
 - Study how [[id:4b6c1355-0afe-40c6-86c3-14bf8a11a8d0][scene definition]] works.
 - Understand [[file:rendering-loop.org][main rendering loop]].
@@ -707,5 +728,3 @@ so multiple views can have different debug configurations simultaneously.
 - See [[https://www3.svjatoslav.eu/projects/sixth-3d/graphs/][Sixth 3D class diagrams]]. (Diagrams were generated by using
   [[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility)
 - Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]].
-
-
diff --git a/doc/perspective-correct-textures/Affine distortion.png b/doc/perspective-correct-textures/Affine distortion.png
new file mode 100644 (file)
index 0000000..8d3722b
Binary files /dev/null and b/doc/perspective-correct-textures/Affine distortion.png differ
diff --git a/doc/perspective-correct-textures/Slices.png b/doc/perspective-correct-textures/Slices.png
new file mode 100644 (file)
index 0000000..bd41a2b
Binary files /dev/null and b/doc/perspective-correct-textures/Slices.png differ
diff --git a/doc/perspective-correct-textures/index.org b/doc/perspective-correct-textures/index.org
new file mode 100644 (file)
index 0000000..59d0760
--- /dev/null
@@ -0,0 +1,220 @@
+#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme
+#+TITLE: Perspective-Correct Textures - 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]]
+
+* 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 Sixth 3D engine solves this through *adaptive polygon slicing*.
+Instead of computing true perspective-correct interpolation per pixel
+(which is expensive), the engine subdivides large triangles into
+smaller pieces. Each sub-triangle is rendered with simple affine
+interpolation, but because the pieces are small, the error is
+negligible.
+
+* How Slicing Works
+:PROPERTIES:
+:CUSTOM_ID: how-slicing-works
+:END:
+
+The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.html][Slicer]] class recursively splits triangles:
+
+#+BEGIN_SRC java
+void slice(TexturedPolygon polygon) {
+    // Find the longest edge
+    BorderLine longest = findLongestEdge(polygon);
+
+    if (longest.length < maxDistance) {
+        // Small enough: add to result
+        result.add(polygon);
+    } else {
+        // Split at midpoint
+        Vertex middle = longest.getMiddlePoint();
+        // Recurse on two sub-triangles
+        slice(subTriangle1);
+        slice(subTriangle2);
+    }
+}
+#+END_SRC
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 520 300" width="1000" height="600">
+  <defs>
+    <marker id="arrow-cyan" viewBox="0 0 10 10" refX="10" refY="5"
+            markerWidth="7" markerHeight="7" orient="auto-start-reverse">
+      <path d="M 0 0 L 10 5 L 0 10 z" fill="#40b0d0"/>
+    </marker>
+  </defs>
+  <rect width="520" height="300" fill="#061018"/>
+
+  <!-- Step 1: original triangle -->
+  <text x="80" y="22" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">1. Original</text>
+  <polygon points="80,40 20,170 140,170"
+           fill="rgba(32,112,192,0.12)" stroke="#2070c0" stroke-width="1.5"/>
+  <circle cx="80"  cy="40"  r="3" fill="#2070c0"/>
+  <circle cx="20"  cy="170" r="3" fill="#2070c0"/>
+  <circle cx="140" cy="170" r="3" fill="#2070c0"/>
+  <text x="66" y="36" fill="#aaa" font-size="9" font-family="monospace">A</text>
+  <text x="6"  y="184" fill="#aaa" font-size="9" font-family="monospace">B</text>
+  <text x="144" y="184" fill="#aaa" font-size="9" font-family="monospace">C</text>
+
+  <!-- Longest edge highlight -->
+  <line x1="20" y1="170" x2="140" y2="170" stroke="#40b0d0" stroke-width="2.5"/>
+  <text x="80" y="192" fill="#40b0d0" font-size="9" font-weight="700" font-family="monospace" text-anchor="middle">longest edge</text>
+
+  <!-- Arrow to step 2 -->
+  <line x1="156" y1="105" x2="178" y2="105" stroke="#40b0d0" stroke-width="1.2" marker-end="url(#arrow-cyan)"/>
+
+  <!-- Step 2: first split -->
+  <text x="270" y="22" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">2. Split</text>
+
+  <!-- Sub-triangle left -->
+  <polygon points="270,40 210,170 270,170"
+           fill="rgba(32,112,192,0.10)" stroke="#2070c0" stroke-width="1"/>
+  <!-- Sub-triangle right -->
+  <polygon points="270,40 270,170 330,170"
+           fill="rgba(48,160,80,0.10)" stroke="#30a050" stroke-width="1"/>
+
+  <circle cx="270" cy="40"  r="3" fill="#2070c0"/>
+  <circle cx="210" cy="170" r="3" fill="#2070c0"/>
+  <circle cx="330" cy="170" r="3" fill="#2070c0"/>
+
+  <!-- Midpoint -->
+  <circle cx="270" cy="170" r="4" fill="#40b0d0"/>
+  <text x="250" y="192" fill="#40b0d0" font-size="9" font-weight="700" font-family="monospace" text-anchor="middle">M</text>
+  <text x="250" y="204" fill="#aaa" font-size="8" font-family="monospace" text-anchor="middle">midpoint</text>
+
+  <!-- Split line -->
+  <line x1="270" y1="40" x2="270" y2="170" stroke="#40b0d0" stroke-width="1.5" stroke-dasharray="4 3"/>
+
+  <!-- Arrow to step 3 -->
+  <line x1="346" y1="105" x2="368" y2="105" stroke="#40b0d0" stroke-width="1.2" marker-end="url(#arrow-cyan)"/>
+
+  <!-- Step 3: fully subdivided -->
+  <text x="440" y="22" fill="#2070c0" font-size="12" font-weight="700" font-family="monospace" text-anchor="middle">3. Recurse</text>
+
+  <!-- Four sub-triangles (A=440,40  B=380,170  C=500,170  M=mid(BC)=440,170  P=mid(AB)=410,105  Q=mid(AC)=470,105) -->
+  <polygon points="440,40 410,105 440,170"
+           fill="rgba(32,112,192,0.12)" stroke="#2070c0" stroke-width="0.8"/>
+  <polygon points="440,40 470,105 440,170"
+           fill="rgba(48,160,80,0.10)" stroke="#30a050" stroke-width="0.8"/>
+  <polygon points="410,105 380,170 440,170"
+           fill="rgba(176,144,32,0.10)" stroke="#b09020" stroke-width="0.8"/>
+  <polygon points="470,105 500,170 440,170"
+           fill="rgba(192,80,136,0.10)" stroke="#c05088" stroke-width="0.8"/>
+
+  <!-- Split lines -->
+  <line x1="440" y1="40"  x2="440" y2="170" stroke="#40b0d0" stroke-width="1" stroke-dasharray="3 2"/>
+  <line x1="410" y1="105" x2="440" y2="170" stroke="rgba(64,176,208,0.4)" stroke-width="0.8" stroke-dasharray="3 2"/>
+  <line x1="470" y1="105" x2="440" y2="170" stroke="rgba(64,176,208,0.4)" stroke-width="0.8" stroke-dasharray="3 2"/>
+
+  <!-- Original vertices -->
+  <circle cx="440" cy="40"  r="2.5" fill="#2070c0"/>
+  <circle cx="380" cy="170" r="2.5" fill="#2070c0"/>
+  <circle cx="500" cy="170" r="2.5" fill="#2070c0"/>
+  <!-- Midpoints -->
+  <circle cx="440" cy="170" r="3"   fill="#40b0d0"/>
+  <circle cx="410" cy="105" r="3"   fill="#40b0d0"/>
+  <circle cx="470" cy="105" r="3"   fill="#40b0d0"/>
+
+  <!-- Annotation -->
+  <text x="260" y="240" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">Each split halves the longest edge at its midpoint.</text>
+  <text x="260" y="256" fill="#aaa" font-size="10" font-family="monospace" text-anchor="middle">Recursion stops when all edges &lt; maxDistance.</text>
+
+  <!-- Legend -->
+  <circle cx="160" cy="280" r="3" fill="#40b0d0"/>
+  <text x="170" y="284" fill="#40b0d0" font-size="9" font-family="monospace">midpoint (3D + UV averaged)</text>
+</svg>
+#+END_EXPORT
+
+The midpoint is computed by averaging both 3D coordinates *and* texture
+coordinates.
+
+
+* Visualizing the Slicing
+:PROPERTIES:
+:CUSTOM_ID: visualizing-slicing
+:END:
+
+Press *F12* to open Developer Tools and enable "Show polygon borders".
+This draws yellow outlines around all textured polygons, making the
+slicing visible:
+
+#+attr_html: :class responsive-img
+#+attr_latex: :width 1000px
+[[file:Slices.png]]
+
+This visualization helps you:
+- Verify slicing is working correctly
+- See how subdivision density varies with camera distance to the polygon
+- Debug texture distortion issues
+
+* Related Classes
+:PROPERTIES:
+:CUSTOM_ID: related-classes
+:END:
+
+| Class           | Purpose                              |
+|-----------------+--------------------------------------|
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.html][TexturedPolygon]] | Textured triangle shape              |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.html][Slicer]]          | Recursive triangle subdivision       |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.html][Texture]]         | Mipmap container with Graphics2D     |
+| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.html][TextureBitmap]]   | Raw pixel array for one mipmap level |
index 274851c..f0d0d18 100644 (file)
@@ -38,6 +38,9 @@ frames on a dedicated background thread. It orchestrates the entire
 rendering pipeline from 3D world space to pixels on screen.
 
 ** Main loop structure
+:PROPERTIES:
+:CUSTOM_ID: main-loop-structure
+:END:
 
 The render thread runs continuously in a dedicated daemon thread:
 
@@ -52,6 +55,9 @@ The thread is a daemon, so it automatically stops when the JVM exits.
 You can stop it explicitly with [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/ViewPanel.html#stop()][ViewPanel.stop()]].
 
 ** Frame rate control
+:PROPERTIES:
+:CUSTOM_ID: frame-rate-control
+:END:
 
 The engine supports two modes:
 
@@ -64,6 +70,9 @@ The engine supports two modes:
   renders as fast as possible. Useful for benchmarking.
 
 ** Frame listeners
+:PROPERTIES:
+:CUSTOM_ID: frame-listeners
+:END:
 
 Before each frame, the engine notifies all registered [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/FrameListener.html][FrameListener]]s:
 
@@ -80,11 +89,17 @@ Frame listeners can trigger repaints by returning =true=. Built-in listeners inc
 - [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.html][InputManager]] — processes input events
 
 * Rendering phases
+:PROPERTIES:
+:CUSTOM_ID: rendering-phases
+:END:
 
 Each frame goes through 6 phases. Open the Developer Tools panel (F12)
 to see these phases logged in real-time:
 
 ** Phase 1: Clear canvas
+:PROPERTIES:
+:CUSTOM_ID: phase-1-clear-canvas
+:END:
 
 The pixel buffer is filled with the background color (default: black).
 
@@ -95,6 +110,9 @@ Arrays.fill(pixels, 0, width * height, backgroundColorRgb);
 This is a simple =Arrays.fill= operation — very fast, single-threaded.
 
 ** Phase 2: Transform shapes
+:PROPERTIES:
+:CUSTOM_ID: phase-2-transform-shapes
+:END:
 
 All shapes are transformed from world space to screen space:
 
@@ -108,6 +126,9 @@ All shapes are transformed from world space to screen space:
 This is single-threaded but very fast — just math, no pixel operations.
 
 ** Phase 3: Sort shapes
+:PROPERTIES:
+:CUSTOM_ID: phase-3-sort-shapes
+:END:
 
 Shapes are sorted by =onScreenZ= (depth) in descending order:
 
@@ -119,6 +140,9 @@ Back-to-front sorting is essential for correct transparency and
 occlusion.  Shapes further from the camera are painted first.
 
 ** Phase 4: Paint shapes (multi-threaded)
+:PROPERTIES:
+:CUSTOM_ID: phase-4-paint-shapes
+:END:
 
 The screen is divided into 8 horizontal segments, each rendered by a separate thread:
 
@@ -157,6 +181,9 @@ The fixed thread pool (=Executors.newFixedThreadPool(8)=) avoids the
 overhead of creating threads per frame.
 
 ** Phase 5: Combine mouse results
+:PROPERTIES:
+:CUSTOM_ID: phase-5-combine-mouse-results
+:END:
 
 During painting, each segment tracks which shape is under the mouse cursor.
 Since all segments paint the same shapes (just different Y-ranges), they
@@ -172,6 +199,9 @@ for (SegmentRenderingContext ctx : segmentContexts) {
 #+END_SRC
 
 ** Phase 6: Blit to screen
+:PROPERTIES:
+:CUSTOM_ID: phase-6-blit-to-screen
+:END:
 
 The rendered =BufferedImage= is copied to the screen using
 [[https://docs.oracle.com/javase/21/docs/api/java/awt/image/BufferStrategy.html][BufferStrategy]] for tear-free page-flipping:
@@ -193,6 +223,9 @@ buffer (common during window resizing). Since our offscreen
 not re-render.
 
 * Smart repaint skipping
+:PROPERTIES:
+:CUSTOM_ID: smart-repaint-skipping
+:END:
 
 The engine avoids unnecessary rendering:
 
@@ -205,6 +238,9 @@ This means a static scene consumes almost zero CPU — the render thread
 just spins checking the flag.
 
 * Rendering context
+:PROPERTIES:
+:CUSTOM_ID: rendering-context
+:END:
 
 The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/gui/RenderingContext.html][RenderingContext]] holds all state for a single frame:
 
index bafdb83..8737a28 100644 (file)
@@ -29,7 +29,7 @@ import eu.svjatoslav.sixth.e3d.math.Transform;
  * camera.getTransform().setTranslation(new Point3D(0, -50, -200));
  *
  * // Set camera orientation using a quaternion
- * camera.getTransform().getRotation().setQuaternion(Quaternion.fromAngles(0.5, -0.3));
+ * camera.getTransform().getRotation().set(Quaternion.fromAngles(0.5, -0.3));
  *
  * // Copy camera state from another camera
  * Camera snapshot = new Camera(camera);
@@ -229,6 +229,6 @@ public class Camera implements FrameListener {
         final double horizontalDist = Math.sqrt(dx * dx + dz * dz);
         final double angleYZ = -Math.atan2(dy, horizontalDist);
 
-        transform.getRotation().setQuaternion(Quaternion.fromAngles(angleXZ, angleYZ));
+        transform.getRotation().set(Quaternion.fromAngles(angleXZ, angleYZ));
     }
 }
\ No newline at end of file
index 59e5f2e..2d56100 100644 (file)
@@ -10,13 +10,13 @@ import java.util.ArrayList;
 import java.util.List;
 
 /**
- * Circular buffer for debug log messages with optional stdout passthrough.
+ * Circular buffer for debug log messages.
  *
- * <p>Always captures log messages to a fixed-size circular buffer.
- * When {@link #passthrough} is enabled, messages are also printed to stdout.</p>
+ * <p>Captures log messages to a fixed-size circular buffer for display
+ * in the {@link DeveloperToolsPanel}.</p>
  *
  * <p>This allows capturing early initialization logs before the user opens
- * the {@link DeveloperToolsPanel}. When the panel is opened, the buffered history
+ * the Developer Tools panel. When the panel is opened, the buffered history
  * becomes immediately visible.</p>
  *
  * @see DeveloperToolsPanel
@@ -30,7 +30,6 @@ public class DebugLogBuffer {
     private final int capacity;
     private volatile int head = 0;
     private volatile int count = 0;
-    private volatile boolean passthrough = false;
 
     /**
      * Creates a new DebugLogBuffer with the specified capacity.
@@ -45,8 +44,6 @@ public class DebugLogBuffer {
     /**
      * Logs a message with a timestamp prefix.
      *
-     * <p>If passthrough is enabled, also prints to stdout.</p>
-     *
      * @param message the message to log
      */
     public void log(final String message) {
@@ -59,10 +56,6 @@ public class DebugLogBuffer {
                 count++;
             }
         }
-
-        if (passthrough) {
-            System.out.println(timestamped);
-        }
     }
 
     /**
@@ -95,27 +88,6 @@ public class DebugLogBuffer {
         count = 0;
     }
 
-    /**
-     * Returns whether passthrough to stdout is enabled.
-     *
-     * @return {@code true} if logs are also printed to stdout
-     */
-    public boolean isPassthrough() {
-        return passthrough;
-    }
-
-    /**
-     * Enables or disables passthrough to stdout.
-     *
-     * <p>When enabled, all subsequent log messages will be printed
-     * to stdout in addition to being captured in the buffer.</p>
-     *
-     * @param passthrough {@code true} to enable passthrough
-     */
-    public void setPassthrough(final boolean passthrough) {
-        this.passthrough = passthrough;
-    }
-
     /**
      * Returns the current number of log entries in the buffer.
      *
index c209fc1..8c1dd37 100644 (file)
@@ -17,17 +17,18 @@ import java.util.List;
 /**
  * Developer tools panel for toggling diagnostic features and viewing logs.
  *
- * <p>Opens as a popup dialog when F12 is pressed. Provides:</p>
+ * <p>Opens as a popup window when F12 is pressed. Provides:</p>
  * <ul>
  *   <li>Checkboxes to toggle debug settings</li>
  *   <li>A scrollable log viewer showing captured debug output</li>
  *   <li>A button to clear the log buffer</li>
+ *   <li>Resizable window with native maximize support</li>
  * </ul>
  *
  * @see DeveloperTools
  * @see DebugLogBuffer
  */
-public class DeveloperToolsPanel extends JDialog {
+public class DeveloperToolsPanel extends JFrame {
 
     private static final int LOG_UPDATE_INTERVAL_MS = 500;
 
@@ -51,11 +52,11 @@ public class DeveloperToolsPanel extends JDialog {
      */
     public DeveloperToolsPanel(final Frame parent, final DeveloperTools developerTools,
                        final DebugLogBuffer debugLogBuffer) {
-        super(parent, "Developer Tools", false);
+        super("Developer Tools");
         this.developerTools = developerTools;
         this.debugLogBuffer = debugLogBuffer;
 
-        setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
+        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
         setLayout(new BorderLayout(8, 8));
 
         final JPanel settingsPanel = createSettingsPanel();
@@ -86,7 +87,6 @@ public class DeveloperToolsPanel extends JDialog {
         addWindowListener(new WindowAdapter() {
             @Override
             public void windowOpened(final WindowEvent e) {
-                debugLogBuffer.setPassthrough(true);
                 updateLogDisplay();
                 updateTimer.start();
             }
@@ -94,7 +94,6 @@ public class DeveloperToolsPanel extends JDialog {
             @Override
             public void windowClosed(final WindowEvent e) {
                 updateTimer.stop();
-                debugLogBuffer.setPassthrough(false);
             }
         });
     }
index 28097b0..5645d25 100644 (file)
@@ -7,6 +7,7 @@ package eu.svjatoslav.sixth.e3d.gui;
 import eu.svjatoslav.sixth.e3d.geometry.Point2D;
 import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseEvent;
 import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
+import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager;
 
 import java.awt.*;
 import java.awt.image.BufferedImage;
@@ -43,12 +44,25 @@ public class RenderingContext {
      */
     public static final int bufferedImageType = BufferedImage.TYPE_INT_RGB;
 
+    /**
+     * Number of horizontal segments for parallel rendering.
+     * Each segment is rendered by a separate thread.
+     */
+    public static final int NUM_RENDER_SEGMENTS = 8;
+
     /**
      * Java2D graphics context for drawing text, anti-aliased shapes, and other
      * high-level graphics operations onto the render buffer.
      */
     public final Graphics2D graphics;
 
+    /**
+     * Segment-specific Graphics2D contexts, each pre-clipped to a horizontal band.
+     * Used for thread-safe text and shape rendering without synchronization.
+     * Only initialized in the main RenderingContext; null in segment views.
+     */
+    private Graphics2D[] segmentGraphics;
+
     /**
      * Pixels of the rendering area.
      * Each pixel is a single int in RGB format: {@code (r << 16) | (g << 8) | b}.
@@ -103,8 +117,8 @@ public class RenderingContext {
     /**
      * Mouse click event that needs to be processed.
      * This event is processed only once per frame.
-     * If there are multiple objects under mouse cursor, the top-most object will receive the event.
-     * If there are no objects under mouse cursor, the event will be ignored.
+     * If there are multiple objects under the mouse cursor, the top-most object will receive the event.
+     * If there are no objects under the mouse cursor, the event will be ignored.
      * If there is no event, this field will be null.
      * This field is set to null after the event is processed.
      */
@@ -119,6 +133,19 @@ public class RenderingContext {
      */
     public DeveloperTools developerTools;
 
+    /**
+     * Debug log buffer for capturing diagnostic output.
+     * Shapes can log messages here that appear in the Developer Tools panel.
+     */
+    public DebugLogBuffer debugLogBuffer;
+
+    /**
+     * Global lighting manager for the scene.
+     * All shaded polygons use this to calculate lighting. Contains all light sources
+     * and ambient light settings for the world.
+     */
+    public LightingManager lightingManager;
+
     /**
      * Creates a new rendering context for full-screen rendering.
      *
@@ -160,6 +187,8 @@ public class RenderingContext {
         graphics = (Graphics2D) bufferedImage.getGraphics();
         graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
         graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+        segmentGraphics = createSegmentGraphics();
     }
 
     /**
@@ -182,6 +211,9 @@ public class RenderingContext {
         this.pixels = parent.pixels;
         this.graphics = parent.graphics;
         this.developerTools = parent.developerTools;
+        this.debugLogBuffer = parent.debugLogBuffer;
+        this.lightingManager = parent.lightingManager;
+        this.segmentGraphics = null;
     }
 
     /**
@@ -194,6 +226,58 @@ public class RenderingContext {
         currentObjectUnderMouseCursor = null;
     }
 
+    /**
+     * Creates Graphics2D contexts for each render segment, pre-clipped to Y bounds.
+     *
+     * @return array of Graphics2D objects, one per segment
+     */
+    private Graphics2D[] createSegmentGraphics() {
+        final Graphics2D[] contexts = new Graphics2D[NUM_RENDER_SEGMENTS];
+        final int segmentHeight = height / NUM_RENDER_SEGMENTS;
+
+        for (int i = 0; i < NUM_RENDER_SEGMENTS; i++) {
+            final int minY = i * segmentHeight;
+            final int maxY = (i == NUM_RENDER_SEGMENTS - 1) ? height : (i + 1) * segmentHeight;
+
+            final Graphics2D g = bufferedImage.createGraphics();
+            g.setClip(0, minY, width, maxY - minY);
+            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+            g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+            contexts[i] = g;
+        }
+
+        return contexts;
+    }
+
+    /**
+     * Returns the Graphics2D context for a specific render segment.
+     * Each segment's Graphics2D is pre-clipped to its Y bounds.
+     *
+     * @param segmentIndex the segment index (0 to NUM_RENDER_SEGMENTS-1)
+     * @return the Graphics2D for that segment
+     * @throws NullPointerException if called on a segment view (not the main context)
+     */
+    public Graphics2D getSegmentGraphics(final int segmentIndex) {
+        return segmentGraphics[segmentIndex];
+    }
+
+    /**
+     * Disposes all Graphics2D resources associated with this context.
+     * Should be called when the context is no longer needed (e.g., on resize).
+     */
+    public void dispose() {
+        if (segmentGraphics != null) {
+            for (final Graphics2D g : segmentGraphics) {
+                if (g != null) {
+                    g.dispose();
+                }
+            }
+        }
+        if (graphics != null) {
+            graphics.dispose();
+        }
+    }
+
     /**
      * Executes a graphics operation in a thread-safe manner.
      * This must be used for all Graphics2D operations (text, lines, etc.)
index 7f26423..f01b2a8 100644 (file)
@@ -25,24 +25,28 @@ import java.util.function.Consumer;
 public class SegmentRenderingContext extends RenderingContext {
 
     private final RenderingContext parent;
+    private final int segmentIndex;
     private MouseInteractionController segmentMouseHit;
 
     /**
      * Creates a segment view of a parent rendering context.
      *
-     * @param parent     the parent rendering context to delegate to
-     * @param renderMinY minimum Y coordinate (inclusive) for this segment
-     * @param renderMaxY maximum Y coordinate (exclusive) for this segment
+     * @param parent       the parent rendering context to delegate to
+     * @param renderMinY   minimum Y coordinate (inclusive) for this segment
+     * @param renderMaxY   maximum Y coordinate (exclusive) for this segment
+     * @param segmentIndex the index of this segment (0 to NUM_RENDER_SEGMENTS-1)
      */
     public SegmentRenderingContext(final RenderingContext parent,
-                                    final int renderMinY, final int renderMaxY) {
+                                    final int renderMinY, final int renderMaxY,
+                                    final int segmentIndex) {
         super(parent, renderMinY, renderMaxY);
         this.parent = parent;
+        this.segmentIndex = segmentIndex;
     }
 
     @Override
     public void executeWithGraphics(final Consumer<Graphics2D> operation) {
-        parent.executeWithGraphics(operation);
+        operation.accept(parent.getSegmentGraphics(segmentIndex));
     }
 
     @Override
index 0e35b3e..3377919 100755 (executable)
@@ -9,6 +9,7 @@ import eu.svjatoslav.sixth.e3d.gui.humaninput.KeyboardFocusStack;
 import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController;
 import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
 import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection;
+import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager;
 
 import java.awt.*;
 import java.awt.event.ComponentAdapter;
@@ -77,7 +78,6 @@ import java.util.concurrent.Executors;
 public class ViewPanel extends Canvas {
     private static final long serialVersionUID = 1683277888885045387L;
     private static final int NUM_BUFFERS = 2;
-    private static final int NUM_RENDER_SEGMENTS = 8;
 
     /** The input manager handling mouse and keyboard events. */
     private final InputManager inputManager = new InputManager(this);
@@ -90,7 +90,7 @@ public class ViewPanel extends Canvas {
     /** The set of frame listeners notified before each frame. */
     private final Set<FrameListener> frameListeners = ConcurrentHashMap.newKeySet();
     /** The executor service for parallel rendering. */
-    private final ExecutorService renderExecutor = Executors.newFixedThreadPool(NUM_RENDER_SEGMENTS);
+    private final ExecutorService renderExecutor = Executors.newFixedThreadPool(RenderingContext.NUM_RENDER_SEGMENTS);
     /** The background color of the view. */
     public Color backgroundColor = Color.BLACK;
 
@@ -101,6 +101,14 @@ public class ViewPanel extends Canvas {
     /** The developer tools panel popup, or null if not currently shown. */
     private DeveloperToolsPanel developerToolsPanel = null;
 
+    /**
+     * Global lighting manager for the scene.
+     * Contains all light sources and ambient light settings. Shaded polygons
+     * access this via the RenderingContext during paint(). Add lights here
+     * to illuminate the world.
+     */
+    private final LightingManager lightingManager = new LightingManager();
+
     /**
      * Stores milliseconds when the last frame was updated. This is needed to calculate the time delta between frames.
      * Time delta is used to calculate smooth animation.
@@ -152,6 +160,9 @@ public class ViewPanel extends Canvas {
 
         initializeCanvas();
 
+        // Set default ambient light for the scene
+        lightingManager.setAmbientLight(new Color(50, 50, 50));
+
         addComponentListener(new ComponentAdapter() {
             @Override
             public void componentResized(final ComponentEvent e) {
@@ -278,6 +289,16 @@ public class ViewPanel extends Canvas {
         return debugLogBuffer;
     }
 
+    /**
+     * Returns the global lighting manager for the scene.
+     * Add light sources here to illuminate the world.
+     *
+     * @return the lighting manager
+     */
+    public LightingManager getLightingManager() {
+        return lightingManager;
+    }
+
     /**
      * Shows the developer tools panel, toggling it if already open.
      * Called when F12 is pressed.
@@ -362,36 +383,27 @@ public class ViewPanel extends Canvas {
         }
 
         renderFrameCount++;
-        debugLogBuffer.log("[VIEWPANEL] renderFrame #" + renderFrameCount 
-            + " START, shapes=" + rootShapeCollection.getShapes().size()
-            + ", frameNumber=" + renderingContext.frameNumber
-            + ", bufferSize=" + renderingContext.width + "x" + renderingContext.height);
 
         try {
             // === 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();
-            debugLogBuffer.log("[VIEWPANEL] Phase 1 done: canvas cleared");
-            
             rootShapeCollection.transformShapes(this, renderingContext);
-            debugLogBuffer.log("[VIEWPANEL] Phase 2 done: shapes transformed, queued=" + rootShapeCollection.getQueuedShapeCount());
-            
             rootShapeCollection.sortShapes();
-            debugLogBuffer.log("[VIEWPANEL] Phase 3 done: shapes sorted");
 
             // Phase 4: Paint segments in parallel
             final int height = renderingContext.height;
-            final int segmentHeight = height / NUM_RENDER_SEGMENTS;
-            final SegmentRenderingContext[] segmentContexts = new SegmentRenderingContext[NUM_RENDER_SEGMENTS];
-            final CountDownLatch latch = new CountDownLatch(NUM_RENDER_SEGMENTS);
+            final int segmentHeight = height / RenderingContext.NUM_RENDER_SEGMENTS;
+            final SegmentRenderingContext[] segmentContexts = new SegmentRenderingContext[RenderingContext.NUM_RENDER_SEGMENTS];
+            final CountDownLatch latch = new CountDownLatch(RenderingContext.NUM_RENDER_SEGMENTS);
 
-            for (int i = 0; i < NUM_RENDER_SEGMENTS; i++) {
+            for (int i = 0; i < RenderingContext.NUM_RENDER_SEGMENTS; i++) {
                 final int segmentIndex = i;
                 final int minY = i * segmentHeight;
-                final int maxY = (i == NUM_RENDER_SEGMENTS - 1) ? height : (i + 1) * segmentHeight;
+                final int maxY = (i == RenderingContext.NUM_RENDER_SEGMENTS - 1) ? height : (i + 1) * segmentHeight;
 
-                segmentContexts[i] = new SegmentRenderingContext(renderingContext, minY, maxY);
+                segmentContexts[i] = new SegmentRenderingContext(renderingContext, minY, maxY, segmentIndex);
 
                 // Skip odd segments when renderAlternateSegments is enabled for overdraw debugging
                 if (developerTools.renderAlternateSegments && (i % 2 == 1)) {
@@ -415,29 +427,21 @@ public class ViewPanel extends Canvas {
                 Thread.currentThread().interrupt();
                 return;
             }
-            debugLogBuffer.log("[VIEWPANEL] Phase 4 done: segments painted");
 
             // Phase 5: Combine mouse results
             combineMouseResults(segmentContexts);
-            debugLogBuffer.log("[VIEWPANEL] Phase 5 done: mouse results combined");
 
             // === Blit loop — only re-blit, never re-render ===
             // contentsRestored() can trigger when the OS recreates the back buffer
             // (common during window creation). Since our offscreen bufferedImage still
             // contains the correct frame data, we only need to re-blit, not re-render.
-            int blitCount = 0;
             do {
                 Graphics2D g = null;
                 try {
                     g = (Graphics2D) bufferStrategy.getDrawGraphics();
                     if (g != null) {
-                        debugLogBuffer.log("[VIEWPANEL] Blit attempt #" + (blitCount + 1) + 
-                            ", image=" + renderingContext.bufferedImage.getWidth() + "x" + renderingContext.bufferedImage.getHeight() + 
-                            ", type=" + renderingContext.bufferedImage.getType() +
-                            ", g=" + g.getClass().getSimpleName());
                         // Use image observer to ensure proper image loading
                         g.drawImage(renderingContext.bufferedImage, 0, 0, this);
-                        blitCount++;
                     }
                 } catch (final Exception e) {
                     debugLogBuffer.log("[VIEWPANEL] Blit exception: " + e.getMessage());
@@ -446,17 +450,14 @@ public class ViewPanel extends Canvas {
                     if (g != null) g.dispose();
                 }
             } while (bufferStrategy.contentsRestored());
-            debugLogBuffer.log("[VIEWPANEL] Phase 6 done: blit complete (x" + blitCount + "), contentsRestored=" + bufferStrategy.contentsRestored() + ", contentsLost=" + bufferStrategy.contentsLost());
 
             if (bufferStrategy.contentsLost()) {
                 debugLogBuffer.log("[VIEWPANEL] Buffer contents LOST, reinitializing");
                 bufferStrategyInitialized = false;
                 bufferStrategy = null;
             } else {
-                debugLogBuffer.log("[VIEWPANEL] Calling bufferStrategy.show()");
                 bufferStrategy.show();
                 java.awt.Toolkit.getDefaultToolkit().sync();
-                debugLogBuffer.log("[VIEWPANEL] show() completed");
             }
         } catch (final Exception e) {
             debugLogBuffer.log("[VIEWPANEL] renderFrame exception: " + e.getMessage());
@@ -468,17 +469,16 @@ public class ViewPanel extends Canvas {
 
     private void clearCanvasAllSegments() {
         final int rgb = (backgroundColor.r << 16) | (backgroundColor.g << 8) | backgroundColor.b;
-        debugLogBuffer.log("[VIEWPANEL] Clearing canvas with color: 0x" + Integer.toHexString(rgb) + " (black=0x0)");
         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 black
-            final int segmentHeight = height / NUM_RENDER_SEGMENTS;
-            for (int seg = 0; seg < NUM_RENDER_SEGMENTS; seg += 2) {
+            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 == NUM_RENDER_SEGMENTS - 1) ? height : (seg + 1) * segmentHeight;
+                final int maxY = (seg == RenderingContext.NUM_RENDER_SEGMENTS - 1) ? height : (seg + 1) * segmentHeight;
                 Arrays.fill(pixels, minY * width, maxY * width, rgb);
             }
         } else {
@@ -624,7 +624,10 @@ public class ViewPanel extends Canvas {
         int panelHeight = getHeight();
 
         if (panelWidth <= 0 || panelHeight <= 0) {
-            renderingContext = null;
+            if (renderingContext != null) {
+                renderingContext.dispose();
+                renderingContext = null;
+            }
             return;
         }
 
@@ -632,8 +635,13 @@ public class ViewPanel extends Canvas {
         if ((renderingContext == null)
                 || (renderingContext.width != panelWidth)
                 || (renderingContext.height != panelHeight)) {
+            if (renderingContext != null) {
+                renderingContext.dispose();
+            }
             renderingContext = new RenderingContext(panelWidth, panelHeight);
             renderingContext.developerTools = developerTools;
+            renderingContext.debugLogBuffer = debugLogBuffer;
+            renderingContext.lightingManager = lightingManager;
         }
 
         renderingContext.prepareForNewFrameRendering();
index dc038aa..2d11012 100644 (file)
@@ -10,7 +10,6 @@ import eu.svjatoslav.sixth.e3d.gui.Camera;
 import eu.svjatoslav.sixth.e3d.gui.FrameListener;
 import eu.svjatoslav.sixth.e3d.gui.ViewPanel;
 import eu.svjatoslav.sixth.e3d.math.Quaternion;
-import eu.svjatoslav.sixth.e3d.math.Rotation;
 
 import java.awt.*;
 import java.awt.event.*;
@@ -278,7 +277,7 @@ public class InputManager implements
                       Math.min( Math.PI / 2 - 0.001, cameraPitch));
 
         final Camera camera = viewPanel.getCamera();
-        camera.getTransform().getRotation().setQuaternion(Quaternion.fromAngles(cameraYaw, cameraPitch));
+        camera.getTransform().getRotation().set(Quaternion.fromAngles(cameraYaw, cameraPitch));
 
         mouseDelta.zero();
         return true;
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/DiamondSquare.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/DiamondSquare.java
new file mode 100644 (file)
index 0000000..1801d49
--- /dev/null
@@ -0,0 +1,171 @@
+/*
+ * Sixth 3D engine. Author: Svjatoslav Agejenko.
+ * This project is released under Creative Commons Zero (CC0) license.
+ */
+
+package eu.svjatoslav.sixth.e3d.math;
+
+import java.util.Random;
+
+/**
+ * Diamond-square algorithm for procedural noise generation.
+ * <p>
+ * Generates realistic fractal noise suitable for terrain, textures,
+ * and other procedural content. The algorithm produces a 2D map
+ * where each value falls within the specified [min, max] range.
+ * <p>
+ * Grid size must be 2^n + 1 (e.g., 3, 5, 9, 17, 33, 65, 129, 257).
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/Diamond-square_algorithm">Diamond-square algorithm</a>
+ */
+public final class DiamondSquare {
+
+    private static final double DEFAULT_ROUGHNESS = 0.6;
+
+    private DiamondSquare() {
+    }
+
+    /**
+     * Generates a fractal noise map using the diamond-square algorithm.
+     *
+     * @param gridSize the size of the grid (must be 2^n + 1)
+     * @param min      the minimum value in the output
+     * @param max      the maximum value in the output
+     * @param seed     random seed for reproducible results
+     * @return a 2D array of values in range [min, max]
+     * @throws IllegalArgumentException if gridSize is not 2^n + 1
+     */
+    public static double[][] generateMap(int gridSize, double min, double max, long seed) {
+        return generateMap(gridSize, min, max, DEFAULT_ROUGHNESS, seed);
+    }
+
+    /**
+     * Generates a fractal noise map using the diamond-square algorithm with custom roughness.
+     *
+     * @param gridSize  the size of the grid (must be 2^n + 1)
+     * @param min       the minimum value in the output
+     * @param max       the maximum value in the output
+     * @param roughness the roughness factor (0.0 to 1.0), higher values produce more variation
+     * @param seed      random seed for reproducible results
+     * @return a 2D array of values in range [min, max]
+     * @throws IllegalArgumentException if gridSize is not 2^n + 1
+     */
+    public static double[][] generateMap(int gridSize, double min, double max, double roughness, long seed) {
+        if (!isValidGridSize(gridSize)) {
+            throw new IllegalArgumentException("Grid size must be 2^n + 1 (e.g., 65, 129, 257)");
+        }
+
+        Random random = new Random(seed);
+        double[][] map = new double[gridSize][gridSize];
+
+        map[0][0] = random.nextDouble();
+        map[0][gridSize - 1] = random.nextDouble();
+        map[gridSize - 1][0] = random.nextDouble();
+        map[gridSize - 1][gridSize - 1] = random.nextDouble();
+
+        int stepSize = gridSize - 1;
+        double currentScale = roughness;
+
+        while (stepSize > 1) {
+            int halfStep = stepSize / 2;
+
+            for (int y = 0; y < gridSize - 1; y += stepSize) {
+                for (int x = 0; x < gridSize - 1; x += stepSize) {
+                    double avg = (map[y][x] +
+                            map[y][x + stepSize] +
+                            map[y + stepSize][x] +
+                            map[y + stepSize][x + stepSize]) / 4.0;
+                    map[y + halfStep][x + halfStep] =
+                            avg + (random.nextDouble() - 0.5) * currentScale;
+                }
+            }
+
+            for (int y = 0; y < gridSize; y += stepSize) {
+                for (int x = 0; x < gridSize; x += stepSize) {
+                    if (x + halfStep < gridSize) {
+                        double avg = map[y][x];
+                        if (x - halfStep >= 0) {
+                            avg += map[y][x - halfStep];
+                        }
+                        if (x + stepSize < gridSize) {
+                            avg += map[y][x + stepSize];
+                        }
+                        if (y + halfStep < gridSize) {
+                            avg += map[y + halfStep][x + halfStep];
+                        } else if (y - halfStep >= 0) {
+                            avg += map[y - halfStep][x + halfStep];
+                        }
+                        map[y][x + halfStep] =
+                                avg / 4.0 + (random.nextDouble() - 0.5) * currentScale;
+                    }
+
+                    if (y + halfStep < gridSize) {
+                        double avg = map[y][x];
+                        if (y - halfStep >= 0) {
+                            avg += map[y - halfStep][x];
+                        }
+                        if (y + stepSize < gridSize) {
+                            avg += map[y + stepSize][x];
+                        }
+                        if (x + halfStep < gridSize) {
+                            avg += map[y + halfStep][x + halfStep];
+                        } else if (x - halfStep >= 0) {
+                            avg += map[y + halfStep][x - halfStep];
+                        }
+                        map[y + halfStep][x] =
+                                avg / 4.0 + (random.nextDouble() - 0.5) * currentScale;
+                    }
+                }
+            }
+
+            stepSize = halfStep;
+            currentScale *= roughness;
+        }
+
+        normalize(map, min, max);
+        return map;
+    }
+
+    private static void normalize(double[][] map, double min, double max) {
+        double actualMin = Double.MAX_VALUE;
+        double actualMax = Double.MIN_VALUE;
+
+        for (double[] row : map) {
+            for (double value : row) {
+                if (value < actualMin) actualMin = value;
+                if (value > actualMax) actualMax = value;
+            }
+        }
+
+        double range = actualMax - actualMin;
+        double targetRange = max - min;
+
+        if (range == 0) {
+            for (int y = 0; y < map.length; y++) {
+                for (int x = 0; x < map[y].length; x++) {
+                    map[y][x] = min;
+                }
+            }
+            return;
+        }
+
+        for (int y = 0; y < map.length; y++) {
+            for (int x = 0; x < map[y].length; x++) {
+                map[y][x] = min + (map[y][x] - actualMin) / range * targetRange;
+            }
+        }
+    }
+
+    /**
+     * Checks if the grid size is valid for the diamond-square algorithm.
+     * Valid sizes are 2^n + 1 (e.g., 3, 5, 9, 17, 33, 65, 129, 257).
+     *
+     * @param size the grid size to validate
+     * @return true if the size is valid
+     */
+    public static boolean isValidGridSize(int size) {
+        if (size < 3) return false;
+        int value = size - 1;
+        return (value & (value - 1)) == 0;
+    }
+}
\ No newline at end of file
index 86aef8a..42bab87 100644 (file)
@@ -15,16 +15,55 @@ import static java.lang.Math.sin;
  * <p>Quaternions provide a compact representation of rotations that avoids
  * gimbal lock and enables smooth interpolation (slerp).</p>
  *
+ * <p>Usage example:</p>
+ * <pre>{@code
+ * // Create a rotation from yaw and pitch angles
+ * Quaternion rotation = Quaternion.fromAngles(0.5, -0.3);
+ *
+ * // Apply rotation to a point
+ * Point3D point = new Point3D(1, 0, 0);
+ * rotation.rotate(point);
+ *
+ * // Combine rotations
+ * Quaternion combined = rotation.multiply(otherRotation);
+ * }</pre>
+ *
  * @see Matrix3x3
- * @see Rotation
+ * @see Transform
  */
 public class Quaternion {
 
+    /**
+     * The scalar (real) component of the quaternion.
+     */
     public double w;
+
+    /**
+     * The i component (x-axis rotation factor).
+     */
     public double x;
+
+    /**
+     * The j component (y-axis rotation factor).
+     */
     public double y;
+
+    /**
+     * The k component (z-axis rotation factor).
+     */
     public double z;
 
+    /**
+     * Creates an identity quaternion representing no rotation.
+     * Equivalent to Quaternion(1, 0, 0, 0).
+     */
+    public Quaternion() {
+        this.w = 1;
+        this.x = 0;
+        this.y = 0;
+        this.z = 0;
+    }
+
     /**
      * Creates a quaternion with the specified components.
      *
@@ -40,6 +79,27 @@ public class Quaternion {
         this.z = z;
     }
 
+    /**
+     * Creates a copy of this quaternion.
+     *
+     * @return a new quaternion with the same component values
+     */
+    public Quaternion clone() {
+        return new Quaternion(w, x, y, z);
+    }
+
+    /**
+     * Copies the values from another quaternion into this one.
+     *
+     * @param other the quaternion to copy from
+     */
+    public void set(final Quaternion other) {
+        this.w = other.w;
+        this.x = other.x;
+        this.y = other.y;
+        this.z = other.z;
+    }
+
     /**
      * Returns the identity quaternion representing no rotation.
      *
@@ -110,6 +170,18 @@ public class Quaternion {
         return this;
     }
 
+    /**
+     * Returns the inverse (conjugate) of this unit quaternion.
+     *
+     * <p>For a unit quaternion, the inverse equals the conjugate: (w, -x, -y, -z).
+     * This represents the opposite rotation.</p>
+     *
+     * @return a new quaternion representing the inverse rotation
+     */
+    public Quaternion invert() {
+        return new Quaternion(w, -x, -y, -z);
+    }
+
     /**
      * Converts this quaternion to a 3x3 rotation matrix.
      *
@@ -133,4 +205,14 @@ public class Quaternion {
         return m;
     }
 
+    /**
+     * Converts this quaternion to a 3x3 rotation matrix.
+     * Alias for {@link #toMatrix3x3()} for API convenience.
+     *
+     * @return a new matrix representing this rotation
+     */
+    public Matrix3x3 toMatrix() {
+        return toMatrix3x3();
+    }
+
 }
\ No newline at end of file
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java
deleted file mode 100644 (file)
index 8e529d0..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Sixth 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.sixth.e3d.math;
-
-import eu.svjatoslav.sixth.e3d.geometry.Point3D;
-
-/**
- * Represents a rotation in 3D space using a quaternion.
- *
- * <p>Quaternions provide smooth interpolation and avoid gimbal lock
- * compared to Euler angles.</p>
- *
- * @see Transform
- * @see Quaternion
- */
-public class Rotation implements Cloneable {
-
-    private Quaternion quaternion;
-
-    /**
-     * Creates a rotation with no rotation (identity).
-     */
-    public Rotation() {
-        quaternion = Quaternion.identity();
-    }
-
-    /**
-     * Creates a copy of this rotation with the same orientation.
-     *
-     * @return a new rotation with the same quaternion values
-     */
-    @Override
-    public Rotation clone() {
-        final Rotation r = new Rotation();
-        r.quaternion = new Quaternion(quaternion.w, quaternion.x, quaternion.y, quaternion.z);
-        return r;
-    }
-
-    /**
-     * Rotates a point around the origin using this rotation.
-     *
-     * @param point3d the point to rotate (modified in place)
-     */
-    public void rotate(final Point3D point3d) {
-        toMatrix().transform(point3d, point3d);
-    }
-
-    /**
-     * Sets the rotation from a quaternion.
-     *
-     * @param q the quaternion to set
-     */
-    public void setQuaternion(final Quaternion q) {
-        quaternion = new Quaternion(q.w, q.x, q.y, q.z);
-    }
-
-    /**
-     * Returns the internal quaternion.
-     *
-     * @return the quaternion (not a copy)
-     */
-    public Quaternion getQuaternion() {
-        return quaternion;
-    }
-
-    /**
-     * Converts this rotation to a 3x3 transformation matrix.
-     *
-     * @return a matrix representing this rotation
-     */
-    public Matrix3x3 toMatrix() {
-        return quaternion.toMatrix3x3();
-    }
-
-}
\ No newline at end of file
index 047308a..9a75ece 100755 (executable)
@@ -11,7 +11,8 @@ import eu.svjatoslav.sixth.e3d.geometry.Point3D;
  *
  * <p>Transformations are applied in order: rotation first, then translation.</p>
  *
- * @see Rotation
+ * @see Quaternion
+ * @see Point3D
  */
 public class Transform implements Cloneable {
 
@@ -23,14 +24,14 @@ public class Transform implements Cloneable {
     /**
      * The rotation applied before translation.
      */
-    private final Rotation rotation;
+    private final Quaternion rotation;
 
     /**
      * Creates a transform with no translation or rotation (identity transform).
      */
     public Transform() {
         translation = new Point3D();
-        rotation = new Rotation();
+        rotation = new Quaternion();
     }
 
     /**
@@ -40,7 +41,7 @@ public class Transform implements Cloneable {
      */
     public Transform(final Point3D translation) {
         this.translation = translation;
-        rotation = new Rotation();
+        rotation = new Quaternion();
     }
 
     /**
@@ -53,7 +54,7 @@ public class Transform implements Cloneable {
      */
     public static Transform fromAngles(final Point3D translation, final double angleXZ, final double angleYZ) {
         final Transform t = new Transform(translation);
-        t.rotation.setQuaternion(Quaternion.fromAngles(angleXZ, angleYZ));
+        t.rotation.set(Quaternion.fromAngles(angleXZ, angleYZ));
         return t;
     }
 
@@ -61,11 +62,11 @@ public class Transform implements Cloneable {
      * Creates a transform with the specified translation and rotation.
      *
      * @param translation the translation
-     * @param rotation    the rotation
+     * @param rotation    the rotation (will be cloned)
      */
-    public Transform(final Point3D translation, final Rotation rotation) {
+    public Transform(final Point3D translation, final Quaternion rotation) {
         this.translation = translation;
-        this.rotation = rotation;
+        this.rotation = rotation.clone();
     }
 
     /**
@@ -81,9 +82,9 @@ public class Transform implements Cloneable {
     /**
      * Returns the rotation component of this transform.
      *
-     * @return the rotation (mutable reference)
+     * @return the rotation quaternion (mutable reference)
      */
-    public Rotation getRotation() {
+    public Quaternion getRotation() {
         return rotation;
     }
 
index 1da9fdd..20cea76 100644 (file)
@@ -58,13 +58,13 @@ public class Vertex {
     /**
      * Texture coordinate for UV mapping (optional).
      */
-    public Point2D textureCoordinate;
+    public Point2D textureCoordinate; // TODO: is this proper term ?
 
 
     /**
      * The frame number when this vertex was last transformed (for caching).
      */
-    private int lastTransformedFrame = -1;  // Start at -1 so first frame (frameNumber=1) will transform
+    private int lastTransformedFrame = -1;  // Start at -1 so the first frame (frameNumber=1) will transform
 
     /**
      * Creates a vertex at the origin (0, 0, 0) with no texture coordinate.
index d585576..900c508 100644 (file)
@@ -34,7 +34,7 @@ public class CameraView {
         bottomLeft = new Point3D(0, 0, SIZE).rotate(-viewAngle, viewAngle);
         bottomRight = new Point3D(0, 0, SIZE).rotate(viewAngle, viewAngle);
 
-        final Matrix3x3 m = camera.getTransform().getRotation().toMatrix();
+        final Matrix3x3 m = camera.getTransform().getRotation().invert().toMatrix3x3();
         final Point3D temp = new Point3D();
         
         temp.clone(topLeft);
index d751a84..47edea9 100755 (executable)
@@ -79,7 +79,7 @@ public class RaytracingCamera extends TexturedRectangle {
         bottomLeft.rotate(cameraCenter, -viewAngle, viewAngle);
         bottomRight.rotate(cameraCenter, viewAngle, viewAngle);
 
-        final Matrix3x3 m = camera.getTransform().getRotation().toMatrix();
+        final Matrix3x3 m = camera.getTransform().getRotation().invert().toMatrix3x3();
         final Point3D temp = new Point3D();
         
         temp.clone(topLeft);
index c075580..02b4c0c 100755 (executable)
@@ -106,8 +106,7 @@ public class ShapeCollection {
 
         final Camera camera = viewPanel.getCamera();
 
-        cameraRotationTransform.getRotation().setQuaternion(
-                camera.getTransform().getRotation().getQuaternion());
+        cameraRotationTransform.getRotation().set(camera.getTransform().getRotation());
         transformStack.addTransform(cameraRotationTransform);
 
         final Point3D cameraLocation = camera.getTransform().getTranslation();
index d62c034..164ae54 100644 (file)
@@ -39,7 +39,6 @@ public class SolidPolygon extends AbstractCoordinateShape {
     private final Point3D cachedCenter = new Point3D();
     private Color color;
     private boolean shadingEnabled = false;
-    private LightingManager lightingManager;
     private boolean backfaceCulling = false;
 
     /**
@@ -241,15 +240,6 @@ public class SolidPolygon extends AbstractCoordinateShape {
         this.color = color;
     }
 
-    /**
-     * Returns the lighting manager used for shading calculations.
-     *
-     * @return the lighting manager, or null if shading is not enabled
-     */
-    public LightingManager getLightingManager() {
-        return lightingManager;
-    }
-
     /**
      * Checks if shading is enabled for this polygon.
      *
@@ -261,13 +251,13 @@ public class SolidPolygon extends AbstractCoordinateShape {
 
     /**
      * Enables or disables shading for this polygon.
+     * When enabled, the polygon uses the global lighting manager from the
+     * rendering context to calculate flat shading based on light sources.
      *
-     * @param shadingEnabled  true to enable shading, false to disable
-     * @param lightingManager the lighting manager to use for shading calculations
+     * @param shadingEnabled true to enable shading, false to disable
      */
-    public void setShadingEnabled(final boolean shadingEnabled, final LightingManager lightingManager) {
+    public void setShadingEnabled(final boolean shadingEnabled) {
         this.shadingEnabled = shadingEnabled;
-        this.lightingManager = lightingManager;
     }
 
     /**
@@ -370,10 +360,10 @@ public class SolidPolygon extends AbstractCoordinateShape {
 
         Color paintColor = color;
 
-        if (shadingEnabled && lightingManager != null) {
+        if (shadingEnabled && renderBuffer.lightingManager != null) {
             calculateCenter(cachedCenter);
             calculateNormal(cachedNormal);
-            paintColor = lightingManager.calculateLighting(cachedCenter, cachedNormal, color);
+            paintColor = renderBuffer.lightingManager.calculateLighting(cachedCenter, cachedNormal, color);
         }
 
         drawPolygon(renderBuffer, onScreenPoint1, onScreenPoint2,
index d3455a0..f456590 100644 (file)
@@ -12,7 +12,6 @@ import eu.svjatoslav.sixth.e3d.math.Transform;
 import eu.svjatoslav.sixth.e3d.math.TransformStack;
 import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
 import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator;
-import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager;
 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape;
 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line;
 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
@@ -66,13 +65,45 @@ import java.util.List;
  * @see eu.svjatoslav.sixth.e3d.renderer.raster.slicer.Slicer the level-of-detail polygon slicer
  */
 public class AbstractCompositeShape extends AbstractShape {
+    /**
+     * The original sub-shapes added to this composite, each wrapped with group
+     * identifier and visibility state. Shapes are stored in insertion order and
+     * remain in this collection even when hidden.
+     */
     private final List<SubShape> originalSubShapes = new ArrayList<>();
+
+    /**
+     * Tracks the distance and angle between the camera and this shape to compute
+     * an appropriate slice factor for level-of-detail adjustments.
+     */
     private final ViewSpaceTracker viewSpaceTracker;
+
+    /**
+     * The current slice factor used for tessellating textured polygons into smaller
+     * triangles for perspective-correct rendering. Higher values produce more triangles
+     * for distant objects; lower values for nearby objects. Updated dynamically based
+     * on view-space analysis.
+     */
     double currentSliceFactor = 5;
+
+    /**
+     * The processed list of sub-shapes ready for rendering. Contains non-textured
+     * shapes directly, and sliced triangles for textured polygons. Regenerated when
+     * {@link #slicingOutdated} is true.
+     */
     private List<AbstractShape> renderedSubShapes = new ArrayList<>();
+
+    /**
+     * Flag indicating whether the rendered sub-shapes need to be regenerated.
+     * Set to true when sub-shapes are added, removed, or when group visibility changes.
+     */
     private boolean slicingOutdated = true;
+
+    /**
+     * The position and orientation transform for this composite shape.
+     * Applied to all sub-shapes during the rendering transform pass.
+     */
     private Transform transform;
-    private LightingManager lightingManager;
 
     /**
      * Creates a composite shape at the world origin with no rotation.
@@ -119,15 +150,12 @@ public class AbstractCompositeShape extends AbstractShape {
      * @param groupId the group identifier, or {@code null} for ungrouped shapes
      */
     public void addShape(final AbstractShape shape, final String groupId) {
-        final SubShape subShape = new SubShape(shape);
-        subShape.setGroup(groupId);
-        subShape.setVisible(true);
-        originalSubShapes.add(subShape);
+        originalSubShapes.add(new SubShape(shape, groupId, true));
         slicingOutdated = true;
     }
 
     /**
-     * This method should be overridden by anyone wanting to customize shape
+     * This method should be overridden by anyone wanting to customize the shape
      * before it is rendered.
      *
      * @param transformPipe the current transform stack
@@ -174,8 +202,7 @@ public class AbstractCompositeShape extends AbstractShape {
      * @see #removeGroup(String)
      */
     public void hideGroup(final String groupIdentifier) {
-        for (int i = 0; i < originalSubShapes.size(); i++) {
-            final SubShape subShape = originalSubShapes.get(i);
+        for (final SubShape subShape : originalSubShapes) {
             if (subShape.matchesGroup(groupIdentifier)) {
                 subShape.setVisible(false);
                 slicingOutdated = true;
@@ -183,19 +210,27 @@ public class AbstractCompositeShape extends AbstractShape {
         }
     }
 
-    private boolean isReslicingNeeded(double proposedNewSliceFactor, double currentSliceFactor) {
+    /**
+     * Determines whether textured polygons need to be re-sliced based on slice factor change.
+     * <p>
+     * Re-slicing is needed if the slicing state is marked outdated, or if the ratio between
+     * the larger and smaller slice factor exceeds 1.5x. This threshold prevents frequent
+     * re-slicing for minor view changes while ensuring significant LOD changes trigger updates.
+     *
+     * @param proposedNewSliceFactor the slice factor computed from current view distance
+     * @param currentSliceFactor     the slice factor currently in use
+     * @return {@code true} if re-slicing should be performed
+     */
+    private boolean isReslicingNeeded(final double proposedNewSliceFactor, final double currentSliceFactor) {
 
         if (slicingOutdated)
             return true;
 
         // reslice if there is significant difference between proposed and current slice factor
-        if (proposedNewSliceFactor > currentSliceFactor) {
-            final double tmp = proposedNewSliceFactor;
-            proposedNewSliceFactor = currentSliceFactor;
-            currentSliceFactor = tmp;
-        }
+        final double larger = Math.max(proposedNewSliceFactor, currentSliceFactor);
+        final double smaller = Math.min(proposedNewSliceFactor, currentSliceFactor);
 
-        return (currentSliceFactor / proposedNewSliceFactor) > 1.5d;
+        return (larger / smaller) > 1.5d;
     }
 
     /**
@@ -233,13 +268,18 @@ public class AbstractCompositeShape extends AbstractShape {
         return result;
     }
 
-    private void resliceIfNeeded() {
+    /**
+     * Checks if re-slicing is needed and performs it if so.
+     *
+     * @param context the rendering context for logging
+     */
+    private void resliceIfNeeded(final RenderingContext context) {
 
         final double proposedSliceFactor = viewSpaceTracker.proposeSliceFactor();
 
         if (isReslicingNeeded(proposedSliceFactor, currentSliceFactor)) {
             currentSliceFactor = proposedSliceFactor;
-            reslice();
+            reslice(context);
         }
     }
 
@@ -295,39 +335,21 @@ public class AbstractCompositeShape extends AbstractShape {
         this.transform = transform;
     }
 
-    /**
-     * Sets the lighting manager for this composite shape and enables shading on all SolidPolygon sub-shapes.
-     *
-     * @param lightingManager the lighting manager to use for shading calculations
-     */
-    public void setLightingManager(final LightingManager lightingManager) {
-        this.lightingManager = lightingManager;
-        applyShadingToPolygons();
-    }
-
     /**
      * Enables or disables shading for all SolidPolygon sub-shapes.
+     * When enabled, polygons use the global lighting manager from the rendering
+     * context to calculate flat shading based on light sources.
      *
-     * @param shadingEnabled true to enable shading, false to disable
+     * @param shadingEnabled {@code true} to enable shading, {@code false} to disable
      */
     public void setShadingEnabled(final boolean shadingEnabled) {
         for (final SubShape subShape : getOriginalSubShapes()) {
             final AbstractShape shape = subShape.getShape();
             if (shape instanceof SolidPolygon) {
-                ((SolidPolygon) shape).setShadingEnabled(shadingEnabled, lightingManager);
+                ((SolidPolygon) shape).setShadingEnabled(shadingEnabled);
             }
-        }
-    }
 
-    private void applyShadingToPolygons() {
-        if (lightingManager == null)
-            return;
-
-        for (final SubShape subShape : getOriginalSubShapes()) {
-            final AbstractShape shape = subShape.getShape();
-            if (shape instanceof SolidPolygon) {
-                ((SolidPolygon) shape).setShadingEnabled(true, lightingManager);
-            }
+            // TODO: if shape is abstract composite, it seems that it would be good to enabled sharding recursively there too
         }
     }
 
@@ -363,41 +385,61 @@ public class AbstractCompositeShape extends AbstractShape {
         }
     }
 
-    private void reslice() {
+    /**
+     * Re-slices all textured polygons and rebuilds the rendered sub-shapes list.
+     * Logs the operation to the debug log buffer if available.
+     *
+     * @param context the rendering context for logging, may be {@code null}
+     */
+    private void reslice(final RenderingContext context) {
         slicingOutdated = false;
 
         final List<AbstractShape> result = new ArrayList<>();
 
         final Slicer slicer = new Slicer(currentSliceFactor);
+        int texturedPolygonCount = 0;
+        int otherShapeCount = 0;
+
         for (int i = 0; i < originalSubShapes.size(); i++) {
             final SubShape subShape = originalSubShapes.get(i);
             if (subShape.isVisible()) {
-                if (subShape.getShape() instanceof TexturedPolygon)
+                if (subShape.getShape() instanceof TexturedPolygon) {
                     slicer.slice((TexturedPolygon) subShape.getShape());
-                else
+                    texturedPolygonCount++;
+                } else {
                     result.add(subShape.getShape());
+                    otherShapeCount++;
+                }
             }
         }
 
         result.addAll(slicer.getResult());
 
         renderedSubShapes = result;
+
+        // Log to developer tools console if available
+        if (context != null && context.debugLogBuffer != null) {
+            context.debugLogBuffer.log("reslice: " + getClass().getSimpleName()
+                    + " sliceFactor=" + String.format("%.2f", currentSliceFactor)
+                    + " texturedPolygons=" + texturedPolygonCount
+                    + " otherShapes=" + otherShapeCount
+                    + " resultingTexturedPolygons=" + slicer.getResult().size());
+        }
     }
 
     @Override
     public void transform(final TransformStack transformPipe,
                           final RenderAggregator aggregator, final RenderingContext context) {
 
-        // add current composite shape transform to the end of the transform
-        // pipeline
+        // Add the current composite shape transform to the end of the transform
+        // pipeline.
         transformPipe.addTransform(transform);
 
         viewSpaceTracker.analyze(transformPipe, context);
 
         beforeTransformHook(transformPipe, context);
 
-        // hack, to get somewhat perspective correct textures
-        resliceIfNeeded();
+        resliceIfNeeded(context);
 
         // transform rendered subshapes
         for (final AbstractShape shape : renderedSubShapes)
index 6530b2d..b139a2b 100644 (file)
@@ -4,6 +4,8 @@
  */
 package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base;
 
+import java.util.Objects;
+
 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape;
 
 /**
@@ -20,17 +22,44 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape;
  */
 public class SubShape {
 
+    /**
+     * The wrapped shape that belongs to the parent composite shape.
+     * This is the actual renderable geometry (line, polygon, etc.).
+     */
     private final AbstractShape shape;
+
+    /**
+     * Whether this sub-shape should be rendered.
+     * Hidden shapes remain in the composite but are excluded from rendering.
+     */
     private boolean visible = true;
+
+    /**
+     * The group identifier for batch visibility operations.
+     * {@code null} indicates this shape is not part of any named group.
+     */
     private String groupIdentifier;
 
     /**
-     * Creates a sub-shape wrapper around the given shape.
+     * Creates a sub-shape wrapper around the given shape with default visibility (visible).
      *
      * @param shape the shape to wrap
      */
-    public SubShape(AbstractShape shape) {
+    public SubShape(final AbstractShape shape) {
+        this(shape, null, true);
+    }
+
+    /**
+     * Creates a sub-shape with all properties specified.
+     *
+     * @param shape           the shape to wrap
+     * @param groupIdentifier the group identifier, or {@code null} for ungrouped
+     * @param visible         whether the shape is initially visible
+     */
+    public SubShape(final AbstractShape shape, final String groupIdentifier, final boolean visible) {
         this.shape = shape;
+        this.groupIdentifier = groupIdentifier;
+        this.visible = visible;
     }
 
     /**
@@ -49,10 +78,7 @@ public class SubShape {
      * @return {@code true} if this sub-shape belongs to the specified group
      */
     public boolean matchesGroup(final String groupIdentifier) {
-        if (this.groupIdentifier == null)
-            return groupIdentifier == null;
-
-        return this.groupIdentifier.equals(groupIdentifier);
+        return Objects.equals(this.groupIdentifier, groupIdentifier);
     }
 
     /**
index 7558eb8..689d0c3 100644 (file)
@@ -75,7 +75,7 @@ public class Texture {
      * Index 0 is 1/2 the primary, index 1 is 1/4, and so on.
      * Entries are lazily populated on first access.
      */
-    TextureBitmap[] downSampled = new TextureBitmap[8];
+    TextureBitmap[] downSampled = new TextureBitmap[8]; // TODO: consider renaming it to mipmap to use standard terminology
 
     /**
      * Creates a new texture with the specified dimensions and upscale capacity.
index 831ba29..db4567a 100644 (file)
@@ -11,24 +11,49 @@ import static org.junit.Assert.assertEquals;
 public class QuaternionTest {
 
     @Test
-    public void testFromAnglesMatchesRotation() {
-        final Rotation rotation = new Rotation();
-        rotation.setQuaternion(Quaternion.fromAngles(0.5, 0.3));
-        final Matrix3x3 rotationMatrix = rotation.toMatrix();
-
+    public void testFromAnglesProducesValidMatrix() {
         final Quaternion quaternion = Quaternion.fromAngles(0.5, 0.3);
-        final Matrix3x3 quaternionMatrix = quaternion.toMatrix3x3();
+        final Matrix3x3 matrix = quaternion.toMatrix();
+
+        // Verify matrix is a valid rotation (determinant ≈ 1)
+        final double det = matrix.m00 * (matrix.m11 * matrix.m22 - matrix.m12 * matrix.m21)
+                         - matrix.m01 * (matrix.m10 * matrix.m22 - matrix.m12 * matrix.m20)
+                         + matrix.m02 * (matrix.m10 * matrix.m21 - matrix.m11 * matrix.m20);
+        assertEquals(1.0, det, 0.0001);
+    }
+
+    @Test
+    public void testToMatrixAliasesToMatrix3x3() {
+        final Quaternion quaternion = Quaternion.fromAngles(0.7, -0.4);
+        final Matrix3x3 m1 = quaternion.toMatrix();
+        final Matrix3x3 m2 = quaternion.toMatrix3x3();
 
         final double epsilon = 0.0001;
-        assertEquals(rotationMatrix.m00, quaternionMatrix.m00, epsilon);
-        assertEquals(rotationMatrix.m01, quaternionMatrix.m01, epsilon);
-        assertEquals(rotationMatrix.m02, quaternionMatrix.m02, epsilon);
-        assertEquals(rotationMatrix.m10, quaternionMatrix.m10, epsilon);
-        assertEquals(rotationMatrix.m11, quaternionMatrix.m11, epsilon);
-        assertEquals(rotationMatrix.m12, quaternionMatrix.m12, epsilon);
-        assertEquals(rotationMatrix.m20, quaternionMatrix.m20, epsilon);
-        assertEquals(rotationMatrix.m21, quaternionMatrix.m21, epsilon);
-        assertEquals(rotationMatrix.m22, quaternionMatrix.m22, epsilon);
+        assertEquals(m1.m00, m2.m00, epsilon);
+        assertEquals(m1.m01, m2.m01, epsilon);
+        assertEquals(m1.m02, m2.m02, epsilon);
+        assertEquals(m1.m10, m2.m10, epsilon);
+        assertEquals(m1.m11, m2.m11, epsilon);
+        assertEquals(m1.m12, m2.m12, epsilon);
+        assertEquals(m1.m20, m2.m20, epsilon);
+        assertEquals(m1.m21, m2.m21, epsilon);
+        assertEquals(m1.m22, m2.m22, epsilon);
+    }
+
+    @Test
+    public void testCloneProducesIndependentCopy() {
+        final Quaternion original = Quaternion.fromAngles(0.5, 0.3);
+        final Quaternion clone = original.clone();
+
+        assertEquals(original.w, clone.w, 0.0001);
+        assertEquals(original.x, clone.x, 0.0001);
+        assertEquals(original.y, clone.y, 0.0001);
+        assertEquals(original.z, clone.z, 0.0001);
+
+        // Modify original, verify clone is unaffected
+        final double originalW = original.w;
+        original.w = 0;
+        assertEquals(originalW, clone.w, 0.0001);
     }
 
 }
\ No newline at end of file
diff --git a/src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java b/src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java
deleted file mode 100644 (file)
index 79cb7c4..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Sixth 3D engine. Author: Svjatoslav Agejenko.
- * This project is released under Creative Commons Zero (CC0) license.
- */
-package eu.svjatoslav.sixth.e3d.math;
-
-import eu.svjatoslav.sixth.e3d.geometry.Point3D;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-
-public class RotationMatrixTest {
-
-    @Test
-    public void testToMatrixMatchesRotate() {
-        final Rotation rotation = new Rotation();
-        rotation.setQuaternion(Quaternion.fromAngles(0.5, 0.3));
-        final Point3D original = new Point3D(1, 2, 3);
-
-        final Point3D viaRotate = new Point3D(original);
-        rotation.rotate(viaRotate);
-
-        final Point3D viaMatrix = new Point3D();
-        rotation.toMatrix().transform(original, viaMatrix);
-
-        final double epsilon = 0.0001;
-        assertEquals(viaRotate.x, viaMatrix.x, epsilon);
-        assertEquals(viaRotate.y, viaMatrix.y, epsilon);
-        assertEquals(viaRotate.z, viaMatrix.z, epsilon);
-    }
-
-}
\ No newline at end of file