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
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
## 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
* 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]].
- 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.
- 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:
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">
- 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">
3D engine.
** Face (Triangle)
+:PROPERTIES:
+:CUSTOM_ID: face-triangle
+:END:
#+BEGIN_EXPORT html
<svg viewBox="0 0 320 240" width="320" height="240">
- 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">
- 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">
- Gouraud/Phong → vertex normals + interpolation
** Mesh
+:PROPERTIES:
+:CUSTOM_ID: mesh
+:END:
#+BEGIN_EXPORT html
<svg viewBox="0 0 320 240" width="320" height="240">
- [[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">
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
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.
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
diagnostic captures.
** API access
+:PROPERTIES:
+:CUSTOM_ID: api-access
+:END:
You can access and control developer tools programmatically:
- 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
: 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]].
- 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]].
-
-
--- /dev/null
+#+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 < 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 |
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:
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:
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:
- [[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).
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:
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:
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:
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
#+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:
not re-render.
* Smart repaint skipping
+:PROPERTIES:
+:CUSTOM_ID: smart-repaint-skipping
+:END:
The engine avoids unnecessary rendering:
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:
* 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);
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
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
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.
/**
* 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) {
count++;
}
}
-
- if (passthrough) {
- System.out.println(timestamped);
- }
}
/**
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.
*
/**
* 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;
*/
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();
addWindowListener(new WindowAdapter() {
@Override
public void windowOpened(final WindowEvent e) {
- debugLogBuffer.setPassthrough(true);
updateLogDisplay();
updateTimer.start();
}
@Override
public void windowClosed(final WindowEvent e) {
updateTimer.stop();
- debugLogBuffer.setPassthrough(false);
}
});
}
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;
*/
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}.
/**
* 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.
*/
*/
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.
*
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();
}
/**
this.pixels = parent.pixels;
this.graphics = parent.graphics;
this.developerTools = parent.developerTools;
+ this.debugLogBuffer = parent.debugLogBuffer;
+ this.lightingManager = parent.lightingManager;
+ this.segmentGraphics = null;
}
/**
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.)
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
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;
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);
/** 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;
/** 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.
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) {
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.
}
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)) {
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());
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());
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 {
int panelHeight = getHeight();
if (panelWidth <= 0 || panelHeight <= 0) {
- renderingContext = null;
+ if (renderingContext != null) {
+ renderingContext.dispose();
+ renderingContext = null;
+ }
return;
}
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();
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.*;
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;
--- /dev/null
+/*
+ * 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
* <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.
*
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.
*
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.
*
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
+++ /dev/null
-/*
- * 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
*
* <p>Transformations are applied in order: rotation first, then translation.</p>
*
- * @see Rotation
+ * @see Quaternion
+ * @see Point3D
*/
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();
}
/**
*/
public Transform(final Point3D translation) {
this.translation = translation;
- rotation = new Rotation();
+ rotation = new Quaternion();
}
/**
*/
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;
}
* 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();
}
/**
/**
* 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;
}
/**
* 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.
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);
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);
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();
private final Point3D cachedCenter = new Point3D();
private Color color;
private boolean shadingEnabled = false;
- private LightingManager lightingManager;
private boolean backfaceCulling = false;
/**
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.
*
/**
* 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;
}
/**
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,
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;
* @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.
* @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
* @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;
}
}
- 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;
}
/**
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);
}
}
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
}
}
}
}
- 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)
*/
package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base;
+import java.util.Objects;
+
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;
}
/**
* @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 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.
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
+++ /dev/null
-/*
- * 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