From: Svjatoslav Agejenko Date: Sun, 22 Mar 2026 19:10:55 +0000 (+0200) Subject: feat(engine): add DiamondSquare and improve rendering architecture X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=343e15d7b33be804dc538e090daec49b5c7e803b;p=sixth-3d.git feat(engine): add DiamondSquare and improve rendering architecture 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 --- diff --git a/AGENTS.md b/AGENTS.md index 4299ec5..a88e01f 100644 --- 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 diff --git a/TODO.org b/TODO.org index d54e650..a186422 100644 --- a/TODO.org +++ b/TODO.org @@ -1,54 +1,123 @@ * 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: diff --git a/doc/index.org b/doc/index.org index 4f08956..be446dc 100644 --- a/doc/index.org +++ b/doc/index.org @@ -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 @@ -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 @@ -292,6 +301,9 @@ faces. 3D engine. ** Face (Triangle) +:PROPERTIES: +:CUSTOM_ID: face-triangle +:END: #+BEGIN_EXPORT html @@ -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 @@ -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 @@ -388,6 +406,9 @@ determines how bright a surface appears. - Gouraud/Phong → vertex normals + interpolation ** Mesh +:PROPERTIES: +:CUSTOM_ID: mesh +:END: #+BEGIN_EXPORT html @@ -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 @@ -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 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 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 index 0000000..59d0760 --- /dev/null +++ b/doc/perspective-correct-textures/index.org @@ -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 + +#+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 + + + + + + + + + + 1. Original + + + + + A + B + C + + + + longest edge + + + + + + 2. Split + + + + + + + + + + + + + M + midpoint + + + + + + + + + 3. Recurse + + + + + + + + + + + + + + + + + + + + + + + Each split halves the longest edge at its midpoint. + Recursion stops when all edges < maxDistance. + + + + midpoint (3D + UV averaged) + +#+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 | diff --git a/doc/rendering-loop.org b/doc/rendering-loop.org index 274851c..f0d0d18 100644 --- a/doc/rendering-loop.org +++ b/doc/rendering-loop.org @@ -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: diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java index bafdb83..8737a28 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java @@ -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 diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/DebugLogBuffer.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/DebugLogBuffer.java index 59e5f2e..2d56100 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/DebugLogBuffer.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/DebugLogBuffer.java @@ -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. * - *

Always captures log messages to a fixed-size circular buffer. - * When {@link #passthrough} is enabled, messages are also printed to stdout.

+ *

Captures log messages to a fixed-size circular buffer for display + * in the {@link DeveloperToolsPanel}.

* *

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.

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

If passthrough is enabled, also prints to stdout.

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

When enabled, all subsequent log messages will be printed - * to stdout in addition to being captured in the buffer.

- * - * @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. * diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java index c209fc1..8c1dd37 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/DeveloperToolsPanel.java @@ -17,17 +17,18 @@ import java.util.List; /** * Developer tools panel for toggling diagnostic features and viewing logs. * - *

Opens as a popup dialog when F12 is pressed. Provides:

+ *

Opens as a popup window when F12 is pressed. Provides:

* * * @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); } }); } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java index 28097b0..5645d25 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/RenderingContext.java @@ -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.) diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java index 7f26423..f01b2a8 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/SegmentRenderingContext.java @@ -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 operation) { - parent.executeWithGraphics(operation); + operation.accept(parent.getSegmentGraphics(segmentIndex)); } @Override diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java index 0e35b3e..3377919 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/ViewPanel.java @@ -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 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(); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java index dc038aa..2d11012 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/humaninput/InputManager.java @@ -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 index 0000000..1801d49 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/DiamondSquare.java @@ -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. + *

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

+ * Grid size must be 2^n + 1 (e.g., 3, 5, 9, 17, 33, 65, 129, 257). + * + * @see Diamond-square algorithm + */ +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 diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Quaternion.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Quaternion.java index 86aef8a..42bab87 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Quaternion.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Quaternion.java @@ -15,16 +15,55 @@ import static java.lang.Math.sin; *

Quaternions provide a compact representation of rotations that avoids * gimbal lock and enables smooth interpolation (slerp).

* + *

Usage example:

+ *
{@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);
+ * }
+ * * @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. + * + *

For a unit quaternion, the inverse equals the conjugate: (w, -x, -y, -z). + * This represents the opposite rotation.

+ * + * @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 index 8e529d0..0000000 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Rotation.java +++ /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. - * - *

Quaternions provide smooth interpolation and avoid gimbal lock - * compared to Euler angles.

- * - * @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 diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java index 047308a..9a75ece 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java @@ -11,7 +11,8 @@ import eu.svjatoslav.sixth.e3d.geometry.Point3D; * *

Transformations are applied in order: rotation first, then translation.

* - * @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; } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java index 1da9fdd..20cea76 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java @@ -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. diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java index d585576..900c508 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java @@ -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); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java index d751a84..47edea9 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/RaytracingCamera.java @@ -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); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java index c075580..02b4c0c 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/ShapeCollection.java @@ -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(); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java index d62c034..164ae54 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java @@ -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, diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java index d3455a0..f456590 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java @@ -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 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 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. + *

+ * 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 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) diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java index 6530b2d..b139a2b 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java @@ -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); } /** diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java index 7558eb8..689d0c3 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java @@ -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. diff --git a/src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java b/src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java index 831ba29..db4567a 100644 --- a/src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java +++ b/src/test/java/eu/svjatoslav/sixth/e3d/math/QuaternionTest.java @@ -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 index 79cb7c4..0000000 --- a/src/test/java/eu/svjatoslav/sixth/e3d/math/RotationMatrixTest.java +++ /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