From fb35e84abbdbff4a52b57025915993a699eb9727 Mon Sep 17 00:00:00 2001 From: Svjatoslav Agejenko Date: Sat, 26 Sep 2026 10:26:35 +0300 Subject: [PATCH] initial commit --- .gitignore | 9 + AGENTS.org | 583 ++++++ COPYING | 121 ++ .../Agentic development/Golden workflow.svg | 74 + .../Agentic development/Headless lanes.svg | 78 + .../Agentic development/Pixel assertion.svg | 65 + .../Agentic development/diff-example.png | Bin 0 -> 10414 bytes .../Agentic development/snapshot-example.png | Bin 0 -> 20380 bytes Documentation/CSG/BSP tree.svg | 45 + Documentation/CSG/CSG demo.png | Bin 0 -> 35668 bytes Documentation/CSG/CSG intersect.svg | 41 + Documentation/CSG/CSG operations.svg | 37 + Documentation/CSG/CSG union.svg | 38 + Documentation/CSG/Polygon clipping.svg | 39 + Documentation/CSG/index.org | 284 +++ Documentation/Coordinate system.svg | 18 + Documentation/Depth buffer/index.org | 130 ++ .../Developer tools/Developer tools.png | Bin 0 -> 375468 bytes .../Render alternative segments.png | Bin 0 -> 135109 bytes .../Render polygon borders.png | Bin 0 -> 241317 bytes .../Show segment boundaries.png | Bin 0 -> 134547 bytes .../Developer tools/Thread timeline.png | Bin 0 -> 55149 bytes Documentation/Edge.svg | 12 + Documentation/Example.png | Bin 0 -> 67796 bytes Documentation/Face triangle.svg | 14 + .../Frustum culling/Frustum diagram.svg | 58 + .../Frustum culling/P-vertex AABB.svg | 38 + Documentation/Frustum culling/index.org | 177 ++ .../Global illumination/Bounce estimator.svg | 76 + .../Global illumination/GI pipeline.svg | 55 + .../Global illumination.png | Bin 0 -> 389278 bytes .../Global illumination/Lightmap mapping.svg | 75 + .../Global illumination/gi-converged.png | Bin 0 -> 168546 bytes Documentation/Global illumination/gi-flat.png | Bin 0 -> 12046 bytes .../Global illumination/gi-start.png | Bin 0 -> 4238 bytes Documentation/Global illumination/index.org | 251 +++ Documentation/Mesh.svg | 22 + .../Near plane clip/Clip algorithm.svg | 66 + .../Near plane clip/Fan triangulation.svg | 39 + .../Near plane clip/Near plane straddle.svg | 57 + Documentation/Near plane clip/index.org | 151 ++ .../Near plane clip/near-clip-after.png | Bin 0 -> 12046 bytes .../Near plane clip/near-clip-before.png | Bin 0 -> 10900 bytes Documentation/Normal vector.svg | 18 + .../Adaptive interval.svg | 59 + .../Affine distortion.png | Bin 0 -> 25825 bytes .../Scanline correction.svg | 41 + .../Perspective correct textures/index.org | 186 ++ Documentation/Point3D vertex.svg | 105 ++ .../Rendering loop/CPU scheduling.png | Bin 0 -> 30584 bytes .../Rendering loop/Double buffering.svg | 47 + Documentation/Rendering loop/Paint tiles.svg | 34 + .../Rendering loop/Painter's algorithm.svg | 13 + .../Rendering loop/Render pipeline.svg | 47 + Documentation/Rendering loop/index.org | 541 ++++++ Documentation/SDF textures/SDF concept.svg | 81 + .../SDF textures/SDF glyph pipeline.svg | 88 + .../SDF textures/SDF minification.svg | 71 + Documentation/SDF textures/glyph-sdf-S.png | Bin 0 -> 6097 bytes Documentation/SDF textures/index.org | 252 +++ Documentation/SDF textures/sdf-angled.png | Bin 0 -> 5386 bytes Documentation/SDF textures/sdf-far-zoom.png | Bin 0 -> 1776 bytes Documentation/SDF textures/sdf-far.png | Bin 0 -> 1706 bytes Documentation/SDF textures/sdf-mid.png | Bin 0 -> 3931 bytes Documentation/SDF textures/sdf-near.png | Bin 0 -> 11920 bytes .../Shading/Ambient light comparison.svg | 51 + .../Shading/Distance attenuation.svg | 91 + Documentation/Shading/Lambert cosine law.svg | 92 + Documentation/Shading/Shaded sphere.png | Bin 0 -> 23417 bytes Documentation/Shading/Shading pipeline.svg | 35 + Documentation/Shading/index.org | 266 +++ .../Stereo geometry.svg | 79 + .../Stereoscopic rendering/Stereo per eye.svg | 49 + .../Stereo pipeline.svg | 68 + .../Stereoscopic rendering/index.org | 190 ++ .../mono-comparison.png | Bin 0 -> 21058 bytes .../stereo-side-by-side.png | Bin 0 -> 31366 bytes Documentation/Winding order.svg | 35 + Documentation/export-docs.sh | 46 + Documentation/index.org | 1257 +++++++++++++ .../aukio-3d-architecture-review.md | 343 ++++ .../aukio-3d-perf-review.md | 273 +++ Documentation/style.css | 35 + TODO.org | 88 + Tools/Open with IntelliJ IDEA | 54 + Tools/Update web site | 101 ++ pom.xml | 157 ++ .../eu/svjatoslav/aukio/cfg/AukioConfig.java | 410 +++++ .../eu/svjatoslav/aukio/cfg/package-info.java | 11 + .../aukio/e3d/diag/DebugLogBuffer.java | 99 ++ .../aukio/e3d/diag/Diagnostics.java | 37 + .../aukio/e3d/diag/EngineConfig.java | 88 + .../aukio/e3d/diag/PersistentLog.java | 136 ++ .../svjatoslav/aukio/e3d/diag/Telemetry.java | 105 ++ .../e3d/diag/ThreadActivityRecorder.java | 212 +++ .../eu/svjatoslav/aukio/e3d/geometry/Box.java | 216 +++ .../svjatoslav/aukio/e3d/geometry/Camera.java | 243 +++ .../aukio/e3d/geometry/IntegerPoint.java | 39 + .../aukio/e3d/geometry/Point2D.java | 313 ++++ .../aukio/e3d/geometry/Point3D.java | 586 ++++++ .../aukio/e3d/geometry/Polygon.java | 83 + .../aukio/e3d/geometry/Rectangle.java | 83 + .../aukio/e3d/geometry/package-info.java | 7 + .../svjatoslav/aukio/e3d/gui/BugReport.java | 233 +++ .../aukio/e3d/gui/DeveloperTools.java | 46 + .../aukio/e3d/gui/DeveloperToolsPanel.java | 651 +++++++ .../aukio/e3d/gui/DeviceHotplug.java | 78 + .../aukio/e3d/gui/FrameListener.java | 52 + .../aukio/e3d/gui/GuiComponent.java | 222 +++ .../svjatoslav/aukio/e3d/gui/TextPointer.java | 123 ++ .../e3d/gui/ThreadTimelineComponent.java | 323 ++++ .../svjatoslav/aukio/e3d/gui/ViewFrame.java | 345 ++++ .../svjatoslav/aukio/e3d/gui/ViewPanel.java | 1575 +++++++++++++++++ .../aukio/e3d/gui/ViewSpaceTracker.java | 112 ++ .../e3d/gui/headtrack/HeadLookController.java | 144 ++ .../aukio/e3d/gui/headtrack/HeadTracker.java | 308 ++++ .../gui/headtrack/HeadTrackingManager.java | 117 ++ .../aukio/e3d/gui/headtrack/RayNeoHid.java | 149 ++ .../e3d/gui/humaninput/InputManager.java | 378 ++++ .../gui/humaninput/KeyboardFocusStack.java | 103 ++ .../e3d/gui/humaninput/KeyboardHelper.java | 124 ++ .../gui/humaninput/KeyboardInputHandler.java | 54 + .../aukio/e3d/gui/humaninput/MouseEvent.java | 59 + .../MouseInteractionController.java | 110 ++ .../WorldNavigationUserInputTracker.java | 93 + .../e3d/gui/humaninput/package-info.java | 7 + .../aukio/e3d/gui/package-info.java | 25 + .../gui/spacemouse/SpaceMouseController.java | 161 ++ .../e3d/gui/spacemouse/SpaceMouseManager.java | 119 ++ .../e3d/gui/spacemouse/SpaceNavigatorHid.java | 214 +++ .../gui/textEditorComponent/Character.java | 29 + .../gui/textEditorComponent/LookAndFeel.java | 41 + .../e3d/gui/textEditorComponent/Page.java | 162 ++ .../TextEditComponent.java | 915 ++++++++++ .../e3d/gui/textEditorComponent/TextLine.java | 410 +++++ .../gui/textEditorComponent/package-info.java | 6 + .../aukio/e3d/headless/GoldenImage.java | 168 ++ .../aukio/e3d/headless/PixelAssertions.java | 146 ++ .../aukio/e3d/headless/SceneDump.java | 103 ++ .../aukio/e3d/headless/Snapshot.java | 200 +++ .../aukio/e3d/headless/package-info.java | 22 + .../aukio/e3d/math/DiamondSquare.java | 171 ++ .../svjatoslav/aukio/e3d/math/Matrix3x3.java | 67 + .../svjatoslav/aukio/e3d/math/Quaternion.java | 281 +++ .../svjatoslav/aukio/e3d/math/Transform.java | 257 +++ .../aukio/e3d/math/TransformStack.java | 222 +++ .../aukio/e3d/math/package-info.java | 9 + .../eu/svjatoslav/aukio/e3d/package-info.java | 7 + .../e3d/renderer/octree/OctreeVolume.java | 1137 ++++++++++++ .../e3d/renderer/octree/package-info.java | 21 + .../renderer/octree/raytracer/CameraView.java | 55 + .../octree/raytracer/LightSource.java | 42 + .../e3d/renderer/octree/raytracer/Ray.java | 71 + .../renderer/octree/raytracer/RayTracer.java | 411 +++++ .../octree/raytracer/RaytracingCamera.java | 136 ++ .../octree/raytracer/package-info.java | 21 + .../aukio/e3d/renderer/package-info.java | 11 + .../aukio/e3d/renderer/raster/Color.java | 353 ++++ .../renderer/raster/CullingStatistics.java | 64 + .../aukio/e3d/renderer/raster/Frustum.java | 269 +++ .../aukio/e3d/renderer/raster/HiZPyramid.java | 198 +++ .../raster/ParallelTransformCoordinator.java | 204 +++ .../e3d/renderer/raster/RadixLongSort.java | 220 +++ .../e3d/renderer/raster/RenderAggregator.java | 803 +++++++++ .../e3d/renderer/raster/RenderingContext.java | 714 ++++++++ .../raster/SegmentRenderingContext.java | 105 ++ .../e3d/renderer/raster/ShapeCollection.java | 521 ++++++ .../aukio/e3d/renderer/raster/StereoEye.java | 19 + .../aukio/e3d/renderer/raster/Vertex.java | 293 +++ .../raster/gi/GlobalIllumination.java | 905 ++++++++++ .../e3d/renderer/raster/gi/Lightmap.java | 279 +++ .../renderer/raster/gi/LightmappedShape.java | 20 + .../raster/gi/LightmappedTriangle.java | 65 + .../e3d/renderer/raster/gi/TriangleBvh.java | 231 +++ .../e3d/renderer/raster/gi/package-info.java | 22 + .../raster/lighting/GiLightProvider.java | 50 + .../renderer/raster/lighting/LightSource.java | 136 ++ .../raster/lighting/LightingManager.java | 278 +++ .../raster/lighting/package-info.java | 21 + .../e3d/renderer/raster/package-info.java | 26 + .../shapes/AbstractCoordinateShape.java | 643 +++++++ .../renderer/raster/shapes/AbstractShape.java | 157 ++ .../raster/shapes/basic/Billboard.java | 261 +++ .../raster/shapes/basic/GlowingPoint.java | 114 ++ .../raster/shapes/basic/line/Line.java | 537 ++++++ .../shapes/basic/line/LineAppearance.java | 97 + .../shapes/basic/line/LineInterpolator.java | 101 ++ .../shapes/basic/line/package-info.java | 22 + .../raster/shapes/basic/package-info.java | 28 + .../basic/solidpolygon/LineInterpolator.java | 151 ++ .../basic/solidpolygon/SolidPolygon.java | 823 +++++++++ .../basic/solidpolygon/package-info.java | 22 + .../basic/texturedpolygon/MeshTriangle.java | 138 ++ .../PerspectiveBorderInterpolator.java | 164 ++ .../PolygonBorderInterpolator.java | 196 ++ .../texturedpolygon/TexturedTriangle.java | 1481 ++++++++++++++++ .../texturedpolygon/TriangleMeshBlock.java | 502 ++++++ .../basic/texturedpolygon/package-info.java | 28 + .../composite/ForwardOrientedTextBlock.java | 108 ++ .../raster/shapes/composite/Graph.java | 180 ++ .../shapes/composite/LightSourceMarker.java | 43 + .../composite/LightmappedCompositeShape.java | 176 ++ .../shapes/composite/TexturedRectangle.java | 180 ++ .../base/AbstractCompositeShape.java | 1203 +++++++++++++ .../raster/shapes/composite/base/BspTree.java | 230 +++ .../raster/shapes/composite/base/Csg.java | 173 ++ .../raster/shapes/composite/base/Plane.java | 228 +++ .../shapes/composite/base/PolygonType.java | 56 + .../shapes/composite/base/SubShape.java | 128 ++ .../shapes/composite/base/package-info.java | 24 + .../raster/shapes/composite/package-info.java | 23 + .../composite/solid/SolidPolygonArrow.java | 324 ++++ .../composite/solid/SolidPolygonCone.java | 268 +++ .../composite/solid/SolidPolygonCube.java | 45 + .../composite/solid/SolidPolygonCylinder.java | 200 +++ .../composite/solid/SolidPolygonPyramid.java | 258 +++ .../solid/SolidPolygonRectangularBox.java | 122 ++ .../composite/solid/SolidPolygonSphere.java | 84 + .../shapes/composite/solid/package-info.java | 24 + .../composite/textcanvas/SdfGlyphCache.java | 278 +++ .../composite/textcanvas/TextCanvas.java | 363 ++++ .../composite/textcanvas/package-info.java | 9 + .../shapes/composite/wireframe/Grid2D.java | 80 + .../shapes/composite/wireframe/Grid3D.java | 87 + .../composite/wireframe/WireframeArrow.java | 321 ++++ .../composite/wireframe/WireframeBox.java | 104 ++ .../composite/wireframe/WireframeCone.java | 247 +++ .../composite/wireframe/WireframeCube.java | 45 + .../wireframe/WireframeCylinder.java | 188 ++ .../composite/wireframe/WireframePyramid.java | 246 +++ .../composite/wireframe/WireframeSphere.java | 87 + .../composite/wireframe/package-info.java | 24 + .../renderer/raster/shapes/package-info.java | 25 + .../e3d/renderer/raster/texture/Texture.java | 479 +++++ .../raster/texture/TextureBitmap.java | 291 +++ .../raster/texture/TextureGenerator.java | 327 ++++ .../renderer/raster/texture/package-info.java | 22 + .../aukio/e3d/examples/hourglass.png | Bin 0 -> 2161 bytes .../svjatoslav/aukio/cfg/AukioConfigTest.java | 206 +++ .../gui/textEditorComponent/TextLineTest.java | 116 ++ .../gui/textEditorComponent/package-info.java | 13 + .../e3d/headless/HeadlessToolkitTest.java | 85 + .../aukio/e3d/math/QuaternionTest.java | 59 + .../aukio/e3d/math/TransformStackTest.java | 139 ++ .../e3d/renderer/octree/OctreeVolumeTest.java | 49 + .../raster/ParallelTransformTest.java | 286 +++ .../renderer/raster/RadixLongSortTest.java | 143 ++ .../renderer/raster/SegmentBinningTest.java | 286 +++ .../TexturedTriangleBlendTest.java | 239 +++ .../TexturedTrianglePerspectiveTest.java | 305 ++++ .../raster/shapes/composite/base/CsgTest.java | 172 ++ 251 files changed, 41919 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.org create mode 100644 COPYING create mode 100644 Documentation/Agentic development/Golden workflow.svg create mode 100644 Documentation/Agentic development/Headless lanes.svg create mode 100644 Documentation/Agentic development/Pixel assertion.svg create mode 100644 Documentation/Agentic development/diff-example.png create mode 100644 Documentation/Agentic development/snapshot-example.png create mode 100644 Documentation/CSG/BSP tree.svg create mode 100644 Documentation/CSG/CSG demo.png create mode 100644 Documentation/CSG/CSG intersect.svg create mode 100644 Documentation/CSG/CSG operations.svg create mode 100644 Documentation/CSG/CSG union.svg create mode 100644 Documentation/CSG/Polygon clipping.svg create mode 100644 Documentation/CSG/index.org create mode 100644 Documentation/Coordinate system.svg create mode 100644 Documentation/Depth buffer/index.org create mode 100644 Documentation/Developer tools/Developer tools.png create mode 100644 Documentation/Developer tools/Render alternative segments.png create mode 100644 Documentation/Developer tools/Render polygon borders.png create mode 100644 Documentation/Developer tools/Show segment boundaries.png create mode 100644 Documentation/Developer tools/Thread timeline.png create mode 100644 Documentation/Edge.svg create mode 100644 Documentation/Example.png create mode 100644 Documentation/Face triangle.svg create mode 100644 Documentation/Frustum culling/Frustum diagram.svg create mode 100644 Documentation/Frustum culling/P-vertex AABB.svg create mode 100644 Documentation/Frustum culling/index.org create mode 100644 Documentation/Global illumination/Bounce estimator.svg create mode 100644 Documentation/Global illumination/GI pipeline.svg create mode 100644 Documentation/Global illumination/Global illumination.png create mode 100644 Documentation/Global illumination/Lightmap mapping.svg create mode 100644 Documentation/Global illumination/gi-converged.png create mode 100644 Documentation/Global illumination/gi-flat.png create mode 100644 Documentation/Global illumination/gi-start.png create mode 100644 Documentation/Global illumination/index.org create mode 100644 Documentation/Mesh.svg create mode 100644 Documentation/Near plane clip/Clip algorithm.svg create mode 100644 Documentation/Near plane clip/Fan triangulation.svg create mode 100644 Documentation/Near plane clip/Near plane straddle.svg create mode 100644 Documentation/Near plane clip/index.org create mode 100644 Documentation/Near plane clip/near-clip-after.png create mode 100644 Documentation/Near plane clip/near-clip-before.png create mode 100644 Documentation/Normal vector.svg create mode 100644 Documentation/Perspective correct textures/Adaptive interval.svg create mode 100644 Documentation/Perspective correct textures/Affine distortion.png create mode 100644 Documentation/Perspective correct textures/Scanline correction.svg create mode 100644 Documentation/Perspective correct textures/index.org create mode 100644 Documentation/Point3D vertex.svg create mode 100644 Documentation/Rendering loop/CPU scheduling.png create mode 100644 Documentation/Rendering loop/Double buffering.svg create mode 100644 Documentation/Rendering loop/Paint tiles.svg create mode 100644 Documentation/Rendering loop/Painter's algorithm.svg create mode 100644 Documentation/Rendering loop/Render pipeline.svg create mode 100644 Documentation/Rendering loop/index.org create mode 100644 Documentation/SDF textures/SDF concept.svg create mode 100644 Documentation/SDF textures/SDF glyph pipeline.svg create mode 100644 Documentation/SDF textures/SDF minification.svg create mode 100644 Documentation/SDF textures/glyph-sdf-S.png create mode 100644 Documentation/SDF textures/index.org create mode 100644 Documentation/SDF textures/sdf-angled.png create mode 100644 Documentation/SDF textures/sdf-far-zoom.png create mode 100644 Documentation/SDF textures/sdf-far.png create mode 100644 Documentation/SDF textures/sdf-mid.png create mode 100644 Documentation/SDF textures/sdf-near.png create mode 100644 Documentation/Shading/Ambient light comparison.svg create mode 100644 Documentation/Shading/Distance attenuation.svg create mode 100644 Documentation/Shading/Lambert cosine law.svg create mode 100644 Documentation/Shading/Shaded sphere.png create mode 100644 Documentation/Shading/Shading pipeline.svg create mode 100644 Documentation/Shading/index.org create mode 100644 Documentation/Stereoscopic rendering/Stereo geometry.svg create mode 100644 Documentation/Stereoscopic rendering/Stereo per eye.svg create mode 100644 Documentation/Stereoscopic rendering/Stereo pipeline.svg create mode 100644 Documentation/Stereoscopic rendering/index.org create mode 100644 Documentation/Stereoscopic rendering/mono-comparison.png create mode 100644 Documentation/Stereoscopic rendering/stereo-side-by-side.png create mode 100644 Documentation/Winding order.svg create mode 100755 Documentation/export-docs.sh create mode 100644 Documentation/index.org create mode 100644 Documentation/reviews/2026-09-20-codebase-review/aukio-3d-architecture-review.md create mode 100644 Documentation/reviews/2026-09-20-codebase-review/aukio-3d-perf-review.md create mode 100644 Documentation/style.css create mode 100644 TODO.org create mode 100755 Tools/Open with IntelliJ IDEA create mode 100755 Tools/Update web site create mode 100644 pom.xml create mode 100644 src/main/java/eu/svjatoslav/aukio/cfg/AukioConfig.java create mode 100644 src/main/java/eu/svjatoslav/aukio/cfg/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/diag/DebugLogBuffer.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/diag/Diagnostics.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/diag/EngineConfig.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/diag/PersistentLog.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/diag/Telemetry.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/diag/ThreadActivityRecorder.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/geometry/Box.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/geometry/Camera.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/geometry/IntegerPoint.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point2D.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point3D.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/geometry/Polygon.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/geometry/Rectangle.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/geometry/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/BugReport.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperTools.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperToolsPanel.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/DeviceHotplug.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/FrameListener.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/GuiComponent.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/gui/TextPointer.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadTimelineComponent.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewFrame.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewSpaceTracker.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/HeadLookController.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/HeadTracker.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/HeadTrackingManager.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/RayNeoHid.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardFocusStack.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardHelper.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardInputHandler.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/MouseEvent.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/MouseInteractionController.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/WorldNavigationUserInputTracker.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseController.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseManager.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceNavigatorHid.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/Character.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/LookAndFeel.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/Page.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/TextEditComponent.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/TextLine.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/headless/GoldenImage.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/headless/PixelAssertions.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/headless/SceneDump.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/headless/Snapshot.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/headless/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/math/DiamondSquare.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/math/Matrix3x3.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/math/Quaternion.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/math/Transform.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/math/TransformStack.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/math/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/package-info.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolume.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/CameraView.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/LightSource.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/Ray.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RayTracer.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RaytracingCamera.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/package-info.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Color.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/CullingStatistics.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Frustum.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/HiZPyramid.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformCoordinator.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSort.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderAggregator.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentRenderingContext.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/StereoEye.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/GlobalIllumination.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/Lightmap.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedShape.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/TriangleBvh.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/package-info.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/GlowingPoint.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineAppearance.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineInterpolator.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/MeshTriangle.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PerspectiveBorderInterpolator.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TriangleMeshBlock.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/Graph.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightSourceMarker.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightmappedCompositeShape.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/TexturedRectangle.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/BspTree.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Csg.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/PolygonType.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/SubShape.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCube.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/SdfGlyphCache.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/Grid2D.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/Grid3D.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeArrow.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCone.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCylinder.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframePyramid.java create mode 100755 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeSphere.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/package-info.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureGenerator.java create mode 100644 src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/package-info.java create mode 100644 src/main/resources/eu/svjatoslav/aukio/e3d/examples/hourglass.png create mode 100644 src/test/java/eu/svjatoslav/aukio/cfg/AukioConfigTest.java create mode 100644 src/test/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/TextLineTest.java create mode 100644 src/test/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/package-info.java create mode 100644 src/test/java/eu/svjatoslav/aukio/e3d/headless/HeadlessToolkitTest.java create mode 100644 src/test/java/eu/svjatoslav/aukio/e3d/math/QuaternionTest.java create mode 100644 src/test/java/eu/svjatoslav/aukio/e3d/math/TransformStackTest.java create mode 100644 src/test/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolumeTest.java create mode 100644 src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformTest.java create mode 100644 src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSortTest.java create mode 100644 src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentBinningTest.java create mode 100644 src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangleBlendTest.java create mode 100644 src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTrianglePerspectiveTest.java create mode 100644 src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/CsgTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..319eac6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/.idea/ +/target/ +/.classpath +/.project +/.settings/ +/Documentation/graphs/ +/Documentation/apidocs/ +/*.iml +*.html diff --git a/AGENTS.org b/AGENTS.org new file mode 100644 index 0000000..675193a --- /dev/null +++ b/AGENTS.org @@ -0,0 +1,583 @@ +:PROPERTIES: +:ID: d69fec29-3842-4e10-aece-89829c522c79 +:END: +#+TITLE: Aukio 3D Engine - Quick Reference +#+LANGUAGE: en +#+OPTIONS: H:20 num:20 author:nil + +Software-based 3D rendering engine (no OpenGL/DirectX). Pure Java +rasterizer with texture support, lighting, CSG operations, and camera +navigation. + +* Quick Lookup: "I Want To..." +:PROPERTIES: +:ID: 5136cb8e-4ade-4a28-91f7-3ec0ee98721c +:END: + +| Task | Class (path) | Key Constructor/Method | +|-------------------------------+-----------------------------------------------------+-----------------------------------------------------------| +| *Create a window* | ~ViewFrame~ (~gui/ViewFrame.java~) | ~new ViewFrame()~ → ~.getViewPanel()~ | +| *Add shapes to scene* | ~ShapeCollection~ (~raster/ShapeCollection.java~) | ~viewPanel.getRootShapeCollection().addShape(shape)~ | +| *Position camera* | ~Camera~ (~geometry/Camera.java~) | ~camera.getTransform().setTranslation(Point3D)~ | +| *Create a wireframe cube* | ~WireframeCube~ (~shapes/composite/wireframe/~) | ~new WireframeCube(center, halfSize, appearance)~ | +| *Create a solid cube* | ~SolidPolygonCube~ (~shapes/composite/solid/~) | ~new SolidPolygonCube(center, halfSize, color)~ | +| *Create a line* | ~Line~ (~shapes/basic/line/~) | ~new Line(p1, p2, color, width)~ | +| *Create a polygon* | ~SolidPolygon~ (~shapes/basic/solidpolygon/~) | ~SolidPolygon.triangle(...)~ or ~.quad(...)~ | +| *Create a sphere (wireframe)* | ~WireframeSphere~ (~shapes/composite/wireframe/~) | ~new WireframeSphere(center, radius, appearance)~ | +| *Create a sphere (solid)* | ~SolidPolygonSphere~ (~shapes/composite/solid/~) | ~new SolidPolygonSphere(center, radius, segments, color)~ | +| *Create text in 3D* | ~TextCanvas~ (~shapes/composite/textcanvas/~) | ~new TextCanvas(transform, text, fgColor, bgColor)~ | +| *Add a light source* | ~LightSource~ (~raster/lighting/~) | ~lighting.addLight(new LightSource(pos, color))~ | +| *Enable shading* | ~AbstractCompositeShape~ (~shapes/composite/base/~) | ~shape.setShadingEnabled(true)~ | +| *CSG: subtract* | ~AbstractCompositeShape~ (~shapes/composite/base/~) | ~shape.subtract(otherShape)~ | +| *CSG: union* | ~AbstractCompositeShape~ (~shapes/composite/base/~) | ~shape.union(otherShape)~ | +| *CSG: intersect* | ~AbstractCompositeShape~ (~shapes/composite/base/~) | ~shape.intersect(otherShape)~ | +| *Animate per-frame* | ~FrameListener~ (~gui/FrameListener.java~) | ~viewPanel.addFrameListener((panel, deltaMs) -> {...})~ | +| *Handle mouse clicks* | ~MouseInteractionController~ (~gui/humaninput/~) | ~shape.setMouseInteractionController(controller)~ | +| *Position/rotate shape* | ~AbstractCompositeShape~ (~shapes/composite/base/~) | ~new AbstractCompositeShape(location)~ | +| *Hide/show shape groups* | ~ShapeCollection~ (~raster/ShapeCollection.java~) | ~.hideGroup("debug")~ / ~.showGroup("debug")~ | +| *Create a billboard* | ~Billboard~ (~shapes/basic/~) | ~new Billboard(position, scale, texture)~ | +| *Create a glowing point* | ~GlowingPoint~ (~shapes/basic/~) | ~new GlowingPoint(position, scale, color)~ | +| *Render without a window* | ~Snapshot~ (~headless/Snapshot.java~) | ~Snapshot.render(scene, lighting, pose, w, h)~ | +| *Assert pixels painted* | ~PixelAssertions~ (~headless/PixelAssertions.java~) | ~.unpaintedFraction(image, bg, x0, y0, x1, y1)~ | +| *Compare against golden PNG* | ~GoldenImage~ (~headless/GoldenImage.java~) | ~.compare(actual, goldenFile, tolerance, maxFraction)~ | +| *Dump scene state* | ~SceneDump~ (~headless/SceneDump.java~) | ~SceneDump.dump(scene, lighting, camera, gi)~ | + +* Code Examples +:PROPERTIES: +:ID: fdd7c315-c513-4925-8ca5-acb1f87a7380 +:END: + +** Basic Scene Setup + +#+begin_src java +import eu.svjatoslav.aukio.e3d.gui.ViewFrame; +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; +import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; + +// Create window with 3D view +ViewFrame frame = new ViewFrame(); +ViewPanel viewPanel = frame.getViewPanel(); +ShapeCollection scene = viewPanel.getRootShapeCollection(); + +// Position camera (behind origin, looking forward) +viewPanel.getCamera().getTransform().setTranslation(new Point3D(0, 0, -200)); + +// Add shapes here... +// scene.addShape(...); +#+end_src + +** Creating Shapes + +#+begin_src java +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe.*; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.*; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.*; + +// Wireframe shapes (use LineAppearance for color/width) +LineAppearance appearance = new LineAppearance(2.0, Color.CYAN); +scene.addShape(new WireframeCube(new Point3D(0, 0, 200), 50, appearance)); +scene.addShape(new WireframeSphere(new Point3D(100, 0, 300), 40, appearance)); +scene.addShape(new WireframeBox(p1, p2, appearance)); + +// Solid shapes (use Color directly) +scene.addShape(new SolidPolygonCube(new Point3D(0, 0, 200), 50, Color.GREEN)); +scene.addShape(new SolidPolygonSphere(new Point3D(100, 0, 300), 40, 16, Color.RED)); + +// Simple line +scene.addShape(new Line( + new Point3D(-50, 0, 100), + new Point3D(50, 0, 100), + Color.YELLOW, 3.0 +)); + +// Polygon (triangle or quad) +scene.addShape(SolidPolygon.triangle( + new Point3D(0, 0, 0), + new Point3D(50, 0, 0), + new Point3D(25, 50, 0), + Color.BLUE +)); +scene.addShape(SolidPolygon.quad( + new Point3D(-50, -50, 0), + new Point3D(50, -50, 0), + new Point3D(50, 50, 0), + new Point3D(-50, 50, 0), + Color.WHITE +)); +#+end_src + +** Custom Composite Shape + +#+begin_src java +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +AbstractCompositeShape myShape = new AbstractCompositeShape(new Point3D(0, 0, 200)); +myShape.addShape(new Line(p1, p2, Color.RED, 2.0)); +myShape.addShape(new SolidPolygonCube(Point3D.origin(), 10, Color.BLUE)); +scene.addShape(myShape); +#+end_src + +** Lighting and Shading + +#+begin_src java +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.*; + +LightingManager lighting = viewPanel.getLightingManager(); +lighting.addLight(new LightSource(new Point3D(100, -100, 200), Color.YELLOW)); +lighting.setAmbientLight(new Color(20, 20, 20)); + +// Enable shading on solid shapes +SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 50, 16, Color.RED); +sphere.setShadingEnabled(true); +scene.addShape(sphere); +#+end_src + +** CSG Operations (Boolean Operations) + +#+begin_src java +// Create two shapes +SolidPolygonCube box = new SolidPolygonCube(new Point3D(0, 0, 200), 50, Color.GREEN); +SolidPolygonSphere sphere = new SolidPolygonSphere(new Point3D(0, 0, 200), 35, 16, Color.RED); + +// Subtract sphere from box (creates a box with spherical hole) +box.subtract(sphere); +scene.addShape(box); + +// Union: combine shapes +// box.union(sphere); + +// Intersect: keep only overlapping parts +// box.intersect(sphere); +#+end_src + +** Text in 3D + +#+begin_src java +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas; +import eu.svjatoslav.aukio.e3d.gui.TextPointer; +import eu.svjatoslav.aukio.e3d.math.Transform; + +// Create text canvas +Transform location = new Transform(new Point3D(0, 0, 500)); +TextCanvas canvas = new TextCanvas(location, "Hello World!", Color.WHITE, Color.BLACK); +scene.addShape(canvas); + +// Or create blank canvas and write to it +TextCanvas blank = new TextCanvas(location, new TextPointer(10, 40), Color.GREEN, Color.BLACK); +blank.locate(0, 0); // row 0, column 0 +blank.print("Line 1"); +blank.locate(1, 0); +blank.print("Line 2"); +blank.setForegroundColor(Color.YELLOW); +blank.putChar('X'); +#+end_src + +** Animation with FrameListener + +#+begin_src java +viewPanel.addFrameListener((panel, deltaMs) -> { + double rotationIncrement = deltaMs * 0.001; // radians per ms + + // Update shape transform + currentAngle += rotationIncrement; + myShape.setTransform(new Transform( + myShape.getLocation(), + currentAngle, 0 // yaw, pitch + )); + + return true; // return true to request repaint +}); +#+end_src + +** Camera Control + +#+begin_src java +import eu.svjatoslav.aukio.e3d.math.Quaternion; + +Camera camera = viewPanel.getCamera(); + +// Set position +camera.getTransform().setTranslation(new Point3D(100, -50, -300)); + +// Set orientation (quaternion from yaw/pitch angles) +camera.getTransform().getRotation().set(Quaternion.fromAngles(0.5, -0.3)); + +// Look at a specific point (convenience method) +// camera.lookAt(new Point3D(0, 0, 200)); +#+end_src + +** Mouse Interaction on Shapes + +#+begin_src java +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController; +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent; + +SolidPolygonCube clickableCube = new SolidPolygonCube(Point3D.origin(), 50, Color.BLUE); +clickableCube.setMouseInteractionController(new MouseInteractionController() { + @Override + public void mouseClicked(final MouseEvent event) { + System.out.println("Cube clicked at: " + event.coordinate); + } + + @Override + public void mouseEntered(final MouseEvent event) { + clickableCube.setColor(Color.RED); + } + + @Override + public void mouseExited(final MouseEvent event) { + clickableCube.setColor(Color.BLUE); + } +}); +scene.addShape(clickableCube); +#+end_src + +* Class Catalog +:PROPERTIES: +:ID: 5522594a-969f-4cdb-b80e-d3d6bc732647 +:END: + +** Geometry (~geometry/~) + +| Class | File | Purpose | Key Methods | +|-----------+----------------+---------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------| +| ~Point3D~ | ~Point3D.java~ | Mutable 3D point/vector. *Public fields:* ~x~, ~y~, ~z~ | ~.add()~, ~.subtract()~, ~.multiply()~, ~.rotate()~, ~.getDistanceTo()~, ~.clone()~, ~.withAdded()~ (returns new) | +| ~Point2D~ | ~Point2D.java~ | 2D screen coordinate | ~.add()~, ~.subtract()~, ~.to3D()~ | +| ~Box~ | ~Box.java~ | Axis-aligned bounding box | ~.getCenter()~, ~.enlarge()~, ~.intersectsAABB()~ | +| ~Frustum~ | ~Frustum.java~ | View frustum (6 planes) | ~.update()~, ~.intersectsAABB()~ | +| ~BspTree~ | ~BspTree.java~ | BSP tree for CSG | ~.addPolygons()~, ~.clipPolygons()~, ~.invert()~, ~.allPolygons()~ | +| ~Plane~ | ~Plane.java~ | Infinite plane (Hesse normal) | ~.fromPoints()~, ~.splitPolygon()~ | + +** Math (~math/~) + +| Class | File | Purpose | Key Methods | +|------------------+-----------------------+-------------------------------+---------------------------------------------------------------------------------| +| ~Transform~ | ~Transform.java~ | Translation + rotation | ~.setTranslation()~, ~.transform(point)~, ~.withTransformed()~ | +| ~TransformStack~ | ~TransformStack.java~ | Stack of transforms | ~.addTransform()~, ~.transform()~, ~.dropTransform()~ | +| ~Quaternion~ | ~Quaternion.java~ | 3D rotation (unit quaternion) | ~.fromAngles(yaw, pitch)~, ~.multiply()~, ~.invert()~, ~.toMatrix3x3()~ | +| ~Vertex~ | ~Vertex.java~ | Wraps Point3D + transform | ~.coordinate~, ~.transformedCoordinate~, ~.calculateLocationRelativeToViewer()~ | + +** Renderer Core (~renderer/raster/~) + +| Class | File | Purpose | Key Methods | +|--------------------+-------------------------+----------------------------------+-----------------------------------------------------------------------------------------------------| +| ~Color~ | ~Color.java~ | RGBA color (NOT java.awt.Color!) | ~.set(r,g,b,a)~, ~.toAwtColor()~. Constants: ~RED~, ~GREEN~, ~BLUE~, ~BLACK~, ~WHITE~, ~CYAN~, etc. | +| ~ShapeCollection~ | ~ShapeCollection.java~ | Root scene container | ~.addShape()~, ~.hideGroup()~, ~.showGroup()~, ~.removeGroup()~ | +| ~RenderAggregator~ | ~RenderAggregator.java~ | Collects, sorts, paints shapes | ~.queueShapeForRendering()~, ~.sort()~, ~.paint()~ | + +** Shapes - Base (~renderer/raster/shapes/~) + +| Class | File | Purpose | +|---------------------------+---------------------------------------+-----------------------------------------------------------------| +| ~AbstractShape~ | ~shapes/AbstractShape.java~ | Base class for all shapes. Bounding box caching. | +| ~AbstractCoordinateShape~ | ~shapes/AbstractCoordinateShape.java~ | Base for shapes with vertices. Has ~List~, ~onScreenZ~. | + +** Shapes - Basic (~shapes/basic/~) + +| Class | File | Purpose | Constructor | +|--------------------+-----------------------------------------------+------------------------------+---------------------------------------------------------| +| ~Line~ | ~basic/line/Line.java~ | 3D line segment | ~new Line(p1, p2, color, width)~ | +| ~LineAppearance~ | ~basic/line/LineAppearance.java~ | Factory for consistent lines | ~new LineAppearance(width, color)~ → ~.getLine(p1, p2)~ | +| ~SolidPolygon~ | ~basic/solidpolygon/SolidPolygon.java~ | Solid convex N-gon | ~SolidPolygon.triangle(...)~ or ~.quad(...)~ | +| ~TexturedTriangle~ | ~basic/texturedpolygon/TexturedTriangle.java~ | Textured triangle with UV | ~new TexturedTriangle(v1,v2,v3,texture)~ | +| ~Billboard~ | ~basic/Billboard.java~ | Texture facing camera | ~new Billboard(position, scale, texture)~ | +| ~GlowingPoint~ | ~basic/GlowingPoint.java~ | Glowing circular point | ~new GlowingPoint(position, scale, color)~ | + +** Shapes - Composite Wireframe (~shapes/composite/wireframe/~) + +| Class | Constructor | +|---------------------+-------------------------------------------------------------| +| ~WireframeCube~ | ~new WireframeCube(center, halfSize, appearance)~ | +| ~WireframeBox~ | ~new WireframeBox(corner1, corner2, appearance)~ | +| ~WireframeSphere~ | ~new WireframeSphere(center, radius, appearance)~ | +| ~WireframeCylinder~ | ~new WireframeCylinder(center, radius, height, appearance)~ | +| ~WireframeCone~ | ~new WireframeCone(center, radius, height, appearance)~ | +| ~WireframeArrow~ | ~new WireframeArrow(start, end, appearance)~ | +| ~Grid2D~ | 2D grid plane | +| ~Grid3D~ | 3D grid in space | + +** Shapes - Composite Solid (~shapes/composite/solid/~) + +| Class | Constructor | +|------------------------------+-----------------------------------------------------------| +| ~SolidPolygonCube~ | ~new SolidPolygonCube(center, halfSize, color)~ | +| ~SolidPolygonRectangularBox~ | ~new SolidPolygonRectangularBox(corner1, corner2, color)~ | +| ~SolidPolygonSphere~ | ~new SolidPolygonSphere(center, radius, segments, color)~ | +| ~SolidPolygonCylinder~ | Solid cylinder | +| ~SolidPolygonCone~ | Solid cone | +| ~SolidPolygonArrow~ | Solid arrow | + +** Shapes - Composite Base (~shapes/composite/base/~) + +| Class | File | Purpose | Key Methods | +|--------------------------+------------------------------------+-----------------------+-------------------------------------------------------------------------------------------------| +| ~AbstractCompositeShape~ | ~base/AbstractCompositeShape.java~ | Group shapes, CSG ops | ~.addShape()~, ~.subtract()~, ~.union()~, ~.intersect()~, ~.setShadingEnabled()~, ~.setColor()~ | +| ~LightmappedCompositeShape~ | ~composite/LightmappedCompositeShape.java~ | Composite that fan-triangulates and optionally carries GI lightmaps | ~.setLightmappingEnabled(true)~ | +| ~TriangleMeshBlock~ | ~basic/texturedpolygon/TriangleMeshBlock.java~ | SoA fast path for large textured triangle meshes (flat double[] arrays, one tight transform loop) | build from mesh data | + +** Text (~shapes/composite/textcanvas/~) + +| Class | File | Purpose | Key Methods | +|--------------+------------------------------+-----------------+-----------------------------------------------------------------------------------| +| ~TextCanvas~ | ~textcanvas/TextCanvas.java~ | Text grid in 3D | ~.print()~, ~.locate(row,col)~, ~.clear()~, ~.setForegroundColor()~, ~.putChar()~ | + +** Texture (~renderer/raster/texture/~) + +| Class | File | Purpose | +|--------------------+---------------------------------+----------------------------------------------| +| ~Texture~ | ~texture/Texture.java~ | 2D texture with mipmaps | +| ~TextureBitmap~ | ~texture/TextureBitmap.java~ | Raw pixel array for one mipmap level | +| ~TextureGenerator~ | ~texture/TextureGenerator.java~ | Factory for common textures (glows, borders) | + +** Lighting (~renderer/raster/lighting/~) + +| Class | File | Purpose | Key Methods | +|-------------------+---------------------------------+----------------+-----------------------------------------------------------| +| ~LightingManager~ | ~lighting/LightingManager.java~ | Manages lights | ~.addLight()~, ~.setAmbientLight()~, ~.computeLighting()~ | +| ~LightSource~ | ~lighting/LightSource.java~ | Point light | ~new LightSource(position, color)~ → ~.setIntensity()~ | + +** GUI (~gui/~) + +| Class | File | Purpose | Key Methods | +|-----------------+--------------------------+------------------------------+--------------------------------------------------------------------------------------------------------| +| ~ViewPanel~ | ~gui/ViewPanel.java~ | AWT Canvas, render loop | ~.getRootShapeCollection()~, ~.getCamera()~, ~.getLightingManager()~, ~.addFrameListener()~, ~.stop()~ | +| ~ViewFrame~ | ~gui/ViewFrame.java~ | JFrame wrapper | ~new ViewFrame()~ → ~.getViewPanel()~ | +| ~Camera~ | ~geometry/Camera.java~ | Viewer position/orientation | ~.getTransform()~, ~.setTransform()~ | +| ~FrameListener~ | ~gui/FrameListener.java~ | Per-frame callback interface | ~.onFrame(panel, deltaMs)~ → return true to repaint | + +** Input (~gui/humaninput/~) + +| Class | File | Purpose | +|------------------------------+----------------------------------------------+--------------------------------| +| ~InputManager~ | ~humaninput/InputManager.java~ | Mouse/keyboard tracking | +| ~MouseInteractionController~ | ~humaninput/MouseInteractionController.java~ | Interface for clickable shapes | +| ~KeyboardFocusStack~ | ~humaninput/KeyboardFocusStack.java~ | Focus management for widgets | + +** Octree Renderer (~renderer/octree/~) + +Alternative rendering path for voxel volumes with ray tracing. + +| Class | File | Purpose | +|----------------+-----------------------------------+-----------------------------| +| ~OctreeVolume~ | ~octree/OctreeVolume.java~ | Sparse voxel octree storage | +| ~RayTracer~ | ~octree/raytracer/RayTracer.java~ | Ray tracing renderer | + +* Architecture & Key Concepts +:PROPERTIES: +:ID: 4be2246b-fcb6-4a1f-bcf8-7d657a67d957 +:END: + +** Coordinate System (CRITICAL) + +Aukio 3D uses *left-handed coordinates* matching 2D screen space: + +| Axis | Positive = | Example | +|------+-------------+----------------------------------| +| X | RIGHT | Larger X = further right | +| Y | DOWN | Smaller Y = higher (up visually) | +| Z | INTO screen | Negative Z = closer to camera | + +*To place A ABOVE B:* give A a *smaller Y* (~y - offset~) +*To place A BELOW B:* give A a *larger Y* (~y + offset~) + +This is opposite to Y-up engines (OpenGL, Unity, Blender). + +** Shape Hierarchy + +#+begin_example +AbstractShape (base) + ├── AbstractCoordinateShape (has vertices) + │ ├── Line + │ ├── SolidPolygon + │ ├── TexturedTriangle + │ ├── Billboard + │ └── GlowingPoint + └── AbstractCompositeShape (groups shapes) + ├── Wireframe shapes (WireframeCube, WireframeSphere, ...) + ├── Solid shapes (SolidPolygonCube, SolidPolygonSphere, ...) + └── TextCanvas +#+end_example + +** Render Pipeline + +#+begin_example +ViewPanel.renderFrame() + 1. ShapeCollection.transformShapes() — apply camera transform, cull, queue + 2. ShapeCollection.sortShapes() — radix sort by Z (back-to-front) + 3. ShapeCollection.paintShapes() — tiled parallel paint, two-pass z-buffer + (opaque front-to-back with depth writes, then alpha back-to-front) + 4. BufferStrategy.show() — page flip to display +#+end_example + +- Shapes implement ~transform()~ to project from world to screen space +- Shapes implement ~paint()~ to rasterize to pixel buffer +- ~onScreenZ~ determines render order (set during transform phase) + +** Backface Culling + +Uses signed area in screen space: +- ~signedArea < 0~ → front-facing (CCW winding) +- ~signedArea > 0~ → back-facing (CW winding) + +Vertex order for front face: *top → lower-left → lower-right* (as seen from camera) + +* Build & Test +:PROPERTIES: +:ID: 7909eb6a-c9a0-45f6-b1a5-a347e469c6cf +:END: + +#+begin_src bash +# Build +mvn clean install + +# Run all tests +mvn test + +# Run single test class +mvn test -Dtest=TextLineTest + +# Run specific test method +mvn test -Dtest=TextLineTest#testAddIdent + +# Golden-image regression tests (aukio-3d-demos repo) +cd ../aukio-3d-demos && mvn clean package +mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt +java -cp "target/classes:$(cat cp.txt)" \ + eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens # verify +java -cp "target/classes:$(cat cp.txt)" \ + eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens --update # regenerate goldens + +# Regenerate all HTML documentation (org -> HTML, darksun theme) +Documentation/export-docs.sh # export +Documentation/export-docs.sh --check # export + headless-Chrome screenshots to /tmp +#+end_src + +Test files: ~src/test/java/~ (JUnit 4) + +* Headless Testing Toolkit (~headless/~) +:PROPERTIES: +:ID: b1f4a2c8-headless-toolkit +:END: + +Windowless rendering and verification — no Swing frame, no X display, +no render thread. Drives the same transform/sort/paint pipeline the +on-screen path uses. Built for tests, doc tooling and AI agents. + +** Rendering a snapshot + +#+begin_src java +import eu.svjatoslav.aukio.e3d.headless.Snapshot; + +// Pose string = "x, y, z, yaw, pitch, roll" — the exact format demos +// print and bug reports quote. Snapshot.cameraFromPose / poseString +// convert both ways. +BufferedImage image = Snapshot.render(scene, lighting, + "290.31, -35.59, -2.10, -0.58, -0.15, 0.0", 640, 480); +Snapshot.save(image, "/tmp/snap.png"); +#+end_src + +~Snapshot.render(scene, lighting, camera, w, h)~ is the Camera-based +overload; ~Snapshot.renderInto(scene, camera, ctx, backgroundArgb)~ +paints into an existing ~RenderingContext~ with a chosen background +(sentinel color = "unpainted" for hole detection). + +** Pixel assertions + +#+begin_src java +import eu.svjatoslav.aukio.e3d.headless.PixelAssertions; + +double holes = PixelAssertions.unpaintedFraction(image, 0, + 0.15, 0.45, 0.85, 1.0); // lower-center band, relative coords +long red = PixelAssertions.countColor(image, 0xFF0000); +String grid = PixelAssertions.dumpPixelGrid(image, 320, 240, 3, 8); +#+end_src + +** Golden-image comparison + +#+begin_src java +import eu.svjatoslav.aukio.e3d.headless.GoldenImage; + +GoldenImage.Result r = GoldenImage.compare(actual, + new File("goldens/house.png"), 4, 0.005); // channel tol, max diff fraction +GoldenImage.saveDiff(actual, goldenFile, "/tmp/diff.png"); // red = differs +#+end_src + +CLI: ~java eu.svjatoslav.aukio.e3d.headless.GoldenImage a.png b.png [tol] [maxFrac]~ +(exit 0 = match, 1 = differ). + +The House demo has ready-made goldens in the aukio-3d-demos repo: +~eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens~ checks two poses +(default view + the near-plane straddle bug pose) against +~aukio-3d-demos/goldens/*.png~ and asserts the floor has no holes. +Run ~--update~ to regenerate after an intentional visual change. + +** Scene dump + +#+begin_src java +import eu.svjatoslav.aukio.e3d.headless.SceneDump; + +System.out.println(SceneDump.dump(scene, lighting, camera, gi)); +// shapes: 5 top-level, 546 queued for rendering +// lights: 4 (ambient #181818) + per-light pos/color/intensity +// camera: 290.31, -35.59, -2.10, -0.58, -0.15, -0.00 +// GI: running, 152034 work items, converged +#+end_src + +** Engine API added for headless use + +- ~ShapeCollection.transformShapes(Camera, RenderingContext)~ — camera-based + overload; the ~ViewPanel~ variant delegates to it +- ~ShapeCollection.getRootComposite()~ — direct root access (GI snapshots) +- ~RenderingContext.getImage()~ — the backing BufferedImage +- ~GlobalIllumination.isRunning() / isConverged() / getWorkItemCount()~ + +** House demo scene without a window (aukio-3d-demos) + +~HouseDemo.buildHouse(house)~, ~HouseDemo.addFurniture(house)~ and +~HouseDemo.addLights(lighting, scene)~ are public: tests and tools can +rebuild the exact demo scene headlessly. + +* Tips for AI Agents +:PROPERTIES: +:ID: 597b2b14-1ee1-440b-bd40-ab7f3ed405de +:END: + +1. *Always use project Color:* ~eu.svjatoslav.aukio.e3d.renderer.raster.Color~ (NOT ~java.awt.Color~) +2. *Point3D is mutable:* Clone before storing references: ~point.clone()~ +3. *Y is down:* Remember coordinate system when positioning elements +4. *SolidPolygon works for quads:* Use ~SolidPolygon.quad(p1,p2,p3,p4,color)~ - automatically triangulated +5. *CSG on AbstractCompositeShape:* Only composite shapes support ~subtract()~, ~union()~, ~intersect()~ +6. *Animations via FrameListener:* Return ~true~ from ~onFrame()~ to trigger repaint +7. *Shading needs lighting:* ~setShadingEnabled(true)~ + add ~LightSource~ to ~LightingManager~ +8. *Wireframe shapes need LineAppearance:* ~new LineAppearance(width, Color)~ for consistent line styling +9. *Group visibility:* Use ~.addShape(shape, "groupName")~ then ~.hideGroup()~ / ~.showGroup()~ + +* Documentation (Org Mode) +:PROPERTIES: +:ID: 1d942e3b-6071-440f-bd9b-876c8f7c53de +:END: + +| Path | Topic | +|----------------------------------------------+---------------------------------------------------------------------| +| ~Documentation/index.org~ | Main: coordinate system, shapes, CSG, developer tools | +| ~Documentation/Rendering loop/index.org~ | 5-phase pipeline, multi-threaded paint | +| ~Documentation/Shading/index.org~ | Lambert shading, lights, distance attenuation | +| ~Documentation/CSG/index.org~ | Boolean ops via BSP trees | +| ~Documentation/Frustum culling/index.org~ | View frustum culling | +| ~Documentation/Near plane clip/index.org~ | Near-plane polygon clipping (straddling geometry) | +| ~Documentation/Global illumination/index.org~ | Progressive GI: lightmaps, bounces, convergence | +| ~Documentation/Perspective correct textures/index.org~ | Texture mapping math | +| ~Documentation/Stereoscopic rendering/index.org~ | Side-by-side stereo: two passes, per-eye viewports, IPD | +| ~Documentation/Depth buffer/index.org~ | Two-pass z-buffer, zw, depth margin, Hi-Z pyramid, determinism | +| ~Documentation/SDF textures/index.org~ | SDF text: glyph fields, coverage window, TextCanvas | + +Regenerate all HTML: ~Documentation/export-docs.sh~ (add ~--check~ for rendered +screenshots of every page). + +Every heading in a doc page needs a ~:CUSTOM_ID:~ property (kebab-case slug) +right below the heading line. Without it org-html export generates +~org~ anchors and TOC links come out as +~#outline-container-org5fddfe7~ instead of human-readable +~#outline-container-engine-internals~. diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/COPYING @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/Documentation/Agentic development/Golden workflow.svg b/Documentation/Agentic development/Golden workflow.svg new file mode 100644 index 0000000..aa0e7fb --- /dev/null +++ b/Documentation/Agentic development/Golden workflow.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Snapshot.render + scene + pose + + + + + + goldens/*.png + committed reference + + + + + GoldenImage + .compare + + + + + PASS + exit 0 + + + + + FAIL, exit 1 + + diff PNG to /tmp + + + + + bug? fix code + intended? run + --update + + + + regenerates reference + + tolerance: per-channel delta + max differing-pixel fraction — shading is deterministic, keep both tight + diff --git a/Documentation/Agentic development/Headless lanes.svg b/Documentation/Agentic development/Headless lanes.svg new file mode 100644 index 0000000..1303a80 --- /dev/null +++ b/Documentation/Agentic development/Headless lanes.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ViewPanel + window + render thread + camera from user input + + + + Snapshot.render() + no window, no display + camera from pose string + + + + SAME pipeline + transform + ↓ + sort by Z + ↓ + paint + + + + screen + BufferStrategy + + + BufferedImage + → PNG (Snapshot.save) + → PixelAssertions + → GoldenImage + + + + + + + + + + + identical results + headless rendering drives the very same code the window uses — a test render IS the real render + diff --git a/Documentation/Agentic development/Pixel assertion.svg b/Documentation/Agentic development/Pixel assertion.svg new file mode 100644 index 0000000..a9218f6 --- /dev/null +++ b/Documentation/Agentic development/Pixel assertion.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + rendered frame (640×480) + + + + + + painted + painted + + + + region (0.15, 0.45) → (0.85, 1.0) + + + + hole + + + + PixelAssertions + unpaintedFraction(img, + bg=0x000000, + 0.15, 0.45, + 0.85, 1.0) + counts pixels that still + equal the background + → 0.017 > 0.01 FAIL + + + + + sentinel background: "nothing rendered here" is unambiguous, even in dark scenes + diff --git a/Documentation/Agentic development/diff-example.png b/Documentation/Agentic development/diff-example.png new file mode 100644 index 0000000000000000000000000000000000000000..941d5ca0af12a3e722842aea910e0a65af3029bb GIT binary patch literal 10414 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 literal 0 HcmV?d00001 diff --git a/Documentation/Agentic development/snapshot-example.png b/Documentation/Agentic development/snapshot-example.png new file mode 100644 index 0000000000000000000000000000000000000000..92ea03c54aafde8a4b35d9e9770949912bf75c3d GIT binary patch literal 20380 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 R0a5pmwHj^mxs@9a{s&cOsK@{S literal 0 HcmV?d00001 diff --git a/Documentation/CSG/BSP tree.svg b/Documentation/CSG/BSP tree.svg new file mode 100644 index 0000000..eb89c2c --- /dev/null +++ b/Documentation/CSG/BSP tree.svg @@ -0,0 +1,45 @@ + + + + + + + + + Plane P₁ + + + + + + + + + front + back + + + + Front (P₂) + + + + Back (P₃) + + + + + + + + + + + leaf + + leaf + + + Each plane divides space into front (normal side) and back (opposite) + Polygons are classified and split at each partitioning plane + diff --git a/Documentation/CSG/CSG demo.png b/Documentation/CSG/CSG demo.png new file mode 100644 index 0000000000000000000000000000000000000000..2275350ce6d1e62e0b28fae99bf68e24ce20b358 GIT binary patch literal 35668 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 IEPRsx3-D+e;Q#;t literal 0 HcmV?d00001 diff --git a/Documentation/CSG/CSG intersect.svg b/Documentation/CSG/CSG intersect.svg new file mode 100644 index 0000000..a912f81 --- /dev/null +++ b/Documentation/CSG/CSG intersect.svg @@ -0,0 +1,41 @@ + + + + + + + + + + Input + + A + + B + + + ∩ + intersect + + + + + + + Result: A ∩ B + + + + + + + + + + + + Find overlap + between both + Only shared volume + remains + diff --git a/Documentation/CSG/CSG operations.svg b/Documentation/CSG/CSG operations.svg new file mode 100644 index 0000000..3f73cbe --- /dev/null +++ b/Documentation/CSG/CSG operations.svg @@ -0,0 +1,37 @@ + + + + + + + + + Input + + A + + B + + + − + subtract + + + + + + + Result: A − B + + + + + + cavity + + + B is the "cutter" + carves out of A + Cube with cavity + interior faces visible + diff --git a/Documentation/CSG/CSG union.svg b/Documentation/CSG/CSG union.svg new file mode 100644 index 0000000..f1eedec --- /dev/null +++ b/Documentation/CSG/CSG union.svg @@ -0,0 +1,38 @@ + + + + + + + + Input + + A + + B + + + + + union + + + + + + + Result: A + B + + + + + + removed + + + Keeps all geometry + from both shapes + Single combined volume + interior faces removed + diff --git a/Documentation/CSG/Polygon clipping.svg b/Documentation/CSG/Polygon clipping.svg new file mode 100644 index 0000000..41de628 --- /dev/null +++ b/Documentation/CSG/Polygon clipping.svg @@ -0,0 +1,39 @@ + + + + + + + + Polygon crosses plane + + + plane + + + + + → + split + + + Split into fragments + + + + + front + + + + back + + + + + + new edge + + + Spanning polygons are split; each fragment goes to its respective subtree + diff --git a/Documentation/CSG/index.org b/Documentation/CSG/index.org new file mode 100644 index 0000000..ebdbad6 --- /dev/null +++ b/Documentation/CSG/index.org @@ -0,0 +1,284 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Constructive Solid Geometry - Aukio 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 + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* What is CSG? +:PROPERTIES: +:CUSTOM_ID: what-is-csg +:ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +:END: + +*Constructive Solid Geometry* (CSG) is a modeling technique that builds +complex 3D shapes by combining simpler primitives using boolean +operations. Instead of manually creating every vertex and face, you +define shapes as the result of operations like "merge these two cubes" +or "carve a hole using this sphere." + +CSG is particularly powerful for: +- *Procedural modeling* — generate complex geometry algorithmically +- *CAD/CAM applications* — define parts as combinations of primitives +- *Game development* — create architectural elements, holes, cavities +- *Rapid prototyping* — iterate on designs by adjusting operations + +The three fundamental CSG operations are: + +| Operation | Symbol | Result | +|-------------+--------+-------------------------------------------| +| Subtract | A - B | A with B carved out (holes, cavities) | +| Union | A + B | Combined volume (both shapes merged) | +| Intersect | A ∩ B | Volume where both overlap | + +See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#csg-demo][CSG demo]] for an interactive visualization. + +* The Three Operations +:PROPERTIES: +:CUSTOM_ID: the-three-operations +:END: + +#+attr_html: :class responsive-img +#+attr_latex: :width 1000px +[[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#csg-demo][file:CSG%20demo.png]] + +The screenshot above shows all three operations displayed left to right: +subtract (green cube with spherical cavity), union (merged green and +orange shapes), and intersect (only the overlapping region in blue). + +The diagrams below use the same green cube (A) and orange sphere (B) as +the screenshot above. Each operation transforms these inputs differently, +producing the results shown from left to right in the image. + +** Subtract (A - B) +:PROPERTIES: +:CUSTOM_ID: subtract-operation +:END: + +#+INCLUDE: "CSG operations.svg" export html + +*Subtract* removes the orange sphere (B) from the green cube (A), carving +out a cavity. The diagram shows B acting as a "cutter" — where it overlaps +A, a hole is created. Interior faces *are preserved* and become visible, +allowing you to see inside the carved-out space (shown as the orange dashed +curve in the result). + +This matches the leftmost shape in the screenshot: a green cube with a +visible spherical hollow inside, showing the interior surfaces created by +the subtraction. + +This operation is ideal for creating: +- Holes and tunnels +- Carved-out spaces +- Hollow objects + +#+BEGIN_SRC java +SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN); +SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE); + +cube.subtract(sphere); // cube now has a spherical cavity +#+END_SRC + +** Union (A + B) +:PROPERTIES: +:CUSTOM_ID: union-operation +:END: + +#+INCLUDE: "CSG union.svg" export html + +*Union* merges the green cube (A) and orange sphere (B) into one continuous +volume. The diagram shows both shapes combining — the interior seam (where +they overlap) is removed, creating a single solid surface with no internal +boundaries (indicated by the dashed blue line labeled "removed"). + +This corresponds to the center shape in the screenshot: both green and +orange colors present but seamlessly joined, forming one unified object. + +#+BEGIN_SRC java +SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN); +SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE); + +cube.union(sphere); // cube now contains the merged result +#+END_SRC + +** Intersect (A ∩ B) +:PROPERTIES: +:CUSTOM_ID: intersect-operation +:END: + +#+INCLUDE: "CSG intersect.svg" export html + +*Intersect* keeps only the volume where the green cube (A) and orange +sphere (B) overlap — the region that is inside *both* shapes +simultaneously. The diagram shows this as the blue-shaded area: the +portion of the sphere that fits within the cube boundaries. Everything +else is discarded. + +This is the rightmost shape in the screenshot: only the overlapping +portion remains, showing which parts of space were occupied by both the +cube and sphere at the same time. + +This operation is useful for: +- Creating shapes constrained by multiple boundaries +- Finding collision regions +- Trimming geometry to fit within bounds + +#+BEGIN_SRC java +SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 80, Color.GREEN); +SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 60, 8, Color.ORANGE); + +cube.intersect(sphere); // only the overlapping region remains +#+END_SRC + +* BSP Tree Algorithm +:PROPERTIES: +:CUSTOM_ID: bsp-tree-algorithm +:END: + +CSG boolean operations are implemented using *Binary Space Partitioning* +(BSP) trees. A BSP tree recursively divides 3D space using planes, +creating a hierarchical structure that enables efficient polygon clipping +and spatial queries. + +** BSP Tree Structure +:PROPERTIES: +:CUSTOM_ID: bsp-tree-structure +:END: + +#+INCLUDE: "BSP tree.svg" export html + +Each BSP node contains: +- A *partitioning plane* that divides space into two half-spaces +- *Polygons* that lie exactly on this plane (coplanar) +- *Front* subtree — polygons on the same side as the plane's normal +- *Back* subtree — polygons on the opposite side + +** Key BSP Operations +:PROPERTIES: +:CUSTOM_ID: key-bsp-operations +:END: + +The BSP tree provides three core operations that enable CSG: + +| Operation | Description | +|----------------+--------------------------------------------------| +| =invert()= | Flip all normals, swap front/back children | +| =clipTo(tree)= | Remove polygons inside the other tree's solid | +| =addPolygons()= | Insert new polygons, splitting at planes | + +*Invert* is fundamental to CSG. By flipping inside/outside, we can +transform subtraction and intersection into variations of clipping: + +- **Subtract** = invert A, clip against B, add B's clipped parts, invert back +- **Intersect** = invert A, clip B against A, invert B, clip A against B, combine, invert A back + +** Polygon Clipping +:PROPERTIES: +:CUSTOM_ID: polygon-clipping +:END: + +When a polygon crosses a partitioning plane, it's *split* into two +fragments: + +#+INCLUDE: "Polygon clipping.svg" export html + +This recursive splitting ensures that all polygons are cleanly classified +as entirely in front, entirely behind, or exactly on a plane — never +"spanning" across. + +* Using CSG in Aukio 3D +:PROPERTIES: +:CUSTOM_ID: using-csg-in-aukio-3d +:END: + +CSG operations are methods on [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]. They modify the +shape *in-place* — the result replaces the original geometry. + +** Basic Usage +:PROPERTIES: +:CUSTOM_ID: basic-usage +:END: + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.*; + +// Create two shapes +SolidPolygonCube cube = new SolidPolygonCube(Point3D.origin(), 100, Color.GREEN); +SolidPolygonSphere sphere = new SolidPolygonSphere(Point3D.origin(), 70, 12, Color.ORANGE); + +// Perform CSG operations (in-place modification) +cube.subtract(sphere); // Cube with spherical cavity +// or +cube.union(sphere); // Merged shape +// or +cube.intersect(sphere); // Only overlapping region + +// Add to scene +shapes.addShape(cube.setBackfaceCulling(true)); +#+END_SRC + +** Child Handling Behavior +:PROPERTIES: +:CUSTOM_ID: child-handling +:END: + +CSG operations only affect *SolidPolygon* geometry. Other children are +preserved as objects: + +| Child Type | Union | Subtract | Intersect | +|-----------------------+------------------+------------------+------------------| +| SolidPolygon (this) | Replaced with result | Replaced with result | Replaced with result | +| SolidPolygon (other) | Merged into result | Discarded (cutter) | Discarded | +| Line, TextCanvas (this) | Preserved | Preserved | Preserved | +| Line, TextCanvas (other) | Merged into this shape | Discarded | Discarded | +| Nested composite | Preserved as object — but see below | same | same | + +*Nested composites are not CSG-safe.* Polygon extraction recurses into +them, so their SolidPolygons are included in the BSP result — while the +nested composite object itself is also preserved, duplicating that +geometry in the render. Apply CSG to flat composites, or extract the +nested polygons first. + +This allows you to attach labels, decorations, or wireframe overlays to +shapes without them being affected by CSG operations (for union, the +other shape's decorations are copied over too). + +** Important Notes +:PROPERTIES: +:CUSTOM_ID: important-notes +:END: + +1. *Shapes are modified in-place*. The original geometry is replaced. + Clone shapes beforehand if you need to preserve the originals. + +2. *CSG works on SolidPolygon children only*. TexturedTriangle and other + shape types are not processed. + +3. *Result quality depends on mesh density*. Low-polygon inputs may + produce visible artifacts at intersection boundaries. Use higher + subdivision counts for smoother results. + +4. *Backface culling is recommended*. CSG results often have internal + faces from the cutting operation. Enable culling to hide backfaces: + =shape.setBackfaceCulling(true)= + +* Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|--------------------------+------------------------------------------------------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/BspTree.html][BspTree]] | BSP tree for spatial partitioning and CSG operations | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Plane]] | Partitioning plane used by BSP nodes | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Polygon shape processed by CSG | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] | Base class with union/subtract/intersect methods | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.html][SolidPolygonMesh]] | Custom polygon mesh for arbitrary geometry | +| SolidPolygon* primitives | See [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery]] for all available shapes | diff --git a/Documentation/Coordinate system.svg b/Documentation/Coordinate system.svg new file mode 100644 index 0000000..4497bf3 --- /dev/null +++ b/Documentation/Coordinate system.svg @@ -0,0 +1,18 @@ + + + + + + X + right (+) / left (-) + + + Y + down (+) / up (-) + + + Z + away (+) / towards (-) + Origin + (0, 0, 0) + \ No newline at end of file diff --git a/Documentation/Depth buffer/index.org b/Documentation/Depth buffer/index.org new file mode 100644 index 0000000..2f2c75d --- /dev/null +++ b/Documentation/Depth buffer/index.org @@ -0,0 +1,130 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Depth Buffer - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \setlength{\parindent}{15pt} +#+LATEX_HEADER: \usepackage{palatino} +#+LATEX_HEADER: \usepackage{charter} +#+HTML_HEAD: + +[[file:../index.html#outline-container-depth-buffer][<- Back to index]] + +* Per-pixel visibility +:PROPERTIES: +:CUSTOM_ID: per-pixel +:END: + +The engine resolves visibility with a depth buffer, not paint order. +Every rasterized triangle carries a per-pixel depth quantity =zw = +1/z= (camera-space), interpolated linearly across each span — =1/z= is +affine in screen space, so it rides the same edge interpolators as the +texture gradients. A fragment wins a pixel only where + +#+BEGIN_EXAMPLE +zw > stored - margin * zw^2 (margin = 0 by default) +#+END_EXAMPLE + +Default margin 0 is *strict depth*: the nearer fragment always wins, +regardless of paint order, so overlapping depth ranges — a floor tile +extending under furniture, a wall seen through a doorway — come out +correct per pixel. A nonzero margin (= RenderingContext.DEPTH_MARGIN_DZ =, +=-Daukio.zbuffer.margin=, world units) re-opens a tolerance window +*behind* the stored depth for near-coplanar pairs; any nonzero window +re-imports per-triangle sort errors into per-pixel occlusion, which is +why the default is strict. + +The depth buffer is allocated once per frame context +(=RenderingContext.depth=, float per pixel) and cleared per tile +together with the pixel buffer. + +* Two passes +:PROPERTIES: +:CUSTOM_ID: two-passes +:END: + +=RenderAggregator.paintSorted= paints the sorted queue in two passes: + +1. *Opaque pass* — opaque-class triangles, iterated front-to-back (the + queue is back-to-front, so reversed), depth test + depth write. + Front-to-back order is a pure performance hint: hidden fragments + die on the depth test *before* the texture fetch (early-z). +2. *Alpha pass* — alpha-class triangles (translucent solid polygons, + alpha-carrying textures, SDF text), iterated back-to-front in queue + order, depth test but *no depth write*. Translucency never + occludes, and overlapping translucent surfaces keep painter-coherent + mutual order. + +A shape's class comes from its paint color or texture: solid polygons +with =alpha = 255= are opaque, anything translucent is alpha-class; +textured triangles are alpha-class when the texture has alpha or is an +SDF mask. + +* Which shapes carry depth +:PROPERTIES: +:CUSTOM_ID: shapes +:END: + +- =TexturedTriangle= — opaque or alpha class by texture. +- =SolidPolygon= — depth-tested since 2026-09-17; opaque when its + (possibly shaded) color is fully opaque, translucent otherwise. + Near-plane-clipped quads fan-triangulate with depth like any other + triangles. +- =LightmappedTriangle= — a textured triangle, so the same rules. +- =Line=, =Billboard=, =GlowingPoint= — no depth by design: they are + 2D overlays (wireframes, markers, sprites) and always paint on top, + in the alpha pass. + +Because every occluder writes depth, scene code no longer needs any +ordering structure: composites just fan-triangulate their polygons. +(=LightmappedCompositeShape= exists only to wrap polygons as lightmap +carriers for the GI system, not to order them.) + +* Hi-Z occlusion pyramid +:PROPERTIES: +:CUSTOM_ID: hi-z +:END: + +After each successful paint, =ViewPanel= builds a Hi-Z pyramid from +the depth buffer (gui/HiZPyramid): 8-pixel tiles pooled upward, +each tile storing the *minimum* =zw= (farthest written depth — +max-pooling would store the nearest occluder and wrongly cull geometry +visible between near gaps). Next frame, =TriangleMeshBlock= projects +its world AABB's 8 corners and, when the nearest corner is still +behind the pyramid's stored depth, skips the whole block before any +per-triangle work. + +The test is conservative by construction (min-pooling plus sky pixels +at =-inf=), so it never culls visible geometry; wrong culls under +camera motion self-heal in one frame. Knobs: =-Daukio.hiz.margin=0.02=, +kill switch =-Daukio.hiz=false=. Headless snapshots never build the +pyramid, so golden renders are structurally unaffected. Stereo skips +the test (the pyramid is mono). + +* Determinism and depth dumps +:PROPERTIES: +:CUSTOM_ID: determinism +:END: + +The renderer is bit-deterministic: same scene and camera give +bit-identical pixels across runs, which is what the golden-image +regression tests compare. =Snapshot= (the headless toolkit) supports +=-Daukio.zbuffer.dumpDepth=path.png= to write a grayscale depth map +alongside the color image — useful when hunting depth-window bugs +(bisect those with =-Daukio.zbuffer.margin=0=). + +* Related classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Role | +|----------------------+----------------------------------------------------------| +| =RenderingContext= | =depth= buffer, =depthPass=, =DEPTH_MARGIN_DZ= constant | +| =RenderAggregator= | =paintSorted= two-pass driver, queue sort | +| =TexturedTriangle= | Z span writers (perspective and affine) | +| =SolidPolygon= | flat-color Z span writer, two-pass classification | +| =HiZPyramid= | temporal whole-block occlusion culling | +| =ViewPanel= | per-tile depth clear, pyramid rebuild after paint | + +[[file:../index.html#outline-container-depth-buffer][Back to main documentation]] diff --git a/Documentation/Developer tools/Developer tools.png b/Documentation/Developer tools/Developer tools.png new file mode 100644 index 0000000000000000000000000000000000000000..825b0de53cdab862994a07ae1bd80bd52d1be585 GIT binary patch literal 375468 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%0Af}e+(~3 zIL+FB`Zru)6#bzw$FE2SVq;j0AVL>UTD2%s9Hc#uuY#*FHu>Ehz^BL_M>yb4P@gIl z>GE7~b@M$4)T_QW$B$oGTCputaNM?_XqeY?$l)9CIDK|&(d^%Ows+>qLn^%1@qY1k z*!xZSD}$l-(DL8eFD)mbK;THOCgvFNIOIc!`j=XV?7P3qxk;xEPTde>V!tVHnnLIC zp-OR3ew7;zcEY~)oZtTHIl8sHK>Lewoy>)hlt<0{MKy_n2d?tdYJoo^QSxt!tvWc* z{q~A(;U{;A(;fTEH|V8F)EdRewsiu!*g$-XWvyM0O6#nN-?VF$+(9@jn;y5j_0Vg9 z1=ba9C2<3f`9!jeGoth)F1zGxtjNq(AIa+%i>1Y>_=)fRpdMz6yVbFESM{4mwwPm^ zQG@lmRap~ti^cDK%i6Jsvt*wI&B3zE_8!LCOm%BW-vY<1%%hw$uhsqio#n@0!1;w$ zsZ{AdgG`4#*f_fm&`^>YokRwikZ|I}KOsS7)?=pSWdv2RO9i|6M~;6lC611y$x+|< zQH!(N9R)`W)n7>Ge%h|O=A2=8=U)C%+I4#t3Z3Kcked_x=_39!JJ&&+>a%U=Vr#nh z4(m=K25z;R%ZnqXMT9B=kT+!5<JKAk>G~& zNq7idepYlQbrNG|HK)g&0zt$<3J%}y)QXq5hdgPb5S0fLD1&bNED6N_c|Adx7?M2b zg$ndtT}__`RrbhX*HsOS$xYv3L51~wZa%4Xx_hXk;!2^RzdS_dj2G~J9KpcwqoEyOei($slj@PDs7%k#uGyC<;3`*LjJem z2OqBi69l}KVQZ_E6(TmJUzo|n{|j3{q`$x-FpL|!Fa)C_NjYIMxc2zKU~dr;7fF#K zRqj1aa+^O3m*cwNpFI|qW;9|neBb+if1k*~gr3zLG+|j+>+|zvy{x-r;f*UzM(t%6 zj1LJrdH%!drYK5j?YIS@W2D&e`;sv{t463S#iF`IvLTAVQzAg#471P->YJy?HG$)S zK`0an7=KdS{!q@ zf{%>WMK>MfWK(P?q)}C_E|h6bWUrCN4nzBS+f3J(dE88R)RWhRrvLyT07*naR4EdK z3=d&KrLRg*3vR}^lDp-PfaI!+!VGMbcri(h^F~3GA>=VDS-+y|$#P!UX*jKt0Wn?M z2y8cwoS|dy@ZoYPXv1g}V<%s=u!OLNh$`^CiH~e3_#73Zx(oSZ3c2BhaTvQUz{Qgr zA(R+Z6u}_k(#}&emN~)R3XG-(HLCaRgL*f+* zb(fRHWcn5wkW!6Np(`UPElD$543mTsI28O6_+DiPVd}&l^P?~m#N^ypOxwzDArjmT zGt*|FQ?_uT5{Q%vAaRqMFbx5Kz54Lc|3D=FcR}K3C-~YXy{G@}ZHv=8+_|%Fn@9xM zWj6Q3dR0+jCTK;?)Zys3J=msOTib*V96QZ$!1e8XOCQy`hI+c2Su7O0BW6+0)OAzP z45GRUv8Gc^Yt{UhpP#%{LSO*cUc4hn zfu@$lFM}31_p?}00F-5J1eS7;g#oB%LuhjhpdW_7!VF`1$Fx!}(-?a&mZhCfK7yp#WEC2d)uD9tHyBI8n3|6JUsUsi&(}yuKtzI)Y>; zFTp3I8n+kEH>tAiBO#AQO>C~4SXC1ilsD1%$N`%h!L#F zAW7MXhjboQV^3Tl;T~0Zd->)SH3{ebqS$2PeyF_Y3eitaj`QQGti3on?1~znZrZwE z%e1W3&T3W6Zf|Fc0!tPl=>^()V({`n|`0Y(BAvqn~zX zdtXT;o3nlYp!jKb=d(%fd-txyf$=8?W9)_c0z(|f%h&>2%BU0o!m=?j=0oro24q*y|B%y1#S`vT2zZ1xo^q*iG1 z*q3>x85QU&@JXzw5m4fcE*3f2$Oa6gA_1#^c22cv*QD{q*<}~)U7fCG%APv0kr7OF2S*IchrzCj&$;-UwoOeJlgo^FNw|<#}BSrG}v?*+QBEaVRdA zwbi1`fM@wwATiHaC!3x`Cu4QwZhp@Hhd;)`$Y!_(r&;`HH% zy*u~*oJcmCqtVsX=;*`d6Fd5G_wnr4uU9198OOix?LGcXfE?|7ap1x3!@aB7?t=%r z?=SMMC(50D(?r5mJ9U^B8W?*(C5UU1Ow{7CX%94QdmGU0ZP?RvXgWFAq92FbW0PZU z;>3h_AYi^XaBb2ZXnXbkcgJUi<#H#`IPugTN)lfl>fB;Xwe=q8X-L-(1X%DA!E*9$ z({u@yEneKjv0*s|NtFSGS&V8d7Puvl%UKA;iZxM~2~*#~Y-;Z1(Ee z+edoYEUJUL+BnkAy2$yw8)hyUW3YIglxc4y3|`^9Knm1U4|nh?~y977|zZ)KLB5N`JqzwpIfCn$*uW|as4hotq5NGmJe+lCCM=Mln}|Nd)uWZbdh#p;^ULD1lFtLvDlSZ zZk1T66A4iwIJhy|1#WWNrA{2aN*C*hDbBg$4@h8{Nf2lKVPqyd%gb3ckd^J!Fbh`> zLdj>-(ea3D2)u8X1qdLT_o&|7D#6wUz(z&FA;7SB#(Zul89iN6{t|LT;!+n;p!1sfpC@Eu2QI2;a3Q3 z+z1SD6BmYDqr$nj+pt+&K6ZUgVAqAoO_5im2(NiF+O-qs$NK| zSzo`-ZV&ftZTsyKE$(+SY&7Wk_%ENW3AOjDJ0`Pr2RYmK2gm+z>#F>E6XST>=4BrhBiGz-`ogQw80t4Mp1-1>iQiIGN0&j5Jg1cWQD(A ztPshNaG2U1Tj~G#=cF!Ml7f_$gLo8=JzHT+cR~r9oajUh**zzMf{0ag|gV+j*2Qf=|m3n#m;uYNQ(WlPo<4) z)dtaqib^bD;nOhpZ!(hUN_eVVRcp@A&ud}*{wGd9W`j|g%Z34A+e6T10Y#q`1^Ey# zu;Q7b^x|1sX8pN)(-$qPs(rDr_$ZO1=VMj05OIVi38y1kLrExcoT8pg&z9TCIRPm~ zorO6YK@N#jw>l83xU6f=-5l=kxmzz0mGdO8bFufM1~BIrwJ_-URx-a48o|$x2IWCc z(y&T<{mT-mg{uiJ1%RdkBaC{M9Rz=kKa^H4jjRX~M}u}C3hpSMyk7JkhaQWnO6CB< zI3d(X1tt*6{Yvy>DH1Kq8l97swAx9dZz4pKg>u-Ole?W{oFu*x#I(QI&*rrr%AlvU zkpqPeF|E6+%97P){#k{4RzWl!!yIFB!~Y&Vf|c!!bp`SeDs2|DaHHkcKok zDF>yfwR5x;)8(fFF@f zoxIxF**QIJ#7*8E9lh%}OC(k;$3&}_{o(VI;ftlL@Qsi`nNLn9r}M>!S*LKBNvY*U zV5EnZ-GwCZVl!>`44nl&ONSGhvZfs6YHU%-cqnnw#Qz`!bHcc67|FA#510*lElGB~7)0bPXzL(k(#gg5(_y41A z$GzIU6Nlg0dHQtcaQ)ZgWR=tU?ZNRp*v||khlh)c0-HTOb#34Wgc&|38XfCmq0m7g zg-j!^@kj~PcE}ltvN0$XT@u zz{|@A=jTTm{Cro8)et(drP--xF$)6>aZ^m)m@i>xAmun>kAwdvrmJxA6e%Zjbo$&t^z z9XDy)4!t-Psj%VRNtP8AHFT-dbu`t}IRq0K<)B>bB^+L7rYh?_P6tfevI1QvVY=vD zl$OnOJ=I|91J`M0NuDIXk08|`Gjvr5DVN0ZbiH_Q!n}WZcCxqs;&Of-@KK~(}hHq=5{HO3yWY#0Tm$Ve7JH#_0 zJC+}3DqH;X@AW}la_E3c3n)~QIRnsIQEVd&No4CvAX_a-@@Hb}MQPCFrYe}5l4=r% za=k1Zi;BA;_8DA^96?Vk59ugfW#2i$AaYsJxX6Euut_;y7|miLes+)y_G*ynh@l3N zX|k?23lvAodAL7R>GK4D)bQsMm$JR09q&v+Uk!k4h;_a(JNhJHr&AsjMLEtyF71yj z%S0F9{o9Cszv=rcYBTom(c`kMRo;q&uBd4MI|g_89@_O2f^ZY2+gG^x}a z3^RqmYAy`Vqo z2~L9$r3A5HblxA2rFaB9t?f^cA=@@n-%rnzliJ~ob#jcoIEx@=;Iwn;D zT=&m2r#ufMo5zVou#sh*qnG>R@!RU(-P4qMfGaSWI|avrG}%Rx#bygC9stA5Ij{pq zerA6@#|9o!SyZjtRtO9P`nQbavp{n25hHoJcJyfWNg(;WM)G)L?Pq-_I} zPoHEYpVLTwy!+8%Rs9x79*NuU$SG9l*4VxJ2D$fGu@jUD3gcmHwL^DlkZLf6PKfZHZ&8{sKX`Y5m z9cWnK$;ZH>>bikdy@tN2Kws-WM|}}QK-~b7_I9Z6dX@m3KqD~DoVrbE2O7~ngiDd` zdLr4a$!H9?&zMF8+KB0$?srX9$;uX130>JS6KGPClnTd~VZ&mWyBN!3`3N@?9GeCS z*DE|cz82VFdr2v>Z-E<9T!%n9=vkWLu;3Q)0KrsHC_}?`y`bGI_pCP(sXov47K*=D z3GmpjQq$I)kUNZq=Jqd+Bch?sxDi6EMq!DNV<{9MYXLw6qhd0dj06+wS5;Gqhu(`D z(MDqX_B&a|VkqrU;z6IuikytF0!1{;Fc04eFeVhzmbyVq>#8?V$yF%UGO#qKe{p#{+&il$O2@Ukec%^~n=7DptP)rr z$b+2Z>zQx)X;Cz>2JTke;kxHqrmlKU5+jyt zR0Ryego`;G>c+%0iGYX!9M)MD5_i4HgJ{u|QJQ;-$eW2~17eR9X*EymQp}km6jNzU z1O(P1Sauh{0H#6X5l~zXmVa5EZk}X@f>G&FbKr_VIB7$$xX5UwU5zS=LbYF|^txv@ zXGEGPN>8h!_So>=#bAS`ui?1(D#E7>se(l>0DF!>o1~V@Yn> zh2yP9Rwv+EK0UL?^S~BB*OOcJ4!8F&X9F|=3SphAER1bJDcB*{yV|ZEXlOm`7`bUg zl)K)%T92E^a-afFD516|mcObd2rp`6*v-q`?d@HGN4oBGVpJ}gp0u23CI+>3Q`&;9 ztK{aVAYi<#V9HWK{BV7h;anTG-|{A95HK~7lw|AGKR*W~ZFkjEsc>)f9``o--`@bq zhc2sw{>IkP<1hM1W?O%KIBjn*-Td#at&Qr3dj}8i9&G*Awe{YY@?*8}aQZuqr1g=s zudhA&KwLKO-fb-^Qg6TYk=%~29!L)NpC6BJjLR=n+hmq&=y_6bz}?iMI&K82ym)n|nJk)G7KJ1ufd+(v zh3xPfI%1kAb2KNh!rF^EmNzH8*FdX%qi&=*yOA~wAl_oajIq)fK`3IXO+1Z>Bo+ec z6_=&2dSrOL!^AECDMlwPQ|ayYPA|^wl@$|dXk{ft$chASvMeXbVmmHC;AE*sfC#r? zflUM0WnGDP?e+x6$y^?Fawu4mCh!Ck2_P8elAa8xjMY)6^mKR93}E%#r(JnCl@%+` z7e;TXDJI!H4SZWGH6;s3aj|T6Y~Qy9eX|va1Wcf|0a{=S#lAX0L=h{gB1N54{Rtxo z`DwzLhghUE!*?ell1f4VJq*oAFp+@7IF-!<5aCQqY02tqw_EL21nvwZ(g9ENJR8~q zng=~Fs0ToLOj?Jf;-{uy5?XK9m50C(aMk*4+RggkzTH1;&$54i-c&QIs-Kp}jj3RO zup3%-j2{G~u8LrpBwB>=JnjI?jl|UHd0Adv{Jb~ZyS$j_O6soXx{)fz&WmPNCT^T~ z&=_TI4mtG$k&$p7E3)8bf$`B#)G~l(D`|Zyjrl{wSrX3^#f)9Sc6d?Pxc}})o;F0)~);NE3uyP(2V(m?3*WlT>bk$&Zf**$x~$&>Rnw@ zl5hL92!y6pT7ZX+=Z6uH-BywyAgXIfC%SUZZR>P=XTM?>Q{eb`v3WGRcquSzZ~R`4 zxe=hxq%iRM&41XtV%Er&B>W3HIcT~A`J}`{W4GKAYDq{fEKo861zHe?VB&>6E}4O0 z^GilP7}yz{2s}6$FwBDS1}`xR{B~Jb#&`#Z505iDF}n_SgLhzFa^13bxu5Wka~TUW z23u-Def3qpc}o6pATE zQXLo!K!Z*%5&>;~G5LIEHZ|6uFrlr_89;!{+UBLG^c_xkSPBJGGVC=-!9$u}R&;tW zSk)xubHEDlGC&uT)H=9`K*U6JcyYMA}FcVc4@!MBF%Y z_OE5g0EeOD77e4o;ZKYNDP^V7lFW#vjXr<+9{`ePd$#{~AbItobb~T835VF*wM9fa zn>+f})<$Qu^JeqoKlk@))%~l=?S0qQL&MkW0>!xGJ%eZpI3Q{JfgO#Pu$zR5gTz-} z7_cf%RzAZlXOqYu+td&oya>?JkrzdqkOb z#oyuK{r$ysSvb7w4|AIeU{JbjA?GF$~776}7Nc<%SO_tXvin33t_g;J6fw?l&Z zm;T8hiU4EnhO2|B%LP5EB#IRx9S(gjg7NGcKG`f*J?%aSX&(V{!lGVljg5 zoIW;2$$uly2U*iWzPzs;&O`1)UN|s_c{lCS^&wnNY(@>^=8v-5)r^qu?xZZ$CC8W} zjp}-?@5qQ@XMQ$g@^+7pXY*erM|pd9cXus=f$P&&;Q6x$cyV+y)9@?|3C%*J*cQ=p zCpVXAa~1jAPtuZSo<9UY-ms;{5?E?f3msegR}CBosY78skT*}@#BfVV$Vv!YPT{4W zWJChV*)xzl+q3=Yfy53r%g#<`dt2Yqw~4-qG||~2I_wG5`H0?p`qZqK-K31eaJ!F7 zKSHM7mfMwpq;&1vqudylg=0a7X7j4KxjDZ+T`bhz-u=^0A4a3S<2MB1S7Edbx5F^$ zH=1Sysivh!M&BR}e2n6*>SqP@CGbRC*X=m+>QON`IBRasTaVT6k8Cx#aSVTsCxmUW z9n$97mRBR4T7tQ;`|Wcx#b=f7>am{az1W9tp1Yo;8iLuwnDD|QoH&`CSjbL2Kg{Y% z(B^!(8fA{>d9lNUp(cGnH3SL(17s{oT`Q90D9+v3)Zn;52Ay14R}&`G+F5sS>mCi% zGOPQQ>$(@|p&iWPz1o*nT8=iPa|CroOXFA`lC^bP55d0p8Ii(#3#a5r13ep|NYmdCc`3 zNnQm^Bf^;Jj422IO^t+kA0qvN6Od#So*&BuC|WU z$7-&~>aX{+l8$CN%+RadxI>jcoisH!EW3v#av-hbr3OgkWr6gKUK0n5>`v`)7cLK1 zcVB)#QuB5fYLQ7@d-tcnrGTfs9%lwrlJ}O8KxkC&jjUvRI%Ne=Gq(;-Dt*+<37|Tn z`jh}nQ7fZHDEQ0%1!YJ0XM1xZvsj2gBu5LKD3 zcBzLY|DHX+54klsvgS$+E)*p>8eBEq)E$PW_Zf|73Zjg;r)TrkT+>XHA=Tn0Nr>Yn zL_~f@*YZtf`%_p6@x_3Bw@B@0aPtvd)t{$%@A&x3-Ex>1qtF4}Br%!EX;bvNZXe zrirzk9bMm3FBo9W_OKQ0RhJ%4xUT!z(9bfIYH@4}ur3TsK9N2wy0Na6U;n6ag0y4J zfUcbqAQ_WTVxn1P56+321|TnWTRd;Gk)X|75olX(zuYe^WIo2#dlRR@cc(+_*2*B{4*^xKJNZj0R)|6%X?T^m=n@E>rQa+oO; zq{d;eM;;0NLKskVthY{VTS23dU zZ?#+N(@ytLke=gFJFl^8Dq5&epfCk0lo7*_l@Wy0n{h2eDNWN52_M^4L80Tl z%`8QYrgKV%GB_`n)*3Q2*-5tAP%hlLTy+~|3G%>n-0m;o!ZGSl7FGz6Uw}ME~ zlH~?J-3S~KsA5}6u8Cvy?kpGBW%YpxDp`zA4FkmtdQomi1h9~qxO&=T_cIP2pWO5Y zIpQjm3YH~3_IN^hS!plTB~83tyb3&ocTeH?OsX7TO^^JMfhtw? zdnl8Z2l>_j0pe89S}U>~guuY@jOF~{p-fyT?&R6XrLLr4BovcPA7!qK(mcqD5Kq4@ zUxDP+UhQuWB*|OfjC8l7iY2L{x3O4_BdiDymXeOr>AZix{u6}tGV=Jzas9Zv72xsG zTJZYMlgY`V2z1hdIgC8p#|H=eO_p>OTMdLSYZqUB`*MoVqEI`D-nLq;UXD&@A8SQ1 ztXQEO^c5vd6I@I`HgfIaycg^BbaJv^++J7hDY4VJd1%x#3RwK=_a}xb z8Hp63&ukZ#+b|eH;q}LV)GKxIVWVO!{+|}h8oU@B1bEQ@0%iy%Hz zWB2#t;H43W!^K*Ad9+cO&NKsSN|P!rX@`Lb2E<@8`AxcEQkBMZ5yl)1TxLOwF;@b~ zbB=J6En6UBnS@CBdJO;i4)tAJ-c(U~TolFGroW_)HW+1I5Lh-v(Q_|}qAH4P%k~Om zXu>$}%Pcrl*TA0pHVrP{Dl1ie)e)mrb!1Y( zMya8$OqC%Mi%-l%MEUZmLP?k#e!_`B90MuPv2Baj#9Uwh+JaohnMcCD6Gv%$9{?86 z)E}Ywj}KD{wQRWUn@EzWnVAMbuuTC2l+i-dOrlI~C$B*AYOnV9-`YBmB;)tAG6#2`NgOC?CkjReE&G9{&h+y?M4V@?10Zfh0cXW7oxRU2;y&L@V{h-7CodTp=)M~YdRyYH71su-Jo9XY5yS-!|vrmG3Osg`` z7rv3WesQWr{m8;Lq#SEf(xlyPLsC6`mtheqYfdC3L5x$lEJ2*5PZ#2?R&GpS&`|(4 zSL=zceye{;ZZ*;;;u#~W(bxjwL5u0pZhw6t?(1s(`K0>K?CEJ$j3+MMdj$Pg7^T34pSt1!;Jz}1HKh+2tln*q%}?1JQc zEcnHKz4)@~zVr;yVOQ^}71h{~vLMQ!?bhI?7p9J2sv1d$2?^9FazkL-!_i1gYRX4} zs*a9kTOIpkpcGUyGS~MlXQ&fcO*kE&gONAN1;ZmHC^?sT)$nXukF7vZ zwK^sori9FJ?0N3Ji}JuBS`o*jB$M04E0Da}tNrbP1mIzhg)p=tvFxO*=Xs&ay*?)g zEqL5 z@?iV`b zO>OqlVMMstP5tdw?#Z29eU{{>!7lWWr*#el3>Ld*VW9z*k^;GfWdx)hH&6$uA2ahXxNnc zmi8zLj_U$0=l5PoZO^yeY}j`UL%R!2rzs?Xv=Nh+`Ls=`NqvjZw+An!k9Gb$HJN5d zsw!Zli{WzB6yL}|n5x(6fmih6h@83oHWu%=ok8d1!^Qvp#u{Ht``ojS#u_T zg6#u?)|bY-5orv8w=hX@&EmqhX^cbGq)wJ=Hcg0A>|1QH)q6AEz2ojb*dh-|;6k85 z*7JQnqvyx^GO1mVu`=D(bt!Qua%({(tV$iLD6_Ni%fO30Yd%Vun8>KSvxyWdGY~Td zm?aiGMWY+|KmXiUd$m{lY3%=m#7(N>u1|c`?`UR6-BmxXrbdCSOJV-7At&U6_;k-Y; z+s%}i`e_{3JT8{%{*bc8NU>rHSGyR1+061NE%HKn{OgMQ!tu(GiB(sz6Sz^yUf%Fk zT>T)B<z?;B&pHN`M&3mXgB5}1e$%K|c4U9DtEsUjofmoUYG@-R1~ z#ma(geCb)Dbd*6uHS~M6SNpk*3kg?`qYhUNRqJS)+UcmADpXZHutn$dzxLD8^l{)8PHKlh2mK&} zP1CmRgxl!)`n?z$B}zbqx?KMD_4(tc`_G%r=6W(&w$0|3U#_p$>;3M_Jl!^7V{^SG-%d%Mb zOfia389v-qc|W!Wm&1r{XLS?+ATcnwSUva#7z_*$$CI6Cf5-_$l(MwTA5}31?n-=F zxgRTp0GZA{Y*sDBM%!ZUsAYs@9$bqWL5#RNH4yU*6by|7*DMT8qaFP7>Eq=YU-iY! z)3QNCH>urjE&|C-vgZMnlmnCORs>AX=g3eJr%%#8h$CK@=!8A>MpUrx2|#<_yeE9cS)9Cnz(I zDG*vaNa69of*RB`P%khX zJqRq@af0Qty`4`+BOx8@_3G^1+uy@KZ?{dVcD$64zQu*&s5!OCXq&1k*0fwZ2pH5P ztZQ~RwLsWgpm?!4^<$e_i?ex| zR+aA*-fBwF{aU7y1#Fv%=~S}ex<)F?6|AIgQqP%Sl{r_BaSM1zZ&UE2#C^4P&c0A0 zWey~W&IO3LRa_#;a%uuZn%ijR1+(DGm6&q){9w)9DDul*zwT98@6mOFyO()g#8%=> z7eNHW)Tw$39PMBLo>!ozn}y*Pxpc)2)3BGC&Ot--klphE+k?ba}o(XU=&DZrL$H+NhhK`9+7;$wRi z7XC1UXK%(k{xt7Ic@QCy&xs@+I%Mm^xGo_m1K%s!@eO%>O<(QRetID}>InEaoJ@=` zAsyq0d)JPxt11G&e_L^1J307VJi#VEvpO;LewriQG+B-Aw^6kR#PQAk58 zauQtZpB}dTX=5K9{qgTi)665oKFUs^+O$w1!e1I{psKa%LN!xDu2?v2mO5Cvn$|Q} z!&v76Lp0Ng4B7)|dmHYjSzP*ErII`zoQssv#ceDydZoAL5qr1|SlyeBcj6oVK|t2m zZNu@ZF%66k-Fy$pUYs{fT}DtbK&X()#W+Oy?XMqK=Mj&C_w&-pQW+0}kVQ_wL+~yO z{SfR|BD3tUiFN;Gi51gM!jq-mO!WWS$@;Crq|zNe4Qz>V!HR>`JdTUizuCKb*T%Ih z{uiiO5cd{0dv9ZN=jt1MVar(HMgkXpAcDd5z$+(UQf)1}FgV#X0*h%3h7=-Gye6p7 zi&-!eG9l%VNtbPg*;mXgykeW(J=dAH?4K}&50*cWk*{QQe&=^S&#lPPxzy31aXGi- za4tob84_Fq-7%~+;Y!|cCOg(+uFG*(di~h>@LJa9aXF&i=|+9fu1F>?V-@AU+-`1w z2g=~T94z&8y}6a$OZHvp&x^&P){(uQ6-Z}FI#xqBMzI?vzNbIDR^niG0av14TVz+l z=_8L1vpDK@-W$P$nr=$jWW6O#wJ#i!d1lm*^)5yw9bDMr7>zkFHDamF^;roxql9Xo zyUb!(x5shzT46mazwD(E2V$hI*NiKc&6unpMGjI_#kcvYUffUZ!F^;ow#|G6ojDlk zR-vde)fm19e^|NwNDsH^K@Rf60}1dENeh68Xz~RO;I#^cJNJ)yjZ>%^oBuUGfhJiG$P1kiP0a0KMe+HM$z%*$$ysguPSG0(rLd+qQOsIwo*-Y zQg)EuG5oNPg@zSZO%(%Gj3~M>dyN9~;B4tt3!KiK28IuLr6)^$Uc_kN{D^#91>!${I8_ z&=Y_c8BZb26Uook`b4pu&TtTfA)uV1;Pd+N%jL!DYCAtYn;TVG6}!EgaR*FrBZr;* zF}`pGAanKYYV$`TGhmEtkF~g$iCS`jO+o2xLxJM>fjf35LHd5sWeSfK3kb&7@So=_ zh$lvO99tgpWTjDeIGy$iol`odM52t|Y%cS08ua1_7rG3?q-mg+GlU~+c9SJRuh;da zpN7MPtCK8?tT0K@$cu(?%2#j0`v+8sxNL^T2Fd3DzkB1n^ zK@Rfc14+9jp`^7>0zNuIU@*6YWFAKZKthOZ{f~2%7FytF$-_KxqQp*J8pWO$0vU-E z8Eg>W1#~s&7ENkuVhT9M?)l%Re?DEt9vZtN%DpaT>s5>NzJ5yE?e1}6X3U&2KrhV% z!qQU(MrGoz4J2rPV7SN2u5E%f%(-ITFA{ zb7mRq+5PR)-ih^eTh9Q3h}kpyelT@3+Os*Kk!6>fU2?;uIx#6x%h|;fb4E@wc&y<9 zjor}t(#V9dx&V$5IVcSWHs(o;V?qY&v6840WzCsU6@*}=#=51*QW?i$05Y?yUVrl& z4wWddVfk<-C(mEL!Fo3H5(oGwNLD#xhOX{R*?FJ>b_574krJStR?Efut|+9hQ4ZAF zHqEzv7W2LnWTIOt9Dwych%G?lVUgXl@ zG@JJNY07k1U*LMNL4W(YX?$P0!^i@7Vlb7St>SKE7${$@C9$#;^?a>xM~M`FFdj}7 zm1XX*pRt?Q(|TqV53Z`d^z%PMSasAwR~&?tHBxQ8EM^~#!)lFE}fd@l!jtB z1uI&PW{JG;O>oVjE3K%waKB8Z*#JgsT01o)9{$D26<2{fuDkfUr4FfpE z33t@QH7S)V1oWGO0yibk(d6TFyS?oCsjK+T&ABdXI+Xi5QhUA6wG~Cuv9+I0!XwoJ z5G>>bKw@BS+b&X`toeG+2&QNW%$9*Txlu! z&6XHTA$D>JrrA{HORNLlVZty@u6=*h1B~VV_zb7xY4{N6>9~s) zY?6)&Jo`90+@=RP$PW)BqTOomD?DV-U5LG->&3;@78>kp=FL{fv|ORhWn@{B2T;h; z^hMS{V?pd`!b?+5yB(3K-3%Y6gN(#OkC5YwV*NJ<(^uk^8Kp*dqGdndv`MSAt=cB< z@`0`~U<72U4?K~k-2pI#(g3ZGKT z_vSDEO5!R!omY4B(?U9V=3CH@+s)?c#nJQswS?dMp?^o4&(F`lJpX$CcH87dbG3YW zzI}WdkPa}^fK_j30(3F!j)5=rM`5BNlgbtVLR$)&#u*Xwr75q;UQ7*s{KEP#d*|=s zIFiNjzd)g=3S>&bS!kqENk!=gl7$6IBv7CRVFZc?Hk=00e?r@VId`uU!-`1^4ZUD? zf=L<&zBU8K%`kL$%xu<^3^NnV9_+zz$yKTExcw9Mpq~v+Y;4*1ect!ISMPb0j@;nq zFE~ybzjn9tkqW`~sKaa`xq!=&SZU)ju-tpkhl&jC4?bM41ygpSYSOm}iyRk!5!@)Z z=P$VoSuS6Fo}F>z)o0fj6ZY1;mFXA)@YhZz!mXtNE6l9&yK#e}fAm^w3d*+kbz zH1(N`f>w%DD`6jwG&kjK8=h|3sEB6CpD6BW*Z1zm?$!IZ!IsCv6hOBKjbl=y|~s<h^S@*dbVLk&FkV{R-NdqLgA2B(f>Q9Q~$)4>00VL)^kYr#C zh68~U-2hPMI)S4^hZY$Q?+$0rGyiDBI{Ux`Nj$MkmxII-&-$MsQ7A!NX8?q0+%VZv zP5;aGeEpZBXAVwkC5kkgf$WwL#k)VNxNkOfs=38P6O~+*%MMdV*qARmS}`O3As#hM zO^jS=RYQ&~!`Mu*x4+JvGD{n;v71WF>>4tvh?@mv4L$i=2MHy1M#JOhkXID=e>X?haF8SkN#@Rrq<(8Vw23kt9L0 zFR+L!l@Td2FkwL+7-VD8wQ6Akb*^pJ-R4Dt{hG2IU!QZA&R@6`4O#H@RtJ)VAysO{ z(qX^?3Ae4 zcNq=_`|ATz75=v0P$ZIyyM<1~VM0MQF2YQ%NfxA+eSKAk@DE}l86DrM0Wm*z?#ETD z_@{SQ%fo*51~WB!@qju-b&#T@aJ9y(%RC1K%Ex|&!>lS%aV(p9k_RK;Q0xbO2Cd`A z)kPQOf-U+SDt5$)xLVP|vb@z&68%_MjQ@Qcf(dXxgP6)TwlTil()ZiT%Tn~&wJBGZ zVzE^)x(~3p_|gHO?3C%l6G)!y$^QHx84NVj;9{gNwR3zxLzdOPy~*eo%#uTQzt!g( zR(NA(_}wDR>ts|iy@aHQTcl9t8VFGdoBu797$O^D$&jk&zf8WJ{TMz|xzn>8Y#W2T|Zog!zIuIPq|pf&FH(dhDd8Z_SH zDiSAWL)*uo%F7hLhb2HCO zv!et;?x!g@0Qus*r>YXI)xUkLBG)U~^%R$E>~D`T7DXCpiA^d{;&<5f6Xc<_PS&To zZnvwQue+hHFzM~<_rG2*m)D=qSMT^YP=qP>^K}S3OD3WXVlXg=)o}OQR&w;k`2O2C zqIF1Tn-GMq%`dxZnoRX&94+J)>y9EoB2r$xa@y=o-2}5v-XHFMx!CPzi)W6e{!k3I z7oW(+BE$iRf_E00b$vohc|1wUzX_!isV)1h0Ke>5rE8<{$Mvfzpnh!~Jf!+nY1d1?SMtV&QL(C^QEOz_%`E0LVr3EqQMz0$4&8cSYS4KmpP?q{t}(RWaK8cEEAyMF z5lh3#36DZgiX5RxJ)1XXf4;+5EJ~9|rs*2#SR&qP?<)1n2if-Og!_2gmz^p&o{o0E z{8JM>z6-5y956MvpT)AU5Kv{!3l?!m)xd>8q9mTjNfKt6KR$`uL@j;PR7&N~S+V{y z{_9Wwx=$G4k}QlS$SG|OQxjNJkrkpA$fXgTpxDn#h;`Gj#787E#6`(}Y;hoq-R~_6 zB9=?it0|WxlK1Nl+hC->d#ToJEbGMx!?MX*-S^vj=7|Z6b_ctus7Q5NU7^#5yQ`lr zUfsNZqrKS=er)!a_Rt5Q^-F#Dn1wW~COVlwU;BDkFtOq{6%P*VUlp@ zq#z#=e+9W8Z`E{M$;Bk6^*Bb}`jh}~^>B5~A+j3kZj}wp0ijSk>~C~d05y-|P~S>* z_aSlW$dZeaBA}qhVhJZo*FQWKPP#*P^9{Nn^*kMR95AY1oGLs3q50)Szo%H0Rk|Jl zdn7fL>t_B4=dP-@Nqzg;K7BVG6|O2{PF##H?CDLv*zA=UoN(38&Aj{Ls@*4hvM2kq zf@C;Yh@*!CQ5`XO)CmF5cz|sV2D-(=@WKAD=w7ZC3u4WXVVPvc!*}jP`hMEJc#U*u z8gdhitlCAA)dWuHDGe#o#IpYJcXf8C+)Vxff!ZTB3$HaDm}GN?gSeJ8?3PdsF<^k& zhGIad8uN|-vnJNR*}HxhxsfdV7bp~^< zW}xZll1pYf80Z;K(yW_J1^{P3n7<6O(0GD{JJUh$3=Pc;_TbRCH#k@)gYCdx@WN$R z)pNVq!QEvGQw<8bkvQJ`%pi3Z{i z&ZqUlj~dSy;jYy!4k?n5p&6^mU3Na62+y7H?hq0Bq1(uK?3DqPLJ&Bn-^ST#k{(}% zgNwHXVVW!y%|d|2H^a&uqEVKm_Agvp(~^EH-Mn=JKPzt*ic)%Aw%F&(b&c8F8pnz22CN$7Ew{3%?`CLr z(D~J7@s1n!C(quECSoc;y4kB!ef4vpsi-I7SD0g*DgzSy`9V08d&c^DJ;!WW&O*IMaYW*!Y3`@w1I_f6I)l`F&Q=Jd^?u$gEOY`dGn zH>HWALzXR7+ueUOOqaxg1L4$vE4iJV&wlL<)OMUaOocZR) zQ)p!kH(jRD*l!rO3nJbM>DC4HE3Q~nD1a^T9jdP`!e%t`L5q!0$18)*&zdX@p}u}p z6r7I`$I>7*Fy*Hd$AFmS8y}NU#*dt|>|%~s8n@GwL)&}_EqlJUC5d^OUH7HLE3&J- z^85Am(#R!a8gwKqx8)&y$M8q95Y}E)k*v~_M5pLZPax=*NevBA$;Mb^IzO$>p>%W! zt)qV~HDF;=#dD699!6O83#cR%mNFr~g@Z&qLa3oIP^hv^NfeO?Svq|mzZe`HOHLyV zouvQlDq*IVEl#DrlNH}DP$c|VDzHt&xemAZQ;{@+6uG~@5IWlreX(*2KTb9FuzZY)zIwZO4(YC-s` zYHM382H34-`HH5Mrkk3`e?MKG$VlS&Y8P#Q80a5|@vvmEJ=^Wa007siS|_}+wPINN zL$|1FH_O||_0MkHtDEC~o0v$JqgS^VM(}j>$PDlKOB+XbBN3RBVq@u-;Qf-;5?I?(OVo`b;045h-bNfB%>RjbYpg#0;YZMxU>oV@x_S zN;OVSFp9U{Eer+12uPbRIo_!2+s11$r`9CkF?KjTVD@^v9_yMu%A?LL_(e2k$)ao$ z=7npuo?HyO2X5%JgRURavM?i;&{VZEE3XV$GB77G+*XhFnR@81%y~QiqBIJftLAKGe(?+tiNpXHTEMdwG3*d;8|Q#SaIgW8D~uS$Huc zx|$DIYy#cJ+hiX=WKCDe`A_Rh2w;%CFF4^yYhrC?gJG9OZjq#w<$EleOfuRvq$Y&X z+2ZB-(fQ?QV$nV`G$Sk;ez=|9j$sf)yhm?W`xi5Jc#JB(`)nDx@Kwb$4=O;BEGWvh zK+eYmsEq$D2Nv$G_5S!|hTIzE9ChYO4yJpKsv@Sj@$Oy0f6?Dp`10HT@XKGm-24A_ zydU@De*9OA#MoA8G`8LoVnA#`-Lklu7d280tWN&Q&GckFp5rk$oY=DXa-CqRDnh2! zIGvRRbq1-Uc7Z%d)hNzd%T2rngm#46_b<&uvOM*7YJk(Os-|ZR1pvdzh;t1!q1aa1 zu%uo^alZ=#P+_bAf&oKR7M`vNymOy%3a|wdczNzeL7&A^%tRmZ>46N+F4t=Tf2%tk zGr^5uCBnBQ`Z9VDM|~JZs7SS}Qq$T?C0f_iYT7~24WeMcK>);qD=B*a^uxC{XBvtc zoaRGBWWeaQzac95KyR5_{r7WoNF(`3MgO0Mqy^2_}1tMPY#d;R+Qh)AJuc-`?K-v|e5wjAxwx8G)3B##f`W#d3+6NFAqsIKvg8P zTjy?OR;+c4jy19SFZQk`v~4X5LpNDzDI~+yToEE6>q=Kv^kZxZ3%rp)3mJrQ@M;!? zmd8K>#;eX^Hns}}YiNt+1{6V| zAs1PKh*jk7U<&n{k3DrHrkxfI_28pGco>h(k2oT#m=rr=; z;gt^MG&Z86?l~nqKB`P}o@>`SSY?BztuDs!Vp{M;D_2#OV%Ot28~2A3^DL6QVzw2W<$ zWr=e%w=9Phy&0I57O1_Fq|Yxte%wQ48mL=pz@QYNdNodcq}JmYKpr4oWHGB5QNq?h z(`mNeL|nB)z6V7^N3ZP4=biI*RCXCUb+>=xIZWy8gfsa{V7I3|9RO2T7)4_k+WEaprI8GXg zX{900Ls7}6;`Q!W&+%9nAv2!w5Kr=N%eF+_ub0!vwzP2opk7b>5Ne(KH@9!6p2CzY zt>JKEju}O+?*o%F_3ed_4V_Uuq7TuvZL8uMo*R}T&$dr4oSnCy%AbEeIq8D7{;+Yw ztUul@$_r%$y@0bqp(s4x`VNq6UF>`x7ruiB&*0-r&9M%d)=%^PQT<7KRU-aE!(a)7*%!XDOg^7=HfEM;VOB}x?h0_>x7JC8K#xD z2%vnda+=D|Z&*=dhl@eG#6R_kx0;Xp_aAU#*S6%SOmM>Feao@Bsa*=Va!qT7S>%em zgXt7N#A><;B}wE7B_e=ACG%+{uR9=EwL6>66dEfP=pr9lV$gG66U(Uaiz*L6o>G69g;^ zcKx;!hJY0a(2A&Z{TYYbW5(8UEdzh?-{wf}b=TIqTWf3Y`dfo%YmYWBcD8!&2xJWI zT~Ivub@}x0`GU5bZa!OtwidgV%gZfmr@gJg>3=W$>-~FM{dX&;u(Fl?4j_SoqU!=L zXM_>xJP$Z3C$DgbR~F?pt|q>% z1B)}$x>eO&(JjS>NeD{sxY*6u!BLiq5kj@=6NL*!PnE?1B90kiEE8Vi*iNe2gNX`%XI@Oipq;H8t9^S{*Qb(kz15&#UO=c?Ew=%tnFVFDOP5z6 zTk%!ii7FmVhDq`;D~X&Y*tIu$k>Yz1?Q??huQ!6cJz^9F4s@Hly-n-<;TX0F-0O{~ znd;&Iewc+J-U3n1f6?neDx-wUYR7VovMM?IheEzk{5-|llqZ7z=LcF;Ck zQBbb7(@OcM9a>d82;7sqn~U>=53O5DepSx-aBxToW6wP8GD0um`{5>G~ z%k$paqkG-U{^q+!o0~g_o8;}*)@6_2!T*+fmyf;ylCQhy^84kMwbR4P0onQW!e^WP z%ijByQ&`!`eou}>T25^z{#uY|l0;IW%qs!}v=v$5dA-}4&Sk7l=YvJ%5?+*74XExf zTz0qt0tFGa65a7E3v)?ot2`=OW`b)v-`E`m*Nvoi9r#=kc1GvqY`;jdPl*RsEe)x% zRdu3HO;t3!5MP0Cj1ea)m=jgv1T!G&m{$Q(2#_ERjOHBI3W6%Tm`yV3J45d6RL^`uo58QIYPiuZ$alGcI z+|j)m!qW+&KPxJfp-D!b=r+2rox zept3tuEN5y-&ddBtM7%33@xy;makNx21O9Gn+4Xk%Thx&@gy6%UWpx=wXS->EFhw5 zOXG#Z*vZc8Y+nn@Zq#dK|9;bwq2ZC%dv{=_e%%n zBz8Q}ON+)Bx}nX5QXnP{CLB;0eDIr4>qQj=Y2gsqrPloAU3XSqPTr6BDv`VWt+Qb*+=6jb?leFar?LmBf|j`f$2@ zmtI{_zt2k~?!Vp_w7)%-G+z+_NPm9XZ%RZ9{7_2$MzaW-f;J2_McLV1%?(>Z8Uv7? zSP82#hY?F9wklVAW2DQKs;*Y_YK3RlnT#-2<*Bg}&C99KiVX8gYa3R-|Io$eEbFL& zO*6A@nda)(HspF`I?teBWHZ;5MyneiISiz2V}6mxg&EfCy&krXmQd>MtgaZ3L8FWU|j$@6SQS%|#M7hjS{H#x!J?B(1!*%f1@N0cFZXW8qf1W7hXgq^qj ze&F@#;(v9~m8nBC?q=M$(qQ+LNJVI?&KnVzTDS z{V!uAfA|z5;SrDHbr}7@liJg*t=-xhFaFH%;8XM9w?MM#_2i3@Kiuap&p!^_I=)vM zJh_`)-r1e~SN2;V!Gx>)T2MEbcXS|Mj;MC$?+!n!)S8Rsdmh$RS_ z&@8WlmR)azPTO&)Uueh*Xow{t11=_rXc=@Z%f&7nKQr*(|9CK6U6`J#U7d82R;cGR zXj}te+dzVh(D=k4nqLG`s}o4bkr1*(O>)+vBvXq_TWLGgJ&#t5IEa0M4lbb;-A z*+863w8OFQo?L2;{hR=!ZD5OT)5cJmOxjdQ%9vqALHwXCQb%_T$q;SBz@82io~NcW zw#8iKXyF6J-#Z_TR|`f&coxC5M?uo-Lr5bCBVI2B?#mCEOdo`{iLQ z#C|96ebcN|6MtLl+q&256zxR6)+?nZDV}JU6-JdRa2#Sk_F;+Gu|m<}_|cTmo`%a(F%-1J%6F*z`hGVLvvF$D;mt&j@hYDA_mE zmu0NBj*isd1IcLX1#|w-8K>B~P3&b5!4Ef#OpE z#?uldbq|JGqwr9-5k%d3B%~5;_etfZQ8P8IS3lP$p2NfI_ml)qslv6{)jP*|IFjvRDrWQKfBVnClZmBm&4_1mp3#Ajp!ZOD)EG zjTyZh(v(;A<&MD*aO}(>w5AtBpbNphwIv6)#$ZyKkYj1e zGLY;cq&>6?U0C+A@43_2OXo7DIru~V!$SfKneX@gzW06K?}#{j2!bRM_s3%zXT((i z74hh;=awjMy0x~9q6d>Dlzj$)z!BC$+5TGy`3Rd0qg(x$G+d}_x7XhM_-O743 zK~fE;je-6xAi3M=+`W6_=EjSifBXg!zwMFyfBAQSVx#t%=wUikN|7dtB; zS=q||5Rmkt2zgB7NrOg@P?Rl}`(Y9#OX+sC8LuZD70sqVhT3!i+;-J6tfjceA)5hg zU`r`*5MWpcIo`pf*{}f20wY#9U(V8s&EL9ixg=;6y00|}btjg_hGGF^jrL{eYAG8?#X&R01s)h?{Vg z50k^=!Qx{OQRcz1q}sBJ#Y0wJ0k~YjMjV9lGSju3f=CbouxA=w!+`K`Y7tyJA8&qa z7J4BG?RKH$z1y&vew@H^R>0ukzHoBCC?I)QTw9aZ z)S|w&M&FCWXC_}18eCk7{K)ksb3T++kq_$-HTnygl8D*Cd8&wJJi-fJ z=Ckg|29OtJY;&?`b4136gouo(cI6VrFqEPH;b0_>cA0FugCQJE@yM;TMW!h19bJ`! zfTDEfojFdUniDR{-SqPM;k_uD z`)>gWMTZSmo^^BQm#+}zdc zY-N80Nak$(29|*&sx13WT{bY0LA?hFZ`q=zM(h4QjnZE&Wo$Q@M);@apWnT@dVBif z#p~lrISO!)r_{f&X;W-4iox2@4Z|@NVqw;JR^8KvH@_cJH0nrtchYF30IIE4EFT3e zO>=CdX+X9B0Zu{Y+?bdq7u8#_g+wOX&Nd{F80Yy?#}EPfnjSc&AGK=*H)nk$0Q3se zLFSE5_X%C8H8o8uQO*O^{;1CzDmDxeNfy9M)fEkEnn7U(%Z6==u!));3L+eDAe58V z{$A&-wsRyK$(N~-o?&^iRdSkHSi&~tL&}#516B#V{GbP=Bb1czru#8$M`_IOz8$#k zal4UI-sB`3$CbTp2$E`)hQvGAhAg#Zn*~y6Hj>w_-}F4bceQW^r?~<|S5Q?;mDY5< zSQLsFADSXs<8Yq>3|&yoBC%vuAf-?*1>;A4YyGu=O1jwZHysTtYVcF5tRF)+bz7+m zbp8DCZiPZ=!AER5CtER&?rHoBprUcF8rjrq0e|T14SxTUs?>Hlo=V73T7*F4s z%Z_%_+*dfT)U<+NdvmD_vlNCoIXSt$zIuDLm`;D*em#00%qF`+U)SkF;&n}?&ro{y zO#kLTcV#PE*`EnWhT1ZVZqP&O_ZqA}(JdeY^KzDy2U{0R>S=HH<&%R~x2ON!oNjMT zk1pO`Uq3(i^!O!fUwHWN$>jO{x+zVmq;OQhu%_{Y*N*ZO()s{KgF_Tgf+75u*M?I37p+an4^(xOsUgt zR6KO{c!VuTpt%2t!SE*3_Q$qYNN!EqaXqZBr_tiD`-!70r?07&ruO$s9`(nz5__SgdA8wb zjLWdfs1B=-s(ME0Rtfk`*AQjVQbw4%ZlsvTVuv$<*=q(-P`xw@HpiVV{>R?Aytb|7 zaeO%|!-c7=qgLuUVP(~ zbk^e`Fo4V&78zDef>8M^P8o?c`9={48s-Nt2v2&;x`;Hdz9dFk zmfU+BSGA^blC;16Zc!;0tTnSP%TcUI%Nob=!y(`DTp1)+8pliIy^FzOZbgpVKN@8L zWOZ{ig;-^AA5j28C|0R05@M3Sjc7>tjRqu!ZGx_4uwOnOLhW+OIv5n}Xo|yfkF7ci zFwUlDh(x4flf)uM5Ym)>PRQEc_|HE;V{PrPmLL|67%QdV%K=h2mU#l6@2qdVe*Jph z@AsbcfBt#);syEKgXG@s?b`>*uq_hue#HmuIhzc8^}2eY%<4K9=hB#|;X8 z|HImMdt2VhA2t>{0Z5G`vU`#l`w!I-Si<1VL81fR8&xaXVH5K~}Z!ZnrOq5r~5yKr^is1|f!*RV8g14&_Qu zn9ybT`!dGqi60ccts_JYCUox9?Dpc2T)VyKlWPscYy9uy>T`kBINczwKScIWY4d#C z#)*k@PeYdCCbr*E%>uj*5EWDZtzf28nlFS+r%-$8*az0wLegbDZ?ykp|(Pa%37J|`A$(d z(SYHpRu(y)w#sl7R03(lNqiwzOySpG?2gGZuhD5H#-XMm1QagxbHkM>S`&ISMGJ`< zt>!P$ko@1ngL6}7S?GG3X&eLi#>f&9b(je(iYC3BIRq?*NNoaxh5)i6>MzD<^>05$ zph;8B(XFUwB8`AS0Z}Bc$x2&Rj;O=Hu2CX&h*a9xIQsha=>rC;gCUhaD+dalM}_im ztsXyFdH!WlMzM>sdenIEC$*i`0Q<*#klfq7efuEsh1PhsxcF3MuzorD?c)90G0kZ8 zy5Rn$PC-4g>&){XzIzlNKH~qqF?)F|JPk@qFO?diRU3kMHDoC-mXTRztAQ!=rtNx| zlO0!*Wgtl*AxnSOf6>1@J2_oS5iXUYuCFg{e!W@jY>!7!BM?$DL9;^(Xi5|}Lq8dz zs-7)sq729nMag(5_eSIl#|l*X2Wq9afx7K5+%!WgZmYk{sAf~Vp6)qxLFXaQyCT4X zG3fFf5_qa=M&pM66T4@{CJPm(c-XL{Y!$5q1IOJ zHm_9rDoZ&q1s^L8NSEL5VH#kY-_c4MER$gge5iYE7 zDy(T;Np1?U?!w!vG8Xnm3>65}2eSYrqe)Coo0`4Pv|@(Kl@{~k-IKUf=k-el?;h2}Ir}5lB#k zoDkD#nr6HZ>b~p`=4@bRzzdxHTmbtKv ze#a}Y8+op*T9m86yWG~D_YZ+0W0l-QBYynoSQKH^3V;_M+m>f0$S_DmLagTAEC5Br z!J=f-yloPvnC-ezDGnSGeFS!d{jQO+rbmY<9mxbtV7cmwuY|suvMK^&p(-MP3V$%2 z?@RJ*p;hw09X$r@a>{avAY!iTW}Qx2#7;=)gvfnFdF0p5Ej4NE>NJ#EE)H>~-VKf1mADYU8IHnqlTXgXoIvXc5> zg_aw5%r#aT4Br^;E2@BvhD254BD~aKw4eG+Lz~YTMwYoAHaLg&B*seP&f0u`U*@3c zx!T9~Hwzh7dSV>SVXgEyE($DRUP)NWW`5O;{Ypum>-CgQ>_mF$vA^t43jx3p5X&<`Z&iv)d;K^QF!0YfI( zAZX}n1n+xaz4v|3AU0+YsQ+KhEC3iyY==Q!fXMZSNlMHYM`#G}Y~E0o!mX%*s4?98 z7BCb-U@=E^)xn6Ob0h+0``JHOtf~qf|EbfFd2F_XRkj6)xq5fy?!V>{yDGDfskyq+ zF||Lp$`$_>NN(dcK6@bf`C^Y#8Znt7uv?lWupz_VWA9jOd8fU>ZF&Zk2;h+*oS%1u zZT8~f#DRvqv}juD73vh_^L3KtWi4~C6PZ#(t3Zo}feN6Cc;p7_Y8mn}I_!Zyf4-Qx z$%j`zysPo+^~JNxFHRqQYm$DDX%5jHECZ@Bb-s3{^#7*9&E3{=glgw>xl~G{^x_v$ zXRHYXQ91AX12^@`#Z>Nk@{bFERRd{;=*TK`ik$_pvWE zn!j)~=%%HbR@0Y2?Wr|4`sU5mM2DlrB@B@})QiYyT#g%SW3>dsqVjCdC@Rg+poUea zZ$*^3&eK6y=nIR86wq$)@QjHa-A@ODs9C}lot*9Iqi)o9!Wd)x-JW5KmSHe(gwZ7T zdu4RI_iEmY#iQ>}6*+MwP878PV^vu&Kv(_xCp#(eC)9I#>L^S@)b6A}2XL{&Dctj` zrG#YX@Kbx_la%D}@Q~wpp3~vWH&^@32z1OFZeNC(DrHSaa0F;C(fvTNfF~*!;pbG&HFXzZ_iGS?umnGMs*Za!gy6+*;cI9^y98@ z_xwSteq0pPq{c?CsRnZQRD^4d(kn)d-yA@5^& z$Yp8z>ZgDI_vFR?i$|Bwcg7vI_ZTa!KlaU~A&P8bD2C8Y9d~m;wOO6J&8EE#H5v_! zfkw$rWA?Qq22Y+sy>RV*m4@?AtCRHCmo|g#{Nz%LBn`{;*P5BE=@pPQ#PgylV<^|@ zU#)?r@YO7UDIKMQV8pD&xd;0Gyzl*A8opT11&GIWc}u8&l-mPsOgfpQ6B&F?jdTlHXstMQE~Hr z$ix((8b&M&9^2Hyvt7Jhr+($lB}Wvl%~O*aW$q57uvlM{Jh`}+^p9n&f_~I4(*VJB zqaV5-P9?>Ea=>vO$4z?-i5ndGgeT2r7LJo7>GC_}>{3Eu*k4bxu7w<40}^qs4VNF} zsyIOyi9O33vWn3~%w$f26Fn;en)n2iy|@Q^VBi~uRUsxK zBwZZ!P5YNQHUPeM2bRT9J#|B5IJgE9tC6_0#_B$4A^+;@>-@ZMW@> zUDI{9Hu#m;yX&6zA7G{3Vmm^owW6+=ZQ{t_7D#U6Ha>eG8G9>*bN>zth8u!aB4~`; zm=$0RC_;@MEH4E2Sv<|f;Qq~Dz3n7j?49ncsa7clILfI%(8N)9z~r%n#SKG=iO1s- z^SnZc(v-qY|6MVrKP+kv@?!tw>&s_5S8{7T|2tL4m+Y6UNOm4v%2bx4s&X_#*9KYM zXhoKHTNJ8#7pY_uHJqQPW!gwL~{@^ez#w}yKpRB6!(6QJWWgz z6&MwYWBDpDm|+gFA@R*7$f+S3MARrIA~IxEl^QP;sEecU!VOFk*je%PIs;Hq7Z+xI z9H1G;dckC=WhOa9I;Axgz*(k{7OxV7$}fYPDCG{agEcX>vCUer|0 zN^1<2_(uFf96Izvm9HrzKOISh7&6w*L)}DTp=J9&g*^+}=98QF2_n|TDZ$OymR@5E z(IFt1=0(4Rc}kDOTrV?X7hmlih+saia$|OcuyO2CXNDTfd1v1P%9A~;`u|4?dJ;jor z)>z`{zl8o_Nyy-f1U}dxjEk2-p*y7Xk`BR#vS12Bnt_0vICDc$b(TkvrK5(D#FYr|1Q2< zw~CJ)5hDEA0N9B}w(B^e=(yIn{pMtz5Yo8QxvK#s>zXaL*t367)s>~T-j|stOSU>t#DG%8(;L>W+Vj) z=MG@RD-%t)N7<53-r((-EI5@^16FGEttc*%WWi_FCGC0um*@ujY zI;nWEYc2X&@u)M1auaF}*yA%6eqEb>(X2O&9huZ>Y*T2}(B1r7@+WuTW?m0! zhEQ)&a);jUZ$NTWH}#tb37)R4HCa|sS}a$uGzgU_gPp}9aA<54|J6ndZao2@N(pKkpVma^=i1CE{7Le zrP=mIVdY}K-?K!KvB1trY>7Hd++cs(G)*ACTP-zG&0Krc9J@MMpgeSvP7r%8llr4! zsHvl#Ir5!MFV=l8($ggD@R@rW`m#<$jd^i=zB3HE#p%j6MWQ3kD~W~)wt2swq(N%5 zGvoRoK=nzPHjKya!MqzU&rFS&j;V{~=_kh0lf>-@o4)H?jy~H$7SAIR=bL%XeEOx= zHF)Gz(YrCBHx^sQi?cK&!uiVIQ*qMNMSd6z*Mr;)^E8z^XcH%834p688o^CPh)o5q zh)nr1DWwW1xBl(5#Z`Op=cPj8UX&(3Bj)f-qv}M8^RZ;C4^-%=2m7ka-dt+o*H4=m#QC5fY3sKZ<&R8xS11B*TSU1h^uM@#Z8m&C#p5 zVa-!F>5G6b`j;+(33&P(jHo7I2$}Eb#IooQ0p?(K)OfOLAXb=u!PdkEn${XMSzyi9 z+TEik{6nTe*DwuIuQklNwAR|IX*VFbshj%EgG8EY^)>dIAk7BlN&8E_=t+Y1mCrW+ z*kwm?vn5O)*XxK?j*d3idj0guEO$i(NRn zB5Mam^dzTPB( z#01dB8ooYkR+h4AB)J)9OxNj-i#Fr5#l$r(uHwRlLB2>I9iE@tog{p>+Nur!@DO=6 zpQjEE3&%_8y4nySQGt`@9@QNwpR0JXT$bhXO#2+qVlK~KI*jjmJ%^Sb_eb&JDS^7{ z#$LbEC)V!IVeSBu_)ZkeK>(TE-QdS3)vgBj`fQIUq~9g#I7*G&Z)caAKq6|vAZ21G znc#@&IwMJ`?{!ZGvaDH?b43X`Wg<(Q@~cgfTYDT=5$55Y`rB2e1&oXc)j*Ig@}UDu z(Pi2l%hX_X3fS&^>wC6g5=naWUF8~&jT7D=3&Yx4U<|J12*McJ05e?+FdnjSM!G(> zaN6_U{PYfp`irM-H0O20SQw;m(45bW@zy?omM10&oOhrT<{%%1sXMzuE#39p_#%YcyCau??zJ=ogseE^VECc73=(ar12Dr zPus2qT~!tomA0oqS^kqCFR2DO->Y>Q9PnwxZTcq54rA}2cK-q2Xzd-bS`*!M2q)B7 zPy>5|!IxKuv1sNhuxy+%VdcnPIks0ZB#sex)Fdh5>L0FnxuxL|?QEe+dROm7%M$Y_ z`{-3u?mB>vRs>?ll;?+Ay=Z9nHPy^KBb@k=UdUdd0-LHR6YvrP0V%`^Z{l^pAM|^t>s`zsRUq7N!hoCClYKJ&> ze6Y)3ZAfy)xj(EHlF)4N6aiG%2^|%jYlIOnC;QDiXfyS?SPB!KD+i!=5)np(@7{4j zq^1wnM~Tb190&N=CFs1QVBv&BdH7B9Z?lq*^i3CCdprw*C`yBWuy-z@ZEI;9hulS1 zhQjFOMDo#iETOk8>xC^LK{hscp@1g9Lj~{HW;R4D1i{WVDKx8keF@^ffP)# z2;7t`lA3^NX+xI7G?YTK2{(5!chP}a49tc*4zF>s=cK4k3fwNPUn%zNz9Kutt^vK@gmD z^q~eU@=kxUq>}+JmH`H_&<7kGMOA5 zv@|xh^mX~e|7zxQz?Ob5kQ~&e*GJd&^KD7^+xX5wg1Ry?9?s72rTe~6x*K5#3S$W@ zO7SR0N;EZr#rbl5-kBhO`03?h44c4)zov4%UfE~UD4*tex_feUvA5f4t{!jA9<~Y2 zor$3{nNZv7J)Lrxi-wd4PIg*n^ zK*+A*WO6OHV7LhxW;R-B1dk~C1EJB#6ylaBg2A?`=_T8!A0?oa@bpBw)TsrwSxoKe zSv~EMY{FKGMJ-h+prvf6;JIdAxAV)6ZD@X$U^5!)Gk}CL3Z+%4HhiljNfx_(vEQqS zKnaRA(zV87OVTF|(^FdsH554p5WVE%tv&pjb{)NnH1ge`A1vWrG~7r zsqrZ!m!ic?5zR8nH+t?ta;*BFttVsuzrT+_QaxB%IjDY}=)Y{{OI7mwfaJ$y^<;W_ z1p;gA79_Xvor5HozrTi$&y_J0!%8|92@?c{VedPG7zKe_t2v)}2gJ{i<1Jw=@37D7l=AxnRnTQ=rR;Fi2RKoE?jpt9i~V{3n0 zLmr<-?vsR<)0`+BwdNv`$8QJWhgX3=iV*m{CeWg$YU#L;(O!!%$x8&O0x@3HZG(F~ zQ0O?SDk80b`ml%Ms-=6Eempzr!2FHx3}6$Pgza7g4vs)(9aa>W#;GhPP)W5c+Dy@= zVdWDk&ezmbet9aZr_&_t}*ACozOiMHFPr(?5Q z8yg>Ye%s&PZZ?~cEPD_wuU>r^4vz=5{s<)SEtq%@b8D^9yfSP_m1xx}N~R@A($RXz z$z{A{+bY6gJbrO$;c-d#>7w1`H1fPBS)E$JQy8Xl-H$KmGAd~ai=NE7W;etbDuDx0 ziJHqs6i|GO;#pRRPOl1qQC8)XZDX5dn4k&?iIa@rH-tvsE$dnN=oJZG4I%6lA*AEQ z;<7KvG^7wJ3qHP(dMQdW%MNKxl?ey%?OLbKNjBL(CgUjRqum;~i4ijh!naxp5BR$2 z22YE~ghz1n(Zy?-R=H;WoX8lm+$GhJER&mQL+dtCR0`o{NcZ%7JgZBnMC0`UB=y}2 zf;>(l5d^{IH3SYYctVWLKH6d*#8@KY6mcEsAO;eAL(G1o=dRc8CEt(S;425u4wK1h zkmuJASMI!n85?e`{e0)$*vXe53G(^M^jE)xn`S*3~$#-||EUn*yM zC-Ohw95GWS3IN082nkRDfdLSVmZp$0h=8yezFV9C<-2py0v3abWbJS@vhjj2728U? zwh6UJNkDJv*^Xogo)a|7wsM{*W(I@|R8El{0pu^QGM+YBH;zygmr#_Z@uMM{pr`QE zX8-yvXFzeHDF7zb3P&V>fODciQIx>2K%!x?<#4BU_P3qg=JxZe%{BmhU!+_?`e*Y2 zXROK%2&}9WnVyP}zNSZbNu1KWk~t|vSt1su+6{=7%GTD#lbyZ&-I49f?*8u1^FXu= zkB^TB$9s~R)-yRL*RvBweA3#k)>V!xC5YhOR!DChZPLNqK6n|MAr$T5Tzr4PGb|gX zE(TL_nwUg^#^v>?oGQp@nsHHkp%6Qq^3@EGagh&uPI0;lC?y*0Ly(B!X#12J`8M)Y zFe&l0&GQxBr^Eb{=i!M;Q)Os@;W&|_f_a{@Vx-&*n_gzrQwVQPyYzE;PDOc^dE3-D z&jj7;Y>FfG^X^O6mY|MM0V>OY0|6(9Phjbf6bX=zF1kP^)4SETghWVgAwkn|I;*91 ze6wlhpbDwOFIh)%xNs2nXy@PTozH9AXd1^MbI3_dVfEvctzQw6kYv5GWjPj>jRd|( zpo0y@IJg7?X>rKKX+vO#E+#M}E*PAoJ%lv)5(rGdB!@s!a!4BrpnGG~y+jQG;w>@`Gyx{i*yQ9o zIU5d_m_Xt&JSN3-fz{RjV{E@n-+7nb8N{dR9UP2I>>ot){C@gi^jZ4dgOP=2%jwq- zc3yu8l3t`pdS9<>4VoF&8{O$WYo}jdgJgJ_?#-pI%qGK@M@NS}KUjz&V(o-QmY!W+ zu#4t*y@vEfb#ap~NjRE2IG@t|m$6JRC z&od<1T%0|7y0;P%WMkv7>!X^h-UdP=fzD)WlAATCvWp6MoW$b_jw`%^i!`HPG$o?< zrUITPF|{>qJ5^C-m<|+4k%^0Od9ma*`v8wXaKCN=fmzq*0Kim$$+}L|Ky_X#JFKZE zRC(as-am=(l5vY}g7r_HM=@Nn&QWA@kM;&>`O|QAF6h!i|5dMKse}rRx>>OoJU`_x zq@IWX03ZNKL_t)kIa5oyDWiq~u4_o`qqFVjD|^3e9L5=1Nh)N~?&!=6js1IN*3)MD zMF03&Sdh|dd7%cN2M1fEKn}mp_b7VcDyKEG&d>M09zkf zqt3FM<^{oJeNZ5{j90Nshy_=@WHw(jeXf)PC6|R9d;IjJ&U$eBcwMd;T%iwP)*4UA z>y^xEGnvgIgjvdMZ5aedAV`XnktZAB5IcNScN;TKBkTExWaOZu__A57X5zDWoIzs@ zijT#{(6QL}fB#$TuOoLq#Kpw&=s|jLC^F1@mtKDTzg>hyM)ogn zKys75aggwWz*8iSmQoEHmkAt(Qo_+?C?XiZ8JuCF{ame)iKXS^&$G>tAd~6tq}D)Z z3UnjSSQddyQoL0vS17aGC0%c{`;zt=0T&JL_nyPM|7V<>|$sZ9I zhoqt??!?|nWF(Qy^ZMRW5>XK`lgZkK{sJpVHNQ}`%i~sMYyZ{z-A9kM-?tAT1p>q* zBr+9ea5&G|jk-GdeD^VPe^U98$5E9M`2};#-&~Qv!AW4C4wLV0mWnrQH_~hZk#l^)ZD^WP7pZ?h0e*fFqn>RqDieQC?qY=0>JbannWi^Q6NLXq5E^i z0A>~b!X+h6@y;F3G$>M3a#nb(DOxt`xpuMr7CPG5X_ZSr8NzvOJ`0k59d<>jE9#+Y z^3A$<+NBjbuV7A1S!=E)*_6I{0$FHK5Vk3HaD@LARwxw=xT@J9K71>QWqb+{uABX| zeL*04>a3RoS&Nc8AxHoPJ1=3r-y{ZRk;ox&U?CxhB$0fw!L;!B)1NDAMQnaPn=5th z22S118P(233!RnbEw}2LCTEUk$HwXI82szVT}t=vjHE9Ip%1S?GP*qRI(=t!Cmp3b zUx8#UQY2rQIfl&)>s?Mn_5K9O7nfnde?;%sEnoFKI8U^%^vf`BXCyuM;M#B>W%VXP z#4H`$OguN~TL#J4m|W=#w_+Wcr86bn0y>Tp@u4=snbzUb<4^Zyk8-!R*8E#dyLEf| z`|-vmGF`H6W$}zxAnL9l@l;$T5(I@YjEa&8WlC)XD0Vkf&QF(;P9@|@3B`Xv(O9f? zDB>t8(19_?0?WTTHNCQg80Tv5v=g}(CPXQGz2KyzOd0If(W`&`wDM&8 z*LGi#^Ch1Ka+%0@*zNKZDc##*fg)2Id%rB|KRj%;R4gvxDjeyPN+`v6GE^>cMc{Q= zR!P1dC=I8PZy4iRlJVjUL*;Xci=bRmh&U7YL{0eo!ttGoZ^)dYDkPa(7*V?Yi4ZMI z%O}Uji}%mAo6U6@KsnV3Z`RiVfSb*=oFl@7)c#xrN==q?0P}MhEtuelgbE1~@B+19 z(Bbdl>8`37BTFJ5ro=*55owa_le9>Pd6BeCOWXts5>%@hE7h50h2qExqzRofYIVI@ zO>j^GDHa5&6kRsDIt@CAjFT~G@=tl z4HmVc;8q5recat-0pRxECPVa4UJ;RzRINmHN}$sWOt)rONwW~RRvyD_I~d2FM~Vbr zyg#s!F#loiY+l<)(lE~El2c>D_E)Jt-P7Gv^`~3iQqz)E8SXVcUc0ccn1bitlqJ4CBqpP~I_!S=x9upVy$GL=#}anjO?%z3DAwy}!51 z*k+xY^u25?Bzb0nM6DqDp{~+hj?z@6^K^4c?asAb%%@U3j<1eJl8om}2vnYp9~qtj zfRQ-`P=L~+fXW~sON|U?)|}9_AtOgralE~|iVrGjjpi&^aGUnm7po`_lvF+w9{>D) zV|np#cJ5~^XklidU{h;m(Z-fvfCv#BqJKLEWlf}1EHjzOfkq=>1G!jhhN8^9ZDu`* z_U%=riS}oKs3A^x@gly`id(16h;l%+C?sgJ2q0QtwNy^q+hP!kw3==bb&9UXdueD^ zNPFI3y`Y<25en-wMcXMO&KXp-+VqcGqFE{&<*cG72>yde^}(=VnweIq>IPnM$!CiK zU7(=RT8a(9thxVm-3)T<%JP;;yI96F%~aWhtog10BSjJcK1XHWJ~E6jNLK?7V$bjK zk_@E#!|h~*5r%2EaRC4=!Skax0Y4SbHh4UNGdiCT3iT59#~jIa9I8J=5qx;~VEiBo z)Z^Q|U>?;MPVyHA^S{rLbYex)`Q6IaNjInO?Ib$~S1v$uUi~&l((CzZZ=`7ClN`y3 zXgO`$oH>|JZk~>PeRud^d;5BiS(hBSEPri~Xapr78b^sU4bpva@7;|DL?6 z$8=0ehyaqyeX^kmA?unt6=CIN(D1Wcxg-N` z?WpSJ5vNFLN8~k4=5?ModBI{8OXs6c6SGB@lh1xja=xum60*8 zwtsU~W$EJP&X?zR77vqzP;2cyChC6L9a^&+d#N0oyT5>{iVySUS1(tACCL&e1CT?I z%5v34I$iQ4Imj0Zp;R1WCAr@C_{yEs6|VaE9Y6vBk19T@i2`ElWe8>|3b5k3P6^!9 zK2oS(ANa0@*}aVvbEW=@M3T=;=o(Xk_b4 z5h~B@zc$>s*Ct6>dOhDRA=j#yq?BpchR)IW?3^S|;XISLjgtvJfhWkmfro#RBUy+Y zTM<7#T<;9e?p*z4VR+`mojcQ$1^=2K9-jYj=A7!ufj`|xa^B6!d;gpn?p(hB$$52q zW_aepwG}(KVn*~vj-6VYr_SJ$wqF)@uZ*8L#dAvrLfvZ$QH77-jNJz8teXJV4x4*T_-scZ$ltsF6z&LY&jL) zkov)dNqS>A&h)Y*{8_d{1%i;JCPu18x*sI*TBYfs2rxhxZA%Q4uPh1qiDlYhHH)CE zLb_~gT3Ylk*0wYOrqhO9SlX|f1i_k4R~QuZ>ZhNLL5f&2dZ+&q@xkgp?yIVx>R}Dj ze%;fz-u(3J>GJa@_eR$tB9LenYYeMa8!TD&ss0LoX$}h#6g4O?mYg81N`H>!@oQ=B?erc&Q?yi&@bdjXn1+6T(iAi?s5a-7eIKtV48-XN| z7)T@@{{8Z-t;=#*{`WzG58zu13yX^{r)uwxx?aDe%1}i8V!{w4en1v^!0N~%zBl@s z*R=nzcRsIeYgrs;+O=>NlI~TmbfpMM+^b)F(`$s#laLPGm^%i(o)?QY&IO5ttZYNX8jBf7}ZN{fp? zi}ShPbI-ZoZIjM_D6AA7-^(_T?afIVW9Wn4sI2;R0!F>iQUzXQv8rK7MXb)2HrX!E zdO1#t{G*87bXHx~FkDBIG!0VdgdKi*+BYA4JC97iSFU*V3LHByH-eIZbr`m*uo+o? zIU+N2wwno)OtQprIWo=We~-f|P+r&}=K(Lt+e;75QlWgRpxOw^Jwv;tN5~(4K zI`Mjmk2A&PXc&kB2E1Tqvc^T_uvY0kAR=&uzMfp1$TvIW^!a208E1_*O8mjVLImTM zrofFXx7N*SoF(fK9j_1VR=Lxz#X~_Ljp^j5M|;I;A0j%>uyRFRK{xXoOH5Ooo@YdR z`)V-J_&}>TdY4uRn_R5#4U?(N+u^C^>dXOPOFg&fcYq-5-3UQm+p;X( zEOsIw28yl}XO8W)Qm>It6>?=O#qwxvOf8er^S_@zk;7+QF>-2lE&27J9>h+m?}#Ds zYD^^@m*t`u$ki4g@K98M2M@0n43R%v|8geUpn4!WV$cjBf#j33{gflz|4-Yw+z*QY zn7Z%h%_v8p96m z^*7ePoq$TM+Q`k&O~c4lNZ*P&2Ouu@Q`0paPO;3zLCf|4gt-ruV_J)=AX5HNBthP` zRSe%;kH38Wc=Gb)$;VgE5Ko2RQLpP9MwF)>=30EhYj~_?Uq@6x0gq6E>H6l-uj;_5 ziJWFWf2Un+8E|JZ5umFfr1HYx6ln-z$)QE!X{9JqFz)nFZL^}*ypU_-jv%PbbZy#H z);eh>V(J}xZ2fsRbfIAy^z!QC+x@-ggFmrYMp$hq-7>7VSG2+JTTh_PG(s;49#;Df z^Kd(FS~F#-)cyC@T1x@d5LmF>tAL6p`BB9zH6wriNt7zxh`5@%H4jBdRdu#2Wo!}V z3rTW0U3WzzOYuaJ9GjcAZQ=Z#YEt#)N=o8JUiVvPa$hsk8e(?i>nZQp^iC${`Z47z2{^;>bpmqVuqYN21g{xSt}a!4+I z;(AST&?afbIWT5QM{xVB4@jdv?N*}VT9UMxs9a5sU)`2a*?;Y=viTqpuRaDQz{?t* zURcH!kNRvqZl}#Io9#4~FAMA)Nbce;zI%`?9zRk*KY;NAKE)bd@f<_eJhfyLP=8aX zZmytZHlHJS(C3HAHXVZ55YYOEL94}g*C!7!axkw6vdUv#GI*L;rA?7#IkI_wjkboQ z6h6>oEV#U3L3$}?8&sAZ;xdc68%r$eoGz{NgI%hj%YBDriPTYKt18Eg<1>M2!o0SX zJBFCvxZY?_54Z}kiOF~#oLN@=%V(ezuLCa$>unBr)3aaRAMd|^_xj#s4rOvarIJ+a z1no6G5+&R>EFjscC~7c6k=s?^I?p3YQLGV*qTIU+^Z5BY+}^r0wpM#8z-kJv|{Rug)e77@0@nwyk$N_YKMmHHV-ax*c(FhT=fnuxBbAkF(vKD2K8U3#9f^CEBmSFWIv(lDA4gmNXXIt*{uypqYDl~07z6#gSh?n zLN_=9^~K8{g`6XJbxvh<8B5yXYs3%Oj4|ksGZI!={##-B4kUMR7vDWd)<>2!)?~yK z3MytPQf-#5tZeuzg(X~UZ&rlDk|6L3qpU!)u$>kfjUO6yS{bfJxkc&(3z^u^ctKN9 z(qy}F#qw(X>~)NbouSY+Sea*amL~|CkezIx(S4ca*WIS6Wm09sD4kA(!LtcP?9OM> zcPu}&>QLz^E`Umw@0|sK)p3zA&2~jD&31DuMb`>byWE_ax{LEU-+RtIM{`sH4JAQ_ zr7GgeU#@Vot!{pvRC$VuLJ3y*SBH5?GE|jPqELxJ6F^f>+`NI1D?t-Eiby1BAW0LI zLd7F!*pEulAD?bsKHGkAXv(tJ?YG$06Q73^cPfvEUBFI&`0By|bB&gC$YQ>R4 z^EBwRFL6jYuM4IY+By*HoRK^D8;g)(kCZKW7)ALVTXR^f+54lf)3ots)*VyJQLJIIoJp4lFDA}F z*Y$L+Z&tTd%wyxoxC63GR_(B=M#osYddYE}Mof zfT-hZ z>&#tQ(+JLHqmJfHvxnyZ*lS<@x_S2O=4#_8oyD;}6N5qrESD;}|9Lq*PnphFnNVFf z1Qn{$uvd^Z6sl)b#Us(iS$7t&F=lLBeF0GVbhSs8PXYLg<2n` zdG?J&gFwp>#r1TcF+m94`-PeWHTF7MF~T@s@Njenuy48+XKui=M{?hgau?@JVa#ug zi=3H=JUq8*e!w|!es*FeWc1tNn3^6n-|x=%j^`)Xb8Okc`01Q;G(bqc+_QaLhs_Tm z?D#W2ZLaGz-l6^k3RQp9mdNM5nK%eg(<(NCA_pndX?BX8JCNMjo&E42xu%|$QTWB* z=!)@M&Y;7wKT5oQrfwUIOQ|A5F5@9Y$zn@bWr#T96PFkHL4pur%rA#3AFKIdzojhj z&ixTzN%toByPfA`a>LQuVMjqj zwRc|(1a$f`j3<*ES7|zWF&+!`td5a@z85+2>ya{D zpSINk)=-_5*mytI8lC|`%*;_wC%Ja3@0g5{o+haOG}GF%|cSW#7*yoACAB7*;**?&!5WB5qI95BtOA)>P|iR z>LesIR{E4yfIz2tRooBTH2qhYGTnn;8HxWjK zR#V%lkw=6Pmp$FJV7(By#d4Y>v8*W^b_1ZWv$Io9Dds{JU@F@stG!#pr>EC0sN-_e zjrX=Xuks>xu$LBY z7}y{Wj<4*pD2hb$_BA5rMIr*S|7QF9+D~2{@o6jQ3E}i+D^Ruf)xI={_)*)}8q^N; zU_o0BqQeSzm~sYHRLDvONqQkFMVL$fnsNJu97HM5$r zfqXPuTFSuTyyXZ>LfwV=7lU`Cihtmf{w#Vjx7@GZeX0nIAFxXjIYMSv-kLJmzP`%nhW7u+r#g$@{n2n-*+d&uV z3?ki9MPn#9T=3rQKB()``h}nxF9+(QpC_r5;&`5kXgZ%EKzkL=C`y%lqz9dD86p$K zBNeW2DlwTTPsS7bmoFa=b~m4IOP*n66_dJ48VZ&l+-$n%~)@+KhmT5Bk|Lk2&OCwtvW_mBm-1;~t=TucvQ&rSC z^^p`wD&&I%+$4}igiu5b0YM~QH`+=Ty$M2ND+sZp3(+u}0l|gnZXz!17C~C3#bwj> zu4g0mEaoIwWxxk>QueY`@H9z=XJz^v3Lm;po>S}d_Blh-jpN} z#O*?jH|@~FZ128i045KK@jOS;Um7IWuP>@j#^*B%zS}5=E!5qz;yTA)Pv7ru5BtO6 zZ;q8;&O*z|-;KqNP7V(bpB@;woL~HTc>o*$%*4fGq87F!5yd^xwzs^oytlV}xcP_g z&c{s@ekfeepjsqigGFuFeSrn`;w3(3|Td z@Lb!X`Z%P@7@WXkfNM5m6Ol+WR-cH>N0yK)MA{iI7L;rMo=?^$EVa@V{7`&wf~rCd z$_OH@4Fy&6nq*FxKyI{3T;M>R2Ld#EEp%_$=yW=ZGE9&H(@b=V>D{|EeNZ)I=fIuMP0xcQpO`tR8@svRdQsCG_1M#_LM0rEGVyhLNYqr z6xMdTKD4B~-~fu@u=U%XcB06NEJ>CIq0FR!jZETu0GdeBHa?vl>|E^cj{0v zsB%d@Fl-`nJm!L!$D|= zytw=LboOkuzq7p^x>lYpJu8>5UvF>UtPdEq48*Xltgwr^|kxa*v0&P5^ zjlWED(R7y6UK2#s#FFOMV{yM9E$UA5st5!SPLpt9Eh}8nsdM=$u9S7t^8$pWGAu3R zcxbpB0$4Yx2{{+=eFA|^nQ~F7DvP!*2*Wi~6D6y+G)SfLwCuQ6xct{nCp_0R3#B@B zpEo7C;PT<=bbIG;x6`%MA`9%=!u?*N2~+G|IO!V-aUE%S$mk5oOo*r$c3V|9JqL?0 zmv3mlZ`>CMBY}dJ&dSJT8Mq(y0|O{z%auf8;PX$&sXnZTTi?4SLFem2o^x5SyBo@~ zW)N)3ND3K+@d|q+hw2uhNZw~8a^#y_wRn^sU#cp5g`VmicR;jY`MnRuOi(A#Bk{MrYAw(KlP*%Fe|;0rve@6*4!5<} zXU3wra0PJB(5+I6^2ZmRBo&gWv>hcDut;&~!t*}g*5la8RTQ!E0h@2J;@qC>><*Gd zojclRnF1R=wK_-UQ89y3h1OkfAj^tteIGPtK{2faYNi3U`h(;B#a$*`zMP$$Ja-7& z-g$p?dU9}hbMtBO`h0Wu&C!RsvKVB#uV=h5%JI@w9Nft^YhY>UdO1Rck6N2%Wk zBmCG1EmAI5toZm(yr-%0m<%{!1_!Cwcnno0Vu3uBhJrSZjhG4~E>4$T=9KwDA!)Q@ zkyeXQM@S)1W!DsnV5T$yV6Gu~^L1TeCnWBV8WyLiPMVVmhpg!?u@Ht#qHwxsNdyr% zTx(hUe6zXy1@PE#m5NUtO&Se&{Z*yS-kfl2jzAbBSwK zxw=YhiJCM`{O!-6cyW7QR+O1a(c}N=0Pv58gQE>hq6%#;pRUK_nH*D{ctUHVQcf+! zJNLcWlt_@ae3cB0g&@_e=h{fLjpXqBC2Yhc#(zDgT#(xDPw&52+ugsp`52bP!m8Nz z!NJkV;V(~5kB<)z@811&`L0(YRdH-JUlzDZ2CS0!=rb(~Vw7oXNQhA@=JaBs^F#ASOgA&L=z65|{SjT{U<{VQ*upxW^VPlQeA`=VN=}UOqo?5=uy4DPxN2*;Q-v-| zL)1V^>lu)fGzh|pxiC;SfPsu77eKoqheQFrAGl|+5H0|yvc+-C=(cVK$w5_E-5W>Y4ROS4gHsQI?B?AB_?I=IA6b+YA6@ z_XZ6yyUIJI;$us+ukHh2=rSGzZ;DvJ5yvQZors)MTqcC3P81*m00epw&-Xj-t6x&} zmiFcdY@Q8@T!yO<50&JU(%w#svf;^nIVY9Ph~Oz$9_@a^;%v zBd8N%L%poy(d=RavGdBL5v0h(Z zD}qE7$&Or$A5O-TDS>r|O1S&?$xMS12|myhDm?7An-#zNT&S%DicNmLpz~S-<3$Wp zc3Cx-rjKrh`OFGzKCWT}^_5Q3A7%WV22G@b|5sw;>E^Si8yoj`7J5gY0=AsrDwpy2 z->0YHa{jNMKK*|EVRAR6>Mri$`v*x-_|BeK4Qj6!S<}U_CBF|igdvKh-<=GGA~o_!b~}`E)RT-7 zE@BsDvL2!FKf`|Y^T~q)S&&FIILKI1gek!jp&4Td7}Mq6eb6lUKF5jensBIb+(s0;Q7Cfdj7mgG)lc#nZ@(U0Acs6UlWA#V{LWk8~(oh9BknbCe%}$wHWqI`Og^3Nr zzyUt36t#u{P;Ae4K@No5xDtfd`@rwkQFc8AxR@zE4a(uIK#9aOxxeA$N~CBf$@v_GCl_W-yKIZC<_^?;kx-^+K)Wtau4Ok!}<+ zbxQ>#!6hUf^Rf-yK3YBVwY{J%UZ8Uf1=|L2O>k)GO`z>uqe>W}#m%dP&So zg?%}p$*aRxe=1E&`RVg`cY(3^hy_)_YIt|*$;CbjV5o*`?cvQTh!}gYKh-d=2c^di zPYGsN3m!G5_rgw1{-#th+_eP$&--~Ic8jp}@DiLnn%6FsrrDCVHmPN_M zc!LYT5__76MYnP(()649<3g+D!;kBpoy*xGQ6v_WrW!;L$&GcG?FMCgkSFf9N9h~-7t+Pz$8NzEdwbPfP}mgWa%o&Qvk%Bu$%G7 zAJIjQz%ts{`f=~)eD66r2UBswf_}SJ)lj|Q&4^H8o**Ty@VLuxt!mKkyI;K~@EHIZ z9b~2i=)=0M2@K!U%XRp0odIHc zQ*sK{yAPpX7t3+$dJUEKtpsw=aPvZ!GpIq2SX8ihZSwS$$6p#xFYWPvUcT6yJRH@$ zj?W8yQLRykuD;yBamE(?xFt!Q7np}zwjk4s2hEMCtzrXH;$clDBu-IY-4-FnaPlX1 zCKFlseVFW|YXfz*uP5%WdX66@J4GAPyU~c;Fsyd@h@*V;qX*>{DgUj~yBnr=c4t34 zNVeWXOJd$0W*;QdveHb49=kTE2MZ|+M)S&i^41gus3VZUE2%>LFWuZ@{d7K163_3M zhQ?DbU)&x+)AaocjNHQ7f>-iVeq3%vl*G6R9#mPNO0@)~3DXZFBW~x*_It8K=tRhw z%-o0oW3Ce}e0gI;LA#O3$7x&ouV>%B0mCdqft3bA-OxurudmM??CrfietGg@XJ_~9 z)ytdD@BjO+e?EFN{3<&MDnvBfS)J;#%*woh`%Gma^Yx<2=EB}7fg-1wmLYK1mU+}@ z=DPP2T$OVrNmM2N0uyMkX!#yHU_JTd0Iyz*x(*Klea$c^=(>lWwmu&3oV=ZzaEzoH z^kftT<@A|$ZvfM5dMORkNy%h!sgJ?^%6uhkJVzMtEW^s825Y@W@p{QmH(8iTP=9;< zTt;PU>n5B1@Fl#i^b)?&u*KaWY?IKcW;_gG=HhY!O~R~k?Qz(kM@oKF5$jofv2qU< zt6zd*C6nm6MVJ{5^EIS#lM;ah#qpw{Mj#?2L0~m5%5@OONQC_FJBB{vAPN$#Q>~qv z@t4czp{|FzY-w^XM4F+I%`@(3x7hf{>)o4skl{~j10jq?F$e5W5KjKs5L6;SRYj2Q zg>qC-uNUgr?fb4B{_kPr!?G$8WMhgNe!2<|E~svTarbzEY88zT;b1-a{h?Y?l6!wo zYal^fM>5j(*zy;nXpsca){I2=!Go6MHW_d?Oz-T@et3{fmw(Q3MBSQWaA)9WQbFLR z95}nEG~j%8~t&8#|3(BJ}=BaUqIM3+wuo(s*|qkSaHu={?) z*?2Z8*#>NIMV%9_Ha^F7;D=+DQ_P$lEDj0^P*e~wGU8it+Vqm5n{-MR9E<+Dbg|4R z>A9c2r>$`{_t4F;lcjV?vK%gxmci+e5V3sqSQPU&&mLX5t#+xB&7VCBN_?OvP^iTj zXrIrGZ2VFpybv(52ybU?jfe*BUKFbMx3x;bPME}v9=Vq(pcSuWcb6)sZeEeVby0uU;;;x8;m%@_yc{5XT|pdUx&W?8EWS)}eq@ zclM|+J|?PdLj)57@pk`MwQi-zcSX|f&(yCM3PLN@aSPt{(YQZvI;mPtZB^>@p03*5 z@&qGn4&s+jg~^0Uql0xNNjD`1iSpB2QUOp%D-Y7_(j=J#OE^uXR}u-H)(&vcEmYcf z!}QMX?1u*l*!sx$ql&jN2GxW&E3;84dAq!&O2Et-I%f}rZi>iCFbhj7g}iD(fy$Gm z*rlA#xtUC%CwUN+r3Mzd4WSG%opV^ka*R&hX`$1d_W>v~wVB0^q{^nuPeuT8#0J1@ zG*e*(7b?$FML^ivh+W??sTSc1VaSwvo_qG5N)i}!K)Q^DsuJ;;L0$%BCGyH^de*`m zrz`fw=m`r13F6UYO7+UY-k*u3E4wdVygGjU>&IV~m)~xlitLClTbfuZLW!M8Ep)r> zcmXV4@*xsY)lntU3-j9Im^KuBLtM6a!9`8*jBdBtIHgJg5mt~{K z37Dm)WAac@Og!NmiovzGY$sbbdlyfzN3zNE-p9}T zocB2ilv_dm*+17%)z6yo&CU4`gbRP5g?tuhuU!^PKRwK681@yo5 zq9{@Zg1A@;mbrRWZ8Q05VS$*gTxlY>!9Y%ALZ%Yz_@p&?dn+6BBRWl%y}-g8f>=W` zr*RPDItQS@_cP7Lpkmd$T1K%c1)*u&xd66>EOSBVx_(_ABh^&t2r@gq*dp?OnxC#=Qo zk1sAlTse7p9^y)95%sT^A3lHjboKg|_XL|9PoR}R4M~uj4JlRiSN&>UA8MEm1x_nUb5+%kH62@qB2WRrf)I=8MK0qe#Q|#q619Va2 zbgL?WQs$bDQRde7Wf93B59+b&R!o!)Zg@W>WM6!#7c(SPN;{vfpl&IcT{(&=ic+yY zKhUI%S1Sv7ui!$yx@4wtjhKdgZAz~U+vQzX3Ab5qPfXnm8%aVQ1Ec0t&u!IeLJPuL zlSp4aKQ?C^;^6nPVJDSPBC*n^4MEbk56Jkalv8rL6q?W*Uc-95@#t_5+Qv@{M%1>9 zNibs{K^ZkE{|JuvhT&I*9`}hTw0J_F?u=|xmMu!LtWi#nwzWQXqvdKXDouVKtW8HS z?_nOTA^}d#*h208+m$V~xki7YEflfdFDopw9*<`n;o(?9`g&qVSQHlZMR}1~yuJAM zogZ({m+3s_@%;lyY44-gKb!0@d%J*xItX_&rDMftx!s5i+UzaT@1Y7-MJS45xNA!W z?H;HPCMiGJ*nG>SBZKg0T{#E@2@8&a2^nxrpAetoO0jL_6@%be=Q|9YxMZwfx2kEu zi1lHtQ!J(g#Gd^m6IQ|Gv^jXLG?G?bFj&G1k@V<4e^L5^IQ1qtL?X2St@!y#{o~h{kKP=1pITr=V>0=qBc2u3=&PZqsA2JDL?urx z%IN}SR~4>kq!cJyp-H5rfmEtpd^pHteGkBRVwp7*40rc|;+ibe+V0DLXl(|3MI4dnou>le0DLR^B&Kz&)-%PlyMn)In#8rlS!gjm9`fci5Y}FuR$-?IQ=z~E(M|) z_mk6RF_!Ij*tIrqDN@F{y~rq>^`ksrrt_G`cMl{M=#D5nOaddgI+8RpAijy~^+e@P zIwq`oNCPsS)w*`}fwRJI<0&7zc=n9-|v-Lm+Anuec z#CLfh(K$POkx2@VAH6??O)hi_B!+=4tXWc$ z*VyaR-#x5!3WZjga*)fZWJsaLPI*aazc@8u*q12jq2z8JoCgn)+;nnF@2(^%Yb=eW zj-wc3(=7jNWlH0y&I{Rev{pa(@aS-x9iI9pu{473mo*caL6GnkBm%vJ!y|O?=Iu~N zlozGjcEopljVPr7c^bEM1LM~YXIhf?I2M;;S&LA*>%#h=-*+UMwWpc_FJdR@-JTd)dpv9QV@xB2=_tWXui*C#%?JvEqOCxkQ3zeCN|Kt& z)hKDp^{HFmymr}y3~ri*UOgL(3#7nIGtN)<0P?j)_tQfy(R}^O`?n26t~I>U{ez%} zsO7j_UryCKv5#j9YFc!0FP@+-5}SR7Jl{-N`pM}&6|A7X##6{TUPd7;YZeI*OXrKz z3qcY=CF&aUF!hA)a;DKC!7MpiEu35k<9mzm!6 zt+*{)6)y?%{pldPh22g}1?qV3^4M0l8g!g$ir<5MA+12l-#b=vC1RCcJ#N&?Z@+o| zt)?kXDy2l9p-p;J4$5&$lTFY1;d|PWi$Sd&pj@|jLrdhdLC-JRnWKHhbR8>7xLek2 zcHTaI#%(*QB@tpn=SV+KnCKQ?Oib_aA`tcLkGda&dFiHS5_C>tVofiEGX_ecb=9fC&&yCZ%}#Y*0EU)3vBxdF0a-` z{mRgO^#!M_bAt|V(@x!qPc+vW?jPlG#`HCB;VO>Hmzxo}G4KfIK$e{y2PnHKm5r|n zy(Nv;B#E#2=31+~w)W+hf0%#u)zvb+vMc+R3+)5|03ZNKL_t)|gQOQ*_g)@%wyUE- zXpDFERJf+9kqD6-zBNiG+SRRedjNPqhrhMtbFR4RO{5Y;1S1eYT9}ij(}N9#wfZQ} z_y!8lxjVn8KZMp)?8ZB8a@vR)-N6m%p39*anAT zOn@zp=MsvNB!V`3V*++nq@_*a_#H=Vh#3J8Lx5FkP?kgrjk{crdpndstnUBm^XF=B zb1X?PfQlVO#Lf@0(Q&A2J>sy9xInvhC!H99F)F&eMSm33fr+2reZ`p$=2FORg0;(6ZR_i2D?iLJU!g)k9t zn4LZ8=jvUjN?Bu?mD|NpJyE!k&z4HJV%6b|bw{q13cW{ft+dc5i$&D75k;-GB9fqs z_!j4r19lqP#(G=+=g+n!hV*2vRX@?KvqPS3c3R%cr}yukS}?^%;+_3`e0?RVEoCcj zw+$S;Y2&Uy5L&boSh@R+J6S67mf)7nm89#I-DE+2Gjw|SdK(Qo$(@=Pc=cYNmRQDX z^(J+9wf*(`Y9=|%()M-@V@j`|D-ugBs+0=3svoZ>%w1j4%tZ)^G-d#Yj7V5A*3`8q z+5X=kS-B?cfBAchiL=M={&V^6FIOx6%C7AH0}|D=(|4W*ZG%XsVk(3CJ;6FpkT<` z5Wb`|;%IQ|>Ahkmn{{frLg3nG zJ7jb6=X@7aw+Sn5rrPNn>behCvju|ygQyp=Ep;Bw1-mE(8b}D1*@_0D#JdU^6T^gr zVS*qJ5+92hm`H*x7kUdAF@Oz~Q!aA@(%mNT6WQ)JUT&s{9aVeh%ipF8yN5ZfG0B+ zsT+YB*K@JX7D!$>{Wp786WT_Sg~4<1VKHrR^-uLrwYs*t>FRd3tpAZ@THuQWI!GXd zgoQxh2w7&pe;7Ojgu&PjgmvPbQ?mHtVPFg<7elnoA%Q(ElO+tyToyKmWe$P4 zZIzwO4D2rKA$#+K5+!xflDeh$z3;tO@9BhM6Z*(_e47!$&63~jmDz19tpvCLGwTa( zr9-C=r*kon)Y5gvYMYTGRED}JZ_;rW;%(ceO0Oaa%8=PGn9R(m0#a2CNz%-}r~gh& zoS*+sm@v^kUrVn2^7P5X^*{bvuI2v&BuqJx(aP4PuKaqao%~eH_BW>n(+f_sE-c{~ zF$v<`*Hvja0IO=4m8a%=)vyyS50@JA!GP)AWZQC#Y5n)xOq2ta1H zOOm4zgp1V(>`HYXo%9>A%fehx0)(LPjx;}Cl4Pmn_6}B`Wz!9@Va8&isd#tV;N=cL z@G8Qt2394Pig4Mhwwlww`nF&|Mv~}hiU?vLCZdbW>cl*jA`uCNfrSo7jx5nRKX5gP zK^c2<%!X(s*ePNO(c9()V1Sz35rAMNz#s3tf3GWFvR5y!3v`)^i^sA;5Df-aK}rw_ zGIhr*$IR!|5|EumNv_Wq7&HKtBa5X$ClBOqRnIFW8^mshrb&^1;aV8#Vmxs-E@{xv zD7y8)E$qKk_8;*oDM8?_9+tC^mr034Ih>2KjjiW7N^SSh5h~R)g76lxzG&*-V$xDRu!Xsb(r=KKenTVyQpXzFTIs}+)38dct$Zh z<7iB^i?)VD+ee6uo?S3%sY9xnzT;qLg@ND{dG|_NGV3RJsfRIMT6*Ioih(F7>QDnh z-!1+4wXj_)+QZw!Qd-Dpf-=Kwnan1Vb%D3q{0~5Kz#nfWPmjmnpYF8}PTBl?t-bdJ z8*e8k-|n{$9$qcnnw;EfPflJfIvppI`@cDQE!T1_pA1N<@vNdgdo2(lbmcRttE+( zP0Cqx*-@1uN)fC$y^dFezppC_bbB~Op)OIN@nD76I6Dqn(XQlHN*}1tHdp)xghe9a$9FAY!qNGo^T-hARsv;7+Q#_z)+J# z%d?Pt=7ZgBo)^F<3bGlLcs9bC8JaSK8f0NrWlTmJ@65(5L6tUse!ntQyW0$q<^>oC zy)<;wkyQj{T9$YXUj`Rf3BZM=em)x*Jzc&;I_^qSTfnNw4Qd6iP5@7h2D@Fxj27y- z<>4GiyP;d{>XwHv>{VVo)#H!<-pYJ)JT)aMare#zhSY;fqvD3Kgkw|q`dKAfEL8H$ z(2u#bdQ{8C%2~fUv9F7XSs6mZFhh5XSKHznNvQ1`&MT*6qBM4 zOZaM)zdiC7c>Q?WYel6u2hShe|LV{PsM3*~Uc4HoHUi0=i5ThkG_8KrJ<_x`aoD|? zBl!^twTIrj#dna zjUyiZ$JCJbq?{n?uA2xE2?yq9d$$GCpIRUGhJ8os!~~%>6lU@>*?eaHcc0JXYX22T zl6y~Xo*q9rNWNwBwKX=58UMIBvGrs>`St^ld@s;D{o3z#7nE|xM9_mv?+Lkpbq)o#BNuj zQn~}36t1BBqA@LjD6S#GL4edEc zHG+&*0O{7#=L-oxrO#%S&6@Xk!?NyeoW%*Tu&&|Zl`~7_8DEyg6}qfLe@-F>895Y! zj}e-Y1c}2pERs3w3s`{KXsB8r_yzE%1eGU!g^VB&z`b!8?WzV%%#C;Au0%9)>kmJ_ zAFJJ=AVDG5Gna(LX4w`}0+uw)bB+;1P&y}N*w-tHNeloE@=|_GTunV24w2#gDH3_`6!}`1nMx6~yBG16X$Q;-Hd% zmtu-{pC4vgsnisv^YMJtiYIFIa?4z3F25)$l~Poxxk0P??xF}CYE1BCEef~z?hyyt z26t>3iu3Lp3UPY`ZR)N)=WVTw{#-w__vA5>eFh{PPrg4rUrRpZ^P9fV zj$SuPy_ReFgspZPy^^Dd|9BbI7fUJKACGp&8)J+KqbOYOQrFU_tx)f$WX&{JqlV|J zz|sNIka0K^1T=V&MY-vQDS03Q9y2>lESU+HPzh)S2TL_&ak)Zilp9DoH_j!*6l?G= z_Rc1>jU5rd z&qadW%y$cgrBzs*;SkXXi~?_+cV`JRsziOYTY|4_RrlyJwFst#nq5T<1{*xnl!NIZ45ph!_-Z@C_ zUAs2Twa3=hwZDp2rf*&3NIpAh0+P>;UfQKy+W%p{1qt)LxoZCC{HY2&F+S>as6N@8 zo~#~8N!v~kmd>VWLNtl@wc8eBngm?zAV z%?5h5547Q{We0%V5?DACQ~bIekhdyUrmJdMgUYtLy$pb=^@snOg~I(YJQ)QBvqFuk zTrR1T-HZ1^=t37H73^TmgH&Z$uUd@VQGo#=vQ6nWfkOx6K0ZE*x zm=3{oXE<2^+xjD_IH)Ps>o$LoKRtJqnf3FJTz2UH*sWE$^AGgAZ)faM#dEyztrJ<6 zal{;scj~yN5^>eBIUXGoCJa*w9Sd4IKxKKPW9Z|UZcPv(mm?b^wYg?Ot zF(`MlE=%|jKtmOkPpmNnx1noBh@yDKDa3ZQo7Ptf2Opvm)`+K3f*}_ML8GuSYqpn4 z`O1x1$N)ct07z*(7EKrIPLu*oS1WBS_Dl*cCSfg)h%z5Xgn6aK)t~-$@9eHwSqK{0 zAeVZ(#)-Cj)&mR_<{T%SRb)%)sYY$lx6*)J^dxdT0d?3XkwX1Y_lS+SueBiCvnF7G z7WaFZez469#Sy+=>}yXg0DVz;mGwN16@H%(?CeWG;a>>ae2;%QFKs3EJS{S!lpPtm zgCeA7D(!q2L+an zm=Ys+d|%JaC}IK@c>S{R?Y)ZY9=v=N6pQ-$Z7w5hA4j#KTvU*uco=>2cBWL!RjJ)- z#g3+H+pv(eDg z!okmH4ddJp&Z6d3c=#quEJcbxbsxmI!>G(@;gds8W)Sk z=z&My?|MuK9Wjvvqfqe_dh>C}e`H4_VbI$XW(UndX)!fek{7>ym}DG)-N)n3sCeBm z1AdD8A(Y}$0`%fktbhf{iQr~m6ClB)BK2KS^(#rDiGoERR^3 ztmFWR`$Pa|aFNz-vj8Pm;o|jNdIhDH%Kd>US>v%{0LtAkoSg@`Aqoep3J=X$N7RUF zg!sgOFkLsUlX5u-G6sNMGCJWG^#NhPU+AFwJeQDW>^o z83Og?KBS^XYG@#*HPf2cW}4;VYDxHP4ieFmNI*&+Qf6iW0=@^92_-;mt-_rovYmvF zY;6ab(6XXQV+oIXnUu~XDbOvMh^-zq#%Xf=*zu6cAld)Q+&%klCzL`>1`3UkW7zs! zY*ur_w4|GlOs=2D4 z%BW`bAyaf;-=pQ=?w5&Ica?v#cP^oABxwMK%wZ3M=zvSrU0vPP>Y}=-ekz*QWA#7+ zT_n)K0wEM^1Oktdt&3wDgAI!?m{}VP+9-1fN*D+e0y&uE=plz;g25!rh8)8rhlR}E zl0z`eeXBKd*o9?Jd&ogQ5|U^=8YKR&KK)gHHRa`>pWWBAbg(rM!}jG~rqqc0W?4?@ zMs)Y`)T*DBsL^dCwXoc7SKLzcp|E^AQyGX zr<#jsI;C5JcE|Su{AP!`;%jw$Cr|nKX8aZ3Jee{cGugr3%?ry@Fs6>dbWIt}7u&o1 z_D+u#yLc@=kULf@+s%j=#W60q`7g4l_35q!jR)D1vf4SUj>kZ8#9_gY2b=R%gp}ya zdCz)}7-@&MtWV6j9_GMx$A?(BDs- z=_J%>hS8}-smpjp`#_7)>*Z9l*zS~D&E0OTZU@=!+j`Q=uDqzBj-$-mZv9>)89iqd z+vk&j?Y;cl;c-xMD|%{tpbQdW7{alzvU*Vo032|}AswAqtd_^P?Nc`EPseWey5C_5 zp_H@7!`1gYIKY7|=xF_Vapmr(ZR}Kt!ZQfj*J70E2FA&dve6zA;}pi&;YHjT zdR}g)eN|6LrE)?si(5-qlbWj} z83O`Af+RTpvkAu~agBK?giLJsfglAjAv@RvHa0`xqKwZNR$#o531B9rWD`TKy5Yrh zBfubT5P$F6aes7jv412<>0Gml)9Y(CT$Z@tjXFjE5o(eOD4U$*jB3+wLy<#u!{%;s zw_4p;<$Nvchv4WG0P%vOuj-BBhSJWlyGLoDZuU_Vuv)4N)W&qw{5q#_r7KT!4;HF=H5;shj*i*qqL%8 zu@@Bcg>LbNtohlf6{W7;X9&scP7fd5dA@gFc+T}2gyZv+pa?|HbQmI^Zr7M+2l14G zt1E50-Yl@J?O5`)v#M6gR1!Ge81e1XLO$*z&oJ(ID`?PL4fypz_WP^rn1td)e6~)X;?5N zTUuID%1d9mvq#4ZkSt;mpFK!=G@p+;&IY~w(PplziJ>_K2XUQbY!XRVQyP(MWB?gq zxWCnqO`>wcv;a+F1~w*HC$oBcudqTYt29e=NGLc%(B&L$=-pwZ)3&I*GLWkvAT(yk zc)rFRZEU}QwK(M$EdW@5Mwmsl2AAEe)^2W{_1@mQ<@?dDBFo;LnJN_I$tQwYlPdFK zwFhX{G?D28WTr7mXHsVv2>$`&qq(v`i(U{n#Ua=Sy6~77hGINSHyvRnPkZrb8(JhkKWYI$>z&gY4PTuX7`{fw&eYM9)p42ES6;DHQ1bl%rNDw{Xzm=bm8 z)z*zfHrd*h-#rV`^1H)%wWFDi6yz6V1sRr~`0Z1pnyS;NT9d+r^g--FS~;FEA_#6qQ$KS zAk~WNPO#22VhJ6o>hkEMURV>@V2lox)3?)3j~fhg*V?^$w3hvDq;ugFA!a2uel1+J zJGpjB+Sj1-!5(HF%2o4 zASEhR;r8U-@@Pm12sE1JIE9o*hG1^PkxA%j6l?QMxN5RmeNxYZ+~%bDhr(7I9NEQk z!D892QQs`2LNP#|n3y1GSaiArw`nlP2*R~hrx=5X%@M`hhicPn^)?e*la+q^)B8&I zAOG14?NBtL@-I6G6TG*6pRATZPca&UT6qPmfI8C)I)ft8iL$jFe_T=2$z&JFI81x+ z1Vthz$u08`!eGR;`+O;sqJbZoN{5$&D^lc`Fn1nM%^#7%xL8IVTkew8CUM zUb2&O$?(1}OWBhPD9eMIr$`_GJJ)|V7wf5Usq^#qA2joym%OCcf}%M_iDgwGzWDgT zn}cZ{Sp~OKEoTO#5rYpO9QKQa8QUkS5z@rMVg_-S+_BtN~-Z_fU5%dn())OG#W zGpg=wJbw1#}C?dHv}W9mS9yqOu6{3lPpD9&;)PdD{)2@3=ZmToF+WFVph4 z#22}%BHj<*o@xBSx`$YzB&Mp`RAid&x~Dr1i|MBm4pzkS?5e|=v{t6xQYQM)OS9Zu zrnz*w|DCy6XQf_BjW!Q5*~Nj{>I`!vo+P0H-WqKUha@RthETyY;g^B}Gn1c=xBl|C zJ0Q7>yZGvXBvDZZiMalB)+=LT47<_j54d7RB=&jLUA_vKT$Xh^mC)AgG){c#77S|3SPLmJb}rXpvm z7F{9?BnOqjFqs5qY}fbVl3xn3mNX@hq^purMbSNhr`36Mp}N=+RSIu3QaFjh(a{OD zj0s7!O~CRpQh{VAfcE%N?<&8(uX;qp63}R98-XVUem&ogGSet(wjD7EpAbu%MrM&T zt0pPdl77WoaV}B<;?|Y2LKYWR}e*ERjm#2Gs=ZE|IFK;~~U%dS7x8JWnzW(ui??|## zSg_<2Oyqh!$>vheLp)ciag&y72kO?O(% z=)r1TlnS-|wF0f2kVQNM_lN)>QSL!4@3j%#~uS(PM_0~ZWd+Mm&aB+3#CFt82r`NqTc z!s7MkuD{@$j{9r|D8c;Ij8fG_My}JTYh@%YHe1cAdMi|dCx1ekE=7P&xDSn__8($^ z2*$fg%3~$;Lph_&CLdFF*HScRkr2s(^BLG|8OF_Xgfq==2*TI$XN|uHm+tL#e;PFW})U7aT=jsx7%t+$8 z!5>~ew^bF{Q&&dRVu)Vs1oMj+SGrCr=oHG?Rz|Lto5e;d)fm^x z`qjT!ruq1yAlwDt?BUolkL6P?Ct(>j4O#}z}UIFF(#M9(7t6 z%jtU6K3Z18Y-fQRjUpd?oT_+zX>K7*V=NZrqRxs;(x)Tp$^k>n6)DnA^rQzR@!(R~ z>Hf_Q37S$mY8jb9y|(HVza5fX&k^eF17~fu>lY%BTY0xM*i4_a7lquK?sWBg_r?Wb z_G%fLteCw6lDoKzuO3KRPHSn5{0ucGm)pJxeS{>PV9DGb3bWDh#&%F4fX{1O9Bz%E9Od{@$;xF0WqC^4M55pKyAki>@ zm_SO06zbxdt#Kdg2_96f6K0v9eNasA#VfPu zmGtVjge3Ipm#td1ab#rh#ReZNFvf#}7lR3oed#8lFk!(2%(Q_(8#jlLhQbgi6N)fHqR)joMNZnCX9M}Cy6wE%$Da2S2}PR*N-p8Wc^&tXOgb|(>U zC=;0=iN>-+770?vtVAbY%x~Pecd)npB*~0?@bLKbUr%3M{V*PH-Tic!IR0vA{nE<7 zf9$3?GoVp`mIp-(e%X&_mn*Jli=IB@U;%B-I8IkNXqp;_K_X!FGMhv7;DUpTs6;qU zHraVr-xyUIL@m1!YJ-;Ycpy{p&`Aw5V!6OZPBHub^xHp4=r4hN4U+4+u0K3TA~Kh$ zc~rJwFn@jJ$X3>N4UwF(1iJ|#)+8)tP%^EUNHiqjHp8t9R7Qt@q(ukd7M;(lnK=0V zO}Z?t2EtIII1;LqJ4pg|`{p3;hF{9IwY(;FNWtJG(Ud$%(^L(ut?eSKz$F$L+I(xl zE62jex#s(I zo1;9@Re^HiQl!3PdhKjK+iGOkO^HRl#KWvmX}Dcd6SVcyLI^@;$K?5==ex|4b4kxe zDx!6yLp_33)HN7*=lO1^Tcvyy`JR(&SH8R3Y{wzLRE$d9j8m!C-^`NC-Pwht!;AaL z+^+zkj-ZO4T21(I4(-$N=ijWzHo5$$(x=!7NDI{(wc@jBw$)dCLrgIO5vK?U4^v2{ zj^?|IjQi~urwgvUJbiNZ;OxWSFRz~cydUcv;?0O3=x(~*WnM2s=ySGS5&}^Fv@dZI zUEx+&)YZf7hGq%cOx56QJX4VhktQsbDie#Ek;rl^NstMu_x6?g5i#^nibf?QN^SM0 z;}L-*R|;=lgXFrd>kki-B(jebIo0F}^N%M4yD${t2$faT5u)~bKnIdfyHv+jneuI# z@Hv)oBu3F(Su6~EE1k;?qv?JjhLnYnh8rzOQDN*FX}y__B!AFP!gz6;6&Yx8U^ZyS zY5b99&RMRzuB9jbi<2#Urwj7IFbyF&32bV79$1yai+8WfO$WI+@JB`ewX$zP){X{PN-N8TYTQuEsyU zcy{OF=X9&UK#lv-T(#}B6^#=rP1Q{=by_Pajb7{Ar}4#qjJzS_EY4kDjQ!$&1!?%_ z(?qPK2;!?l;EWV?F|CDb{^acIn}UB+(i!Ie3M7C2AJlaVysrN~NDxd;IzW-djd=S9 z+*wtMr6|<`JYkMT#NLHx2@*}YRJ2-;ZKs4Q5%{t$SZ}?s9&d)%^hHqo@+B^ zHzpfWHB*d(Fj^)XxNED)aSf5;$$ngbC$(jD#WzfzLz9V!T*;<2p|QSF?J|wPpqm~w znR4Rx(|jqgcHdr}7=~fJUmtb^&jefRO;+hBxni(tw#h05kODl7bU?JfLz zpXBu$Q(KoL3SS^K4Vjasfh?M_Lv>@-^fJO&gyDCOr^@j zTuFZS*7B~lfBF15F&{DJUPH+M3Iv5_Hf=A_TBkzF$sn5sJK}O2?3Qz+8Oju!T2@wC z%+e@cRfc_~x$=D4lBnKX)40jx){WjlB3Kq1_YWT(KRr9Uy87|*+4z$UAfkyT@fEcmIV4 zh0Xi{vaEscXTI~D-~7CB48hAQq3;O=cJ#YhZrknk*C$cngO;FVlTw}^cUBoHfJJSI z!aRBTk6YrZ6ymROO%aMHh>)+XW4U+5eD}cmDbzIO`hZ3rv?+8EFmkGsSThB&|UJ9>y|F9ipi$j9_x|YITYiW$OST z!n>Ny5Z@#`9vs!z%r4`9>l^=c2H2y;K5+FLV(C=c~YZG}?P*wtBGf--!6)z;V& z+-*2~z0%*iEVk=v(UA{}x|&m|dUh^BgVJxPiVhLaJ59-}bt@gifX!}DzWTY9z1aWH zzlQ*2IO|}_imFPrcwS=`@jT}VOj(G(0JD-~{K@k8fOClSb5mR$wkPtZO$Zs2QBoQ0Zpu+iHJU0%!vmmT0TE!jTe19gtE{n( z1|wo1!C^37O>QGeJv`4fyNw2DkP-nrXOUHCn-(u~T0Z1N+6jZ@JQ<}_gV02~x#S9! z@rKku9LTh_cS!;dG|Mtuyc*Tv;ju{_TP6e(ZTNlpa#UG^uXYScS?qXmfZ|p+5Sg-5=zI4$PD9udOq%jK z^-_HR-AbLcGcaSiY=*6|Y-7zR=AY$b$l=8y>qeMcAB_**JrR<_%1Qh7Hs7T^8-Cyhxzt8` zR}bF4Zb0N@_Z-tau}z&ts*zldl2nq*HFWg~@rRq~&HZYMr-T`*XquD`G0ut(9M@17 zKm^3ZwqY5>z&$i8I9wkilQ`Z6sEp7uJLp^6=-ChWx0PVKyWDqs#z<|CtYzI5rBdg( zHmM-eiKhqvL%YWv%vNvTOAO1={);4F`NEa=Z$5nZ^S4*WpH~NC1}`y%DcB%A-)gOg zmDcF(PBAczZlH;S_b=z9Zu?p$ZO|UR2FW!@uE*C860xj7Iq!Q2vtTs*2uzl<4U`eE zfES^1gkq8itdSAiyQwV&u$^|VWSXufm{PK)SBleSAJku;yIsd>f*uD1Ku|2w5mIXt z&>5E}zLkaDj)MRTjmQ=S)~oV%4c5kO(C8ch&H%$mBLWXP!Gt4R6pX`9FLT`tTe>{_ z{>-wh4?7xBMghi~J6T6l#GGj$mm~;LEFut<@GR@z&sx-hNrVofDB;5JK)0P(>oKHh zSxKiE#341U+?@fN&t&3vjCD_82suEKDru}PuP7%FqGZ==@#qJCxcy0<&VSr|k_QrP z>V3AChy-o3 zo8K9XEVr0`I>S@OQWREo9gxL>3Svki1u&o`Gcorfs-)pa2H?XGLp4Kiekx7WRYt4CcNe#z~K9=@l5>G6O=JO=DKYDbO zaAoVe|M`{8{lA@E9j}CVyn!B$ez{#6L!suKy!hu`^9HN7tG8}AX+CXSgX9_{*W;@P zNm4xqWNyX5B{Cr&FA&Wlk}w1VAy$`Y8cF*nDztD`hP5q=W;szs4oz;mQXD)Ph|ZE1 zbSL1j8S21>7He7nNV>P7&NhOuI$0ylT&&=Pi+uo4X(KE*Oj&e@F@mW>LO2Rd6WuR| zhEB%gZkWWfu}hh6@yXs6e{y

36bQ=jD>pDGB(+S=F=%`6A&URzN^Gq`Q`qgFgv& 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 >MD|D!_Z^r9tEXV)R3MO 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%0A0?kY>ZG%iXy(V>!1FTbjlf7Rv~VJ=w+8imI{B}3 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

)L((9ifH?}B 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

!F~r6R04!n3<}%&h3~n;l*9ygWjT;dV=% znDcoc4yh25G<=k1e=&j=&!8Ws9(06h1ekF;*s$%wiDFTK!U(%PFW?Om9 zUIErFWhZ|acA!GRg=ahAF%$wo@djp<`mpB&G;wu=J)M}CfL>egzF7dJxz+JD6%VJ) zmgNVGZ8#sGZ*n@VHyWf(?vE;Y=J&GJAgb4>EVZ7VXR4XU=G&9qvyK1&3oaEpU^-_r z=1;JIPa#eP=>yhk#{8Ke%DEFbPVrU=Ijx(;%Yp~RHpmSZr@|F zeFc&gNLC>E5kTU;{2*U$*K6BxZ876b6$TA0q1G&p>2CP_xf5YR<;EU4L>^|jCHn+( ztw<>Q{Z%f3DJ7~}iw&_Wrdzxq3ObIEBy&_DSYvHKc9qD*ZKu;KilRWsBH1-LHr9?{ z?1wnQ8lwP+L7*goAA-~-U=f@3?*g!6+~T_)k|YP#BRr$ScvfY{*sZsx*pMb~nX0g+ zUm?5bh=o}5!v<6-umlaiBnOAu7F2WK;u%{?Vp|BBD7&s*BBYZq&?t7VMA zMfxQVO#ff4#oB~7`fQLyKl`<$3-sF)v{A}gkh@i$=LzOztMH# z;L=jTx0LG2Hul&cNnDC;{Ci#@aj}VYS%%q-%Z#-(@sqo^L2`cQrBAUI;dt&<=GE@2 z@y*}g2P8jTfn)`e6-fT?An9Cw(C*QAOJbXr^#_fZE$tTof`>a@KfVpSNd^(mfUJ9U zK~coX#N3dm5%S#^+NjTd(Z*D^^cU-^C>nN2lFAgnkJ4mJ)NO&P6MB|%re8!0lAJx? zuSchqC=OWVaW>_r9qs`*ss<2 z!w18s`9Oc8(Z9wLGnPD(kYsCQN7m{hEk0Pr2!kyffzWE~L$~t&;5~VLNV0SfO?OYp zY3Wk3ETJ_EqPe7(UJ6Y}4yA#xw;p=PrIbLg{eDK$Y?_3mX~P~g2uWkleDfpto_XH) zd7oDm-4|0T!j5rp8f35sn?hP_(4!uo9ud+rF)BD5<{*?q#^aaIW{G>XgLJYML+BR3 z8O-BfUO8J^wPw}%`N#{+Pp`>}qr}v*-gUtpKT=v1FH;i4QXnzVr+pVmZFNS`&#o>kd>nw%TX@aVo|KYdUAtk&&I196}kYGKx)548XZJgn_WrD zn>=)dG3(#%x7shT;u{v2oXl6!iOqLS7@qPS#x+^4fU z*hVD_mx-piaU>gbB)h$9uA)Rocvfxf;wvnY(^E7tT}MeVdlg?+X45#(Uyxc5&{>wr zxU@gNmBlMfUUtqO+oulgx-?C;0H7%>U$4_$isG1OpA#}Yx*R4-3dto_bCMe*MzDJ2g`H$#NiO$4j;gC`TXeEra!5F0!tfR`1p27(p zo}6$`wQAUR?g;^JH)Jfgwas)S^5%Q2Ts>0Dz#JHy7c4{Q~cLpq7hB)~1%Q4>9TjDfao_$>+;J}%uo@p_|@Dx1V2+W1ctNJ69| zMM)${x5y^3BLVhdoZ^jmd7BciFTXwv$JjrkWK<-Qzmp_UFCp6RtJp;7YsxJ4ryBDj zDYdwEwW9T{gci$CAZY_(f@_HS^m*}!7vX@JlU!mg?l;0}`g*-aBj%YOMdZI#Ct`#Q zLORTh9?9&Awaq&*DFs@XqKbj!f;_E&FU-fzuKA{d-e{0{tz3KjTS8D#O#4awKtP`I zW%cOMyAHI0c02_X)XlTF7e*9`g2hd6EOn5S5I9nPVFFZJ7s@l8o!kbor>fzScg_`4 zdCf?$*joWfhY4Z~Md@Ot_x3+V4Hir@>L^kyA}r^{P6?lsh<7|(f}k^8X6j``c!v{E zj???n-M8-jDinwny@q_^@qn(9^rhB9L`uq@JCEiL@h~wMwPDuXQ+`bsVkB<%@gG(yghrpt;&QvJ{=Q-f(R38 z)$uTZHOe%ST)DNyH}xO5DYklo24i&a?OwZjd_wp zU|-w>MR(81LOl(_x@vR~~UeB@U7A2ZOzfeMl zf@ke_Dbu`{I6Mj^V?IT}$&~3E0S{Y}Dfa%yUn0jvOH_YsL3RDGp!(J-b$3${I|aAx zBK`KT%xe+((mW{gHY0W=cL8VSDsOxOpg~^=0B7#MPjvoCnM!#wfiM5{v!#Va%EBUL zG3M)Sk)kA*f~$^}bhoJjOaKwV81_KBR-c!wq2D~f`5~#uluRhWLQ3+USEJ13mw^Q# zr)TGqw8IkUW>+G9taGbiRiywz2J-^M0&A^7(Im%$U(G(HBy^{hAl51y1hvMz0$a#t z{leGR~3;5jn!tSaV*J3D3h^EY5{{7+>Z&HKEAK zth=TXgI9XHd$kRukT1AZyT#pAvKqZLtl8C`i#AEcnI24zuQD9D+C9BbSCh;(xdMm; z%0tPAHs893(EUeGL}U!CxP;Yd;Kz2qur*{YdSfUV#}9YGKC&y0UMt0|-ZjjDMM04@ zt2j{S=Eaek;6BGlirSxL6X5{HxXn{W+w6j<3)qb?Rw>O&jCdZ5#mTyTrG-Vx z!Xjlc=8qrr=FL~$zQ@LVO!4jJcUz<|8+pHYS|<;+E31P=m<#{mo(WnRP@K(p$K>pt zXduTNwB!y1nCU6;E^U&y@tO`aM(6MHh}BUiIE4V>(TfZH!p)NF9JAi?eBSfYY#r0X@$F>rGQEZr_mdXjhu2PF2weq8#Ru*C`z$Hj??o{Ow=fM8rB%6 z!t{uepN=wMB8VwoSzV-Y+seWe%ba@4!?iT9fvEns3Ay*6K`(R1-niTn)h2^$gv-VP z^|Iaf<&ZyDE3seJzgiC4LK`enLE!00h*@pf2lBbc7uMIJdEaI}o?qcea`QGkXzxM8 z8s|_vo0%m&?q0wStaT{NthUq{Xx#Wk`D&|a>~}^AEX~`xZ+e@yD1RTdjJHp3y9*K_ z?0@+LrkvGDfAZwP``-v8-)52WU#hnLI$M5Z;NX7|hWdYmwtiqV=esWcCpppY?VtQ_ z_O2zgk?e{ZUJ)KVUe$vtl|ELLJ|zqNKS=(_R?D(CmhnO|Mi?yH2#n;i2?qoMA0Jrfv-!~8cQtn2~)fX0i~YiSKx)3aJRY}H6nmQq|1X} zaHW>W@T2E%8f_uYFRbv=q))G|X#d(C5Uxr93JiuOQvxvFnEEs?{7LKqvFqAT&5z^JKtY9W?@$;qe3@EK)O9!~>>}}6lDk=_58{Iicj9UTzX^7y& zRSqP(y*;prEFR{g_1(?>?fTU<>z74P)`2)kS82%z2=JS$ZdG+!z-?O^97N~G%0;rR zRTZC3NhBam2Xh2?=gLHIyV7NtaYEfd+ONUl(efEvqFsXUO68Y6}zB9&T~ zTCa2@RT6hOlJ2F{Y$yKy=I~BOgIgUR-fOh}vn$plFL zFM(v>+};l)!#|uO+=K6`wgOY5)v#O52p-`o-_IuJ-aJ9LJyLFoP`*!}5p71B_ECl8 z<7$;ziAc&F2KoB)XM}iZme(R2B_ay3sNdeBGllqCY^_YG&#qu>(ge)q`OVOlyjx3b zyW8bU>I{me&}tLWgH{7Bu`_pY#B{F2+d#F$8oYADR_`-#e8iTIB4llA*tLbHIHccY zT{6zfFE7yaW}z>V(mAN~>3F=A2WH(xn!Jpu-$ML@*g#n!$6|ItLXDt1=V<0;07vMG zU!(ZsFTm~0qOE;2jSxk`OY1STwf4V(Xp%VcIpZ80Nv2WM?H<-|HvqCMZzf72evm2C zC6t(RE#R2~!#zqe<#hbKf>)WV&2Tia)9Z14YyD(K#cHIH--2XDpQqKT9N3ZYtjag*08xue-Ywc9lvw?=!i6 zKwE4`GyL(Z-QRyd0g?%jOn~Gk1d_qP{$X$7An`*K5!f+sfJ8iSf#kh;BoIJ8T15Hs z8ut1aIO^^(cltu~LGX8H0tJ%3yqO54W*G~$ncuZLxj5S^5>f<3mI^dGlptc%q#g3m zQ^Hh6YgRKHP=`g#g31qDAi5e`@+*X#9-U(pa1EWD4Ul zLTvMfZLUC{tK3AJ)#@5{HMS{#PT(~RQ?6-HbSUjr5(w2V=ogL{_sSF>3f~r56*NZH@o>a8c}|PdcnK9z zE#|fJdt2ptzjN^zWW7nNP_wb6Twa*>*bTiyA*`sQQroY`lgrJJwJhVHj+=;=YROas zpB_K@n&@WWJRpbjK(ot4&dVNNgUHnU0sqXaIaNXEh@ z_KA9gVmS2K7akKWuc9VKP3ritJQw1H%l+S;joVJbhq{DTiGw5kI;xKCKgy$Q+_qYw z9him3`Ixh>h!y@3=pKr#-Yi-@w4WzM89xbhwm1ME&Dk`Qwqg1UwnL|`Q`R_;_*uF`0&+B`rqcSGZ}f6GdYSJW1SJECKbs9NG3q?-vY@y7?8JtWE^YW2PD?^Xv{kj#wcF{b<%=C_b%3N zuh!Xaoh`^VA^Y^?*dWU+rZZM0NyaQ+nv!WxhD?XmY*H_rSf50~UI$1-|4IRNU4!tF zaxHBU)+sQJv;ZX3}?zg6bRYxnFi;;rnqY;Z`W2R*DY>XF`+dmMjx@#?)k~SX#2%& zy6xZRZ}ob2lleHBF;(p zqG*0oHcVMgzjELGZxSzybZ#@11Kar!|(Zb)fq*6nEl zhth;9=o-f*Q{}MG*ruC{KaEp^q8u~d(2Bb zaaK(Ra|Q^oLM6*$l)bi;5@WSOMAL{vh7g0&Zb2c-g!#a>vB5~ET(CkJY8ZjoTFEg~ zG05?SAiAe1i9qHq!ZO0`lC_Ayida}}fp7{+kvNUxu^>b{p=2H)gn}^@YLquvagN~@ zUm)XE>^H_G!8qfEVL`_;{)(nk|6=cILL0g6_>wn=2g93rFr$$)V~r(^Bxdw!p^wqm zN^4*2MF-0WVUTPwAY?115M-CIzO210NtQ0Ose9U8S_-}dpUR$c=|U+ig#_ALY1rFd z3L%%Wz4yKOlAN@qkQ8!|4@MfKrx^*}Z~p)P?*qAf0|!iyeYv$J75brDRg7J^W#WZ` z`|hk!0iaAzB#2?L2KcNNxZoG+&4>E&(~*AgrbKd7A`?! zsgoDXoAYg$a)4|$g3z?wBw+)}QpNYL0W{{y@ts5)+S5%M$hBCf1x2c5;o}rkMaV^A zx>D{$Z}txE-+zRkk}~YZoQVW_(h~)Q))VJ?NmL#vaso}cL^FVQXnQLW<|IxPzJ9)h z8#Ct2YIKy!$XKdFm@PGV%&awpnezPhu-z!DIi_9D{QcJ#%y3EBe%+JO z45EzUy(eG&<<7qalK*0nvTALufMf+E|MTDX_>pb_iFk2GukJ2xZw;I=Y0DmQv2iMC95p+5gSNq%~}#AVajW?O>C-fo)jB>ht9PFfiUiBWhkIL zy76HoipG?Tg|=B}CoSgt+Rm0H(E&U~AON&6QbWSGNLh}01nCDT#p(;CiC>wZW<2e7 zx!6Ut76A_w^WL3?{Sp*#hnz_U&_Q>^1lsh~=Ni!IMBU4CECfRV?0E&8UoFL)fcVI- zN4k1w4&l^y+=9@8jjm_Q;wn#~FgJ!BQaG3$UtR-xo(lm!K&2EsJcL4`Ezaz-&bFcJ zJK0O71(?fFu&vVXfUjm#wjKSWmm}KLo3_T}vgAJo`n`v76$~Yg6X#iQh6(}J9+m*` z>^ak|Oa@C#tzO0|%4c<^#nk<*b~gH9VrOzjaKzkZifH9bwy!o$&<^9KLWkm&p!+ZA!g`<93tAGwRT{B_U2NT8xA?ogl{KOl(VeD5Ac0d;(pL0E)Owv9u1})|@w8JiB^Bsa8Y!sbG}z z=OdW4c*!I5lDcXOf}-!$$P-AU(2-RT3=g%X8Be{Hcc=Fgh>v>r;P|9wSQ|r&o@iLD zdr<&&uSQX|Xo>(5YE0#nJvZ4qYg6bf7brt`h~FQ`0>=R{r*NDV+72VN$?8GGO~Cs3 zRZd~H%|akkZ!?My1_Q20XX8^tu6mif2k+hyAZhGpRI@W+#tSq#X6hA2q&18m>;Bi% z6eoOW3lcAw>D#*!Sk{>^i*sLvk0O0IQPL^IF6{P|>^Q$jx z$`IMcLXV>w-EMw=`VrR3N>eMaY$Cj{`5jI9ES@S+`HR!TITl%*BxOec#aNLS1vw^& zqB}DoMT#uN+NFIQLFovsW$+eOu)wB50jvGqIJ!);a@6fKbE!dNcp=E{YOtz7S=+q9 z7%vG3QOsxK=BbVk1f~G)0;qsuQClM=l)od#d}nxt)FY1&!7^6_zXTVkhVEUzb_M@v zGsrCiiM#h3HuDy4RwljT7{zsGqu$Xhc=9WPBgoeg#@55+_~o^?!jr2{r*KB2vEV$q zkk#XMe9NaU8};EFg2}=Y3~twT=b_qxy?{pu2*r2n-pPS`6Do$ezJz6QEu4?U<DSkc8*p;-${w}fSy`m4EK*i${>Uv-Tp&R(=5M1xXnUZ89Dv#Ny z4dHdN)BN)kwKa^73n?}nGVD1>1Gj9H$l_is9ej0ytE3je#T-V+YOsPJvoTRj`15xm zfS$JV*DL@8Y`Q@@Yd8gr zngm90!9W-zjN+(pG~YTj9xFqoQB!LMS2Z#m$3>-#z#$M7Uwp7MSzk|@^H9lFm-Ai0OTN1@!kt7k~uH3TIq)KID@ zv0+jgAa7E#%d+yxDLE39FRw?Y35$$Ln+OM$RIO01zIga>qnAV~}NI5VB=m63UoAlKZI)9xnZl0&Mx)!p(=$T*)og%(Ij0^)h}^a734P|9{m5v_0EPWbe`E?^NWY!V^sQNFuMC5l2k5=1iV z3$QY)D73@QDO(g{aw_H#4iB*}Xy*3u2Nl9d=zc9du$Di^wGP53gAoK_0D5rdyamiO z!Bi@U9ypgEUnrcGW5CqGm-}2?eY&yX#5gr4!4mG=(g?yzq%ku;-wBR(bIp9FOD5uVtcFJ z!&=DI$`oL|4*hnGuSjhhZI{L#$e_WG27r?U4NqCMU+EXf`&B0}!&&r9lS0pz%z{{qLWg5}$o%0DEDoxzbJ>pM(xyv-m zP3CCiw98?HlXY2OMCE+XHk6+}o9z@`tvRI4tQ2&_Si+|2u*Hnmm>7Ux_-D;0T4&}*n$!A=vUWb z%E-ig6iLo_6<335!s(Tm#_@M=qlAitk|TkYYAEeEZt+ME66BGpvEqydE36{%1YpV- zVIPa7&&ZpHJ#^bI5oCe-2by%+Wj8Rn#so&I(d8hG%L(Q%2f=HKrBP&vyLD@HIN6q{9wMy1qa12gFE1P9wtGf zU&8~HmdYd$mSNJ59wusu7LXuXheED>OatBDTTPRWkyzWLgJTJQIPhcDJ-ofM8IcM= z2?3GzA`Yzq;22CsA=vxPBP1;o5DOA8!cACng@`hp00wiTYZA!VWv-A;Fn*>W*><0C zAc6eO>rFaufJSvFkSofHtpjKes~rrH%{&T0Z>q`YItep*U z?g%gkZ7+895O3lgn~un|9&$Iaw1#c9>|cq5u$Ii>CeN?2ywBt$9Q) zctL8FdA^dsK{b@wr5^H%EVH~W&!)KqGec3pn5H_R07j~CjuQm8N@b{>79uVkv4?(9 zI*%z9XtbjM;&rgk12~`hgml^6j*TN5RX`l#3PLG5yhNRNK^3-Dpnh=3e}0{d>Uzj} z(X2+Ktx;wYG49_?Pbi|qH#q~r4}fHyYRhR^ES&~DpoZ04lwjbY99 ze2S+1@*ec(UEdV?W5sUz3TsX>0o(faU^%n5 zr`K{E{Poxw{~AA`?UsZ1<>co}3m{pn#rjx)1dvmyANt7t$IdG8=2m3>40hMlL#L`m zPw)_4T|Bxpv#d%O$1^F6n*(g9{RM^i!ZcKjvM1)HTvEXNXxLVZ#&tCsfn zaUAo0R+bTRtmZBHA_(D|myoa#j3gjsEJ^zc1bHXfQJwa#1^?_4EN`OS=zXF8qU-=j@4z$JwqBFXsLBt*tG1Tp|Qu$m;8FgbJS5rFHzpNM`Gee=4H zBKYwrW-ED)KyTh|i#D&UZV7ck$roMAN2@IpOQ1GtHk$a}#Mk`gEil_%Xcb$OEQkQC zA3s2zP^RGFqfsq9>=`f-vEEqY!FgxeAxy_7(qHY*m!n~tX%GF}>Y947qb@ep=&EH?3WuE6|opym&k_f4?EaguFn8iI7 zOBi>{Yz$=-jv8{=Jrn8TYXT7ku*z<4KoAImLWsbLfv^P6F`mvUY*tT0wj4;>+Ga_R>S&n;(+gogN|qgIc>4hf_A`0hL_@6&xTK}<#^W@8l^UpoDsm;~C& zU3Br~@buRQKXyi!JGE}9SsbIuDi^(@Zr$l7x#E<3{91bv6_&Vgbxc?|#1-6kBCN7L zWkWL0z51qPI<<3|H@96m`senazn=lgEM{?2fTT7D4GQ;d<<Ct_6wi`xfk#((yOe`)PqPP1 zuIH3Ogd8)uE9P|tj0%8x0GYpI52QqqQVXO{X8&X;B-khjE5hVqhfr42jR6UgHPgS4 zy)SZ|T%d&CR@z0-ZC66XMH5uaGW$Gw6tmqHy_P=~OeKvFJ#cG4Pq9i?L5v~^G6fMCdp&AD+t29sBdZQI1yuWmI`vb*08T48t!sIv5;o zqA=6~8XjgHGnS5-Eptb$q+Lo~E$>2qP@o4`=!h1Wu_MUj$VQB#XWmCCU&Cj?^R3B! z;Lj$LL8hOAUVpZJ^5nr_c>1>2{dROYYGf+S;--Tp``lX4(;OXNxz?m9Nv^#bn>4I* zQ<7X|%ObJE@twG}h-r`fak>L;>;N?t+$2nbB3nmL@>s>EHguP|_ z5MQmZF%Y&qqo1~OK_Bu`Qc-3T?Mq#Nf?9_?&Vx#xM2+!vS=Q{fDhgZSNe3U8g6^g! zNi6~>?J{D->puA~Vl`v&Zc2X8n5+5hqr`(~UW4Mw=0eC0QpQo43&V$;E@ggVVRQ#%MJ9ws*YN{Ai3O{ru?4Utg62(=EORx69>NHGV4O z1F_Ee3*~NzTL^N4p>1XIW__Qla?LmYodL-#W^q%1q|DC^2fvC~O~3u6@q*g{5Lc`Q zDfqkA-5!9~ET+yc5GIv{B(CX#t+!?V#3i{#(30p$iTD0Mj$N29``pMwaYR=X!x02F zbVcvrBLP=RxBTWXqmS~FsHo^jKnWhbd#&IgvSJ2KBYarIzzxjOy36PT@UxCpVreUc zI(aV&QJPU`K-L-DFTgEet(O*#jH?~&a3$C~C#qXDy7u!Ft$Ka*0R|vty88x_ z9^b@Nmx7RaltmxqCXA;DA!J(jZa=)*M$M>6ODK@4J&i_V< zgSI^G^hLfdM0@6SBq`$Hrt}Ss7_6?oGoMlso1D?6PqP3WPNXNxzn$DiPg@q6 zeD&kysDIY~%Wk>2v2x!^Y>9a;&eB(fK9?LLM}}?b%euQPc*lLidmR{P=pO&U$4{MN zyUNXTCvVQ?WH&C;v4eB%;zX$RoqN9jhorDS!9>>A9|U{WZB-QOZbby z?+GOzc$ay?DiY7@9<6SnLSMQJmBgTnL#-smr3R}486XJ?0M)Q5#?E(2%2X097J?$fkw?Np zyKSqbxHY(_v*zIRq4yapnxr@W`URWD*kVO~r_vyoe>l00BhQ@~Nvfu7J!l3P&Eev>!zFCL_!QkNwk-vi|<{d+C?!CYro_`_9XigstWNzXrpypk=?(tiJEZm3_ZzrGj_9$dV=Ji|R!H>oxc?M#=z zEw6F%K|U-;>f_ATeXeH~-!-m~^E++7ckZ39Gi>Sp^EMleAAUarl3C2+#sG=xVWN#~ z{DKTW)YYl6zlz--mB63`9WxDJ+qcKzr^Cm5(ys>4T^@->B1hy!T@odcB8hDGU@g@D zMeY%O5$i6wh}fJ7?-DVNhIx_l43v=UhAwL&9xHWafT4B zsrx9b)L2OzBnp6EAg&NgA%Vm-is5lQJpcu!e3#YTK_0{j`X75|6WUgGhGE0GivyP< z9k^FlzxV3rT0+v*k5&CxvK86Z#x`C=#;B&YrHTsK8ZF#PPD`?B?A3^5U^3&C2PT+= zKnaC}Ks(cc1WI9;VX`cg4zugBT}+7wZ@TFD&b^YHmQLHHL1!g_Bukb!&;8E#e((F# zshF1PEz_h?_Ku%MnlaceI}mn@(zu2iCA2fIuVT8&E#h}{8W>BEoi^2KzB{5R3%@eo zYPk#jQ3yOiPKyU&WI+m$^{4q6j&FGhYSqj<8G zv<0**q^#_*GW#B46ONGQDmaq<8Ow8!SUtgn|AGM zMw5&B-ljzpO?IWkN-JJ{+q5y`66#s8-YJw!#DH~UO6Y$4j!@dW{Mq5m6eQC%UDu`} zsm_2WVDtV5U-mwK$Ffjg9{FnFfK!jKje7q0d_od53#!`J$HW=+iSdjm5>e5qLrHTl zEk+W%9E8%OOr76-zF28dRkC{xp3pv9Oy}Z@k|;?bk|E~EBGe^zRg`I+N@9+IM5X0- zDckrLb(j>%6b%a_li~9!^lR9zS4LqyI2KEKEBTCFsNq=`zJ|~<^aj#k|jV6V({z-7fj$SsWFni|IPRPw0_vQJRS^& zf4gk8Hr^Z^zI;sWlt0$+p`CgA(BX`v=FJf1g*VM7jE=!nLQFoI6D5v6eQC% zUDv20sm!<_JHznXZoKXdZzEbKV~nsJ#|Ei{82_Nb@8|Q}po6F(x%vK?M^=N8q!UPz z&?&S`sA=0M!(@m&FG=+8uS7NK)+BAmZf77-HA!b2F+`6@Jfb`q9+BLvj7f{~Doq!} zG_8Np2Wc)PWu&i;ID)VEF+wzz3n?f-s8K?aVM@xVj?~DNSqGeB&;up^ zB(RE4Ua`Td?q*1tdr30D;8GxENtq=G#BzWj8EvC)L=MSeZdeGwig-aI<+w@9AW7pIGNem2P#{fgsN!Hv8(1lqX!;#Rn@$*!AZ@ zkwc)`t~w26JMUY|@~ILlze@5*bXB4soyuI#l_Qc^^r}&!Q3jMY-CDC-DOakWEE3h! zeR;i3In|Xr@dm}UoL=a9=>7}3S>&5PCV{t|NC-lCvd{sa6VnNpl7xOGf+_%G*{<;k z5Mi)m8?dcff+jp1b%pt-QInte(E!43y-e6MQgm;f=2y@XL)n%A zf@5=JF~e$#D~9yEsOg>Qt8mce3TzVce3wFLqMJPCs;?Rv83p`m2;=$Kpd`}txJ8m6 zxaDJ5j88twTCEGEi674@R9XN*Fj&}BV%*aNQY3Rb^{qB;+=&Om8QZS;EtM9=3IR;iiWH=_?8-LbCXX70l8#5BQzUwl>W8O{1L6YhP#Tyc5toVvaUjG`v03?$XI+rDLhM@r&|H%DYf5kjD^0y<)^EXTwQr3mZW z7_Hr>NB3zF&6}YkM7esYOyI8SM`h^0;~4VJBHx@I!J}4>Hc4g&jxdr;WUVM1-O=qB zx!W+4xTLp}|E}cv^)E;ETqu82_h6LCN9jqFYTPpyvBh37KIyUii+XJErE{d$qJ(7W zzB6z_jz59H#br5ve1yDKgB(ydv{V{-i+5!+4m)|M30CAKi;ev z*=i`R5YO(!rg91QCvlZH;uin<`hVewwQiiBae;9A-cjda>)u6w+&1B^jTzyfAmnZe zY2 zvP}ac1G)~Uz1yNl1FA-8OpQk<3c12!CKD9XRHq5u6<{SsjQ+NF&O0XTj!$AYd^$24U!;XTp&a;<&oiN35eGgnSGn3egF+AgOH31 zXl0K&3DsbjMMN6zbPT+WRIpGL`d@#qxI zd~>efKlwjE!q>Sqyx6{|(|d<6OY56!qte7(%3=2|68tyILQ)WZbK3ibAoT9OxEvnN zOhGbT({+77;!4g~$Net7xaovbtzNHZ&O5YWQ*=4}Zsn-UE~_8#y(FYK?LPDJ`Pq1S$*zkOKFg)o8BaQx!q|uYa=&vngTb89rmM(Vi!8S%!Yg>w`8A;J0 zTRGd1oE%eH!lrafawsX(gqmf`USfP$vI`46l`Pqkg&r2#oZ4e9A)AuR9{S#!8Oho1 zWp_^oJ&dhCe5cS}vO~ zK}p1Atc4kvl)|8bv9{utiLCJkVd1)^YQeG;xr-BL0NqUxZo~_;ImdoB*uA-}(GJpi z5+L-F{42;+>fj~pXlgng);CYdW*WQSA`I;Kf+*XQxF4k}sL39YDT5N-&IM^}Db30r zCL}t`&I2W&GZKpPxR5xC#+Ufy7C2k7XObQzhv1t7D>gWT(RGAEl<#xhYH#j#kB&H+ zY?ffr6rxF%`HZx!iuY>H@(i79EV|H*=on*a`$91 zGUKof>_yEeraJ8KO#0}1;)uD^b;%MhzIXifv%R&urz_(keEe{sm71IO@?6A64+N0B z{AL0qlQ9_=2}lkDQ7HQ2g@i@Y`hPb>pN?zqm|pzy=NA4))a z6$Of#B>8RU#hhFLpjkB%Nyuq`HvZM8>oHZzGpsO=NbE^cL;@!)Lb3*Gm4?CXRh{g~ zfXLpB{9DnuA7L6ycv1EU7AlFr?+|5GEPB&uSxL;)Ccz{3XKL1?&1 zE|YqEggRlrT|!~UuPv-FN)p|hM|h7hE=nwfVT)!v6tfCK4%uq^ZS)wZumzexXYl|4 zAOJ~3K~&ex5lcaNE?LxgrNP%jR`$mj+y-X4)$A%K3_#512R9eldAeK%f495KO;s40 zAbjWE6J%>Eoi3cR$q7+NZFLK3ab2`D8`k4_M#Nxe`Lwi5Fp)hTb%{c`p{ZUhZkZih?0a6p-UO~n={|=l)GKuZ`5G2NlaA-yyD9O&cd=`iU2l09 zJCoMhfBhjqVxh@%fhJF%9lt*2++=q%Fe<_ylG^n2jeq}rdg;ZDTdzKungGdUOvXh5 zlEbMr6xu_zz|7?{t2mMi-u&0#_9bg$9DBH`9VKJ*p`BJ@_10!TxJ@W6hf~m(G=>s> z;TSK=0SGcml4UDgNb}2)z++l9W=T@hWOi4Qr3f@RpyU~vkgO93o!^)Js=O!xuhy$R z;tUeWbIa}>>y%UvTZW;6X^U|ogG)&8211eKIoaaqS{}{g4kHQU1&CoqS z!{_6!Mx!Nh3zZhvAd(7NsBydA1~+oV`Qk6l2C$GTq1*D2w8npbAMu?or1SM&L!m-O zIC}X}BkZv(X2i_yJZAJnXY<5JF559wcTRYrec2Q)^7P*BEaMazO8+UX~$N%!oc-8$@seJ+@lQ9_=07%$1cqTzFySseZjyl}U%t{ZR zJRaOxv^U2Q0^LV`-(J-C9X~d8Yf|>zgI@zRO0+Q!6>+h_+k7b`f?kb;tHjR@hv4M2WJ4 za_nZYin$a7$JLngJSz#NF{;khaH+t<=z+|wQ`|=?65BJLnW*j>Ctnd9R=9unpC%7x4Ze(;HqGJ8$P7sV#f}-^77i1Rn1S}7PVaD zLk3>k8cESf!JWh@XZz*4mo_L4IsLd%v^4v*4Uw_prX6o!CWWz#wri$aeMGzu5bg8x zxG9ll+3X4RzRmGEA{+qmQ=9=(moj6@8Atdbg5=}7tqTlyWO(Q5)lgF{~naL zDP)tufaG<@MA{Y9G^eWhR9KO6k&G17wOIV=tEXJGPKapu6}j%RCfa^;?1AlG4zxv? z5f;eJgFIg(L8r^*zxq(%h}VZnu4896uG2ggb@HMiGrmzp$iSpS-U`Z9W(4?fBN4Q4 zN>7a@N~1J3wi&LV{y9F99cvtAv`-uzDl)y$IxHZ(0S7X|BoBx?I|@Fc+rO*hP(s*_ zpgF#{;#GvxC`o51<|u$R6mx%Qa6iZB&NdGM9{tgi_Ia}}4kipu;0TT`e=+DTFV}rr zp6$@sGo@5zplH_F%Q80mWOhVh=GF#P_8h zmb3f3s{-MQr~;W9e{3vYXFwu1fHSq(nOU5QY3}^D(}SZaKf5e&6Zy~Y{CFHO$4^W& z`AzTmH9CHMx^jKFemH&Oj}HfL{&{U`0wj|$85apiLWfi2BPd72E)uVPD0C^umabWL zxqAO#@HFK{D5V=AE^U6d4d<#6S+98} z6TAMcmkn-~j(co<&6N<=0q2c;8NvLxc~=@ig%d&Xv`p*k3>NlKkf9I7H5k|2GkNRK zFu&UN6TT0{cnh?M2jOk_lZpxnG;A>J;y4nnk2b|zVf&Vf$vR35xdgsaDe~I84@baq z!T;MALUhRUibp1tN1vX&<@V*`Xac~g$p;5V-QD%=YQ1Sk@|JkxfW<5fz8^KGrmTPz z(B1jBZNu;h_lQjN306HjIJC!0Kr_ZG@R+1vJ&5x?!58rfG(8fRtIC&{z8? zxQOc`ZWO;O>ZBI{j^=!VfZFs~?xs*2OcZ3tSkjE%vN0WFJ5GSnA*W0rum zut4(63hb>(Lk~Hom`)A(P;J*2$%gFb%90h2Rqze@FVf^dA7_vh@sX1N*1$RIu< z9b}{nEmB`?a;62jf}_@h<5t2T%6y?HA$-$FNenJ>mNGtv;=Uid@cA9#036^CE1X>r zzB}Xq+~pS~%u0wU>s+7i?=BtS((&>{I`T;048>WFh`T`e)+d6SFZhK|H1t)MnE!JqxFAP+JbXaOO6=9 z??o+tEbb`Mk!6yZndoZpSLlUKnx!Ci89K5w>VIG^Y;lkmD#Gw3pr!eO;@kwf0nN!|mlN z4>|HS0=PRL*ZG&kmVt$QpadfU@&Ou-M^-#gUJQUPwulBq4|AYm1P^TJ;xD5{SR@TT zKZJCQZ?00Aekn*#)aj$huz>1fCbU=qeEczr?aA`!=zohN?sC&j{{HQES8qRf_|DBC zG#LTOXpPpJ4J5{|&q+FxO*EP?;SKHZgVOYZ5e1)U&uYStzU+xSCVvR`kbQ80!%W21 zyk)zT_ubyxhpHOPPjwcUl9(u1)*8-gL1(bgq()~#oxoC%es)<%C~{7ORZ|$DwBu)n zK@b8H`>U(iKEm_UXye}ul422;Xv6^e-ac?Mni$O)`?9&&LzVSpjY|Bg&V#?D|Ge{7B z-fvYpy4}!q4|iv=_I`cZ?Yee)9K@(c4Q-*FH5#VDWFuPh_|dZcam4i#shdE!AgXuO z^wyf170?iIpn|iqs2_^_3-RAg>yVl0K)`^Ny?+}O#*{XTd-Y|XgxSk~dObiQ*QE<5 z|L9%4{p_O`A3eMM;L1}6O-4X6TBG%514;7YxoOEUrV8tBB|j1qE`+eT78NCqAD@3> zS%(05A&`jFT{|(#@djyqd~|pyGDzBpVsc~w@xU|{m8Rp_a>)=TjzM(=H`K#VPXq_y zKSn6yWmc4`Naj1D)}^XSRZqkj3yO$$zNjyVK8EZ7m#pOj+-}ZH^S7*4oF1<41&Wjf z$}G=|7G4t13bz#d@UHtAyITiWT}fmaXd*QcM8|-|83#?! z%}eAVXYOTo(96YZCub6T^WM^XcuxlS*6P_7w3~9vOPoI3-CeJp)t&-g<}p0js$A!L zV}M85oD*E2=N zZ9^0$?$BjvDB#`!lN*11e7S={TsXXN2!sOZlYo6B!TJ45hbm?j1JU$FOSs^R`>8*b zHY^dL_?9Vsf`jDmpn}^ zzz%=T`JV3J1T$IeZU;GzDAuJAWPJA9SnDt_MiEnvzrFU{hP1%^PK~X$omrC|T&)!v zf}419oQahWc3%qZuyTz*@{HMJY% zl*GUYO?v$_t!39sxfKheNneiYc_F52&wH1A{;FfB)tprg4kG@Ay%~y`orcIkG=iF` ziit&2p-jy)y7?hL`=SmI}EWe27RYgpBOpFl?p+e@uzg9_Ei z>6T5$hwx3Lpgevo!>}TOd}MZeWl_+R^oLR;(WT5{Sqvt!Zc2!#yB%E8!T8y1l({DyfO&COJ>Nh?8|UT4dfyyGr3#L=F#fKyap3wvx6Kztsd0w>!b{a?-|= zY)*d4salh9X2WDftDKGR6~M1B$WeoZ>Dd-<73_rJWJ2s`>+|2dP8>P+@Wo)N{fGBI zdCIxT*WZkQWVA->%?6Uz(K&suM7*vkUu1o)@1BJ+yh)rg{LnN`_IuYM1s48{c_O-7 z3W9jL_S5IFCQh`qJQ_g}Yh?#j1&vLtT9`;h^TDFQMAz@-{a;lT)l^MRJSb`r12>}o zWbf)>+eogkfoUEv81VtS{9AJWIOJlNq(~7YMNyP#nhhO% z*uu0y$T4`xWJ+wCtaPy^SckDUv?d>1DjU--Fe=kf8a)3xbO5%R*l32L^U>0^4xcl@ zRVt9Ft$`{h7!x@C)He{QY&7GBme<+9DRl7l@#0k=gFnLh(RnbV-8W2`w$b6GZ$_C0 zp4yW=0Lf^BG<_Sjad?4B)f2Z}q7fGkA;NMDkB%hrwZnRi299IJ6vncE`tW*dyJ0RY z)$@8$^YE+pKuB(^KmX>JznB5ZY|Yk(4J2&)4tE=5+Y1vwW$s5irv%v`28xH+lU+X> ze>(aXXJw8pZ9vlydNM5~U;pQ{h1@bzxDsgFselOGrfs!o2$JG@CRG%J*@cDZzyH)~ z6*R@Bx*k(ddN^)SRFLV7%of)$#E~6YR!K4%)#&4CuPoj}*2433ndYis$C)6+bh3C* zR##18hyLHH62;4$C9SRVf9-RQ2^Y>&v9>OtYZdf=cLsNf%5;dyJdFWZB2-QBaGBUi94 zrTMon0{Ld+p4Eo5<-uO%5K>9zoJ>V50qAt&p??;`vw00n7)(8t<4l2~VU$+zro3I7 z6jk)9xUj!``hyaZfBf!yzp*nQnXTFSkb$JVeFr!M{io@etYjZbQGvqz9@ia0NOL-> zqy6sN(^nv;?z&Kcw4#FHr+z-WWXO?oD=(*x=z@_P;IgV})Gl%~lax`#DLXNyTfco; zV9cbtqKdO3JJKtdwHA0C1wlctEy#*&?P?%ZXp|``jqE=T`fM3=7lGDRI@qsn@a172 zI~|!$g+z$Al-vHhv7KLka6M+lK|x5HHXqMM$`*$<_Lm_Xrz|AfARD8N=RlvGF!9tBLNcsNmxsXC+e|ZVvx3dB6<)~bM#4gsp;lC z$4|Z+J$({R`743(?DeX)P>>&E$lwVl3`z_N^QU*iH{ z3x0UMyD!s*x8STTRU)a7Ck?*)JI+t%-7XU&J#PAeKhqsVH#ge-!0r(*`eR?YsEcFt zkC#Tpp`?%jPbMSX3YZTBCu%Jel^RrHCUmze%c1;cg1tvfXX#cVBlaLmdH%XA!q(yX z4=6*ziZZdi;(O5;t~~ky3CRpdW^1;7oIvucz(#(d#Hb9mBGlO>21)^>vu)I!w6BuA zST0To-2Hkq+7G1TOPlVMX%(vMlh2+rFQ?Ott7|M@NusBvq>rx>p{lfKv}2TS?IBh? z@5NUy3Dd(FR?N~>UCqk+iOgyVTB7A;l?7`F+0NxV1Pjz+8dDj)=@q|)tqz`F;Q4s1 ze^6(fkZCWMPm%76;9W6bkECIp7|Vo_DCrU)>%d#UcAW1KOVo8Z7%ob*p^M`~;1;~# zt5+P!nt!u7e=Zig(JYoA2QtxB@GBd=ae^0RqKk2x&9~*Fzv`cU`9_x15#Mg2F@68( zL#9AF!lpjKPzR!Q*#z3gZ~kjU^MSQwYPr3p2JLi-=Fo8Z$T~}@PUqris}~9vJ1KW5 z)_MhHh4p&duv3ZxY=u2#r7A?N@>~I7vaMo<_*}BFGs&0BtQ>CHU})d8@)&*OzO=m`J$fSO^YXR7`g#RY*jPr`aRCm z6(w)(kA9X>4Nb5OtA}G-r&_sSMMDE}&Xy9uMgO;tQz}%(%Cr{+vBn*bM6;wR6JqqXpHxv2o?Z`JiaCpe zfpO#qo4o2P%GdYGEDzT+tE?dAN5IN*lqGM{gSs4XSnwCSGVdi( zGLsNY9-iKR_JAvxt_FQsc{x@d92f=~L&kR`34#Kv)`yl~`I=1WB@mNIH3rr|{BSba za{lJNw4tzD%)p#!`b`(}rt;|3`z9naAepV%`oMwYIoy$fF&86J7fuY9(P9uLd86vG zX{MZia7AMgO;r`DC`FTISWYk*!ABIE;*PCje4~|W;C22!Qwq&o zq>o8*2|ImUt^JR^YiVsHyTVP*=0I_+1EsgBq>`>x=q(FLmL&^W_NHyTu#7PV>w!T? z)@Y}80^OT*i!9QREPA>Zg9Cv;Zw50=j2Dw3i)J7&Fp!XCL+D+y$uNtMUyyrmNw%7v zStQUy2Xw&#OLA4YB+hrw zFO-(jccesy1_zGYbkqGSA@8sElG2PAad0GQ z%{0$Vre^!=di2)>&4q&r~gT<@xMw)mO!#x%k`lH z$>Vz(!&EI0DXG|aOVUQ>!`{3>EoV(t)x}II??7qnK0P!i{VZ>ZsN8+?pc^&yc!KjP z(~kLmu1wPD%I$V5;}N&1=~{!bK{I4(um3G;S#C48$#_oWAI{LPu9UZ!o5eJxMsMgT zqNvSdEy4@OOKU~7Zq)TTqKQh5aD^(2ZuV*Kif+UP{gBRz#oIBfJJR_+aFg{*xGs*B zELHeooyx!9=gz{&BH``B8|wZI1Rk(`*;e;sNjBkOQA5Oz&5K=@?eObSBrrLMBaB#x zYYxT!>6b*E41~Sf?M7dx?j3-|s`e+U4C{XTkzfPUh#Toczw_eZCh(Q1r#|OOPx+IHPc0gL{QFmZ9>tK z69Y6HGJ&s^EnIM94~iRV2UZ6~h7KhaRbI0-pEj^rWK_rx#y@sKvILUlTCNWqNG>1W z!=w+TeEJqCn}NTQFr*y@q;d827Tjz0w57O2F zQQXrYPDgoZni$l>ZrzMJu{2}h(n$`VMHYq|a(fuzy6 z#|FD-0<=N}seoIR`)Hq!Y03!0sG(tl?`$rY;dSPsUOi&X*JJ*PAHud(V843he|~9d zbvbNBNLpmf8Qzs@ zya~XX*wEK!Opf!>%=06O39sxADI8(lPbBSfbe86IG(nu0$Wjhg}!>36|d-J}_2h7stGLrLndpKKOi3nYy>Xd|yEFLzS&1kSy17ec(Xy zS>s;yXf7Eg_VBeh_RqA$EJ_YyB}mV^y=f%_h#QA&`m8IW>BDdN6IJ-n{QQr}uOECI zAgfMpYDoJ-SIFmyYPxC08?|_e1{%}NR5Gc2t?u)$E={*vR6WP!w=O8EF~%H9 zqk0s-$IZghisNQCfnph60{;lOIci4Hu$4i|diEa73dv9L=03ZNKL_t&m znT7jNKg{LAGMWhdD|1jO5%3;H0CeeWsBsq0I_W}EoPXZ1?|!6+dI^{?*>TsUulot_ z1CC_LE6Z>vH1Cz%!Iknf(JH)L1SL=0nLhc3i*T%@01B28W05wAEQJ}T10@YfV6NKb zn~A&_S|t7C`FT+Ve@8`rebSi@GJ6>WVpl0@aaWb#l%&26Ed!h=cTt)4V)r>`??`xWJO0KfZo z{PbfHg35iuPoFYQd8VDHxm}lA4s4m?k^2t#4%t>XZTS-}`5Mp{}#wzi*o8QgDtGz1*#8)<&)amR|n>Vgf z#OiZ}6wKc_wIeIn=i|g>C`w6u=wXG4U#j*jAIaj3E+ct}b^?xN76Q!A2M9$S36mTE z@cVVw@rZ-WcUm;hTG>3(QlW+^`YE{q?_yBT1zv}Lg4yTq!1pex?F`1!}*Uz)cs ztk6_y2MdJX>Wczu1if%NP%so@d2d@z1R2=qJbj-L^) z=PIN{XE>0M-H{S;*DCGCeX)S4r_y>RpGyWtUyT1CX(mCPyqZ??pK`{n3h9)&Px`Hm zV$?MAu4vTt3pwO;G>Wm~3NIJ? z3&5j`9FQz#7sjSQ)&4yL;7Jk4izIQx<6@{f;*pe(c~zt>)I<_OX-QRt3K`(oyH`YL zJ$rjLnHg-}m}#C<^QYtguy;MLjU-oilDZTWPhHUZ(fZMnS|tm$ezYVk+mfx7C0!(= zgJp~{Se7v;wiRC%duCzg5^RRB1A(2{QH z-D;aG+1o60P#ugwtyXu-ysy6Z-uJ#gy0heSTH=6`5vl@b52mDyc?fFQnmd(&J4C3D ze01-J^QS-~D$E}okIz)_x9F-EsG*7>NG^xnqi?Qo0E+t3V0)M!RcNF!YVRD*PPeYN zMmX%T+eqQ9mJ7*jgy;Ba{^mx}2m{g_d!q14N%66eFZYS3LIgPM z`dERIhXC({1l;jEK6BC?M-rH%O5x~$o+A%t(9?_6g3|MopoCI$hwq-=alQ;k6Yr30 zZhmmlL{s-7t#b!0aEVnKa>~fsF`=osk)cVWQJ*HAo^EI8<>k~%fMLhj`pBJ<;s^4_ z8~_y@N82m%5Hc$Hm&W+}^RH+Tt2@!q^G&Z|OM=S!td__H04|{7jyIWn@Os$f-K4X0 zdC!)Rl+L9}OP!s={_j)?o}YB}KGLgbJp!FrqDSZB8$xxU%p$<5H_?O`rnG-ez=M=2)-rXS- ze^h@TOJ=gOIurn>cW1oO#8i|S0Bln83-D@GA*QHWe7q_dM~|=Yoqeva^2UVP;S>eU z^ic;5XK4`SeLV{$?PZY`G>2EPjum+9pRPveLmcUVN&o&<0WLEn3Jrw|eP2(Ke*Oq4 ziX<`N4G9X2%>5r#CXK$mJc4v^d*+OHAE#6{DuHcOp1Mc=T1~*7vB?u<%A%-auT6Io z>7`wQGxXso!4u1B+#}yQouLZe?rd-0O3!O zriDD~;Pa1yBR`SZS^&vnE!L+GBn<4OJQ>jev1Wmc<4;(E=sbejXFXac5=X9gx*8>^ zFv)4JRO*K`kNcbS{QUe&JMsP!*iwO+DVD6UL5z>N4+XKT#j>>nXmW1VjIz!2jlaB* zyhG1|vPiTtl9pI8#_9t@WB@FUaWl(fb=B-;!B@0EY*F^Sd*t-(CohBLnoyLl~qqb2~CByyC$8`i z1h2s);A=O^xUrqwF0DIL~QD)%oYyn zvd0n`Dr)vWT*zN9E{{C62@N!cnPb-@OZ`nm?VM^nYXzJKI6~~{g(5bHL2Ia)K`rFi z?@p8dAv93k{y0GLQ>R)0$zmkElT_D|9@yH`@Nt_V$wPdPX@8)o_Smb6@o` zwFvT&-HW9#eeq0FyJnQUKY4$Cc|%`*C|4^vD_4}XB!04)&I=L@TBd16U7Hd{_VQvJ z%(IH7Yk&LZwZskpgYU_A%FM!fjgr6s8^s|mX$bEFw|LxGs|`h)+MxMC-<5q}165_F zubpl2cU=N0LZ-g4`@M6ZjL*w3nC;y1t&93aOXN z{NY1|7k`QXg$EXnl2MR8^;P*Ix2BlT({HY5jZRy)*BvzH?YAKlaP^k)rdLl{q6e=L z>8y%GK1x)lw%MlJDc4hm0G4o^@PXY`ljyGzO?LWS9w;Q(n2dw8mT)OYa!(}%U6cx_ zq#0Rp7~#{Zm;GuTV+QT>CqFYGSpdmmE!L+FB=JXR2$dqH@Lg=R&4}QhbR1|cg*61P zCw4}M1fpsu{d6)WJ+`ZYS^kU3`}3>wOPwY4eA?(G$}FET@?3%j z`MnN4Ie$?wi(z{efn~Ryo6^#bO&mNAHnfOqr7~lZBbhTb7j# z2K7Aq)%gu#2M5q&_dUN5GBaQ%gE%LJOI{-EjL4OQjGV40L|&Z}KwQex#v zUB>^DgEibrw^|+~&N)Ykj36gE1AaJd^9_DDo;Jh-$gw;ef#>q}gVGUv?kQ!N ziPDfi*DFGFw6jWI0BtJKicvZ2G`g(_$JxRB@NU{|G*T%$rDy)j-ub*Xl3a0ope_%Z zQa@-(-D<1Vl1dWY`eh5*l7C@IzDPy~w=sjkvV}m%*7zpRWqjEgV|K?dhlT8EGX$0$ z1{M>D@gW;>Foc|znXtzt`7e^cAXU{Z+e?x;%`OvAVPlO!rIt0kPrv%U_kCG4?x~Qr z5}MG%gKWCoJinypVCB+nlW2w3Bl03&I(PT|-O zX$u9N#1#^SR?eY2MIw5;mo^|Q3sOsEhTq#k;Sc=&GU2*^`9YuoSCtB4W_}^8EBSdP zC(3!PL<6xRw0G&OWolZ=X4c8+Gw~ZtnVHp*V2MUKNY=7zX4n+}v}~~((c5~D4mP7X} z5qs%$n)vh)Cs05WMm@mpa1(h9ciZT~-IyzoCW~4^!lY4?MF2$(6msk($>zZTaoevh zCL1U3$|M4V#n=Mz^>P}9(R~3hbnFg!ObZ<^fYX?#L2c$k6ITBG<-7Cdos6+0vI1QP zim94H!RA+V_H|Hb(Dgh)5SrTHDU`uq1R6>I?d&fJuVSA3AUzWGZ~Ke&Tytmq>I$W& zO0yNsx=Bd#`-V^nIHgKt6{hJ)W*CjkJ2l7^`R7iTGREak{{N$o=+^k*ldTy@W^1-S z^`kB4k?9ejfnHEB=7{IqHC*UzejhCc&hDg(W7vEk%x>r1`sarlWcy}tSFE`a`0nyE z3#-<41_w$?Gp&eCl|m(}#8(=5WZQ9p5Lv9H70@=-_b-%FnGt+$XHop zE1S^Ch0d<3IQSM14w_8?lR+=-GbumKV06v7t`f6vSQ~SbH6~RHljadZ}!utA-`Ap(5kx5=l`!@VIH- zG)jCgLOr0mIiYFaQ$@g}oL-Iy_6Xpn*cheSs3;8Rug|Zx1WZu(dgn(w%Tpaa!QcIxTd7-&Tgl6MciHKIDNd%Fi0? z9ibhOeha$?ttH}nLb6`z_AS3KLVZWCl6=bU>qFPI%HkLedSZ~HBT@#=x`-Iydsq2o zwV`!YJvn*F zYl5_?fxg`Ur3`jyVn8S^Gl3yC}l@l?j zU1Co>obz~EG0Bza-KZ-tGztd4;^E3u%wIQfvUSwK^DS&HLogCV>_qA1osU+u%5x3q z6J=pQfYhkskL|7g2S|L0I?f^4lC`oDMVPMstxwprLxc(mI|0c<;ba`xZH>Sp(=>-n zV0Wy5KX#WGviR$#lP9rovLQ|EEAtSZ+$CC(GM?S~7m>5uQnZh7p2*Lzn={Z7)?cb4 z9bpDu^H>Za(P2~vog&n_WuWsmgetk_&doSTO`5IU;$x_Ulj3BpBhSV>VWd0?uqfH0qd|5@U;2Bgr9?!=7?>dZsDa z;}YFf&~0yY+tR=h!)8IIB$_|YB&DX751-{J6aFNAV|x*=wy?qOWBA;(pejd5+&B2p zWhpLc>|y4CK?+S({5$vkVTXojbM0Vj11GQWAdS9c5{(M7ej`Cru_aryPRP<(gpvl3 zRhA^;6bXn%oxS?cK%)C~zeg>ZjL5RLHYVWYk`U?xIvdf55Klt?dLw&MsO%ug!!U4h zpC9(18<##3sezj70BTm*(E6)BdJ+lfcjGKLHR!% zP7HZ6R!<<^<6O*)Z8u4tV{%}fb%=^A%l+;)vRW+14;y^JIi20Z~uQ{0 z@Z5hnX4)5#nTRp;bEK~lYD1?zwQRhz4yGDa63{$2$D?b!*{V!#P!ADg4FM9-{SofR z?+(PIeY^wvB*gFeLnIb!GoGBaAC688Mbkn5h^(-ZedwaUuE;P(e=~wOA8hGu;X#VA*waAgd|O09gR#fWEDkEVRo?v z7}T}TW>#-FHuiyCbj7{yhLLwPUl>pmc}iyvFxO-`S-W1_7rzP>H14pI(f9~&KO?Cl zLN9t9LOG51^M9)18)pOIUeNNzP%kDTHYX>A9?R^QxdI@PD$y0PfanNEf~|~6lUq1A zRgx!=D22ffYfwcTFN4Ty<5 zJ*(ofzS^l^$K4WV(5>WRV3Qt-rv#a>w8E8xk6>t(6n%XTL1Fvq^g}vbvrDxnFD>ne zSj9!Mv{o|c){l(EBmcuKEtP=7PJ~ia45_rCE2AziY&qiO0vVcwxB6&IVX^!2#JlG zyEpC%IGA)L4e40u>zXb=zQY%XuYX}fojAvlS7hsx$r0V1CL+$p)2AmVOEC7;0 zFi9F#KG>e&NpoeCm81Sb=s5yX&?bFR zvki^9ak9zAJZ2kMD187h<;^Ltn$X&IljkGXmQty~!mH=!*^Cj+*oI$MZ zu~}gK41i?|h+%XWM?gtnV&VKB0RgHDyxAKpD=C^c@5_0+ilHxv%ARp505(V3loYa? z1fBtV_1*Jn4AtEO>v#?M(hQzsq*R*^XCq~GOFgdim6M|3EkI0l;R$-F#^cre|KaX@ z<*Z^PZ7KIFVDd1?4r<^f6|m?TZNV-^T3vxAw}V^puR)!Zn|HDh+1N+;>bGB(kW7GN zvL@@kr?!?JuxRU;;=w=x_Oj{KBy+@}|EANf$dW`|RsrIIthmj)RR}HUf-3&>eoMx+ zKE2PEvWb;B@6V^d5s*<>=X`$JKB9#^DoDY2m@UlGIxaug1DS z&8M$_%1E=7A?ye^+0N?kpVuSd-FqhN*7GAL898l|i*<`;UMri(q!#h$la5)Yza;7=>cg~_b4HXh$8so!%`IJv{QmrRu|YvIOvAQyvxZV}3oXY^=(;B0 zL$fz{-I(nsDFTc(ZtzBges`Fk61Oxr=!+wJVU}q=$J`Eb&c>qh@lfMYrhoW6?#y75 z$;ixITwrt~#}JeCOl_EeJlnisTrLhUvo45b*WLtT|a2efWK8Lg&$?REXyUB zae4mKJf@6@VSUXW{5bt}s_tY9f@=&eGDNm%7()+5n$I`_SM0x>epEw=WZSlZl9_m# zo>F=$dTh=%cKTerTD>43MDbG^N&!znUd7Zk>B$65R^!$V^EC#kxIA5}-rT%Ds=#Lh zOLPY7L@tL%`yIM1j^upr@RG_LECM}>BR=#2skjTR)5FpDxxQkR=F$~FGa;Uw4ILOE6@`;_o!pdNF-i)< z`a$hK4hc_iEI64hCL!w%Giu>)%Me&ujv{!OLM}hm+G&Sl@V%>l{o(Kb{(^*L0wj|) zS@$1E+;R|TP0f?Mq_^qE@%k~HblR&Y2^?lpmYdDN=XM`aXR0bNBu5Wr@OgN?%V6tF z6Cv%d=jIZnX|^eibdAd&bGfXRw^ILQ?|NR_xbk?|cX{x5(idhlGa5;wAI}yVX-2k? zZT(!^^2H85SVjm#WD9|?ESnNydl%fxHfGr-Y)zXcr|jmCZYb>fuysrh?YYpjbRnf( zdg>*oobo^Hdv7G!$z{W4S+byqY>=gqW(M>9ywCT?=R=s9uR}MTQdQ#l@vZ{EPJ~3% zy@OpKsQc<1; z(1k2Jrgn~Ayp1AGGKwiU?*Ub*ofy)a?8K;$Ma^3P03ZNKL_t(B`0HPVWC0#H&1hbQ z^Yv@s+hBma!V7)P`ouyEgcCX{@K>&(n8<|X`SDg*lW)EKrH~0LyaFWm-@haxuLjr* zrSsD`Bv3tw*48=J)j0NP1n?B|G|dj~elN=7LX>$0(>Z9+6o+q=#8RgwJ-0v6jFJ?t zm#{l)4jd3yJVTSy6)|AgaH<|m^hc0^BL=eL8LacWkkeZN*!mLK1Pc0-maVBQs#;m6 zZuxv}S)j@7pMQFO^5a+6KYRT2e?mycKr&wA^^OBc6ki!`g6kr&ju(5!bo1;Sj+@Xs zUXIn$B8SDwD9Gq*vl{ITohJsux9-Si{dNpt!xqv`ckfdMIuC(WDXn$%{_asxcvWq8 zi~@ueO2V;84kf1N$h$$b_;)w8ZN=IqFgaId+e;CmscPJBaul%?(K;N7TZRdr!qvlW ztCTkp&QIYDB)fqxj7sD)sqQF5TzGItoi@&NQv;h4I+SQknXv?$bfMRP9i5Sc*N?Wa zI`UcBt)-v$J zJuH+@P|(SjMoqtWE_q7%IHuC&m=x|uoJ_f!E1NY*1v^Tvv|?h)0jB@n-k{ZMX_m7l z8&6gKK=Ok&v;}^g;2S<21^xY8GNk0fUPcc^8>i=soGGPfOPqLqc89Z{(yY7)y)U*3 z1V^K`mMa(h*%;&pC78ifV+Sj)YDBg%?a*+%U98>v`T5Do^XCu#`QphRe|T#!d4n|B z7)Zuzyxwgf`5<&HtFG7jc5 z;^((f#U+qjVhN}aAO%oDlp3a2osUNp)%3@!a7y=8DjKz#Qi@o7(fUnPtioI&)MiCN5(&1$nLRvukk%>45sQCgRS6$r(P{RO8A)61~3 z&Zv)d5qhOJ%kt^6!;0eW{7~+yBP!PzW^o@OHoNx=KSZde9FCf_&{lFHouu-#P*vEN zadNtFJ(zI1BWRn>>qnaUL|8+UQY3&->nIDm5bUO@Rk&B`n+VOalqu) zQpR?d|B3B@{It3KC?*P?$91_mSdsF+tD|ey{Hrr1(FN=eIWF$^L*|9+Z=222Hr+62 z6zhR|e>(Zu-f7V;~u?@%q0964kikN#hof9H*nFy4c5}nIa3N zT#*y6^zv+Rd!V?U8Bvnku@6jd$YQ!cvo8ZhNUI3^b zkqzkU>4~s9O@MaTVa}LVv`9o`xwMb}`XMujQUQmV;6R~iL34VNW`x3{r{7rgXVW~e zyE6zSq}RB@W&;SXAr`-kBZJ+(r;d{5qyqVIR(5WAeYH+NmO_f_z2lxf#R4!TLZ!|R zk&mH4iGPdl{q0eA0gt3hhp>4Dl^Isvgj8{xN8v8=W)%I-U=A_po5DU3;JYQ^b;Uoo zj`;f$Ns}FXhtLM{$~83XWu0z6%ntjuP#3#HY>J|}+RnLAoAeAtMymA`5WtrQMLBGV zLZaCJ=%#_3R$8XoP%JP=%o*1TvfL7#$F!2ezJ=44L?xsQ>K{#PU_ba$iBt^dO5~Ew zy+1DQVZ>Ro+(6RAN&Z?JiDcVy2otJVG>x;yhj&HffBN{p11A4Ivo!{i@fxpp97wd3 z020LJ@jj|$&#rD#SC(({3&M=Bwz&S$IUJXkT`^;lG_a$M8bzH1!bH!BRMw4KZG=C* zp@&5tMRcg%*!zo>Qv8O^X+=yi9kAx1vAZkjh2q$>oai%11;|uHSnR4Kt~A)e)~W!E z;F<}3#RSrsPsV9bi6>xIG!GXQx56KzAq#X!p;Hh;_+l`mF!5@t$w`!{R|Qx>qGfd% zxn3Aeia;6n+J1#?S6wzYB~rA)rZ-R!kK*>$kr?HtgV_}lrvSRw8GP*+&PsV$ETatC z@JLwD^VKCudNBxD#o@3oC=34KIduOEgj(L5q>p4bMCIaxXImVrJZMH8KORV$1So25 zf{6SSyE?v1ebnw6{`{a#aR8n`VZX-h$YT1|FZ}hGz;{l)Vs&bUrPXlDyG5douV*=_ zIqT7Mh!ZSN*BN;ekE~05QG?dAxsdn(Nj+xxuO-gilJglxEE`V-NY~Tnl!N(8BH1^! z_}fp06yoXQ&#r&<x~9@o)ta1=v7R-ZlFv*pLQ~u*pxS(b*@eB-VE7UO=q^ogU;V?Q`NOiJwc3! z8i2#3QU~&AcUS5-#xwRpYoZUSXt+X%t8ti!sB3@QyOWaCBN8`YL)qz9m}mty3D`gs z#ZM>F6+%B9A_X8)MNWSygbGoi$%`&~>?$;ihtqq71-dQk2Wa55K9ohNQCn6X_*L>y zN`nlC_*>?yIZ@0&AR)vajLVzJf&NN&dxi-C`sE$igwmoAZ|pE8afD>rf!7qjP;QFf zTRaQsaPQX$QK2Uji<|shL~w?0B}U0Ff@vorj*N-3gF{%asTfDvBC!^Y4BD^CF^)gK z`Qk>>iVC53Y`3>cc%(EIL3F|jYGuRmH16xefs|msxk+)ZMf+)$#)5cmazHNm<>l$B z5E}N&9A3WbiE{GuZgK_`iDHf=2^P+YyM~8@l8e2sKe_zx^p;@q283h`B;z$+?>dl> zD}q8`Bx7qJK1vap7GC3Q1EtvH?}c6ly?T5)btZhid8l(yqK#}Mtz>P!hh^Ubwzfpa z?Z4Q&mexkHE8K852Z}2ls4A6SrPq~(N^hZV>xnFR;}%-TMi>p2EKGzS?M)*)4em{* z%@C$ZpvlCGfu01KZo&*E5FutUt3WnQh9oeHz>p#TAiMbmx%ZYN+uf6lJF6z>fH9_| ztGZ=zzWbfWcka8`y^1&U>f{4|DB|;5YA~T}25B@W>U*6p9y}=7mA_HC{#3f@81HSi@Ey52tw9z}}@G5V81-k6cHR5m^)ETU~&YlX+f+1~^WMmCM&uQp7w( zJ?NudPD4Jzbvr@gmP0AUMD-OfnGHgL2hWMM66J&|GtBR^Tz4Ul5(G77Ksg#ZsySjQpS9w;PTV+k35VPcdC#G^Xr|Gt*@YgymN6ngJBkh?PF1oCAUS{d z=2uFB$(Nu1{Wl^7|M{2ifF`%jf)6?^HA9ycmg4cc+f*p6_!Q05RCMQ2y=zck z(Aj+;eXonAfLa{&{M@}UpNMd|?-SS-%mb*GJJstAfl&@0Af(?TtO$J%qO4U^HO2wCm;MO3DE0lACqR3J(q!ZS-(-x~YZJr@eqf z_GHck9aq&zj6+C3$jI-@^MzRM;ZJ_(d-Xq2lO>QWZE61>Kw`^|Gi5;-p7(T!nA^? zaw_>SOChyLi{x#c6c2}!C=gm@kq{G2t3tL+Z9L&yeK#UWU+?;qAz_v9} zZS{y1P^1&a+@01}=a-X;>97V1v%g^4(@Nc33E6A5n z29RNJujV7Y*_*J$WEX4kxHrH9Xw>_`$qAU$89;TFo#`gE3okXnt-Lv7U@a{RF zI9p_`D04w+*c6j4Y04B6P2}K4Q6=q<%Uje$u1K*UdGG3z%Sn!dDKYEb-A^o z@1fK|ba)$?37fWXdL$JdMIwb%Mk0^!% zUNjLXVb+aC4yxJtcL);h2`LkQWcC7YVTsT>f#yLAFWI=kkA{m!Wo}_vwK8{25+^ndT@%99PXDJlqR>C+PhLpo~Lvu9A7|4 z7kprcuzo_!)`j7&C?V5t>}3@vg{B>;IxM;h*BA?<4zL8>LD}LSfLH&D^T!5RwH_PV z33N`x^0R*$(T6Tbjm=5zvdMvAb^A!x09*^cr8Ncue>~T+`G6dBDw`Em2OUjL-Zrk? zKput`jUP~W{q(=N{dWPA+i9&Vfn;e*d%uCi@!=hzrc0ZdKt1LnuGJ9u7J?xpVn<`V zw*M5}a`9Xg8(ZyzmQV}c$7adhkrKDBzANExMN37-&2eNhXq28`C&iDN&cMDR=i#u~4rd zu%gNxyLL6*R(6l;qyz3ckZhC{k{kniGNoX?k>n^Kt{t#afOR4)0wkRzgwgckC3L>| zGj^AhVO%J3SlScQv8V&ts^=y!B{-fn2j}(brKLQ&0kg*P&zSDx3oHyb2wM~dhWhIdQ3B}m_9{Sl8B}v8cKqHI{rPn>PRD9 z#bY{6ktlKd%#z0Gi!-F5Q^S=?-%YBW5r{eJ{0wgQ+s`S0pNuh1tS)f?>5Ah&*|a4U zXFwwFj+O|FMZFWThhok1Ao(lp`B?rWS>SI(EyEy0)?R4xkv~dEmEU#J z0}N*PAXT>ns(*X@sGHs0ln~HM>@O&n**5h+Mujz--B`GOK4`3DYMm??U~G?unJ_@j zo<(Bvb+cmAoayLsLB+bVn{vI^2PIk6+&^hWg{wf*5s0Uz$lMgObT>}kxh_R=TVt71=|x3qoGt8gmi9-yB>#z-K{|oc=ug`}ZYl7jOx*PNE0-+VMkI z7;zE!j?{tDfn~U=G;9FaUQwMHo*-gIVGx$bvDWIXA?vkTn4GTF#ci!7Nr_Q8*thpd zKBQ=b`*r_ZADqf0#^y)`XR?exYB&*@rvtJ&F{WU=Nm$Bb+X7WvA^F5V{=>h!uNO&j zBpj-M&nu55MrfRE*A*Fkb^}|FRJ|q7CdL?vN7vAbI3(@12D4A|zsFb27Fet{%NOUgzGYuU(M)`q1P}faKEK+Vk^vX=pnn#&(AAABgo@$fOalEfMm4MIF%a3(QxUsw-m1GIB-Mi@ z$AM;YjpQYFr8{C`Zg~JSQqW`hj=0o0Rc$&AyV6F?h*}wWeql@QagD;M&TE||sf>HvGKcKcpWgd^+I;SpPn=6!m|QZ}{hl%?n5l%ioG2B@h! zB3>~wW2^7u6i2g=F2$&6NjZ`QMBMo3h|;DANMgZipvKc&XW}Vp2FzfXGTmg9-IN6p zDaX!ckwIFM6BhgokjRxho8&RsWecuCE~lZH6*WHMr@6%MOpS#_WK-rWom9aXvmwsX zVp9Q${(=&eh(#bF*|K0}OwQ6H=b5tt?Pktp?V9A%x8~RJXUlxCw}SHxEIEcGVLAFp zCa5@J^^{-XhNELzB1}mx!Xl~RpyjZfWd9AE`g4^dONt+7e7hb$xzMlveJsM2a8tlq zJw!?|Ldv5G{l_#|NHP-Ecw?jTut5cG$Rjx7^RivyV#k$~nK(K1)$39-iz*LUW7_PpHs^5YA)3Mw#p^3>0BJ$)k4AiBERGvw0y-!O*@bUgWpH)H%eJ6V zMmhX`r+xTH;7CI{U`hZ)$FV1X6uCn;tiI~N97J} z)YP;P;@|#wk2;zetki44?kvV@Dw?@0qM1N|IU1wW?P<^d@er(<01F$0i;Ey6e^OZ& zKq!{>HtdSOu1Dre4Tg_V1|#mE$0=(tAR1D;YEq(39+bY~gCjD+SR#G4CL&;@N?;Ol zNSdfS!tdv<`sEqJ2smz4B(NY6md~;&Tpr+Pi>2e`?-_Aob}^<|rtyo&rP zG+J0{xIM#FAaxQY(HA5_WH+6~*^)o{^2t~G3S}a2T|!ZHgE*k>0L&P&+`p8=Ryp4Y zcD5nV>APKRXJr72aevUp{nNxwzAHX)LnC}k#w|#E#M8WhR26VygNHCc@o!@EEbCF^fGBY+Q&K0YL+k!~_w5Xi$M zM^;o|O(1%rO|{q?WJ$=1cgs^TlEmw0y4XfZDyEyEl)&FbkLF8052-HE9a1i^(XGrl z>FgNxsnTPwmoKDALcjC#H=klU$#XsX{MoZ-e}82^*Z&hpz9;OtdZ?`-kPK~TZwVxq z?eCC&)Assat4$yQcSm#%I>;Z-t=83GUtgSJx zwT%`Hzk9k=4v*a8tc4THgMq-4!@nYi)WH_Q;51b%bEl5&eb9IQ+3FPCz$;-=Q> z7!uK9T*f-->wZu%37rUQ-~983M_uBDC?4k)b*$?lmkxO%bKoZ4cB;t)p=@e4p3%H^ zE}|wZ#ck5ZNUjo(i$z1qt@NljKOl)9D=&y^ScAgp%~2H+c_zR8;oFVGdSLQv2_;^h z>-S%WCjT2q-g`SBxgsA!AQ{@wE&?R)y#h$$0!V}pRKyu{qb3}QmmdoI!n4AgbrLY6 zd>GS5ss`bk`6BbO_X*gLY$Q&J4Q}4NcI~*`zMUAX@Q1ZE8JHaI-=J0rtr@J@Hphc; zN24MTq1Gz)u@ww5xz7RyL8Bf;~T=hyd1dwUdh zVi=lM%kMEl7}*s6ES}*}ZLkKse9) z>L&%-8{em+Zgp=>KA7`A*}B&m9IJn;mXE7DbO984(VW7w6K@~lDxZ`6R4z%$qoch# zOb9l^v*^a(gwN#%AnndS!VDKmPl z-K7L#0v47xDwMmD{@SwyNVvEMUrgVxw8OdHxw*c=cYgQD$AACoZy*0dgeHCo{*^Gf z79`{UKS*RkOM+yW+8Wx>UJE4SuS{(b&rc{g%?hPye=#kzoyy8>Sg&oNWqgS8ny06W zGW1yHS~S3jH=RLC7k8Uy3nL>V`24)fy4Cgg#rE_0`572hZ>$^k6k98lauJ)1B@{Ij z5kSxxBNj2$q#%p>VyeS9OD8nPaY&JnFOKZ^U6@K#)Rb)LQZ(_F2s0GMLWEhY6`@x| zt>D5YIl~0OCMLxe0zbmGKpKd$&zhcw-Ln_pptU zYiJ9jc87h9B7iU_yL~#vs;EF&a;q-B6c%VfC7o|cD8)@jI_9EbwPbBOFw5pQ*mvJ& zY-M+w&c8deEI^`4l@$Nv3)^=u7hnZ_ev{k%#N33pBPEC_aCSB`kk8!3ReKo*xr$Vw<&7hf`Ui4*v zx)?cJ8p9yK@(qySciy}Osh$UqAOGl^CHM!|(&RdjjNbqxvcxVyGAy5Jw*a0Cpq^FP1e z&72mC!Hb_B>=&8KN@YO}GiekGu^cG{Q*4pDs1_^`q9zPYHI3+t{XN!@vmzvGv+S?A zlFkrKL?T9#dO9_;B@?{5;!q?D-a*Qy8% z)jP0n4yKXFI|cQt8P`w zeT8ak&>NwUSwpa)5%0-R!lS{OF=uVb001BWNklyPAf?`tT_l(Ex*8_f z6X+5gp}ywJ!=tCS0206V_w)Y_lWRaSeiM-VK$r&-`DzG~p$+YS2g&$#ATgw9BY3QW z?Wy5vW={|on^7nuz_t?Cz`+C*QrPiWMS98EusepW>Nz-d02YCR~FW_i>po}Wsx^{e+<#+0b?3Xa!iqv zk;?4Vav_2IM}5EiZd14Wp8TK?4=xUyN#pZc099bM0LEfu}=jYvK_a{dmGQPFWyop0%5w$Ww zNW^NeSWe40#T38K8O$0Ia?;(0OxG&Y@|yYVXWXiXA(2I>h*Ky@Xx@A~G3vDpqaPlM z=W%Gqtsz?QtLFXj_D&7z?vBkUtK;&_Mxy~^h{CJuzJMZ*@(IZs%Ij#hgVFLb8i=G& zoS@yNr=ZAd`AUcA2)ElfDYxfNX0Qz$(f#b7h=-uL4H3$6babdIimL&b>r-{UNv^ca!gvVr(F zFEIlGJpA(DO%XyUP~6`Vu~8uozn#K%y#R;BXKmXRG32`Uo5D|4ZbPG%xOml63&|3< zaoAghg%|JR7N(J^lEq<^!4X`6>ic@H^%rJHTxyGN`{kFn3z91|kyR5PCcV()wo_a8 z?!9%-1BrY!1j*2bcEi+GD|!c4+BmFk_1#|3TMI}Co~r853hVjTTxDsL<+0=lZs=%~ z_e4j!%@ZFZBklUg(#XIT_Rl^$qE+6pi+P2RlqN!xohijoj*DZ-G@({_$;>c5or|eN z(Bz~2{S%cj)`>asr~hIq3n{B)YB5U*O=wW22TKagpy`rXw zTYPj2QMQM&8SxQjojNb7;vR0j!>jVUI ziZ4IAwanI?YhWU$;0JSypI--(H*N%ydwr1H8-iqLLwgO7ym2i^EaQ%FO*^Gzf5VaI zQ*by4ix3O2gWUrCM(FfSa_NJQQGlw@+@!f8964>C&HLOyoskY5`MR}^erSdjH5}C< zW4bxY$P{By!wiv>Lv$+?W>%Ol>82Yaj%C6&UNb$xxkK##uy-}RZCqE>z%}5D4j$(L z!_VOihnz>!9Dax*D3T(@h?G!3RJEW5!7w0EECGU|6clYGrGT_?EEs5lrU(#c)pX<3 zC<>v1@d}YcFVfWR57{o;mNHd(J-& zs7W_zo3ZKBDGOJ5AhZ zvp$xg7vyc%FP_IS4ZCs(JQq6zToCfE`P|IvWXc{WUxn1w@6^u@7Q(^MQ8pCuS;;7^ zH59s{2xDI%?hQoEL}7t$kc=Z&OsM%}?{-+DCI0%GgNi~^A&oBA*l0rH<-cPj;MLsB zUCIgseByvhY)Mw>8K0Dx9w8&yFMZ6P?F;-OO?1kP&p(#t79^%55-n=8ORH{B#ip%wJm>b!TaoB-n z&ZoBO;$dHa2|FeyA3b`!)9I|!;h_#jUUe)^0)=2~?A{OMS>EoQ)NGJrl3+Ox?(pxY z#W14~+yC)rH~dmBF_QHrgNJJ!JFXi1XDLKqAkE>ko-_v~qU5d9m>IN|MNGXpaOAS3 z9W>6Z%M|TA);EL{j4XBb7ba#fKg1*DISu4FHTm%8zxm{=KmF_L-+p~LkX-x0K=NZz z*X1u>Qy`hvw7xHpTzdwPbmy)TBoUh zqQft(ukOk?D*-gc`pQiLb0ZNg|6>1yn}8#FJ(L5Ok-1pg4=@|Rv*z3%jG!kheQ*ywZ~Xd5SnFh7dPzJe~A!|Q)2FFn302!*)O+0BCDhm zSUp*#l!FOpCdw@82x1C8@|&*>WJ9I&Ts4n$H zOgqL>1nc}$NQ=;1j-wnVyiSaamJ&lU$;1^^aI`3FLC-L7-x`NYR;6W(9q|eKoaQ^l z=6G}_4%R;S9}<#tyCxh>KKNZ#yT8Sg9<$k;!_37}_rSQTF4 zc3cb=G%z(tN&RRMZ-Fo$49(jeax$5+^D6IO#1ShASCwyHKk|4yS;lBFbTp*3nM_if zmy`D`N^`<6#q{ZE!^sb~?uN{m9ntRZy&I01`rU_rbA()LL!my-SG>9^R}wh_>6*$Z zNs>roTBRWvg(Gs?%YbW8bPx3v!(AVdVkoG9520W%H2Vm$<>#!KnX_q!`+|KW1~bT< zjm5d`DhHwq>+vLz(DMQMm{yQQ_G|nNi~hMAh@}$xaO5WAjIOPT4|VP)))}?|wUKxj zKKna>wJ7CZ#N$pq_O>6bcE}Q0?{pR^2a}GlQzuQJhLRvad=#)B3SaBC*3=FTV@cY^wUEeHfV}0wnUB>&-uZcpgZuy=)u#A5AU6r!}qT zt?QR$w#y}o_~iMVx5p?XA?X#dy8soW%#AIF?*0&4mRv&IIe9XfvOlYUbCQqW_Lv@8 zidSgaXEWd3g4kh?Y2jK?xHx)aUa#L&6klOTDJ@5IC8iu5=M7U;{(0}$gb-5<*@v9J zc)k4kTWbhn z9~t!q#q7mbzy}C1Hmt@o3u3H#>wJAOBYzfkkk& zz^@%VQE1e&QG#&1YA16VInp)FPwS>eB`BDx$Y$5>#!8H~IgZ55knt;eq;N8GEJCq3 z>L?sh_%#ELA{w=+;xP6O8u`g?JMjy#>#)da0&lkviSC}m5f@AV+G06`qX11ZlWao& z9FSO=6gGj0@Q2$3pn*Dv)A2~bYoOF?MKJG=s!)W(9JGpc{$^X4q@-IJ6!Lf>j2Gvi zB?l(LHFyi_^o!Rh9J+U$lo9Gn9de^;>^zz?#;tdE6JTQ!f zI5`RAh!!dYt0^P9(Dwzju&Jp1nGMa<0u5O`*&ncSz6}+;(Hyyf-+q2#LZYqFM z$AP4h4;YCNoL@CByH0Gtne`k$e(GIlATXK{XHjRk!0hF~6U9He^v86hxA4I>djztI zCcZZzLeGEAXily7Of_Yys;XpFwe|R-x<}VFQb?p*^n_AVbK;KN?rth3?fQ!G>{)aD z&H07kB(-W@m^kuqI~+}#i@9d+kfyUBOEn}PCu47Tp8Af3#L!x&kEqzrUfNORS$m=~ z2L>+7q(ttwcxJB3IV%B__H}WO9FRKXK{nxqh-A6U!89isjx*M)l=&6AVs)+|wwPoP z;>bGKf&E*oWFW7ZUpPUr5f}dYtppMq6|CVth*H_sK`!G2cefNgXLpIUn_GPeCRYoW z4Z%z*BLRD=P;|e3Vrp(^T{^ca#9nb_OgCso5@NwmKf&n1c?$H z8@5l1V(9x`H<~KRO<))+cW>=2$<`p{7MH~^yrFi(TcF}%^yNcvEmcGqCKN! zpe{G8tclGZ2{9kF+~rs^78$mdm^#=8d2(EEto5Ikf>z5^GQHNmVirp zfQyVsbSqh7R&w2nf#CvuMPS`cS~X#q)gzQcQ|{qQ4kM&$O>7|%x+_G=OzgAaMpyRV zj|co^I0n~_3HvJTUK?d8*}NLtVcTy`zX^b((_iJdaMB^DWH=il!5F{_A_o#|R&Z7e zK8b~)Y_fM31CM8y0M_!_F1yynG)1M$Un;fd=XY;79}9GR`0#%NB-5D2H11+ddL*K@ zQ`zj4Lq}iXiwl;hj|6Ea&q7^8%Ss4~#t<<$O8D>d5r`+pzXVJ4Nxt}s++Q$bwW7iS z#LR-B_C~52i_e;7x}48wG|pJeDhb)4X*dPPQSNhSQIggVFTQ66jm?paBK@{FkZAlH zDKr)O`@iq-+}vaY$hAK&37A!DOV+hq@alqXjOGga<259@!5NHX3CsII<*EZK8OG_s zjgCp~JaniRorl8_vRT-r8+6-;t|#1LVo4(Rn@`r^3iqxQZbG_28&2q$B|fi#ertvp zUVWLw>Ogd278w1Ga*C6G@+o4$k|iZK%;K;g=RXcw`|TcOM^~cC)!1s&)&RW(49;Ez z9g9h24h(Al?er7MapY=t6^}75C%AN?=(#9X6X8ygu-{|J@iHNVanr#%t6-~_nNa;a zT`ZV#ey>zGLCq;f+efq=j(pAX54a$@@DJa7#LCtbNTxB3J09`D7)W@|%+6Xu+UG`v zD^;V5b(RY^!|tZ(~bRJS&( zQg^2b97s&Z@v^G@nd_T5$_zhO<{)7-sS-7`Sa+x?Da}uP{`Jq4I_$9NOYw=QemPoA zNZng>gzg$LvY*HTdLSf)IvKmh%Qk!6RsiFK3Y+!hUQ(<=v|n*S)d#d9w;EdjC8G)IENo0r)=LJ zwQsB&&deyY_S4rNk&sM*WE#`B>x?8l1`>Xhw@j3kEgT3DI9))?vci=WHG%)1tH)pX;e=I;|>dtL>JH7k-h(bkEgu zPK}Ob?3Qo~E*VcwKmR08s4^Nm04uSmqdA77Ts+4D?E!(D4WVCFV<~=X>=(d`me?Xs z>5^{%8|KNa)UFJ~^E8I63msA1c@Id?(+JWg-QR-7@g4|6`Nl9umJ>I(xudWo@qz6+ zb^ZW!ar)=4NC0~ITR`&TAIj)selB~qZucMc9}7S_xVTRHC;S#G92 zcvvSwk*|doFA{;G0v7YkauQ{`i}umw^7vUoUs6k}YFwCNoHModjjA%u(XmL_A_g%5d^7j+{g`$LNvnt!#!54^CeeJhd`U>8?>=^q6Ku zt0b_7RIIQCj~Kqn}ZQ&YK)TV#*vH38NH;bCcL_ z1|*H!;an`;$?z=SWbcms4w4D^P6k!r=`o*M$5iPZHemy66d;3#(x*_GuS~iEhM6Gz zlFNImsm5_pwB9#+4XQUq7yjmWmK>>d#e}U_6_)dI`K@UF3x~36@EV-HB;xQ7$b6|k z=Va-p=aWGIzq7~4u9|g`xMMHLvVsFzXp?^bGC?2Fe^ALxc5smPIT4DIXfU=5t$)4c zmn>PmWJgbE{_fZ{KUgCSXh3MYpBWU=)w*cec$~6S&c-7Gc)aQPSlL}A zBf2_S^)6(L3Z|ZX{}FLy3MA8*#$5-J*ceD`;Blr)_j3ie0Xxf*j!^2?7(0_1#9J1P zhvb7}zwp3)@E`WBrL~dl3J)ADE)=)wLh1b|J+57-q*6&mOO~bX$d8TNXki&+3|3n) z7|F6X6T64Ty_z=7WEut%rhC;B0!@2o!fbmtOc)3RCd_h3$PdUii|ljHy`@J#HX$(d zLT6!>^(d8Uald=c`Of#@BMpVtK)vI0h#UWYg2=%RhtM>myi-j|{?@AL0%fd#RIcpu za?GJ9{%f0IoPAGdT0|=~w%C9Ea&yKSHq#;*BWyA)$XSveeKSzjv1o|cT)8X$bTqcR z&Di>kHHG!86e{u(p9zEzoA?&g;q9taQ0-}QkDlOWo_&{@zC)wLM`)rkLOa+v@h z!#%x;DbTTaf1#WRcI>XA4`0+sFhEZ%fx;p&3e>g;Ise%rZ~*^^#$h-VJ@(d-)P zqzDRkq_WGwtQ2J%G-vl&@imvs;2lPB%p_YZfLAW&BU-A#{_)Kd1kmh&`#PyJt>U5z z0KsOoB{Nnos zkSt;mi2u^Nv_{;W|vg;TP{GO`c?rA?3uv}Cn1n(MNMvWYCbTh`M05Xyq; znm~8@bg35#eRla+r;rIZ!SlyEj+Tx~9Jz2ElkLdtB%jeW*S{)saNHgJ+ntIOYZS2Y z;}hfN>@3YG)eM7)6*J$*j=NU-!(gUgLc47rs*sDZZRVtpr;5A?l1jvv^Cg_`3los$ zSU?D^M8O*&V}?)!Mc z;$`uDdiT$_x1;UrUW=)oNgPPVB0k|6e*42W&y*h)%H+4ggDfQ}s-$mrdK5L(vZVn{ zma!a$BxFow#&{SVZdnOJ`xok7sbP{GAnUPXCVos!{lf9&ZvS5zDgR@ueE}qkSj0yU zB=#p%?2x6k9VUB>47cUiw!VwgX zk4;(tG^2j^4YvR%F%6pZ^I->PGiWuS+zx8wF1jKnfuyj;AJn+g zgy-d!bbLUd{}AgLX7+a5pLN;C<$B2eKv^phaur+aQ7AN2&$ZV=P{D&IUockDb%7&# zA>*Xn6?R<7WtL+su1DOo?lFLUHDyxJwJY;#zhnAgvs}s>ns)R3OO00O*VIut3QHP? ztW8@hUk;~QzZF~gY_T>Ybt>@c-USg0V=gx5J|b4o48UH{nQsEzp?&E2%M;>SK4?uIWDX>d`TQ9G z-xJuB!*m0X|HU6q*LYs1ammkZiJQK!=2SM@cs@(b!ZYLnBv&_1vv=v}cKdpJo85}c zq>vikp1K?4<@1+W)iT*pLX!4QpJpp$Ki)~cLo~~GiEoH(qldLj<+m#`qqrpb>Uumc z{@)#q*xV=X=)_-&YYFSKPZvP4h(#=764vS`ScTbCr4z9wb_%4&xmtsO@Ozr4y6lnb zKh3)A`(CB{A$`Uy?8}}=Itii1gx3X2>gZpWuQ@NSfR@rT`HWsGoH6u4`>(JLx!BQUD8^&z$s>@_ck|p?&oX#v&b9I^kAzgIz!1)Yxk&Etxsq zZ(ljYQ#ioU#bG;J!Alt+7?qL%S<6zkgyN0$At}k>_8&Ri6F5@y6f#h<^3#j}47sdE zL`J~a^qXKg3IbaMg4tSJTpE#DCIGpDS9m~2^gbpgj8)gRQLORV^WTz)Z8r%7?jrjh zaW3jWH4^crmB6aHC`t}uV{z`J1i|8AaLFXowPF>!m0P|Y%_tXx!VrNJh1ck(zy9TL zWC0|LSj0yjc{xX-!?d;B5{rwbmaxXk$$hyWrS=2&c3z_K9yA*7TaUyh&3C%lY0aKq zQwNV8J%2%uIe1mCpqu(C%kFM6QvgO)4?DISDMxvh&yd8!KTXE2TsxC9E0t%LH!&Kg zSC7xGG*dGTbr>#E001BWNkl+$q1@TyfeJpIv zfj|cfd5~P9t>dCU6uAPUboTnI7YHnrvOeE<@D*#@Ebf3WPDQJgcb(XeTS;&Tr|e`% z;Eu_Q^WB+gDanNf-sCC(`(u?cbi&28yRa{(7LQ7H!Bq_wX%IqsE+aw8c;>3Z3{9p$#}i*t@19Qdn)^Yb5QdCNP#uqA9_^=wcV$wWGxh$7xb z4roqbT_Z^lK$B>s5+g;8EQMWk3YIX_-VsME=8lR_i4@_QQKsW0su6nrY5^pRSi~ZN z(A6AC;IvDINZsnnL&K*mZeel7wCVL+mmS0WkhWGZG_vJ}pe7GXwa-JF0lL@_`sev; z$J3W+B4gPm^mh+g89VNJBw}adW<06uoaG~)(ohM@hrX^kh3Zl^ZnM8%+HN$jX=egT z3@xlBO|7FDry?cc)*VJqT2P+Dk!ed|&*qRr_4PJ;PnwrB$5j#mPrP0q%bATy8SL>3 z=}(?K&@dlip34qZIm~Y{_nnhCX_=UGrDyL-S}Jsj+^Ud#N4>GqwdC>-e>-AqdzXr= zpjKJACwKf$Zji`Zc*~Fxf|R6y_Kp}mAgsQ66w5ek%X70ZW~r2+l>4+OG81G5|9pN1 zk(T(dm3LA#6sjZvUm|9Qo&U0TEv=1YS9r+L;zDst7ph9{N0mwzReB4(EZdQ7ZDiwx zWsDKXZecKPKXz_2!QQ01F*6tpp=V~*olTP_Oz@&H-b}BCWYZ*&#XuI>%%7OW>~qh( zrH7{>i&-?ypko^f*V7N~SKm3``3^aX7-;4!uUR_d;1N;WbU?_rlxS#d1I(XwrFWkq z&H^bKA%#E4lvncm>yK7yp8?4%X7Q&hR(rBlUOz3nF?{Rjr^-RXu2{ zp;RW!aU>{tiIun1tnL)9s2zIu{W9z7^OkaQa-wOLrRzAlgt2VMCQD}oR;xGbyX@g2 zp9h2X<_Kh6x{Z?$ap>c8rye%CJ3S6JLDo<6%fP-^U!PvSL_?4i&X|2(y|S3e+5{2| zx)Hk)1M*?egpGL8KI?X{4l)-2WinBa7U+Zs0X+E^yV)vJ&IsTPB(r#V$r%t8?Ha}y z1f!lqFvvej$O4gcE7CKTDn7h@tib3xeH`3inCANTX}88W)Ko|W)zLJJq-HeiE7c|R z+#CT3j9@O&%=@#X)H0D1;eW!A<4EItC1O_FUVVs8&1Sj&@5dt~Ga#A8EI#oV7Cs|J zlo~|>sV=@Q`=!paH0bW8Wjo&9dT`33aS~l)wAJ*5$1Nq$^oc=<@wl&j>=W&ZEu2fz zmV-%*N<=Li4eBo0y-k}#hMLehmXx46S5%#(qDEtx$`WYf6jwBZ?I$+>XuY*HcFZBf zuf5?=f{i4)qG|Lb<+u5QxO>Og)=|g$aG7s)p;jRr3OJx3_X@*;T;Ic#=$tpW09}Qx zEkPy)BrMM7KoTczK8(6wH$;ieOww3(*KBkdX*aOxMtN|LM38j$<9P>d9u>>39Vw#g zP-dsh>1E$t{pRi6R-r`2+(yvF{T{fa#3+Zmf%rqfVhfd9G)YRAd1IZos<1`>(keDV~T0)~4kiXg^WdpYMH_ywA{MQU_}J0b2l00+}ONm3jW1)8Gm5$xBWzw5u+wyP238O2$=P z=O~d-6W|Ke2t@Q)*io&0PYN6fi~4)0rK)=Ti}#G~U(uppXURY9oNy?q(AYfvy|ihp zGv*NJ(*wRFwlgG}oL;>HR-1}kQxk~^^f+DUxLy$=6B2Vwa4bouMjE@~y+0kcVGVJF z&|tw6GcDqHLF{K75O4`gQjS>DrdBx=Lq(cJf^NhxD1km$0+GO)f)Y-0PM=V|Jhyi< zcHFb&CD&6fDPaQ3l!kIx9sx-gf`tsRb%>GgzW4V>K$2*fF?6YBWV{HfZDXuP&%Ut=yxLO(TDIjvQfyJ>13hd#Ck5 zma8Xp&dnAX8YFCE-CQL(Kj$7}scmHq6MQco#lp6G&-Pa1gz6<}$xf0****dbIPj`r z=U5$ezvEhnl>An5(E@=MR4L@E72xcSpHoi7`qpUh)b%^`=m$pZEUC!qHk)!usgm6T zNd#zncT&s1>U(nWivct9ve8^ZkVyiG;NHE1K;nvSdPuPHn^)I#`HTuP6}Hk6EsNPO zC5==|Njt%sP;3#6(~^W@FH_L9T+KSShKb}m>wPe3xgu@AT*#)5U5K0cPZUHV0 zQj)Vdoq)DYre(pT1}cgCZl@iHv70?+_O+BF3(~O>UXVV&Cgch9(x98YBP@m-PcER# z<8mcR7JAs$xC09v#<8VZ1&Mf{p9(x;DP$jp2PFLRG+vf!sgaGm!5{Ac+ZJn>nyRBt z_=1I4m4mjk{4fpJVG)9l3C2+6A!<1|4mYw z!lfxQ7%R8t>H6LM1W?|9Z9=Fnp#d@IXDX_~$Z7%5qKpnTY%V+|#w^b+RM~g?bp#J=Ro{AlA?TTG! zwtaHJzGBbU_g3b~L)~vS4KS!qPmY}$1!)r<=j5WD6wm_sg`(HZEWXYp_t-77>$?1H zpv~|@KX`i=iOJ^jhMnNJTkCQNmT4|i*kc(chXm3?otObiot=~84mo67>&qJkJ^c}% zieI@o>N*@gI~nVz{>R?6wYHUAVZjn~gS3w}kz`%0qa*3aith5MY}v9ES<;Jb{9uXT zn8>oM!Ps#eGQ~-0E)R7?pq-gOo2JY#>5!RWoC(FyHu#|_beNa61DW2Kr{-ba%JebN zkRQ-}Il4`pX`Hd$K)o^c(YfuTZO;1kTHjjVRCBMA&CCZOF3z($x?FHbAq&{quLJG0+L1N~OLTBXgoEF=a_PlZqsH^U~BnYGsiBCe-K@i0g2%>=qM*&5~qPEb_EpI?gF+mo{ z)`rN4n!fgs;uo}phk1F%lP;AMjKi$%qrG;aGKNTyr3a%cxBEbkO3}xe!YCO-pfe;| zB*gA#q1SL;{Eb_G=l|2{bzh;f>fm5nu?6D-F|Z^L*tDSRQt5?fe|fvhVOa=Y{Mm=( z=1tNh)1*=;O1<@lWhTuyqA{zjAt(oR zhvGE|nBhl+oIGlI-ZQuxw+B6N~TJ^o%s|r_bxgMciC1GRT~-a+-t* z*w-=YLlX}KM~iXE>$KTJLmA?TxcaVKxY8Z_8eJ%mD?{l3DWAE~ zBqZAlCX57OAuOn}^+htUBEDy!MT=mhm03{${z>Er`ZA|NsW0GkO!MQ z25V8KswGy5qW8s`a6Cj{O2ui4MGYvCP>}i>Ra1Z(erM16S=3XQs$C>SM8bXKuis_i zCmZ)!a_Kd$lG5+fpXws_WRz?MN^QYjR!t%!tX6X>X3R#*jc_9mZaNCiID-HB8AfkI^+s z`*8Qx&CG|>-wO=uf9vmn2jts4`hf$yf^g zU+#5G%GAQn!NkG3_HNA6lcp&4;FV?lwuR-M`6 zpZ)yM91;p7t#|VAz4PZjXzhM{>HN9BEj?$a!SIQ>`;}%l;D64yKJDe5>^5ig;&X4F zej!WpqJ5anyFZh;bMTof#rC6n=YRjfw>M1Yz1t^!B<`Mmd4qQKwb!oh1=9DqKlA>M z*?|qn^QR9>|9bcQrN?{dX5X;wSs>Xo=+@5K%3Wg8hf0TKr>zPdf!~-GWbom1W@ktA zy)H0{BKMP^Y_^nA-0h^`d_&;KTkk(5Ma^jZ#|2ro_){vtyp$&DjHgczQh7lf-2B=$;L*U6m0Fl(j zNQ+Go+>4i=31OtVC|Idvl81vDo-QLEv+FAWgUJd%`k11d5@s^;6S|kfR8j&76T=oP ziqIWZG{EjljlG4((aMYOF?!rvx1~v4<08XWogg_w+|{n5PJ3;oX3mzmu40ExkH z>guI^1(I*vY2N)LzWt|nzgYU!-TxVou#vd;(suLh&rhHJ{O!E}$^Q0t06=me1M++z zx%;_{9HN`s#hQ(&H&;Q@*~ zC72ZRdCU`kuM0L^CF^|UDmK5*t1!jj;Y6283dTvE($xYvnRX#N`^lYlNK2jIK{XH2 zOA*n`EXBz%^6{Lz<(6bx{o^}aZL1#ipb{b?^-8Np6NAYL6H{rVS8J8>N@TPS^h-vj zP&Mo|g<=dyws}SYOJR$W{}6X|KW!yf+|u}ABzrC+85@j0;R5m}#(V~n1;Y!VJOGO9 z$gJ1Kj`#?Osw7cKBC!dIwn|i~T5TYrdDyJFX;h&_jrOHg;9-l%L!*k-zNOU)>m=FFS}qqdG#)1EDo{_B7IY&cQ|lKra5cVVS= zqNT9?x)zWWirLC{$-cS0vS}$jC_c;3o0YW;uVAlSV9Bhl9P`nFQjit4ira6DJdr5c zv@6rV*RNI%pIA=Phgg!RzQ1MJ+Ar%-ynv)`x0`gVr%z-a<9hpe|CoFovpbo{8C2O- znGkm;Qny!1NtJ{kBsksPwPcDVSwv+*h(KJgNXlDd*cEwdV}i@GpeIEjx{4Fp-CtSD zR(7gg8O&u7MP_Hgpe!l+LFq=p^$HK>Ao3(ek&Mbm=bL570X(mlkrjnCi&O3n=TOc z$L4@7k!WeL#XNP5HU<{Li+neT;8?Jgu4{RSN@4?1Z@s2nkPhZ?;y?Eh-42XXc>3y9 zbw3uEnmvanlXwD8q@PzQCi*07LWrKc1iGI)xiw#{U{Y_mhll^D$B261F&y`nFE`L= zFssI4;BV0L-GDhpK^?~NKr-h?;~0WVEhq$r$dC*8<}l{|F(>v1JKb>UArA?HKoWRR zg_o3w9s)WF2g9(ToqBZ{Yhb}t;M~6_ne96|M_sfKfU@1wV-wL4x>0M!#|C-tAZ^U6 zOKiLGm%9);19=#V_rt{&<2lAJ^^K%b)A;!W+87*_VHQKSc4yuj&4+f9<`etS7q0jC zm~Q5tqh^1?%l*Io0zgvSTrNJ%m|utQ4r?b`9vzt88T<0e;U@cU^T2$^GuL)AYnIzb zg-xQ?*dG-h9aN4T;_HCqiD~C?xx($j)}fIn5=EQp`u_HJC9XW!Pn_S1>e<86Ubb>B zDk!Mci&-O&a=m5W+T+h~R8Hg!s_ecWb$g|hOp+iIa6w2)iQo{qJNmJ3+g{0pAp&+4 z=?GkucOp-1tXlG?Mo&`U4<_#R-jp`?jq0LJ^~WHJs4_bX2Gv&7ZWLTUJK8nnN#=R? zi;^mZQwEZ*t}Y2AEeuG!=K)&`X%3^Ejfs!G_)QHM>Aho};|j-}Q5yD8svbe>fcyuH z4fFlM={}>{Wq~8V!U2{T>;cxt549l>xXf^)PK$)w(&=`aro{$l7*foPEdsZuH4)74 z_z0YkX+CIlmF?>QT>a+nbX{vPdL7@Ksyrm(CIjj@JUuqrk0-vyIBCKX1Vczns-ybo z6t$lWo45UcX4YbVVyc=w;oa6O*D4nK@bFDdm}q@Kk}Uw{CigqKu}_k%?s%-rAV zC?B=?oP&~=!4duIc&b^4dIwD#9EXk}wy!tgCm_T?=YwzV^L4V>W*X@V#mkDt%xYs_ zDur_W6S3zLv+?QFI5ohBY%ah1`Fsk&uuzF+FAA~3W>_}{ldXt_p8vT_?Is|xA7lzg zPff4G?(*B(i7%H6$~#kf`Rtf}E$^D{#H+JFI&X1Va9yw$){SS7i3WovcHe`#{FtR= zNWxxxnw5k^g(puf&)ynEDwyCR5Km+;S@uio29WSPwXsUcpCUa2NU~~o(0Xd8R{j{w z5KoTJUd}sU{eJ0F z9UJPF0RRV$ z)?0OHM2~0-f%p(;3s86a6pP!%J}@8Cko@g=f(C(3*yaWVu)xU9LT&`s8GFgT|1)zU zfLlN_L>z(^KMKUS{zK!BkaXCIeD&oGTJYuz0F#XgFU}{pUzIS<(N+-%OkwDa!|q{T zHt3JU5sbt=k&b%!#p8z@IaaA@s1Zi|PV!JxfdX&=!hHAOXmoyO=Yn%%Vv?R&bf3$o z<~LHL)u0bSXLBd)780Pql(-0!4dMC^eedrh<^NVl%0RMGF{gY0!t>bv&c zs?~8pp{7Q%F1fZ_%FnQ_Jc`P0O&~duy1i0LWl5N*i~DRbTP&3YqA+2IpvDATHq|i6W}Z&VoU;6}217-Isl|Ci5ioqWgJCmBOh4NmrMFBn(J; zFw|Yb%04xE>(&j92X*F6zfONAxWZ9n%sOR+9vPBQ(?oO#zSlQMP3}?Y|JRzQ=8^w? zK88o6M-{pv_NV(vpo0O54fPHm@c8)#;6^Xf5*i9ExE82Agn>xI9P{OEnoi-rf;^0XQMj6%>8%{?ue4skDcqC98~As^`i`3AN|g)R=ul1mYH)G2 zkH_0^2}?%wgBQ&DF^YV8kibNrqY6rFJt4$QD*nsV5ir#`u!tSccXMgXcG8|7xWq!* z;<@1fBB%=z!g#UtC4}8+JV{SXBH<+^PxYBF4`C$@HvwVSVbQi&ZHFG}4Wov51diZ~ z%zWPsJeOUlGm3RA>4{B;as$uL z@66|N^SOD<>@JZy8ixdde0@2HRueUlG`n!eCy##tAbF(@1wRngFqXjn(mQ>)TXmh+Ak1l#zj^|JGLE74JS0be`Y4a}CcCV@EX4tOSA*7_We z@GZk&lqcS}_UVO$W07*naRBJ6=`#5#=Djev5ac1BS^!7sad^!uc5$Q?cTu64DT?Yh{kQd@dTu zB?$Ed8oyzYr7Sq7+1<*>G?bQ>lpc{fQ4kXu>;84h>H$z}pVYrtRYMB|-QK#_q z;+7f-(!RPiApO5I1)wA2xxIuowy7t3s1iBpj8b&e6sTUhrhMiUOer6 zXVE7+U-I;)Q#8ov`du z?@n|kGGY4psTa0xNWij)&K4Yzq$I`;2oqt3*D#;C1r{J#{;D@BB4l>H417LuignLk z#{xT2I^TjrU$BRNJ;!1)V$58;ar?!Y))`(OkhmN+ge%;wh3}K;pAi8Pv8LVZd1zPr zDOMO9CoU)m@aU86t=0$+pMwMX1$%r>1?a6^q&V{RzqQ>-o++F_d2{d?uS%4#qRJW7 zvuzS=g=M#dCDt8L?w8z&Zl^>^8j_@bS6(BUrcqO6%vpSP`ObL}wV;Yr9;Hw55M?Sg zYyp!vRN6%{DHBX^Iq%iEVUkAaY)=vrs~eZ|plWf3BN+AJ%SP;7XZ0g2qRu-fLK*t8 zi4QzC!Ryi8!QoMsF9vG4m1CVyc-@B4!X(K}iOBS5y-MmOjRjYxc#n1zZeG9Jq7zcQ z!L08s2ep58SW;$4EQXsXnI0sjI}FLuEhX2k=EA(1{DrlSTlr_zNDyPq?hH>DP^e^+}qVAtJE^7^JXRpM*CA~ zzt5F#qF&mv@BQT13=*S`gg;Mxx!e<1H=eT29evOOB>PV<{_*cCO#S!1v;1kTJYQx7 z86xH7AHQ4v=u}T~)BduNaC5`d7W?RP5fe2{C3BCO+-!n9tmTQ@uG{w=Nj>y>nBUOmhp2`NS#KJ$oGs z>_{=b1(w@rS~K@9vbq&9W-eZ2Z5d};XZUX=R4)f4Dzb6y5b;&-yWjhj2?J8O>(*N8 zKtoiu!|WLz>zEjT342rk#+0X1%`WVa*eLzSZB!wCJ-01{LWc_2zV(5g>60e?lN2|c zuB>TzNb^9lFgg52Vlw{g1xr4>}`MX!TYORdq3}*>R17mQv zdZAcf*|9=xPusrMu9gXj=mH12Sy{|1fOLsetAvcY&@~1m-Z(E`SB$)2U?Ghm)?uB+ zomV=4|hvX=!RDY73hHxY0ejXAuwkpMm!xQAWYR5&=DQyV2aL2U z!z=ZdjXP;M%N%h{)a{GKtP3}!vqru_f$d2v8cL5;v#X)K>52g+1Ecg3rxNFs2f%u_ zgn7Q%x8+EA34`Q>BIRYu9gh8V>)~Pd;Tt>OtB`rj8*(hS{}7OPc-GcfprHY$Xf!l^ ztv5bmhk{Av3sHMg{689m9v2r+1+LA4Jvbri5mUg)wLblnJSdti6U&`TKHu0HlRN{g zDsf60R6?|=DWP=r04 zPxIi#=Ad;@Bl4y`Osc!PI%zEAlh(B0r75gC$@nCxlSo#}-k}&JJ8;Jupt4y3Jj~|^ z3V=_W42Kdd^ULhT?x1jyY02$P1j^F9<{O7L7zt8pztMsOCwR+qt;{I73ZRBz;|eei zUXExBr}La8#fdH1#!GqA@x@gCS!%E}5pKp=IekEf65%Bzj`77=dRgEA$uUN287U|V z)qnZtjZ3V)kx*<@vc1i?mZs>LiwGytM3mFRpyK%Idi4ENqk&f+gsQ8gkt>!aDVVG; zaoYvJolQjixHsgx0TM5xksk{M*U29k zMPp6`MJzy4Ha%7AZGV@eF=Rwi3Sf_}30B}-7hBjxtO`;4@ucA`t9fgEy+S{^E-4a; z1vS8u+ok(_JWNuffC2-OLyBefn0s>2EBR+eB&icEQNSrBl~^p6`L%*>Z@qUg22QaK zfj2>aIS(jS3JcTTF;c8!ZE82TKuED<{iujnsl_orR>~CjvU+tW ziC4EnoPeHp(407pLRv<}zAyfE0wgCua>VQpSA^r}-VfjO`Cf_4W8aYP21ry}DBh?h z`zvHYc&hRv6i|GJ3rPx#cB7ru@FLg~V35j}hh}nSBVIE&Pi>F1AZavVgdTJSdxd@e z@y3S|38ZC!h>l%OYLY05)XzG)r<{!bWC93e)C@-t`ZELD=|IG(a$1yFJ@ku&jL`Ls zJM;1~W?6 z(wK?HC91z~{s=)^=Sf^zLK`m&mK5Qdier@~? z;P$`Tkk?Yy+7ZP;wSIv`8e&aAt?|?xuy&HN$sLk0&QAe}pvC#X0$Y~6PN*K98w__r zDIAHh&;;JxxJ{vkSplTgS{t4}#FCP9c)X@Dl!MUOv*Fl(*gK!sM$R*i8{RoQRJ`LC z^kXDhBgq;|Xe3$EAg^po!Ylb=86Rw8jKTH}W`$&D4@>P`QtzRxP3d-NVAJmA;u6BP zA!IR4>zY6kQUZHw1HBXid&wn-_77C$Kdtp5XA!a1K!yMkPo*&=O z6SbHOFv6r&ohl=mIGc?0wGGr(s56P)fu!iG5!GK9hT`ZS_aykyS6hHY-lnEErwiMJ zD<;V#c|2n^!y1Z<<}6)}4(l1bi)JUS$(c$Siao7S41QDk}K>U%f4G9IzQ zMu(}CkV`UnMu^pm)#RI-?suL`;xZYz+Y_?Z5*eY$Jjx-@nebEs@?mRUj zDduL0R-};7tkaK`Cu0O+Y^lVTAU^(ONp|c%G6v4rh0+vGonqQ1=})nVyO_`JJd=5? zGUHK2RZ>zO^kGQ=C!R#&*aiR#Wa64VoXP;JQ{3XSt@UFB2?NoYMWa~K?}*MTg1uro z4`n*(6LicB#O=)PlP{)ZYkx9i+|X6&{)F z?_ce$FScbj*ZsepS3q)!k-vz9$R!p2r9O~XHZ}RYLh@~BBQvhO?{4(i^y~UWwqjn~ zLAQyc7?WgBb>1#3={`oIo7P$o+QM~Y&G_Z4>q7L`&gjS>QNo|@m5D(bepyN%sC-= z%775@<&7#ZOhIiINBUihiNOvwVui^P4qBXx#04e=t>q3IldIsPKZrDz9%OpdIr-zq zHtm}rAAq=?2HGIe{3_P}jcJ&Ic*Gw!G9^h8#^H2CKwk57Jc z4>9$=kXM2cua#mZ=fjD>f_mBP9csdc2-#d1Mp1dlJV44GZEZHW^tO-pkB`LA-bWvt zS3vTbW8{ATD*i&K@F!3HITiYn3V-JKzp3!w58^*pA&JAlGMkg8^tH*5q6DNNM0s<` zZPpF1iIMH@$UeBsNS`bC_ z$u+K3DXu6b)%2OMLMl-<_<3EIK}!HxTXe#o&J==uIs&(!9Pn*?18~ep^gHIWCzomr zjImW5;?DQ+;%VNAI=Q+?dq>7j#i2A~+?`MUe^s8>V732n2M|HH(rH z<~N+r8_5KLF%sIUV9-L{@lOf)pTCWrRhfduH7#rq97!cUF*?e3^W7V_#n#`&)-2cD z&cmKlGf5x~4eUoF*S>aTLQh#n=J2T0LcNfll3a}NFPgDIsD|8lETN~a813PL&x_Em zJ$%aJkCLt5iUMZR>cU$XoX+_(3y~5uavD;9n<8_C-OzT#z<`8F06lEm6Vr=aOpeOO zrK8^Z!IPW6{nKC0Nl0F2jQkHk#a{>&-h1-TEhI0r-t||V3V;2jQ{k_FKZyTag~T^9 z@YsQ$i2Yvf{gfzCl9Aj0j9n&fnk(-45$iQ&%u8!+%{T%ve#KmqaIMbGea$I~#LuR+ zHp*iJv5?PpEOe|>RV@U^NhMb&Da>V!#&Wch%BwzpCC7z?RO(S#aWDCsnUwV)WX4XQ z9glagurN#-CXguQJ8tK+m{d0DAW2nLMtFF#l4izfo#}#=qF1Z4`}=cj<1kBen6=U1 z_xJh*ha~3>XgmMz1sF>Q$InP33^W49(sG@^lHrWOr6F;D`C@CaKHF>E!Uljel*km> zR1H$)Ul(Am6$ z2Pn*x;L5Tybwx;yVPPLu3X2}0b{!5Lehn!)NVQm~O%qiCb+pi^Vin_oNR?`>b#1)` zK|B>)8VcFW$ZaLKp5B@VTP|+Tn9Zb?Kca{C9)9)V$Dci)n7mkK>ov#7JE-{6g!9io z8u;jQ@8oG#_ytkn-6t=PODg<1P~lInaG^rt9~vloHo+w?bl4z@9(1!-lU=9yp68*& zRYyFQH@xto;rzg6SRqpl(e?Y@*0`J2a*|>vQ3=uAw|h@Dt|GKuqg*pWEFTbZp#}*v zMl8RYF2&7oEY0sqPH&$ubBnvygLrvJvkTxj_3VQiPJ}Xv9l9Nv=^9IW6_FGT0I48s zq-Fpqk91GL<0+WbX1)E{9y@C9S6R1%9F=RoIHg4u;D7kD6|8y?hT%dYE|6Hmm?-0S zXXTAhk-!u)8y&un`@q5_Qt@0ySO#OPo8bXdmPaH=c1#g6jJiSZe+r{lBL)^as2n|I z?>T0DR4p4ye37w&u*bZ=7a}WB36LZ$#%32&z?NpNxY0&cNKO*WZk&$S9SriACP4Km zjY87E+}{0%cOa<3Ts&9D#%rvL%-@o+Sq|(u9*|HSATAu3Sk*U5!(#MTJHsO*KWq#N z5sgi>%*E2L-hTI+Z{B@AF?q3o-ub6@@;h1K=S_uopS(OSsqp7O zg+KcUE;6+>Jm6F6 z_k;wvs};xKWuzmF;dOynsWU`jQUBu`{7baUbt#_^){hfP3SY*~7SkL9RE3Cgm3{-PY(#b0Zqx<)AK%N*czEzF!jl5!=qGC{j~165aGWau`( zm4@z3&q7X&w!Uw*0B5m)XZ@cl2N4_qR6;7HXCHj_#P8nz?Bfr=dM+_}!GPp7$H@QG zqq+1E&j0b}fuH={JL&!O|NOC+NQHNvyelM^RQQ)qg+KcUF0$ATQb^iSop{4_7ivl^ z?`5LK9ivF2+(^5gLNfKHIBV%#MP%;k?iLwY5#^D8-<)GYEd0>~QDmLn40&=y!29VZ zk5D%Pd2}UiMzWD2@_>h#e`H8BbH&PR%PeLm*abCyn?+;Fi7P9Dd0iR3Sp3}P7CTU8A2S@f7v^`*S5|(jt73a_`%t8 z^ni4vBgscE=aGaY>lMpZWV^EDo7nimHpUq2ST$9gD28RTQ$lhv5)%q#DlWSEJ;zWUZO?tKZn>*H7wGw>h$Z zd;2E;t7iG0-VUs{$Z@fWJ$i3KCoq;yguDFy#vN9rDB~<_GVJ?ptz2j)$TZ;Ny+7pS zRa>Hwz`rgqlj(r$>I*nj>eLfMK$h)(RCO_4eFJcw zNPdtQc@>ci6V6}%@#`WHqCR{oEBw2r!mB6m{`-sy|Kh3eFHuBt`?eI4ozDIZcd*d# zY{BlyL6X~HNJ+yhm;jIFoUAQDtf~qj5s1nu#4EpMOKw^#=BS50C9ZN8TI;X*{@zs| z)jI`PPX=eQb~Y~=!2X1_qU%{p?<`ZQ8Ig);{ypEcA~&!0JELNmcG(U&ZCa1m!r&IS zwhvg1tpKvg%!DdqXML%l+-2)oQ9?+H;9{B06U@rE!3yy6Y|$0mTxCb}oo#ygBBfcW zC8nAY9Frhku-7PA1KiE1!FC>GWJJuPSz&|>k9WqP76ZPyZ_)>@j!0-1R})b>vEpKb z-vE+J^N2aYUl;JqPq{It02$l?m_iHz>I5|ii5Y&{br|Jl(V{Qx@CsjlM;FZsnWlT=4^gfSd zoUdi@_@XMUb?8pB8?dgfPN^#X$nlro-thwDbc+Wdet)8@5xNozdx47LdNMqr1MFl| z@qma&?yg!l+nC8j2gtrbO^$Ee{P6yjlVc=#hO|>Nc&sqVg@l+c5my8jvL4NUK9AE& zBL5UtkSjCKYAoBNbwUs6@v-Enqy|Pr>bN{b*i0Da{mc$BVS*la46@M9r4cKlnh(PG zD#tD!(iX}(gbG~LJHG4&s4=<$Z~aN!i-mGzy))@hQh0FCofE~4peS{Sd*|2R`X_9f z@;sz6VU~!}JgXRif&Mf~+T$6lsXl~o5TgDxR-kSwzj_o(5Hla`wTYJ5s3+TMuw6`^ z81u;zHXl}8yU8_SvXWiPc163^X3+6R>>YX2Ncw#TE+gfe#N>Gcl4oG#-*_5E{*CV+ zMqZ)fhY9EBzWSL+Sg8+3S>dNcg&!i*pHbnz11kJW)V1Xbi95`$ZF+h+sOkU^J_R0h zM?j*U1<3*i@c3o>l_whC-!K)VvD=L$M`cJgfSu&7ym|00qtS_~#g)U>W2{#2jaL#8 zqYzFOQu>uKo5~#5d0#1IUA-$_jLjjr@TKJDdh1u8b}>F8C6t-Px&;U^q!^vnhB|)* zJ7tGLw>3|XDapD0w^2WjQNsE*ZO&b}OdlNbz+|;a2BBPj+~s1S!Hmgd!6X}N3r0(( zpyVRU&s~`+Lir(`-xR*^S)eg>y%>G3NK0O6D}}YMUxN-2R#-)50TdJ^1$-d>N^_%> zDyl}U?O%vw92%i;)O1*lV_sH@`h0G4FAq+305e`>|#5VO|Z14^kiY~ zj|T)b8aIddkaj&Jz->m-$*uC z8RprEs~ADHN{?1qo2Xaz=(u!7#O)QJttOqR(8I&U%MM3Hy^NEPKPPtRT$Kw+Xumr_ z*bYpl_^vG8@N>zi6CFqkl8pSFH7;7vQGr6)X(uCjB#@doH~=dp0u{E~nA|ETN{Xs| z`1~|$q7g&GdzCLF#oO3@RBpSYL|vg`(HIJ78Ni+ha85B~?X~ z*&9ohVXZLuNjKcMWHe9_bB}l4GICrwDndV!6jIqOUH3&SUlDdfB|RRGa2*L*{c(1q zjow&3J*p%hU+T&??l?fseft-UMwO~eVa?Vy(CN7vidpJ(dXcWo?h_DRPw4V|QG#}H zYPLmJFFsrpy}^_!EeXWk1XUIDyQIdWEfZmu%!=NsbTXs*j$0QGud^8Q3Kx@rNI)&Y zn!|^bI29otq_+I_BU#)D8mt1iPz zAdR#Yg9{f#F3bv1;b>y%Q|hv+5A6*=jW1ONeC;EK9WHq0)C|9&&~<0hv3Yrk-y2sU zBwWtZX<=Dk6JZmCd;(R*A|vMYe|s7upk*N>BwAPcqUEH2@z7i4~WNea8MTRheX%Q<+PUy zEQ%hDSOs&QT_}chuTIXVa;w5sw{Jw1^BYCYA03)efBbb@Ig*5{Snm=-P*cSXLhAx} zBBtAIx!=1-YjyExwM7YRm{8lS5eECtX1U$7A!)RlJ9KmLkS-ok9+>!wZO1nX9n)SK zWfw%#G27saz#36TQqqE?g)=M5h$XtFw)m)!AIkNj?QY2*>^Tix6+gI@l%G;Ypd^>s zElXcs)4-W`RLEp>#ln@ek6O$Q_lrfiZffapw(Cy6+hS}zh&0C4rl=wukf}97QG5TF zOmlfE;)mukiwP#!G|upk{6?@ljtwYjL2wEyZETNuJ1G3R?7o-D{YTIEPbS6W^C$P- z`{KQOPd@+sy)T~p{F5^wc}0&iD*S0bkXJ)UqV1lLh`uVK!jUNtghvxmxp0q^0-hxp zU^wmw9r-t7*Q!*d!qBOmczc70081mw`FI2*zJ7E+6enU-WH%lmI*rcG{6tw!uHm{ z@I;gZEcjlPUMkF!RG0-O#AWyW9tRm`syM?!fN)X?qqmzhbtG@n858VLwquZ&K-jhQ zIq^Iy+4{iESNbm6Lh?=CHb6v!S$xz?Skdj!qOf*AenoVGL5haTF&(`YSQ51|61Tf8 zsn92G1qmemQCW%GO$HJwg>u4G_{sZ^&=|4X>pC}(td+3PU8fqCVWaOjpj0tgfs8Y0 zUPCyAc^>s7JnXa)zSoaFdcg;+FD6eOKDqYUl~4ALQ#llp!`tEQfZv|qwZ$jl63f@_ z7vl9_{O-<}_q=92vefDi@bwvY9&NqH;lHN< z3R0^^BKf4y5lNb)B%P58BZ-Bv>(>W{`uX{wjl56I`qSA^CTcE|?QeYa0sqSGYI7et zEeAV>VDRt%V$H@HM^?pS2IEni5jqmc5p;{zCm>2b3BWg3>9#$^#Ef^2`MV31Wzs0! z=`Ku{CJX~f)ZnFrP$5!z>YK%1O+3cY5`ShL;W+SGM)VM)j zcoD+|ei3c~vSh#fzSUpe_>QYwU~x~S$cyOFZk^1LdMeu!d$kakjkdOed|b*U8YpS( zgdWj+-Dav)@aj4L>EC|Nhs_X^Yfm0BDS7(YFJ4_t4u$0Kc6dABLXzvJwyYXPWy0G| zo04EJ{CcA}3jJbAY(y~U9ZMFZaYva<=wjnoMy^Q)9@xlm$rl`@)+saPWtL!knkg|6 z$K7`&X|5vW6d=A>JgR1Ry&YPzG%4)tT0z|;DK&vfyU5Oq#BJT}cRJ;$CuVi$-um%r z>#KKyfS=-qpRcHGY!7t>cP1hV6!WNZ&PW~$R%7c7EV8XGrU92ce!X9>{H7xfa1 zc)7dd(a(`g1zB8iVubJ#Os9f$)5iGndWDQij{eykvQ8)NuhcajznF&f&^fMkvR9Zt zcu6DB)>&vm#d|U(a#;vLbfyW2v0F|4YEQ6mZ|lYns7m^PmFe}OAKeLpYF0pjV@>YB zM8DSbB_cHp7A28{n==?G5sx0jZ{OI%2ksA7{`T*%i4}g#yY>fQT1C;AHIMUrX+oVRjSI;rxaEg0T zGcw^Ji!mOtViigWth@p95vI_qfc(3qJ$cWp+6vD^%%(LlIi`a2=5sHaB@hp+);EdT`K`II1}f>qAT~T(qcLJnz&KFsjj_ z=v>-}400NMN`WrSUZr)m?00fTvzEC-hAp!2)-8~pU7vp;LG)*BT%w=5dJ=5hN|;8`S9 zbMiJWEemjyo7-!2pttNRidervSP{c0(W0vy^d!UtT{GL4XNrTw4`{0iJ6el9%_Tyj zG}DWa7snI@Ba4PZ-S>~J^KOi@_f{GY%hOjmKAs;|dy^>gedo%nugR^?TIv zWco(;@xBssEZX6@?=n1(WSU~vQDpKF>C&RnfsKnF9Fo;Dj&EMS75-5Z98p=oVVu^yeI+Y=EW_Ixyx6`L+{U#h zI(*aHXi%JI%O+wyIBV3v9>8oEbEkJa75>WZPu6sEyI^eJHpriDe0LxGOq zV;y7Z-&BXZ(i2J$`m{-i#({Mr`?r+{a$#`sE6(z@Ynd}iDr2KFf#gLZ6EWI!W5a>Vr9?k^?5PU z9BiFx$K%yDfObb>e-c7)8nXGu+MDHDCD6YgmPMr{Ijvv6^QdXi2XskV1dJoTh8!r| zz*XO{P-FsFnaXX`^3HM9T+_@xIc`A+s}4Iv;TZB`lW-S}yr6&>A;1Tg4t__k5-cUs z^}VepFrgK{MfAMY7Z$pQ{FYiZOW>#?mPwZAM1R*Ar;g0imV~zQ$7{=eB1Eg413JMZ($Ba-}xZ>MSk&G0@T%FEF zg%tY02YelZ)*1K5UbSxCdhVJc@m$2q(tTf0JRYn$`Q&PUygj1NcosgPFhhohjwq@n**-*k0nYpiCYLk$a# zO=KI;8Qm1!ryatnQSTTJ_v==b#m(!#2SDrkyfT=DCjyJ>aI*8q^}7jj4Cj6K5~Y|V z(R?gXcrM+sw`CGp)vlMH4G`8*O1#1(+QjxNrpwXYnyTZVb=U0#APvBu~5EBb+TF?vXN!jd#+0SaW6W= z6Pr*82w6K;01^JpQY$_~&&QRCNq}@Fj$6Z9%7rS3#a#<=2q6q{x}hVEQlza(f3Slr zg`+PNM=Ht4nbxC$HFBCD7CQGFatd%_<#F2_d|c8VtZUQPIy z!mFOQ);xBpg!9=@27&{Mf32Nu$xQn#k%_fPeRQ)8g|rVi1%7CVG6hazmGiz^L^y44 zfr_Nj91~5kJDhVuK0t9%)zP&>ym?=a5sKib%aup3131k_H##l7%e!pq*oWB{D$iW$ zs4@x{QPY=?x6g+4EmEZR3RK`>3OK~+ar;*koj&2O_P7+1?zWpEf9KWfAION8f;rW~ zPJCTK(B6l*roX$@>3y8DAf)T~M22!!fXd+r}1rS4I=UDSqQfH^Z=oCaV_v{!F&B>T*eM*9lkn23S>Cfgb|$X0`;K; z9+TknM;-gfvFXZ=?X3UrSzO6(yWgPQw0gLo*m8+%12Fo^3mFVd?l`db9&p#pb8ZR- z9~QPfbV1$z?-|e&(rbG!Xy9c?nLLldG>t6r4TT@|bc`?d^=B*122gGyHY?|6bTQHk zRyIGCUfGc-K2mbiPI?-<88#fwnW(FPj|d&!n~6dV3jl>uN|O@qy1j8p1s_3CnxsW=r1}{6=TX zdQ=9v3SIW#t*^9lgM;d=*JdEd9GGO`OsqfXd<;)lQ&dM$F* zzKxyUa^9Lsj@(W!peCfldzASS{eQdt%Z zCdzIMy@)u2c)}BTWo)T0FPY|XVo=-24!bH0Ba4@jGeRUz8$L3x>j@y|KB9HH*QB1tXYu=$tz=;yO5DCbW9YxqHq7aTmi zEgl-6zpoykyRS~sKm57vdYIY%xB%FG!-|P>?VZdLbRayoPHjU*guuNa$$Wub!3~0J9!%FmZ5!0*{UdLZZNV|RSc$H>z=8(ec#FZgER1@CYhm)6SwyC$;Z_1IA0ui zGF`mluhx@2@qX`+0txc^4Nbcxp$J@Xke}pT#rafsdW3Cr1=AFo5!sOxuQ!m5Cyy!o z`_SUl%?~H~upu2XWKNe>wCQ!TkxBf-ltV4B_gq!*spb)Pr807hvEm{PiZnBpcOXe1 zXj?PK4J>pk`D(2d)$h@4GDhnL6_es84C++So*nnvUat`J_^3eP2dR*X`*7BsMw)D@c}>NXr>dOaO_G>DrZD!Y&k%b&*c$8E@uUSSV zv+G#oeSb3NSxg!S68{i9(xR^D%aL;kf(mqi9TkI;IShKQX#BN1OL+U*s zsJK}IIY;4zx@0a@49&oBqDwNkiO=Y1^g_C}iUNUDt8wtwi^PBr08VrT{cIYmT1el2 zd#HF?atVwB5=2}%0B&AUu9@5V$Pa+!Vfa?wLjZ7X42^(fo;;hx(v_dhXCC6qWGj+t zrU&PXgb?jclMkI(;RFz%MsoL6_ZuKBPMrCVsztS0rlF<3j$`l~8!bl(f6oPs@>r{U z5U^Q-(D=h4tfn8!RO7y72PZ6=ZRl`^ms?6o27zKHcWZBuw`B9p_63SP&iDHy$VkW$ zhqwbMEe*k0D+{Xtu5z=(CTH(%y>`cYe->fsiicjg$$@GaMQwW zP{Qh3TWmIkxbBt+XAn{<&M#12PZdOy&m&XN2q|{VaVJ75Of6L}93Q`b`M!z2x4&APZhRYX#oigPZ*_kM(lBCi>kIh7!r#A76#817}$*bfpsuf<+ zOyW2qj^+k1V0aONn;1TZgsWc``QrsHHMq-!Gy_R?Hh|C1T9sOhUIZ%YMF7gv5ca{p zLowt7_5C@#3eO4jZ$wYF#~5B-H;Rg;d3FOWEkEV8E&uHs`!dVaI8-+EBSY-bArsB1 zwS0NX>a5&f%V2r^9CgK2gEVW6l09ZMPRo5JN{vK?5_rf^^5ff9cQ)0;&;|(!s@Tc0 ze{Ra&@H=H&@_B*iL{wog1kOZ2#IIi^02N(Dk7Fs{qjUwf;6fmmpB#%tHw|8Og?tY9 zE>q7Vr)$j8&Sn*yZo=Qz$GK1>CO z&iziQ&j>tz2VU%7_k*do;Pmf5`!f5EA90<-G2Mv8gsTzG%AS`e&kA<|BN_dD`DA*% z7}b{h7LyA@joDmrvKE0qjflj&?hscx8wCP?{fjV9sk<4=GWuDODC8L+s70@RB|sc3 zG$N}P@i|h=SDl&&7jMtzx8RNHgt1ul9|*^&`t`jx3*YHfsNh@#2On(t1?M3SHPIhL zK&ql+i#SM~*E!*o@x{QM*qFCq*s7Jf*|4>u7|5ssvM%-44H^`6!}tckimFT?!UyK$cHMui6lyhKE$gfg&$g63bNl=X|M#m9*;YxlNiBR=$O4b8JrQmeRd8D zRw@8^l5Yj|vv^Lp)=TBfH+urn>q}}eb_T~J+!lEPg;0E^{9@~ZDVLf(7(S-QnJ%hA zT3Ar_?j@Ipsv(cSA~xlJedYYJmxp5@_|r+nnc7sG!pm@zEkhT*O~LnvhlRp;F|x;y z_lx4y@Nl=10uWJ*;|8oNHkI;8f8j3H`c*tAN3mShqmo!oNGY@Gm#^R>$T-&aCN|jF`J4+hRPkMog`~>aS53+#@Rdi+Vp}vT8F5; zqziCSrQTT+c?NtuqTK6-NJt+TDeh>uwab0lLRexR0Ki`7hUO&k8K=CNu*I5UX(abO z3ej(I5&UO4NMfDz?;nR$dAB=WE@=O!w$p(|e}Kym>`a!#C(%{Yw!E7xo>Khw!4Iyo z(j|gpjULg(1hziK)}?w~$+E!Z#7%dbnbE0?q}j#}^y0asRm_eOf{0cxCYA*akYdbI z(d|48fu2fYos1P+wt_8;=e>Y7PT^1D?-m@FP3cqpiJg_OvM?(UiKYh<3-R{~Rd#vR zXbn_wx!c0kcsgv((!fcqM6}4pVAyF_aG5V$u82Uq++vng$>$KcI4722*UHJrO=E!v z7|=yGph%ikY@j9=PuJNt>LCM&9K=wPzKMy&R%c%F#wfzWcdgi&42l*N(? zV!bhfheB)b$v*w-Y{(l!3Lw5@b9MT+bg7(SJ%H25F~7lHBuSmDWO)QuqxQGh$2SgG zLiG6bI8^F!tyzAIg(O!+-nx zA}XK+R!9?b1gF7j`lmK9j(K4xJJd<#B(n6Ich;x|T*_d8;3a7_%DYZ}Li9OVun7hd zAK4MpI0f&Q* zU~5u?3%*-uYTXo@t45Fjq(ai$rU#@%VmXvo0K7IgLXvhTd4SUDdB8od;IlT;v}Ast zHD~L=zQ|CQ+;d8vhUl0@9!werhxy|g=aLzQs!{;aeDQ7 zxi&o=7&WHRBDj@|n`vSoIkVCGYnkgP<#mY?U{8qcgCa2V7n@%}PFb<&43yJrvX-Am zL@gKJIUsG6f>ITg!X_^>> zmF82b{7IuqKPVC=qT+}>GEf-jjsY(ThZv`FMf%7uKV$OS{U{UBfPAjd?hi#G`o!L# z?lR*2AoZKUwde$N7kUl%=Vdd)OFqXWg!SU}R$6Kdxc5Pf@nNqhRPC-9; z|6l7TWT_R}o{Q?+6xl75@B56X*Bwf+XeH}1ihtf?umXv@sTL`)80Ya|W;dgq%_}4# zi!X`HsX`cKi7{Di8`n*IWlW}zFhw(%(l$@N$;*eihs$~o<=`Jv{z~N8M$60Nw zH^5PvS(mthhvCF@aTfkdsem@+V3^Ls<+$X z%-wUPg>1S2V*lnahO&a;T6p;$=+Z&+Z7DuK1FQV#%lrDgdEQ2uVrVsnEQB2VYI6}D zJRybdP+#0~V8vmj)=En=Ok0_5&`TR7zyO1(B?Gft!pkcxKjvLYfWdP1a}CgzZHk68 zXofT(1{Im`rGo^qKkfY;7-bVw?*6_`$l8Js-?T+_nxpWZ+I?@ao^-!ef9&U~zGAfX z!)-6;QGIIK1>=1Nx&9#r3h1l#ad^qCRf3CEF}_M89?Q!5UgR8ZX|G)+Uq^NeS{-eb z>J2KqRHK{fu+nU3-u>e>Y+X$7mDT0j;L4n5ZTw%fVXn?uz&8o+m-bs88TW+8t7`K| zeP>t{S~Gp09y=z|5-&Q(kc=;Bgua!4<^i=vyubc9JVy`(NpKlN1G<1FWKq$Pi>&$o z)dE1J5LB!|ZzZCRJ1JVETHmzYaH}h1rb!nBg*DLKB{-324}5LAb~|?8h*}?9E>y^;IOn*K6vbwx%HY;Ta&bDKcN7Bv$;9nda6Ge7v04G{# z(c)Ho`YV>ybLjla(gca}b&NnirRGt_%%Gz)^lwJN3;I)v?tGY89)jxX z_q>AKX{$=9zU8(HzS#~)iHLI}z3N{$msk)+pHh?!K9Z;PKYV>{74#@EP|E<`|Fj)% zI5v5FUNgM>2X+q|D^*c95{{*`-wAQlopY_Z?%tosnlqx-j)0yBX5L&BWmF47o5mF^ z0su`rx**-+22cvu%(V-whjY~fAOO8`EgX9FxiulUc8gRPn{5 z=fwhQlyQnftc~|nxOK!#)t~MaC5xo^#7=TpvJBUPtY+04KqFdxY>~WVoOwVed9Uov3EEcjTsBS%HgAZN(!QJbn{xd6V%QBp}=MMWp z2yC*I5|?an4BbxyrS8@nv5S^LoZaE>?#T`PRR1VnK^F1he4E(7rELZ#1&1QZI=MLE z0xM2(U!X?9bum_S`1jg3o5#fc?4jmoVLKgkAEax2)#Y(ToVA!ic^PQO-X`E7hcs2P z0HAZO1qMdyC{l76>ykGqlmx!|U;0?4KE7176s$c=36U}j(&{|&N`ZM`J1=xFIzzn6 z2s$0Z3Ane<0;GtCG>n0evDX(8B6^kir{b?e1D$-eK!rowumtyRtqB;9KHa#HR=n`& z2OFQ{zm!@Vj-V%ud-X9IfhZxAtZB5RuW8XqBt?kwP4KSu;{iR zmEj$paOn4^23p>-zv&nLo6^4PW{LGzLxa-7^8!Vn-7guqKx0(qL^pKtG9eEuJr5ix z0+|tL9>Fi`DBX^lwAy=~yO2M>67yRvYBx7c(;qX-)Gi*~hN72)hAhU6lpw_HEVGx2 zj(;nn+_QIx|hnk3@ zn`t8^Um66tCKNJobVIdLUm6wAqfSC&gnRfGbg8OcG8=x5xvesRR)`cD$=O;M!9|V0 z(Nh7G@AqDYCsBOU2_a5{c0L*gPJEpjMPyNX-A*S zTs`B8@u}c-p0X7aFW{md=6UdbuMa?3`fl+%X)Rrt`m6Cu_|OT_As@5NhpO!~64lD# zz@4Q!4xPRu`-dR{$ix^n(y_Cx?Y&N=r_&lF= z@`crAzuk)i71D{}WfpkCALB9xM$&7%8^z8cQBf6t`7ZK|n9LBBAwhXv^2!O#8`v!F zHuEWf;tvHMYCf&(!3COIH611}1-7poPvPV9OYu3d)k!HR1`%^z(mDcJ#q$qoTS_^M zouYQwF2#27EHwzg#8CWR1N$PlS4MG6AOIHanl)4Int}@30+hz4E9)R591{e#mzLnE z3RKGC6_ez*<cVd2WNXMXl^ zaq-o>i|2#FKh8E_Gl#Qk&1=uA9#O)pJ0!{&`S{6)d+pi+yDyy7AO^1GYoA3e#EN{U zmxr~CTlQmy9$6!OHU=f6n-H}>=W-rnpqq%Nqx_!5R%K(D?!On&>{dWGOv+84!g_Xg zv7F<|M*-VB@Z}AmTCNdrP|Z2Pe6W~v463>yDX4Wk@}L=ETXe=1c6F(sq~fS50aA%4D$7(_^p zkMOy_+K7*(2&^yP+esW%b3hWGgeTn}Vnf|{$J_ogukkxnZjN)U+~>rZuK7IEbi!pv z>)0ICjimBAY1V1)f9XZ(_Jq;_t>e}<=iT|z(@eJt0Am`ta6L0=TtenDj_I=H-uG;N z_Gul^)?t|QQn9qV&!@y3yA<$h`z5DsvT@646;uw@k0RC?Wq?;>0hr{{22=%6wNlEO z{Y9c=Ru|C=!AU`tK8zZ=qU~;-F4Kj=Q3fvIgRW(-x4=#dZS2V{*9!PJiebg$oFtXh z>AD|~Vjw+()I$do)WIHbh+BiHjl^tz)f0-(%mwuUrNdgdRKSv?H=a!evBfSfC1C@{ zx4k*=KCuhfjo3;4-9*e?8 z_1W|*e7_>~9}9#x|FJSM^BJm`H68&4n70rBzzR-3W%dRHn;IQRZ!ng(P&Hu>^nPTs z4gnIxryDsa4PIpHNq|fkB~fg!)@1qK%HAC7lvAIfA zyXIwAA!4X5xdQu22S zHJ^_ss7JkOjBUw+B|{AoI|P_i@R{YgV{WXk{o;MDCTXm?3I}Zl!%8&7nf-Bn`W17i z`Vy!|;|$ev)dhhiBUJY#2luk60Kqf?`M;1hK~Q9~{8QBdBLK!lE}7lsUw}pAfJD~V zrbFhqxOG;*9Bn*ttl?=+h+lLcvvd|scDh-b1G#a2*72+XSp$}2qx&O{iX_L9q}1`Q zmSDZVZ)I#40~8pAP&WhzpO7(fuO%n31eMN7W`z7j3ZYMmL6DsaENj!1|2XT}AdTu~ zKv)mvbvMBma4iBHTS?%=kj}aO%Pj#1rV>soqWZUHufhJ z!V-+tSHs6&g$Gk|=-UtDcVijIGg+A|YMi$9@r>O@0i{6@$8r9X5T+%0HV4h+Vvl%0 zlT|`W>&pZh|8r+p3HcL-Hwp(SCLv&0=T6#7JEmlyeMD5_8HF_QHE!`797a-0eA2Up za|Lm{iV&VNC7B$eYdm@Os&S^79n*P6d$;71V;Zvt)52|UVnghQi?Cc;V5-lTYMWoc zzwZkCwXLt+hZqvv_)_!H*us$Vuib%_Y}_gwNQ|8bxfwewNq>fP1hePKUyOcsFeCv;LC z;co|ig$-c6&}4`fGHW_1jx6w3{4v#@pW?)viyfr|B@rB}&p{0D(rbLnlo9Cbib4y` znu~WU3>)fw&_i{Ld7&2N5)Wv+tb3&L9`6&-oBtp2-SMEVbHFRDe7W;9*h@gsZ{s7P z=<{4f^htu(x7%V12viap5R#1O<3UY0_qi>I+BD~??ToLgJZ9WY*R~KNIdG3R(CrGi zlq#(RzlWk~Y!(C{Eb%xsbj8#sw#C~fTG#;MkadfL+ZkGd8GnMX4NN&Ip9vf{k_kh* zrKTXv#FLLo@V6n?1}{-qC+`qGsCU)YqCOgtc_(@TwGZfbw&+d88wsm>f>t8r4rCn=A%BW7A*&zI3`%x)v4!mfdHE-8}BYkUzn_9kaB|v z3F0)T)IcW2Pgl~0LHX-6F?}S^M|SmthWF?S^vGnh#d8_Q8)_&)-K^zxC(-6W`Igt- zlhgDF_s5DG_!m07n>#XCX)Uz(9ud*nt4|o+F1V8{XqXrySp@?FzJr^ z_?$3ZKApnc8jKkm)iN9D)YOvVS6zMaq(?7T{zHLe@FaN1YyJXt+!HtE4!UMHHT-U&av z%}5=ZSy%>MzFmb5XdjKh)OU`9N_Z0l)`PC@;8@Lu zY0flbR;(D6%RiGEVn;UfFRfv7Es;A-)}#ERWK^o9%+2(o7tp>_Gtw~#k*kC@esJ1A z^coa^8$jrO$Gi*e-q)$MrPd7<^QPa}!S#xA`pdXBq{m<`fAv|14veF?mqsvz=~f{! zjxIJl^7`5XhT$oi(qq!;i3Zxi^h1m1;qr!7ZF-wP?05DVd#P zHIA5417!+^a)hN_mPcswlUWxS$S54Ei&tU@y3Vz>2Yv-j0(E-UI=STrzU+zu0l>c@ zMy?utK%K4c6Gmjb5e3GJR)-nSr$)u-`wuP5_Bvcf2X~spoS#h(du;YbUnx~*X$2a5 zw3FB}*%=bHdxs2j%bMv_DM(^x$7jhLBNG>O0e?>~H4nWphFX$3kQl@PK|N+2=m8Ap zOQ&zPP)f?iPkqW%;eq32Sk3j@kSBJg;@!Es@u-ub4ps$+k{R*I)HSp>4z)_{ zO5-U^eJGdgQ(SJoTdoP&%N^4p=7eMhkHKWQRU=nq8x1PWAF@zMZcKA+`d2hBDmzVm zKU_6p6Tm`?d6swOLyXHDV4g#?*99vma`6W`bM2NqRWEw#d=c-EP2ep|^P^vHu-OoQ zz`Ck6pa9xa#L&pAD*lbJ#+e^b^oBWh{Q#Jnq< z)9SB9d&xvq`?40}Yynn+elp@^h{FO3lR_-VeU|^YN_T1t97Gp&F+D{_@WC+#aFoCzLbrbzYCXKgz=4Z(TheK`D&vhTsX2g{JBolDR!9A=G=Ae|p2#!~X$oLV^nDOZ7 z&mhNw#}n>P!mkg;=d_{XKiAEd}N@V-{A{Dy4$;1{k%R zCS7i`Mkmn;Q2HlemrbuK#I$6CpwB>VhUZso&02!l`-o_~D)pF}4YUMLB3QmcX5qd# zAlMTtZ+^D_9&KTr-sq@gZX>j42B|58DYYAVENPJ*^cvi62YmR+KHE+QMpHnb7w$VI zkWpoA&CSWJMJkXaDK_&!1)a#!V`DjnkRe~=mS%suB0Wb4G*+mHzfZ^;6Vk+;t?qq9xl{p ztolvnr!0bRYIqD?Fe7t+HSi2*5BX53)F!E#KFSA?$Tl?J&mM5G1$ipj0I%N)C5Nq- zLy-1jvBQXF2Kn(eNl!+ah>Mpl9?S?={z7LDIqU=goP3i&x+ ziFPG9n<5oG#Q4`(Jbt}FDDlZ0>hD5F);Z?#sgt_gRnY~ult6iHNNftXQQdvd8fVKa zOFd(B2R64?F7}yU<%jhlQ5eRS)n3GHk;@_4#%mwMu(PFbv}>?I4HS(_KO%P`8yvxK zFtrutsSQC8b1>p53%Z1aE6@f{qm?GPH`Q5ui}!`3Yr|vH2}=RSn$6mj#$Dh^Uo#WY z6b44t&*#jGcMsfdj_&4zC$*=c5VQ}2)noIA3Mki|8p5^LJdEJ|IlBAzCKhikj{8&8 zv-g4e$Y{&NzD{Gvq0nT8u|2Qn-5pd#(3CivE9?q;=&x?pRbY0e?Lm8YIwwE2lmqb) zvcRh(+R9tJ`M5k;pmJgds9IT)StQQNU)e%WQC;Pqums)SBlWQ)5B>^l9#9NnD)rLv zQ_)w}lwru*S@l|Y4Lms?JG@3@ineHP*%M9vg6;oEwsE*cnNzeC_`~Gg*{(#*NHm<>k z83u=I&kMY(IyAWIx>bzPajg6}_t4WRRNPGZ$8a=UucLr5oDe6|dp=zaIf$;?Vc1+J z)2{xUz8(VkkceO-BAkqsLd=}z7onED;e8X&VVE(6H4w;NhDzt7o#pPXM`@SX4!ZZB zdlUS7lBtoCY!hoOW*yLVjvL-Hnbi_=nJu)Yx@ZCiXV#}7yU=n5+eZ&i#eQ**&%yT) zn2|(fL`JDwmvC?4oxyKCKTUt8^i1yvGS(5OQ=JkbX_likW-LLN*!mg$MueFFE7}L2 zbdJ##b_Xhgu&1@FCCo9w(QCD{tw7DZ#DHznH$m0>5eyB>hxe$Q3dh{r#oWUZ^{dL< zSCOxJ+T4NustWb#mP_FBR`-yoz#@2Db8b36^KPbAQ55oUl+q@#rp;@AQ8JK4K4*P6A z%ZHtbjah$@3CW$G+&Bq}cBA|z1Njyg~BK#m+;EkqL{1$8>$h*hvdqNYwc_atCh>@EC3ejgx7Lfh8YNBcWPt z=3|Wa4)7iMAcE`?C-?fwE#hf3du9gp-Jn_AEOl8WHctD|2{DmjEWmsA`5Oobh*8~y z1h4~Z+o&vf4JI99C;HxfX_|SbmzFiUzRj+0?W|ENLLovy)e=WHwb!`o=<#Om>85H& z8|TwF4SC99Y62m41pN~MJL9~cAWl#Y_Abk#Gr|f*NoZcVhMBPV{UlFrqjVkl`v_d( zb4hyCm*RrBDK`r85#k@!fLGYT0ywoiR#~doH8v}8$TJB9qRf@NkAx6kUpu0C62BnBBh zjj=y5#jRbPLP7;);2IL~i^Tel9c#HOBXInTp|s3-46XJ7A>yyw20b27N{i1voU|jd z87XHA%fg-rGRw{vei#Yz3K*^2u2YMMeC+2@nD#)iF)tIEpXoEc%!|VrfEcUaC|C>&<37vs9Nmc@O7taGft2gL$elj~%4-izcS6CEg)`rvx| zS<_jkY)Fv^^0`eo6iASWP!JMJZ|Avd?mDspC4!(v8Usg}$By;05t*Zwm2FmEZf?c5 z(F4%~BuJ$4#$*qM)CxOM>2|4N#GRdRSvH56l1V~rbdbP{i~}td{nK*jXi>z4QftER z!`Mj=BVRO6>g#H|M<)a~0i1ETgfHicH6|+JH!#t8I?r%8pEZ0MVBe?VQG*BS;mVa=twl zRG}UcT`_mFPL5{!BaQgiQnw$F>2l%^A^rHG+(F>(Ct8_sM$TO#170FTS(DKM7q4!DMsAVx(?p4PRCZ*#Oq>3yq(6(tUXJ z&EwtrbGf#xVEti+VKDOCl@%y9ob{V{&Wad=PPqozZ+XW@+=+;f4DD$Rsv9GI05lg?{b6s7Z@f@iu;NvY`?c4LS2ccl<4maBoU#BcxBx!bC_>ga}Q0X!Xp|C7si>MgklMH;)F$B z)?Uo+OD*jawogNy>EllipC408&;;_^H1CHgrz;O#>7WDdcmX)o53AOy@QHTcf4QD( zEQ<6&n-HAYE2;Kho1i%zV#ocu9$>Y1rrREBUVzTVdwxP=y8Q(6u#AkQN5+1YoF94*8_t;xgui_4YR6Gz>fz1tdEo=ScgmDW-VA9bIn zNgJacBM*W_L%;^9x(=Z2RsOFQ06(UiL=sayvM}zFKN))>8t2FW3eSp2st-?qSk1+I zRb{%FxI`%NuYSiPRSC5@ID~#+$`*|A1Sz%lXc;mfRy%D@tH?7u&}R@ z{HGBOywFU0bXH_v2lZ428cJ+`9c2q~-TOUaV4}e0i?{mDd*nw^h|z4!y&O~r{T6wx zu3FHVTE^FpAw7SDcy}+rOLB_)h>(}AZ0u}y>>UFT znB>CeLOB+bzmn!p9lh^AI9LB1;DR^-um`4TfbdpyTrB1$ZDbKr0j}Me9*+unn~ds9!5i+E!kFrviT zLC0S*E!d7~vb}_M+Nr*qhZq z(>S@*4S+|dL9gPd1!m{>LRK`1gC(h?ciki;#)K8Q3KxQhqECCkMdKpDSM`D0r|lZ^ z57;8dubu_8(#jiGZR_-KW&>eHD`@eBB8MD0UIXx{x1RvNt#h=m=cn0qR?}Nxs7f@e zpGwe&TI+W?e!3IWO!F1zY7gW^<7Y+?53_>S*I~j=eeL-zn<|8$W)4s+=LSkc~`5CzW zja)c)f)h7!Ea5%f$HguVJ4ka+5xsnhyAkr&MX1xzZRx2_7|ct`|Ne@Yu;P9>{@lP_ z`#jxp566e-Zh6B>(rhwzO-tDpv@06-&0g(XKaC-^am}b1?JL0ep~X}+S6v^kChEw^ zBL}Z~ts;$zB>3kZe+#sxaB{yxN^0pDGYA)&RvnHlZzKf`_tImni)7gr#FUBjiM4k& zinChw*WpM6Q`g)wK_LntQ`if#^V1hqR%H4!h!IHF1d^GtL+|*nq@&s=&M*mV zw=n~c-J;|E4z*uU5s|Krc5c@*_~_LjD3k$VEr{ow6_D1%G?t(~QLe^qoQ|(rvuL8W zXGZr^Y7N2n_W;<9&wJmB>bKj`h3N@N$#=c$R?e-tI0*=1jb1~j>(%!Z9M}(O53JL1HzH8sa}sjQ@Z-a#QApCCj2grRo>P#yT}g zh?_SdS*KwLsCQO5mn~YFn=LXihjJAX&fesC@<1rZG)Lj}-PIY8%M=o#p`5|f2% zI$Ap7QnDlZweqGc8W~H<(3C}YmIW)kINM3(Dse{~5rflZRaDm`s;y?$8Mp05M#JMP zA*s&>1#6YnnIKvMCtM#b3mC_JH8vh7TSTvl6TP-!+QG609eb>wDWSRlU~-lSZ<@Fw zE4r5yq9Q+sd8Vk8kz2iK8+WLTc10%zPwW!NI350-$==%IpuBp&qG4AX&w&pcqd}fa z&6}n)27JVnbh~FkO>l?eZnt4}KAp-iUKNfYjk#seKT~=*@RkJyQV+|(9^ar)y%@Pe z6U~R*>3Elq^-70z!d=~(QA+MyXTp`@9o^&CgmR%;y zN(%aAJ;H*v7U*5W4 z2C;yhj#@>OM^%e6fVaMX2nop>E+hXfq~ec672dh?FAb9a>hn&$?yB&oUb!m#spkvv z9}!5@_Q-GKyTvlZBo<&s1+x$z^6d*{-o9Oj!6PhUNy@@@7gy^}xab_*xzj@@S$n|8#XNhJr6bM}^w(~(6* z6Oys}832B?D(EJ^PKW{5Tv7DGo6y=tm^wp~beq;062w_UOCfMOEHVu~#;a<-c*w~1 z*qDxb+R_)1G6H(seYRKZch<8w3mc4C3qoEiB}+xgW0!ArMPbVW0fFL(eS@EJn}s;0 zDVtaJU*-Ib%jdQzZPx=`x_r=Pi3x40Od~WNfNE`(Qd&cbBoV=$R59319u(Tb+#HdC z0`sc-bmRSt7D1!2z>QEg3};s+ZJ7ac?+_A_H(W+uNyQH;oPYk&cRu>uIXS2menhJ9 z_MMl|tXogS~B# z*PVa5S90Wq4rFJI1&-~jo13c>IN$sG_xYm58IlNBU50NuVQzjN{|GWN0dJ6P!bW+A z9iJ@Kll4+)oA|*eH*B9cI}=EwYAWROj~N-ERElXa3vvzYbIJ%BtV%c3sltlvUSh{QL!k;cWUz} z7)DcZP8OUf+9vHxG7Ygi#0C z+57zoeTI_d_Rs>eCuBU7<~tzdc%2>B$+(ly$UrnK%*{dhG9e?>k1}@Mt+<1C_wJ#c zIq4!oS|fb2$XvwebqZsCL^He^fFpAIk4^*imRAVXM@V)X4Gs0TiZn$pUXIfk&xrx- zvUtRgMdpj?%t$TH5nfv?_{kB#ofeKuyH1Y8&%qc%6d;9BzJw2+l-gV5pse{6Xt}$L zs9fTVuo-iX%c@qndVIXOP2Q~ICdD34X<;G5!~_B;A1sVQz6ICPM?whOJcIm~H7<;V z5BEo`N%q0i2(Pv?rGEd87{3|9RXr!X6fIZDgS)-YY`x?d`8P}=T^r&2Z@=pM!DI8F zdFkg~tcO&1wel??xuU}No(g|<2`*hoI^&U|Hl>Vo51$u*2 ztn&ud265)x@+JgrTGbSUmpZZ~BHpiOOVKctP#1|fy?$!wus$t)~XCad|4 z(~U^nJ%X1k=1F*$##ENe5cg)Fc*Vk?RAX%6-RKv0)cDOZRUa_P9kE!XMh|))ki6s= zc@>d#63$=w{wq8Yq7HUtgog8FT2 zmkT&8G4UBmGEr6iT5<@_#v+s~TBWmr_A0w>H*C0?w!4r$TK)Z@EFO$Hs(^HEKO*;e z9E_mv9-=id&^R{h09iSR#RRDl2f3RrjO7F-S67*0^;PorN4PV9>o=nq9jEEv1%10y^(k(2|wz>8)GZs$A<# zTQy~bC%KME`aS$M@FrO`0IbB~2Z ziIe|hw_up(JwpB`~2{WOY)cz8#j^gbXt7bE|;8%F-|w+$n&Dm$Ho^S+;nDzM2k zSC@Y7#d=7ES1WJ5uBh<6roul1;=fcOIa407CKoCY3~J}}`1%I4^s*!~YNl)?kmRPA z&&ENR=HG4>Zfrk&o{n%S`Q*zRpMEVz6~F%U#+RQkE&1rK;dNDqt8(mm9mJD9cBGJB z7By~;p$JGFnFQS>^HkuWId>ow_jLp?D3x&zM)`Dv8w=_d#=HdLVu21F;sYb|fTI}Q zJFJF*;<_9Y#3V0YMMD*bvw$51rtOiWX zW4l~P@>^5?I^R|IIa>17-p9!#@_2^U+7w|l8tZq7SrVv;I(zB?(-8v#D-O5V>|9LL zMiM^R4wZ4WdA@tS(S$bha^RdFd>--znkZ*42&$(t9eTMt58{C$Fm^HD-NeaG~S~9*&XT!Cm-%`K$$l) zr}P2Um5a&pWGQ({enouqKgDs~9hM(S2>iO3y7hRa#yA2`iBm2ZEs_Y=-YiTkk~gmKAY z;CiN-`243AA|x*}MqZ)fI|=7~zxfW|kJQ04H1f-#!ndt_-xU@9nH%f{75?lJT&|F` z$0Knk43y97#L`w<({ZaiY=;t=t$?{}S^C4#@@L%Hf276`{|7C(f22o~VM?l8{%q7zdGf35gmLW|clJk{-{>~mwdCgqjXX&as_0r~{hSj$ypY8( zDC+D4S`grjhfGPczZRur$UERjwCj*Jk(~JTZ~w@v>@g0ISNl0dOY6v0q^h|KJLMC* zo93HjgZ7Wl%RxnK+PlvsVlvfZ!^j-h!v+N=X3Ig2@tUjffqFhi5)^nz;82nTWjnTe zxE0EYeB4-o=&ikDnGNL2D*EHr;4DtQ^?N{NBE{;DPg`%o6bB1zEw`V2o5|E> z@O$&8QIFyTt+MpW+yv_>pa|DeXzo!U8#5A-DRHM8xK;S0tb6AR@;4U#ForX6* z=zTzPB_x;lx}w5g@`C)I2+84BeI3&y(4Bf>IhODT-Qax8-O<2hotOqqwEe2>Q1*iz z9e(o%RZC0$bhwdB&JyA9$An_Un*2+SLM}~9>C=T z2+e<K&1GI~5Egp`Te;f+xl;@2boUU}~9C zQeq=c#sGv*PrEah{jkKWBxB)q&_kv~iZ%KUvtsu2v?Uix`WXDm9TlGt@nIip2}qAp z7!~7p)2fb;V(p&fiApsMrRMpWPw?u6}`(%%a=G>yF%x?}` z6y`DMen8R_k~3U-RQR(#A^#sjvh~FUqmGCrYc=`TJN-d|JKMM71=F>TjVuwakvV;S zyR^m=G)I%$hqw0jU+wy$X~|D_?$Ab+QZ~K`m5>JwK6Q|3xj|Z!Wvq z>VW)ahoDsn9NYz{H%sW9iAd?qaAOoCwrY+^|O1&&e ztGfra-Jc%~7h9A*NYa8v3y$m2+`0&`QVFyMkmy}+;B^nWkbL`h`=X|!ZYSfW-`R@)nY8K}>B;A@>wPL?5#+t2|Njq6?4K>pJQ_L#PttBu&3YJUZ*kCLkjQR~> zNi-GrB8mCz@k|26u*FZ+3!_YAHW5T7vpW1^_XoWGtX?YtW^*Bou|oUz-Kx|uKvrc1 zJSm9DoRMYc=0h7WK0NrB0aE}eL-C#MXPS*XLGX}V7L@_i%QX@I0AHtWy1r2*fBXs8 z5$XEY^;M21%PeY>Cxx8D(_=(a;eAPb0al6iam}OATfzd8hm>!3^WGg=+r5|19yO=a zE%l`K3o+qmZ=$3PaYSnhU4vookhLBE1w(f~I#5FvG0lrf@j}RcX{#qBy`{JGgyhDB zlgTU?CF0?7=0u?+YtvO@QPOMgC2fY^7;NZy`;iK#G+sMgf1VBT8*0hk@2jy^vsr8~ zElGrIt0az8VUrC-oxR}BsMkBYHm-_SGQ#TpwG-X0ppn?vKM?STi8YOCC}QbHeCLw` zZ@>;C2y@xDXCcXxQ>jU2<4%-9^9T_INPUGC%K_BuD3x#mP>ylKN{YMHqp^s3i;lK+ ztY-ppyrjxhu#*_7$kPbP5;zcStXo*z(ew3Fo9vdK0F4xf>54Xqa%az!GlWqG_jiJr z31hp)j?*Tn7ak21lRv$aYUeR2m8*1FZ+9kq&Lc#|#T^`V@UgIq^OS)JX2V#K%8x10lc7>?KJn z5f625tfvQqi2wYkCW`Q~69~k@6@)zQKIY&;)*1|<%M;}yeL#q|cR`3Gc^`{^1`Jm9 za2x{wIh4m@fEDIj#8}4}k zCeIQhZ&-Kn#9m$=G5^$qI1}#t^C9Rx?)-~yA6}X}e`?zH3?a$+6gUO~l%$Q6U8|VS z8KgwqIEP9g><;aF=d?7XJzNv8s0=M`AAWrbI-)M0>?<=vK9{SNInuOb+%Y;hCr2`l zM)|TfT%<3-fKsF2vvhoj{-FWhtQ$!iy0-f0goCYW z%HHj}uz>Xu%wc$i#ejQzT@OfJA0vPN4_B_-|3F{&3O9FWiIKnf^o?E>F@Ksu@_O9) z=iBKncK!3t8+ZOf*SYhj7ri`PNTvc{&z?wP=(J55IJg&MC70oFg}KtI>ts@$=UA)z(E<8 zvf2bffnos|MXD%3hGlRmjsR!x^7b!|0)|+!&Zh6eG!c3?Q!X}ZRh}tix;(k{7)Epp z3BwE+&i(AMA_-hJ_dK)*x{nwgi>yNOsPwR)y<$fHU$5=g=)Fdx_;$0H&z1|DqF`bR zTEk%^K}TJi#ydn;*g3Mmmi#`aaw$0GcatRf_{%paB(I8*=S-r%=D z7~(IP{nY|w&Tt-LQie3sln>cd-P2jvU=nm%$D@j(G+G3X7OLfPwb5)Sw}AsIJ8LgTq7hQ}4L3Iu~o`?6SK>)!?A zcR6I-FP08!HTrI&NZ%`x7NZ4oF|nfkl8ht{I!VPv&=$bxf41K7b8*QeBx2!DX^{0&XYxxpsYnTX5f5tTmE{d zc3PK`@9qfvtlu4u^wT07tWm+Rui#nxy*<+@v>Ra|S zO;Z(Vue;~mKfX3jqzMeE=KBzXuYG-ePdMNEJ-^@Y^BW;%=fi&{Ns;T2p9rA=y2@c% zE?Jv8tb&ZBk{Hss z5KObcqb6$}Q5?@RfA=zmkjOU=syT3cO|fgTe$f%^#=bDg!GhT3Ff25Rhb!XWzL;!C zzCL)A(e`B3#Y&x4zskGEu-!$PE$6cIBu9?&nV4csWq)Qb<-r<5l3 zydu-AFjAUoNY0Lt|KuOnul@8rZJ+$#>B~>Qt^R4f;uv`~MxuVgDUL4RG<4wg*RH>( ze}_84dClqm51-!FH12#Y1ii(bzX0xhQyoczUKdMMey=-OzjL_iKQuO@ApzCZ%VRFN z=g$xJb`Q^QE_u48Ei3tBapxvoAjc=NkkCYAB4r{$3tF#uM!l%qJ-g(YFF0WD=nop} z*#q5ph4>-5Hm>A%^&oU(ckttTY>0scNNS7)CtAO709AI+L z1#0j^gwxlO%t7S$%gVv46E%j2ut)>4wsmpj$eg8`6+8`QUdkvEwtk$?efUn^Akvmw z>$jj_tVx_!Xw34@(WOQP5qPOjf-@eaxdA0M-=!%8mkSYT&zu@#%a|mU)V2WY3FZ0gxxfi9jQp>qCV znZwg|p4_F2hd(Okj!$yAV^2P#_E!vJyUpeft7qRwAN*T*KU@3c41WDZ(+$ZBG4jtE zVdS4R#K560a?#xRQxNnPcfQ%& z`HOHQV~J{gpq=+V-}rXmbZu zACL|##ByY4beJ70GOCD&v_X#b4+#$);`eMz5n_1T zJQ@(<^05fNlK`@X;{_UAf{u7XFOjzlalKfX<#}69wRVr+v=@YmBtn!d^Me@p^+|ab znVrYvC~=G!*0_Acg5aTa`zLg&=5pSB2%@v3w)SM#R=S)GbYxT>D?Jbo(dxZGmu70X zA$gS1Vp;qcR(<7cO?kA6^e~Q3o89^0{w~ktl7)oiPKJ-P+IQ-91brqEW(TIXGSE>9|RMSaD7?!^?Y zD$KDc?x0tF$}M!9ZXC>?wG8p}_SxM4#v)dWkK+Y-0m*ULO1%wr|f3uS6cne1Kd zVztSt$44STcTb<5xf==#idHmGVzV)bgn4U2K>zO6ZR`ME;*QRk`Gb@a8=qJ>p=m#m z;wWl;uP`FO^WbJta11eH491B##dCt09Wcn0HXdpyVHP&AFt`*SBM<!a%?>cb1mV3#w-4!>`lBL`O7`I$EgDT{z1%s^z){#Y@Hn= z|C?XE{pr8-eb4^-_Mg6etL{(h)y2rGN6|iAJ0&AxUWb`0LQzbgRgt}b{ zcfJ;a-r~+*0C)anB~<4jBsTErlTf6fb^@2cuL3%>oZlC~9LKeGCD_5|rQGJhAGh|; zTSwXs*MAptPcs&3Cw>(ikETF^$`?4{ivN4f;2`T$y zR5d1;^dLF{3 znzuF7$czL}ON(2<{nGO)L#*3)Q$w)Aj28#Ej*O&YqN%uTB9l+h6VI|jP)P|mRMgcK zHJS7d?@FA6m9pw`)-Yc6TZy^PM`JJ$?M3{nN6LlnJp&LC;N|VTu|PUPMjq$25G5$C_7Bs?W=c!C%@!9A%jM&vN}xiDv5b2nmE~s4jC*&s(dM!*;nrcZ4M|H#zE48Z z;?7_81-UdKk#9;@wUrW$!?-!87mI*~p4BkG?NHBh9CU)6$7+WsHaE_(46$zeYL};= z!btvvRA`E7Gx{dilHstIuW?niB-$V1=|(ybi;IDC#p}lKRof8lQ~{4@3nA%rI_XFE zKEMvN$x3j0i(8hl-0^a@+hgp?LCDGq;9vobQbLqWVoEVBABcaLjg)wVhEOz7B3l?h z64hQE<7FyHpd$igh|Z9ki`&LtFs8K?N%D`v4_=2^yLPQLT@aym%ce3unw&(cwLG#} zGlWiffvG7DQa=5BBX5kJR9`d_ep%p4D_q&Rzt0x8@6%jmuo5mW^wZ1$Ev*hCK3%81 zQ=~icm%Wydw6@mv|F!*>z4HrcEW6`);CCMmgqt~#+?d4t(;I{NXH4_QM1v;2sN}&6 zB0|)xjP*~2c59$^eJC1AXJvP3x9dXLGA+|C(+=*RS{axQ+gWyATG~#TPIu_gGA~nT zODPMbEcCVKoO|xS=+4Yejr!n$apLjb+<0z0-~9f4eusplUbhG~(0uE_;~j%mKD99@ z)N^GL0P7k*VVPy-$y^46$`IUgmN3$X|ECGRCz%}Grl`tyeemC^#>6W7mCg`u^2!Y^d7zaL}h5x z!&nL!gPw(56d|d0c`1%DM!CbOFl9!HR)&w1ATlanD8NXyOOqY)a=_gWC_g(U-`h21 z{szWqQAcQ@v8L4|2C58JBk;st&TgDye3~UsN2_})rIfi(+h&yLdFi%HQ?`Wg^x@!E zp|v%c`G^(00P?%@U&Wg@mnRPP_D0j<*kVb{=ctl=0g@=W{jlAht#ctcALrwY3rVSD z0liMzraM&lqb#J4_?SA=>IZXLd3Z!V)-uFaA!)cI9%mmQ0>I+xt5YsD<=1YDl}mg3 zN!>2-2c33rkKOOG7c#0H5v<7&kA%WqX6e7(-5&UC|`tg0qVfSK^a^1}dUYrK==}nS?W_Tv@ug zS*oqJtgaJ;rGH&hTvju{`suEYgOE}dT*<{==Q}(yrf~(7u=xPHY0CY{ur?$mB{Z*? zI5Wo7;3F*(_1zFNhD@M-b@xBC!~}OV_xGD~jH7aQjk*^%;19vQ*WY?M-X-h`r|K?6iDjXS_`VnPFPO1kOn` zCOft;OzfYi?h~`2d_bHYzFihz!{5p|8^^!jfKer~4m@53hV0%Lk5G-U8vq%qE>`76 zRoRV$=`F_H3KAjtp;n8iBW-~BDGhhe*eyUUOfhLpzaY%=OfEceAd-J`u2Sh*BahJT zwRRgM$BmIUpYTk7#HWdoe-2dqsZindm48NsKBvOBjL%Jle=>;wM1`bWKBBRe+UQ#t z(LC!pPDKF~!DZAPy*vAHmm#){M{DmcHz)9F+8FOnEugSJ>{j!A?SVvhiD=25CM6-K z4=2+Fu@@n)uc++iLt+BUQVMzFI5fs);?`em7Z8Ye8?%;(BCEcm9<(YXAxYyS`iNmP z+gn`a-F2yA_&Po0;64O{NkFm$M!ePq5XohshaNtlIJxqB!RF@WGFi|#m50KJ=@zW* zkhvOYHHkS>!3W>vv*kR$rfaW;Cj{D46?6h~&Kek~$B zQ&ozJ+z_^2n&0`==1sgjf$8cd-*1%4yVu)okQ_Hg-h9I67m^dk$Ug@v{#2;&+R8t( zko;fQyL8s6@R!<7g}?M<5dVn^394HTd#?DH5tVXhI*EYC+j-c}g{UxsKN z?_A$$?#+t0ih5bAB|q)chf-mb2zH~m+rc+(r;Wx-#EyMUOI)%Gcl)ef;2n9pXhn&& z&ziJKuvn|Jhz7j+vt5v*#eZWwfrmjfa2AobI>fwON|9lg+mA%35xsB$aybn^FHd(+ zL|CNUE#w*0sv^Z0jEdkJ2EQBp>;;s2CsLc#R;ZKpjF2w_ML}3m+p+K56$B|0@_6|7 zcBM>2U@5^vT)|gBPu4OuatD>}Te@9kLc&TJr*$L&Im08hI=9x%BrZ*rrnR?4P~aW0 zWCjb~Zzf;AzU{vJ{VP}gctS#QJQ(?w6V5tDo}=Q;g!2#Hv%L3#Hfd&spAr>bU3q1k zQ{hj53U9u`sS3%VR=W#CJQSw~L_jO89vtKSTY33NR;UyX>Bz0N>&UIw);4i|P+oD0 z$b~#wDsD6qL7r%@({0o05N-Ls?!EmzW|w42Weg!JZAcYOFmx>kr1Z*==m^KHsYHaz z#M>Xeg<|4|LwmSira}Bsm7t!GanDquO0pPDh-1yPYe|-`{ay%G$?F7CBopFpj2Q#)1&fm1W`FCydNLKhsQ{mN>SH?LN{sgG- ztxIsqLQ;fjkD2_-lX{e$1V#+CY$5gIDf^i%ZPJl{wp&LYeEg^FJT2zPp#-V}BPXg_ zA|X)5?}u?$PVP}an}|{83i4|cqkkWyK`~0Km_5@+RNzG zodOnX!5qsAtEfr7K@`sR(44JWtgqtN;^~VJ>0YFgGutssuV`oUF*NL>%@F%&;Yk6B zE}}V1eDDCuW{R0Y9`E7ry=BbUVjzE*a*Y=R_o{tk?W&MdJy|MVAOT0AYF7AcUG+8Q z1i?p)mqiDKqYUGcU@#ri^x&xt{xlU+MM|}`Sdo{LC>;jeitz(#N&VLP{7z%@>hk;> z+b@26<+Xp@c;yf66O&`c$ZIF?4d{$x(vNlrTEh?nn zV)$*(sqpH{YX=p+bqP*cNMNgI`0%@=CU8mX9K4}4)fFmLP%#=aAo8P?q@!c(s{3Re zd2plgHR@ZUD66TOxjN9^Ve{5SBx%hGMZaW=g-SRPWFGyC80T$6D22MDij2l6%CP{V z9Yv!kgME>nXw)CtUVD9WXRa(pvx(s%suh(A#FUS<sc))PB zcR+Qlbp*)Qg-JB*9h8v-Ly*uADkALzpN29%_%_+%y9~>|HCqsZ3$Y;Ct;ZwKcnBGO zCwNwtU5J^4#L2(u6bw-jQ0^c7FE>dHlE2(8nU#k;j_cK=BZ4qUZt?M-*UoP$H;Sw#7sDU;ma>jM+l!hk`;c^ zRCoiV-*Qfcx10nO{%lz!)+34uK-)?}>ejNZZX&CPGJ?y^uYB6JGDMS(+X!#Bd~G z|ML&#cgslZ6H$kxbeRQ64e8*n0hc^gtWE;DAjjDCluHL>YX_ilA8W;~1y^LNTtRZ$ zO{9orhI#1=st>KkF5-iG*uMcFY_u#pF$2KifXnX_>qNo(@>TCp;lE%&)C`*ZQM1OM zr~0X9>!2iTjbsl2QsV3ZtwIGIx3!=@Y0jVesZZ8MD!c*Gum85^RQS)Z zNG!)eg+B!1KV^&Lu#POKk19OJho*)0tJpu_W$+QrtwnU?(TV7Y#jJNmlVCI{OXS0LG5H?CvXhd}I7Hir_Mr8GD_ zl;%)&X~Sj^YC>?M@66pBl(o%drtBst!0){4ELfh5*w zG!mou{m=99|2>uTqw9BMcPws{*|lI}WUS92-P}G9&>v6AoIHPuiKK@&>8Uk~dUaE`GexBY|35eP&L#;;zFH1E1+QhMc)* zv`Ofl+tR9_H7>wK>O7gFLbIs9CSuU!`Hlj#(C1tHiKAj)j{S+6&y^3KefIS6$NTR- z{nw9w-l@Rkc#Qm&<1q49{tt}2@dE2F@%hEb3sihF;oSE54`{+l>}+L)pA8khiA>+6 z!oLD4{5k5{I#x&;&SBn;>hpqV+NnvhE-15l%P8m3Rutv#{`2h{uSiF1ceay=f%Q3S zR%;;aR%4bKAdZx(g$ity_ux1zpv#+ogLs&|WN9g*P#TxE=nv>}F~LrDosi3Ww;zy! zDact)?jtc1u{F$QA4ZB~I#^gIs$D6VW8nwrVNrRpjtYD0750hbp^ZsEk_dq&Ts72! zrON!wdpF7x<6@DHLbNR&glD_p7rDcXZbR3BTf9A&57RZxAP!yM#pC&F`<@*n-JK54wxts^wU?%hcc$4caJ>{kblD_wUN@TF|wB+$H z;)cA+)P#p12|5N5&_hP_BO`p~$wrdk3XqFa4gfk2(VJ${Qwv2ePbjonPmt=!%<^|F zrHzF)DbKCndK70yL1=7BW_zz5DC!ncSd?BgFGh!>n3Oayk7`)HQGbM2y4HO9v-YMF z`FPVsP{cg^5LNtJK79TbH<5HwOS*`ZGu*mV__Mtr=hNr@!uDSXjkg!vzYl|=wmRhG zAqo_XTJI?6)OgZi8)CDL*e-2dq5}@I|fKhO$fr^)PY^7OZ4PXr+ z=VeOb57GvJxSJg*%Zm0_Y}GI&1`)%A_JQ?}Lw=q#mYX}ad;kOHBF(n9w}c^-!%`$3 zpA;9kup3mHTuTZ{HFadaad@$*o0nI%)>`Q(Ae@fk{p~&4cMI0n<)n75u#XR+f%4%$ zKQ-Nmof4C-keuDtrNW=_1^K24Nzs~n*O!cXi%Zd5U_`;aa&V2jVjZ#B9_``=8!EyW zh!L|)q4wcm%!2z(*cY|MI}k{%0Lv;-zGA9ZS?lx6+&@s3vRYi?0PI~4YG)!4V`LQE z{o;$QpZGIHULZ;P#CXN&tagrgglO_HZZWOfvmzv+yAs23COt|KFsli(9cfXYAMH!I z{Wwy@&7G^{z5g7vQ4*cN!2Ro!y1D&{BrTW~6fPZWCANNN;J8W#{c=hcAz_p)ppj;p zSIOxx0E)M1D!fk$J_?9H#|JI34OKFqDJ-kjxMl8e|2t2cg3*p>NmoeDZtGIv&-#LV zgM@@GK@RDK4tI;wEm6`_^?H5esN*)oRvoduw*5A1n9?GEXwz^Ibf7z=t>j+Al1>8& zzsIwP4I*|ZA(ZoxzWcJG(72?m4Er&ostOjW!PI%G5*h=Z7E#z(Pd{FZ#Gh z%f>Rk`;nB?f4(V6%n-VwBe%Ha+%gA+OolXb*)#l>D$BkxTttR}{L``mUJSt4o> ziSmZGZ;9646;R6gv{Ts_7@C<;NeFiDAU@h&;V+Jn2~9ofqKQjZ6EK938;cIW+=!k) zqY|U$XaM7-46xxaO}NE8+RF)&&=}w}K+;ENTX|xbzk2oI^&eG0s<+cJ^oW3dwYjKC`4LrxnomZ0Z;;za*((X)2~R zM@%=(*n;Ld%swr(AG4jRmUM-ryLGqoFC-Hl7x0_b(IyIPu~|mej1JLWv7A(tqt4q9 z+jT_Sc7GUihdcrw&+F?c|A-+i!4;Q-P!-%%tG^~xwKJ3JAkMHY5BUoNjIGY1ni zE-5Q<#WOQsDZ<+J2cwvOu**+MhayhO!-Za2vhQ`%I*6|a7)oCID-c2ESaES?3N)ri zaV*-;iXv=OySC3u4~~UsKonBL`FZc&jawDSfwWkKsYx?QXr>Omn#9!Xu=PEd=Ivb9 z{nn_UF0AgQmef{OKcrzMwnT4lmg z{4=sgYvsca-+D2x@zliRUKby?Pz7?QY+;dokcarx}>U9ja;f2q!RyeITR$MWM;I3>}BKsvU~1U|iHjas)6d14$r|;l9fJ-tFaieIZYSlrWh5 zVw;51UqQ_+&&?!N>^zS)oX_uqfp!j?aYcYY2r@|Nc=w4LRv zBj%6&5NE=jKN*7F<<5Wk?Za2*&YzmL9Va9g#k!yt!32x*-W;g5)l4>x@<@?r@yMHJ zp(8fitvlXO(4EBOppoN>+PzMB$nc;$N0kAP9f+B*@PG`C^<5?&=McIUAnxE8001BW zNkl2QG1sm9FU=bNNPkhYdI*NI6>!NWwQg2pFcQQUKS~*>Kf39M5?xA5bHOk zxnXtj*(1@B4b!X`GPc4kfeqnohl1`8mCR98I`wyd{~Z&KQnHZcS;w%0g*f1pOlu*9 zM%EY?(gNd5S#n`8Wz`b4OF7)%dite(-(Ooy?!EgG82RSkmtXz+wQqfHu;owUou5OD z{DtEWdRfH$X$r~L) z?yh#+hWH{Ku|3&(ouW~a+QKelP%QVMBu2d>T4=%-e-*QIi6?_2!oLpLev}iKR0lO_ zVg0-P7|_;wrF>P{qvaB1shoFgU7{+Z{^l<~(*li*kca{(GK>zF5V+US5%0&8PX>FO zY7()GaG!*pqJISJMfrd1on2^K=NZQZ&$)QPk$k}Vwk6p*)Y zQ3Ac)`*}VjCvluec0nRI_R+^VmhkzX|NA`u-$UC05Y4(+=Ro!^#bkNtrmQSB54KG= zQEH$nf5+eQv6UQ?g8&|H;*)JTb_%0KrtiR9FCX^BBd_K%To{(CXA<#pcqk;lmEvAA9Rkj#jA z5sUlN?wwsN!e%C%n-AA~5QoH_H$%`n-1#%j%)E5i~QT<>{SPc^+?4 zquLm&2)%R73MeWgAd}qV;?X`o6tX97$)(VAlaOE?nKt%B5jj{~zr-RvOk~`G&&V1b z*k|Q7zEBn#TDJIW>pc_1R6v3Q>4!v%w<<}!F70uy6~jaUwu+`p%p}Kt2}AOjQu~WF zCI%tdKSuu8nX`ZYqe(}0-!QV}b>8`r$H)s%2it@B=^W@NVCHWhd;1FwJZ>hOn-BNd z_TD1yycs#z;m)^#JKu6433!TNJ+BS*Zf0)rpZU59@gtA64^c-fCpWL~>4V8gH5%&E zJAi{I(UXxUJiG^Z28Ks_JPxN47j^b>8H-3{5*$IAI7Ayj679?KC>_x|`;+Ao_9X}v zuWm-CEBzj_JcR{|{HrT!7ZINcB0yCwZiuzS9wAi{ub+?}QJ56j$?%aV!N|{{2pg)1 z##iOJPvl!UGWie&8q}mrAngvLmT!N}&fgP8T>J({oCvnOap{rVb383c*XAxhVi^*$ zJXO&?Rf6kEO&eN$wWX>hT5+H1cG4MIPP&vmx0ns{3Reu~@#@7F}DUK4Ony z`OtnGsKu1HpZvU0M58OH8m$gbDOF0~g;6RfzG)SzBygfkqB0q+TY!q3&o3|U{GP^3 zu$EMB{Es|z5P=Pw9JX>*MFUpE5Gq#W*W)T>Z)5=V24oqb1YOOtB^L2^`@#f*1jnt6 zmu|YY-YTpR#&5R4{tXb`7SPkv2%$@$ zS&FPyD%k_?JIN{}InZiiK0c6!Y!(Ad#GTpvUFF+np0WGv#^lep-#h?Dp4?tOV{#;S zbhf;acYfqC@&=gs;~a?wGe2fAfX7Y0iurK&6q0sx=gk-39qxP^xbv+O62u&d7^y2Q zdFhBHY0#g8T2nA_hU>_!_m!5%5TC3g7R%DqckyabQ7Gof{Hc_*q<3p%$diJxe5*afIQo0DWehF5a zx}*#!-gE0ON!F!QiZ{*G#Yvsmlcm+>0tql$b7AMvy?b&&yOS(Ok^V0lBr(@jfHAvf zq#WC~DoN%6$`!G!g{`TzVUPKJmD-=*m>7km0RaDCFDm>4&1{%iNThJGC-3~IW90e6 z_jDw@KBU3SnUI(riNy#kf0B^2n>*hGLGN(qTg{zsosbl&mK;n;8x9r{5DE8l)+sRu zBj#-==N4vPueCmg_%t1{tZ%xy0zCu6NHl7(+67G#2JmTr&|zCbDF{bIF(ks>!zBH$ zA7xM+RZvRlUEXoaH9={7?Oozan3h~xzfX!J!$g9>SbV}nodGO3VMnFNf{4ljKFkf@RapA=8qyy9e6oY#3m zb0fSxk9B}Un00+OaOU@2YTu_Zx%L<_F$#%^7;o7NBQG}I`SR6I@4Ta(Z~t}bI~*E@I~L(z>KlPR2l$NA53z!lC6UfAFQkavloccf>Sc{Tc_HS z4@7eYu?d?S`OJZJtN_6pZYV|hW|H-cvl7ZB!GM2P5V%jUAKWiP7gZ*oUJ%?q2d&i2 zRPq@Ai`i3EWOs}h8~MSv3?2-C%Tn5B-YszikwEtnA$V*e{g--ceK`Sg6MqrW!24i{ zY{E%%JzsT!U}mG22T=!OJnbHz>o*e4-sg#e)5!hz=P7C4!MH@BC|B3rYdcK|3wndZ z_^-+yNd2K%b(Z}|jl4;^nab+S*GxPrRvxKlt)|gJZ#T#~jfXln_s=L5>4WtfNBuHT zRi@6oO7zn9XuoMSXf)^+8%YpxT_o8yRRP#D$78hMBE9n6aK!k_8v?X3E)?jjE0-3Z4_#$`_e(7|GZ4wzxe>azQYgme9MBl)^A) z`U-Hhk7sIh{ZrGYlF($So+c+|Cx+xK#B0*9-qr_uf)b&`GD^@&Zmoq}nKaEpYXebJ z$4AQE2)anqL|gew|B_PMRwG20P~fFvHDxJGES?K(eb?cu-xQ$C_;h+Od3G~W{1C5T z8;M?lrN8tMbk>pORA2$WtDJy3rBiE7ixkuv0Y z!r;&Y8Y>N%2=CJ^!$9NB$kUQTx82_v-&nng;GWFzwJbH?9da~Rxqf^-W7};=nWp}k zhvRX7i}2?Mgx7Y00o*h+o+WwXeLf%f+|jW&u%%m7aRwZp_&OR|{3}Ojk895<)QsP8 zJIoyxkqUe(L9;qF$)e5D2`Hq%30U^s2OUN3m)t9A1s3}NxzUgS4CMG?4p;Y~o$9*U z?iB_z>faJXM|G6NVe3($YA=kvBvFPN;$IO8cx9lK>-ZvfbTyG#wy%AUhc!uVSNWpG zP(9G!9orYh&ZH7M;;ghA4x_CpD}`!E3YxG6oU19_x})=*M6CEMRqDj&PZt~kiVJe>}L)<5PYW*4Mu3Poj`a4U;t2I z`Wok?WX2mDZCu}gIu!nU0}uN}1r)9<(prj0Uj_b^b$mgqu(}&(U6F~3hPY$(i?2k) z9Nxqiom-|tij||KJUxpf+%A$(>f(#_cM&k%n@tFKxYz=9?NMbu81(f3ztALK*Bfh~ z+{jfS257tTHsi}g##4!C2p_96@WsHV{GD?Y8FzK0L1 z8f_<5tL9w$v5E{)29ZW4UKt#_dCLUTm!Eeg1&rq2st*oJK_!}N#tx5NB!_$1M@`M9 z9I-rH3#R;1@egl@ULR8*kJHB*1Njh5*VsC?p~d5&5nd-1I1$7t`>+cA5N9PMN7P2} zf~lBwuih~m>c2LTo}D$-dtN#CHY$BJ4nQM0?<74Ne8Yq@LsK3ZY3unqjb%3Q4Is`e z(OLc1#wbFb`y23>1FfBnzUpO1l?GQZ%10)4A$3XnNZ~YM4qH5Q;;P>vQ^pQO2n15W zWMqeG*bgqB%+ik>Ai^2{ghY1L&{ zV^HJOK!MNhLsva_)XO;I*C{pm{_+rq^Aeq5cg`Tx-n}$9%9U_1b0jJXtIKE;?$es{ zXNMxW3w9tJE4ImYApm~|-F48+TSElU*dp{bb6&0L^cdoG)PYw?# z@yur3kuKElNQ2x6biaGN{E3(K0p{EQ8gFTJtkLVs&MbFUQtt7w*ne&uzzY=9G@C1F zMz%m(l*BStXEx@eLX?#u5l<(5 z$8`;^m&tkX()kBkT?Vvr8fp5J{K3VT>6xxsWpe%GMw5>Y?oS8-PYFJ5o>IzHlvzF` zTb?)NQAt8!$VpUKJ+-LC#;sHV(TUXqMp`E9paU1X5B??C(D7pS^(Y~g59>&&S@dM< zb1xcfRMQkERaJfUP&J>703-?#yf711;a_)y9KnL~fEWrd%(z)`A9B%N(kJcRrJBnP z+#qNg@3ATx4CzRI-DgIJ%Ri_&TvkyygeUy&f(q$6RPECHlN*%bODdCt9ZYx8ojlhu zPv}?pTZ^zk;!yn=PB^4=Na8hTXElmrfvkQ!g>Ks_rt~b^lmvP|ev~unp2fcX<)C{` zGnOfnw9p9Bny2bku5mgpdNkSU$Pd=36wnE`N zN!pky3CUYMfoGQSG#O~eUP^3})e z8)~U@RR%Xfz{5l9Dzw^SMHnG!RYd=qpEW<)GwGWZ{22~_de)cGpD7&=E2Lm1LBfkp zqibl$&vhj>k_0b#*^V$sg<79S=qUY)(pp>L6|8!z7nqpxXi4LG%=MK+NG^M^B%t`f zF8`-mFwlF_un5^9xOAlUuf0Qc`_KS+M+k<@7@Gkpc_@Blak;&5T0SV?$wQD~JSJ5) zk1H)cu^%{((Ms7>6ZldQ&+S86UVS{Kls&kR{5uLZ@y}pupvLU3F8B?d)z=YeLWal0 z*c7`vK5_3B=qR|WE+2kj2zf(J|6VQHNa4-c-|0!SC5;zHb2gEYsaqE*t@K2Uw`MY3 zGA|sMl%;@ZGCzt z6)2ks@cNi;XqcA1bn6|Zr99Gz2oq8Wv5Q-d4$66B%bA%s2XltW&)JwsOlR?mp~f%S z2z9ZncsI3wq^MDT#$-zQT5{k?GcQB%Nrunb?7umZ&3{^1YX30u2M8{wNkySReyyEw zq7K6Pn#(O3+p`A~7?EVe+EQ{J>F-KA4&mATO&o#KFmyBxEn_BW56}^PQH`V4h)P<~ z2>_KF6C0vKijd!IWlpJ)8}K0tY(5yE6L05iHl6 zj7DEMP~Ps!jIwL6BT)f%-EvxSo^LV8ja`pkMmm9Nr1YMZpehLMUm^JfDU;sLspdYy zfyjazbn2yN20a-b^^;7nm)De>h;97iH-gN}NZ0%RqDo>B(0k=|bZWsxt(9C7y{yht z3c9hee>wewj`MPD0)Fl8sP9j05B*myE-a?V+pk~dU_WLQh{q9xKARoa4V)EM)WTtP ze$a8GKrJu`Dn?RRefHIG6HNi0n}hG6t{qYJk4uUIas;#Ymv@&~6*CjQ z+BC}3HP3XPOPE5uO%%S|*#Zjm9tbx4L<#;|h2|+7jh7LadR*O>*Vu7gc6ZIu$gL}P zBhJra4$MkJuw$mLp#)$B{h)>!8)`s+w%r1E^=%M?u@M`ncuKls!<^Ubq6&c8Ic5()P==EO9gL z=^E-G;za(VgkZkk)`lDgp*?Mup=Hw993tf9_we0z(Srv(c@k)nnxQ7+xDo|ixy-~a zm2b@>i*IGj7niTQcBXq?5+=ZG!`B6gM)U#2|0f$92cyN6BIp>)i)u=JRo5*(u1dNZ z+)K#ZKL3?%7MvqW#sv{>`sQ;SZ^kZ0mMH+0S7%lu>?n39Yyy%6l&8IiWiRJkAU5OZ zoDH+}KtoHoQV`|eDDB#EsjM%ITH9nq7ksf+HpMpSXS|t@@ji@2W=M4^;aYa!OgCpQ zCGY*$3p-_hE+k$igt+ildfT7v>n5&}grvoBPV8AGK_?8Fw0&X2X z8W(0WfO5#`+bK={Gxur%MEDI&^@1PjFHuno?|b=%UAk}DmLRf!qI8PBOjyA@tV8*7@PY6eM@7|q8&`{;CPWwJ2lUbf+2o7%KG z6()C(x1f~#(4v5-yMS_=a_(pAk2sGv{MT4mF7K66tn3s#vWdmmzXpx*+dG(VbSO3J z5yyGH1XI!>^v09n@f0c3HbWjC`@z_}!acdXLjk4qD5zwOHeZw_Q z4vnn6xW&$#3Yo33kmQ7ZPjA82Vu4xwZliaAyq`yuBy(-_v4z}w?D?(Lr;Hr<)#P>E z8270?kL163ySPx7x!z3*0*zNQM*3IR@~iNX)#8CX2&^8~&X&<=m6IjFg{XVBqGiC- z4YWz5V+}>AOi5_jcmERXtFq^vpTbBO(%7jm8-?>^3gVJlVT3eHQFx1xgkuw?rYdD+ zG^gLMK{H_Ys-+HhRSyUM%KAi7e?P4X7D0VPiZqXub@fmghQU{GgNFcj(q&xII)#j- z6Wj7)bj6$?`_Bytx%i~Xs>IxHY48qc(aDlG(x-(iDx`WwIuLpW9diCShpsj10?N;h z4R0T3Il5K^E1wL#B{9nA2!8!VG}Gd;Xd_cgqPg~sx>rLS34?KLOpTRH zdogEmLs9GwO5+0Ua(I30GzL3;)s8iL{hQg@uoP?=+7!x5*}M+So7LMK7Yj)Wo-y7Q z^N15+yD52gysO7Yj~Bl%jRFPIdmnfd%u1hss`ldq0i%S$No@S5eWmTVx@Ot1 z%g+P2$I>fJgw6_uhHn9gvxyhe_kKS$`uV98p|N}U+b-LC!)x^LFuo1s#56iMAdBoD zTNx=uVPqekOutP=a?ddx-{qwBd%`*b@OY1EQz^0$3(B4kP=&MZ)C;;-`zSS!<1SC^ zdf8(N;*SlUtI}Z>c&KWBEE-mB-UFhKcCVR})+6Z-$EY`s(6tmFu@$s<);kX61FJwf zzs+ruEbg!+u`-mf!l3>}tJNhuYlZ8J!U$>>lUIl#W(fek2JhP#7C0q3whU&Ot9Z#qTMh| zniXoHTSDuUrDC|-B6j@KiO-xTV%5#gV2}O*G#WAaf z=t72cSFl*HmT0$$l?0}_-Zw$E7)OJ{75Div3WA|F2{F;H%?fw=;Ay*)^8i|!V<62pQxS77X`y=Ac-LN ztGW8l92-x{8D{_Ul18V?jSTpUv>NPQRtv0sek`1WOX&F}6CCw~KM9?NaT>)SkmcL{ z7Kv;sH3z!1JrtnX^%9jSu>s6dE{!_*cV#;{aar$P%PmcWlu5#qYw|L*-py_cWAgTDZiJGxkm zDP|p&FbOtl%Z!*>H(&NnFlOp%g7bKmwNyFRCu@hQk`3!U!?~eCfGLiKQ0c6C2tV@BCVV|EgbjuhH|0hq_F$Y z`Gx%(jytnTY9xA#BO5o)P?8L9cx!7J!(d#)N+EPbJQ`9mZjOlLdSk+2jD$)h6Cr11 ziAvJ)@GZN4MAy%ZZ(aIFz@SFj6st#(V)MAN^7G$*FG`-6ufbO4N?TS_iTNpzr-&a6 z{Kkz_QiPoBM=M=;`Rq`O{ssvqWHeBvOsJygSVjlze{2G3Mm01+9aWvR9f|*R!_1PJ zvFaPQSTibp4#z)19nA41s!+qRPfT%`ObhJQ9{mX_;*o|$1$|@rRJC$G`#(6FR%B6^EpYJPqt6GqXv;}^PT1{U@{snQ!A(W#lu7fNyeT$Iv{Q|Ev@OAts{> zuR7A0S2l8O3g6QZ3B?j^Z%hy*bxecKi5G%ZtZs^{pSt?k3?d2&~k7zi(AN`RWuM~-SuNc zx+V>1!xe2xU-|l`fk32&cC{-$`R|lO;z$&ln-Ks$~XFCRx zOH?G@2xtpJH;}tg<6~W`@W!$@z`Mg43^(7-F{T6VQFEbOZ16g$8J1HTl~Ze!=NYSE zaK@P-MYTLWbH;Er{)7pGv2s$xY8n(#Ll(43_f&mSsCe1OevN0*n~YE z`+V8qeh(wxvBKkKbV!R;=1sNvXpXeYoQzGpL|@-OXh3&L6gC zxBisgI(M)4@2&RJ`p9*A{pt4BKmh<=qh96`x|c%XofZ5p$ZO)QL^6f2mgl6^#oyqt z8>Y+F$XDK8<^YFjM`=m4yi$h{HeyR$Pi=3|{A_dbYXOfRp9tGuhUP*a5isQq9KpD` z>wq24OGitap)!69rsuK^l(hcw69kvbm{dgY=&-@(*`8KCnO{zFu40-q1K;&U?;EN8 zw)inDNr=mig)u{dYp@3-9ysM7u|5HB+mfZvSKNl=B<6FKz|*!9&gA%eu*$3zLf;fq zT2(q{ldg_sz;%~ch5KCDx#-R6=k~Ku&wg-KeA3d|PVRAnnfUK2`%km8hZjp7#H=JI z2VcEiF}+XMEjW?O?}vPSIlW3`Pi-4rNyAMylE;y}8RAOHcJn(gW)epdR0OU_si_ci zNhX`UqJ9{8xK$`3RU&14G^_GgQh7%FakjLfT$+V{g8~E94s0jjrZS&61_S6+v-pch zNS7(#;&{NJ9V}tF5x=-0#X#YYxy)(=BX#qDMfm9A7P1SwxrpicM-QUc=`NpE+Vl6| zQul5w)}C?`+_*d>`N%9ztcihEGHvjjR&Qif?1mGy#_~eHT4ydf>*tPg-Sut|Rh>Sm ze{=1p%N2~<6(24$j<6J##k&2t4%z29HfB*G3(%QxcfzB;yG_MF+VcQrsUYmzR(CjB2@klUZ1}r@86<xq0z@MgW_S3R!UUuh&YNomwaa7p62A)KQsK zwHR?E?aSLR0Q6du#UP|R@zZtyvV8dHf6_J5mM6 zgtSI*jza}P{zPgrQFInwdrsTKQq_LDd^XZ>XRb2ilM=E1V}tRoj}=$&T2V`{wJaxO z+u2IH??MlgOvN=Eal5daF4v+D%YnnbR)ti`fLnR|Z}@;eacKq|05(W=3(*iN3@zE& zTDv-`MwZmYtY*!DNh9qv>u zt^^2Ci|bzZB!BHMy1uSIQ6WUdbPD$RYnM=TYUXL$-crj+wV~Q%sgu%4`;S^IAwr&I zw3(pAK*)H#ZNClWvz($7Hgve&cl`d^>lG+c?CI}8ZIYz3D6ky{$)XZY#g@E*nrwU> z;u+-FYaoFk$6u&01Od(cM?%3_lxx~B^f^Pbt&0o>J|bQ=5I)jo8>TBB9mDbByLo1c z5o;zDN#YK`EZ`KcOhAMHY0Mdhmo+nD4~#)KkFDslmt5@=j4!okr=i}z=o3v7r7<&V zbLhZ@2L3YmFYi;#BETjv$r2$?@ZWhqMm(QnF7YM{d4YQZ5<9hMVLXT%TkD;UY~}~w z2NZPoXA_>&qbI6@5l2(I`~99WHQu*!NNuCFnu74LEj&CaL*l7ki>N;Oo`S`u88$Hk z!X)tsl5Xj~u~?D>X+PxVQS&0N`G*C0ThT@!T{9b%lz#z+e8<48g2}(~grzFw+1g0- zTQE<^YYMTl>hN7myluKR$GCB`S*B3xb`Yd|y9%7vM5?@sNvcw#GKH2}2LcoVs6H`e zpHH9tJ1RU17m^_D(GkUN^=OI*LL~`i@6t9}=qSjNDK4}u+LZWOI>TAa1QX|r;sthd z<7zvB(B<6kMOl^`J&IWVB{T}fE$I7!V+@Q>#icu23Cs&u@H<@!8tXn5|^F+(g@L&&2nu`p=ORa3MXeP0*70 z0cgWSIT^7#tx!j;tg#Gm@=%yvBDkcqwF#W>S;3-YD2kT((+Tl0dy>fgXThz$Nj%m& zx8qdP1a|WYkS>y#Y0JedExb5m-*82H&O|_#>Bxu z(YGNN^z_sUvS`!EbZ6Ny?l?Lnu_!WNkZJ~chj}%#M#%vEKv@Pvd439BKw;)W&(9XB z29!~@cKU*ThSpM$lyQ5iE6ZBiQ-`Q|c9X2thLHmtV)6^oGvTe6EaNb#b zb&wwKl$0TkQXuThg_G7?8s6{sORKNa^XRn(6ns9o3J_KL)5tnsts)}*I|5{v@!4Se zkl5ig4T8ELka_innyb@o6FafN^g16o=hs8) zd;RWg6UERa9mJL++VmNTi&CDJxHH~G`A|-;7}roje}<^1>5c>;lqwpByXXkA1P(%H zUWf`Mgy&sZs(x?478>Vh_%cQ_)}YO#EJIFZRtX`<9V}ke)H4Slv#z74#%}FtZNIJY z6XAHe<^q6iJw7z5YL@mq&6<0>$lXHIAY@(8VU0>h4v+hPRtY=1`}j;iv!yY#&&5%W z;3OL9iYXm6iuRkgzvG_NW2ZtHoAUBXeWNDE7A9X6P)VDNnhKt z1F+%;p#)4agS==*EN72Ye`LN2w!fg(RJ)rcRi8J7?d`!!e@@Bl;1rvSs>lqr+NRzM z;3v$VuFCcLyzma37qp*f=d`W3|7Gl1Gx9(ts6x>tt#51O=3_@9!V^JHfI?O%i*=^w z*kcAk9@66zbuM0VVHuU45@?R*RqdE!Mj*8`Xm$NbqnES>0FKBj&i$aF8{22^)RZ8a zDMoIfzCIQ5Rv!Y5*z!u~)YVIQGMa&dN@qNBD%y#mZYphmp2K^3!hARY z!qW?edARHI6y8e8k`p*i9gwJTu9!3n`Ry>zS@a26;YFsTQmu{g#4IYGW9h@K1mYD%X;)~)^R^^2I z3~iT)DSM&DLX;Sny6kB|W79;n!P?gT{)Z)A)TyI7#=z)zPkHZQhNzfp>bD=BohkTyuEfn>9V{RN<^ zvwjmtNkv)8eXCuJq901aFAtlOVkRVzkcEoAt{e+GQ1NwE+aUMSF?N!_@ZX-MBUSzc z5$j72Ah)OURW(dB&HCZ%MC>DU;@Ap1Kgx5Ezw<99z zKW5~>>w&`=Mzk^_zt>4j{F`BB_i*vh{6q`*CWooLf##@{7LG!!E_;#y@1Hcrp{{uQ zeJl;7q^}QJ$&(&nCd=dh`WK1Oz#WuG6ubu<`9d+50UD2*K$x{vXRjjC21XOzI+*Sm ze!A`?o^NN$0S z3co?_X3Sd6uNaT4L=p?u#?BaE;-|#^VRDeK27~~=Cw5E_1kRy}&{8OI7p9%P-5VK1 zC*&TKl)48BByU#EZsZn2O*11(Y2lj;sXk~ZVW*;1{u5ngot(RA^ryun`zceuZ!kY^ zBLxXh0lZ|BeE+6&%~&unr}+*idLx0;zqy*{=izdAu(GG`vS5W^_awf*#!H)hE3H+N6U;U$V< z)=zWXLQv|38GKskPm19a9vV{9ZuT?W;E&oU90f!NAoD^S00X6mLxyKFtcB;}r zK*`c={#}zjkiQvZfC=^hP$(*2+x*webk6gm+$y&SYmG!uG}P}{Rr5qHzdDh!>P`gd zz7!G2{z(TPoQytok5fW02dg^Rq8YagTeNB*J+L`VE33P+bKn=OodUN|I(X%t1lzF& zIrP@*@0HLrK`fIQs!XbQQ<8(NaavVqTBQQt=f59oU|H~oe-1Ig(U59q=H=}ec0-KO znD?W1J?g|ksi<(1gkhO{f0Pon{q5t$8->HnPlzr>h_0kT=U)=dNQcK+D5c;_4G|lE zwO1TUhr`)?)Ai8H>|lH62TL<{I-_B65@6fCEM_e5W+L^zxr@bFOe) znF9H_^bOzXu26oCrl2OpngV9Oqjf$HN&GPql|_6W0(~c7oaOKh?jNDU*Quu*z`zsC z!GKUooNnVlbLqewy`p9aLLDSl&mCZ18x-wu{^&Sa*X#TJ>3W}m@aT2`caWClzZn;_2w@(}t^f2m9lFQp7ugZ1B`p>gSdQ4CNJf zW5aPjFTf-wqGK1|SoC#>m&KGB8@-)Y`P(mqq=(1l>a#wsBsiEN{}?kA#>Urh>N;)V z!WPix_gH?3A$Gcfr>#p?A)j(PHHGfeF`G|giCTWuyEV@8tDChdnE#>F1j#s@HzUj1 z)KdK3UQYn_ne@R%Sae3t5P!+qI%hZ`fhW-K%RJT-k63{DdX~!9K^6c5T&&AYE*5o? z819=+o{Stf?a#7fr|>)YT#Rp{zxc6!)D7 z8x}((*{&d~)S^*kpE+JxZW=|kW%^htI|bu|(F4&WKCuthN8q(=PBz4D2yyFNIf8ze z(tQcME`if;MrP7U%Iv;MF^db>Nh})V%(+(KtV%VHpa`R(ATtio=Ad#WNP_wY4!n(I zwD#d?cYYX>J%<9@hZaLad^pj^zWyS?m(11SwHe^K<=cUxmviwx(O1+E`wt%TYYQ^n zDY-j=cVZ~}vHW}NqqEZ>VPkBE!L^|gJRpm>CBbJf`Xt_u`B#i3`%_u!p4A>k8A<4BGoXnx%c~Trn(Uwg0=52YvHo8^~46j(QZ`84CmWj zv0t*ebgrTED%!I@F|x-cw{&DRh(pKvXje3GzJOQ$sPmADfb?h0X@8UVPf*#6TDATI zyB*c=rg*ywO zE(cG{&I8d74CY}=5eRkUb6n+D_)6Q4nhP53@$+;HGvZRX2iAhSt`rGdobm9w{Ec4h z+07E$!!OnGD4nn4v>`ACA2cMn(Ig#`@f4rszbR}9r5+!$yC8@45& zByaYmj=^hdkq3YGiS{!K4CUh;!T?wdTT|Odx+0CglSu+MO*HSR)ht)tYoz1fcP)Qx z%0+&M$X4gDH@Jog(w^Uq)xF&Q()9O+wF4TgHrtwqhE|r%>HjAk{|}S|*sHfl2Zn;A zPbkbEpVy{tk+3?R|5cbOC@quGtr**QEd4fNQeg9FuM>WZSWEL+F z(J8YYGpY@y_7AlDfd#@CRAajo{^Z(=?OR4U0$9t5i9bl-PuyRnP+LgD;1JKL#Hiab zneV(laN*l;rynRD+pOnRb9sXj5>A*5k>eUdh$|p9Kc&pHJ;7hM2qg^>oE13ed_ab4 zxoR6q5yVXsMSW}C2Fy!fbmTX(A;w6v&T1fLX3`eMrVN}RO?2mKbJOZVX3baflH96K zAeS&Jd43n>qJ^kzYg;Ri8ZsPn=m&CwR^g$Rui;K1IOL%X=GLbzD})GDGY1n+yqaV! zR$p_^KAl-e1?elpMS^T$Vu@t~=2E7k7AVXv6gvJHo$95f`EJ8sWCj`PKWpkby<)cB zZHWv$|E-LdrIKl_Xa;_`W*Zt9rHY)p6yLRGPVP3RW>IKmxDwLs{F-QxjaY9zqK|F- zuDqd56ErrWJpp}e6R>~=Zgu`ve&yCf>KK`r0b?=3_p)gn<<3ivOtn3%xY4t{(@wj} zRyapM^h}-}f-%Tk#J`IMAkv~mF$B*VqA8*%F|k;>6GL5C(R&o30}(EqR&v;~A5E8{S(o0RRbH~;KP(?3ThTT%Qk$d52yrlDr( z%~C3bCGjkp-y?jYL}vNI`M;WzccOh&R=;!L#X+}YG^mf#0S{AD28(>b?P+JaXcDc`5BKd+8_8 zda#n6zxk3B_wy$==}a7{dzmp6hc#sl$D2+*ZJnUT0$KvxyC$azHJ_2R6s*;(Sxuo#{znIySpUYDyzkex$2*VbLg@)CNUMl3katm9+FAh z7bPfAjK~{Q6)A zJx!FO7+%klhzDCjtJDBZku{H?mB`KYyiZ-9_!uEhXSvdE&*w@*QRvrv%TY9t9OF(q zKm~URX$l|bF-Ht9ndpQkVnqYls%{{uHJr5Rw?#o5z=ny$h@T-ED%xIawtShwNyY|%?k;km;eP5r>Syt&D_O}KUxfXh8=4#z z(Fq^}(%oL?o7YvwgS2D~o%v^`o?q-Eb81wiO-g>rMy6G-?;lRp{>iz&y4@+V6q`>P zYY>LHkfjYmoEb)69cUA_qLG(51<9=N6+`TO-0oC8o44S93cz-*Q1jF^A|rq54>OOx zkNew8ebh9hSk+K)Eq!#54^vD>Wi#cNM4RnNqIILXZv53)U4OgbN-L||h6JJ<5#%Eb zniov*N`CtH@-lN{__1)Ml<3D2X3jdWi(WRYwbs@l?esVeBKX;dAsGE`_|Z6`P2H$Q zNfm@9YJ=A%^yb4lzM5^ArTuH-r?Z`Qqp&-6lExFmy9(D==_0orWv`_k)8 zB--6q5C6vxLA5_QTkQ7q7+`~fn-D`?*``JGYYkVPfF#sqmA@|Zj1-b}x{gX`?wxDU z*;U9!C_#nau8{jC4NrI=o$@XSQ7CsxTWFXNP;k!_s_8chS)92Qj!}xmiP~LE zkAIik?!}*x^q-$d_O3D+D>tqpEzzi;4(R;WN8bgnEX4h^wcXkwT;gl!*AE>O)x{E9 z{tyl`VbVAd!VXUd4qCe4h6nG2X;ZaORntguoZB!jC7rO`0O)$|lBHN~JpEn~4^Bwp ziMzeG@597RV_$=6JiPS>Ukwn(9(N}%*A+bS4}=_hoewiVNxnxWZ(0mMp3)v zr=x%FsS`fZNyMdD6Q{Z0B_1Lth|@;ST4H7a&^Zt}-)ASF&!;ZHmPPVk@SNhmHW2LW z{z&e3AfwAT!zg^`h72b~w)oi^h65tfpo$rAap5CzYA zs`}kGIbgg|V1jG70sI&I_m95h9r#zZxKJ@d^`e?gtWvg2wrO#S(e-{g4RCUxK)De% z)Jao1bZOCMOXR;ZP4_jyjD5Yv5K4oS2%z?aUkU2FnhvSht_jqoiHXBPOi3zs#TZqIZHt=z_avDy zHu4&r7&>#AbcJ!weX8U}tJB6TwtM%(H-3_qia&n|rFiN9eIp}>gmyWPQ@8JVhmY$4 z(R7=6o4rq7+BznY@vlz@!(M^OJ|X7_5rg>H#y<11I{*pR_pW{{Szabw(9>Y|is zt-fV^h+rS-qcKMRilT*{k|HtNoHaosD5SRtWFZZ=`% zixF4S)nhJl>|cfW#VU&X_NK3t7wVjyzM$gL4mF1- zK0%BwQ?2mHY(HLs)vnL4MCy+d>`-IxTKfhEPA!F&W%q#&>_5{^Ji99BiPZfIhU(Z#10b#7KN9+31M1)Sz zQsf4CE=?M8^L_F7=$D2B=V3p$Vcf+%&_F&O%u($goa_VnRoxH6VVve-G=%URkT8Pi zEJ}6F9sffVJ~&a``@)s}>9U3-p0tLf!_-aa){u(95kpISzu>FFa>Oe;3& zSqvw-3kKp+bc3U&YF*Fq+qhm$79qbM*e?b7aJ^uDcd3aF7$3kgAY8SbXet{zD3i^6 zQsM7k*7qh)`*OB~$TBZB7o5k_0K2Y(-#LAvQc#D-DmAK+y-nqy@%=Qk2(~#pwG>ab zrUVjufRtWGA=7h`DAjT?|92KZr6arE+I?8|WHraMK7g<<6iZ3^hl?95uZpT}F{bmI z4V--USewP-SA$OX?J<321t-Tc2uQdM2!FDT)F})d3R>oN&0;@k>fJ1e@PAmK&v7_b za7%%SWZs>XlC!y~Fy~+8B5`U5q>sVrz+#}}yEAL7FKa^Rhk$zwR-=7-wZd|Vp zpP9(eXE8zxg-~&#B(0Rbo19Q_#|vLb=#9O}OD+%pk5Dr|zt#vk+b~%VB4xO!{o$`- zw$68s+$g5bt2C?-Yq~$)+`4k1e<~1}*`_|%_~zaP zH{c-Az1L6YBGD3ZdxQvROPhOI6q&`oK&gh~`spB#Gy^VjxS6INjhFcU-YmM&#(Q{l zJx-ADZ&%cI4FS*H$q7@h66|?upV~)|?3xB(0PB?Tfj9`xQ%{~-(~zkbE@sE5WUk%6G0Yf0ezxk{~P%6G(C&L+crNYg7O-B$rT9kCjWZ8ro9}w(p&qq3g3`$ z&lsf71OcnL=f>7l3e#C1&#_q7omo%G*Wm7Zb5*8l<-Sq69LW zNR~W@I@eT(=0VlcCd)TZ+8z1T6qSk@NeEkwgK%NC4?mH?{Fo3^mbCH64@x z3+q1)fdbPyhg7O01nfmK7w$rQkLNiqltTk%}X~#WKkH?(POZE7wj)7O0M65)WFUM zZGwBaTO7hK2nZ50B2&i0`%5mVJT{lP#M2W3>cY%M>IbbpH&i(%tIpgl>N^XVhgX3E z2Z5^C;5frpN|ZnG{iRMsv$>Pb0rF1sj9~1}Hw-knsXpTI3mal7$g$(~dCc6UqWOH! zM1hG3VFZB-mV7fn88Xdm`8XkJ-GS(1Vmt)7F5HzY?k{}Cu^gvr`g|JbuENnTRA!hR zf4`w>HFu`FsPb2jsDCaOesnk>M9~8GQIh!_uLy2p5u~kYSq-EZ4%0eQWdLF1^ir`e zPIlYd`W{(Dk@?LH@Dgfmn_1sYoUOy&q~XMoYzD*PTs-w-fNt$M*3wdooZmLw92q7u z9#(?ow$kz2GIR1JpIL{u--ewzJDYLGF?{)31PXC3_bS>-v{VPlSVckdsw|J;e4`SH(kyQKl# z!*2sl^^~BwHmH`gQbfl_YMnO$6Xs~4!F-~D-TJvLDKT_7YJT$S5YGL9wC{u^^Ngul zeR$38|1kTZG7Qm#c@BImZqsqZ+GF*vigI9wsSqBt3}yJjm6n6k+OS*02RW`c+ocQ7 zuZVHHu|XD2)c&Hyl=pF-)ble-R#h9A0GEuS)zCfN$_5Y7;1XcK8yn{)v*FZXolpf7 zkw~2Mk%~sFrNg(Og3j91q5!{lPK#elcMKo60|u4+e!tMDzGgdzr=WTaXe(so30tMi znK+#51;xYV$ra4k1Q!m}T1&_{i6*x$*kCD9V|+~Vf?s%LbKLWSTg%b6c`of&UF*p%Yg*VD?Z&*^d*b~?HWo0#myX`!)JT)jMp ztDVo9wTs@2PcU~~-Ovj?PsT9&Kd$-E0W0hGZd9d}D}_H5E7Ai*kRQHE=&3l`TiX{) z_qr(|#Z1!NEbbaT0YdXhI?3cOxKDHdxt9O_E`msRY(h;4P*iP{V5IMAnq3?40@!^j zK&tNjy*icZZ~zEQ^FiK3KngZ^=**tSyvhnGWNu)Qog!I9uQ6lm9bt%4ifWF%oS&7F z!@Vua0wQzYe1yuUo0Q`(7B?4tX{#~W3>K-O3Mkhy_LWKLI$UXcWkk6}G)v9%+)SMo z%ij|1<0m~Zx_)nlf|jkl*K@?^^8+BbrWB+iJ`UBwHL}CBc}#a;V~rw45A8`=jXF?^ zg*#Zt?rp`ta%9Fw1r=q%Z6p@FmmCUInUYy;7E9`&qz@yVFK5+-5oKB2LWN$kvLYc+;RYyt{MFSP|0Q!Y`>;im^{6f=gb6iYs{K>t3;E}U1^%%hAowK>Ty`n+ zyc6@dvz2V zILMCMtI0l>8-l&*bF8n*^#GWq|C~d?T0;VD_3P4G_=m@MRed%Ao2)mOSanp{ThGfk z*Ni>Ne-OAL5vI1hVs=`;Ra~9T_$zHMSWfGL+7{wN2eDROlGn9;Zyt)IxT)HG^+_0M z!SzGKUu$HLmUbL-7k@o;c$=eo2x)hI53N2=;xfXKL2Mb=4uOE0rBcQ$VXfs)OJ0mihSDF-!$!E&2x5|{+C zN`$9P(^|vdY~r+5JPPv2dg?`Uhr_8KPj`qYd1~Lyds5w4|lU9;jGWX4%3W-HC%ciZZmjr4em2QfZ*=#8r+BAHV|Bb1q%+rNpN=(B)EnIcjxlF-&*(l zf$rYDcXh8mwX5(OI;cM_{|nnuSsvE390W#Pv}n7&CH3x#Zyt z>7Alii|xa|c91{B*LIu%8spNAz(Ss~e@icmluNc=|zOCv5oAz~n1K;JaTJ-~qwS z+-%5Ehgyf}(%Wsp^LaKCu&40L{)zVbi&3C@r`Kv+^9*r*Hx&|uNcZ--qc=kW)p#7j zAMoadCUE-%d!_9vI7&|=j*-orCk9sp)z|Lh%uePa@}ZkEceMO5&VA2R9?JnVOON_s z`@D!>DT)Nfb%FOgg5}QTF{)MD0!*8?W!5sJHqiyle4^qp4^PY|_BU z)9JaWU(rPanJh^Vn}F=#?Yjn_yfeZm#MimRI%#4T3lf1n#IgrI7;gr!FfRRe#8=F> zIT?Kj`yB(%rg~p-j1dP#Ylg$qwUfxCU1%4{Y<(_e|2!fe(=|peZWNiFw|aAmD`}F? zg^PxU2E@Lnteck5Dz4Km^;QPbGiJ$hsD=yUl!{;kJ1*?nSW$)Tx7F1 z6qSi|{r{~PP&eBAeS~Fd)4wjCP*Q75)nhTT%Dfc7b;~nAdefkGEOCgYsI|Tn zr2C}>zhHX~&amRv< zD+m~XW&e#I;TH!#%Xxj$pELfoSVmx=QcQHiWlwzHZ6m&&GMDrm=vlEwcV8S^ZO9A1 zm4!&m;dvoIX%>~ahDDI@X=4yrDV>C7i%34lVy5BJ%8bIl-ofIW!p4+qm^_4idfKKy zmnT1{K673mgpBFF9DkiRp$6G5z5e7G7^9xl4tZn>mMp?gZ%y$TA~x|AL0K|(k96%| z4?NJEHcy894iR{A-i4sg!tbd7q^Y0HO-N_jlI8bW1%{K285tK&N*-Bvz&~rUSda)W zG7dYdb(*GxryP&a(C#!;FNTt2uY?;~d~ClLMjkGgmnfiWxL_zKQHJ)F#SwuPXg4su z>la!M9o{;92)EMauZWE05{uH8xd4jYOz!m`^H_kte)-s&Czw1`u{*|Pq9c`Ikuv*V z%5*%yF86W+?xZYgK4sKe)cyR`RAOR|JtXLin14)&qjvg%y?W!xQAm`$q3LNfzz)57 z;2maesjzxKah#ub55hZ*LAP(=TsxjllTS@QdX0=**3gF%e0i7c}r zq)p_mKmxy&qs0;_YSbxo^$m-_--}Km$6Yg$$)HymaO|-<**v1CHUww&o6K?>j4LDQ zQ9@Ofq~S_jJra_zjw#<>JvQ7o$6r;`L_WOB7@tATv=m9N3pBBqBuPwPND90qjqzMk z!66+UR;W@{`)`gt|Ap}hcF6=$S`9H2QPlW-+9GZbwWNAaePU9~%?+S^vv%2UFFR;% zB^N!WDX4&>`A5<8tbph@c^o;F6)K6(TS8?$-hqP;*O$@e!$2U{I1K1kga-ZF54DN4R9J;fQF5PftKE#EoKWYgT}>ZSiaLOnU;n_+6g#d9H0(}pyu+$zTe z*a)^;5yp8d@l#dGLm4p!sB{?^_zzdVi3sGKu>CtZou4lGRGKsuqi8q{^NfTg9y+va z7dGZ_dnw@^D_N#y7`i~aD{u#rdcV!~WUzkdwMQD<^VXe5m?Tf-r$M7hi?^}@p4d-3 zSoOhemt-9(DOf{P1zhWF8rOek9k**94*#SdNb4E{qW!V;_QswRab|5F>_0;BFQ*Nr zR!QIKmi(j{ReJ3Jj6ni|)BXq~H(A$f%Tn1RD^cu4{}nSJp=l_f>RGK5>IR`-ai0vR(|R$C?4K`2y3XROg;&SQb^jZ#6k%_vhlMox1n%O6;D~ zJkn-2#Db)$Z+#6dnUUA2wt{Z|2mW&;X8UNa)dD=xHn*xZ7)T1E-(R&G?lEKYF?J1 zoacu^mOW)>)TCEStd#b-B600N+3$K?-v8=E;6I22w|>5DsN_P z7OTJNO2`w-*ZjY+`xbK->1>ga6(AsAnxP%wy$r4=yjq_9ZHhY~%15#Eymv3zBZRBp zb8UTCg-IavP>8n@)b>UW8n%?kbC#o7op;4E61Yh2`y@hS-bh}0v|zeN!#biZX;l09 z)VC_NS0)pGw+7Z<{nXlYKirvxt{Ji9lECxw9cnpKSm@r%#N;E7v_#s`L$Xb$*O1cq zH5Ys9UyLeS{-MPrA7{%DeyGe{G>-Z%+Y=UDgI{}v##i&n>4c~A=XgBHUz6Ey#R%D2fOeanSxol+02ncVZ2aPi(>hO?Z}|a@eOcZdm{& z?w$7MqiglrIQ|x5dbqwUT-0R6mCWf*(Ea;Jd34HY$dWaaHAL7taj-G!;U(b76fQT$ylZwD`K#@(J_)#el=ab$K^aGKAHhKVh#pJ)#zPsRq6vB>T#P$!tk))w z?@ut_jJK@4fF34SC+kZbtp5)b%=I=5%S>SvhDaDltP z)L;twhi9I@w$pXU#MFEH05Vh0Ig*W?y|7kep+5q*?~TZ?=GCOl9%%#iNoRG^ht7V8 zOe1e**FpJzJ@P!~^xIi*^C(omH`TJ@kvA&Lf|Qrj<8yuywGL`9H|_sKohiXV!kVr@ zwQOzk;g8clS@QxLR_aWPvGF*r2KnGj9to-J~MbM1!bPR*<$W2air(f*O1n z0$IE*p_?R_$e7q9`v8!(^Echzl=C?_oZ`qr?DHUKby$I?a6frWs@_7OX50ncCXQNw zf6RbQ!jW)6OduRhoK{T|s*;oxOARk1LgBOU>_y!c|9ARPOS{s`OBox~Qa~~+bIPIf zeYSj|ii!ix!)NEqZT6pvS3Rlj`>#(?3+TO38j|TN0iC*Z^yYNtJzGT)B^rro8R=;Q zfThfS*-ArSys^*~h}Oov;LI9<%la-hnf1-Z}~TA&}H$M zs?HjUfn&T+*YH@2!NZl`)MqL~92K%L%C8Kk{}LcKY-iS0>peN7axBGS4vrdX*(D&dbozv+{oS0Iq*)`6G+FPMu@}yg0=2lyC2uZN%sWcfNq)cdMpf|!$ zdkdp8s~cXn;1bTG|4uo3Lc@EPde*0@yqb{s?2qG>PJppqZ-iAX9Ypo1*19Krpe;65 zO2a#mC<=Z~am1f^fKg|nARk4&bXs?ExKS2OxfdSS=KY4BY~xFTlS-WJ{SJ!w^giRq zuo-|o&H~kZl*O;`O2dK-!FBz)gi3>grzl4%aK=~`U4_O$|- zKm=>SpOq9T>TzOE%Vq1vylYbb&3=QjRHl2Pz-2|xVvF{mbOlA)?r(88OCXS!>)T?~ zj{FDc-i%wNoS@))h*bASju|Bg8APv6c;CFs?^(EL0InqRy8CV=tv`WMIvnd{rye&a zm!5nw7hQli#pM{lNf=5(^=5*mVmt9TqWL^D5fxtQ3pd{UV!|ayoakZ?0zFbX$HYb6 z-r{2`fTU84hqDyfd@5kcwTkn&*K9&+gN;5JK3F<BI0__0Iu_g-={u zYKwPPay(xn2U^i+J@mEBl@k*SDQ!kvxPKw!HR8?}fSg)xS7gibT<8OnE3hDf&#V>s zEY)A7=w_DO63{(QbQS8=g05%F!Ao8#2%xW*7i+!_-s+Q+E?w z>^)IrmYuUQ(i&kQW58F1bf5QPmsm{T%Z}dG*S4F*@Dsr~PI0d|lcS>n-)7wmaQ2)3 zO@R{x`8xmrfRjdC1ZZn-VI{K#?VrfCu7Bs1tRFiz96dYrBY!UU+?ixq!dR$iQ1DCkBWS!@9BJkZ5CYDlkHK`nL9>GAE> zTZ%~V$~$vuJlcqz5cz;+cFF7o^dxq7t{JBk?&LZ%cYw5pVj={cB)w?&<&cU>#|U0F z2$EwdjkJ~TXYrDY=L%Mko(`Y5>R`3*VzvEqS0|t<3~nWxRvnQ z^mBO0PI)pk5bu&V0BC)P)G{H=_!sc@PK(8%a5*8olSEa;chs*x9@Y**qc<8qBye7#bpJr8h{v@v2<<(5ackIv1hT8*I0+$}97Q86s_|b^rQg!sX8cu{J6;z;* z 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%0AB+xc*wcjoe! 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 z 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 literal 0 HcmV?d00001 diff --git a/Documentation/Developer tools/Render polygon borders.png b/Documentation/Developer tools/Render polygon borders.png new file mode 100644 index 0000000000000000000000000000000000000000..5ec218258c73eb41c0c9090e49445be0d1503fe9 GIT binary patch literal 241317 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 z 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

&i$NA$fH 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

}c<631XPkQisycXPeNhfxj0k$WIfE*Tw74PqDCH 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

pULsFbcCGv;86;H1LXLWr)Y4#yDok028zHN>l| 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 Hu0mjf!0;Fp literal 0 HcmV?d00001 diff --git a/Documentation/Developer tools/Thread timeline.png b/Documentation/Developer tools/Thread timeline.png new file mode 100644 index 0000000000000000000000000000000000000000..dd1d37865e340b655705eed52eff3e70ec99615a GIT binary patch literal 55149 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

BZ-HW@NawqX-7$F)*4#o}i_nPH9RMG7SP8Tm_ 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 PME`Ve>T6fsxF7K!!RLC* literal 0 HcmV?d00001 diff --git a/Documentation/Edge.svg b/Documentation/Edge.svg new file mode 100644 index 0000000..e9af1cf --- /dev/null +++ b/Documentation/Edge.svg @@ -0,0 +1,12 @@ + + + + + + + + V₁ + V₂ + V₃ + edge + diff --git a/Documentation/Example.png b/Documentation/Example.png new file mode 100644 index 0000000000000000000000000000000000000000..709424030b06354af0c4ac691eb441f119d853b1 GIT binary patch literal 67796 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 KMNUMnLSTZv%wjVD literal 0 HcmV?d00001 diff --git a/Documentation/Face triangle.svg b/Documentation/Face triangle.svg new file mode 100644 index 0000000..509c841 --- /dev/null +++ b/Documentation/Face triangle.svg @@ -0,0 +1,14 @@ + + + + + + + + + + V₁ + V₂ + V₃ + FACE + diff --git a/Documentation/Frustum culling/Frustum diagram.svg b/Documentation/Frustum culling/Frustum diagram.svg new file mode 100644 index 0000000..b59d4a8 --- /dev/null +++ b/Documentation/Frustum culling/Frustum diagram.svg @@ -0,0 +1,58 @@ + + + + + + + + + +Z + (view direction) + + + + Camera + + + + + + + + + + + + + + Near + + + + Far + + + + + + visible region + + + Top plane + Bottom plane + + + + ✓ + rendered + + + + ✗ + culled + + + + ✗ + culled + diff --git a/Documentation/Frustum culling/P-vertex AABB.svg b/Documentation/Frustum culling/P-vertex AABB.svg new file mode 100644 index 0000000..a3acfb8 --- /dev/null +++ b/Documentation/Frustum culling/P-vertex AABB.svg @@ -0,0 +1,38 @@ + + + + + + + + P-vertex: corner most aligned with plane normal + If P is behind the plane → entire AABB is outside + + + + Plane + + + inside frustum + + outside frustum + + + + + N + + + + inside + + + P + + + + outside + + + P + diff --git a/Documentation/Frustum culling/index.org b/Documentation/Frustum culling/index.org new file mode 100644 index 0000000..9c4a941 --- /dev/null +++ b/Documentation/Frustum culling/index.org @@ -0,0 +1,177 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Frustum & View Frustum Culling - Aukio 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 + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* Frustum & View Frustum Culling +:PROPERTIES: +:CUSTOM_ID: frustum-view-frustum-culling +:END: + +#+INCLUDE: "Frustum diagram.svg" export html + +The *view frustum* is a truncated pyramid-shaped volume that represents +everything the camera can see. Objects completely outside this volume are +skipped during rendering — a powerful optimization called *frustum culling*. + +** The Six Frustum Planes +:PROPERTIES: +:CUSTOM_ID: frustum-planes +:END: + +The frustum is defined by six clipping planes: + +| Plane | Purpose | +|---------+--------------------------------------------| +| Left | Left edge of viewport | +| Right | Right edge of viewport | +| Top | Top edge of viewport (smaller Y in Y-down) | +| Bottom | Bottom edge of viewport (larger Y) | +| Near | Closest visible distance from camera | +| Far | Farthest visible distance from camera | + +Each plane divides 3D space into "inside" (visible) and "outside" +(culled). An object must pass all six plane tests to be considered +potentially visible. + +** Frustum Culling vs Backface Culling +:PROPERTIES: +:CUSTOM_ID: frustum-vs-backface-culling +:END: + +These are complementary optimizations at different levels: + +| Optimization | Level | What it skips | +|-----------------+--------------+----------------------------------| +| Frustum culling | Object level | Entire composite shapes + children | +| Backface culling | Polygon level | Individual triangles facing away | + +*Frustum culling* happens first during the transform phase — entire +object trees are skipped with a single bounding box test. *Backface +culling* happens later during rasterization — individual triangles +are checked before being drawn. + +For best performance, use both: organize your scene with composite +shapes for effective frustum culling, and enable backface culling on +closed meshes. + +* How Frustum Culling Works in Aukio 3D +:PROPERTIES: +:CUSTOM_ID: frustum-culling-implementation +:END: + +Frustum culling is applied automatically to all [[../index.org#mesh][composite shapes]] +during Phase 1 (transform) of the [[../Rendering loop/][rendering loop]]: + +1. *Update frustum*: Compute 6 planes from camera FOV and viewport size +2. *For each composite shape*: + - Get its [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Box.html][Axis-Aligned Bounding Box (AABB)]] + - Transform all 8 corners to view space + - Test against frustum using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Frustum.html#intersectsAABB][intersectsAABB()]] + - If outside: skip the entire composite and all children + - If inside: continue transforming children + +(The root composite itself is never tested — it is always rendered.) + +** The AABB Intersection Algorithm +:PROPERTIES: +:CUSTOM_ID: aabb-intersection-algorithm +:END: + +The intersection test uses an optimized "P-vertex" approach: + +#+INCLUDE: "P-vertex AABB.svg" export html + +For each plane, instead of testing all 8 corners of the bounding box, +we test only the *P-vertex* — the corner most aligned with the plane +normal. If this "best" corner is behind the plane, the entire box must +be outside the frustum. + +- Plane normal points *into* the frustum (toward visible region) +- P-vertex: select corner based on normal direction + - If normal.x > 0 → use maxX (rightmost corner) + - If normal.x < 0 → use minX (leftmost corner) + - Same logic for Y and Z +- Test: =dot(normal, P-vertex) < distance= → outside + +This reduces from 48 tests (8 corners × 6 planes) to just 6 tests per +object. + +* Performance Benefits +:PROPERTIES: +:CUSTOM_ID: frustum-performance +:END: + +Frustum culling can dramatically improve performance for large scenes: + +- *High cull % (60-90%)*: Excellent — most objects skipped entirely +- *Medium cull % (20-60%)*: Moderate benefit +- *Low cull % (0-20%)*: Limited benefit — most objects visible + +A composite shape that is culled skips: +- Transforming all its children +- Computing bounding boxes for children +- All polygon-level operations (backface culling, rasterization) + +Open Developer Tools (F12) to see real-time [[../index.org#frustum-culling-statistics][frustum culling statistics]]. + +* Scene Design for Effective Culling +:PROPERTIES: +:CUSTOM_ID: frustum-scene-design +:END: + +Frustum culling works best when you organize your scene into +well-defined composite shapes: + +#+BEGIN_SRC java +// Good: Each building is a separate composite +AbstractCompositeShape cityBlock = new AbstractCompositeShape(); +for (Building building : buildings) { + AbstractCompositeShape buildingComposite = new AbstractCompositeShape(); + buildingComposite.addShape(buildingWalls); + buildingComposite.addShape(buildingRoof); + buildingComposite.addShape(buildingInterior); + cityBlock.addShape(buildingComposite); +} + +// Less effective: Everything in one giant composite +AbstractCompositeShape allObjects = new AbstractCompositeShape(); +allObjects.addShape(building1Walls); +allObjects.addShape(building1Roof); +allObjects.addShape(building2Walls); +// ... hundreds of shapes directly in root +#+END_SRC + +*Best practices:* + +- Use composites to group objects that occupy a bounded region of space +- Keep bounding boxes tight (don't add distant objects to the same composite) +- Nest composites hierarchically for multi-level culling (city → block → building) +- Call [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.html#invalidateBounds()][invalidateBounds()]] after moving shapes — the bounding box is + recomputed lazily on next use + +* Technical Details +:PROPERTIES: +:CUSTOM_ID: frustum-technical-details +:END: + +The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Frustum.html][Frustum]] class: + +- Computes planes in *view space* (camera at origin, looking along +Z) +- FOV derived from =projectionScale = width / 3= (≈112° horizontal FOV) +- Default clip distances: Near = 1.0, Far = 10000.0 +- Planes stored in [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html][Hesse normal form]]: (normal vector, distance) + +The frustum is updated once per render pass in +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.html][ShapeCollection.transformShapesBegin()]], from the camera state and +the stereo viewport width — in stereo mode each eye gets its own +frustum, so the update runs twice per frame. diff --git a/Documentation/Global illumination/Bounce estimator.svg b/Documentation/Global illumination/Bounce estimator.svg new file mode 100644 index 0000000..0d1972e --- /dev/null +++ b/Documentation/Global illumination/Bounce estimator.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + surface + + + + + + + P + + normal + + + + cosine-weighted + hemisphere + + + + + + + + + Q + + + + + light + + + + shadow ray: clear + + + + + + occluder + blocked + + + + at Q: direct light (cached shadow bits) + + Q's current indirect estimate + + + + P's target = (albedo / π) x (direct + indirect) + blended in with an exponential moving average + + one sample per texel per visit: one shadow ray + one bounce ray — bounce light ripples deeper every sweep + diff --git a/Documentation/Global illumination/GI pipeline.svg b/Documentation/Global illumination/GI pipeline.svg new file mode 100644 index 0000000..0f63023 --- /dev/null +++ b/Documentation/Global illumination/GI pipeline.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + scene snapshot + triangles + lights + + BVH + + + GI workers + Monte Carlo + sweeps + + + lightmaps + per-texel indirect + + shadow bits + + + composite + baseColor x light + double-buffered + swap + + + painter + plain texture + lookup + + + + + + + + + + bounce reads last sweep's estimate + + zero ray casting + on render threads + diff --git a/Documentation/Global illumination/Global illumination.png b/Documentation/Global illumination/Global illumination.png new file mode 100644 index 0000000000000000000000000000000000000000..990919392a119db5700fe02d83dc11bdbffd1496 GIT binary patch literal 389278 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≷VO*r8j2i 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%0Db_9; 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%0D~H+hSn*O&PEr9o6GrIQwhxOhu)oGPHYCowaXEKI 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%0AygdJP5C$dU0O>%9@r(&Xv-Ge^iaPSY?1H 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 + + + + + + + + + + + + + + + + + invalid half (u+v > 1) + filled from neighbors so sampling + near the hypotenuse stays clean + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + one texel = one surface + patch, sampled at center + + + + a (u=0, v=0) + + b (u=1, v=0) + + c (u=0, v=1) + + + + e1 = b − a + + e2 = c − a + + + + world(u,v) = a + e1·u + e2·v + texels = edge / unitsPerTexel + + every texel owns a fixed patch of the triangle — shadows and gradients live INSIDE the surface + diff --git a/Documentation/Global illumination/gi-converged.png b/Documentation/Global illumination/gi-converged.png new file mode 100644 index 0000000000000000000000000000000000000000..1f81bfd7332f94feda0bbbc5cde48e484df9a570 GIT binary patch literal 168546 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

W?Vl*o1GG`QX0L^~G~*J_g1tii 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%0APLX4&+tfhpr^V16NrRT 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⁣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 literal 0 HcmV?d00001 diff --git a/Documentation/Global illumination/gi-start.png b/Documentation/Global illumination/gi-start.png new file mode 100644 index 0000000000000000000000000000000000000000..6f7f3f091bcf64a7620c154e04c6ed61e496bc97 GIT binary patch literal 4238 zcmeI0TUZlk8pnqqV4;e%+SsB5Ls75~5`rde*_v9=6e%@!~WR}vwk~lr@HpS=d5s|*nDJ%fY11)XO2imInB)*N8ht|fd z=4&SO#x%M|3caB=T~0O0>BwZ{u20&fBdjon)?9|o?8zgJzNPYA606O|U`MY+5(UOo z2@&<~kc}7RE%1NU!xIwk&GgVG4j>`Bc}Oj&+PM zhv{$O0ekWthGO0Hb$Ho2avUSfe)=iwyN+ziP}s=};&<=&)K;0!U7dW_Ci4sBjQQ2w z)RQ@{H)$EBb4sZneTc|Vtd>(@H!?Zh(U%GF>nGo{ncu5SF-s~-Ez9Y6$Ov00&#lh# zrzZw+e}X2jCK7GtH)@>;P5sh1`j9n4VJ)Xf3IgaZ+IuD$IjMs=9pZAfx#lP=aUd^> zkQ=??adfEaJJY& za7Cm4jxKow9P_c`v!KCiNV+ziwqZ? z`n{WU<pj)qKrg^J@PS4C1UcKk&v^kSO$+)QmemiaI{jQfF$FtK?~L>iXm$Y*!0YO8C87y4K%ZNm28_3mE1(ztkb|ud=k3 z8Unn`<>d^Lg>D9i4e$wk{rbk*}_dm4vLQPILDOq1yqx?k!uF{{MOocrta zV%*B7i<^5W->|7*tpCMoJ%a{{uuGKn6?^KRq<5!3amJ4Ef+C=I_e@FI!Tdia5PiDT z@FNM7T?Sv7YC+ijAu$<>O23(tht1?B@Pp|~h*MukOtPZdZxNUF!n$uh(Tk}{Vt)Xp zYXJe*B*@%I=MCGMN8=M>^b=~3eCRc`D7}I|;f{?-|6!<{0(7THPPeVHZNC*Sg1LkL zA{|t%lBADEXhqy?o*$FtcN~|TuCZ>q5BQ=UnJFfw(@8v2SyMKh;~opym2%e7kY`*V zCy83Wnmd=x#pT4_2&=|Xr}<%mxh9KOlyoxtDxgHbLW{Ou1z6VUud&;jYuH7xE973w zSsH-afOfc7am<#h+QBTEPPe|}$r3#%uv)Fz$6ZK~(kiLMzsk*!Uq`BtJlRcw$;O_j zVsTIVEC}$se{E{oBQYK;<15+yzEF~`88l=Mmsi(JdyZ#chhSf&^X}v2vbE^LN$mU} zXhx?}S1^D&w}Dm_mZnpibvq5<%klKD<)rnBU$0}DBz?yk9IpwR-v%~lrL47&DSC-_ z+W8IG8i02Ka6IoWz89g7Yzrl*4gV(`!AWX>17Ghk`rYb_B)!0w=^1Lw0yqAlS62Xx zDIlSC-Lg&?%fVOj8EVc3wC_QT7u;iY^|Yt>?qzWsHvs=zz`vF@NCE0{*r;YZ1KX=c+u0C6IEESt`2Db^+Q`-K z0MTM)cl*LglOiyV<#1Nv=jamy!zj1^7x{?W<3kW?&t#mFGyzOQ9%O&$9y|~!1lKZLF0DRCZ}~fEyzFxdEETEGFsmhURU>( z!nNqVYzmHMJs-amhTl7Tjb8oQIN6t=g4MEz_@u+@GY#9# zIS&#E;H2Q_GJyURpoKJ5KVHEjt0Eb4QSA=Ur&*)p6$&KHh~CPk@G2Ye*_IJEWobQR z;8xZ^039OI=HfYZ866ElM!D-x$Y+qFhH#^`sxOE%8isE`mv_;BmSZC+hc_xMxcGYC zP-_#)t&!y*zk@O(V=bsr#TD1((Ym}rPL*gV+ksb-4o13NSJdf_eKvKj2D&h;wpU}XsNGj5kahjzYFC8ph02lsR)N%F(fPSC-ViJ5a zojp|e7bKER)JxT1t9^n8eP650TIy&In=35LOHfRcXhaVWDC5kdqbP*saC{LHIjDB? zEtI3kbV?~pm+*7$ORDmiWptRepVQdG&JVEldkk6lok@g{->GLEblf|9WGq`BusBzU zlx!#$(Rx)QhL5o_!bq^S3%^HU6WQkFG?r^o=<0ikQXyLE6Wx~4t_w56&)#Dcb4Asl zIu3C%>CCt5PC)~Yhl~mN;e1FFt=+;qZ-|o%xkv137S|`$ASDfkPh*3uHQNvpp)~{m + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* What global illumination adds +:PROPERTIES: +:CUSTOM_ID: what-gi-adds +:END: + +Plain per-polygon shading lights every polygon with one flat color: +no shadows, and surfaces that receive no direct light stay uniformly +dark. A room corner reads as a flat silhouette instead of a corner. + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:gi-flat.png]] + +*Aukio 3D* can optionally compute *global illumination* progressively +on background CPU threads: shadows appear, light pools under lamps +with smooth falloff, and colored light *bleeds* — a red sofa tints the +floor next to it red. All of it converges gradually over the first +seconds of a scene, then idles. + +Same camera, same house: flat shading (top) versus converged GI +(below). Note the soft shadow of the partition wall, the lamp glow on +the ceiling, and the subtle color variation across the floor. + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:gi-converged.png]] + +* The big idea: GI off the render path +:PROPERTIES: +:CUSTOM_ID: off-the-render-path +:END: + +Ray tracing is far too slow to run per frame in a software renderer, +so it doesn't: *the render loop never traces a single ray.* Painting a +lightmapped triangle is an ordinary texture lookup, exactly as fast as +any textured polygon. + +All the expensive work happens on dedicated low-priority worker threads +that continuously refine per-surface lighting values. Whenever the +values have improved enough, the workers regenerate each triangle's +*composite texture* (baseColor x total lighting) into a back buffer +and swap it in atomically — painters never see a half-updated texture. + +#+INCLUDE: "GI pipeline.svg" export html + +Because painters only read finished textures, frame rate is completely +decoupled from GI quality: you can crank lightmap resolution up and the +only cost is CPU time on the worker threads, not frame time. + +* Lightmaps: a texture per triangle +:PROPERTIES: +:CUSTOM_ID: lightmaps +:END: + +Flat shading can only color a polygon uniformly — shadows and gradients +need resolution *inside* the polygon. Every lightmapped triangle +therefore owns a small generated texture, its *lightmap*, whose texels +map onto the triangle surface by an affine rule: + +#+INCLUDE: "Lightmap mapping.svg" export html + +The triangle's UVs are pinned to (0,0), (1,0), (0,1), so the valid +texel region is the half where u+v <= 1; the other half of the square +texture is flood-filled from valid neighbors so that nearest sampling +near the hypotenuse never picks up garbage. + +Each texel stores two things, both written only by GI threads: + +- *Indirect irradiance* (RGB floats) — the accumulated bounced light. +- *Per-light visibility bits* — whether the last shadow ray from this + texel reached each lamp (cached so later queries cost nothing). + +Resolution is set in world units per texel +(=LightmappedCompositeShape.setLightmapUnitsPerTexel()=, default 12): a 100-unit +wall cell gets an 8x8 lightmap. Halving the units quadruples the +tracing work. + +*What you trace is what you see:* the composite texture the painter +samples is the lightmap itself, at native texel resolution — there is +no upscaling step. Shadow-edge smoothness comes from tracing at finer +resolution, never from interpolation. (An earlier bilinear-upscaling +pass produced visibly artificial results and was removed; finer texels +cost more CPU on the GI threads but look right.) + +* One sample: a shadow ray and a bounce ray +:PROPERTIES: +:CUSTOM_ID: one-sample +:END: + +The work list is flat: one item per lightmap texel (and one per plain +polygon, see below). Worker threads walk it round-robin, and every +visit to a texel casts exactly two rays from the texel's world +position, nudged slightly off the surface along the normal: + +1. *Shadow ray* toward a lamp. Answers visible/occluded, cached in the + texel's visibility bits. On a texel's *first* visit all lamps are + tested at once, so direct light and hard shadows appear after a + single sweep instead of trickling in lamp by lamp; later visits + re-test one lamp at a time, round-robin. +2. *Bounce ray* in a random cosine-weighted direction around the + normal. Wherever it lands (point Q), the sample reads Q's *direct* + lighting — using Q's cached shadow bits, no new shadow rays — plus + Q's *current indirect estimate*, and blends the sum into the texel's + own indirect value. + +#+INCLUDE: "Bounce estimator.svg" export html + +Reading Q's current indirect estimate instead of recursing is what +makes bounce light propagate: sweep 1 learns "Q is directly lit", +sweep 2 learns "P sees a lit Q", sweep 3 learns "R sees a lit P"... +Light ripples one surface deeper with every sweep, with no recursion +limit and no exponential ray explosion. The =1/pi= diffuse gain keeps +the feedback loop from diverging: without it the indirect term +amplifies itself and the scene saturates to white. + +* Progressive convergence +:PROPERTIES: +:CUSTOM_ID: convergence +:END: + +Monte Carlo samples are noisy, so blending happens at *two nested +levels*, both exponential moving averages: + +1. *Inner, per sample*: each texel blends every new bounce-ray result + into its indirect estimate with a constant weight (=alpha = 0.15=, + mode =fixed=). Every ray hit stays equally intensive forever — an + unlit area fades to darkness at the same rate a lit area brightens. + (The old =adaptive= mode, which decays alpha with sample count, is + still available; see the knobs below.) +2. *Outer, per composite update*: the value that reaches the screen is + a second EMA over the COMPLETE sum =ambient + direct + indirect=. + The texture can only move =e3d.gi.compositeAlpha= (default 0.2) of + the remaining distance per 500 ms update — so direct light, shadows + and bounce light all glide in together over a few seconds, and no + single-frame jump is possible by construction. + +The world starts at a *uniform medium irradiance* +(=e3d.gi.initialIrradiance=, default 128): the scene is visible from +the very first frame, then lit areas brighten and unlit areas sink to +darkness as the workers sweep — lights and shadows gradually become +distinguished instead of the old pitch-black start with a sudden flash +once the first sweep landed: + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:gi-start.png]] + +Consequences of the design: + +- *Hysteresis is free*: when a lamp moves or geometry changes, old + light fades out gradually instead of popping — the same pair of EMAs + that accumulates light also drains it. +- *Convergence detection*: per-sample deltas are pure noise (and with a + constant alpha they never settle), so the system watches the average + per-texel movement of the on-screen estimate; after five composite + updates below =e3d.gi.calmThreshold= (default 1.0 light unit) the + workers drop to a low duty cycle (~50% of sweep time, capped) instead + of burning CPU. Any scene or light change rebuilds the snapshot and + restarts full-speed tracing. +- *Despeckle*: at composite time the indirect channel is blended 50/50 + with the mean of its valid 4-neighbors, killing single-texel Monte + Carlo spikes without blurring real gradients. +- *Composite cadence*: textures regenerate at most every 500 ms — one + atomic swap per triangle, invisible to painters. + +* Plain polygons get GI too +:PROPERTIES: +:CUSTOM_ID: plain-polygons +:END: + +Surfaces that are not lightmapped (ordinary =SolidPolygon= with shading +enabled) still benefit, at per-polygon resolution, through the +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.html][GiLightProvider]] +interface that =GlobalIllumination= installs into the +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]]: + +- =isLightVisible(polygon, light)= answers from cached shadow bits — + direct-light shadows fade in on flat-shaded geometry. +- =addIndirectLight(polygon, baseColor, result)= adds the polygon's + bounced-light estimate into the flat-shaded color. + +Both are called from parallel render-pool threads, so they only read +volatile caches — never trace. + +* Enabling GI +:PROPERTIES: +:CUSTOM_ID: enabling +:END: + +#+BEGIN_SRC java +// Per-texel lightmaps on composite geometry (the House demo setup): +LightmappedCompositeShape house = new LightmappedCompositeShape(); +house.setLightmappingEnabled(true); +house.setLightmapUnitsPerTexel(3.0); // fine texels: quality from traced rays + +// Start the workers (2 threads by default; more converge faster): +viewPanel.enableGlobalIllumination(4); +#+END_SRC + +Tuning knobs (system properties): + +| Property | Default | Effect | +|---------------------------+---------+-----------------------------------------| +| =e3d.gi.alphaMode= | fixed | =adaptive= decays the inner EMA alpha with sample count | +| =e3d.gi.alphaFloor= | 0.08 | adaptive-mode floor; higher adapts faster but noisier | +| =e3d.gi.compositeAlpha= | 0.2 | outer EMA: fade speed of the on-screen estimate per 500 ms update | +| =e3d.gi.initialIrradiance=| 128 | uniform medium start (0..255 light units) | +| =e3d.gi.calmThreshold= | 1.0 | convergence: avg estimate movement (light units) | +| =e3d.gi.despeckle= | true | neighbor-smoothing of indirect at composite time | +| =e3d.gi.debug= | false | sweep statistics to stdout | +| =e3d.gi.dumpLightmaps= | (unset) | dump composite lightmaps as PNGs to the given dir | + +* Limitations +:PROPERTIES: +:CUSTOM_ID: limitations +:END: + +- *Diffuse light only* — no specular bounce, no caustics. +- Polygon vertices are traced in composite-local space; scenes that put + non-identity transforms on composites are traced incorrectly. +- The bounce estimate is one ray deep per sample — correctness comes + from sweep-over-sweep propagation, so deeply indirect corners take + several sweeps to brighten. + +* Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|----------------------+---------------------------------------------------------------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/GlobalIllumination.html][GlobalIllumination]] | Progressive tracer: sweeps, EMA convergence, composite swaps | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/Lightmap.html][Lightmap]] | Per-triangle texel state + double-buffered composite textures | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.html][LightmappedTriangle]] | Textured triangle whose texture is the GI composite | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/gi/TriangleBvh.html][TriangleBvh]] | BVH over world triangles: nearest-hit and any-hit ray queries (Möller–Trumbore) | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.html][GiLightProvider]] | Cache-read interface feeding the flat-shading path | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightmappedCompositeShape.html][LightmappedCompositeShape]] | Wraps polygons into lightmapped triangles | diff --git a/Documentation/Mesh.svg b/Documentation/Mesh.svg new file mode 100644 index 0000000..8a20f46 --- /dev/null +++ b/Documentation/Mesh.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + triangulated + section + + diff --git a/Documentation/Near plane clip/Clip algorithm.svg b/Documentation/Near plane clip/Clip algorithm.svg new file mode 100644 index 0000000..1d09a7e --- /dev/null +++ b/Documentation/Near plane clip/Clip algorithm.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + behind (z ≤ near) + in front (z > near) + + + + near plane + + + + + + + + + + + + + + v0 + + v1 + + v2 + + + + p′ + + p″ + + + t = 0.5 along v0 → v2 + + + + + t = (near − z1) / (z2 − z1) + p = p1 + t·(p2 − p1) + uv = uv1 + t·(uv2 − uv1) + + every edge crossing the plane spawns an interpolated vertex; + in-front vertices pass through unchanged + diff --git a/Documentation/Near plane clip/Fan triangulation.svg b/Documentation/Near plane clip/Fan triangulation.svg new file mode 100644 index 0000000..94a8656 --- /dev/null +++ b/Documentation/Near plane clip/Fan triangulation.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + v0 + + v1 + + p″ + + p′ + + + T1 = (v0, v1, p″) + T2 = (v0, p″, p′) + + + a triangle cut once + becomes a quad; + the rasterizer paints it + as a 2-triangle fan + sharing v0 + diff --git a/Documentation/Near plane clip/Near plane straddle.svg b/Documentation/Near plane clip/Near plane straddle.svg new file mode 100644 index 0000000..aa2c9a6 --- /dev/null +++ b/Documentation/Near plane clip/Near plane straddle.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + z (depth) → + x ↓ + + + + + camera + + + + + near plane z = 1 + + + + + + + floor tiles + + + + + + kept fragment + cut away + + + old: one vertex behind ⇒ + whole tile dropped + + + + new: clip at the plane, + paint the surviving fragment + + diff --git a/Documentation/Near plane clip/index.org b/Documentation/Near plane clip/index.org new file mode 100644 index 0000000..0c59a70 --- /dev/null +++ b/Documentation/Near plane clip/index.org @@ -0,0 +1,151 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Near-Plane Clipping - Aukio 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 + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* The problem +:PROPERTIES: +:CUSTOM_ID: the-problem +:END: + +When the camera brushes against geometry — a floor tile under your +feet, a wall you lean into — part of a polygon can end up *behind* the +viewer while the rest stays in front. Perspective projection divides by +depth (=screenX = x / z=), so a vertex at z ≤ 0 has no meaningful screen +position at all. + +The naive way out — dropping any polygon that has even one vertex +behind the camera — makes whole tiles vanish exactly when they are +closest and largest on screen. Walking through the House demo, floor +tiles blinked out of existence at the bottom of the frame: + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:near-clip-before.png]] + +*Aukio 3D* instead *clips the polygon against the near plane* and +renders the surviving fragment. The same frame with clipping enabled — +the floor is solid to the bottom edge: + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:near-clip-after.png]] + +Think of the camera plane as the edge of a table and the polygon as a +sheet of paper partly hanging off it. Dropping the polygon means +throwing away the whole sheet. Clipping takes scissors, cuts the sheet +along the table edge, and keeps the part that lies on the table. + +#+INCLUDE: "Near plane straddle.svg" export html + +* Why not just clamp z? +:PROPERTIES: +:CUSTOM_ID: why-not-clamp-z +:END: + +A tempting one-liner is to force every vertex to =z = max(z, epsilon)= +and project anyway. It fails geometrically: a vertex at z = −50 clamped +to z = 0.01 projects to a screen coordinate thousands of pixels away, +*in the wrong direction* — the sign flip of the division mirrors it +through the camera. The polygon smears into giant streaks across the +frame instead of ending cleanly at the screen edge. + +Clipping produces the geometrically correct cut: the polygon's new edge +lies exactly on the near plane, and everything the rasterizer receives +has z > 0. + +* How the clipping works +:PROPERTIES: +:CUSTOM_ID: how-it-works +:END: + +Clipping happens in *camera space*, after the transform stack has moved +vertices relative to the viewer but *before* the perspective divide. +The vertex loop is walked edge by edge (Sutherland-Hodgman style) +against the plane =z = nearPlaneDistance=: + +1. An in-front vertex passes through unchanged. +2. An edge that crosses the plane spawns a new vertex at the + intersection, with position, UV and normal all interpolated with the + same parameter =t=. +3. A behind-plane vertex is skipped. + +#+INCLUDE: "Clip algorithm.svg" export html + +Interpolating UVs linearly along the 3D edge is exactly right for the +perspective-correct texture mapper: the intersection vertex is a real +point on the original edge, so its texture coordinate is the same blend +of the endpoints' UVs. Textured fragments therefore show the correct +texels right up to the cut, with no seam. + +Only a polygon with *all* vertices behind the plane is culled — the +legitimate version of the old behavior. + +* From clipped loop to pixels +:PROPERTIES: +:CUSTOM_ID: from-clip-to-pixels +:END: + +A convex N-gon crossing the plane clips to a single contiguous loop of +at most N+1 vertices. For the triangle-based rasterizers this means a +triangle can become a *quad*, which is painted as a two-triangle fan +sharing the first vertex — exact, because the clip of a convex polygon +stays convex: + +#+INCLUDE: "Fan triangulation.svg" export html + +Shape support: + +| Shape | Behavior when straddling | +|--------------------+---------------------------------------------------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Clipped loop painted as triangle fan | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | Fan-painted with interpolated UVs (also inherited by lightmapped GI fragments) | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] | Shortened to the in-front endpoint + intersection | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.html][Billboard]] | Single anchor point: culled when behind, as before | + +Implementation notes: + +- Clipped output is stored *per pipeline slot* on the shape + (=clippedVertices(ctx)=), so the triple-buffered pipeline can + transform frame N+1 while frame N is still painting. +- Depth sorting and tile binning use the clipped vertices' average Z + and screen bounds — a clipped tile sorts as the fragment it became, + not as the polygon that reached behind you. +- New intersection vertices exist only in camera space; they are + projected directly via + [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html#setCameraSpaceCoordinate(double,double,double,eu.svjatoslav.aukio.e3d.gui.RenderingContext)][Vertex.setCameraSpaceCoordinate()]], + bypassing the transform stack. + +* Configuration +:PROPERTIES: +:CUSTOM_ID: configuration +:END: + +The near plane distance is a per-context knob, in world units: + +#+BEGIN_SRC java +// Default is 1.0; smaller values let the camera press closer to +// geometry before the scissors bite, at the cost of larger projected +// coordinates for clipped fragments. +viewPanel.getRenderingContext().nearPlaneDistance = 0.5; +#+END_SRC + +* Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|---------------------------+----------------------------------------------------------------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]] | Vertex-loop clipping in =transform()=, per-slot clip storage | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Vertex.html][Vertex]] | Camera-space projection for generated intersection vertices | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] | Carries =nearPlaneDistance= | diff --git a/Documentation/Near plane clip/near-clip-after.png b/Documentation/Near plane clip/near-clip-after.png new file mode 100644 index 0000000000000000000000000000000000000000..f135bd2633562c53cf1c72f1703875d6a79bb11d GIT binary patch literal 12046 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 literal 0 HcmV?d00001 diff --git a/Documentation/Near plane clip/near-clip-before.png b/Documentation/Near plane clip/near-clip-before.png new file mode 100644 index 0000000000000000000000000000000000000000..6b27c449a20cbea311cef595a335f9b44da5049f GIT binary patch literal 10900 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 JtDfAP{|CAt3KjqW literal 0 HcmV?d00001 diff --git a/Documentation/Normal vector.svg b/Documentation/Normal vector.svg new file mode 100644 index 0000000..016136e --- /dev/null +++ b/Documentation/Normal vector.svg @@ -0,0 +1,18 @@ + + + + + + + + + N̂ + unit normal + (perpendicular + to surface) + + + Light + + L · N = brightness + diff --git a/Documentation/Perspective correct textures/Adaptive interval.svg b/Documentation/Perspective correct textures/Adaptive interval.svg new file mode 100644 index 0000000..924bc5c --- /dev/null +++ b/Documentation/Perspective correct textures/Adaptive interval.svg @@ -0,0 +1,59 @@ + + + + + + perspective curvature → rises toward the far end + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 16 px + 8 px + 4 px + 2 px + + one scanline, 100 px → + grazing-angle floor, far side + diff --git a/Documentation/Perspective correct textures/Affine distortion.png b/Documentation/Perspective correct textures/Affine distortion.png new file mode 100644 index 0000000000000000000000000000000000000000..8d3722b8592e9b229220394a85aa185cc44ee8b7 GIT binary patch literal 25825 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 g0pA(|m!{n3H+ + + + + + + + + + + + + + + screen pixel → + texel u ↑ + + + + + + + + + + + + + + + + + + + + exact perspective + + corrected every 16 px + + plain affine + diff --git a/Documentation/Perspective correct textures/index.org b/Documentation/Perspective correct textures/index.org new file mode 100644 index 0000000..80a7afe --- /dev/null +++ b/Documentation/Perspective correct textures/index.org @@ -0,0 +1,186 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Perspective-Correct Textures - Aukio 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 + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* 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 *Aukio 3D* engine solves this with *subdivided perspective +correction* inside the scanline rasterizer — the same technique Quake +used. + +* How perspective correction works +:PROPERTIES: +:CUSTOM_ID: how-perspective-correction-works +:END: + +Texture coordinates (u, v) are not linear in screen space, so they +cannot simply be stepped per pixel. But divide them by depth and they +become linear: *(u/z, v/z, 1/z) all interpolate linearly* across the +triangle in screen space. + +The rasterizer exploits this: + +1. Compute (u/z, v/z, 1/z) at each vertex +2. Interpolate all three across the scanline with plain additions +3. Every N pixels, recover the exact texture coordinate with one + division: =u = (u/z) / (1/z)= +4. Between correction points, step u/v affinely toward the next exact + point + +#+INCLUDE: "Scanline correction.svg" export html + +The orange polyline hugs the exact green curve: within each 16-pixel +block it is a straight line, but every block starts exactly on the +curve. The dashed pink line is plain affine interpolation — visibly +wrong everywhere except the endpoints. + +Think of it like walking with a map that is slightly distorted: you +walk in a straight line, but every 16 steps you check a landmark and +correct your course. The correction (a division) costs something, so +you do it every N pixels instead of every pixel — the divide cost is +amortized to 1/16th of a per-pixel-correct rasterizer. + +#+BEGIN_SRC java +// Per scanline (simplified from drawHorizontalLinePerspective): +while (done < span) { + // Advance (u/z, v/z, 1/z) to the end of this block + su += dsu * block; sv += dsv * block; sw += dsw * block; + double txNext = su / sw; // one reciprocal = exact texture position + double tyNext = sv / sw; + + // Step affinely through the block + double txStep = (txNext - tx) / block; + double tyStep = (tyNext - ty) / block; + for (int i = 0; i < block; i++) { + plot(x++, texture.sample(tx, ty)); + tx += txStep; ty += tyStep; + } + done += block; +} +#+END_SRC + +** Adaptive correction interval +:PROPERTIES: +:CUSTOM_ID: adaptive-correction-interval +:END: + +Quake used a fixed 16-pixel interval. This engine keeps 16 as the +default but *shrinks the interval when the error bound demands it*. + +The error of affine stepping within a block grows with both the +texture gradient (texels per pixel) and the perspective curvature +(how fast 1/z changes across the span). Each scanline computes + +#+BEGIN_EXAMPLE +error(texels) ≈ texelRate · interval² · k / 2 +#+END_EXAMPLE + +where =k = |d(1/z)| / min(1/z)= is the per-pixel relative depth change, +and picks the largest power-of-two interval from the ladder 16, 8, 4, +2, 1 that keeps the bound under half a texel. Flat, gently angled +spans keep the fast 16-pixel cadence; a floor tile seen at a grazing +angle drops to shorter intervals exactly where the curvature is high. + +#+INCLUDE: "Adaptive interval.svg" export html + +** When affine is good enough +:PROPERTIES: +:CUSTOM_ID: when-affine-good-enough +:END: + +For small or nearly flat triangles, plain affine mapping is already +within half a texel of exact perspective, so the perspective setup is +skipped entirely. The test compares the texture range the triangle +covers against its depth variation: + +#+BEGIN_EXAMPLE +affine is sufficient when texelSpan · (zMax/zMin − 1) < 2 +#+END_EXAMPLE + +Note the criterion is the *texel* span, not the pixel size — a tiny +on-screen triangle can still map many texels into few pixels. Distant +clusters of small triangles (a common case) all render through the +cheaper affine path. + +Triangles straddling the near plane (any vertex closer than z = 0.001) +also fall back to affine, because 1/z interpolation is invalid there. + +** Toggling the correction +:PROPERTIES: +:CUSTOM_ID: toggling-the-correction +:END: + +Perspective correction can be switched off globally for A/B comparison +or debugging: + +#+BEGIN_SRC java +TexturedTriangle.setPerspectiveCorrectionEnabled(false); // plain affine everywhere +#+END_SRC + +With correction disabled, large triangles at steep angles visibly warp +— useful for demonstrating what the correction actually buys. + +* Mipmap selection +:PROPERTIES: +:CUSTOM_ID: mipmap-selection +:END: + +Perspective correction fixes *where* a texel is sampled; mipmapping +decides *which resolution* to sample from. Each triangle estimates its +screen-pixels-per-texel ratio from edge lengths: + +#+BEGIN_EXAMPLE +scaleFactor = (sum of screen edge lengths) / (sum of UV edge lengths) · 1.2 +#+END_EXAMPLE + +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html#getMipmapForScale(double)][Texture.getMipmapForScale()]] +then picks the lazily-generated mipmap level closest to that scale: +halved resolutions when the texture is minified, doubled when strongly +magnified. Sampling a smaller mipmap under minification both speeds up +rendering (better cache behavior) and reduces aliasing. + +* Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|-------------------------------+--------------------------------------------------------------------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | Textured triangle with perspective-correct and SDF rendering paths | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PerspectiveBorderInterpolator.html][PerspectiveBorderInterpolator]] | Edge walker interpolating (u/z, v/z, 1/z) along triangle borders | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.html][PolygonBorderInterpolator]] | Edge walker for plain affine mapping | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html][Texture]] | Mipmap container; also carries the SDF mask and color layers | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html][TextureBitmap]] | Raw pixel array for one mipmap level | + +*See also:* + +- [[file:../SDF textures/][SDF textures]] — signed-distance-field glyph rendering, the + alternative sampling path in =TexturedTriangle= that reuses the same + perspective-correct interpolation for crisp text at any angle. diff --git a/Documentation/Point3D vertex.svg b/Documentation/Point3D vertex.svg new file mode 100644 index 0000000..0954bac --- /dev/null +++ b/Documentation/Point3D vertex.svg @@ -0,0 +1,105 @@ + + + + + + + +Point3D +raw coordinates + + + + + + +(x, y, z) + + + + + +.getDistanceTo() + + + + +.rotate() + + + + +.add() +.subtract() + + + + +.crossProduct() + + + + +.unit() +.dot() + + + + +.multiply() + + +Mutable, fluent API +Positions, vectors, math + + +Vertex +rendering-ready wrapper + + + + + + + +coordinate +Point3D + + + +wraps + + + +transformedCoordinate + + + +onScreenCoordinate + + + +textureCoordinate +UV + + + +normal +for CSG + + +local +camera +space +2D +pixels + + + +local +screen + + + +Tracks position across coordinate spaces + diff --git a/Documentation/Rendering loop/CPU scheduling.png b/Documentation/Rendering loop/CPU scheduling.png new file mode 100644 index 0000000000000000000000000000000000000000..12aa23d4bac229cc02fde38f30072dcd7fdd16e7 GIT binary patch literal 30584 zcmb@sby!@>vj5HANq{6sLT~~T+}%9{3+@mY+y-~oBqTUY2*G7=4emN&;|%WZ?k+RP zkL+_2IQO1=-*cbm`os+18iGh>3t<5V{b8C~^50Q`@onI}!58z^Y<>>D2XzA?iWblfO zg^lYKD<=~NI}-=zS4>>d+uI17#NRoo*qS=I8aS9BfoyE8O&A@G9864X9L;Q<_8vfm zkdPiADaxvf|LjyF9f_Fh8K{hfh2`0^XCX*P`&f-7=$%MT6D)G3pP$4qJYb6=%+_W% zEJqI)c*xvBoNUFR$DlRxnYw5Gc~cWcn*eaIoUXMIFIJ08?ukJG3BT&i&5gdk{;OB7 zz}TtMq>5j@eBtNkCr3guLqfvG$ETp6aC`Plg@xty>(@{y^zGZXC@3gsXisr*ajmSZ zn3$Nz$jIpE=pH|LLPA2~@Z^aI2}xpN;>~9wH#av$MMX0+GbKKw^73+3RaI8iVtz>z zC&oO>aB@DZYPuO$NBXsx;cpkDuJdF}mD-@PAVR^_F!;)T2oMOhIpF)(W z&tZs($H^$3oRX5l_>?u^)wk*iByMyjrta4YloAox&SX3?Ph>vv(sL!f`T%5AW(3g5 zkiKCO)4~EHLSHe3r3e4unhYQ=dmU4m#U@OO0yV~_eE#Sq`D0yOT_hqRMGHbC6%}+2 zjwfu-$l_UGF%+Ghox$lyOk7B>xsdo?6421l7}j!|abSQ{_>joSk<84XsYvv3aWo`^ zU@#c`pm8LuK*y^aLWKSL&;o!VwViF)DW2dA0O5@a?&3G*S4Z;ZV zvXNaB3&*0w?zSg=3lvAfrL{3(W1xA&uc*a}PGv1ElFjz9O-Bp~1&!$eWu+X4lrpE7 zi!|wLln(4`mfoDAp2w&NmV+(%AQu1_h>Ka68x@*g~XyWL_9WLft$%N>Ed z7WR4X%YuA_r9ev3L{8L!_SpSl@RwGaE}AZu7acTM&Aq8Pg_GG98asI^Ytv&|Sm&G5 zK+ip`lXqvkD$W*j_CNCtAGrN9)Bo#_zHIUNB41NTs~7O;P{Zu;n+dF^bybgD@Q~>n zFI;0#XL~T!y^3^6Ub$V+8Ov=7?z(QBD196Q5?^>b#?H%0`rys#k-I?^&z>DonZbHc z9wroN?H{;A&Rahf!j0K|&cN7(Kpej*yC}3t4UPu8TJAk;nvJ$5*XF`5xJmMBAow^d zGMn%z=yRC!$FPglM5#-g9jj;CRx6Im8h-o_>(D)$Zw?mEh#o?r9aWs(@bQwNArqPH zYp;;`wV9^iy%|gmR3A6)cLxoZ=E6-1%%Y)qOkdsgCpLfSOab0{>cTPJO|W9<%*iP1o1V(vtEPM!w>bJA9pW~PLsiW@*v zs4?i`TfW(6#?kXq;4>-%!=+2{k4Hp3ypiWKnoxz}#>hS?IdmT{vHhjpMz(67CCEeW z!ZqK%c%W25o804TqU9an&8uo>&*H^8rxn|W8cgWwUMQzGpY?A(tVV{m>wvP$PTeXLu7|&7&kqBIJH#MWIcw6aS;eGfzSt_(!aG9jP zl3d&HOtx6`5_k31E`ERLK>OC2pgWlKu0=#PF)ii$bDR}`Bb!7Pes-RuvUuCC%9Z~l`EB>3+)REwQ*5`t z5RSJ)jHN@^Vof<2{7M45x?X97L(C72`zvC)Jd)9*zfk*qtci_mI%e>?<|u3w=`A=H za9|rZ@xqCit`d?*M)?fQVq4D-Xt7lxK)XP%kW?Y)F0iG&{hNl`Prx_Rm_-${Ltlzu z85A!-;IxBYhxTOE5Q?VWYCC#Ax-8bSf1nvP?1Mg@&0taWXv)m z8AvaWJX2ZX9h;HDFJ$&L($au4eERDK+xpT#MZv5JRD8Yenwc~at#;PR(gutU(+?%} zBpK8-DF8%@On%%=#IM?p5pfU0GAWjhPt;Mb@osq2`CV1HsGIX7I65Ui;dpJk?opOl z?N1AT{(@{4(z3k+x2{!?sA<(*m=uT3IT0`{aksEj`)Mbv+I4m6=Yd}m$eui342~43 zvbF-41_km9ZKIk!c70hxIb_n`o^A@Ui(8iy?uZi68g)}Ck_D;c-^Ox8lvQsi)P>pdCDb(Xm<>)q=Ox1m3h%A z#Z|nPsl-*h?M~)RiiQ3R#dPkePg`n^77?xGAQ0D0U5?8Yw9bV=04tXcO&M~c=+|N<)9WH{y+}PUfU*r`0G|Rsc|jZ{jdrmJ!qKP34tYJLvpA)JWGc*So|JOI zlWaYj>SqVjh>?X+%+8cPX)6xt$h5D7CI#5eVL5bDyIWfvj9GMi$zkcB@JMG{=5tid1|}MX}1) z#=e^L5;+2aIxFgRvoJWNABQ>Yp97niuy7G(${15{(oy(bXK_>sCUojgV#WPE+D>b` z?_M}8#kTZqL{m8>o5>|tL=%;ds6foOreR7pKN>&MLSweK*%cm72aCdw&-AYvJ>585 zsQpSP6=yn9dK965_fAB0+dGwCBjD8Zxz&wHalyHCr#|rDV6KjmZ9OZzHloQ3+GETz zXB!CWNJ*sERQ^U)v&s0@mZE>o?*y^CFfgv8%uz|2MNhzicEV3fNED__@NJkz1^J3w zI+tC~CT2G4o7~dI)LLDr!d&1fx`i%sT}Y9ul}mKpY{5g=)QgHc2pUjPS|>Kl5FnWOD$J780h(@ zA5gN@em4#Zz0kg$OzZaH<~gG!1oBL)M5P-drlSR@*C^)S8#HYuJHbtptK^RckLekN zBsWeo>6FvSY-S3}7QoJrOO5A-TVVRG2Wl!9RTMmx25%%yu8XKev|$&RD)dX-!z3l> zFXS%$-7sup6=hmQ`5z<*$THsY47%_CxCZ=8gO%WTW-He=cK&OcsaOppJxC}rd$m+9aWuFHwM{c&6wxaEf{dvuD5$BiB9Z(pZ3G~dGb8k{8Ye}R33h&c% zl&ZVH)S}xM+nez1SN@6K8o%i2cEI@TAkPYhR@c$OS!w6X@^72uEjYJB5J8|O(zmfxFw`@hypFjO14?zB%=gA)hGHjJ#gBfhMD;94!Z22rz2W65VhUAA{KKl`+< zfxqy(RKd>QfB}bqIvD_1%%FX(s3v3PvVAxDcV-aBW@mgx`*!AUhnYFQWLyd_@K7GU;U#P!2DJ0KZZioY#rTG5@WA_gzk>(0X^u71q9ZTT7&+?vs*ZY94gk)Ds-Xym%W`6rTT$-~P(f4nt(ij9P{ zjLL?3s|Ksz3L`)9`2z{*eS`=+^~e6x|EduG@&Au^`aTvGjlR8W0RyKjTF3x>La+DW z14FIw+?tx)#@zW_m7`{mCqAPeCJVaIk9IfKr1zacL7k0Pqx4&6auln2Y^72hsJh0 zZ=x0x>365fm1p&D%wNgUovV%5UBM=c7nO%M)i&Mc$Gze)Z3%A%-p_p^7{bjT(<#U>o~d z8T)Tz0&<$#13OcAC8TUaG&GKHP9;39@-~`ezKC?ZiuE_y|AXM=GXjo)&CP&}Tza@K zqQnBOHe}>R>i0#21syj3fQ{!B+c@THWKutU!ssRx<7aGvy~qR)hkjbY#uLs~U3K;O znF2)E7cMSi)^{5^yL6k`L_`Wr93CX+_!>gW|D1m&fg9yO(46nhHY~dTp8?S4(E$3_ z0MJCv_I#6g0pDV8_{UvnLO(dpkpM=mPQE5~mJ2D*vK!%>!%hFC@woAi=BC+h$zzKM zMkYqatZ%N2`;_4jUPpnztw4FK)6b|tLbkj7NXTJ-s9>44QV z8mfnasqmr{bCKx!Vd103mVl8FOj=6iIxzvN!qFAvx7~hMjn@203hu*QF10#(vKP&K zZK2Z+6Y8>6t5sIxZ2J@6;PmVt;E0jMuY^IRfij)?Ot-@Yj9edsv3;4Qc3>$Pm8qzX z6J}gCgsB%29$~klU)a!v>eqlhuo&B{Z*O<74q~gdVu)U`VxA%@cbtS)l<226T)bIV zFt_m89bhIv)|vRZQUllzkjiFJ_ruLSjSv|NiY_uHO1~P}dImo4t;=0zW`am^*qNB66fyZuWq9P zm0;TsEgr>m43KV3lHH?|5PzFz7)y)?AwS#x*-?{FM+%>sPepd`bJesg47JdVe^z+i=Hs<3S)SSapr0w&_UGgngyD#7sn>4x<;=hOEIcCcNk8`$f5T=$X zWQ>h@QQq%?lwjCg0U_q$jec)z)-NJuuOxyXjraVT$EOhQIYo_nGBWnB-!G5ppiyFX zeKnz{wnX=OJze=yZAz?k6LtiyDf*n5EpHT=3vo_nHR3@WTrXYLPr@Bb_(KSO|L?pG` z|A`v5crKl4Zj{yvslvsBG}HRJ2IjKUSxeL21-sshMrr|2{VCs^mZ#5Q@fjKmDjKdJ z^2(|EV~(1L*;ygx@F2*s(T`WRpa1E1dwJKwExyt`yu2fK@s1N=V6hl`E+vfokG7w; zh}Vl{?f;4VK}7!<{=@%r(W69mJj=_VsP!x!9K1e)z8k!_T0N-s_pg=qU*2mcul8NK zOch;=yhPR`+p?1{v?UJi)-Zc`@r?9~nrJ^i+-n&n}RvFxxJ`fQFlvb79 zb=ye7=o_l_7Zt0lS#R{>xw^R)RE@oU%ceWMGp#xh!@f2JXOrEM$g_}swX)kkP#YPtrwdeS-ib+Kc6zaqDKHF`?%*oc6t#Dp%jp^zd zh-a;WtH>~~j(ZzLfmo;TfI2&iYUkjuuWBilsR)BH@ji+myoko@#Wv-YYWcNn|4xU| zBzZ_hU0u3#N5^jWc@!u8cUKfiC+hyWfT0o7D!Um-qH;u!e9vY`*MwmDAUwkZ?8+G8 z(BBv8i33iobk&)CEPP24!ag%G;MhIqZaU*rpEq|-;A^uHVtib12p@HGJP7qd)L$2s zYh70Q=Qa%th>U@}1?gjkhm}oo%EDe;Tqk7d92AZmDtjP#Vm}Mz9VW&O}00SF;G)Wo_G?i6h*#dO4ywV~efIVt|14>>{D>TA>dAf14Je>-9*5^e+3PUIp~X zmGAsXLYA(sl2KnGX5tR$E!E%cVT7;Cr$8~Y^LuO-YN3uX!B#F7>UyfZy980A)FYnt zmYU!Ed`zeF-ImK6whrab7tY=#ZuEvH8(sIZZ=Ein>@^|+=F0r?p$FY~Tft|rcGjF( zApj|>$2ixoRR5aCWavL0Z{(G~T^&_s$g^AVliZVfMqB}h4y+F&u zW6XtK=V;KzF>7_5=28AK=^4J49Lqb@KYm;N8-zbiTM2AxZ90+Fd5K@W4&NRQ8j<1i z@G3|x%jS&$2-lx9hs0o#Ty7jpCR*+uuJHD{w}T5!H(PRVXFf@2i_q80%xs#3nL+bW zEp9EqMdw)dtK0JpTi=VdqZ|kA>?4(CW8N+K?7qpF86`?%V^`D5u6Uf$IzRH-7wdb? z8y022>Rm8qZii{M>52HtcCwPWw{k;!U9>pwI;gE7SWaYAj&SKK3w(C66M09e>@VOW#r8V#xZ3wI?2LU2Y$!fU-(TmQo6To4yjTb8~S3q9(to#Ed<-uU3+ zvj0(ef49jSM|OmpYgA)d>LNrZG-3fI(I2p%{ykAwNgqRL}SQ6r9eh50^U<0n~2 zt3(0|Ucixq&;5fKrYL<{<_!xZiFHO`@o^AHZsqWZ0mLM~!W~C6Qbi@Q@U|YC&u(Ta z6#U(A|FAQjJ#$yLs_NjoB;1ltX-XVh#VmX8k}SR ztLXF`{SXAUT##A1ch9*dc1>`i@h{}P(Fn+ENE%zlE;_nw_>sgs(=crr&_~)JJZQR> z7tHL!74v2cY%G|1ma=WG(WeyQuJORJ(r)b$y{!eJej1yZYjA67V`*xw%qm&0#PDE> z5Io73AI(`;%mOsYE$b12smlyW`}=2S2k1aJD^yMTx8GoEw$86aa)P7~s{b3(U?<_e2_r>g-I7N+0=hb+m{px%s3DU$5k__Yt7m4Qd%qldB3J)WpC zOMI8V)IRresd@4jEtu=BU3h-r*Yki^k6}PWJ$y5Idew0Bwyl^x7Xmsx-t37?3>7A) zlN}WzuUakiK2@I`ZXWh3no+}4Nhhg@oL(+Jk_H4ASNO3<>3@LFIE*=Vxr`PqPtsBaa~AoxjA=@1 zd3EAJc@q-d(lcmPq;#}CVZR@M{dJDe~)M}tRq1Wu&} z%sp=(hQ9x;lZrpFNVGW*orJXUP*=dUJ)8cMcR z2Yinp*-_}DI}{e1vBqB6{_gk|VdzzTGTTZ-%OfR_-Ro5%cLesYE*>I>5c5gJgDJq0Z>^3e3@xF-g!GVzPBUFoG6_i5^xb#P8u{johOXPL_{f z?^U`gyU{}XGwD#63PJ0`kLOUoF=2FcG#>0cL9~Yv02$ zi9(%6cyMynJ_(YETG|o-rf%0uiplTjy^;t*yr6DsdaSmR29f+^==#dt)MZ{zFXc z{r>>RCyp;3RV0zcU0<%yWl`Nq#uG@Lr)A8@fZC3lhGc=pPw82~aN?p;vr#i1k^Vga zxWrq*RR`S~)0?jLK}6i}_jLF5sjf+RSm`A%?Ahj}(|_Xxamf2BhOhmFJ|N`*{mgz^ zv$R`3Yjec^$zyz1eh7Ty<%0lJ@M8H^p7VW3PVF=dSoN+C)*(Jd<7&vV+{`0Sa{M|y zee|PovlTJf8`aB}9Og-7nsiVOsN72=umBm>iAWx;?cv8v3NdrHhJ+o%T|69R1S@KU z;uFIu)3@0awzueGTGH{QB& zD?Zg%e!Ee?{_ovrB>C-zFVyR>DE?)TFwiUMbzi@6iRh7#Zfh@>2Xcb$5U2Ao!pDSr8iP?>d*%xl4D5#a-v43AfQX=P$(SN?B#jGOc9Uo2nMM|8I?=U2T_ESe5 zHXIFTN>%3RV~^0cKQLa;RA zHI|3)UUQqo0I-tM^4mJ29l+*sxnB!OTT*PARPoa!0ZKu~OYn<_#ecHqTtfkO+SH~l zAZ_yov5@*(`d05$cC=JAk5_iY&fP5=_fY&0xrzk&1@xJKiW>opmjrdPKp7Jvn*V*fbRe z6`?)cGk8M|%g!>u!R$)hvj(5b^CG|ubPbxF+1i@fK>5{XY7KOLz0|e*Mps25eK~+} zu{F@YgFYWkA?;HviKQ2xv}athsM;{_L2uAx9(v}+S9 zOeGM=w0Bb&^O(b{$jS$zabe}Bq4MH|3OdCJ+PK${H!Gv0ZWR}`w}p-K+}4gHcyv$q zY;I_Bb91s0OR8$;h~4YP-sFh1h&Em6uo?HjP)yYLYqG5Y*n7Ypq}%uUlH?7ql_^Mo zq>B46H}!3rMhNqZZJe$$;ylDM!)n>-lkoOQq3lBUazyt*j}FReUQD;;kVajMlrdF3>)J`;MT?FpU#apr8=hWXOeEnZ@rYLq^5;khIlq~(n>Y<& zE2STVuNxpLZ4-nX7gLXywyej!?c_v0jM5+>njB-X6AqrDTL9!X+ z@){8iSN)bfJ0^q8g&0@tGr_y;lwjWHsYS95*O6uB;1{-noMkX)6a&b`mf7?ghb1NH zV&$p(1THh-#)*qZS?25|u?A*i0eW6tk%#D0ditj$vM0`CUpYKhyJ>o!?ympku&$K{ zkV1pQi5Qi{oT~DkI*xmRl1T(E(!MwwVs1SB z+v+KP-igr(`Txc0UYGyW>c2&67@$~vylJwiURgtC1~(~7P^bbBlop(&- zsm+yF;}!0)ll8B5rNXvntrT!fj5RWLrp9$nt>;cThxN5!UVo)I9uve` zcL>GX(Vdh)v;giCBU_b*knJ>*7h9)~OuSjPnp|oDDm2Ss zFJk;9gg5vW>mt##fI@8ArQGGXv1z@-iU@hB-TL0vT3o;X)FZP$Bzk$v^-i_f5gFRu zG-WOQqRMSG&j9~e5IfB0tM8-hb4iHLlRH4;+;SH;Zk_r<4{l(A<#*sW)ZLdxUU`3fRJ2V=%Y5kG zu~E5zWX(^;@5d3=#Fzq}F=$5D2Y8>+;CFjy)m>duoGDxe(w{P}3mm zF+Q5+n*|*3&RS-rCOeLj>h*Qt+Oc0O$ToGU*?qOm?n@ko_+!!6nxoPs%vGgU zjEq!x%dV86Q^?85$_~`ehcht*6lTJ6huQ61Ojox_LXbVy)l1zu9s1svE|l)z{fyC* zf#%x~!_5!ryFFzb31H~LJ0YGMy3Y^+y}fsXVu4)D-a^$=enYga0^2t$4y@valm zBF?H9Da-`mNuLvaZQnP6Rk2TvY@HVhy;*|t^(VvKOk>2KyjP3T?IA>Y3j$%P1KzJwR|pGoSxU0R+1X%qD(TCl~Ez`bMjbdweKAbpmQl?-p)G7R6)rZM$WC zeG0R4!hJzbv=oep%Vm?#fTwa{#;A8!wshCCeH2j!kfT_fEjmO5I=iH~lMG%SRE$$BVCSKZBcEE1`YB2T` zlvKD@vd$^<4p3WibX?j=8bPaJpndG=J(CXV4IRx=c7@CZe2$9xe0+UTf2t`zxh5_@ zp4A3s=jyY}lTR)zK<_ay%gTV)_E>HUAucc1LQW{c}c;9gxxhstq+La zS}oOI^#ZQlwljUM*>%|^_%0Zr@OVM#Q+2Ee96AMA*yr+x0PxJ0Q-1>{Q z5~BkyN*hn552eHf#0|DAd;(McBF^QN=eh=%_lkgaD!;h*#B+t|PN@ABlehU_UBj~O zNke#bJamN@-!irK)RW>K^%9=>H8QddC?GJoi1BP8%vOH)YLPa|XcBHmMv5G=EqYx~ z@1gQQk>g{+O=^xA1Wb~AH<xdxRfD-EnRw(-2?ROADMY9n^yDcPRS>>tCjtYVA zX~EWiXhB8dL{olQ^!re4O7SVLgd^lh(szWkh|pjB>P}x=U$MbV>dKp1FK^jr0BdsX z7x|llZ@1nL{|5sk(S$U|EprM?V${~!+IlO>cT74=q@>1SCRCCVR9h;vkw=K)^6Khu zv%-)uA(#}RzT@%K@VR~4s=J)pjiR@=8HOl3WfUxSY~1_w;^JTl^QQN|=TsT<&B zv-6GPui&Ht{hO?EVp=-Qtd62-%UlbHdw#Y%SMPq{zE0<}8lk3(rp~y_K0IC9o>#`s z4gw~eu`ZWpP;9T1{jK$Kzx5!z-vM=WWd2R=Tcl*Cl5zy3#(nJq{Ue9!uh#W3sRuK^ zly4tJ#^4O9s|J~~aTRWD0SceaG!&90H)4q%YWA+~CL#e!BWh)K~L?P4hfUyrisdTUX z++GQH5pNtC?oX!DrC|Vg9jf!h=#q8{%m@x%XFSJjF#?KDjIeoq;uM>?J&_3Ca;;P{?SnvOI{SlEoF6CgMI28Y!+S^a z`!<6!7B)5(3@3sS@<9Rh!i8yW9Rw`aXs7;yjtFwH0@E=r$>~5;BGZgqA*DqQsP;NT z9y|zC<%_|41X2&R(G&~Bdd>^D_5fR{fS7#Ngv8@0K?6P`ZNUfM%cD9v2{kDF$;o%+ zizqa{{k4@_Ktjt-%CMzDy*E=IxvwoTu?REwXaKgCR`kIt-3HzkRSmTRH=o+3`0iV~ z>Ycc5TFn`#d4i6oenQV8%|^i6qxpn?ismOB`SJPOYq?T~s3V0Ebm!d8_wy&o%#XX{ z2L7uv$Ib!&&YALJ<^R|j`=bAgGsIb|o;!tnvWy-x%Bjq=1^H?~4;><>+}q8$>9*_a zs?~#4o0baejOztVE257}y3h;%M&5cSq8~LsX92}Ezrp%JwYjLRnw|UVX}i^7v8~IR zxY#%gx!g$*RyO1k(%={rM#j73wlAs_YE2SUp3qL}smb&>ddHGs0@=vw(x*cgb{-_i zC22$i>@wGQ|L5d)Y+7ZEy2%GIsB899FF$raK~2DkK_8TsQKx@S+Jw_W3l^oRq94&Y zQ5(^iVUHu|yYWQ=gWy|5*LrF<96NcNRi)~zyp*~Xb2`mm;2Y#rB)krROM%}5JnKWo zr@IdDr~;EI^WOqau9cswTs$0LU8!Rm1dUFs zs)lp;)<>?TRgU8J5n%&ORq$1TmLUOqUDPk`aZ%Igi%0!egow-{(5RC59>9x3S(8pYu8z zKP7RFUWtoa*+e=)Ct>Fp2d#bwUB#eaHVQ>k_b4dzti zO&kR!s*hw^U~j&~7IlqJc+s96cJf_Kb%}bSZiW*r7b0Z17(pJu6W-xb-Ch^iQ=nbC z!W8t6h=2zl_!vkUzAY*V6N9oVZ7q^_u717okPefSr%dZONbjlKVmugAKE~C6tb$!{ z-{)-A-#OklTWXx-+kER>9^_h8Rar?CH(BHE*#6V@Ol_N7rq@@3?dT`R3A|gHd(%*; zqkw`|Yyz-DSU7zp;VwHLevc`_q3!x1$8yByrq;R;hg&HS7; zg;Li#OMGQv@0J#FNeug!5hr$F*XIeq8e^CamY|G!gpjm0>l1z~QI9Zd3V2eG79K!~ z>wk-7I=V-;NdijEs*aYH>}O@1HQI849}NNL9s3XDH4*{5=Sl;DdqDA3eMS{IMn<`c zN6HI^F-FGce`GJ9T;|YZ=RSIINc(YMFzEcx;B&U1b;pTlSudCD;NkHWB{`LleEQY3 z+S)DR&E34xx5V82^oGpx#FQMY5};B36o`~q(2aV%x~7U|-gI}RL&ha4@Ad4?l%-F* zx;kyq3|kqCPF|7T)O2~G9-l-VQy=M^=k;+q`1+`+-4~1y8DptCk5F69B67-CSLaST zDVC}+0dMD}dQMmOZ6EpN@!M*L%YD)qm<7JMoCmk&INRS?^Cm!+SL0hTZ3KD9S$Eu? z%jcKGXSELqekwGm-O5(Sw797E3f3?GBGi~D)MzFI*RFhY?ApjDX5n=hn0185)Ef`s z_g*a)1<3<$9dh0M9_YPr`TR}fl9MT@Zv%v-ve|3ToPN9O;Qu|Cr>$iYI z6tY%j9dP5~RBXh*o!hQBEznrf!M>{?q5!1rP{i!W?S#C+`_iwe_#$#=sC?w1C1UuH zd-mI&4)maxABr&Yx{wmW`;ws(pT@>LlL97kB}`CjMivH`_OO#MoID^)R5#+31E+hU>NFqAq#F__-;#9=HJI_| zK^1yMdP3T*?e`AMDL;s89!T$^Fm~?wYu+oU{?I2dQuN~PW)r`+>z2Xz8?qw`bJ10A z$^@P6q3kDrY1tM>ri3G27~PR+|FxmwWh>^r4f=6b&Upyn-bgrn;%$i-M%VHS_o|Kb z7=D|pzYC_QXe6;KM_AStxdIhlLU4M+Xtm!Jg1=!HYMVvHn!_cZ&ZQD^9UN;ia<3W@ zjf%Vkw9L14b0D&|u(Wm@vc3a20}w3_g)!IG?J!k(*e|+UlI3o*uRmc}jDL4__b-&C zf9|K(s~)*YB#>q9<6$~+sqG2!6HVF~!OdNg@E6+CY^W@9V7%bsA}Mvs$CJ#dW5Q4CUOMuTJUFNd{MpX?>*c9nhj=ZP=U2>lV(T!n#ivx=E7%6`ZFw9O z`uu$Q0R^KMl4ryGihatic+8cdTca(p?)5xhdrhq!9j*6nrl9V+Q5v+uUTjm8ll>#s zXYF`XyWfha2EkVvRkm|aU8mLpEHr(-b;r3_S^bri+f$O;+DZ$wwBFf212y>fL4>?X z)Foq7dpo@%2Y8}}fIR&P$9~ieeFN8aZ6RHt6z(v<>Fiz+fiwEXE|#WaJ_jq+(O6lw zg@LX@jdR%wd70i+hDAq%)=;ok#u`oY-0QF-xi^`=doXUOew?ik7`e21u=tiQK_-S-59*5OG|yM_9Kib|JrM44_gb+)|V z6g4@mmeLMUETUrm|$W$~GhZ%>Y8N8AvZrA-u7yUDf3 zsVZal7HySHrI+z(43i4yvTlL7IOCva0Q1;;dcJ=WmYvfgzru?#)=n-u>V@&ui!yH* z)z=dD7q%B6G>b!Di*&A<_n?s)2Y8(Qk!K?hO@{z_R&^?k@f6wSdUf#Gx{EjrKp*dw zA(H&9_3&f6LZi;d2lce8*Vo_2_TgbfV?&IXc58{<+yr4NybYhPTk#Wx@Yp71D?;g} zF+KOr#zJfTz4q#U8a?YJx>^KM82PI{*exOc<3{5OBwQx5z<%Q?k?vE$?OAx->v|4M8wx1!~W(VU*4o+Bo zFR~51f#}r0bNGssea?+5Wy(_(_zkx zG9BZdfGPaJP?9QXc71M1}*GhC6 z1nwEWXYN)Wky(kMYeg_tknD5hW>1mSb2>VDs*6!q=X?=$*i2oflZH-g?c(cIvLkHi<`yvYZzO1jX=`vTTu@pcIcRpZIiy6dSACxh!>xL&Brh*Yjn8sqw0tcDF)*VhPUA4*Jv;ly=6KY|NL1c1VM1|Li__!^ z8KBly`*clQ$Uy>TQfiYFuQJ}wl^}hYN0f{7Td9Ed1K-iBWi&new!eCJeVHIbi zwv8v?dU{dQ@SX6e$eLkJd2 zLfL#lOR@JPB&z+n;?B__bP?yu)^u{s_a^(Jv!xlGW>sZ$Fgj32*VE3ng2M1fM&^i; zprkT7Vq+sB7|GK7D2!&7b6ii6g-cShU@d+ak}Ihq`iFtCTGd=M*JhcKqodKIpaS21 zp-9zC9j(Emw1x=>no|9nMCM3oa}{nl`{}z}f$IE89pXxY%vD9V@V1q%p0-KE{M^k6 zSo;epd%H_}SEu$SVRm`h3U#s)P~ddNulHL%$zr3m)>MU3Z6F~QVjZMs``?_2$*m!& zO9+AuFN^4Tcb^Tm!iofMKJJafO*PM1t>+f>cwg8*JKF%Q=ZIn$4_(U&t}l%be?U~v z?cKbGGe8N*k7FJ32ALRn3;yUzU<{}f9XWJNG@5?%Ppfs%UplGP<@=3^kvY%^NG=@$ zNN*f&r6SjSIBv zqwr_tmwLOFP;>GtT91%hqb9JC+3hRt9sHOQ_zf>bRc?yhJMy`wjepF${7Bi7Q9AJL z+i6f_WTBa*_PG;;O~ju>#-0-#_@ zkKddOSd{nuUTb*4B1deTpG7unIj-V)$jaoKo>t5LJ{T*Y$YA>QEY{cI98x@X>QWKp0LT(As;V z_QXledVHsXnPj>~2e+j4^XJym(02-BE&KaoYVVy-&Lyz-Hm#T$5(zTXM;+xh`-amS zE5ESh=1tjZV4h6G4GhHn)iOG34w3p99~xp+_L1(%gDX zVuQ@$?RGjIbb6EFP?8JO1Pn4jlnc{?$vc>kZ+NuoZJaq*wnnEKK+Wo>R|XeXQHOy^ z!mhhs?h@atJRQ8e92DL=pTTc^u-Ktt65^Hlm6viltbga_uI^?TH3+1DOI;bRj*wN3 zaxs_c3pq7~rFMM0a%_~P%aog0?;AJDJe)7IaCIIx{T45}z)KiI&rM{xm*k31jtA=K zE}1EDV2i13qusH(2m%e`g1ygD*jMrbs%;ZSU97(`{fzS3%UN|$gm5YE(^+Y>sq8CF z_m?6rtkpLYGqU`*)zGFHg3|7=q!4mZOd52-6iHp$BptNbX}y;62LTwwG+kdzY__#u z5_r5eU;jh!P})4t;0@=Sj?u?uAIK)fL@HDs8t(g;Zv-txsq_1!IW_kt3VLP@oA-o; z^;FK)c&Vym$jPDG8=|r=IGS~Z*4EhL8HDwmu5UNe3Lc+(KS*7g@i_C0W%9%pW#S? zovhd;NVc<8UnV3FmG$v!+L-7{WVLp82jvyT&ndHE#|yY#?%m-9$2*a+$!4& z7?Y;wfM%p)^iW3q!P%w{mB9p7E_)p2Qd)R8ySqF8haSO(wZStZW9BLv1)sIdgAi`j zLK0iHvFEd{JCYGIlKnI2FAOBh(uE*iU7?esqr1Ew z5pLbqam?!EE;}{%?AcdfTnshD0e}=Ev}297fc~O0k0C zqt%>fp{?8e`yq{_P61;wHw{x?7HbYkp_oqny^}89pr0iO4&-!9p!5~v9sll1sIORJ z6Y!mUX)XR_naRFm-jDgp<`hI=Xi$?AMCjgIV^dgui}O$KI$GU`NlQcjQ}6C|F?SC! zDj#fQ$=}-gUwGz9hTL=N{)nqqMAz%Nmbby4qZtI{7y4Y$kb7Xm*vK1OgDoYo=_82K zMK;b~2(@e1@2}Sa-gryuBwOn?B<+q&-s86%OAeqsL)uhjb zc2_64Q|-_2yCuIy$QAWf6T^3*R@@tr z;nJ&PyG?o{mvrHD6eo7#EyRq4>hHQ$FYOD;--JBcXwzF+mY?fMIU*!oNsr7_$Z(r6puECPj zRWvk=5J|B1WWmU#rO3b2FYzUJdSNa-)%U`-U+I_5=~N%JQn7NT!Q+u(l)&m>iITb4 zIWZA=>P`(S=!Pv<<4@QOQWHmY7tQ!V^Dfk0s=eJ@2oB`e(k>{D%{^bzGXLBQ8Bz*Z z{o3`OFukHVGk_83Ab2s1#o180mrb8g=l6E5K4;^}&~ugRE6wGx07AWjm~+Ch>l2I> zEu7T+&$AkSrMC#(^>yJM!XOdyvUBxka5P%$?Rn8QdzI+9q$~{UFLe?s+n==V>U+@qE3@ zKP$_h&Tl7jOQUf5+&|lyO4eimkej<0w6tow9(>T+eIP}YSq`HV-Q8nGciv^Q1ciOf+xE7 zzGdI{bHDHV;XMvtNMc>rTG#bk|8<`K^UU!#3K+It6w2O&-TzT&Hw*_+yB+D;7{mx$ zb|7k2)Mnumsck12ny+rUA8jbDeZon_uzo-p^m+s08~ydl+Q9; zV~jKFNM{9i&yt~#3Wevs4{wn<*;`1oqk_Z7YX?7;KCtOb5)12M&s+AWeFckgwO9~* z7X9O(mksZJ2R`%+yP4O}#R{@nAk1#O(@pVp#EdMrv>bOD&#~tJFfu{xAAr+%0T3BC zJsC41N+_kb{jwPdg_f!uysS>@3qTs$u{e2Fa%f7=-x%0ESOR?}cDn9q4e~j{kf^(a_LK7;leA(UdNUihfCS3d>kO zJHZlz4Q)PGqYf^e0TROGKg$0%cS#3C)T%539`2cl+7A{&Fqlw4z*;g$%7VI}C=69p zj_Neq(#S220(x6Tn^?XLAtw*v?U{jbCovk+tcMA``gwT;l7} zxtp&GwcG=L>OaoM9$pg)9l9`0h^8H$o4LT^x8LMnF}&8jR`TcC>+ip2*)LWmlz&q_N0An($Y>KYn$~*rEzU^acoV-zP^Ui$=9WKjDmeV7L4i=nNYTO$%azQ zI(Gt?9UScK$HG@xqN7b-d%--mHWF9^6+1f>FwX)Gw7mA2-`2&;tMpCQjI7LshO&d$^;GU<&c4+mMESCcLgCMq|3+#hgjAJ*7L=s^`_?ir^itQ?i%KDTBp3`G@{ z)u2!lWA=X2S@1n)ShJ;NbGts{jcv$n!vg5iaaBe*O<1QCZjbkH5rbEIz^hJ6XVFtI z`tTu_NkktGyBfqUl^8xXCMm47p8h3h-nGuY#^V@sZ{2A>dXbxYG=^`TA~-^mre{ka zdJ4TYU!c93>~DPMH-=qR)!eBIcq6tjw`qV5f28{KL#L#0*6yOQfnOf`bu zm*9!(0`?B`_;8DWG1ac)5m0eJf=9rVZI?!q0{r}!ne(XQF6wLr+_OX~Q*v<6!Gi(y zA%0nwCaY1TxclLodE8X=Vr;faLkjgIam=VfnwchaHY`I8ZYL=+051xzeu(^v*+FcZ16VGuwKlqc?wA&vOn1o@$8KmDP zBUL5kUAW{_rRQOIwN0Z`pjG#_PfN)Y@K3=ig3cD;6hYG$_5LHSd+TRh_efS{+tLQ9 zL?Yu}rkHB-Cja`kSbSZ7t}*Q>+j>9xJK6m+K}t_Swx(j_^(4~${A*ck8as9+!t0aP?rYV`v)c^)?_?4GrNHaE)3@)U*X}*Awf9;q)78_} zO=RrasCHR0&3dlKYmCxr(=x6|{cXnJNCnqJ=laRoe7(xJ#{48nvaVomPERALHAN3$ zXRFt-W_x7{^6jusFeegY9nX6F+-@=Ehw{@3Om{9 zkS!TKP-gmK1{5hWdJWoYAduR#@K4nvc!nGJ$Jvoon7J@TxY(>J3lG^wDxXC@vb-EA ztke!#^$892xjoQ&VcY4mAlUZpRq)jkSJsKg)ZmP?vHOJGK_dac4mtS_4>+d=J!Bg4 z=#imQZr{>U-(|H%|J@3LS{4o=Z!lI{@me60p0=0$aTB6 zRm@(ounoDcwp9`-J)zD5&dd1|n(y-7H#NN{g44(rT&}>xcwYuR(%Jc)mV?z^P`P^>GTkx~DYz!-xef3fnimZ$$HPsW&rdxO=LMcHys`MRi5Fzd1`ug8^^5($K;n!J<~eQfp4Rhb@~ z%y+(D+=(YV-T>~{q5nt_L}%4KmV_5p_1Q$Nx0U~5IM32KN&Fwg?+I_{}z{x9HMkyTl~ggujp9-aK0vE65`|Cg^#di?;}wXC|q zvn0u_mkgzRkEh~BT2hJr%K}z!Rw2)Ud=c23cI^IN z1miS6gF`CSS4Gj*kUji?UHue{6lK4VH74erXUMEjxt5p*%B9-7)kJev@AV5m!xXXT z3UvxcM^ZR?63eWJa6Akr!jbS=^=!IvI0nhfXCo$2^XL3qsQsr^lU^<_@9z&bXS>&+ z(bgd&lX{-H+{Dan9vVHY2JPr+TyAf-1(@whxal#y_h5Avj+q?I3aO6=)nNYKWYvec z|GCNROMY%Loa@RzHW|`+6vX`Ot`GYS1tWVLXMb6tpj~@n&tRiFZy~%F-)j}LzCFvX zFN%JkrI!tr^drbw{M!;0H=yv7GHOPjO3Cb*(pW~p@^?h>7?hBZl$76(?vpzm^g?s9 zr5wirb2WFkydFEn#?1_@vnN>@^GDvg0{ne?g#B!r9KY|G$kH?C|H9OQ4U3C?AHPqiqiRU~BpR1_sws z21zLVZC(NrnedV6zI*YGfZ+CPRW_E^mg4r^KGV%(yjFI;on3(nBy*yvyQQ`Df?$}> zZI%|UfpW!*qvF&KX}yV@yEq^0-$;b_l$Iu!`x7d$jD3y3AqroHma3!)ZERa|131aTl)lqeX}-PCrP zKnOT@Xs0mfJb{~9hr4aD5|TYWHfzD=TbXR+m^VDVGdP7{&E}xLe$Oz~#zD$P#|~j^ zuO|uV{GQmcveF?*RU@=)%l8)+bH=JYtQy-~OMDb|wwAxg5+D6J+k-!P@p3UT7F(%! z?%kAtn6nE6uRot*0`=6otl^kbW*>s{pF`G`P;XT8@~1`*4(n1@lR6U&;ZaXhY+N|P zDcF15B_Zes!x1kFN|OEk^<0|&_hADP^F~>!xrxRe$HpTLMvi{oSy3K4(WV7qrCtGl z$M*6-%o@$Y{LZr&_G)_e3+o?wRoxc36Cxxk>h=i;4FqCe3~y>X@MQlsupQx85K{e6 z_V4q{;+XqvoFEsGUih%(?-P9Da?}IG??+8ro}OV^$K2uyW%)tnvA|0ms(<+afR;bW_pijv*O#&;KTQC|)tBF$F@T6X29y-Z zQ~CKfvWIHVvF)eojP%cevWI7|xytasgCbb*q6AsfnAXIZla+isJJ&j-!c6ls4=I{% z$F|B}@6cqRt*p^ChVpcgT1ew67-}?)p)*f>6MRX+pqqtXUsn1`e@*dG`+Q??e6h*# zad93aiFEz_PZ$!P<{!ciEN##EnTCK#M{>@8hG=QssNNJAJ6J3!CQY}Y*cr5`3=UYl zt=56VP619IXhSrs86{LAwH?k~`h@)Y`_qIF&d3aUn!6X*CG^=RF13cxa0VPvWkS*w z`6EBUe;14~zZ4F7L`K_MPQ&-6u=%m+^-FKQJ{DwDvy>2mEbw^vU~qIBuQ$4q#deY* z8&p1VReC2-D{1XEuwL*#qOH93FWLW#S7Du4>%dT8X6xqSA~Kj=g?*)6*yZV)b3l&E z3pWJm>(4iRP3)TwX-Ty)_3)VW$mTU#=n9;3HSrtu^YJ}0hSu)sgPv(yce=Vx);d+V zbb3~i<36*O#C2b`avAo%;W{NCiGRqP-}h>*%ISWNi%#8POWhHEsyav|TN@qlv>Ppb z&AXZ$H|`!T-Z4pSsB@%l_uC*Z@9VMV>t2>=iL&yt5ZIrw{tX}m zfe7V^@Te(e*xI(ONDLZRYPe#Ez~$g0n+k;+if^f&##iqNx-MOI6k#eN-|5(c*?1o| zhmBLRKTj7B72b7en(P-?oadcD#I%}lITDOE;mv@%{SPL!u0lYco z&z3D_#$syVIL(`^7`Kb=fVX4vcgFEIxk~u=>(g_V?Tgf`{Y*@ZYH*ZH5+c1hGL9RP z-?Tj_1}nG)bPX(pC2$)Xt#jR=t7g1JW$FZb*A`)JZEM>~0EV{=20+qpd?U0vCB|$v z-b!SgPC|(BdET8gXkqFx2-k3yoG*pm<%<>QAsk$s=LC5ltX?fbN?X@e4z5Me!LJ+> zd>74y9dvTO^<=w#|q3x2^qU2Xsbk5I%6kNKs zqGJJl-TJcNRK4-J`2`R6keV?qzH$8c3S_r_no3$n38?nOEF{Eiwideiqp#lY$P?Hi zo84fRivqnb?iYl4Rw8yZZ@KT)FhD$gedBK`sQTSK&0Q@_J4?d5Qz^S{FDQkNm%e4$ z8$s_cBz1YVu`LaWFL(?&`+S_cpL4|KSrzKJFs#I>v@|wg73499l|5`Kbbx+-<+{1J zA#o4pR3OgoCz3Ug9_oACuaXK7fH>ba>y|j#&ft|7+)#HQww-V6JSY#9 zL183`5eod`w*q)XFU|2p)>eM%gvAQ_=LM-6|6_kN)bY0GQJAPv&fnJ~8k!gsD#+sVH*1(sk(iEW%&H|?-0d5@@lW^kKWZ& z#ZbxU*g-%~5HJ{Gwze9#zcuA>xq2z_o-@2E}Uj8zK3f{+MP*Et#Lcubv3!4&aQ3ug%8bo{Phu+SKiBOM?C2^;W0ww5FzsuXaO@@3& zJ=4#noz|A96F`Ahb)Z~aR+OS?YK2YQkXvX>3%3;z%N5vjkGx5~XA*Z4OeZB6r-=Ya6 z;4kvN`Z7(ZDV%%x>#{ZMK)nFEnaeIgzcXI%N4+#Mdm#Am7>`hj7xzv{soxf#M93($ z-|;>!9~19H&G9TnHX?4u)2mmvEbW!2iru&opK@yknX`bgjFMJF&h*@8=ANuuyFgb~ zSTTKqgW8rf71{WeT6U%*cOlE=!raPwZ|r-v*f4$sq zw00|JJcu5bBlLuCAbS!3$+-Tans|9C$0fJvUqM7J5)MS6`@Y8?_a?xUg!zRU%5!6U z69+|$wp%HW@hdOs>+@`&Yp5k2mVXBR2fFP~<=f{8gI!tjfU zf7FNt3vk}5Adz_96#iQ*3ThPe)mkKFWA4mkb1LSYED9l@v)JW1|7sfla}G=t?K-6P zs~kUCIvH1IKicR5MG`J+@I)40@91U7tUHUS{sVgj7yK7a?F9Jv7edPpsn>z*KOE0K z|K+j(5z40^<`@b^iUO|PTwMD1mx+!7XpSRntpY}JjQad`+dUj6aov&Y8z+M0-rql1*;9|nnesDb5mDTlW3CPH}y zP+nXDS)v>U#m_ns6>hfG7vvM%YEPtdYf4?=f-YjiL&XcTxylh3FtF}~tX;EGSL6r` zx>vcLCss9@84>BQ4^eJW#yK9!Du7q!R_cy&d4Yt41tq&?rhFBd90wIdh6{V^9+S+I z^_HUF^Rt_NIH(|-yL2-YT3K%FDZw)=Y2)w7tK;$_z`Uy5!S7Z2+ira&Q3Xl|Qe|%M#nk|7l zR-K}oD3Mms@K17k1aO&fyl)GzpDoBrRE#&tEn;8|ECUe~6qA;2bJgf->*DYY zb!kimi0dMu<5XO>K%%;(#Lo^3%NPhPXo3#JT!9I6S=_Sai~H2hGJfou4|x>bf&C91 z%4v9>9Jj_*tWQ-=YW|$XtCjW1!cQz&v8(!?UWba00vo&*hCsSy+VuYSj30f~o{JX> zyU?DwjRl2C{n^yD1?7w^VenZomjdLdr%LM>|2wVGftPCUJgFO7L%Y2dpExb z6_-X<-W8~}Ku_e-7nbyiE!%r^Sm?M!Y}^AlXL!Z=C;qGr@WC<%`8TT%?wb0h-{uQz zzRhLjyLb4Uq;!Mf}f~<;_$WZ2UlGY6@n}h z^N2n>YN+9pI{pR!DqQDDl0=5R*HL3vw6 zlXO&oYpIVILJv@~RZfF->|&Mmu3ZN1+z;#Y50|4^ih{;#nEW7~m686ysw$D+tJS_Q zFPM0#h%SCYslDFwAOFxT`bXuQHzL%;|tBDUMRv~_`cof{!_+t4$NG5oEb3swv zjg$NMzywUvwz13`&&$*AI6!)4SH{Oohxtv0ofneo6zH@fhUhn_X(Kv``L|nzJkm}M z!HT}qB%xBvUrsxQZSfg=oW29AJ!^OPI~juBe^$c0R8*BDpd4uOq3*tNT?R?T8sv9> z!dcrcqRYyYBDik?xfz9S>}+B5-8DAMs&XrbdCZ*5D3Urod2y{tJY*9BhcDP zuXLSKq?o;AT&fTS%mDQNZGjI&MGX)axZQf{;`CArZ^Z3d!10O_zlMqK`)SvQ3u)J9 zTCOyA8aojd9iIm*HGS_%#<~_v<-t*znR)4^`jl$y!3ShW?|BX2xSl{hvGweYc&Bl}cT-AAVF$kms za0(%=bWNky)+H~kBZkS%wp{pBXF>wQjll>I_5X3V4usT)t&sERfB*mxL)7d|iJMB@ z<+&HW3v*HVy~Nzzwcc*RU|?A%nZ)8Y5qkZ6+kx!-j5ylqku~&6>648WOG)3wZV)ba(t8P?oST(0fuA`{bSFmYb9#~` znCJIvs|SU!Elj&@cUyjg2cwriD2-scK2c9m`O>Wz_Reb-vMzf7H}>0Aj79QTdnMH?##IMm=@h5O{N6P$Gv0W2CJ9HzbTGUZGY(@NN zw;8ifAnaqpaterMUZ{7?*xikCG@uJRv%6WuJU(Srdh&9#3pn@WtIu}?cCO?~_X@B~Y@GN`hC+L~!>g3m#^ic7|9_OVtL#D2H? z!|UxAYzHC?$aA8(_q#r?yUj;PyNO7SgUidt)f4lYHi3H zMMn5-@*OwRlZ9=&Mq^*^ZFJE6$gu@#KR~$yH}V?7)7yJLhNev19w=rn*l?I$WV#dR zlP~*+-P00Wcu@_8ePK~IBdLw{Uh?xXo0%_bV|^C>;rhmUCd-yx1`yv|q32Fyu=gc2 zc%fI^J&E@LQ6cI%>VC)ihQmwWW=&0(rtx@r0jxE63njbt7|iIujaA(l*!5oduK^?vq^$AuoE!fN>0N*LZc-BDPbPB6#CX z(RF$oify1d=HAFz&d-vYtjWDTpyIur0fl(c;F<2OH=JNa5I|pEcmuS*Qj(+qC+y>1 z9P_^TJjn`k2q5X!$Zy5}=)~Y9e|iKspxW2J_>_{Pk}qlB zv22*hlP{Z-b!Z~bH{8($pNKFIKFA?+$>su&I8^h zsH3$Y2)sl00MyRTeH33K)VhInNk$N5XgV?4ge$}Urelt|8pHBlOvh(b>x{H zSD1}G+f+Jx3gyM3?yHN#N(UND#>-$GWXuQ@yaBBS&8d<`$LBus?~~VmPqE-{IUESGej=x1m`L=q^25*R4OYU*pYayqSnQu3qky$c978SQDP-{$CuiIwe%(b(o7g~XYDfaL{V_H!75Ltn1CZpRXp)#>SiLut*QTVTM|C7{nits>|cp)N&SvJ|&lGORg9KjDtvcqlytuf3 + + + + Without double-buffering + + + display shows partial update + + + + old frame + + + + ← tear + + + + new frame + + + With double-buffering + + + + Back buffer + (draw here) + + + + + + + + + swap + + + + Front buffer + (displayed) + + + complete + frame + diff --git a/Documentation/Rendering loop/Paint tiles.svg b/Documentation/Rendering loop/Paint tiles.svg new file mode 100644 index 0000000..e27d5f8 --- /dev/null +++ b/Documentation/Rendering loop/Paint tiles.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + one shape + + ~10 tiles per thread; threads steal pending + tiles — no fixed thread↔tile assignment + diff --git a/Documentation/Rendering loop/Painter's algorithm.svg b/Documentation/Rendering loop/Painter's algorithm.svg new file mode 100644 index 0000000..7727fc2 --- /dev/null +++ b/Documentation/Rendering loop/Painter's algorithm.svg @@ -0,0 +1,13 @@ + + + + + + Far (Z=500) — painted first + + + Medium (Z=300) — painted second + + + Near (Z=100) — painted last + diff --git a/Documentation/Rendering loop/Render pipeline.svg b/Documentation/Rendering loop/Render pipeline.svg new file mode 100644 index 0000000..927e357 --- /dev/null +++ b/Documentation/Rendering loop/Render pipeline.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + Shapes + + + Transform + + + Sort + + + Bin + + + Paint + + + Present + + + Screen + + + + + + + + + + + 3D vertices + world→screen + back-to-front + per tile + tile grid + own thread + diff --git a/Documentation/Rendering loop/index.org b/Documentation/Rendering loop/index.org new file mode 100644 index 0000000..d1b5e83 --- /dev/null +++ b/Documentation/Rendering loop/index.org @@ -0,0 +1,541 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Rendering Loop - Aukio 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 + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* Rendering loop +:PROPERTIES: +:CUSTOM_ID: rendering-loop +:ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +:END: + +The rendering loop is the heart of the engine, continuously generating +frames on a dedicated background thread. It orchestrates the entire +rendering pipeline from 3D world space to pixels on screen. + +** What is a render loop? +:PROPERTIES: +:CUSTOM_ID: what-is-a-render-loop +:END: + +A *render loop* is a continuous process that generates visual frames +from 3D data. Think of it like a movie camera: each "frame" captures +the current state of the 3D world and converts it into a 2D image that +can be displayed on screen. + +The process transforms shapes through multiple coordinate systems: + +#+INCLUDE: "Render pipeline.svg" export html + +Each step has a specific purpose: + +| Step | Input | Output | Purpose | +|-----------+-------+--------+---------| +| Shapes | 3D [[file:../index.org::#vertex][vertices]], [[file:../index.org::#mesh][meshes]] | Scene data | Objects waiting to be drawn | +| Transform | World coordinates | Screen coordinates | Convert 3D positions to where they appear on screen (see [[file:../index.org::#coordinate-system][coordinate system]]); cull shapes outside the view frustum. Parallel: heavy subtrees fork onto the worker pool | +| Sort | Unordered shapes | Ordered by depth | Ensure correct visibility (far objects painted first). Parallel merge sort for large scenes | +| Bin | Sorted shapes | Per-tile shape lists | Each paint tile iterates only shapes that can touch it. Parallel over the worker pool | +| Paint | Per-tile shape lists | Pixels in buffer | Tiles split the screen into independent work units so clearing and rasterization run in parallel across CPU cores | +| Present | Pixel buffer | Screen image | Hand the completed frame to a dedicated thread that copies it to the display | + +This pipeline runs repeatedly, targeting 60 frames per second by +default. Even if nothing moves, the loop continues running—but the +engine [[#frame-listeners][skips unnecessary work]] when the scene is static. + +The steps above describe one frame *logically*, in the order data flows +through it. In execution the engine is a software pipeline: transform +of the next frame already runs while the previous frame is still being +painted, and presentation happens on its own thread. See +[[#software-pipeline][Software pipeline]]. + +** Main loop structure +:PROPERTIES: +:CUSTOM_ID: main-loop-structure +:END: + +The engine runs two dedicated daemon threads: + +- =e3d-render= — produces frames. It runs continuously: + +#+BEGIN_SRC java +while (renderThreadRunning) { + ensureThatViewIsUpToDate(); // Produce one frame (or skip) + maintainTargetFps(); // Sleep if ahead of schedule +} +#+END_SRC + +- =e3d-present= — presents frames. It takes completed frames from a + mailbox and performs all display-path work (the multi-megabyte + =drawImage=, =BufferStrategy.show()= and the X server round-trip), so + the render thread never blocks on the display. + +Both threads are daemons, so they stop automatically when the JVM +exits. You can stop them explicitly with [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#stop()][ViewPanel.stop()]]. + +** Frame rate control +:PROPERTIES: +:CUSTOM_ID: frame-rate-control +:END: + +The engine supports two modes: + +- *Target FPS mode*: Set with [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#setFrameRate(int)][setFrameRate(int)]]. + The engine tries to maintain the target rate by sleeping between frames. + + - *When rendering is slower than target*: No sleeping occurs. The engine + runs at maximum hardware speed. Missed frames are skipped, not + rendered later — the timing simply resets to current time. + + - *When rendering is faster than target*: The thread sleeps to limit FPS + to the target rate, avoiding unnecessary CPU usage. + + For example, with a 60 FPS target: + - If a complex scene takes 30ms per frame, you get ~33 FPS (hardware limit) + - If the scene later simplifies to 10ms per frame, you get exactly + 60 FPS (throttled by sleeping) + +- *Unlimited mode*: Set =setFrameRate(0)= or negative. No sleeping — + renders as fast as possible, and frames are produced even when the + scene reports no changes, so the measured rate reflects maximum + achievable throughput. Useful for benchmarking. + +*Production vs presentation.* These are measured separately: + +- *Production rate* (=getMeasuredFPS()=) counts frames the pipeline + completes per second. This is the benchmark number. +- *Presentation rate* is how fast frames actually reach the screen. In + capped-FPS mode the present thread is paced to 60 blits per second + (override with =-Daukio3d.presentRate=N=); the cap exists because the + X server also dispatches input, and flooding it with blits causes + desktop-wide mouse/keyboard jitter. When production outruns + presentation, stale frames are dropped from the mailbox instead of + piling up latency. In unlimited (benchmark) mode presentation pacing + is disabled entirely, so it cannot throttle production. + +* Software pipeline +:PROPERTIES: +:CUSTOM_ID: software-pipeline +:END: + +The phases below are described per frame, but consecutive frames +*overlap*. The engine triple-buffers everything a frame writes: + +- 3 framebuffers (each with its own =RenderingContext=) +- 3 projection buffer slots (per-vertex screen state) +- 3 render aggregators (transform output, sort/bin state) + +A render pass P (one per frame, or one per eye in stereo) transforms +into slot P mod 3, so it only conflicts with the paint of pass P-3. +Before each transform the render thread *flushes* completed paint +passes (mouse hits, frame deposit) and blocks only if paint P-3 is +still running — which steady-state worker throughput prevents. Workers +finishing one pass's tiles flow straight into the next pass's queued +tiles with no idle gap. + +Completed frames go to a *presentation mailbox* that keeps only the +newest frame: if the display path is slower than production, stale +frames are dropped (and their buffers released) instead of +accumulating latency — swapchain "mailbox mode". + +A per-buffer *present gate* guarantees painting frame F+3 never +overwrites a buffer the present thread is still blitting frame F from. + +The goal of all this overlap is throughput: keep every CPU core busy, +all the time. No phase waits for another phase of the same frame when +it could already be working on the next one. The Developer Tools +thread-activity timeline shows it working — all 18 worker rows packed +solid with paint, bin and sort tasks from up to three frames at once, +while the render thread (top row) and present thread tick along above +them: + +#+attr_html: :class responsive-img +[[file:CPU scheduling.png]] + +The pipeline can be disabled with =-Daukio3d.pipeline=false=, restoring +strictly sequential phase order (each paint pass is awaited +immediately). This is a kill switch for benchmarking and regression +hunting. + +* Rendering phases +:PROPERTIES: +:CUSTOM_ID: rendering-phases +:END: + +Each frame goes through 6 phases. Phases 2–4 run inside an +asynchronous continuation on the shared worker pool, and phases of +consecutive frames overlap as described in [[#software-pipeline][Software pipeline]]. + +** Phase 1: Transform shapes +:PROPERTIES: +:CUSTOM_ID: phase-1-transform-shapes +:END: + +All shapes are transformed from world space to screen space: + +1. Build camera-relative transform (inverse of camera position/rotation) +2. Update the view frustum from camera state and viewport dimensions +3. Walk the scene tree: + - Cull composite shapes whose bounding box misses the frustum + - Apply camera transform + - Project 3D → 2D (perspective projection) + - Calculate depth for sorting + - Queue for rendering + +*What is coordinate transformation?* + +Every shape exists in "world space" — its own position in the 3D world. +To render it, we must convert to "screen space" — where it appears on +your monitor. This involves: + +- *Translation*: Move coordinates relative to camera position +- *Rotation*: Rotate coordinates based on camera orientation +- *Projection*: Convert 3D (x, y, z) to 2D (x, y) screen pixels + +Objects further away appear smaller (perspective). The [[file:../index.org::#coordinate-system][coordinate system]] +uses Y-down to match screen conventions, making projection straightforward. + +The transform is *parallel and non-blocking*: composites with enough +children fork their render lists into chunk tasks on the shared worker +pool (at any nesting level), and the render thread returns without +waiting. The chunk tasks are drained and merged on a worker thread +inside the paint continuation, while the render thread is already +walking the next pass. + +*Frustum culling* happens here: composites test their bounding box +against the frustum and skip invisible subtrees entirely, saving both +transform and paint work. Per-frame culling statistics are collected +for the developer tools panel. + +** Phase 2: Sort shapes by depth +:PROPERTIES: +:CUSTOM_ID: phase-2-sort-shapes +:END: + +Shapes are sorted by depth in descending order (farthest first), with +the shape id as a deterministic tiebreaker: + +#+BEGIN_SRC java +// ShapesZIndexComparator: descending Z, ties broken by shape id +if (z1 < z2) return 1; // z1 is nearer -> sort after z2 +else if (z1 > z2) return -1; // z1 is farther -> sort before z2 +return Integer.compare(o1.shapeId, o2.shapeId); +#+END_SRC + +Above 8192 queued shapes the sort runs as an instrumented parallel +merge sort on the shared worker pool; below that it is single-threaded. + +*Why sort back-to-front?* + +This implements the *painter's algorithm* — like painting a landscape: +first paint the sky (farthest), then mountains, then trees, then the +foreground. Each layer covers what's behind it. + +#+INCLUDE: "Painter's algorithm.svg" export html + +Without sorting, nearby objects might be painted first and then covered +by distant ones, causing visual errors. This is especially important for +*transparent objects* — you need to see through the near ones to what's +behind. + +The Z value represents distance from the camera after transformation. +Larger values = further away. The id tiebreaker keeps the order +deterministic frame-to-frame, which tiled rendering relies on: every +tile paints its shapes in the same global (Z, id) order. + +** Phase 3: Bin shapes into tiles +:PROPERTIES: +:CUSTOM_ID: phase-3-bin-shapes-into-tiles +:END: + +The sorted queue is binned per paint tile by screen-space overlap: +each tile's bin lists only the shapes whose vertex bounds (plus a +paint margin) can touch that tile. A shape overlapping several tiles +is added to each of their bins. + +This means a paint thread iterates a short local list instead of the +whole scene, and it is what makes the tile grid scale: refining the +grid shrinks each bin instead of just subdividing the clearing work. + +Binning is parallelized over the shared worker pool. + +** Phase 4: Clear and paint tiles (multi-threaded) +:PROPERTIES: +:CUSTOM_ID: phase-4-clear-paint-tiles +:END: + +The viewport is divided into a grid of rectangular *tiles* — roughly +10 tiles per render thread, split into near-squares (square tiles +minimize boundary crossings, i.e. how many tiles each shape overlaps). +These are *not* horizontal bands: each tile has both X and Y bounds. + +#+INCLUDE: "Paint tiles.svg" export html + +Painting is work-stolen, not pre-assigned. All tile tasks go onto a +shared =ForkJoinPool= (sized to 75% of CPU threads by default, at most +cores − 1, so one thread stays free for the rest of the system; +changeable at runtime via =setNumRenderThreads(int)=). A worker that +finishes a cheap tile immediately pulls the next queued task — another +tile (of this or an adjacent frame's pass), a transform chunk, a sort +piece — so cores never idle behind a busy thread. + +Each tile task: + +1. *Clear tile*: fill its rectangle with background color +2. *Paint shapes*: rasterize the tile's bin, back-to-front, clipping + at tile bounds + +Both operations happen within the same task, so clearing always +completes before painting on that tile. Parallel clearing across +disjoint tiles maximizes memory bandwidth utilization. + +Each tile renders through a =SegmentRenderingContext= — a view of the +frame context carrying the tile's X/Y bounds and a =Graphics2D= +pre-clipped to the tile rectangle for thread-safe text and +anti-aliased drawing. (The class name predates the tile grid; a +"segment" is now a tile.) Mouse hit detection happens during painting, +before clipping. + +A =CountDownLatch= tracks completion of all the pass's tiles — but the +render thread does *not* wait for it here. The latch is awaited one +pass later, during the flush (see [[#phase-5-flush-completed-passes][Phase 5]]). + +** Phase 5: Flush completed passes +:PROPERTIES: +:CUSTOM_ID: phase-5-flush-completed-passes +:END: + +Before each new transform, the render thread flushes paint passes that +have completed. For each flushed pass: + +1. Await its tile latch (blocks only when correctness demands it — + transform of pass P may not start before paint of pass P-3 finished) +2. *Combine mouse results*: during painting, each tile tracked which + shape is under the mouse cursor. Since all tiles paint the same + back-to-front order, they should all report the same hit; the first + non-null result wins: + +#+BEGIN_SRC java +for (SegmentRenderingContext ctx : segmentContexts) { + if (ctx.getSegmentMouseHit() != null) { + context.setCurrentObjectUnderMouseCursor(ctx.getSegmentMouseHit()); + return; + } +} +#+END_SRC + + In stereo mode this only runs for the eye whose viewport actually + contains the cursor — each eye sees a different camera position, so + combining for the wrong eye would overwrite a valid hit with null. +3. If this pass completed a frame, deposit the frame into the + presentation mailbox (see [[#software-pipeline][Software pipeline]]). The render + thread never blocks on the display. + +Passes that finished painting are flushed without any blocking, so +completed frames reach the mailbox as early as possible. + +** Phase 6: Present frame +:PROPERTIES: +:CUSTOM_ID: phase-6-present-frame +:END: + +The =e3d-present= thread takes the newest mailbox frame (dropping any +unshown older frame) and copies its =BufferedImage= to the screen using +[[https://cr.openjdk.org/~iris/se/17/latestSpec/api/java.desktop/java/awt/image/BufferStrategy.html][BufferStrategy]] for tear-free page-flipping: + +#+BEGIN_SRC java +do { + Graphics2D g = bufferStrategy.getDrawGraphics(); + g.drawImage(context.bufferedImage, 0, 0, null); + g.dispose(); +} while (bufferStrategy.contentsRestored()); + +// framebuffer released for reuse here +bufferStrategy.show(); +Toolkit.getDefaultToolkit().sync(); +#+END_SRC + +The frame's buffer is released for reuse right after the =drawImage= +loop — =show()= and =sync()= touch only the BufferStrategy's own back +buffer and the X connection, and at high resolutions they cost more +than the draw itself, so the next frame's painters don't wait for them. + +*What is double-buffering?* + +Without double-buffering, the screen updates while pixels are being +written. This causes *screen tearing* — visible horizontal splits where +the top of the frame shows old content while the bottom shows new. + +#+INCLUDE: "Double buffering.svg" export html + +Double-buffering uses two pixel buffers: +- *Back buffer*: Where rendering happens (offscreen, invisible) +- *Front buffer*: What's currently displayed on screen + +When rendering completes, the buffers *swap* in one atomic operation. +The viewer always sees complete frames, never partial updates. + +The =do-while= loop handles the case where the OS recreates the back +buffer (common during window resizing). Since our offscreen +=BufferedImage= still has the correct pixels, we only need to re-blit, +not re-render. + +* Frame listeners and smart repaint skipping +:PROPERTIES: +:CUSTOM_ID: frame-listeners +:ID: e360a877-cca6-4cba-a9a4-ea40b0f1a183 +:END: + +A *FrameListener* is a callback that runs custom logic before each potential +frame. Think of it as your "per-frame hook" — the engine calls all registered +listeners, giving them a chance to update animations, physics, or game logic. + +** Registering a frame listener +:PROPERTIES: +:CUSTOM_ID: registering-frame-listener +:END: + +Use [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#addFrameListener(eu.svjatoslav.aukio.e3d.gui.FrameListener)][addFrameListener()]] to register your callback: + +#+BEGIN_SRC java +// This is how you register a frame listener +viewPanel.addFrameListener((panel, deltaMs) -> { + // Example: simple animation listener + double rotationSpeed = 1.0; // radians per second + shape.rotate(rotationSpeed * deltaMs / 1000.0); // Framerate-independent rotation + return true; // Request repaint (shape moved) +}); +#+END_SRC + +The listener receives two parameters: +- =panel=: The ViewPanel that's rendering +- =deltaMs=: Milliseconds since last frame (for framerate-independent animation) + +The return value controls whether the frame gets rendered: +- =true=: "Something changed — repaint the screen" +- =false=: "Nothing changed — can skip this frame" + +** Frame skipping optimization +:PROPERTIES: +:CUSTOM_ID: frame-skipping-optimization +:END: + +The engine avoids unnecessary rendering. A frame is skipped when: +- *All listeners return false* (nothing changed in your scene) +- *Camera did not move* (built-in Camera listener returns false once + the camera comes to rest) +- *No resize or repaint requests* + +This means a static scene with no animations consumes almost zero CPU. +The render thread keeps running (checking for changes), but actual pixel +rendering is skipped entirely. Skipped frames still flush any pending +paint passes from earlier frames, so in-flight frames always reach the +screen. + +Two exceptions force a frame regardless of listeners: +- *Unlimited (benchmark) mode* (=targetFPS <= 0=) renders continuously, + so the measured rate reflects maximum throughput +- An explicit repaint request (resize, stereo toggle, + =repaintDuringNextViewUpdate()=, etc.) + +#+BEGIN_SRC java +// Example: listener that only requests repaint when needed +viewPanel.addFrameListener((panel, deltaMs) -> { + if (gameState.hasUpdates()) { + gameState.processUpdates(); + return true; // Only repaint when game state actually changed + } + return false; // Skip frame — nothing to update +}); +#+END_SRC + +** Built-in listeners +:PROPERTIES: +:CUSTOM_ID: built-in-listeners +:END: + +The engine registers these listeners by default: +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/Camera.html][Camera]] — applies movement velocity and friction each frame, and + returns true when the camera actually moved (more than a small + threshold), i.e. while the user is actively navigating +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.html][InputManager]] — processes mouse/keyboard events + +When the camera stops moving and you release all keys, the Camera listener +returns false. If your custom listeners also return false, the frame is +skipped until something changes. + +* Rendering context +:PROPERTIES: +:CUSTOM_ID: rendering-context +:END: + +The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/RenderingContext.html][RenderingContext]] holds all state for rendering into one framebuffer: +the pixel buffer, projection parameters, and per-frame bookkeeping. + +| Field | Purpose | +|-------+---------| +| =pixels[]= | Raw pixel buffer (int[] in RGB format) | +| =bufferedImage= | Java2D wrapper around pixels | +| =graphics= | Graphics2D for text, lines, shapes | +| =width=, =height= | Full framebuffer dimensions | +| =centerCoordinate= | Screen center of the active viewport (for projection) | +| =projectionScale= | Perspective scale factor, derived from viewport width. Mutable: each stereo eye sets its own | +| =renderMinX=, =renderMaxX= | X bounds of the active viewport or tile | +| =renderMinY=, =renderMaxY= | Y bounds (full height on the frame context, tile bounds on segment views) | +| =stereoEye=, =stereoViewportWidth=, =stereoViewportOffsetX= | Which eye this pass renders and where its viewport sits in the buffer | +| =tilesX=, =tilesY=, =viewportCount=, =numRenderSegments= | Tile grid geometry (segments = tilesX × tilesY × viewports) | +| =frustum= | View frustum for culling, rebuilt each pass from camera state | +| =frameNumber= | Per-context frame counter | +| =transformCycleId= | Globally unique transform-cycle id, safe key for per-cycle memoization | +| =vertexSlot= | Projection buffer slot (0–2) this pass transforms into | + +** Triple-buffered frame contexts +:PROPERTIES: +:CUSTOM_ID: triple-buffered-frame-contexts +:END: + +The engine keeps /three/ frame contexts, cycled by frame parity. While +frame N is still being painted from one buffer, frame N+1 already +transforms into the next — paint threads never idle waiting for the +transform phase, and vice versa. A per-buffer *present gate* prevents +painting frame F+3 into a buffer the present thread is still blitting +frame F from. + +All three contexts are recreated together when the window is resized, +when the tile grid changes (render thread count), or when stereo mode +is toggled. Otherwise they are reused — =prepareForNewFrameRendering()= +just resets per-frame state like mouse tracking. + +** Per-pass copies +:PROPERTIES: +:CUSTOM_ID: per-pass-copies +:END: + +Each render pass (one per eye in stereo) works on a private /copy/ of +the frame context. The copy shares the pixel buffer, graphics and +services, but owns the projection fields (center, scale, viewport, +vertex slot), so the next pass's setup cannot disturb a pass whose +transform or paint is still in flight. + +Consequence for engine code: per-frame mutable state must be allocated +eagerly on the frame context. Anything created lazily inside a pass +lands on the throwaway copy and is lost. + +** Tile segment views +:PROPERTIES: +:CUSTOM_ID: tile-segment-views +:END: + +Each paint tile gets a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/SegmentRenderingContext.html][SegmentRenderingContext]], +a view that shares the framebuffer with its parent but carries its own +X/Y tile bounds and a pre-clipped =Graphics2D= for thread-safe text and +shape drawing. Mouse hits are tracked per tile and combined after all +tiles finish painting. diff --git a/Documentation/SDF textures/SDF concept.svg b/Documentation/SDF textures/SDF concept.svg new file mode 100644 index 0000000..bc1b750 --- /dev/null +++ b/Documentation/SDF textures/SDF concept.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + Why a distance field, not a bitmap + one glyph edge, magnified 8x - stored coverage vs re-derived coverage + + + BITMAP coverage + what you store is what you get + + + + + + + + + + + + + + + + + + + + + + + + + + + + texel grid IS the resolution limit: + edges stair-step, curves become blocks + + + SDF: distance to edge + a smooth field - the edge is re-derived per pixel + + + + + + + + + + + + + + + + edge recovered at + display resolution + + + d < 0: inside ink + d > 0: outside + d = 0: the edge + + + + coverage = (127.5 - d) * aaK + 128 + aaK scales the gradient window to the pixel footprint - the same 16x32 texel field + serves a 4-pixel label and a full-screen billboard + diff --git a/Documentation/SDF textures/SDF glyph pipeline.svg b/Documentation/SDF textures/SDF glyph pipeline.svg new file mode 100644 index 0000000..09473d6 --- /dev/null +++ b/Documentation/SDF textures/SDF glyph pipeline.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + Glyph field generation (SdfGlyphCache) + once per character, then cached - stamping is a block copy + + + + 1. rasterize glyph + Liberation Mono Bold, AA on + 64x128 px (4x supersample) + font auto-sized to fit cell + advance 0.6em (Courier-compat) + + + + + + 2. distance transform + exact Euclidean EDT + (Felzenszwalb-Huttenlocher, + two separable 1-D passes) + dOut to ink, dIn to background + + + + + + 3. sign, clamp, average + signed = dOut - dIn + clamp to +/- 2 texels spread + average FIELD down to 16x32 + (averaging the field, not coverage, + preserves the edge position) + + + + + + + mask encoding (per texel, 0..255) + 0 = deep inside ink 127.5 = the edge 255 = far outside + + + + + + + + + + + + + + + + + TextCanvas.putChar + stamps the cached 16x32 mask + into the cell position of sdfMask + + + three texture layers + sdfMask: glyph SHAPES (bilinear) + sdfForeground + primary: colors + + why sans-serif bold: Courier's serifs and hairline strokes decay into unresolvable noise + when the distance field is minified - uniform sturdy strokes survive + + + + NO mipmaps on the mask: the edge gradient spans ~2 texels, + half-res masks melt glyph edges - minification is analytic instead + diff --git a/Documentation/SDF textures/SDF minification.svg b/Documentation/SDF textures/SDF minification.svg new file mode 100644 index 0000000..95cce51 --- /dev/null +++ b/Documentation/SDF textures/SDF minification.svg @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + Minification: analytic coverage window + one screen pixel covering many texels still resolves the edge correctly + + + + a screen pixel on the texture + + + + + + + + + + + + + + + + + + + footprint: many texels per pixel + a bitmap would average to mush or alias; + the field still knows where the edge is + + + + per-axis footprint from UV gradients + footX = |dUV/dx|, footY = |dUV/dy| + window follows the SHARPEST axis + + + + + aaK = 2*spread / texelsPerPixel + widens the coverage window as pixels grow + + + + + perceptual corrections (minified text + else reads as gray haze): + SHARPEN x2: sub-pixel window kills halo + coverage gamma < 1: stem darkening + + + + result: graceful degradation + magnified: edges re-derived at display resolution - razor sharp + minified: coverage fades smoothly to clean gray, no crawling aliases + angled: the uncompressed axis keeps its sharpness + diff --git a/Documentation/SDF textures/glyph-sdf-S.png b/Documentation/SDF textures/glyph-sdf-S.png new file mode 100644 index 0000000000000000000000000000000000000000..ecc1de5cec62a3d351fedd72528394265a8c034e GIT binary patch literal 6097 zcmeHLX;4#F6i!eU#aCu38syn#tgT9mEVd@LY61vV8W*HhQ8cKjiUT3CC{d%JnFfm} zwuLBe#F<*G3mBGxKqSQ1;7sB|>p~zg;?4B#_M58EZQoq5btTH}md2 z-*?V;?)l!gar0*b!vlwtNF*S3PINr_Y$B1!xjr7~udMHScacbx1F_MOU-8+UH!q#~ zJaI)+*KNflwNE~qSK@Q7RpJ}Cz4*0$460@Ky zC!D-z{K%GRQ~u?LWzvvdz37D=;emFJur;K#!}u9hI4oJ2klz~89KZZFCyYEffYsHm zO31$_7%@TO$q8aN1hCSN6e@>SuMAeDP&h&5kHQb+jxIRP?_3?M+Q{%4*|JmWU$LU# zc-`d-D?OHliA^=TrT(ow0dpgKo8#r1!o{XnK4cA9HpYAvWJuvcx(ycjars+>nVQ+O z84a)uIC^Q@LG0jZwE?VfF?x8FYyp1w?a`c|=6EzBTK9T906qF=LcY}h$}Pq`EQrI2 z1;?>>Ol|ry9Bn2VA@hHuCTdaWS1 zo{viast&OO5A}!`;OP+&pr}^L%x6GDm6ETSNS~olzNL0M2s>XJS>gs5!!&)88)lpe z?=^0)F=Y28IQ>rKI3e#8xq-*nA(n<1ez0<&+6#~I_lt2;E0nms+8Wh%xj6drPnyYq9|MA zTsbK^MBvk3>*EL&?kqR#X+-U&f9a@l?EQ4WQNYB1vN42&<~iyde}AeAqpO4etHJc7 zt4*w}W;58B3U#Mi+kl5!(ogQXE*DwGsJVMb?juG&Nz-j_1WK{L+BvhF$rlorARbeXE!1QjrVC6;bO|fTfB?j zv99*!Y$uy}4xR@JdMX6JdgUNzY}Z7!JS3*xBs5zlf(5mLEa(Ew)+x%-BC<*8mBH}ER`RdsmC?}D1i4Lf0iAWJjAlqtQ- zR1~U)MHf5~RkRy@i&TDl#dKcS2}|MNZ3}I~lP_dqbGjeRI_{#W@N98X8`29f{3nZ+ zHVTBRMhib=+zHt}w0iJfk2B))wg3JA0KTH%;) z70Qa~JY=7yH5Ku!GyNn+r@-A=*=D`zdGSP=JPSHSmv0q@rigKDh9u|shk7aeK)J%L zjM1R@IWATV54_9nCJ7Xr>G`PsLLmrv_K|D-VkC7h>34<%kq;|ewSbZRmjlu>WXkfGgYsxgqHHAXgWTCS|~_$Yx@rTOLi$ zfkM-eRcpIet=)V*hFt|e0S%RKHch}6G7m0cqpfR2lPlJ6F_x2owvzG4PbJA`o?pzU zSMCqoO_c>N*hW5=E%YnZP`Ug~&^j8wIeV%bumQDDGpc2gJq1$m!Y*36lOjsnkqVH} zpq#la8C4=771L2hH7Zm>N$Cry?R_1&uFQ|?QJ?Ghx^gv-*2lwxyR#1U)pqyeWAh>? zSWicbe`{|G+GZS*d*x#~eFIypFV=Yk&YmDi|N8&OpHg54$DX@CJgS|%5&cw8ijA2c JT^_Y^=O0#a6@~x+ literal 0 HcmV?d00001 diff --git a/Documentation/SDF textures/index.org b/Documentation/SDF textures/index.org new file mode 100644 index 0000000..ff4cf78 --- /dev/null +++ b/Documentation/SDF textures/index.org @@ -0,0 +1,252 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: SDF Textures - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* What SDF textures are +:PROPERTIES: +:CUSTOM_ID: what-sdf-is +:END: + +A regular texture stores *coverage*: each texel says "this much ink +here". That is a photocopy of the glyph — resample it (magnify, minify, +view at an angle) and the stored pixels blur or alias, because the +information about /where the edge is/ was thrown away when the glyph +was rasterized. + +A *signed distance field* (SDF) texture stores something smarter: per +texel, the *distance to the nearest edge* — negative inside the ink, +positive outside, zero exactly on the boundary. The rasterizer then +re-derives coverage per screen pixel from this smooth field. The edge +position survives resampling because the field around it is linear — +bilinear interpolation of a linear ramp is exact. + +#+ATTR_HTML: :width 640 +[[file:SDF concept.svg]] + +In Aukio 3D the mask is a grayscale field in =texture.sdfMask=: + +- =0= — deep inside the ink +- =127.5= — exactly on the edge +- =255= — far outside any glyph + +The gradient spans only =SPREAD_TEXELS = 2.0= texels around the edge — +that narrow band is all the rasterizer needs. + +Here is a real field, dumped straight from =SdfGlyphCache= (glyph "S", +16x32 texels, upscaled 12x with nearest so you can see the texels): + +[[file:glyph-sdf-S.png]] + +Dark inside the strokes, bright outside, and a smooth gray ramp exactly +two texels wide around the contour. + +* Generating glyph fields +:PROPERTIES: +:CUSTOM_ID: glyph-pipeline +:END: + +=SdfGlyphCache= generates each character's distance field once and +caches it in a =ConcurrentHashMap=; stamping a glyph into a canvas is +then just a block copy. + +#+ATTR_HTML: :width 640 +[[file:SDF glyph pipeline.svg]] + +The steps: + +1. *Rasterize* the glyph with AWT at 4x the cell size (64x128 pixels) + with anti-aliasing on, using Liberation Mono Bold (metric-compatible + with Courier New, so the cell grid is unchanged). The font size is + auto-shrunk until the widest glyph fits the scratch without clipping + — a clipped glyph would corrupt the distance field at the cell edge. +2. *Distance transform*: an exact Euclidean distance transform + (Felzenszwalb & Huttenlocher, two separable 1-D passes over parabola + envelopes) is run twice — once for distance to nearest ink pixel, + once for distance to nearest background pixel. +3. *Sign, clamp, average*: signed distance = dOut - dIn, clamped to + +/-2 texels of spread, then the *field* (not coverage) is averaged + down to the 16x32 cell resolution. Averaging the field preserves the + edge position; averaging coverage would not. + +The font choice matters: Courier's serifs and hairline strokes decay +into unresolvable noise when the field is minified. A uniform-stroke +bold sans-serif survives. + +* The rendering path +:PROPERTIES: +:CUSTOM_ID: render-path +:END: + +When =texture.isSdf()= is true (an =sdfMask= is attached), +=TexturedTriangle.paintSdf= takes over. Three layers are involved: + +| Layer | Contents | Sampling | +|------------------+-----------------------------+----------| +| =sdfMask= | glyph shapes (the field) | bilinear | +| =sdfForeground= | ink color, flat per cell | nearest | +| =primaryBitmap= | background color, per cell | nearest | + +Per screen pixel: + +1. Sample the mask bilinearly (fixed-point) -> distance =d=. +2. Convert to coverage: =cov = (127.5 - d) * aaK + 128=, clamped to + [0, 256]. =aaK= scales the 2-texel gradient window to the current + pixel footprint (see next section). +3. Blend: =pixel = bg * (1 - cov) + fg * cov=. + +Perspective-correct interpolation applies to SDF triangles exactly as +it does to regular textured triangles — same affine-sufficiency test, +same subdivided correction. See +[[file:../Perspective correct textures/index.org][Perspective-correct +textures]]; only the per-pixel sampling differs. + +* Minification without mipmaps +:PROPERTIES: +:CUSTOM_ID: minification +:END: + +*There is deliberately no mipmap chain for SDF layers.* A distance +field's edge gradient spans ~2 texels; a half-resolution mask melts the +glyph edges. Worse, the two triangles of a rectangle cross mip +thresholds at slightly different distances, producing a hard diagonal +quality split and sudden blur steps while dollying (observed in +practice). + +Minification is instead handled *analytically*: the coverage window is +widened by the screen-space pixel footprint, giving area-correct +coverage straight from the primary field. + +#+ATTR_HTML: :width 640 +[[file:SDF minification.svg]] + +The footprint is computed per axis from the screen-space UV gradients — +=text on an angled plane is minified mostly along one axis=, and an +isotropic average would blur the axis that still has resolution to +spare. The coverage window follows the sharpest axis. + +Area-correct coverage alone reads as a low-contrast gray haze, so two +perceptual corrections (A/B-tuned on far + angled text) kick in under +minification: + +- *Sharpening* (=SDF_SHARPEN=, default 2): narrows the coverage window + below one pixel — kills the haze halo at the cost of slight shimmer. +- *Coverage gamma* (< 1, automatic from the footprint): darkens stems + like a small-size font rasterizer, keeping thin strokes present. + +Real output, rendered headlessly through the [[file:../index.org::#snapshot][Snapshot tool]]: + +Magnified — edges re-derived at display resolution, razor sharp: + +[[file:sdf-near.png]] + +At moderate distance: + +[[file:sdf-mid.png]] + +Far away — small but clean, fading to gray instead of disintegrating +into aliases (right: 4x nearest zoom of the center): + +[[file:sdf-far.png]] + +[[file:sdf-far-zoom.png]] + +At an oblique angle — foreshortened along one axis, still sharp along +the other: + +[[file:sdf-angled.png]] + +* Using it +:PROPERTIES: +:CUSTOM_ID: using-sdf +:END: + +*TextCanvas* is the main entry point: a textured rectangle carrying a +character grid in 3D space. World cell size 8x16 units, texture cell +16x32 texels (2 texels per world unit). + +#+BEGIN_SRC java +Transform location = new Transform(new Point3D(0, 0, 500)); +TextCanvas canvas = new TextCanvas(location, "Hello, World!", + Color.WHITE, Color.BLACK); +shapeCollection.addShape(canvas); + +// blank canvas + cursor writing +TextCanvas blank = new TextCanvas(location, new TextPointer(10, 40), + Color.GREEN, Color.BLACK); +blank.locate(0, 0); +blank.print("Line 1"); +blank.locate(1, 0); +blank.print("Line 2"); +blank.setForegroundColor(Color.RED); // affects subsequent writes +blank.setTextColor(Color.CYAN); // recolors existing ink only +#+END_SRC + +Colors are per-cell: each =putChar= fills the cell's rectangle in the +background and foreground layers, so one canvas can hold many colors. + +*ForwardOrientedTextBlock* renders the same pipeline onto a billboard +that always faces the camera — for labels that must stay readable from +any angle: + +#+BEGIN_SRC java +ForwardOrientedTextBlock label = new ForwardOrientedTextBlock( + new Point3D(0, -50, 300), 1.0, 2, "Hello, World!", Color.RED); +shapeCollection.addShape(label); +#+END_SRC + +Real use in the demos: the life demo's help panel (=life_demo/Main.java= +=createHelpPanel()=) and the axis labels in =CoordinateSystemDemo=. + +* Tuning knobs +:PROPERTIES: +:CUSTOM_ID: tuning +:END: + +JVM properties (A/B tuning knobs in =TexturedTriangle=): + +| Property | Default | Effect | +|-------------------+---------+-------------------------------------------| +| =e3d.sdf.gamma= | 0 (auto) | fixed coverage gamma; auto derives from footprint | +| =e3d.sdf.sharpen= | 2 | coverage window narrowing; 1 = pixel-exact | +| =e3d.sdf.debug= | false | prints per-triangle footprints and path decisions to stderr | + +* Limitations +:PROPERTIES: +:CUSTOM_ID: limitations +:END: + +- *Fixed cell grid*: TextCanvas is monospace by construction (16x32 + texel cells). Proportional fonts would need a different stamping + scheme. +- *ASCII-oriented cache*: =SdfGlyphCache= measures printable ASCII + (33..126) when sizing the font; exotic glyphs may fit worse. +- *Under extreme minification* text fades to gray by design — that is + the correct physical answer (a sub-pixel glyph has no shape left), + but it means distant labels are decorative, not readable. +- *Bandwidth under minification*: sampling the primary field (no mip + chain) costs more bandwidth per pixel. Text surfaces are small, so + this is the right trade — do not attach SDF masks to huge surfaces. + +* Related classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Role | +|-----------------------------+--------------------------------------------------| +| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.html][TextCanvas]] | Character grid surface in 3D; owns the layers | +| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/SdfGlyphCache.html][SdfGlyphCache]] | Per-glyph field generation + cache (EDT inside) | +| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.html][ForwardOrientedTextBlock]] | Camera-facing text billboard | +| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | =paintSdf= — the scanline path | +| [[file:../apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.html][Texture]] | =sdfMask=, =sdfForeground=, =sdfSpreadTexels= | + +*See also:* + +- [[file:../Perspective correct textures/][Perspective-correct textures]] — the scanline texture-mapping path + that SDF rendering builds on; both paths live in =TexturedTriangle= + and share the same interpolated UVs. + diff --git a/Documentation/SDF textures/sdf-angled.png b/Documentation/SDF textures/sdf-angled.png new file mode 100644 index 0000000000000000000000000000000000000000..34814bf48a46cc9c6a38829ffaf28d3fb6995191 GIT binary patch literal 5386 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 literal 0 HcmV?d00001 diff --git a/Documentation/SDF textures/sdf-far-zoom.png b/Documentation/SDF textures/sdf-far-zoom.png new file mode 100644 index 0000000000000000000000000000000000000000..f0c0a44df03ddc1d7017e46420333cef3416d3ea GIT binary patch literal 1776 zcmbtVZB){E7G}oTamy*4q|jV)cE`!Jn$lC`n1b$xnVQb{uA2+Zm|>7_#|Q+JE$58M zRG!AQ%s?v5*U;mb2+E61BI78KIf@GWNgTxxBoaYq~n8cDwT8;k}eG9^9@4vnE0n~EuAqp z%*>b3jp!C9j{i3v#o_so0`?^5W%nZgOAY=m1Du9glP+X>iX8PoaT z?Iuq3m(>TdhQiGEbX-)27(?uiQ6MU}J(wv~pU(Kf}@@wJ?H|rn+D+7+K9pSa`5KO9+N7i4;BdJ8 zKw4jerE+qX<@m(Xs|cONWbi(~szj#ASYP4?_K84;DV^%*C?`v+sX|hojH>Z1ecbpF zvrFkz>=)srE{BTttm=<24NvnwDL~N}t|-m+H$a_uyWA-^7ALZNIlUeZbPupY%XBRb z`A5j-Ho6)>!6D(D^~bs1U1bG66Sn8VsFio&n2^ ziW<%I=(f&z!a%WVFinFQBBNmfu$GJw+KFuTiEFl#^llXApykW`q5G4aYQmPw27L@jQxw7wU!A>m`mD(=(uX%X$~=@uetu2vPiX z8Uv>Tgb#^k$?XQBX=8ykF4!mCBR5tY=DQi`R~LiDD_lW&{R!w=rnwejdxlO_LMw`R zZpO;prK`s~VhXjIg=$yl_7*sraJ>OuaF5wn4+ll_K%C%%%5lGEgUh+CHI z&Zt^B-&Q*R0(v(UDzu~@Ej^mH8Sx{zY5alQ`C%OQo6DEA{wzP^FpL~)UTj4$_`wQs z-TEnWZL8WFe{K_KjxA~`**--s&!dqz$Gl}nTI+Nr%p7#xcxOi60#9h3yaf%C1FIt$ zzY%%N6L+UBJ$uv>R}mTrKK1tTCw_t85yUI+Bh-?xb<9&cgf$yucbpS=%cAoD^J{$O z5LW_y<9jE~jqfgfPNJGSu*!{Zz%pgApGXx~n94TU7UH@KR8^)m0nr%q8na!?uZ`-U z74gQxuc0!#vrHEN%T5#AaG4pI&l=1fm194u62iT*_Rs`Lwh-Wl0{ZDa++(CK;_SMc z97L!h?2?G>wY(aUS+&xLSYb6^FGu*O8F_qID3jtCOp(iW{Ey``03U8Cfj*oo43ah; zL~%UiN(d%9^!6ZkV#fW1tcm+6{H8`wHX5~0uJTx!c@a!v?r)D5PfbrK>cb}%suTQW z0O`@UdxH$;G539XkSbt$C(Jga#%#8KyGAWQn<-K}J5t!Tapx2RY9is%jJ<>z7k|x) zp77)_*X6$){!As^6?Kd0nrFO5;v`CHqa+xIIvaM@!R$gj&*+#H9Oo|rnLr!Ya+Xb) z_FYa@m~dF*_iM=*e&4R(s1m@@H2meWx656;Ydr`sap#r3(^7 z`O~0ygt5O&w#glEy1CHx&DL`+zn06lUsj~-g1!2cn%}<)_Yzx@_c>gSecn2i)Z}sb P=f>#Bcx3zE&s_NrBWjWV literal 0 HcmV?d00001 diff --git a/Documentation/SDF textures/sdf-far.png b/Documentation/SDF textures/sdf-far.png new file mode 100644 index 0000000000000000000000000000000000000000..64898a2e3e3ab93add9c898c0173b54478eb25ab GIT binary patch literal 1706 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 literal 0 HcmV?d00001 diff --git a/Documentation/SDF textures/sdf-near.png b/Documentation/SDF textures/sdf-near.png new file mode 100644 index 0000000000000000000000000000000000000000..1d493a168d3c1196c03e702b5c94912bc59a3139 GIT binary patch literal 11920 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 aTC%Vr;RR{AipcM&0LrLH7rlA^_kRG}1CT}l literal 0 HcmV?d00001 diff --git a/Documentation/Shading/Ambient light comparison.svg b/Documentation/Shading/Ambient light comparison.svg new file mode 100644 index 0000000..ce07a00 --- /dev/null +++ b/Documentation/Shading/Ambient light comparison.svg @@ -0,0 +1,51 @@ + + + + + + +Ambient Light +base illumination applied to all surfaces equally, regardless of orientation + + + + + + + + + + + + + + +Color(0, 0, 0) +✗ pure black +harsh shadows, no depth + + + + + + + + +Color(50, 50, 50) +✓ balanced +depth preserved + + + + + + + + +Color(150, 150, 150) +✗ too flat +no depth contrast + + +lightingManager.setAmbientLight(new Color(50, 50, 50)) ← default + \ No newline at end of file diff --git a/Documentation/Shading/Distance attenuation.svg b/Documentation/Shading/Distance attenuation.svg new file mode 100644 index 0000000..2edf492 --- /dev/null +++ b/Documentation/Shading/Distance attenuation.svg @@ -0,0 +1,91 @@ + + + + + + + + + +Distance Attenuation +light intensity falls off with distance from source + + + + + + + + + + + + +Light + + + + + + +0.99 + +d = 100 + + + + +0.52 + +d = 300 + + + + +0.29 + +d = 500 + +← attenuation factor shown above each surface → + + +attenuation vs distance + + +d +att + +0 + + +0.5 + + +1.0 + + +100 + + +300 + + +500 + + + + + +0.99 +0.52 +0.29 + + + +attenuation = +1 / (1 + 0.0001 · d²) + + +coefficient 0.0001 was tuned for typical scene scales in Aukio 3D + diff --git a/Documentation/Shading/Lambert cosine law.svg b/Documentation/Shading/Lambert cosine law.svg new file mode 100644 index 0000000..1f4e216 --- /dev/null +++ b/Documentation/Shading/Lambert cosine law.svg @@ -0,0 +1,92 @@ + + + + + + + + +Lambert Cosine Law +how surface orientation determines light intensity + + + + + + +N̂ +normal + + + + + + + + +Light + +L̂ + +θ +surface polygon + +brightness = +dot( N̂ , L̂ ) + += cos( θ ) + +θ = 0° + + +1.00 +θ = 45° + + +0.71 +θ = 90° + +0.00 + +θ > 90° +back-face → skip +dot < 0 → no contribution +— angle examples — + + + + + + θ = 0° + + + 100% + + + + + + + + 45° + θ = 45° + + + 71% + + + + + + + + 90° + θ = 90° + + 0% (skip) + + +N̂ surface normal + +L̂ light direction + diff --git a/Documentation/Shading/Shaded sphere.png b/Documentation/Shading/Shaded sphere.png new file mode 100644 index 0000000000000000000000000000000000000000..fbc6487394d71c8c4916c026c3137e28643ca1f2 GIT binary patch literal 23417 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 literal 0 HcmV?d00001 diff --git a/Documentation/Shading/Shading pipeline.svg b/Documentation/Shading/Shading pipeline.svg new file mode 100644 index 0000000..a58c431 --- /dev/null +++ b/Documentation/Shading/Shading pipeline.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + Transform + compute lighting + + + + Shapes + + + Sort + + + Paint + use cached color + + + Blit + + + + + + + + diff --git a/Documentation/Shading/index.org b/Documentation/Shading/index.org new file mode 100644 index 0000000..fdf95a0 --- /dev/null +++ b/Documentation/Shading/index.org @@ -0,0 +1,266 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Shading & Lighting - Aukio 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 + +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* Overview +:PROPERTIES: +:CUSTOM_ID: shading-lighting +:END: + +#+attr_html: :class responsive-img +#+attr_latex: :width 1000px +[[file:Shaded sphere.png]] + +*Aukio 3D* implements *flat shading* using the [[https://en.wikipedia.org/wiki/Lambert%27s_cosine_law][Lambert cosine +law]]. Each polygon receives a single color based on its orientation +relative to light sources. This is a simple yet effective lighting +model that gives 3D objects depth and realism. + +** The Lighting Model: Lambert Cosine Law +:PROPERTIES: +:CUSTOM_ID: lambert-cosine-law +:END: + +#+INCLUDE: "Lambert cosine law.svg" export html + +The *Lambert cosine law* determines how much light a surface receives +based on its orientation. A surface facing directly toward a light source +receives maximum illumination; as it tilts away, the illumination decreases +proportionally until it reaches zero when perpendicular to the light +direction. This fundamental principle creates the visual cues that make 3D +objects appear solid and dimensional rather than flat. + +The engine implements this law through the dot product of two vectors. The +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] computes a unit vector pointing from the polygon's center +to each [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]], then calculates the dot product with the surface +normal. When the dot product equals 1.0, the surface faces the light +directly and receives full brightness. At 0.71 (a 45-degree angle), it +receives about 71% illumination. At zero or below, the surface faces away +from the light and receives no direct contribution from that source. The +implementation in [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager.computeLighting()]] explicitly checks for +positive dot products before adding light contributions, ensuring that +back-facing surfaces skip unnecessary calculations. + +The surface normal itself is computed by [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Plane.html#computeNormal()][Plane.computeNormal()]], which takes +the first three vertices of a polygon and calculates their cross product to +find the perpendicular direction. This normal, along with the polygon's +center point calculated by [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]], is passed to the lighting manager +during the [[file:../Rendering loop/][transform phase]] of the rendering loop. The transform phase runs +in parallel, but each polygon is transformed by exactly one worker per +pass, so its cached =shadedColor= field has a single writer — the +result is reused allocation-free during the subsequent multi-threaded +paint phase. See the +[[file:../index.org::#normal-vector][Normal Vector]] section for more details on how normals are computed and used +throughout the engine. + +* Light Sources +:PROPERTIES: +:CUSTOM_ID: light-sources +:END: + +Each light source has three properties: + +| Property | Description | +|------------+--------------------------------------| +| Position | 3D world coordinates of the light | +| Color | RGB color of emitted light | +| Intensity | Brightness multiplier (1.0 = normal) | + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; + +// Create a bright yellow light to the right +LightSource rightLight = new LightSource( + new Point3D(200, -100, 0), // position: right, above, at viewer level + Color.YELLOW, // color + 2.0 // intensity: extra bright +); + +// Create a dim blue light from the left +LightSource leftLight = new LightSource( + new Point3D(-150, 50, 100), + Color.BLUE, + 0.5 // intensity: dim +); +#+END_SRC + +Multiple light sources add their contributions together, allowing for +complex lighting setups like the screenshot above showing a sphere lit +by two lights from the right. + +** Distance Attenuation +:PROPERTIES: +:CUSTOM_ID: distance-attenuation +:END: + +#+INCLUDE: "Distance attenuation.svg" export html + +Light intensity decreases with distance using a *simplified inverse +square law*: + +#+BEGIN_SRC +attenuation = 1.0 / (1.0 + 0.0001 * distance²) +#+END_SRC + +- At distance 0: attenuation = 1.0 (full intensity) +- At distance 100: attenuation ≈ 0.99 (almost full) +- At distance 300: attenuation ≈ 0.52 (half intensity) +- At distance 500: attenuation ≈ 0.29 (about 30%) + +This simplified formula prevents harsh cutoffs while still providing +distance-based dimming. The =0.0001= coefficient was tuned for typical +scene scales in Aukio 3D. + +* Ambient Light +:PROPERTIES: +:CUSTOM_ID: ambient-light +:END: + +#+INCLUDE: "Ambient light comparison.svg" export html + +*Ambient light* provides base illumination that affects all surfaces +equally, regardless of orientation. Without ambient light, surfaces not +directly facing a light source would be pure black. + +- Default ambient: =Color(50, 50, 50)= (dim gray) — set by the ViewPanel + constructor; a standalone =new LightingManager()= starts at + =Color(10, 10, 10)= +- Configurable via =lightingManager.setAmbientLight()= +- Too much ambient: flat appearance (no contrast) +- Too little ambient: harsh shadows (pure black areas) + +#+BEGIN_SRC java +// Increase ambient for softer shadows +viewPanel.getLightingManager().setAmbientLight(new Color(80, 80, 80)); + +// Reduce ambient for dramatic contrast +viewPanel.getLightingManager().setAmbientLight(new Color(20, 20, 20)); +#+END_SRC + +* Using Shading in Your Scene +:PROPERTIES: +:CUSTOM_ID: using-shading +:END: + +**Adding light sources:** + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource; + +ViewPanel viewPanel = new ViewPanel(); + +// Get the lighting manager +LightingManager lighting = viewPanel.getLightingManager(); + +// Add light sources +lighting.addLight(new LightSource( + new Point3D(200, -100, 0), // right side, above + Color.YELLOW, + 1.5 // bright +)); + +lighting.addLight(new LightSource( + new Point3D(-100, 0, 200), // left side, further away + new Color(255, 200, 150), // warm white + 1.0 +)); + +// Configure ambient light +lighting.setAmbientLight(new Color(40, 40, 40)); +#+END_SRC + +**Enabling shading on shapes:** + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonRectangularBox; + +// Create a shaded box +SolidPolygonRectangularBox box = new SolidPolygonRectangularBox( + new Point3D(-50, -50, 100), // min corner + new Point3D(50, 50, 200), // max corner + Color.RED +); + +// Enable shading on the box and all its sub-polygons +box.setShadingEnabled(true); + +// Also enable backface culling for closed meshes +box.setBackfaceCulling(true); + +// Add to scene +viewPanel.getRootShapeCollection().addShape(box); +#+END_SRC + +Shading propagates through composite shapes — calling +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setShadingEnabled(boolean)][setShadingEnabled(true)]] on a composite enables shading for all its +sub-polygons. + +** Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|-------+---------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] | Manages light sources and computes shading | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]] | Individual light with position, color, intensity | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] | Polygon shape with shading support | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] | Composite shape with shading propagation | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html][ViewPanel]] | Provides access to LightingManager | +* Implementation details +:PROPERTIES: +:CUSTOM_ID: implementation-details +:END: + +#+INCLUDE: "Shading pipeline.svg" export html + +Lighting is computed during *Phase 1* (transform phase) of the +[[file:../Rendering loop/][rendering loop]]: + +1. Each shaded polygon calculates its center point and surface normal +2. [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] computes lighting from all sources +3. Result stored in reusable =shadedColor= field +4. During *Phase 4* (paint), the cached color is used directly + +**Why during transform phase?** + +- Lighting computed *once per polygon per pass* — not per pixel +- Each polygon is transformed by a single worker, so its cached result + has exactly one writer even though the transform phase runs in parallel +- Result reused during multi-threaded paint phase — efficient + +** Performance Characteristics +:PROPERTIES: +:CUSTOM_ID: performance +:END: + +| Aspect | Cost | +|--------------+-------------------------------| +| Computation | Per polygon, not per pixel | +| Phase | Parallel transform (single writer per polygon) | +| Allocation | Zero (reuses Color instance) | +| Cache | One shadedColor per polygon | + +The shading implementation is optimized for CPU rendering: + +- *Flat shading*: One lighting calculation per polygon (N-vertex polygon = 1 calculation) +- *Reusable Color*: Result stored in existing field, no allocation during render +- *Thread-safe*: One writer per polygon per pass, so no synchronization needed +- *Pre-computed*: All paint workers (tile grid, ~75% of CPU cores by default) read the same cached result + +This approach trades visual fidelity (no per-pixel lighting) for +performance — essential for software rendering where per-pixel lighting +would be prohibitively expensive. diff --git a/Documentation/Stereoscopic rendering/Stereo geometry.svg b/Documentation/Stereoscopic rendering/Stereo geometry.svg new file mode 100644 index 0000000..2f62d50 --- /dev/null +++ b/Documentation/Stereoscopic rendering/Stereo geometry.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + Two parallel cameras, one screen + top-down view of the scene (z grows downward = into the scene) + + + + screen plane (per eye) + + + + near object + + far object + + + + left eye + x - IPD/2 + + right eye + x + IPD/2 + + + + + + IPD = 6.5 units (cm) + + + + + + + + + + + + + + + + + + + + + + + + large disparity = close + + small disparity = far + + cameras stay PARALLEL (no toe-in) - depth comes purely from the x offset + diff --git a/Documentation/Stereoscopic rendering/Stereo per eye.svg b/Documentation/Stereoscopic rendering/Stereo per eye.svg new file mode 100644 index 0000000..5fc9cfd --- /dev/null +++ b/Documentation/Stereoscopic rendering/Stereo per eye.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + What changes per eye + + + + + concern + per-eye behavior + + + camera + translation.x += ±IPD/2 (restored after the pass) + + + projection + scale = eyeWidth/3; x += stereoViewportOffsetX + + + frustum culling + built from stereoViewportWidth - narrower FOV per eye + + + painting + clipped to [renderMinX, renderMaxX) = the eye's half + + + mouse picking + hits combined only for the eye containing the cursor + + + HUD / overlays + drawn once, spanning the full frame (zero disparity) + + + everything else - geometry, textures, lightmaps, GI - is shared: the scene is identical, only the viewpoint moves + diff --git a/Documentation/Stereoscopic rendering/Stereo pipeline.svg b/Documentation/Stereoscopic rendering/Stereo pipeline.svg new file mode 100644 index 0000000..895e1fc --- /dev/null +++ b/Documentation/Stereoscopic rendering/Stereo pipeline.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + One frame = two passes + the triple-buffered pipeline runs the same phases twice, once per eye + + + + camera + translation.x nudged +/- IPD/2 + + + + pass LEFT + transform → sort → tile-bin + viewport [0, w/2) + + + + pass RIGHT + transform → sort → tile-bin + viewport [w/2, w) + + + + + + + + each pass owns a RenderingContext copy + stereoEye, stereoViewportWidth/OffsetX, renderMinX..renderMaxX + + + + + + + + + + left eye pixels + painting clipped to left half + right eye pixels + painting clipped to right half + ONE shared frame buffer → one blit to screen (side-by-side image) + + + + + vertex buffers, aggregators and paint slots still cycle through 3 slots - the two passes overlap freely + diff --git a/Documentation/Stereoscopic rendering/index.org b/Documentation/Stereoscopic rendering/index.org new file mode 100644 index 0000000..f1ceee8 --- /dev/null +++ b/Documentation/Stereoscopic rendering/index.org @@ -0,0 +1,190 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Stereoscopic Rendering - Aukio 3D +#+LANGUAGE: en +#+LATEX_HEADER: \usepackage[margin=1.0in]{geometry} +#+LATEX_HEADER: \usepackage{parskip} +#+LATEX_HEADER: \setlength{\parindent}{15pt} +#+LATEX_HEADER: \usepackage{palatino} +#+LATEX_HEADER: \usepackage{charter} +#+HTML_HEAD: + +[[file:../index.html#outline-container-understanding-3d-engine][<- Back to index]] + +* What stereo rendering adds +:PROPERTIES: +:CUSTOM_ID: what-stereo-adds +:END: + +A single rendered image is flat: the brain infers depth only from +monocular cues (occlusion, shading, perspective, motion parallax while +you move). *Stereoscopic rendering* adds the strongest depth cue of +all — /binocular disparity/: your two eyes see slightly different +images, and the visual cortex turns the difference into a direct +sensation of depth. + +Aukio 3D implements the simplest and most portable form: *side-by-side +stereo*. Every frame renders the scene twice — once from the left eye +position, once from the right — into the left and right halves of the +same image. A VR headset, 3D TV, or a pair of XR glasses in +side-by-side mode feeds each half to the corresponding eye, and the +scene gains real volume. + +#+CAPTION: A side-by-side stereoscopic frame of the House demo, rendered headlessly with [[file:../index.org::#snapshot][the Snapshot tool]]. Left half: left eye. Right half: right eye. Compare the dark cube and the doorway between the halves — the horizontal shift is the disparity your brain reads as depth. +[[file:stereo-side-by-side.png]] + +* The geometry: two parallel cameras +:PROPERTIES: +:CUSTOM_ID: geometry +:END: + +The two eye cameras are identical to the mono camera except for one +thing: the left eye's position is shifted by -IPD/2 and the right +eye's by +IPD/2 along the *world* X axis (the offset is applied to the +camera translation's =x= component directly, then restored). IPD +(inter-pupillary distance) defaults to *6.5 world units* — the House +demo treats 1 unit as 1 cm, and 6.5 cm is the median human IPD. + +Both cameras look in exactly the same direction (*parallel cameras*, +no toe-in). Objects at different depths then land at different +horizontal offsets between the two images — that offset is the +disparity: + +#+CAPTION: Top-down view: the two eye positions and how a near and a far object project onto the screen plane. The near object separates much more between the eyes than the far one. +[[file:Stereo geometry.svg]] + +- near object -> large disparity -> feels close, +- far object -> small disparity -> feels far, +- object at infinity -> zero disparity. + +Larger IPD exaggerates disparity (stronger but potentially straining +depth); smaller IPD flattens the scene. =+=/=-= keys adjust it live in +0.5-unit steps while stereo is active. + +* One frame = two passes +:PROPERTIES: +:CUSTOM_ID: two-passes +:END: + +Stereo does not add a second pipeline — it runs the existing +triple-buffered pipeline *twice per frame*. The render thread in +=ViewPanel.renderFrame()= executes two render passes back to back: + +#+CAPTION: Per frame, the camera is nudged left, a full transform/sort/bin pass runs for the left viewport, then the camera is nudged right and a second pass runs for the right viewport. Both paint into one shared frame buffer, clipped to their half. +[[file:Stereo pipeline.svg]] + +1. *Pass LEFT:* camera translation.x is temporarily decreased by + IPD/2, the scene is transformed, depth-sorted and tile-binned into a + per-pass =RenderingContext= copy whose viewport is the left half of + the frame (=[0, width/2)=), and the paint continuation is submitted + to the worker pool. +2. *Pass RIGHT:* the same with +IPD/2 and the right viewport + (=[width/2, width)=). The camera offset is always restored in a + =finally= block, so the camera never drifts. +3. The two paints write into *one shared frame buffer* — each clipped + to its half — and the completed side-by-side image is blitted to + the screen in one go. + +The triple-buffer machinery (3 vertex slots, 3 aggregator slots, 3 +framebuffers) does not change: a *pass* takes the slot =passCounter % +3=, so left and right passes of the same frame simply occupy +consecutive slots and overlap exactly like consecutive mono frames do. +Workers flow from one pass's tiles straight into the next pass's tiles +with no idle gap. + +* What adapts per eye +:PROPERTIES: +:CUSTOM_ID: per-eye +:END: + +The scene itself — geometry, textures, lightmaps, global illumination +— is shared and identical for both eyes. Only the *viewpoint* moves, +so only view-dependent stages differ per pass: + +#+CAPTION: The per-eye surface area of the engine. Everything not listed here is eye-independent. +[[file:Stereo per eye.svg]] + +- *Projection:* =Vertex= projects with =projectionScale = eyeWidth/3= + (per-eye horizontal FOV) and adds =stereoViewportOffsetX= so the + projected image lands in the correct half of the buffer. The same + offset is applied for near-plane-clip vertices created directly in + camera space. +- *Frustum culling:* the frustum is rebuilt per pass from + =stereoViewportWidth=, so each eye culls against its own (narrower) + view volume — nothing leaks in from the other eye's half. +- *Painting:* every painter clips X to =[renderMinX, renderMaxX)=, + which the pass set to its viewport. No eye can paint into the other + half, even if a polygon crosses the center line. +- *Mouse picking:* in stereo each eye shows the same object at a + different screen X, so a hit can only be resolved against one eye. + =ViewPanel= combines mouse results only for the pass whose viewport + actually contains the cursor. +- *HUD/overlays:* developer tools, crosshair and text are drawn once + over the finished frame, at zero disparity — they sit on the screen + surface, not in the world. + +* Enabling stereo +:PROPERTIES: +:CUSTOM_ID: enabling +:END: + +#+BEGIN_SRC java +ViewPanel viewPanel = ...; + +// Side-by-side stereo on: +viewPanel.setStereoModeEnabled(true); + +// Optional: match the viewer (default 6.5 world units): +viewPanel.setStereoIPD(6.5); +#+END_SRC + +In the demos, *SHIFT+F11* toggles stereo and fullscreen together (XR +glasses want both); plain *F11* remains fullscreen-only. With stereo +active, *+* and *-* adjust the IPD in 0.5-unit steps (clamped at 0.5) +so the viewer can tune comfort at runtime. + +#+CAPTION: The same scene rendered as a normal mono frame (for comparison with the pair above). Notice there is no horizontal offset to read depth from — the picture is flat. +[[file:mono-comparison.png]] + +* Performance and limitations +:PROPERTIES: +:CUSTOM_ID: limitations +:END: + +- Stereo *doubles the per-frame transform, sort and paint work* — two + full passes instead of one. The pipeline overlaps them the same way + it overlaps consecutive mono frames, so throughput drops less than + 2x on a multi-core machine, but expect a real cost. +- Each eye gets *half the horizontal resolution* of the panel. On a + 1920x1080 fullscreen window each eye sees 960x1080 — pixels are + shared, not duplicated. +- IPD is in *world units*: 6.5 only means "6.5 cm" if the scene is + modeled at 1 unit = 1 cm. In a scene with a different scale, divide + or multiply accordingly — or just tune with =+=/=-= until the depth + feels right. +- The eye offset is applied along the *world X axis*, not the camera's + right vector: it is exactly correct when the camera faces along Z + (yaw = 0) and degrades as you turn — at yaw = 90° the eyes would be + offset front-to-back instead of side-to-side. For a fixed-viewing- + direction demo this is fine; a fully rotational stereo camera would + need to apply the IPD along the rotated right vector. +- Side-by-side is a *display format*, not a headset driver: the engine + produces the image; an XR viewer, 3D TV or video player is + responsible for delivering the halves to the eyes. +- Global illumination is unaffected: lightmaps live on the surfaces, + so both eyes sample the same converged lighting for free. + +* Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Role in stereo rendering | +|----------------------+-----------------------------------------------------------------| +| =ViewPanel= | owns stereoModeEnabled/stereoIPD; runs the two passes per frame | +| =StereoEye= | NONE / LEFT / RIGHT tag carried by each pass context | +| =RenderingContext= | per-eye viewport fields: stereoViewportWidth/OffsetX, renderMin/MaxX | +| =Vertex= | per-eye projection: scale from eye width + viewport X offset | +| =ShapeCollection= | rebuilds the frustum per pass from the eye's viewport width | +| =InputManager= | SHIFT+F11 stereo toggle, +/- live IPD adjustment | + +[[file:../index.html#outline-container-understanding-3d-engine][Back to main documentation]] diff --git a/Documentation/Stereoscopic rendering/mono-comparison.png b/Documentation/Stereoscopic rendering/mono-comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..3b70ee03551760d287111a3dfa1789a373006702 GIT binary patch literal 21058 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 HPhI^#ZPGKv literal 0 HcmV?d00001 diff --git a/Documentation/Winding order.svg b/Documentation/Winding order.svg new file mode 100644 index 0000000..d82048e --- /dev/null +++ b/Documentation/Winding order.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + CCW + + + + V₁ + V₂ + V₃ + FRONT FACE ✓ + + + + + CW + + + BACK FACE ✗ + (culled — not drawn) + diff --git a/Documentation/export-docs.sh b/Documentation/export-docs.sh new file mode 100755 index 0000000..7ef274d --- /dev/null +++ b/Documentation/export-docs.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# export-docs.sh — export all org-mode documentation pages to HTML. +# +# Exports every Documentation/**/index.org (and Documentation/index.org) +# with the darksun theme, using the user's Emacs configuration. Run from +# anywhere: +# +# Documentation/export-docs.sh # export all pages +# Documentation/export-docs.sh --check # export, then render every page with +# # headless Chrome to /tmp/doc-check-*.png +# # for visual inspection +# +# Requires: emacs (with ~/.emacs providing the org HTML setup), +# google-chrome (only for --check). + +set -euo pipefail +DOC_DIR="$(cd "$(dirname "$0")" && pwd)" + +mapfile -t PAGES < <(find "$DOC_DIR" -name index.org | sort) + +echo "Exporting ${#PAGES[@]} pages..." +for page in "${PAGES[@]}"; do + rel="${page#"$DOC_DIR"/}" + if emacs --batch -l ~/.emacs --visit="$page" \ + --funcall=org-html-export-to-html --kill 2>&1 \ + | grep -qi "aborted\|unable to resolve link"; then + echo "FAIL $rel" + exit 1 + fi + echo " ok $rel" +done + +if [[ "${1:-}" == "--check" ]]; then + echo "Rendering pages for visual check..." + for page in "${PAGES[@]}"; do + rel="${page#"$DOC_DIR"/}" + html="${page%.org}.html" + out="/tmp/doc-check-$(echo "$rel" | tr '/ ' '__').png" + google-chrome --headless --disable-gpu --hide-scrollbars \ + --virtual-time-budget=8000 --window-size=1100,2000 \ + --screenshot="$out" "file://$html" 2>/dev/null + echo " shot $out" + done +fi + +echo "Done." diff --git a/Documentation/index.org b/Documentation/index.org new file mode 100644 index 0000000..78e010e --- /dev/null +++ b/Documentation/index.org @@ -0,0 +1,1257 @@ +#+SETUPFILE: ~/.emacs.d/org-styles/html/darksun.theme +#+TITLE: Aukio 3D - Realtime 3D engine +#+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 + +#+HTML_HEAD: + +* Introduction +:PROPERTIES: +:CUSTOM_ID: overview +:ID: a31a1f4d-5368-4fd9-aaf8-fa6d81851187 +:END: + +[[file:Example.png]] + +*Aukio 3D* is a realtime 3D rendering engine written in pure Java. It +runs entirely on the CPU — no GPU required, no OpenGL, no Vulkan, no +native libraries. Just Java. + +The motivation is simple: GPU-based 3D is a minefield of accidental +complexity. Drivers are buggy or missing entirely. Features you need +aren't supported on your target hardware. You run out of GPU RAM. You +wrestle with platform-specific interop layers, shader compilation +quirks, and dependency hell. Every GPU API comes with its own +ecosystem of pain — version mismatches, incomplete implementations, +vendor-specific workarounds. I want a library that "just works". + +*Aukio 3D* takes a different path. By rendering everything in software +on the CPU, the entire GPU problem space simply disappears. You add a +Maven dependency, write some Java, and you have a 3D scene. It runs +wherever Java runs. + +This approach is quite practical for many use-cases. Modern systems +ship with many CPU cores, and those with unified memory architectures +offer high bandwidth between CPU and RAM. Software rendering that once +seemed wasteful is now a reasonable choice where you need good-enough +performance without the overhead of a full GPU pipeline. Java's JIT +compiler helps too, optimizing hot rendering paths at runtime. + +Beyond convenience, CPU rendering gives you complete control. You own +every pixel. You can freely experiment with custom rendering +algorithms, optimization strategies, and visual effects without being +constrained by what a GPU API exposes. Instead of brute-forcing +everything through a fixed GPU pipeline, you can implement clever, +application-specific optimizations. + +*Aukio 3D* is part of the larger [[https://www3.svjatoslav.eu/projects/aukio/][Aukio project]], with the long-term goal +of providing a platform for 3D user interfaces and interactive data +visualization. It can also be used as a standalone 3D engine in any +Java project. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/][demos]] for examples of what it can do today. + +*Major features:* +** Global Illumination +:PROPERTIES: +:CUSTOM_ID: global-illumination +:END: + +#+attr_html: :width 600px +#+attr_latex: :width 600px +[[file:Global illumination/Global illumination.png]] + +On top of flat shading, the engine computes progressive *global +illumination* on background CPU threads: real shadows, smooth light +falloff inside polygons (per-texel lightmaps), and indirect bounce +light — while the render loop itself never traces a single ray. + +Read more about [[file:Global illumination/][global illumination]]. + +** Side-by-side stereoscopic rendering support +:PROPERTIES: +:CUSTOM_ID: stereoscopic +:END: + +#+attr_html: :width 600px +#+attr_latex: :width 600px +[[file:Stereoscopic rendering/stereo-side-by-side.png]] + +The engine can render every frame twice — once per eye — into the left +and right halves of the same image, for XR glasses and 3D displays. +The cameras stay parallel and are offset by a configurable IPD +(inter-pupillary distance). Two render passes share one triple-buffered +pipeline, each clipped to its half of the frame buffer; per-eye +projection, frustum culling and mouse picking adapt automatically. + +See [[file:Stereoscopic%20rendering/][Stereoscopic rendering]] for the +geometry, pipeline and tuning. + +** Constructive Solid Geometry +:PROPERTIES: +:CUSTOM_ID: constructive-solid-geometry +:END: + +#+attr_html: :width 600px +#+attr_latex: :width 600px +[[file:CSG/CSG demo.png]] + +*Aukio 3D* allows performing boolean operations against geometry shapes. +So one can subtract, unionize or intersect shapes. + +To understand CSG boolean operations, read more about [[file:CSG/][Constructive +Solid Geometry]]. + +** SDF textures for sharp text +:PROPERTIES: +:CUSTOM_ID: sdf-text +:END: + +[[file:SDF textures/sdf-angled.png]] + +Text and vector-art surfaces do not store coverage; they store a +*signed distance field* — per texel, the distance to the nearest glyph +edge. The rasterizer re-derives coverage per screen pixel from that +smooth field, so text stays sharp at any zoom and fades to clean gray +under minification, all without a mipmap chain. + +See [[file:SDF%20textures/][SDF textures]] for the glyph pipeline, the +render path, analytic minification and tuning knobs. + +* How take engine into use +:PROPERTIES: +:CUSTOM_ID: taking-engine-into-use +:END: + +Add the *Aukio 3D* dependency to your Maven project: + +#+BEGIN_SRC xml + + + eu.svjatoslav + aukio-3d + 1.0.0 + + +#+END_SRC + +Also add the repository (the library is not on Maven Central): + +#+BEGIN_SRC xml + + + svjatoslav.eu + Svjatoslav repository + https://www3.svjatoslav.eu/maven/ + + +#+END_SRC + +- Library requires Java 21 or newer. + +- Study the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/][demo applications]] for practical examples. Start with the + [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#minimal-example][minimal example]] to see the basic boilerplate needed to render a 3D + scene. + +- Study [[#understanding-3d-engine][how Aukio 3D engine works]]. +- Read online [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/][JavaDoc]]. +- See [[https://www3.svjatoslav.eu/projects/aukio-3d/graphs/][*Aukio 3D* class diagrams]]. (Diagrams were generated by using + [[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility) + +* Essential theory +:PROPERTIES: +:CUSTOM_ID: understanding-3d-engine +:ID: 4b6c1355-0afe-40c6-86c3-14bf8a11a8d0 +:END: +** Coordinate System (X, Y, Z) +:PROPERTIES: +:CUSTOM_ID: coordinate-system +:END: + +#+INCLUDE: "Coordinate system.svg" export html + +*Aukio 3D* uses a **left-handed coordinate system with X pointing right +and Y pointing down**, matching standard 2D screen coordinates. This +coordinate system should feel intuitive for people with preexisting 2D +graphics background. + +| Axis | Direction | Meaning | +|------+------------------------------------+-------------------------------------------| +| X | Horizontal, positive = RIGHT | Objects with larger X appear to the right | +| Y | Vertical, positive = DOWN | Lower Y = higher visually (up) | +| Z | Depth, positive = away from viewer | Negative Z = closer to camera | + +*Practical Examples* + +- A point at =(0, 0, 0)= is at the origin. +- A point at =(100, 50, 200)= is: 100 units right, 50 units down + visually, 200 units away from the camera. +- To place object A "above" object B, give A a **smaller Y value** + than B. + +Coordinates in this system are stored using the +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point3D.html][Point3D]] class — a mutable container with public =x=, =y=, =z= fields +supporting vector operations like distance, rotation, and translation. +Vertices (see [[#vertex][below]]) are positioned within this coordinate system. + +The [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#coordinate-system][aukio-3d-demos]] project includes an interactive +coordinate system reference showing X, Y, Z axes as colored arrows +with a grid plane for spatial context. + +** Point3D and Vertex +:PROPERTIES: +:CUSTOM_ID: vertex +:END: + +#+INCLUDE: "Point3D vertex.svg" export html + +Every 3D object is built from *vertices* — corner points that define +the shape's geometry. A triangle has 3 vertices, a cube has 8, and +complex meshes have thousands. The engine uses two related classes to +represent points in 3D space, each serving a different purpose. + + + +*** Point3D — Raw Coordinates +:PROPERTIES: +:CUSTOM_ID: point3d-raw-coordinates +:END: + +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point3D.html][Point3D]] is the fundamental coordinate type throughout the engine. It +stores a position or vector with three public fields: =x=, =y=, =z=. +The class provides vector math operations: distance calculation, +rotation, translation, scaling, dot/cross products, and interpolation. Methods follow a fluent API convention where mutating +operations (like =add=, =multiply=) return =this= for chaining, while +non-mutating variants (like =withAdded=, =withMultiplied=) return new +instances. + +Use =Point3D= for: +- Storing positions, vectors, or any raw 3D coordinate +- Distance and angle calculations between points +- Vector math (dot product, cross product, normalization) +- Rotating or translating positions before shape construction + +#+BEGIN_SRC java +Point3D p1 = new Point3D(100, 50, 200); +Point3D p2 = new Point3D(0, 0, 100); +double distance = p1.getDistanceTo(p2); // Euclidean distance +Point3D direction = p1.withSubtracted(p2).unit(); // New point: unit vector from p2 to p1 +p1.rotate(new Point3D(0,0,0), Math.PI/4, 0); // Rotate p1 in place, 45° in XZ plane +#+END_SRC + +*** Vertex — Rendering-Ready Coordinates +:PROPERTIES: +:CUSTOM_ID: vertex-rendering-ready-coordinates +:END: + +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.html][Vertex]] wraps a =Point3D= and adds the coordinate spaces needed during +rendering. As a shape transforms through the render pipeline, each +vertex tracks its position in multiple spaces: + +| Field | Purpose | +|------------------------+--------------------------------------------------------| +| =coordinate= | Original position in local/model space | +| =transformedCoordinate(ctx)= | Position relative to camera (after transform stack) | +| =onScreenCoordinate(ctx)= | 2D screen pixels (after perspective projection) | +| =textureCoordinate= | Optional UV coords in pixel units (not normalized) | +| =normal= | Optional normal vector for CSG polygon splitting | + +=transformedCoordinate= and =onScreenCoordinate= are accessor methods, +not plain fields: each vertex carries three slots for each, one per +pipeline projection slot, and the accessor picks the slot of the +context's current render pass. This is what lets the triple-buffered +pipeline transform the next frame while previous frames are still +being painted (see [[file:Rendering loop/index.org::#software-pipeline][software pipeline]]). + +During rendering, the vertex is transformed through all spaces: first +applying the +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/TransformStack.html][TransformStack]] to get the camera-relative coordinate, then +projecting to 2D. Results are cached per frame per slot to avoid +recomputing for vertices shared across multiple shapes. + +Use =Vertex= when: +- Constructing triangles, polygons, or textured shapes +- Your geometry needs texture UV coordinates +- You're performing CSG boolean operations (requires =normal=) + +#+BEGIN_SRC java +// Create a textured triangle (texture coordinates use pixel units) +// For a 256x256 texture: (0,0)=top-left, (256,256)=bottom-right +Vertex v1 = new Vertex(new Point3D(0, 0, 100), new Point2D(0, 0)); +Vertex v2 = new Vertex(new Point3D(100, 0, 100), new Point2D(256, 0)); +Vertex v3 = new Vertex(new Point3D(50, 100, 100), new Point2D(128, 256)); +TexturedTriangle triangle = new TexturedTriangle(v1, v2, v3, texture); +#+END_SRC + +*** When to Use Each +:PROPERTIES: +:CUSTOM_ID: when-to-use-each +:END: + +| Use Point3D | Use Vertex | +|--------------------------------------+-----------------------------------------------| +| Positioning shapes, cameras, lights | Building triangles and polygons | +| Vector math (distances, directions) | Texture-mapped geometry | +| Rotating or translating positions | CSG operations | +| Temporary calculations | Shapes that render through transform pipeline | + +For simple shapes without textures, you can pass raw =Point3D= +coordinates directly to constructors — the shape will internally wrap +them in =Vertex= objects. The [[#coordinate-system][coordinate system]] above defines the +meaning of all =x=, =y=, =z= values in both classes. + +** Edge +:PROPERTIES: +:CUSTOM_ID: edge +:END: + +#+INCLUDE: "Edge.svg" export html + +An *edge* is a straight line segment connecting two [[#vertex][vertices]]. Edges +form the wireframe skeleton of a 3D model — the structural framework +visible when surfaces are not rendered. A triangle has 3 edges, a cube +has 12 edges, and complex meshes have thousands. + +In *Aukio 3D*, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class implements edges as renderable shapes. Each +Line connects two [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.html][Vertex]] endpoints and stores two properties: a +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#width][width]] in world units (adjusted for perspective during rendering) and a +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html#color][color]] with alpha transparency. The rendering algorithm switches +between two modes based on the projected screen width: thin lines below +the threshold are drawn as single pixels with alpha-adjusted coloring, +while thicker lines are rendered as filled rectangles with perspective-correct +edge fading using four [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineInterpolator.html][LineInterpolator]] scanline boundaries. + +Wireframe shapes are composite objects built from multiple Line instances. +For example, [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.html][WireframeBox]] creates 12 Line objects — four edges parallel to +each axis — using a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineAppearance.html][LineAppearance]] factory to ensure consistent styling across +all edges. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.html][WireframeCube]] convenience subclass provides a center-point +constructor. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual comparison of +wireframe (edges only) versus solid polygon (surfaces with lighting) +rendering modes. + +** Face (Triangle) +:PROPERTIES: +:CUSTOM_ID: face-triangle +:END: + +#+INCLUDE: "Face triangle.svg" export html + +A *face* is a flat surface enclosed by edges — the visible skin of a 3D +object. While faces can theoretically have any number of sides, 3D +engines standardize on *triangles* because three points always define a +flat plane. A quad (4 vertices) or pentagon (5 vertices) might be +non-planar depending on vertex positions, causing rendering artifacts. +Triangles avoid this problem entirely. + +*** SolidPolygon — Solid-Color Faces +:PROPERTIES: +:CUSTOM_ID: solidpolygon-solid-color-faces +:END: + +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] is the primary face type, supporting any number of vertices +(3 or more). Triangles render directly via scanline rasterization. +N-vertex polygons (quads, pentagons, etc.) are triangulated using fan +decomposition — a quad becomes 2 triangles, a pentagon 3 — but only +when the polygon lives inside an +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] +(the scene graph root is one): the composite triangulates while +building its render list. A standalone SolidPolygon with more than 3 +vertices cannot be painted directly and throws IllegalStateException. + +Each SolidPolygon stores a single fill color with optional alpha +transparency. When shading is enabled, the lighting manager computes +the polygon's illumination once during the transform phase, then +applies the shaded color during painting. Backface culling (see +[[#winding-order-backface-culling][Winding Order & Backface Culling]]) can be enabled per-polygon, or +applied recursively to an entire composite shape via +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] — this propagates the +setting to all SolidPolygon and TexturedTriangle sub-shapes, including +nested composites. + +#+BEGIN_SRC java +// Create a red triangle +SolidPolygon triangle = SolidPolygon.triangle( + new Point3D(0, 0, 100), + new Point3D(50, 0, 100), + new Point3D(25, 50, 100), + Color.RED +); + +// Create a blue quad (internally triangulated) +SolidPolygon quad = SolidPolygon.quad( + new Point3D(-50, -50, 100), + new Point3D(50, -50, 100), + new Point3D(50, 50, 100), + new Point3D(-50, 50, 100), + Color.BLUE +); + +// Enable lighting and culling for a closed mesh +quad.setShadingEnabled(true); +quad.setBackfaceCulling(true); + +// Add to the scene — the root composite triangulates the quad +viewPanel.getRootShapeCollection().addShape(quad); +#+END_SRC + +*** TexturedTriangle — UV-Mapped Faces +:PROPERTIES: +:CUSTOM_ID: texturedtriangle-uv-mapped-faces +:END: + +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] renders faces with image textures mapped via UV +coordinates. Each of the three [[#vertex][vertices]] stores a =textureCoordinate= +(a [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/geometry/Point2D.html][Point2D]] with U and V values in *pixel units* matching the texture +dimensions). For a 256×256 texture, coordinates range from (0,0) at the +top-left corner to (256,256) at the bottom-right. During rasterization, the +engine interpolates these UV coordinates across the triangle's surface, +sampling the texture at each pixel. When mipmaps are used, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html#multiplicationFactor][multiplicationFactor]] +scales coordinates to match the selected mipmap resolution. + +The texture system supports mipmaps — pre-scaled versions of the texture +selected based on the triangle's screen size to reduce aliasing artifacts +on distant surfaces. Texture coordinates are mapped with +perspective-correct interpolation inside the scanline rasterizer, so +large triangles at steep angles render without distortion — see the +[[file:Perspective correct textures/][perspective-correct textures]] page. + +#+BEGIN_SRC java +// Create a 256x256 texture +Texture texture = new Texture(256, 256, 2); // width, height, maxUpscale + +// Create a textured triangle with UV coordinates in pixel units +Vertex v1 = new Vertex(new Point3D(0, 0, 100), new Point2D(0, 0)); // top-left +Vertex v2 = new Vertex(new Point3D(100, 0, 100), new Point2D(256, 0)); // top-right +Vertex v3 = new Vertex(new Point3D(50, 100, 100), new Point2D(128, 256)); // bottom-center + +TexturedTriangle triangle = new TexturedTriangle(v1, v2, v3, texture); +triangle.setBackfaceCulling(true); +#+END_SRC + +Both SolidPolygon and TexturedTriangle extend +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]], which handles vertex transformation and depth +sorting. See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual comparison of solid +versus textured polygon rendering. + +** Normal Vector +:PROPERTIES: +:CUSTOM_ID: normal-vector +:END: + +#+INCLUDE: "Normal vector.svg" export html + +A *normal* is a vector perpendicular to a surface. It tells the +renderer which direction a face is pointing. Normals are critical for +*lighting* — the angle between the light direction and the normal +determines how bright a surface appears. + +**Use cases:** + +| Use case | API | Computation | Location | +|----------------------+----------------------------------------------+----------------------------+-------------------| +| BSP/CSG operations | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#getPlane()][SolidPolygon.getPlane()]] → [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.html#normal][Plane.normal]] | Lazy-cached once | =Plane= | +| Per-frame shading | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.html#computeNormal()][Plane.computeNormal()]] → =cachedNormal= field | Recomputed every frame | =SolidPolygon= | +| Lighting calculation | [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html#computeLighting()][LightingManager.computeLighting()]] | Uses normal via =dot(L,N)= | =LightingManager= | + +**Implementation notes:** +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.html#computeNormal()][Plane.computeNormal()]]: shared zero-allocation helper for computing normals from three points +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.html][Plane]]: stores normals in Hesse normal form (normal + distance) for BSP spatial partitioning +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.html#normal][Vertex.normal]]: optional field for CSG polygon splitting (not used for rendering) + +** Mesh +:PROPERTIES: +:CUSTOM_ID: mesh +:END: + +#+INCLUDE: "Mesh.svg" export html + +A *mesh* is a collection of vertices, edges, and faces that together +define the shape of a 3D object. Even curved surfaces like spheres are +approximated by many small triangles — more triangles means a smoother +appearance. A cube has 8 vertices forming 12 triangular faces, while a +smooth sphere requires hundreds or thousands of triangles depending on +the desired quality. + +In *Aukio 3D*, meshes are built through composition rather than +monolithic vertex/index buffers. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]] class is +the foundation for primitive shapes — each instance stores its own +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.html#vertices][List<Vertex>]] directly. This includes [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] (N-vertex +convex polygons, not limited to triangles), [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] +(UV-mapped triangles), and [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] (wireframe edges). The +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]] class groups multiple shapes into a single +object with its own position, rotation, and transform — useful for +complex models that move or rotate together. + +Complex meshes are constructed procedurally by adding primitive shapes +during initialization. For example, [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.html][SolidPolygonSphere]] generates +triangles using a latitude-longitude grid: with 16 segments, it +creates 960 SolidPolygon triangles by looping through +rings and sectors, calling [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#addShape(eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape)][addShape()]] for each. The generic +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.html][SolidPolygonMesh]] accepts any list of triangles, allowing custom +geometry from procedural generation or external sources. + +During rendering, several automatic optimizations occur. N-vertex +polygons (quads, pentagons, etc.) are triangulated using fan +triangulation inside the composite's render-list builder, converting +an N-vertex polygon into N-2 triangles. Textured triangles render with +perspective-correct texture mapping (see [[file:Perspective correct textures/][perspective-correct textures]]). +Composites perform view frustum culling to skip rendering when entirely +off-screen. Sub-shapes can be organized into named groups via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/SubShape.html][SubShape]] +wrappers, allowing [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#showGroup(java.lang.String)][showGroup()]] and [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#hideGroup(java.lang.String)][hideGroup()]] to toggle visibility of +entire sections. Composite shapes also support CSG boolean operations +— see the [[file:CSG/][Constructive Solid Geometry]] documentation for union, +subtract, and intersect operations. + +The [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#shape-gallery][Shape Gallery demo]] showcases all primitive shapes available in +*Aukio 3D*, rendered in both wireframe mode (edges only via +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.html][WireframeBox]] and similar) and solid polygon mode (filled surfaces with +dynamic lighting). + +** Working with Colors +:PROPERTIES: +:CUSTOM_ID: working-with-colors +:ID: f2c9642a-a093-444f-8992-76c97ff28c16 +:END: + +Aukio 3D uses its own [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html][Color class]] instead of [[https://docs.oracle.com/en/java/javase/21/docs/api/java.desktop/java/awt/Color.html][java.awt.Color]]. This +custom implementation is designed specifically for the engine's +software rasterizer, where avoiding object allocation during rendering +is critical for performance. When rendering thousands of polygons per +frame, creating new Color instances for each one would generate +excessive garbage and trigger frequent garbage collection +pauses. Instead, the engine's Color class uses mutable fields that can +be reused across frames. + +The class stores RGBA components as public integer fields in the range +0–255. This format matches the engine's pixel buffer layout and avoids +costly float-to-int conversions during rasterization. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#r][r]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#g][g]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#b][b]], and +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#a][a]] fields are accessible directly, allowing lighting calculations and +alpha blending to modify colors in-place without allocating new +objects. For example, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] class maintains a reusable +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][shadedColor]] field that gets updated during each frame's lighting +calculation instead of creating a new Color instance per polygon. + +Color provides several constructors for different input formats. The +most common approach is using hex strings via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#hex(java.lang.String)][Color.hex(String)]] or the +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(java.lang.String)][String constructor]], which support formats like ="F80"= (3-digit RGB), +="FF8800"= (6-digit RGB), ="F808"= (4-digit RGBA), and ="FF8800CC"= +(8-digit RGBA). You can also create colors from integer RGBA +components (0–255) using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(int,int,int,int)][new Color(r, g, b, a)]], from floating-point +components (0.0–1.0) via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(double,double,double,double)][new Color(double r, double g, double b, +double a)]], or from a packed RGB integer like =0xFF8800= using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#%3Cinit%3E(int)][new +Color(int rgb)]]. The class also provides predefined constants for +common colors: [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#RED][Color.RED]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#GREEN][Color.GREEN]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#BLUE][Color.BLUE]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#YELLOW][Color.YELLOW]], +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#CYAN][Color.CYAN]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#MAGENTA][Color.MAGENTA]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#WHITE][Color.WHITE]], [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#BLACK][Color.BLACK]], and +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#TRANSPARENT][Color.TRANSPARENT]]. + +The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#set(int,int,int,int)][set(int r, int g, int b, int a)]] method modifies a Color in-place +and returns =this= for method chaining, which is essential for +performance during rendering. For example, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html][LightingManager]] +calculates lighting contributions from all light sources and stores +the final shaded color directly into a reusable Color instance via +=set()=, avoiding any allocation. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#toAwtColor()][toAwtColor()]] method converts a +Aukio 3D Color to a java.awt.Color when needed for Java2D graphics +operations, caching the result to avoid repeated conversion. The +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#toInt()][toInt()]] method packs the color into an ARGB integer suitable for the +engine's pixel buffer, used during rasterization to write pixels +directly. + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import static eu.svjatoslav.aukio.e3d.renderer.raster.Color.hex; + +// Using predefined color constants +Color red = Color.RED; +Color transparent = Color.TRANSPARENT; + +// Create from hex string (recommended for clarity) +Color orange = hex("FF8800"); // RGB, fully opaque +Color semiTransparent = hex("FF880080"); // RGBA, 50% transparent + +// Create from integer components (0-255) +Color custom = new Color(255, 128, 64, 200); + +// Create from packed RGB integer +Color packed = new Color(0xFF8800); + +// Modify existing color in-place (no allocation) +Color reusable = new Color(); +reusable.set(100, 200, 50, 255); + +// Convert to AWT color for Java2D operations +java.awt.Color awtColor = custom.toAwtColor(); + +// Use in lighting calculations (LightingManager modifies in-place) +// See the Shading & Lighting documentation for details +#+END_SRC + +The alpha component controls transparency during rendering. A value of +0 makes the color fully transparent, while 255 makes it fully +opaque. The rasterizer implements alpha blending during the paint +phase: when drawing a semi-transparent pixel, the engine blends the +source color with the existing background pixel proportionally based +on the alpha value. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.html#drawPixel(int,int\[\],int)][TextureBitmap.drawPixel()]] method handles this +blending, multiplying source colors by alpha and background colors by +=(255 - alpha)=, then combining them. You can test whether a color is +fully transparent using [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/Color.html#isTransparent()][isTransparent()]], which returns true when alpha +equals zero. + +For lighting calculations, the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.html][LightSource]] class uses Color to +represent the color and intensity of emitted light. Multiple light +sources contribute to the final shaded color of each polygon, as +described in the [[file:Shading/index.org::#shading-lighting][Shading & Lighting]] documentation. The [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.html#setAmbientLight(eu.svjatoslav.aukio.e3d.renderer.raster.Color)][ambient light]] +provides base illumination that affects all surfaces equally, +regardless of orientation. Colors are also used for wireframe +rendering via the [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.html][Line]] class, where the color field determines the +line's appearance. + +** Shading & Lighting +:PROPERTIES: +:CUSTOM_ID: shading-lighting +:END: + +#+attr_html: :width 600px +#+attr_latex: :width 600px +[[file:Shading/Shaded%20sphere.png]] + + +*Aukio 3D* implements *flat shading* — one normal per polygon, +computed from the first three vertices. Each polygon receives a single +color based on its orientation relative to light sources. + +To understand lighting and shading, read more about [[file:Shading/][shading & lighting]]. + +* 3D engine internals +:PROPERTIES: +:CUSTOM_ID: engine-internals +:END: +** Main render loop +:PROPERTIES: +:CUSTOM_ID: main-render-loop +:END: + +The rendering loop is the heart of the engine, continuously generating +frames at a target rate (typically 60 FPS). Each frame transforms 3D +shapes through a multi-stage pipeline before displaying them on screen. + +#+INCLUDE: "Rendering loop/Render pipeline.svg" export html + +The render loop runs on a dedicated background daemon thread managed by +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html][ViewPanel]], which can optionally sleep between frames to maintain a +target FPS or run unlimited for benchmarking. + +For a detailed walkthrough of each phase with diagrams and code +examples, see the dedicated page: [[file:Rendering loop/][Rendering loop]]. + +** Near-Plane Clipping +:PROPERTIES: +:CUSTOM_ID: near-plane-clipping +:END: + +Individual polygons that straddle the camera's near plane are not +dropped wholesale: the vertex loop is clipped against the plane, new +intersection vertices are generated with 3D-interpolated UVs and +normals, and the clipped polygon — a triangle can become a quad, +painted as a triangle fan — renders normally. Only polygons entirely +behind the near plane are culled. This keeps floor and wall tiles +visible when the camera brushes against them. + +#+INCLUDE: "Near plane clip/Near plane straddle.svg" export html + +Read more about [[file:Near plane clip/][near-plane clipping]]. + +** Depth buffer +:PROPERTIES: +:CUSTOM_ID: depth-buffer +:END: + +Visibility is resolved per pixel by a depth buffer: every triangle +interpolates =1/z= across its spans and wins a pixel only where it is +nearer than the surface already there. Opaque geometry — textured +triangles, solid polygons — paints front-to-back with depth writes; +translucent geometry paints back-to-front with depth tests but no +writes, so it never occludes. Lines and billboards stay painter-ordered +overlays by design. + +See [[file:Depth%20buffer/][Depth buffer]] for the full treatment. + +** Frustum & View Frustum Culling +:PROPERTIES: +:CUSTOM_ID: frustum-view-frustum-culling +:END: + +*Aukio 3D* implements view frustum culling. + +#+INCLUDE: "Frustum culling/Frustum diagram.svg" export html + +To understand frustum culling and object-level visibility +optimization, read more about [[file:Frustum culling/][frustum & view frustum culling.]] + +** Winding Order & Backface Culling +:PROPERTIES: +:CUSTOM_ID: winding-order-backface-culling +:END: + +#+INCLUDE: "Winding order.svg" export html + +The order in which a triangle's vertices are listed determines its +*winding order*. In *Aukio 3D*, screen coordinates have Y-axis pointing +*down*, which inverts the apparent winding direction compared to +standard mathematical convention (Y-up). *Counter-clockwise (CCW)* in +screen space means front-facing. *Backface culling* skips rendering +triangles that face away from the camera — a major performance +optimization. + +- CCW winding (in screen space) → front face (visible) +- CW winding (in screen space) → back face (culled) +- When viewing a polygon from outside: define vertices in *counter-clockwise* order as seen from the camera +- Saves ~50% of triangle rendering +- Implementation uses signed area: =signedArea < 0= means front-facing + (in Y-down screen coordinates, negative signed area corresponds to + visually CCW winding) + +In *Aukio 3D*, backface culling is *optional* and disabled by default. Enable it per-shape: +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#setBackfaceCulling(boolean)][SolidPolygon.setBackfaceCulling(true)]] +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html#setBackfaceCulling(boolean)][TexturedTriangle.setBackfaceCulling(true)]] +- [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] (applies to all + sub-shapes) + +See the [[https://www3.svjatoslav.eu/projects/aukio-3d-demos/#winding-order][Winding Order demo]] for an interactive visualization. + +** Perspective correct textures +:PROPERTIES: +:CUSTOM_ID: perspective-correct-textures +:END: + +#+attr_html: :class responsive-img +#+attr_latex: :width 1000px +[[file:Perspective correct textures/Affine distortion.png]] + +*Aukio 3D* tries to do perspective-correct texture rendering. Read more +about [[file:Perspective correct textures/][perspective-correct texture implementation]]. + +* Developer tools +:PROPERTIES: +:CUSTOM_ID: developer-tools +:ID: 8c5e2a1f-9d3b-4f6a-b8e7-1c4d5f7a9b2e +:END: + +Press *F12* anywhere in the application to open the Developer Tools +panel: + +#+attr_html: :class responsive-img +#+attr_latex: :width 1000px +[[file:Developer tools/Developer tools.png]] + +This debugging interface helps you understand what the engine is doing +internally and diagnose rendering issues. Pressing F12 again closes +the panel. + +** Diagnostic toggles +:PROPERTIES: +:CUSTOM_ID: diagnostic-toggles +:END: + +*** Show polygon borders +:PROPERTIES: +:CUSTOM_ID: show-polygon-borders +:END: + +When enabled, each [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] draws yellow outlines around its +three edges after rendering its texture content. This overlays the +triangle mesh onto the final image: + +#+attr_html: :class responsive-img +[[file:Developer tools/Render polygon borders.png]] + +Use this visualization when investigating: + +- Mesh structure: see the actual triangles as the rasterizer receives + them +- Geometry bugs: spot T-junction gaps and overlapping geometry +- Texture distortion: compare triangle shapes against visible warping + +*** Render alternate segments (overdraw debug) +:PROPERTIES: +:CUSTOM_ID: render-alternate-segments +:END: + +Renders only even-numbered paint tiles while leaving odd-numbered ones +black. (The screen is divided into a grid of rectangular tiles for +parallel rendering — see [[file:Rendering loop/index.org::#phase-4-clear-paint-tiles][the rendering loop documentation]]. +"Segments" is the older name for tiles.) + +#+attr_html: :class responsive-img +[[file:Developer tools/Render alternative segments.png]] + +This toggle helps detect overdraw: threads writing outside their +allocated tile. If you see rendering artifacts in the black tiles, a +paint task is writing pixels outside its assigned area — a clear sign +of a bug. + +*** Show segment boundaries +:PROPERTIES: +:CUSTOM_ID: show-segment-boundaries +:END: + +Draws red lines along the paint tile boundaries, making it easy to see +exactly where each tile's rendered area begins and ends. In stereo +mode each eye's viewport gets its own grid: + +#+attr_html: :class responsive-img +[[file:Developer tools/Show segment boundaries.png]] + +Useful for: + +- Verifying the tile grid division +- Debugging tile-boundary rendering issues (e.g. clipped text or + missing slivers at tile edges) +- Understanding the parallel rendering architecture visually + +** Camera position +:PROPERTIES: +:CUSTOM_ID: camera-position +:END: + +Displays the current camera coordinates and orientation in real-time: + +| Parameter | Description | +|-----------+------------------------------------------| +| x, y, z | Camera position in 3D world space | +| yaw | Rotation around the Y axis (left/right) | +| pitch | Rotation around the X axis (up/down) | +| roll | Rotation around the Z axis (tilt) | + +The *Copy* button copies the full camera position string to the +clipboard in a format ready to paste into bug reports or configuration +files. + +Use this for: +- Reporting exact camera positions when filing bugs +- Saving interesting viewpoints for later reference +- Understanding camera movement during navigation +- Sharing specific views with other developers + +Example copied format: +#+BEGIN_EXAMPLE +500.00, -300.00, -800.00, 0.60, -0.50, -0.00 +#+END_EXAMPLE + +The six numbers map 1:1 onto +[[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/math/Transform.html#set(double,double,double,double,double,double)][Transform.set(x, y, z, yaw, pitch, roll)]], +so a copied viewpoint can be restored at startup — this is how the +demo applications freeze a good camera position into code: + +#+BEGIN_SRC java +// Camera position captured via Developer Tools -> Copy +viewPanel.getCamera().getTransform().set( + 130.66, -65.49, -248.18, // x, y, z + -0.06, -0.36, -0.00); // yaw, pitch, roll +#+END_SRC + +** Frustum culling statistics +:PROPERTIES: +:CUSTOM_ID: frustum-culling-statistics +:END: + +Shows real-time statistics about composite shape frustum culling +efficiency (see the dedicated [[file:Frustum culling/][frustum culling]] page for how +culling itself works): + +| Statistic | Description | +|-----------+----------------------------------------------------------| +| Total | Number of composite shapes tested against the frustum | +| Culled | Number of composites rejected (outside view frustum) | +| Culled % | Percentage of composites that were culled (0-100%) | + +*How to interpret the numbers:* + +- *High cull % (60-90%)*: Excellent — most objects are being correctly culled +- *Medium cull % (20-60%)*: Moderate — some optimization benefit +- *Low cull % (0-20%)*: Limited benefit — either all objects are visible, or scene needs restructuring + +*Example:* +#+BEGIN_EXAMPLE +Total: 473 Culled: 425 (89.9%) +#+END_EXAMPLE + +This means 473 composite shapes were tested, 425 were outside the view +and skipped entirely, and only 48 composites (with all their children) +actually needed to be rendered. This is excellent culling efficiency. + +The statistics update every 200ms while the panel is open. Note that +the root composite is never frustum-tested (it's always rendered), so +the "Total" count excludes it. + +** Render threads +:PROPERTIES: +:CUSTOM_ID: render-threads +:END: + +Shows the number of active render threads versus available CPU cores. +The engine defaults to 75% of available threads (at most cores − 1, so +one thread always stays free for the rest of the system). The count is +changeable at runtime via [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/gui/ViewPanel.html#setNumRenderThreads(int)][ViewPanel.setNumRenderThreads(int)]]; +the worker pool is recreated lazily on the next frame. + +** Frame rate +:PROPERTIES: +:CUSTOM_ID: frame-rate +:END: + +Shows the current target FPS and the measured production rate (frames +completed per second, averaged over a ~500 ms window). The measured +number counts produced frames regardless of how quickly the display +path presents them — see [[file:Rendering loop/index.org::#frame-rate-control][frame rate control]]. + +The *Unlock FPS* toggle switches to unlimited (benchmark) mode: the +engine renders continuously as fast as possible, even when the scene +is static. Toggling off restores the previously locked target rate. + +** Thread activity timeline +:PROPERTIES: +:CUSTOM_ID: thread-activity-timeline +:END: + +A per-thread occupancy view — the software-renderer equivalent of a +GPU frame profiler. Each thread gets a row (the render thread and +present thread on top, then one row per worker), time runs along the X +axis, and each colored block is one recorded work interval. Idle time +is black. + +#+attr_html: :class responsive-img +[[file:Developer tools/Thread timeline.png]] + +Press *Record* to start capturing. The colors encode both the task +kind and which frame the task belongs to — transform, paint, and +binning come in three frame-parity variants (f0/f1/f2), so you can see +up to three frames in flight simultaneously. Additional colors mark +render-thread orchestration, blocked time, blits, and the sort/drain +sub-phases. + +Navigation: mouse wheel scrolls, Ctrl+wheel zooms, and a scrollbar +moves along the captured range. + +The legend colors, exactly as the timeline paints them: + +| Color | Legend label | What it shows | +|-------+--------------+---------------| +| @@html:@@ =#2ECC40= | =transform f0= | Vertex transform chunk task, frame slot 0 | +| @@html:@@ =#B8D900= | =transform f1= | Vertex transform chunk task, frame slot 1 | +| @@html:@@ =#6B8E23= | =transform f2= | Vertex transform chunk task, frame slot 2 | +| @@html:@@ =#0074D9= | =paint f0= | Paint tile task (clear + rasterize one tile), frame slot 0 | +| @@html:@@ =#F012BE= | =paint f1= | Paint tile task, frame slot 1 | +| @@html:@@ =#B10DC9= | =paint f2= | Paint tile task, frame slot 2 | +| @@html:@@ =#39CCCC= | =bin f0= | Tile binning task (assign sorted shapes to tiles), frame slot 0 | +| @@html:@@ =#008B8B= | =bin f1= | Tile binning task, frame slot 1 | +| @@html:@@ =#007070= | =bin f2= | Tile binning task, frame slot 2 | +| @@html:@@ =#A0A0A0= | =render serial= | Render thread orchestration: tree walk and pass submission | +| @@html:@@ =#8B0000= | =blocked= | Render thread waiting for an older paint pass or the present gate | +| @@html:@@ =#FFFFFF= | =blit= | Present thread copying a finished frame to the screen | +| @@html:@@ =#FF851B= | =sort+bin= | A pass's async continuation as a whole: drain, sort, bin, submit paint | +| @@html:@@ =#8B4513= | =drain= | Continuation sub-phase: await and merge parallel transform chunks | +| @@html:@@ =#FFD700= | =sort= | Continuation sub-phase: depth sort of the pass's shapes | + +The f0/f1/f2 suffixes are the frame's projection slot (frame number +modulo 3) — the triple-buffering from the [[file:Rendering loop/index.org::#software-pipeline][software pipeline]]. +When the pipeline is healthy you see interleaved colors from two or +three frames on the worker rows at once: paint tasks of an older frame +overlapping transform and binning of the newer one. Wide =blocked= +spans on the render row, or worker rows with black gaps, mean the +pipeline is starved rather than busy. + +Recording is cheap but not free (~100 ns per task; a single volatile +read when disabled), so leave it off during benchmarking runs. The +captured intervals live in a fixed-size ring buffer — long recordings +keep only the most recent history. + +What to look for: + +- *Solidly packed worker rows* mean the pipeline is keeping all cores + busy — the design goal (see [[file:Rendering loop/index.org::#software-pipeline][software pipeline]]) +- *Long "blocked" spans on the render row* mean the render thread is + waiting for paint passes — workers are the bottleneck +- *Long "blit" spans on the present row* mean the display path + (X server) is the bottleneck; excess frames are being dropped from + the presentation mailbox + +** 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 +- Auto-scrolls to show latest entries +- Updates every 200ms while panel is open +- Captures logs even when panel is closed (replays when reopened) + +Use the *Clear Logs* button to reset the log buffer for fresh +diagnostic captures. + +** Headless & agentic tooling +:PROPERTIES: +:CUSTOM_ID: headless-agentic +:END: + +For windowless rendering, pixel assertions, golden-image regression +tests and scene dumps — built for automated verification and AI agents — +see [[#agentic-development][agentic development tools]]. + +* Agentic development +:PROPERTIES: +:CUSTOM_ID: agentic-development +:END: + +*Aukio 3D* provides good support for automated AI coding agents +(for example OpenCode, Hermes Agent, etc..). + +Thanks to facilities is =eu.svjatoslav.aukio.e3d.headless= package, an +AI agent can render any scene from any pose, assert what got painted, +compare against committed reference images, and dump the full scene +state for a bug report — all without a window, a display, or the +render thread. + +** One pipeline, two drivers +:PROPERTIES: +:CUSTOM_ID: one-pipeline +:END: + +The key design decision: the headless path drives *the very same +transform → sort → paint pipeline* the on-screen ViewPanel +uses. Nothing is reimplemented, so a passing headless test proves the +real render works, and a bug reproduced headlessly is the real bug. + +#+INCLUDE: "Agentic development/Headless lanes.svg" export html + +Making this possible required three small engine changes: + +- ~ShapeCollection.transformShapes(Camera, RenderingContext)~ — the + transform phase now accepts a camera directly; the ViewPanel variant + just forwards its camera. Headless code never touches Swing. +- ~RenderingContext.getImage()~ — hands out the backing BufferedImage + the rasterizer paints into. +- ~GlobalIllumination.isRunning()~ / ~isConverged()~ / + ~getWorkItemCount()~ — GI state became inspectable. + +** Snapshot: render without a window +:PROPERTIES: +:CUSTOM_ID: snapshot +:END: + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.headless.Snapshot; + +ShapeCollection scene = new ShapeCollection(); +scene.addShape(myShape); +LightingManager lighting = new LightingManager(); +lighting.setAmbientLight(Color.hex("181818")); + +// One call: build context, transform, sort, paint, return the image. +BufferedImage image = Snapshot.render(scene, lighting, + "290.31, -35.59, -2.10, -0.58, -0.15, 0.0", 640, 480); +Snapshot.save(image, "/tmp/snapshot.png"); +#+END_SRC + +The pose string is the *same "x, y, z, yaw, pitch, roll" format the +demos print* and users quote in bug reports — paste the pose, reproduce +the exact view. ~Snapshot.cameraFromPose()~ and ~Snapshot.poseString()~ +convert in both directions. + +For tests that need to detect *unpainted* pixels (holes), the +~renderInto()~ variant fills the background with a caller-chosen +sentinel color first, so "nothing was painted here" is unambiguous even +in a pitch-black scene: + +#+BEGIN_SRC java +RenderingContext ctx = new RenderingContext(640, 480, 1); +ctx.lightingManager = lighting; +Snapshot.renderInto(scene, camera, ctx, 0x00010203); // sentinel +#+END_SRC + +A real headless render — the House demo from a bug-report pose: + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:Agentic development/snapshot-example.png]] + +** PixelAssertions: did this region get painted? +:PROPERTIES: +:CUSTOM_ID: pixel-assertions +:END: + +The recurring debugging question — "did the floor actually render, or +did clipping eat it?" — becomes a library call: + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.headless.PixelAssertions; + +// Fraction of a relative rectangle still equal to the background: +double holes = PixelAssertions.unpaintedFraction(image, 0, + 0.15, 0.45, 0.85, 1.0); // lower-center band +if (holes > 0.05) + throw new AssertionError("floor has holes: " + holes); + +long red = PixelAssertions.countColor(image, 0xFF0000); // flat-color tests +String grid = PixelAssertions.dumpPixelGrid(image, 320, 240, 3, 8); // hex dump +#+END_SRC + +#+INCLUDE: "Agentic development/Pixel assertion.svg" export html + +** GoldenImage: compare against a reference +:PROPERTIES: +:CUSTOM_ID: golden-image +:END: + +A pixel counts as different when any RGB channel drifts more than a +per-channel tolerance; the comparison fails when the fraction of +differing pixels exceeds a threshold. Deterministic flat-shaded renders +can use tight tolerances (4, 0.005); noisier paths relax them. + +#+BEGIN_SRC java +import eu.svjatoslav.aukio.e3d.headless.GoldenImage; + +GoldenImage.Result r = GoldenImage.compare(actual, + new File("goldens/house-flat.png"), 4, 0.005); +if (!r.passed) + GoldenImage.saveDiff(actual, goldenFile, "/tmp/diff.png"); // red = differs +#+END_SRC + +#+INCLUDE: "Agentic development/Golden workflow.svg" export html + +There is also a CLI for shell scripts — exit 0 = match, 1 = differ: + +#+BEGIN_SRC bash +java eu.svjatoslav.aukio.e3d.headless.GoldenImage actual.png golden.png 4 0.005 +#+END_SRC + +A real diff: the house rendered with the living-room lamp removed, +compared against the golden. The red region is exactly the room that +lost its light: + +#+attr_html: :class responsive-img +#+attr_latex: :width 640px +[[file:Agentic development/diff-example.png]] + +** SceneDump: the reproducible bug report +:PROPERTIES: +:CUSTOM_ID: scene-dump +:END: + +One call produces everything needed to reproduce what a frame shows: + +#+BEGIN_SRC java +System.out.println(SceneDump.dump(scene, lighting, camera, gi)); +#+END_SRC + +#+BEGIN_EXAMPLE +== SceneDump == +shapes: 5 top-level, 546 queued for rendering +lights: 4 (ambient #181818) + [0] pos=(-800.0, -240.0, 0.0) color=FFD890 intensity=6.0 + [1] pos=(0.0, -240.0, 0.0) color=D8E4FF intensity=5.0 + [2] pos=(800.0, -240.0, 0.0) color=FFB060 intensity=6.0 + [3] pos=(250.0, -60.0, -250.0) color=60FF90 intensity=2.0 +camera: 290.31, -35.59, -2.10, -0.58, -0.15, -0.00 +GI: running, 152034 work items, converged +#+END_EXAMPLE + +The camera line is a pose string — it feeds straight back into +~Snapshot.render()~. + +** HouseGoldens: ready-made regression tests +:PROPERTIES: +:CUSTOM_ID: house-goldens +:END: + +The aukio-3d-demos repo contains a working example of all of the above: +~eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens~ renders the +House demo at two poses (the default view and the near-plane straddle +bug pose), compares both against committed goldens, and independently +asserts the floor has no holes. + +#+BEGIN_SRC bash +cd aukio-3d-demos +mvn clean package +mvn -q dependency:build-classpath -Dmdep.outputFile=cp.txt +java -cp "target/classes:$(cat cp.txt)" \ + eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens # verify +java -cp "target/classes:$(cat cp.txt)" \ + eu.svjatoslav.aukio.e3d.examples.goldens.HouseGoldens --update # regenerate goldens +#+END_SRC + +Exit code 0 = all pass, 1 = any mismatch (with a diff PNG in /tmp). +Demos that want the same treatment expose their scene construction: +~HouseDemo.buildHouse()~, ~addFurniture()~ and ~addLights()~ are public +for exactly this reason. + +** export-docs.sh: regenerate the documentation +:PROPERTIES: +:CUSTOM_ID: export-docs +:END: + +All engine documentation (the pages you are reading) lives as org-mode +files under =doc/=. One script exports every page to HTML with the +darksun theme: + +#+BEGIN_SRC bash +doc/export-docs.sh # export all pages +doc/export-docs.sh --check # + render every page with headless Chrome + # to /tmp/doc-check-*.png for visual review +#+END_SRC + +The =--check= mode is how an agent verifies its own documentation: SVG +label collisions, broken image links and table breakage all show up in +the rendered screenshots. + +** A typical agent session +:PROPERTIES: +:CUSTOM_ID: typical-session +:END: + +#+BEGIN_EXAMPLE +1. Reproduce: Snapshot.render(scene, lighting, bugReportPose, 640, 480) +2. Inspect: SceneDump.dump(...) + view the PNG +3. Fix the engine +4. Verify: PixelAssertions.unpaintedFraction(...) == 0 +5. Regression: HouseGoldens (must stay ALL PASS) +6. Document: edit doc pages, export-docs.sh --check, review shots +#+END_EXAMPLE + +** Related Classes +:PROPERTIES: +:CUSTOM_ID: related-classes +:END: + +| Class | Purpose | +|-----------------+----------------------------------------------------| +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/Snapshot.html][Snapshot]] | Windowless render facade + pose string conversion | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/PixelAssertions.html][PixelAssertions]] | Painted-region / color-count / hex-grid assertions | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/GoldenImage.html][GoldenImage]] | Golden-PNG comparison, diff writer, CLI | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/headless/SceneDump.html][SceneDump]] | Scene state as a reproducible text block | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.html][ShapeCollection]] | ~transformShapes(Camera, ...)~ headless overload | +| [[https://www3.svjatoslav.eu/projects/aukio-3d/apidocs/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.html][RenderingContext]] | ~getImage()~ exposes the painted frame | + +* Source code +:PROPERTIES: +:CUSTOM_ID: source-code +:ID: 978b7ea2-e246-45d0-be76-4d561308e9f3 +:END: + +*This program is free software: released under Creative Commons Zero +(CC0) license* + +*Program author:* +- Svjatoslav Agejenko +- Homepage: https://svjatoslav.eu +- Email: mailto://svjatoslav@svjatoslav.eu +- See also: [[https://www.svjatoslav.eu/projects/][Other software projects hosted at svjatoslav.eu]] + +*Getting the source code:* +- [[https://www2.svjatoslav.eu/gitweb/?p=aukio-3d.git;a=snapshot;h=HEAD;sf=tgz][Download latest source code snapshot in TAR GZ format]] +- [[https://www2.svjatoslav.eu/gitweb/?p=aukio-3d.git;a=summary][Browse Git repository online]] +- Clone Git repository using command: + : git clone https://www3.svjatoslav.eu/git/aukio-3d.git diff --git a/Documentation/reviews/2026-09-20-codebase-review/aukio-3d-architecture-review.md b/Documentation/reviews/2026-09-20-codebase-review/aukio-3d-architecture-review.md new file mode 100644 index 0000000..883839d --- /dev/null +++ b/Documentation/reviews/2026-09-20-codebase-review/aukio-3d-architecture-review.md @@ -0,0 +1,343 @@ +# Architecture & maintainability review — aukio-3d + +Scope: 152 main-source files, ~31.7k LOC at `/home/n0/workspace/Aukio/aukio-3d`. +Read fully: ViewPanel, TexturedTriangle, AbstractCompositeShape, OctreeVolume, RenderAggregator, +RenderingContext, AbstractCoordinateShape, ShapeCollection, SolidPolygon, LineInterpolator, +PolygonBorderInterpolator, SegmentRenderingContext, AbstractShape; skimmed the rest. +Dead-code claims verified by grep across aukio-3d (main+test), aukio-3d-demos, aukio, +aukio-environment-fo4 (no reflection usage found in consumers; demos touch 64 engine classes, +aukio 24, fo4 17 — the API surface is genuinely exercised). + +--- + +## [HIGH] 1. ViewPanel is a god class — 10 distinct responsibilities + +`gui/ViewPanel.java` (1611 lines). Responsibilities, with evidence: + +1. AWT/Swing integration — `getPreferredSize/getMinimumSize/getMaximumSize` (420-432), + `paint/update` overrides (537-543), `initializeCanvas` (545-556), `addNotify` (558-562), + `ensureBufferStrategy` (564-594). +2. Frame-loop & FPS pacing — `renderLoop` (1444-1456), `maintainTargetFps` (1464-1485), + `ensureThatViewIsUpToDate` (1494-1521), `noteFrameBlitted` (987-1001). +3. Pipeline scheduling (transform/sort/bin/paint triple-buffered passes) — `renderFrame` + (598-652), `transformPass` (1094-1132), `submitPaintPass` (1146-1273), + `flushPendingPaint` (800-877), `flushCompletedPasses` (889-901), `PendingPaint`/`PresentJob` + (240-266). +4. Presentation thread + mailbox — `presentFrame` (671-684), `presentLoop` (703-742), + `blitFrame` (744-791), mailbox fields (198-208), `PRESENT_RATE_LIMIT_FPS` (223-224). +5. Stereo configuration & eye-offset math — fields (1029-1039), `transformPass` IPD offset + (1100-1104), stereo accessors (1046-1080). +6. Thread-pool lifecycle — `getOrCreateTransformExecutor` (1295-1321), + `defaultRenderThreadCount` (1284-1287), plus the dead `renderExecutor` machinery + (see finding 7). +7. Input device hot-plug — `initializeHeadTracking` (1346-1352), `initializeSpaceMouse` + (1360-1366). +8. Developer tools integration — `showDeveloperToolsPanel` (516-535), segment-boundary + overlay drawn inline into the framebuffer (849-869). +9. Lighting & GI lifecycle — `lightingManager` (145), `enableGlobalIllumination` (490-510). +10. Raster helpers that belong to the renderer — `clearSegmentPixels` (911-926), + `combineMouseResults` (928-941). + +Refactor sketch: extract (a) `PipelineScheduler` owning renderFrame/transformPass/ +submitPaintPass/flush* + pendingPaints/passCounter/frameContexts (~550 lines), (b) +`FramePresenter` owning mailbox/presentLoop/blitFrame/PresentJob (~150 lines), (c) +`DeviceHotplug` for headtrack+spacemouse init/stop (~60 lines). ViewPanel keeps AWT glue, +public API delegating to the three. The four participants communicate through +RenderingContext fields that already exist. + +Risk: HIGH but contained to the engine — no rasterizer math touched, so golden pixels are +untouched by construction. The risk is concurrency regression in the pipeline itself; mitigated +by the `aukio3d.pipeline=false` kill switch (232-233) and the existing +ParallelTransformTest/SegmentBinningTest. Move code verbatim first (no logic edits), +verify with the demos' HouseGoldens + Fo4Shot runs. + +## [HIGH] 2. Package structure is fictional — total cycle, renderer↔gui inverted + +The import graph over top-level packages contains every possible cycle: gui↔renderer, +geometry↔renderer, geometry↔gui, math↔gui, math↔geometry (computed over all 152 files). + +Root cause: the renderer's core types live in `gui`: +- `gui/RenderingContext.java`, `gui/SegmentRenderingContext.java`, `gui/HiZPyramid.java`, + `gui/CullingStatistics.java`, `gui/ThreadActivityRecorder.java`. +- 15 renderer files import `gui.*`, e.g. `renderer/raster/RenderAggregator.java:7`, + `renderer/raster/ShapeCollection.java:9-12` (imports gui.CullingStatistics, gui.Camera, + **gui.ViewPanel** — the renderer depends on the AWT Canvas), + `renderer/raster/shapes/basic/texturedpolygon/TriangleMeshBlock.java:6` (gui.HiZPyramid). +- 5 gui files import renderer (ViewPanel:21-23, RenderingContext:11+18, GuiComponent:14-16, + LookAndFeel:7, TextEditComponent:13) → direct gui↔renderer cycle. +- geometry depends upward: `geometry/BspTree.java:7` and `geometry/Plane.java:8` import + renderer...SolidPolygon; `geometry/Frustum.java:7` imports gui.Camera; + `geometry/Point3D.java:7` imports renderer.octree.IntegerPoint (for one convenience + constructor, Point3D.java:121). +- `math/Vertex.java:9` imports gui.RenderingContext. + +Refactor sketch (mechanical, move-only): relocate RenderingContext, SegmentRenderingContext, +HiZPyramid, CullingStatistics, ThreadActivityRecorder, StereoEye to `renderer.raster` (or a new +`renderer.core`); move `gui.Camera`+`gui.ViewSpaceTracker` to geometry/math (Camera is pure +transform state). Break geometry→renderer by moving BspTree to +renderer...shapes.basic.solidpolygon (it stores SolidPolygon) or abstracting its element type. +Fix Point3D→IntegerPoint by moving that constructor to IntegerPoint or a small adapter. +ShapeCollection's ViewPanel dependency is only `viewPanel.getCamera()` +(ShapeCollection.java:302-305) — the Camera overload already exists (314), so the ViewPanel +overload is a convenience that could delegate from gui side or take a Camera. +After moves, the intended order math ← geometry ← renderer ← gui/headless becomes real. + +Risk: LOW for correctness (no code changes, only moves + import updates), MEDIUM for consumers: +three downstream repos must update imports. Public API breakage is the cost; do it in one sweep +with a consumer-side sed. Golden tests unaffected (no FP code moves between classes in a way +that changes execution order — class relocation has no runtime effect). + +## [HIGH] 3. Verified dead code — 6 classes + 2 fields (~340 lines) + +Grepped simple-name references across engine main+test and all three consumer repos: + +- `geometry/Circle.java` (30 LOC) — no references anywhere. +- `gui/ViewUpdateTimerTask.java` (31 LOC) — no references; legacy of the pre-render-thread + design (its `run()` calls the now-package-private `ensureThatViewIsUpToDate`). +- `gui/humaninput/Connexion3D.java` (48 LOC) — standalone /dev/hidraw experiment with its own + `main()`; superseded by `gui/spacemouse/SpaceNavigatorHid`. +- `renderer/octree/raytracer/RayHit.java` (56 LOC) — unreferenced even inside the raytracer. +- `renderer/raster/shapes/composite/solid/SolidPolygonMesh.java` (60 LOC) — unreferenced. +- `renderer/raster/shapes/composite/wireframe/WireframeDrawing.java` (75 LOC) — unreferenced. +- `ViewPanel.renderExecutor` + `ensureExecutorMatchesThreadCount()` (ViewPanel.java:121, + 1327-1339, 1406): created, resized, shut down — but **never used to submit a single task**; + `submitPaintPass` uses `getOrCreateTransformExecutor()` (1155). Legacy of the pre-ForkJoinPool + design. ~35 dead lines including the eager `Executors.newFixedThreadPool(...)` allocation at + construction time (121) — a real thread pool allocated and thrown away per ViewPanel. +- `ViewPanel.renderFrameCount` (596, incremented at 607): static, write-only, never read. + +Refactor sketch: delete all of the above. +Risk: ZERO for the classes/fields (no references exist, no reflection in consumers). Keep a +note that Connexion3D's main() is a hardware experiment if the maintainer wants it archived. + +## [HIGH] 4. AbstractCompositeShape carries an embeddable CSG engine + parallel-transform machinery + +`renderer/raster/shapes/composite/base/AbstractCompositeShape.java` (1295 lines). Responsibilities: + +1. Sub-shape registry + group visibility (88-133, 179-195, 335-377, 756-764). +2. Render-list caching & triangulation (775-821, 886-910). +3. CSG boolean engine (~240 lines, fully self-contained): `extractSolidPolygons` (304-315), + `union` (549-574), `subtract` (597-631), `intersect` (654-683), `clonePolygons` (694-700), + `replaceSolidPolygons` (709-725), `mergeNonPolygonChildrenFrom` (735-748). +4. Bounding-box aggregation (230-280). +5. Frustum culling inline in `transform` (912-997; AABB corner transform at 929-971). +6. Parallel-transform machinery (~280 lines): weight-cache fields (1026-1077), + `getTransformWeight` (1095-1120), `shouldForkTransform` (1132-1140), + `transformChildrenParallel` (1192-1275), constants (1003-1020, 1147). +7. Bulk property fan-out via instanceof chains — 8 sites: `setColor` (397-409), + `setShadingEnabled` (494-504), `setBackfaceCulling` (514-526), + `setMouseInteractionController` (422-432), plus 254, 308-311, 478, 791-807, 851-856. + +Also: `transformChildrenParallel(…, aggregator, …)` takes a **dead parameter** — its own +javadoc admits it (1187-1190: "unused in the parallel path"), and the body never uses it. + +Refactor sketch: (a) move the CSG block to a new `Csg` utility class in the same package — +all methods take/return `List` plus the registry mutations already isolated in +`replaceSolidPolygons`/`mergeNonPolygonChildrenFrom`; AbstractCompositeShape keeps three +one-line delegating methods for API compatibility. (b) Extract the weight/fork machinery into +`ParallelTransformPlanner` (fields + getTransformWeight + shouldForkTransform + chunking), +leaving `transform()` as orchestration. (c) Drop the dead `aggregator` parameter from +`transformChildrenParallel`. Result: ~1295 → ~700 lines. + +Risk: LOW-MEDIUM. CSG is call-graph-isolated (only BspTree + registry), no FP-order-sensitive +output (CSG is geometry, not rasterization — pixel risk none; geometry-identical because the +code moves verbatim). The parallel planner touches concurrency — move verbatim, keep field +semantics; verified by ParallelTransformTest. + +## [MEDIUM] 5. RenderAggregator: cohesive, but three separable machines + +`renderer/raster/RenderAggregator.java` (861 lines). Responsibilities: + +1. Shape queue + merge: `queueShapeForRendering` (703-706), `mergeFrom` (716-720), + `mergeAllParallel` (734-795), `reset` (831-838). +2. Sorting with three strategies (~210 lines): `tryRadixSort` (187-228), `parallelMergeSort` + (235-280), `runSortTasks` (283-308), `mergeRuns` (311-322), `awaitAll` (324-334), driver + `sort` (131-174). +3. Tile binning CSR (~280 lines): fields (400-418), `matchingBinIndex` (427-439), `matchAxis` + (452-463), `binForTiles` (500-523), `buildBins` (543-593), `binPhase` (596-634), + `binRangeCsr` (642-683). +4. Two-pass paint orchestration: `paintSorted` (353-366), `paintRange` (370-397). + +Refactor sketch: extract `ShapeSortMachine` (sort strategies + scratch arrays) and +`TileBinMachine` (CSR fields + build/match), aggregator keeps queue + paint and holds one of +each. Also drop `implements Serializable` on the comparator (844) — nothing serializes it. +Note the sort order contract (Z desc, shapeId asc) is the bit-exactness backbone for +binning/paint; extraction must move code verbatim. + +Risk: LOW (pure control flow, no FP math; order preserved by construction). Verified by +SegmentBinningTest + HouseGoldens. + +## [MEDIUM] 6. Rasterizer duplication: 7 span writers, 3 interpolator classes, 2 blend formulas + +Interpolators — three classes with byte-identical cores: +`LineInterpolator` (solidpolygon), `PolygonBorderInterpolator` (texturedpolygon), +`PerspectiveBorderInterpolator` (texturedpolygon). Identical: `containsY` (LineInterpolator: +84-88 = PolygonBorderInterpolator: 85-89 = PerspectiveBorderInterpolator: 101-105), the +`getX` formula (`round(p1.x + (width*(y-p1.y))/height)`, LineInterpolator:100-105 vs +PolygonBorderInterpolator:129-134), `setPointsZW` (LineInterpolator:130-134 = +PolygonBorderInterpolator:182-186 = PerspectiveBorderInterpolator:137+). + +Span/line writers — 7 sites: TexturedTriangle's `drawHorizontalLinePerspectiveZ` (223-405), +`drawHorizontalLineSdf` (968-1086), `drawHorizontalLinePerspectiveSdf` (1093-1263), +`drawHorizontalLineZ` (1312-1429); SolidPolygon's `drawHorizontalLine` (305-386); Line's two +single-pixel painters (180-233, 242-298). Within TexturedTriangle alone: +- the SDF bilinear-fetch + coverage block is byte-identical twice (1023-1066 vs 1197-1240, ~45 lines); +- the adaptive-interval ladder is byte-identical twice (322-335 vs 1163-1177); +- the edge-pair Y-scan loop appears 4× (698-708, 927-937, 950-960, 1293-1303), a 5th in + SolidPolygon (456-468); +- the yTop/yBottom clamp block appears 2× in-file (467-486 vs 596-615), a 3rd in SolidPolygon + (425-440); +- the swap-left/right + clamp preamble appears 4× (239-260, 986-994, 1117-1127, 1327-1346). + +Two *different* alpha-blend approximations exist: SolidPolygon/Line use +`((dest*bgAlpha) + src*alpha) >> 8` (SolidPolygon:376-378, Line:223-225, 289-291); +TexturedTriangle uses `dest + ((alpha*(src-dest) - dest) >> 8)` (386-388 and 3 more sites). +**They are not pixel-equivalent** — unifying span writers would change golden output. + +Refactor sketch (ordered by safety): +(a) SAFE: unify the three interpolators into one `EdgeInterpolator` — keep the exact FP +expression shapes (`(width * (y - p1.y)) / height`, midpoint for `|height| < EPSILON`), only +collapsing storage. No arithmetic changes → bit-exact. +(b) SAFE: extract the SDF bilinear fetch + coverage evaluation into one private static helper +called from both SDF writers — it is already byte-identical, so extraction cannot change +output if the expression text moves verbatim (watch implicit constant folding: keep `int` +casts and shifts identical). +(c) SAFE: single-source the edge-pair Y-scan loop as a small static helper taking the three +interpolators + a span callback — control-flow only. +(d) UNSAFE without re-golden: merging the two blend formulas. Don't — instead add a comment at +each site naming the formula and why they differ, or standardize deliberately and re-bless +goldens as a separate, announced change. + +Risk: (a)-(c) LOW if done as verbatim text motion (FP op order preserved); (d) is a visible +output change — treat as a feature, not a refactor. HouseGoldens/Fo4Shot are the gate. + +## [MEDIUM] 7. RenderingContext: ~45 fields in the wrong package + +`gui/RenderingContext.java` (707 lines). 43 instance fields + 2 statics; 22 public non-final +(measured). Natural clusters: + +- Framebuffer: bufferedImage (158), pixels (91), depth (99), graphics (78), segmentGraphics + (85), width/height (126/131). +- Projection: centerCoordinate (137), projectionScale (144), nearPlaneDistance (184), + frustum (289), viewerPosition (298). +- Tile/viewport geometry: tilesX/tilesY/viewportCount/numRenderSegments (63-72), + renderMinY/MaxY (150/156, **final**), renderMinX/MaxX (274/281, **mutable** — asymmetric, + mutated post-construction at ViewPanel:1224-1225), stereo* (255-267). +- Pipeline bookkeeping: vertexSlot (176), frameNumber (190), transformCycleId (166), + presentGate (344), depthPass (109), transformExecutor (324), transformCoordinator (333), + lastTransformTaskCount (350). +- Culling: subpixelCullingThreshold/Epoch (198/208), cullingStatistics (306), + occlusionPyramid (316). +- Mouse picking (~120 lines incl. methods 627-705): mouseEvent (222), + objectPreviouslyUnderMouseCursor (213), currentObjectUnderMouseCursor (226), + currentMouseTextureU/V (231-232). +- Services smuggled through: developerTools (237), debugLogBuffer (243), lightingManager (250). + +Belongs elsewhere: mouse-picking state+methods → `MousePickState` (owned by context, one +field); `presentGate` → pipeline glue owned by the scheduler/ViewPanel (it is only written at +ViewPanel:617-619 and awaited in the paint continuation); `depthPass` → RenderAggregator +passes it to shapes via context purely as a side-channel; `lastTransformTaskCount` → +diagnostics bundle; `lightingManager`/`developerTools`/`debugLogBuffer` → a small +`FrameServices` bag would cut constructor/copy-constructor duplication. + +Also note the two copy constructors (442-470, 482-521) enumerate fields by hand and already +diverge (the pass copy omits frustum "by design" — comment 520 — and silently drops +presentGate/lastTransformTaskCount). Every new field must be added in up to 3 places; this is +where the next pipeline bug comes from. + +Refactor sketch: (1) move class to renderer.raster (see finding 2); (2) extract MousePickState +(whole methods move); (3) move presentGate to the scheduler; (4) cluster remaining fields into +final sub-objects (Framebuffer, ViewportGeometry) so the copy constructors shrink to field +copies of immutable parts + shallow shares. + +Risk: LOW-MEDIUM — all mechanical, but the class is read by ~every shape; keep accessors as +delegates so shape code (`renderBuffer.pixels`, `.depth`, `.renderMinX`…) is untouched until a +second pass updates call sites. No FP code → goldens safe. The public-field style means +consumers may read these fields directly; keep the same field names visible during transition. + +## [MEDIUM] 8. OctreeVolume: 560-line copy-paste tracer + fully public internals + +`renderer/octree/OctreeVolume.java` (1102 lines), used only by demos' OctreeDemo. + +- Raw storage exposed: `public int[] cell1..cell8` (42-56), `public int cellAllocationPointer` + (61), `usedCellsCount` (64), `masterCellSize` (67), plus `initWorld` (298) re-allocating the + arrays under any concurrent reader. Invariants (cell state encoding -1/-2, 0 = null child) + are unenforceable. +- `doesIntersect` (133-248): 7 near-identical ~16-line face slabs. +- `traceCell` (512-1100): 8 octant branches × up to 7 recursive-probe stanzas each (~12 lines + per stanza) — ~560 lines of mechanical copy-paste with hand-maintained visit orders + ("// 6 8 3 5 2 4 1", 533). +- `getNewCellPointer` (335-350): linear-scan allocator with wraparound; infinite-loops when the + buffer is full (no exhaustion check) — a latent hang, not just style. + +Refactor sketch: (a) represent the 8 children as `int[][] children` or one `int[8]` per cell +indexed by octant — the putCell sub-cube selection (411-461) and traceCell collapse to loops +over octant index with a precomputed per-ray-octant visit-order table (8 orders × 8 octants, +64 entries, generated once). That alone removes ~500 lines. (b) Encapsulate arrays; add +exhaustion failure in getNewCellPointer (return -1 / throw) instead of an infinite loop. +(c) Optionally extract the ray-trace half (doesIntersect/traceCell) into `OctreeRayWalker`. + +Risk: LOW correctness-wise for (b) and the hang fix; MEDIUM for (a) — visit order affects +*which* cell is returned first for ties, and OctreeDemo visuals depend on it. Not covered by +golden tests (octree is not in the golden harnesses), so verify by side-by-side OctreeDemo +screenshots. This subsystem is demo-only; alternatively quarantine it as-is and spend effort +elsewhere. + +## [LOW] 9. AbstractCoordinateShape: slot-explosion + internal duplication + +- 12 scalar fields + 3 lists encode "screen state × 3 slots" (83-110, 165-175); accessors + repeat the same 3-way ternary 7 times (218-271). +- `setSlotScreenState` (126-148) and the write block inside `transform` (503-521) are the same + 3-way branch twice — `transform` could call `setSlotScreenState(slot, …)` (one-line fix). +- Stale docs: six accessors say "slot (0 or 1)" (226, 236, 246, 256, 266, 276) though slots are + 0/1/2. + +Refactor sketch: introduce `private final ScreenState[] slots = {new ScreenState(), …}` (z, +minY, maxY, minX, maxX, clippedVertices) — collapses 15 fields to 1 array + deletes 7 branchy +accessors. Keep public method signatures. Risk: ZERO pixel risk (pure storage, no FP +expressions), LOW merge risk; internal-only. + +## [LOW] 10. API surface & documentation drift + +- `ViewPanel.setFrameRate` (957) vs `getTargetFPS` (966) — setter/getter name mismatch + (setTargetFPS expected). +- TexturedTriangle javadoc links to methods that no longer exist: `{@link + #drawHorizontalLinePerspective}` (214), `{@link #drawHorizontalLine}` (318, 1090, 1308, + 1354, 1381) — the class has only the …Z/…Sdf variants (verified: no `drawHorizontalLine(` + definition). Javadoc build emits warnings for these. +- Stale pipeline comments on the most subtle code: "Double-buffered frame contexts" (ViewPanel + 160) over a 3-element array (165); "Parity (0/1)" (167) while `frameParity = (frameParity + + 1) % 3` (645); duplicated/orphaned javadoc stub above `getInputManager` (394-402). +- ShapeCollection slot docs say "(0 or 1)" (428, 469); a commented-out code block + TODO + (336-337); slot-0-only helpers (`sortShapes()` 421, `getQueuedShapeCount` 499, + `getBinSizes` 520) beside slot-parameterized siblings — test-only callers, fine, but mark + them as such. +- Direct field poke across classes: `AbstractCompositeShape.setColor` writes + `((Line) shape).color = color` (404) into Line's public field (Line.java:61) instead of a + setter — bypasses any future invalidation logic. +- `RenderingContext.bufferedImageType` (48) — constant not in CONSTANT_CASE. +- `RenderAggregator.ShapesZIndexComparator implements Serializable` (844) — nothing + serializes it; also `import java.io.Serializable` (10). + +Risk: ZERO-LOW. All are comment/name/mechanical fixes; renaming setFrameRate would break +consumers — prefer adding a correctly-named alias and deprecating. + +--- + +## Verdict + +**Yes — the architecture is sound for a software rasterizer of this size.** The load-bearing +structure (transform/sort/bin/paint phases, triple-buffered pipeline with per-slot vertex +state, painter + z-buffer two-pass visibility, Hi-Z, SoA TriangleMeshBlock) is deliberately +designed, and unusually well documented: the comments record *measurements and dates* (e.g. +ViewPanel:113-116, 219-221, 1299-1309; AbstractCompositeShape:1009-1012), which is exactly the +evidence-based culture that keeps a performance codebase honest. The problems are not the +design but **entropy concentrated in identifiable places**: ViewPanel's ten responsibilities, +RenderingContext's 45-field blob sitting in the wrong package (which drags the whole import +graph into cycles), and hand-maintained copy-paste in the span writers and the octree tracer. +None of these threaten correctness today; all of them raise the cost of the *next* change. +The recommended program is: (1) delete the verified dead code (free), (2) package relocation + +RenderingContext/ViewPanel extraction (mechanical, no FP risk), (3) safe de-duplication only — +interpolators, identical SDF block, scan-loop helper — leaving the two blend formulas alone, +(4) extract CSG and the transform planner from AbstractCompositeShape. Every step except +octree rework is gateable by the existing golden harnesses with bit-exact expectations. diff --git a/Documentation/reviews/2026-09-20-codebase-review/aukio-3d-perf-review.md b/Documentation/reviews/2026-09-20-codebase-review/aukio-3d-perf-review.md new file mode 100644 index 0000000..4b8a8f1 --- /dev/null +++ b/Documentation/reviews/2026-09-20-codebase-review/aukio-3d-perf-review.md @@ -0,0 +1,273 @@ +# aukio-3d performance review — what's left on the table + +Read-only static analysis, 2026-09-20. Scope: span writers, AoS-vs-SoA split, sort/bin/transform, +memory layout, threading, SDF text path, GI/octree. All line numbers against current HEAD. + +Known-good state (verified, not re-litigated): parallel chunk transform with pooled scratch, +radix sort over packed long keys, CSR tile bins, tiled MT paint with two-pass z-buffer, +Hi-Z block culling, subpixel epoch cache, TriangleMeshBlock SoA transform loop. + +--- + +## Tier 1 — multi-ms/frame class at 500k queued triangles + +### 1. Radix sort runs single-threaded; the executor handed to `sort()` is ignored in the radix path +`RenderAggregator.sort(ExecutorService)` (`RenderAggregator.java:162-165`): +```java +if (executor != null && sortedCount >= PARALLEL_SORT_THRESHOLD) { + if (!tryRadixSort(sortedArray, sortedCount)) // <- executor NOT passed + parallelMergeSort(sortedArray, sortedCount, comparator, executor); +``` +`tryRadixSort` (`RenderAggregator.java:187-228`) is one serial loop: key build +(`:193-196`, one virtual `getZ(slot)` per shape), then `RadixLongSort.sortPairs` +(`RadixLongSort.java:66-98`) = 8 LSD passes, each streaming 500k×(8B key + 4B idx) read + +12B scatter-write ≈ 96 MB of traffic on ONE core, then a serial random-gather permute +(`:224-226`) plus a full `System.arraycopy` back. This is the measured ~17 ms sort. +Bonus: `tryRadixSort` ends `return true` unconditionally — `parallelMergeSort` is dead code. + +- Why slow: serial memory-bound passes on one core while 17 workers idle (sort sits between + drain and bin in the paint continuation — `ViewPanel.java:1200-1205` — so it directly + delays paint-task submission every pass). +- Change sketch: parallel LSD radix on the same pool — per-chunk 256-bin histograms (parallel), + serial 256×chunks prefix, parallel stable scatter with per-(chunk,digit) offsets. Fixed chunk + boundaries + stability = bit-identical output to today (keys exact, equal-key order preserved, + tie-run shapeId fix unchanged). Also: permute into the *other* grow-only buffer and swap + references instead of `arraycopy` back (saves one 4 MB serial copy). Even simpler first step: + parallelize only the key build by folding it into `mergeAllParallel`'s already-parallel copy + (`RenderAggregator.java:755-790`) — compute `zSortKey` while copying each part. +- Impact class: sort 17 ms → ~4-6 ms wall. The largest single remaining pipeline lever. + +### 2. Per-triangle raster setup is recomputed per overlapped tile, per frame +`TexturedTriangle.paintFlat` (`TexturedTriangle.java:621-628`): +```java +final double edge12 = projectedPoint1.getDistanceTo(projectedPoint2); // sqrt +final double edge13 = projectedPoint1.getDistanceTo(projectedPoint3); // sqrt +final double edge23 = projectedPoint2.getDistanceTo(projectedPoint3); // sqrt +final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d; +final TextureBitmap mipmap = texture.getMipmapForScale(scaleFactor); +``` +plus the perspective setup (`:673-682`, three `1d/z` divides + muls) and the per-span curvature +ladder (`:322-335`, ~7 divides per span). All of this depends only on transform-phase outputs, +yet `paint()` runs once per overlapped tile — a triangle in 4 tiles pays it 4×. +Worse on the object path: `paintTriangle` (`:492-497`) computes the same three `getDistanceTo` +values, then `paintFlat` recomputes them — 6 sqrt per tile-paint for non-SDF triangles. +And `MeshTriangle.paint` (`MeshTriangle.java:109`) calls `block.origTtd(index)` +(`TriangleMeshBlock.java:444-456`) = 3 sqrt over the *final, build-time* `uv[]` array, +recomputed every paint of every tile of every frame. + +- Why slow: sqrt ~15-20c each; at ~150k painted triangles × ~1.5 tiles × (3-6 sqrt + divides) + ≈ 30-60M cycles/frame aggregate paint-side setup that is definitionally redundant. +- Change sketch (bit-exact — same expressions, evaluated once instead of N times): + compute `scaleFactor`/mip level/affine-vs-perspective flag once per triangle per slot in + `TriangleMeshBlock.transform` (mesh path) / `AbstractCoordinateShape.transform` (object path), + stash in per-slot handle state, pass into `paintFlat`. Precompute `origTtd` into a + `double[]` at block build (uv is final). In `paintTriangle`, pass the already-computed + `scaleFactor` down instead of recomputing. +- Impact class: ~1-3 ms/frame aggregate at FO4 queue sizes; larger for big-screen triangles + (text quads, terrain near the camera) that span tens of tiles. + +### 3. Scanline edge evaluation: per-getter divisions + per-scanline edge re-selection +`PerspectiveBorderInterpolator.getSU/getSV/getSW/getZW` (`PerspectiveBorderInterpolator.java:112-148`) +each call `interpolationT()` = `(currentY - p1.y) / height` — a **double division per getter**. +`drawHorizontalLinePerspectiveZ` (`TexturedTriangle.java:233-259`) calls getX + 4 channel getters +per edge per scanline: up to 10 divisions/scanline if C2 doesn't CSE them across the inlined +getters, 4 if it does (getX's `(width * (currentY - p1.y)) / height` is a different expression +than `width*t`, so it never shares). Same shape in `PolygonBorderInterpolator` +(`:98-119,129-134,189-194`) and `LineInterpolator.getX/getZW` (`:100-105,144-149`). +Additionally `containsY` (`PerspectiveBorderInterpolator.java:101-105`) recomputes +`Math.min/max(p1.y,p2.y)` per call, and the triangle y-loop (`TexturedTriangle.java:698-708`) +re-tests 2-3 `containsY` per scanline to re-derive which edge pair is active — when the pair +only changes once, at the middle vertex. + +- Why slow: double div ~13-20c; even at the CSE-friendly 4/scanline that's 50-80c/scanline of + division alone. At 500k queued tris with mean height ~5-15 scanlines, several M scanlines/frame + → multiple ms of pure edge math, concentrated on exactly the small distant triangles that + dominate the FO4 scene. +- Change sketch: + a) bit-exact: cache `minY/maxY` at `setPoints`; make one explicit `t = (currentY-p1.y)/height` + per edge per scanline and pass it to the four channel reads (identical expression → + identical bits, and no longer JIT-CSE-dependent). + b) needs-tolerance: express `x = p1.x + width*t` (removes the second div; differs by ≤1ulp + from `(width*dy)/height` — golden tolerance should absorb, verify). + c) bit-exact with care: split the y-loop into [yTop..yMid] and (yMid..yBottom] halves with the + edge pair fixed per half (the current inclusive `containsY` semantics pin which pair owns + the seam scanline — replicate). Removes ~3 containsY + min/max per scanline. +- Impact class: ~2-5 ms/frame aggregate at small-triangle-heavy views. + +--- + +## Tier 2 — ~0.5-2% frame time each, cheap and safe + +### 4. Depth margin costs 2 muls + 1 sub per pixel even though it defaults to 0 +All four z-buffered span writers test +```java +if (zw > depth[offset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) +``` +`TexturedTriangle.java:356` (perspective-Z), `:1386` (affine-Z), `SolidPolygon.java:354,370`. +`DEPTH_MARGIN_DZ` (`RenderingContext.java:120-121`) parses `-Daukio.zbuffer.margin`, default `"0"`. +It's `static final` so C2 folds the constant to 0.0, but `0.0 * zw * zw` is NOT eliminable +(NaN semantics), so every z-tested pixel pays two dependent muls + a sub for a disabled feature. + +- Change sketch (bit-exact: `0.0*zw*zw == +0.0` and `d - 0.0 == d` exactly for finite zw, which + near-plane clipping guarantees): hoist once per span — + `final boolean strict = DEPTH_MARGIN_DZ == 0;` and branch to a margin-free loop copy, or + duplicate the pixel loops under one perfectly-predicted branch. If margin>0 is ever used, + the quadratic term can also be strength-reduced (`m += (2*c*dzw)*zw + c*dzw*dzw`, both + coefficients span-invariant) — that variant reassociates FP, so tolerance-check it. +- Impact class: ~1-1.5c/pixel on every z-tested pixel; at 2-4M tested pixels/frame ≈ 0.5-1% + frame time, for near-zero code risk. + +### 5. Hi-Z pyramid build is a serial full-depth-buffer sweep on the render thread; `occluded()` is `synchronized` +`ViewPanel.flushPendingPaint` (`:825-829`) calls `HiZPyramid.buildFrom` on the render thread +after each pass. `buildFrom` (`HiZPyramid.java:61-108`) min-pools the entire float depth buffer +(8×8 tiles) single-threaded: 8.3 MB @1080p, ~14.7 MB @1440p, ~58 MB at the 4920×2960 stereo +buffer — ~1-2 ms, up to ~5 ms, serial, before the next transform fork can start. +And `occluded()` (`:121`) is `synchronized` — every `TriangleMeshBlock.transform` on every +parallel chunk thread (`TriangleMeshBlock.java:184-216`) takes the same monitor, serializing +block tests against each other and against the multi-ms build. + +- Change sketch: build level 0 per tile in the paint workers' epilogue (depth just written is + still in L2), reduce upper levels serially (tiny); publish the pyramid as an immutable + per-build snapshot behind a volatile reference and drop `synchronized` from `occluded()` + (reads are all from the snapshot). Telemetry `AtomicLong`s → LongAdder. +- Impact class: ~1-2% @1440p mono, ~5% at 4K-class buffers; removes a contention edge that + scales with block count × worker count. + +### 6. SDF text path: 257×Math.pow + LUT allocation + 2×hypot + 2×log per triangle **per tile** per frame +`TexturedTriangle.paintSdf` (`:789-961`) runs per tile-paint (a text canvas quad overlapping +N tiles paints N times). Per call: +- `Math.hypot` ×2 for footprints (`:820-821`) — hypot pays overflow-safe scaling; + `sqrt(x*x+y*y)` is 3-5× cheaper (≤1ulp difference: tolerance-check), +- auto-gamma does `Math.log` ×2 (`:863-864`), +- when minified (any text past arm's length): `covLut = new int[257]` + 257×`Math.pow` + (`:865-869`). For a terminal-sized canvas (~2 triangles × 50-100 tiles) that's + ~200 × (257 pow + 1 KB alloc) ≈ 2M cycles per frame per canvas. + +- Change sketch: cache the LUT — gamma is a smooth function of `maxFootprint`; quantize gamma + to 1/256 steps and keep a per-thread last-LUT (or a tiny `ConcurrentHashMap`). + Hoist footprint/gamma/aaK to once per triangle per frame (paint-margin-free, so it's + tile-invariant). This is computation caching, not allocation pooling. +- Inner loop: the fixed-point bilinear (`:1037-1038` and `:1211-1212`) uses 8 int muls; the + two-lerp form `top = m00*(256-fx)+m10*fx; bot = m01*(256-fx)+m11*fx; d=(top*(256-fy)+bot*fy)>>16` + is **bit-identical** (integer associativity; worst case 33.4M < 2^31, no overflow) at 4 muls. + Halves the mask-eval multiply count on every text pixel. +- Impact class: workspace/terminal views (aukio TerminalPanel renders through this path): + ~1-3 ms/frame with several text canvases; FO4: negligible. + +--- + +## Tier 3 — smaller or situational + +### 7. Two-pass paint visits every bin entry twice +`RenderAggregator.paintSorted` (`:353-366`) iterates each tile's bin twice; +`paintRange` (`:370-397`) virtual-calls `paint()` on every entry in both passes and the shape +early-outs (`TexturedTriangle.paintFlat:563-565`, `SolidPolygon.paint:703-705`) only after a +volatile texture load + field reads. One of the two visits is always waste: ~queue×tiles +no-op dispatches per frame (~750k at 500k×1.5 tiles ≈ 0.5-1% frame). +- Sketch: split each bin into [opaque|alpha] segments at CSR build time (two counts per tile — + binning order within each class preserved, so pixels are bit-identical); pass 1 walks the + opaque segment reversed, pass 2 the alpha segment forward. + +### 8. Mip-chain lazy build races across paint threads; level selection loops per paint +`Texture.getDownscaledBitmap` (`Texture.java:278-291`) checks `downSampled[i] == null` and +builds unsynchronized while 18 paint threads may first-touch the same level on the same frame +→ duplicated full box-filter chain builds per texture (startup stutter on texture-heavy loads), +and the array-slot write has no happens-before edge. `getDownscaleMipmapLevel` (`:180-189`) +loops ≤8 iterations per triangle per tile-paint. +- Sketch: synchronize per-texture on build (or prebuild chains at texture-load time in the + FO4 `TextureSource`); publish via the array only after construction (final fields make the + bitmap itself safe). Replace the halving loop with an exponent-based level pick + (`Math.getExponent`) — verify threshold equivalence for exact powers of two. + +### 9. Octree ray tracer (OctreeDemo path): allocation-per-face-test and non-parametric traversal +`OctreeVolume.doesIntersect` (`OctreeVolume.java:133-248`) allocates a `new Point3D` (or +`clone()`) on *every* face candidate — up to 6 allocations per cell visit — and +`traceCell` (`:512-1100`) orders children by the **origin's octant**, not by parametric +distance along the ray. Because the first solid hit returns immediately, this is both slower +(no near-first pruning; every level re-runs 6-division slab tests per child from scratch) and +suspect (can return a farther voxel over a nearer one when the origin is outside the parent +cell). `RayTracer.run` (`RayTracer.java:157-175`) allocates a `Ray` + 2 `Point3D` + `Color` +per pixel and does 3 divides/pixel for view-plane interpolation (`(x3p * x) / width` — should +be incremental adds); `traceRay` then fires 6 shadow rays × 2 Point3D + Ray allocations per +light. `getNewCellPointer` (`:335-350`) is a linear scan for a free cell — O(cells) per +allocation, so filling big voxel volumes is O(n²). +- Sketch (only if OctreeDemo matters; nothing in FO4 touches this): slab test with precomputed + per-ray inverse direction, hit point computed once from the winning t (no per-face allocs), + Revelles-style parametric child ordering, free-list for cells. +- Impact: background/progressive demo renderer — not frame-critical. + +### 10. GI package: BVH build and query constant factors +`TriangleBvh.build` (`TriangleBvh.java:92-96`) sorts at every tree level with a comparator → +O(n log² n) build per snapshot rebuild (median selection via quickselect is O(n log n) and +lambda-free). `rayBox` (`:172-198`) does 6 divisionss per node visit + NaN fixup branches — +precompute `1/dx,1/dy,1/dz` once per ray and multiply (not bit-identical to division; GI +output feeds lightmaps, not golden pixels — still verify). `nearestNode` (`:123-140`) visits +left before right unconditionally; ordering children by nearer-box-first makes the `hit.t` +bound prune the far child far more often (typically ~2× fewer node visits on nearest-hit). +`GlobalIllumination.updateComposites` (`:610-637`) fills invalid lightmap texels with a +`while(progressed)` whole-map rescan — O(texels × map diameter); a BFS/multi-source queue is +O(texels). Per-sample `new double[3]` returns (`:537`, `:890`) churn on the GI threads — +background threads, so low stakes, but free to remove. +- Impact: GI convergence latency and background CPU burn only; render path is unaffected + (render-side GI cost is already ~zero by design). + +--- + +## Question 2 answer: how much AoS is left, and is extending SoA worth it? + +FO4 (the 500k-tri target): **~100% mesh blocks.** `NifSceneBuilder.java:169-222` (nif parts) and +`WorldStreamer.java:1309-1348` (terrain cells) both emit `TriangleMeshBlock` by default +(`aukio.fo4.meshBlocks`, default true); the `new TexturedTriangle` fallbacks +(`NifSceneBuilder.java:244`, `WorldStreamer.java:1420`) are behind the same flag. +`SolidPolygon` is imported in WorldStreamer but never instantiated for geometry. + +AoS `Vertex`-graph shapes (SolidPolygon, Line, Billboard, SDF text quads, lightmapped +triangles) carry 8-9 heap objects per vertex with per-slot pointer chasing — they remain in: +- demos/benchmarks: SolidCubesTest 16³ cubes ≈ 49k triangles, LitSolidCubesTest, sphere scenes; +- the Aukio workspace UI (TerminalPanel → TextCanvas → 2 SDF triangles per panel); +- CSG/lightmapped content where per-shape objects are semantically needed. + +Verdict: extending SoA to a `SolidPolygonMeshBlock` would only move the cube/sphere demos — +not the FO4 number. **Not worth it** for the stated target; the remaining AoS cost is in +small-N UI/demo scenes where per-shape setup, not pointer chasing, dominates anyway. + +## Question 4 answer: memory layout + +Pixel buffer and depth buffer are both row-major with identical addressing +(`y*width + x` everywhere) — span writes are unit-stride in both, tile clears use +`Arrays.fill` per row (intrinsic-vectorized). No column-major striding anywhere in the hot +paths. Texture reads (`ity*texW+itx`) stride by texture row — inherent to UV mapping and +already mitigated by the mip chain. The only structural note: pixels and depth are two +separate arrays, so each z-tested pixel write touches two cache lines; interleaving them +would break `BufferedImage` blit interop — not recommended. + +## Question 5 answer: thread utilization + +Tiles = 10×workers, near-square (`ViewPanel.java:1548-1552`, TILES_PER_THREAD=10) with one +task per tile on a work-stealing ForkJoinPool (75% of cores, measured choice) — sound. +Per-pass `CountDownLatch(segments)` is one await per frame, negligible. Two notes: the paint +continuation (`ViewPanel.java:1177-1268`) blocks a worker on `previousGate.await` +(`CountDownLatch` — no FJP compensation) — at most a few parked workers, fine; and the +serial sort inside that continuation is the real utilization gap (finding 1). + +--- + +## Verdict + +The architecture is **not** at the pure-Java ceiling. The big structural pieces are right +(SoA transform, radix keys, CSR bins, tiled two-pass z, Hi-Z), but there is one large and +several medium wins left, all compatible with bit-exactness or within stated tolerance: + +**Top 3:** +1. **Parallelize the radix sort** (it's single-threaded inside a pool it was explicitly + handed): ~17 ms → ~4-6 ms at 500k shapes. `RenderAggregator.java:162-228`. +2. **Hoist per-triangle raster setup out of per-tile paint** (mip metric sqrt×3, `origTtd`, + perspective divides — computed once per slot, not once per tile-overlap): + `TexturedTriangle.java:492-497,621-682`, `MeshTriangle.java:109`. Plus the guaranteed-safe + half of the scanline-divide fix (cached min/max, one explicit shared `t` per edge). +3. **Kill the default-disabled depth-margin math in the four z-span writers** + (strict-loop specialization, bit-exact) — ~0.5-1% of frame for a few lines; + and **de-serialize the Hi-Z build + lock-free `occluded()`** — ~1-2% at 1440p, more at 4K. + +(3 is two cheap items bundled; if forced to pick three single items: parallel radix, setup +hoisting, scanline edge-evaluation restructure.) diff --git a/Documentation/style.css b/Documentation/style.css new file mode 100644 index 0000000..3403e0e --- /dev/null +++ b/Documentation/style.css @@ -0,0 +1,35 @@ +.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; +} + +svg text[fill="#666"], +svg text[fill="#999"] { + fill: #aaa !important; +} + +svg line[stroke="#ccc"] { + stroke: #445566 !important; +} + +svg { + background-color: #061018; + border-radius: 8px; + display: block; + margin: 0 auto; +} \ No newline at end of file diff --git a/TODO.org b/TODO.org new file mode 100644 index 0000000..1e51ee8 --- /dev/null +++ b/TODO.org @@ -0,0 +1,88 @@ +* Demos +:PROPERTIES: +:CUSTOM_ID: demos +:END: +** Add more math formula examples to "Mathematical formulas" demo +:PROPERTIES: +:CUSTOM_ID: add-more-math-formula-examples +:END: + +* Performance +:PROPERTIES: +:CUSTOM_ID: performance +:END: +** Group identical Vertices into one during object slicing +Now system will need to compute each unique point in 3D only +once. Polygons can share coordinates. + +** 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. + +** 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 polygon reduction based on view distance (LOD) +:PROPERTIES: +:CUSTOM_ID: add-polygon-reduction-lod +:END: + +** Compute global illumination at progressive resolutions + +At startup, initially compute global illumination using very coarse +lightmap. Then progressively keep recomputing it with finer and finer +level of detail. Once lightmap stabilizes, stop lightmap computation +until there is change in the scene. + +* Features +:PROPERTIES: +:CUSTOM_ID: features +:END: +** Make it possible to configure field of view (FOV) +** Add collision detection (physics engine) +* Add clickable vertexes +:PROPERTIES: +:CUSTOM_ID: add-clickable-vertexes +:END: + +Circular areas with radius. Can be visible, partially transparent or +invisible. + +Use them in 3D graph demo. Clicking on vertexes should place marker +and information billboard showing values at given XYZ location. + +Add formula textbox display on top of 3D graph. +- Consider making separate formula explorer app where formula will be + editable and there will be gallery of pre-vetted formulas. + - make this app under Aukio parent project. + - Consider integrating with FriCAS or similar CAS software so that + formula parsing and computation happens there. + ++ Study and apply where applicable + ++ 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/ + +* Aukio 3D Demos + +** Text editors demo + ++ Improve focus handling: + + Perhaps add shortcut to navigate world without exiting entire + stack of focus. + + Possibility to retain and reuse recently focused elements. + + Store user location in the world and view direction with the + focused window. So that when returning focus to far away object, + user is redirected also to proper location in the world. + ++ Possibility to store recently visited locations in the world and + return to them. + diff --git a/Tools/Open with IntelliJ IDEA b/Tools/Open with IntelliJ IDEA new file mode 100755 index 0000000..304bf94 --- /dev/null +++ b/Tools/Open with IntelliJ IDEA @@ -0,0 +1,54 @@ +#!/bin/bash + +# This script launches IntelliJ IDEA with the current project +# directory. The script is designed to be run by double-clicking it in +# the GNOME Nautilus file manager. + +# First, we change the current working directory to the directory of +# the script. + +# "${0%/*}" gives us the path of the script itself, without the +# script's filename. + +# This command basically tells the system "change the current +# directory to the directory containing this script". + +cd "${0%/*}" + +# Then, we move up one directory level. +# The ".." tells the system to go to the parent directory of the current directory. +# This is done because we assume that the project directory is one level up from the script. +cd .. + +# Now, we use the 'setsid' command to start a new session and run +# IntelliJ IDEA in the background. 'setsid' is a UNIX command that +# runs a program in a new session. + +# The command 'idea .' opens IntelliJ IDEA with the current directory +# as the project directory. The '&' at the end is a UNIX command that +# runs the process in the background. The '> /dev/null' part tells +# the system to redirect all output (both stdout and stderr, denoted +# by '&') that would normally go to the terminal to go to /dev/null +# instead, which is a special file that discards all data written to +# it. + +setsid idea . &>/dev/null & + +# The 'disown' command is a shell built-in that removes a shell job +# from the shell's active list. Therefore, the shell will not send a +# SIGHUP to this particular job when the shell session is terminated. + +# '-h' option specifies that if the shell receives a SIGHUP, it also +# doesn't send a SIGHUP to the job. + +# '$!' is a shell special parameter that expands to the process ID of +# the most recent background job. +disown -h $! + + +sleep 2 + +# Finally, we use the 'exit' command to terminate the shell script. +# This command tells the system to close the terminal window after +# IntelliJ IDEA has been opened. +exit diff --git a/Tools/Update web site b/Tools/Update web site new file mode 100755 index 0000000..a38f227 --- /dev/null +++ b/Tools/Update web site @@ -0,0 +1,101 @@ +#!/bin/bash +cd "${0%/*}"; if [ "$1" != "T" ]; then gnome-terminal -e "'$0' T"; exit; fi; + +cd .. + +# Function to export org to html using emacs in batch mode +export_org_to_html() { + local org_file=$1 + local dir=$(dirname "$org_file") + local base=$(basename "$org_file" .org) + ( + cd "$dir" || return 1 + local html_file="${base}.html" + if [ -f "$html_file" ]; then + rm -f "$html_file" + fi + echo "Exporting: $org_file → $dir/$html_file" + emacs --batch -l ~/.emacs --visit="${base}.org" --funcall=org-html-export-to-html --kill + if [ $? -eq 0 ]; then + echo "✓ Successfully exported $org_file" + else + echo "✗ Failed to export $org_file" + return 1 + fi + ) +} + +export_org_files_to_html() { + echo "🔍 Searching for .org files in Documentation/ ..." + echo "=======================================" + + mapfile -t ORG_FILES < <(find Documentation -type f -name "*.org" | sort) + + if [ ${#ORG_FILES[@]} -eq 0 ]; then + echo "❌ No .org files found!" + return 1 + fi + + echo "Found ${#ORG_FILES[@]} .org file(s):" + printf '%s\n' "${ORG_FILES[@]}" + echo "=======================================" + + SUCCESS_COUNT=0 + FAILED_COUNT=0 + + for org_file in "${ORG_FILES[@]}"; do + export_org_to_html "$org_file" + if [ $? -eq 0 ]; then + ((SUCCESS_COUNT++)) + else + ((FAILED_COUNT++)) + fi + done + + echo "=======================================" + echo "📊 SUMMARY:" + echo " ✓ Successful: $SUCCESS_COUNT" + echo " ✗ Failed: $FAILED_COUNT" + echo " Total: $((SUCCESS_COUNT + FAILED_COUNT))" + echo "" +} + +build_visualization_graphs() { + rm -rf Documentation/graphs/ + mkdir -p Documentation/graphs/ + + javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d Documentation/graphs/ -n "All classes" -t png -ho + javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d Documentation/graphs/ -n "GUI" -t png -w "eu.svjatoslav.aukio.e3d.gui.*" -ho + javainspect -j target/aukio-3d-*-SNAPSHOT.jar -d Documentation/graphs/ -n "Raster engine" -t png -w "eu.svjatoslav.aukio.e3d.renderer.raster.*" -ho + + meviz index -w Documentation/graphs/ -t "Aukio 3D classes" +} + +# Build project jar file and JavaDocs +mvn clean package + +# Put generated JavaDoc HTML files to documentation directory +rm -rf Documentation/apidocs/ +cp -r target/apidocs/ Documentation/ + +# Publish Emacs org-mode files into HTML format +export_org_files_to_html + +# Generate nice looking code visualization diagrams +build_visualization_graphs + + +## Upload assembled documentation to server +echo "📤 Uploading to server..." +rsync -avz --delete -e 'ssh -p 10006' Documentation/ \ + n0@www3.svjatoslav.eu:/mnt/big/projects/aukio-3d/ + +if [ $? -eq 0 ]; then + echo "✓ Upload completed successfully!" +else + echo "✗ Upload failed!" +fi + +echo "" +echo "Press ENTER to close this window." +read diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9844211 --- /dev/null +++ b/pom.xml @@ -0,0 +1,157 @@ + + 4.0.0 + eu.svjatoslav + aukio-3d + 1.1.0-SNAPSHOT + Aukio 3D + 3D engine + + + 21 + 21 + 21 + UTF-8 + UTF-8 + + + + svjatoslav.eu + https://svjatoslav.eu + + + + + net.java.dev.jna + jna + 5.14.0 + + + + org.yaml + snakeyaml + 2.3 + + + + junit + junit + 4.12 + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + true + UTF-8 + + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.4 + + + attach-javadocs + + jar + + + + + + + + foo + bar + + + + ${java.home}/bin/javadoc + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4.3 + + UTF-8 + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5.2 + + + org.apache.maven.scm + maven-scm-provider-gitexe + 1.9.4 + + + + + + + + org.apache.maven.wagon + wagon-ssh-external + 2.6 + + + + + + + + svjatoslav.eu + svjatoslav.eu + scpexe://svjatoslav.eu:10006/srv/maven + + + svjatoslav.eu + svjatoslav.eu + scpexe://svjatoslav.eu:10006/srv/maven + + + + + + svjatoslav.eu + Svjatoslav repository + https://www3.svjatoslav.eu/maven/ + + + + + scm:git:ssh://n0@svjatoslav.eu:10006/home/n0/git/aukio-3d.git + scm:git:ssh://n0@svjatoslav.eu:10006/home/n0/git/aukio-3d.git + HEAD + + + diff --git a/src/main/java/eu/svjatoslav/aukio/cfg/AukioConfig.java b/src/main/java/eu/svjatoslav/aukio/cfg/AukioConfig.java new file mode 100644 index 0000000..fdc9e2e --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/cfg/AukioConfig.java @@ -0,0 +1,410 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.cfg; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.charset.StandardCharsets; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.error.YAMLException; + +/** + * Shared user configuration for the whole Aukio family (engine, workspace + * app, environments), stored as a single YAML file. + * + *

Default file: {@code ~/.config/aukio/config.yaml} (override with + * {@code -Daukio.config=}; the legacy {@code -De3d.config=} + * is honored as a fallback for engine-only tooling). A missing file + * means empty configuration — every getter falls back to its caller + * supplied default.

+ * + *

Tenancy is by YAML section, flat camelCase keys per section: + * {@code e3d} (engine), {@code fo4} (Fallout 4 environment), {@code app} + * (workspace app; its {@code state} subtree is written by the app at + * runtime). Unknown keys in the file are preserved verbatim.

+ * + *

Freshness: every getter checks the file's (mtime, size, fileKey) + * stamp and re-parses only when it changed, so hand edits made while an + * app is running become visible on the next read. Writes + * ({@code setString} and friends) lock a sidecar {@code .lock}, + * re-read the file under the lock, merge the new key, and atomically + * rename a temp file into place — a hand edit made between the writer's + * read and write survives. All cooperating processes must use this + * class; an editor saving a stale buffer over newer YAML is the one + * race no file format can solve (Emacs's changed-on-disk warning is + * the guard).

+ * + *

Known limitation: SnakeYAML rewrites the file on the first + * {@code set}, so human-written header comments are not preserved + * through app writes — conventions live in each repo's AGENTS.org, not + * in the file.

+ * + *

Instances are cached per resolved path in a per-JVM registry: + * {@link #get()} returns the same instance to every caller in this + * JVM (engine, workspace app, environments), so the stamp cache is + * shared. Separate JVMs each hold their own instance.

+ */ +public final class AukioConfig { + + /** Registry of instances keyed by normalized absolute path. */ + private static final ConcurrentHashMap REGISTRY = + new ConcurrentHashMap<>(); + + /** Instance for the default file, honoring system property overrides. */ + public static AukioConfig get() { + return forFile(System.getProperty("aukio.config", + System.getProperty("e3d.config", + System.getProperty("user.home") + + "/.config/aukio/config.yaml"))); + } + + /** Cached instance for the given file (same path → same instance). */ + public static AukioConfig forFile(final String path) { + return forFile(Paths.get(path)); + } + + /** Cached instance for the given file (same path → same instance). */ + public static AukioConfig forFile(final Path path) { + final Path key = path.toAbsolutePath().normalize(); + return REGISTRY.computeIfAbsent(key.toString(), + k -> new AukioConfig(key)); + } + + private final Path file; + private final Object refreshMonitor = new Object(); + private final Object writeMonitor = new Object(); + private volatile Map snapshot; + private volatile Stamp stamp; + + private AukioConfig(final Path file) { + this.file = file; + } + + /** The file this instance reads and writes. */ + public Path path() { + return file; + } + + /** String value at the dotted path, or {@code fallback} when absent. */ + public String getString(final String dottedPath, final String fallback) { + final Object value = navigate(current(), dottedPath); + return value == null ? fallback : String.valueOf(value); + } + + /** Numeric value at the dotted path, or {@code fallback} when absent/unparseable. */ + public double getDouble(final String dottedPath, final double fallback) { + return parseNumber(getString(dottedPath, null), fallback); + } + + /** Numeric value at the dotted path truncated to int. */ + public int getInt(final String dottedPath, final int fallback) { + return (int) parseNumber(getString(dottedPath, null), fallback); + } + + /** Boolean value at the dotted path, or {@code fallback} when absent. */ + public boolean getBoolean(final String dottedPath, final boolean fallback) { + final String value = getString(dottedPath, null); + return value == null ? fallback + : Boolean.parseBoolean(value.trim()); + } + + /** + * All scalar leaves under a section, flattened to dotted keys + * (e.g. the {@code fo4} section's {@code path} and {@code spawnAt} + * entries). Non-scalar values nested deeper are flattened too; + * absent section yields an empty map. + */ + public Map getStringMap(final String dottedPath) { + final Object value = navigate(current(), dottedPath); + if (!(value instanceof Map)) + return Collections.emptyMap(); + final Map out = new LinkedHashMap<>(); + flatten(castMap(value), "", out); + return Collections.unmodifiableMap(out); + } + + /** Sets a string value, creating intermediate sections as needed. */ + public void setString(final String dottedPath, final String value) { + writeMerge(dottedPath, value); + } + + /** Sets an int value. */ + public void setInt(final String dottedPath, final int value) { + writeMerge(dottedPath, Integer.valueOf(value)); + } + + /** Sets a double value. */ + public void setDouble(final String dottedPath, final double value) { + writeMerge(dottedPath, Double.valueOf(value)); + } + + /** Sets a boolean value. */ + public void setBoolean(final String dottedPath, final boolean value) { + writeMerge(dottedPath, Boolean.valueOf(value)); + } + + // ------------------------------------------------------------------ + // reads + // ------------------------------------------------------------------ + + /** Current snapshot, refreshing from disk when the stamp changed. */ + private Map current() { + Map snap = snapshot; + Stamp st = Stamp.of(file); + if (snap == null || !st.equals(stamp)) { + synchronized (refreshMonitor) { + snap = snapshot; + st = Stamp.of(file); + if (snap == null || !st.equals(stamp)) { + snap = freeze(load(file)); + snapshot = snap; + stamp = st; + } + } + } + return snap; + } + + private static Map load(final Path path) { + if (!Files.isRegularFile(path)) + return new LinkedHashMap<>(); + try { + final Object root = new Yaml().load( + Files.readString(path, StandardCharsets.UTF_8)); + if (root instanceof Map) + return castMap(root); + } catch (final IOException e) { + System.err.println("[CONFIG] could not read " + path + ": " + + e.getMessage()); + } catch (final YAMLException e) { + System.err.println("[CONFIG] could not parse " + path + ": " + + e.getMessage()); + } + return new LinkedHashMap<>(); + } + + private static Object navigate(final Map root, + final String dottedPath) { + final String[] segments = dottedPath.split("\\."); + Map level = root; + for (int i = 0; i < segments.length - 1; i++) { + final Object next = level.get(segments[i]); + if (!(next instanceof Map)) + return null; + level = castMap(next); + } + return level.get(segments[segments.length - 1]); + } + + private static void flatten(final Map in, + final String prefix, + final Map out) { + for (final Map.Entry e : in.entrySet()) { + final String key = prefix.isEmpty() ? e.getKey() + : prefix + "." + e.getKey(); + if (e.getValue() instanceof Map) + flatten(castMap(e.getValue()), key, out); + else + out.put(key, String.valueOf(e.getValue())); + } + } + + // ------------------------------------------------------------------ + // writes + // ------------------------------------------------------------------ + + /** + * Locked read-merge-write: the lock sidecar serializes writers + * across JVMs; the fresh read under the lock folds in any hand + * edits made since our last snapshot; the temp file + atomic move + * keeps readers (which never lock) from ever seeing torn content. + */ + private void writeMerge(final String dottedPath, final Object value) { + final Path lockFile = file.resolveSibling( + file.getFileName() + ".lock"); + try { + final Path parent = file.getParent(); + if (parent != null) + Files.createDirectories(parent); + // FileLock serializes writers across JVMs but throws + // OverlappingFileLockException for threads of the same JVM, + // so writers of this instance first exclude each other here. + synchronized (writeMonitor) { + try (FileChannel channel = FileChannel.open(lockFile, + StandardOpenOption.CREATE, StandardOpenOption.WRITE); + FileLock ignored = channel.lock()) { + final Map data = load(file); + merge(data, dottedPath.split("\\."), 0, value); + final byte[] bytes = dump(data) + .getBytes(StandardCharsets.UTF_8); + final Path tmp = file.resolveSibling( + file.getFileName() + ".tmp-" + + UUID.randomUUID()); + Files.write(tmp, bytes, StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + try { + Files.move(tmp, file, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } catch (final AtomicMoveNotSupportedException e) { + Files.move(tmp, file, + StandardCopyOption.REPLACE_EXISTING); + } + snapshot = freeze(data); + stamp = Stamp.of(file); + } + } + } catch (final IOException e) { + throw new UncheckedIOException("could not write " + file, e); + } + } + + private static void merge(final Map level, + final String[] segments, final int index, + final Object value) { + if (index == segments.length - 1) { + level.put(segments[index], value); + return; + } + final Object next = level.get(segments[index]); + final Map child; + if (next instanceof Map) { + child = castMap(next); + } else { + child = new LinkedHashMap<>(); + level.put(segments[index], child); + } + merge(child, segments, index + 1, value); + } + + private static String dump(final Map data) { + final DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + return new Yaml(options).dump(data); + } + + // ------------------------------------------------------------------ + // snapshot immutability + // ------------------------------------------------------------------ + + private static Map freeze(final Map in) { + final Map out = new LinkedHashMap<>(); + for (final Map.Entry e : in.entrySet()) { + final Object value = e.getValue(); + if (value instanceof Map) + out.put(e.getKey(), freeze(castMap(value))); + else if (value instanceof List) + out.put(e.getKey(), freezeList(castList(value))); + else + out.put(e.getKey(), value); + } + return Collections.unmodifiableMap(out); + } + + private static List freezeList(final List in) { + final List out = new ArrayList<>(in.size()); + for (final Object value : in) { + if (value instanceof Map) + out.add(freeze(castMap(value))); + else if (value instanceof List) + out.add(freezeList(castList(value))); + else + out.add(value); + } + return Collections.unmodifiableList(out); + } + + // ------------------------------------------------------------------ + // small helpers + // ------------------------------------------------------------------ + + private static double parseNumber(final String value, + final double fallback) { + if (value == null || value.isBlank()) + return fallback; + try { + return Double.parseDouble(value.trim()); + } catch (final NumberFormatException e) { + return fallback; + } + } + + @SuppressWarnings("unchecked") + private static Map castMap(final Object map) { + return (Map) map; + } + + @SuppressWarnings("unchecked") + private static List castList(final Object list) { + return (List) list; + } + + /** Identity of a file's content for freshness checks. */ + private static final class Stamp { + private static final Stamp MISSING = new Stamp(-1, -1, null); + + private final long mtimeNanos; + private final long size; + private final Object fileKey; + + private Stamp(final long mtimeNanos, final long size, + final Object fileKey) { + this.mtimeNanos = mtimeNanos; + this.size = size; + this.fileKey = fileKey; + } + + static Stamp of(final Path path) { + try { + final BasicFileAttributes attrs = Files.readAttributes(path, + BasicFileAttributes.class); + return new Stamp( + attrs.lastModifiedTime().to(TimeUnit.NANOSECONDS), + attrs.size(), attrs.fileKey()); + } catch (final NoSuchFileException e) { + return MISSING; + } catch (final IOException e) { + return new Stamp(-2, -2, null); + } + } + + @Override + public boolean equals(final Object other) { + if (this == other) + return true; + if (!(other instanceof Stamp)) + return false; + final Stamp o = (Stamp) other; + return mtimeNanos == o.mtimeNanos && size == o.size + && java.util.Objects.equals(fileKey, o.fileKey); + } + + @Override + public int hashCode() { + return java.util.Objects.hash(mtimeNanos, size, fileKey); + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/cfg/package-info.java b/src/main/java/eu/svjatoslav/aukio/cfg/package-info.java new file mode 100644 index 0000000..68e4ad1 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/cfg/package-info.java @@ -0,0 +1,11 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +/** + * Shared YAML-backed configuration for the whole Aukio family + * (engine, workspace app, environments): one file, per-section + * tenancy, mtime-checked freshness, locked atomic writes. See + * {@link eu.svjatoslav.aukio.cfg.AukioConfig}. + */ +package eu.svjatoslav.aukio.cfg; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/diag/DebugLogBuffer.java b/src/main/java/eu/svjatoslav/aukio/e3d/diag/DebugLogBuffer.java new file mode 100644 index 0000000..ba81b5b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/diag/DebugLogBuffer.java @@ -0,0 +1,99 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.diag; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +/** + * Circular buffer for debug log messages. + * + *

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

+ * + *

This allows capturing early initialization logs before the user opens + * the Developer Tools panel. When the panel is opened, the buffered history + * becomes immediately visible.

+ * + * @see eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel + */ +public class DebugLogBuffer { + + private static final DateTimeFormatter TIME_FORMATTER = + DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + + private final String[] buffer; + private final int capacity; + private volatile int head = 0; + private volatile int count = 0; + + /** + * Creates a new DebugLogBuffer with the specified capacity. + * + * @param capacity the maximum number of log entries to retain + */ + public DebugLogBuffer(final int capacity) { + this.capacity = capacity; + this.buffer = new String[capacity]; + } + + /** + * Logs a message with a timestamp prefix. + * + * @param message the message to log + */ + public void log(final String message) { + final String timestamped = LocalDateTime.now().format(TIME_FORMATTER) + " " + message; + + synchronized (this) { + buffer[head] = timestamped; + head = (head + 1) % capacity; + if (count < capacity) { + count++; + } + } + } + + /** + * Returns all buffered log entries in chronological order. + * + * @return a list of timestamped log entries + */ + public synchronized List getEntries() { + final List entries = new ArrayList<>(count); + + if (count < capacity) { + for (int i = 0; i < count; i++) { + entries.add(buffer[i]); + } + } else { + for (int i = 0; i < capacity; i++) { + final int index = (head + i) % capacity; + entries.add(buffer[index]); + } + } + + return entries; + } + + /** + * Clears all buffered log entries. + */ + public synchronized void clear() { + head = 0; + count = 0; + } + + /** + * Returns the current number of log entries in the buffer. + * + * @return the number of entries + */ + public synchronized int size() { + return count; + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/diag/Diagnostics.java b/src/main/java/eu/svjatoslav/aukio/e3d/diag/Diagnostics.java new file mode 100644 index 0000000..28e86c5 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/diag/Diagnostics.java @@ -0,0 +1,37 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.diag; + +/** + * Facade that wires up Aukio diagnostics: persistent rolling log plus the + * periodic telemetry line. + * + *

Installed automatically when the first {@code ViewPanel} is created, + * or explicitly at application startup ({@code Diagnostics.install()} as + * the first statement of {@code main()}) so early boot output is captured + * too. Disable entirely with {@code -De3d.diagnostics=false}.

+ */ +public final class Diagnostics { + + private static volatile boolean installed = false; + + private Diagnostics() { + } + + /** Installs persistent logging and starts telemetry. Idempotent. */ + public static synchronized void install() { + if (installed) + return; + installed = true; + if ("false".equalsIgnoreCase( + System.getProperty("e3d.diagnostics", "true"))) { + System.out.println("[DIAG] diagnostics disabled" + + " (e3d.diagnostics=false)"); + return; + } + PersistentLog.install(); + Telemetry.start(); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/diag/EngineConfig.java b/src/main/java/eu/svjatoslav/aukio/e3d/diag/EngineConfig.java new file mode 100644 index 0000000..9bf9d3f --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/diag/EngineConfig.java @@ -0,0 +1,88 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.diag; + +import eu.svjatoslav.aukio.cfg.AukioConfig; + +import java.io.File; + +/** + * User configuration for Aukio 3D engine settings. Thin facade over + * {@link AukioConfig}: engine keys live in the {@code e3d} section of + * {@code ~/.config/aukio/config.yaml} (override the file with + * {@code -Daukio.config=}; the legacy {@code -De3d.config=} + * is honored as a fallback). A missing file or key means built-in + * defaults. + * + *

Recognized keys (flat camelCase inside the {@code e3d} section): + * {@code ipdCm} — interpupillary distance in centimeters for + * stereoscopic (3D glasses) rendering, default 6.5; {@code bugReportDir} + * — directory under which bug reports are written, default + * {@code ~/.local/share/aukio/bugreports}; {@code logDir} — directory + * for persistent rolling logs, default {@code ~/.cache/aukio/logs}; + * {@code telemetryIntervalSeconds} — how often the telemetry line is + * written to the log, default 5.

+ * + *

Every key can also be set as a system property + * ({@code -De3d.ipd=6.3}, {@code -De3d.bugreport.dir=...}, + * {@code -De3d.log.dir=...}, {@code -De3d.telemetry.interval=...}); + * system properties win over the config file.

+ */ +public final class EngineConfig { + + /** Default stereo IPD in world units (centimeters). */ + public static final double DEFAULT_IPD_CM = 6.5; + + private EngineConfig() { + } + + /** + * Interpupillary distance for stereo rendering, in world units + * (centimeters). + */ + public static double getIpdCm() { + return parseDouble(System.getProperty("e3d.ipd", + AukioConfig.get().getString("e3d.ipdCm", null)), + DEFAULT_IPD_CM); + } + + /** Directory under which bug reports are written. */ + public static File getBugReportDir() { + return new File(expandHome(System.getProperty("e3d.bugreport.dir", + AukioConfig.get().getString("e3d.bugReportDir", + "~/.local/share/aukio/bugreports")))); + } + + /** Directory for persistent rolling logs. */ + public static File getLogDir() { + return new File(expandHome(System.getProperty("e3d.log.dir", + AukioConfig.get().getString("e3d.logDir", + "~/.cache/aukio/logs")))); + } + + /** Telemetry write interval, seconds. */ + public static int getTelemetryIntervalSeconds() { + return (int) parseDouble(System.getProperty("e3d.telemetry.interval", + AukioConfig.get().getString("e3d.telemetryIntervalSeconds", + null)), 5); + } + + private static double parseDouble(final String value, + final double fallback) { + if (value == null || value.isBlank()) + return fallback; + try { + return Double.parseDouble(value.trim()); + } catch (final NumberFormatException e) { + return fallback; + } + } + + private static String expandHome(final String path) { + if (path.startsWith("~/")) + return System.getProperty("user.home") + path.substring(1); + return path; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/diag/PersistentLog.java b/src/main/java/eu/svjatoslav/aukio/e3d/diag/PersistentLog.java new file mode 100644 index 0000000..fa61506 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/diag/PersistentLog.java @@ -0,0 +1,136 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.diag; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; + +/** + * Persistent rolling log: tees {@code System.out} and {@code System.err} + * into a log file on disk so a hung or crashed session still leaves its + * full output behind. + * + *

The file lives at {@code /aukio.log} and is rotated to + * {@code aukio.log.1} (one backup kept) once it exceeds + * {@value #MAX_LOG_BYTES}. Writes are flushed per line so the last output + * before an OutOfMemory freeze is on disk even when the application can + * no longer react to window events.

+ * + * @see Diagnostics + */ +public final class PersistentLog { + + /** Rotate the log once it exceeds this size. */ + private static final long MAX_LOG_BYTES = 4L * 1024 * 1024; + + private static volatile boolean installed = false; + private static volatile File logFile; + + private PersistentLog() { + } + + /** + * Installs the tee. Idempotent; failures (unwritable directory) fall + * back to console-only logging and are reported on stderr. + */ + public static synchronized void install() { + if (installed) + return; + installed = true; + + final File dir = EngineConfig.getLogDir(); + try { + Files.createDirectories(dir.toPath()); + logFile = new File(dir, "aukio.log"); + final SharedFileOutput shared = new SharedFileOutput(logFile); + System.setOut(new PrintStream( + new TeeStream(System.out, shared), true)); + System.setErr(new PrintStream( + new TeeStream(System.err, shared), true)); + System.out.println("[LOG] persistent log: " + + logFile.getAbsolutePath()); + } catch (final IOException e) { + System.err.println("[LOG] persistent logging unavailable in " + + dir + ": " + e.getMessage()); + logFile = null; + } + } + + /** The current log file, or null when persistent logging failed. */ + public static File getLogFile() { + return logFile; + } + + /** + * Single shared append stream to the log file; both the stdout and + * stderr tees write through it. Rotates the file once it grows past + * {@link #MAX_LOG_BYTES}. + */ + private static final class SharedFileOutput { + + private final File file; + private OutputStream out; + + SharedFileOutput(final File file) throws IOException { + this.file = file; + this.out = new FileOutputStream(file, true); + } + + synchronized void write(final int b) throws IOException { + rotateIfNeeded(); + out.write(b); + if (b == '\n') + out.flush(); + } + + synchronized void write(final byte[] b, final int off, + final int len) throws IOException { + rotateIfNeeded(); + out.write(b, off, len); + out.flush(); + } + + private void rotateIfNeeded() throws IOException { + if (file.length() < MAX_LOG_BYTES) + return; + out.close(); + final File backup = new File(file.getParentFile(), + file.getName() + ".1"); + Files.move(file.toPath(), backup.toPath(), + StandardCopyOption.REPLACE_EXISTING); + out = new FileOutputStream(file, true); + } + } + + /** Mirrors writes to the console and to the shared log file. */ + private static final class TeeStream extends OutputStream { + + private final PrintStream console; + private final SharedFileOutput file; + + TeeStream(final PrintStream console, final SharedFileOutput file) { + this.console = console; + this.file = file; + } + + @Override + public void write(final int b) throws IOException { + console.write(b); + file.write(b); + } + + @Override + public void write(final byte[] b, final int off, final int len) + throws IOException { + console.write(b, off, len); + file.write(b, off, len); + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/diag/Telemetry.java b/src/main/java/eu/svjatoslav/aukio/e3d/diag/Telemetry.java new file mode 100644 index 0000000..6d9ca92 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/diag/Telemetry.java @@ -0,0 +1,105 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.diag; + +import java.lang.management.ManagementFactory; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/** + * Periodic telemetry line written to the persistent log: heap usage plus + * whatever counters registered subsystems report (streaming caches, + * loaded cells, queue depths, measured FPS...). + * + *

Slowdown-toward-OOM issues show up here as a monotonic climb in one + * of the numbers across a flight, which names the leaking resource. The + * line is written every {@code telemetry.interval.seconds} (default 5) + * and goes to stdout — the {@link PersistentLog} tee puts it on disk even + * when the application later freezes.

+ */ +public final class Telemetry { + + private static final DateTimeFormatter TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private static final Map> SOURCES = + new ConcurrentHashMap<>(); + + private static volatile boolean started = false; + + private Telemetry() { + } + + /** + * Registers a named telemetry source. The supplier must be fast and + * side-effect free; it runs on the telemetry thread. Registering the + * same name again replaces the previous source. + * + * @param name short subsystem name, e.g. {@code "fo4"} + * @param source produces a compact stats string, e.g. + * {@code "cells=12 tris=450000"} + */ + public static void registerSource(final String name, + final Supplier source) { + SOURCES.put(name, source); + } + + /** Removes a previously registered source (subsystem shutdown). */ + public static void unregisterSource(final String name) { + SOURCES.remove(name); + } + + /** Starts the daemon telemetry thread. Idempotent. */ + public static synchronized void start() { + if (started) + return; + started = true; + final int intervalSeconds = + Math.max(1, EngineConfig.getTelemetryIntervalSeconds()); + final Thread thread = new Thread(() -> { + while (true) { + try { + Thread.sleep(intervalSeconds * 1000L); + } catch (final InterruptedException e) { + return; + } + try { + System.out.println(snapshotLine()); + } catch (final Throwable t) { + // telemetry must never take the application down + } + } + }, "aukio-telemetry"); + thread.setDaemon(true); + thread.start(); + } + + /** + * Builds the current telemetry line. Also used by bug reports to + * include a final snapshot. + */ + public static String snapshotLine() { + final Runtime rt = Runtime.getRuntime(); + final long used = rt.totalMemory() - rt.freeMemory(); + final StringBuilder sb = new StringBuilder("TELEMETRY "); + sb.append(LocalDateTime.now().format(TIME_FORMATTER)); + sb.append(String.format(" heap=%dM/%dM threads=%d", + used / (1024 * 1024), rt.maxMemory() / (1024 * 1024), + ManagementFactory.getThreadMXBean().getThreadCount())); + for (final Map.Entry> entry + : SOURCES.entrySet()) { + try { + sb.append(' ').append(entry.getKey()).append("={") + .append(entry.getValue().get()).append('}'); + } catch (final Throwable t) { + sb.append(' ').append(entry.getKey()).append("={error}"); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/diag/ThreadActivityRecorder.java b/src/main/java/eu/svjatoslav/aukio/e3d/diag/ThreadActivityRecorder.java new file mode 100644 index 0000000..203d061 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/diag/ThreadActivityRecorder.java @@ -0,0 +1,212 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.diag; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Low-overhead recorder of per-thread work intervals for the thread + * timeline display in the Developer Tools window. + * + *

Task submission sites (transform chunks, paint tiles, tile binning) + * and render-thread phases (orchestration, waiting, blit) record their + * [start, end) intervals here when enabled. The timeline component paints + * each thread as a row, time on the X axis, the interval kind as color — + * the software-renderer equivalent of a GPU frame-profiler occupancy + * view. Idle time is simply the absence of intervals (black).

+ * + *

Overhead when disabled: one volatile read per task. When enabled: + * two {@code System.nanoTime()} calls, one atomic increment and four + * array stores per task (~100 ns), negligible at hundreds of tasks per + * frame.

+ * + *

Frame parity distinguishes "current frame" work from "next frame" + * work in the colors: transform/paint/binning kinds come in an even and + * an odd variant, selected by the parity that was current when the task + * was SUBMITTED (captured at task creation, so overlapping frames keep + * their own color).

+ */ +public final class ThreadActivityRecorder { + + /** Kind base: vertex transform chunk (add frame parity 0..2). */ + public static final int KIND_TRANSFORM = 0; + /** Kind base: paint tile task (add frame parity 0..2). */ + public static final int KIND_PAINT = 3; + /** Kind base: tile binning task (add frame parity 0..2). */ + public static final int KIND_BIN = 6; + /** Kind: render thread orchestration (tree walk, submission, merges). */ + public static final int KIND_RENDER = 9; + /** Kind: render thread blocked waiting for worker tasks. */ + public static final int KIND_AWAIT = 10; + /** Kind: render thread blitting the finished frame to screen. */ + public static final int KIND_BLIT = 11; + /** Kind: a pass's asynchronous continuation (drain, sort, bin, paint submission). */ + public static final int KIND_PREP = 12; + /** Kind: continuation sub-phase: draining and merging transform chunks. */ + public static final int KIND_DRAIN = 13; + /** Kind: continuation sub-phase: depth sort. */ + public static final int KIND_SORT = 14; + + /** Number of distinct kinds (three parities each for transform/paint/bin, plus 9..14). */ + public static final int KIND_COUNT = 15; + + private static final int CAPACITY = 1 << 18; + private static final int MASK = CAPACITY - 1; + + private static final long[] starts = new long[CAPACITY]; + private static final long[] ends = new long[CAPACITY]; + private static final byte[] kinds = new byte[CAPACITY]; + private static final byte[] rows = new byte[CAPACITY]; + private static final AtomicInteger cursor = new AtomicInteger(); + + private static volatile boolean enabled = false; + private static volatile int frameParity = 0; + + private static final ConcurrentHashMap rowByThreadName = new ConcurrentHashMap<>(); + private static final CopyOnWriteArrayList rowNames = new CopyOnWriteArrayList<>(); + private static final AtomicInteger nextRow = new AtomicInteger(); + + private ThreadActivityRecorder() { + } + + /** + * @return true when recording is active (checked by task submission sites) + */ + public static boolean isEnabled() { + return enabled; + } + + /** + * Enables or disables recording. Enabling starts with a clean buffer + * and fresh thread-row assignment. + * + * @param value true to start recording + */ + public static void setEnabled(final boolean value) { + if (value && !enabled) { + clear(); + } + enabled = value; + } + + /** + * Drops all recorded intervals and thread-row assignments. + */ + public static void clear() { + cursor.set(0); + rowByThreadName.clear(); + rowNames.clear(); + nextRow.set(0); + // starts[]==0 marks an empty slot for the timeline sweep + java.util.Arrays.fill(starts, 0L); + } + + /** + * Sets the parity (0/1) of the frame currently being prepared. + * Called by the render thread at the start of each frame; task + * submission sites capture it into their tasks so overlapping frames + * keep distinct colors. + * + * @param parity frame parity, 0 or 1 + */ + public static void setFrameParity(final int parity) { + frameParity = parity; + } + + /** + * @return parity of the frame currently being prepared + */ + public static int frameParity() { + return frameParity; + } + + /** + * Records one work interval on the calling thread. + * + * @param kind interval kind (one of the KIND_* bases, plus parity + * for transform/paint/binning) + * @param t0 interval start, from {@link System#nanoTime()} + * @param t1 interval end, from {@link System#nanoTime()} + */ + public static void record(final int kind, final long t0, final long t1) { + // Rows are keyed by thread NAME, not Thread object: after a + // stop()/start() cycle the executor is recreated with fresh + // threads under the same names, and they must reuse the same + // rows — otherwise the timeline fills with dead threads' rows + // and pushes the live workers below the visible area. + final String threadName = Thread.currentThread().getName(); + Integer row = rowByThreadName.get(threadName); + if (row == null) { + row = rowByThreadName.computeIfAbsent(threadName, t -> { + final int r = nextRow.getAndIncrement(); + rowNames.add(t); + return r; + }); + } + if (row > 127) { + return; + } + final int i = cursor.getAndIncrement() & MASK; + starts[i] = t0; + ends[i] = t1; + kinds[i] = (byte) kind; + rows[i] = (byte) (int) row; + } + + // ---- Snapshot access for the timeline component ---- + + /** @return total number of intervals recorded since the last clear */ + public static int cursor() { + return cursor.get(); + } + + /** @return ring buffer capacity */ + public static int capacity() { + return CAPACITY; + } + + /** @return interval start array (index space of the ring buffer) */ + public static long[] starts() { + return starts; + } + + /** @return interval end array (index space of the ring buffer) */ + public static long[] ends() { + return ends; + } + + /** @return interval kind array (index space of the ring buffer) */ + public static byte[] kinds() { + return kinds; + } + + /** @return interval thread-row array (index space of the ring buffer) */ + public static byte[] rows() { + return rows; + } + + /** @return number of distinct thread rows seen so far */ + public static int rowCount() { + return nextRow.get(); + } + + /** + * @param row thread row index + * @return display name for the row ("render" for the render thread, + * otherwise the thread name with the e3d- prefix stripped) + */ + public static String rowName(final int row) { + if (row >= rowNames.size()) { + return "?"; + } + final String name = rowNames.get(row); + if ("e3d-render".equals(name)) { + return "render"; + } + return name.startsWith("e3d-") ? name.substring(4) : name; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Box.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Box.java new file mode 100644 index 0000000..7ae2362 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Box.java @@ -0,0 +1,216 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.geometry; + +import static java.lang.Math.abs; + +/** + * A 3D axis-aligned bounding box defined by two corner points. + * + *

Also known as: 3D rectangle, rectangular box, rectangular parallelepiped, + * cuboid, rhomboid, hexahedron, or rectangular prism.

+ * + *

The box is defined by two points ({@link #p1} and {@link #p2}) that represent + * opposite corners. The box does not enforce ordering of these points.

+ * + *

Example usage:

+ *
{@code
+ * Box box = new Box(new Point3D(0, 0, 0), new Point3D(100, 50, 200));
+ * double volume = box.getWidth() * box.getHeight() * box.getDepth();
+ * box.enlarge(10);  // expand by 10 units in all directions
+ * }
+ * + * @see Point3D + */ +public class Box implements Cloneable { + + /** + * The first corner point of the box. + */ + public final Point3D p1; + /** + * The second corner point of the box (opposite corner from p1). + */ + public final Point3D p2; + + /** + * Creates a new box with both corner points at the origin. + */ + public Box() { + p1 = new Point3D(); + p2 = new Point3D(); + } + + /** + * Creates a new box with the specified corner points. + * + * @param p1 the first corner point + * @param p2 the second corner point (opposite corner) + */ + public Box(final Point3D p1, final Point3D p2) { + this.p1 = p1; + this.p2 = p2; + } + + + /** + * Enlarges the box by the specified border in all directions. + * + * @param border The border to enlarge the box by. + * If the border is negative, the box will be shrunk. + * @return The current box. + */ + public Box enlarge(final double border) { + + if (p1.x < p2.x) { + p1.translateX(-border); + p2.translateX(border); + } else { + p1.translateX(border); + p2.translateX(-border); + } + + if (p1.y < p2.y) { + p1.translateY(-border); + p2.translateY(border); + } else { + p1.translateY(border); + p2.translateY(-border); + } + + if (p1.z < p2.z) { + p1.translateZ(-border); + p2.translateZ(border); + } else { + p1.translateZ(border); + p2.translateZ(-border); + } + + return this; + } + + /** + * Creates a copy of this box with cloned corner points. + * + * @return a new box with the same corner coordinates + */ + @Override + public Box clone() { + return new Box(p1.clone(), p2.clone()); + } + + /** + * Returns the depth of the box (distance along the Z-axis). + * + * @return the depth (always positive) + */ + public double getDepth() { + return abs(p1.z - p2.z); + } + + /** + * Returns the height of the box (distance along the Y-axis). + * + * @return the height (always positive) + */ + public double getHeight() { + return abs(p1.y - p2.y); + } + + /** + * Returns the width of the box (distance along the X-axis). + * + * @return the width (always positive) + */ + public double getWidth() { + return abs(p1.x - p2.x); + } + + + /** + * Sets the size of the box. The box will be centered at the origin. + * Previous size and position of the box will be lost. + * + * @param size {@link Point3D} specifies box size in x, y and z axis. + */ + public void setBoxSize(final Point3D size) { + p2.clone(size).divide(2); + p1.clone(p2).negate(); + } + + /** + * Returns the minimum X coordinate of this box. + * Useful for AABB intersection tests. + * + * @return the smaller X value of p1 and p2 + */ + public double getMinX() { + return Math.min(p1.x, p2.x); + } + + /** + * Returns the maximum X coordinate of this box. + * Useful for AABB intersection tests. + * + * @return the larger X value of p1 and p2 + */ + public double getMaxX() { + return Math.max(p1.x, p2.x); + } + + /** + * Returns the minimum Y coordinate of this box. + * Useful for AABB intersection tests. + * + * @return the smaller Y value of p1 and p2 + */ + public double getMinY() { + return Math.min(p1.y, p2.y); + } + + /** + * Returns the maximum Y coordinate of this box. + * Useful for AABB intersection tests. + * + * @return the larger Y value of p1 and p2 + */ + public double getMaxY() { + return Math.max(p1.y, p2.y); + } + + /** + * Returns the minimum Z coordinate of this box. + * Useful for AABB intersection tests. + * + * @return the smaller Z value of p1 and p2 + */ + public double getMinZ() { + return Math.min(p1.z, p2.z); + } + + /** + * Returns the maximum Z coordinate of this box. + * Useful for AABB intersection tests. + * + * @return the larger Z value of p1 and p2 + */ + public double getMaxZ() { + return Math.max(p1.z, p2.z); + } + + /** + * Returns the geometric center of this box. + * + * @return a new Point3D at the center of the box + */ + public Point3D getCenter() { + return new Point3D( + (p1.x + p2.x) / 2.0, + (p1.y + p2.y) / 2.0, + (p1.z + p2.z) / 2.0 + ); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Camera.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Camera.java new file mode 100644 index 0000000..c4c534b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Camera.java @@ -0,0 +1,243 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.geometry; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.math.Matrix3x3; +import eu.svjatoslav.aukio.e3d.math.Quaternion; +import eu.svjatoslav.aukio.e3d.math.Transform; + +/** + * Represents the viewer's camera in the 3D world, with position, orientation, and movement. + * + *

The camera is the user's "eyes" in the 3D scene. It has a position (location), + * a looking direction (defined by a quaternion), and a movement system with + * velocity, acceleration, and friction for smooth camera navigation.

+ * + *

By default, the user can navigate using arrow keys (handled by + * {@link eu.svjatoslav.aukio.e3d.gui.humaninput.WorldNavigationUserInputTracker}), + * and the mouse controls the look direction (handled by + * {@link eu.svjatoslav.aukio.e3d.gui.humaninput.InputManager}).

+ * + *

Programmatic camera control:

+ *
{@code
+ * Camera camera = viewPanel.getCamera();
+ *
+ * // Set camera position
+ * camera.getTransform().setTranslation(new Point3D(0, -50, -200));
+ *
+ * // Set camera orientation using a quaternion
+ * camera.getTransform().getRotation().set(Quaternion.fromAngles(0.5, -0.3));
+ *
+ * // Copy camera state from another camera
+ * Camera snapshot = new Camera(camera);
+ * }
+ * + * @see eu.svjatoslav.aukio.e3d.gui.ViewPanel#getCamera() + * @see eu.svjatoslav.aukio.e3d.gui.humaninput.WorldNavigationUserInputTracker default keyboard navigation + */ +public class Camera { + + /** + * Camera movement speed limit, relative to the world. When camera coordinates are + * updated within the world, camera orientation relative to the world is + * taken into account. + */ + public static final double SPEED_LIMIT = 30; + + /** World units moved per millisecond per unit of velocity. + * Public: direct-drive controllers (SpaceMouse) reuse it to match + * the keyboard/mouse feel. */ + public static final double SPEED_MULTIPLIER = .02d; + /** + * Determines amount of friction user experiences every millisecond while moving around in space. + */ + private static final double MILLISECOND_FRICTION = 1.005; + /** + * Camera movement speed, relative to camera itself. When camera coordinates + * are updated within the world, camera orientation relative to the world is + * taken into account. + */ + private final Point3D movementVector = new Point3D(); + private final Point3D previousLocation = new Point3D(); + /** + * Camera acceleration factor for movement speed. Higher values result in faster acceleration. + */ + public double cameraAcceleration = 0.1; + /** + * The transform containing camera location and orientation. + */ + private final Transform transform; + + /** + * Creates a camera at the world origin with no rotation. + */ + public Camera() { + transform = new Transform(); + } + + /** + * Creates a copy of an existing camera, cloning its position and orientation. + * + * @param sourceView the camera to copy + */ + public Camera(final Camera sourceView) { + transform = sourceView.getTransform().clone(); + } + + /** + * Creates a camera with the specified transform (position and orientation). + * + * @param transform the initial transform defining position and rotation + */ + public Camera(final Transform transform){ + this.transform = transform; + } + + /** + * Per-frame camera physics tick (movement integration, friction). + * Registered on the panel via a {@code FrameListener} adapter in + * {@code ViewPanel} — decoupled from the gui interface so the camera + * stays in the geometry package. + * + * @param millisecondsSinceLastFrame frame delta in milliseconds + * @return true when the camera moved enough to need a repaint + */ + public boolean onFrame(final int millisecondsSinceLastFrame) { + + previousLocation.clone(transform.getTranslation()); + translateCameraLocationBasedOnMovementVector(millisecondsSinceLastFrame); + applyFrictionToMovement(millisecondsSinceLastFrame); + return isFrameRepaintNeeded(); + } + + private boolean isFrameRepaintNeeded() { + final double distanceMoved = transform.getTranslation().getDistanceTo(previousLocation); + return distanceMoved > 0.03; + } + + /** + * Clamps the camera's movement speed to {@link #SPEED_LIMIT}. + * Called after modifying the movement vector to prevent excessive velocity. + */ + public void enforceSpeedLimit() { + final double currentSpeed = movementVector.getVectorLength(); + + if (currentSpeed <= SPEED_LIMIT) + return; + + movementVector.divide(currentSpeed / SPEED_LIMIT); + } + + /** + * Returns the current movement velocity vector, relative to the camera's orientation. + * Modify this vector to programmatically move the camera. + * + * @return the movement vector (mutable reference) + */ + public Point3D getMovementVector() { + return movementVector; + } + + /** + * Returns the current movement speed (magnitude of the movement vector). + * + * @return the scalar speed value + */ + public double getMovementSpeed() { + return movementVector.getVectorLength(); + } + + /** + * Apply friction to camera movement vector. + * + * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate. + * Therefore, we take frame rendering time into account when translating + * camera between consecutive frames. + */ + private void applyFrictionToMovement(int millisecondsPassedSinceLastFrame) { + for (int i = 0; i < millisecondsPassedSinceLastFrame; i++) + applyMillisecondFrictionToUserMovementVector(); + } + + /** + * Apply friction to camera movement vector. + */ + private void applyMillisecondFrictionToUserMovementVector() { + movementVector.x /= MILLISECOND_FRICTION; + movementVector.y /= MILLISECOND_FRICTION; + movementVector.z /= MILLISECOND_FRICTION; + } + + /** + * Translate coordinates based on camera movement vector and camera orientation in the world. + * + * @param millisecondsPassedSinceLastFrame We want camera movement to be independent of framerate. + * Therefore, we take frame rendering time into account when translating + * camera between consecutive frames. + */ + private void translateCameraLocationBasedOnMovementVector(int millisecondsPassedSinceLastFrame) { + final Matrix3x3 m = transform.getRotation().toMatrix(); + + final double forwardX = m.m20; + final double forwardY = m.m21; + final double forwardZ = m.m22; + + final double rightX = m.m00; + final double rightY = m.m01; + final double rightZ = m.m02; + + final Point3D location = transform.getTranslation(); + final double ms = millisecondsPassedSinceLastFrame; + + location.x += forwardX * movementVector.z * SPEED_MULTIPLIER * ms; + location.y += forwardY * movementVector.z * SPEED_MULTIPLIER * ms; + location.z += forwardZ * movementVector.z * SPEED_MULTIPLIER * ms; + + location.x += rightX * movementVector.x * SPEED_MULTIPLIER * ms; + location.y += rightY * movementVector.x * SPEED_MULTIPLIER * ms; + location.z += rightZ * movementVector.x * SPEED_MULTIPLIER * ms; + + location.y += movementVector.y * SPEED_MULTIPLIER * ms; + } + + /** + * Returns the transform containing this camera's location and orientation. + * + * @return the transform (mutable reference) + */ + public Transform getTransform() { + return transform; + } + + /** + * Orients the camera to look at a target point in world coordinates. + * + *

Calculates the required XZ and YZ rotation angles to point the camera + * from its current position toward the target. Useful for programmatic + * camera control, cinematic sequences, and following objects.

+ * + *

Example:

+ *
{@code
+     * Camera camera = viewPanel.getCamera();
+     * camera.getTransform().setTranslation(new Point3D(100, -50, -200));
+     * camera.lookAt(new Point3D(0, 0, 0));  // Point camera at origin
+     * }
+ * + * @param target the world-space point to look at + */ + public void lookAt(final Point3D target) { + final Point3D pos = transform.getTranslation(); + final double dx = target.x - pos.x; + final double dy = target.y - pos.y; + final double dz = target.z - pos.z; + + final double angleXZ = -Math.atan2(dx, dz); + final double horizontalDist = Math.sqrt(dx * dx + dz * dz); + final double angleYZ = -Math.atan2(dy, horizontalDist); + + transform.getRotation().set(Quaternion.fromAngles(angleXZ, angleYZ)); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/IntegerPoint.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/IntegerPoint.java new file mode 100644 index 0000000..955267b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/IntegerPoint.java @@ -0,0 +1,39 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.geometry; + +/** + * Point in 3D space with integer coordinates. Used for octree voxel positions. + */ +public class IntegerPoint +{ + /** X coordinate. */ + public int x; + /** Y coordinate. */ + public int y; + /** Z coordinate. */ + public int z = 0; + + /** + * Creates a point at the origin (0, 0, 0). + */ + public IntegerPoint() + { + } + + /** + * Creates a point with the specified coordinates. + * + * @param x the X coordinate + * @param y the Y coordinate + * @param z the Z coordinate + */ + public IntegerPoint(final int x, final int y, final int z) + { + this.x = x; + this.y = y; + this.z = z; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point2D.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point2D.java new file mode 100755 index 0000000..7dc2004 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point2D.java @@ -0,0 +1,313 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.geometry; + +import static java.lang.Math.sqrt; + +/** + * A mutable 2D point or vector with double-precision coordinates. + * + *

{@code Point2D} represents either a position in 2D space or a directional vector, + * with public {@code x} and {@code y} fields for direct access. It is commonly used + * for screen-space coordinates after 3D-to-2D projection.

+ * + *

All mutation methods return {@code this} for fluent chaining:

+ *
{@code
+ * Point2D p = new Point2D(10, 20)
+ *     .multiply(2.0)
+ *     .add(new Point2D(5, 5))
+ *     .negate();
+ * // p is now (-25, -45)
+ * }
+ * + *

Mutability convention:

+ *
    + *
  • Imperative verbs ({@code add}, {@code subtract}, {@code negate}, {@code multiply}, + * {@code divide}) mutate this point and return {@code this}
  • + *
  • {@code with}-prefixed methods ({@code withAdded}, {@code withSubtracted}, {@code withNegated}, + * {@code withMultiplied}, {@code withDivided}) return a new point without modifying this one
  • + *
+ * + *

Warning: This class is mutable with public fields. Clone before storing + * references that should not be shared:

+ *
{@code
+ * Point2D safeCopy = original.clone();
+ * }
+ * + * @see Point3D the 3D equivalent + */ +public class Point2D implements Cloneable { + + /** X coordinate (horizontal axis). */ + public double x; + /** Y coordinate (vertical axis, positive = down in screen space). */ + public double y; + + /** + * Creates a point at the origin (0, 0). + */ + public Point2D() { + } + + /** + * Creates a point with the specified coordinates. + * + * @param x the X coordinate + * @param y the Y coordinate + */ + public Point2D(final double x, final double y) { + this.x = x; + this.y = y; + } + + /** + * Creates a point by copying coordinates from another point. + * + * @param parent the point to copy from + */ + public Point2D(final Point2D parent) { + x = parent.x; + y = parent.y; + } + + + /** + * Adds another point to this point in place. + * This point is modified, the other point is not. + * + * @param otherPoint the point to add + * @return this point (for chaining) + * @see #withAdded(Point2D) for the non-mutating version that returns a new point + */ + public Point2D add(final Point2D otherPoint) { + x += otherPoint.x; + y += otherPoint.y; + return this; + } + + /** + * Checks if both coordinates are zero. + * + * @return {@code true} if current point coordinates are equal to zero + */ + public boolean isZero() { + return (x == 0) && (y == 0); + } + + /** + * Creates a new point by copying this point's coordinates. + * + * @return a new point with the same coordinates + */ + @Override + public Point2D clone() { + return new Point2D(this); + } + + /** + * Copies coordinates from another point into this point. + * + * @param otherPoint the point to copy coordinates from + */ + public void clone(final Point2D otherPoint) { + x = otherPoint.x; + y = otherPoint.y; + } + + /** + * Sets this point to the midpoint between two other points. + * + * @param p1 the first point + * @param p2 the second point + * @return this point (for chaining) + */ + public Point2D setToMiddle(final Point2D p1, final Point2D p2) { + x = (p1.x + p2.x) / 2d; + y = (p1.y + p2.y) / 2d; + return this; + } + + /** + * Computes the angle on the X-Y plane between this point and another point. + * + * @param anotherPoint the other point + * @return the angle in radians + */ + public double getAngleXY(final Point2D anotherPoint) { + return Math.atan2(x - anotherPoint.x, y - anotherPoint.y); + } + + /** + * Computes the Euclidean distance from this point to another point. + * + * @param anotherPoint the point to compute distance to + * @return the distance between the two points + */ + public double getDistanceTo(final Point2D anotherPoint) { + final double xDiff = x - anotherPoint.x; + final double yDiff = y - anotherPoint.y; + + return sqrt(((xDiff * xDiff) + (yDiff * yDiff))); + } + + /** + * Computes the length of this vector (magnitude). + * + * @return the vector length + */ + public double getVectorLength() { + return sqrt(((x * x) + (y * y))); + } + + /** + * Negates this point's coordinates in place. + * This point is modified. + * + * @return this point (for chaining) + * @see #withNegated() for the non-mutating version that returns a new point + */ + public Point2D negate() { + x = -x; + y = -y; + return this; + } + + /** + * Rounds this point's coordinates to integer values. + */ + public void roundToInteger() { + x = (int) x; + y = (int) y; + } + + /** + * Subtracts another point from this point in place. + * This point is modified, the other point is not. + * + * @param otherPoint the point to subtract + * @return this point (for chaining) + * @see #withSubtracted(Point2D) for the non-mutating version that returns a new point + */ + public Point2D subtract(final Point2D otherPoint) { + x -= otherPoint.x; + y -= otherPoint.y; + return this; + } + + /** + * Multiplies both coordinates by a factor. + * This point is modified. + * + * @param factor the multiplier + * @return this point (for chaining) + * @see #withMultiplied(double) for the non-mutating version that returns a new point + */ + public Point2D multiply(final double factor) { + x *= factor; + y *= factor; + return this; + } + + /** + * Divides both coordinates by a factor. + * This point is modified. + * + * @param factor the divisor + * @return this point (for chaining) + * @see #withDivided(double) for the non-mutating version that returns a new point + */ + public Point2D divide(final double factor) { + x /= factor; + y /= factor; + return this; + } + + /** + * Converts this 2D point to a 3D point with z = 0. + * + * @return a new 3D point with the same x, y and z = 0 + */ + public Point3D to3D() { + return new Point3D(x, y, 0); + } + + /** + * Resets this point's coordinates to (0, 0). + * + * @return this point (for chaining) + */ + public Point2D zero() { + x = 0; + y = 0; + return this; + } + + @Override + public String toString() { + return "Point2D{" + + "x=" + x + + ", y=" + y + + '}'; + } + + /** + * Returns a new point that is the sum of this point and another. + * This point is not modified. + * + * @param other the point to add + * @return a new Point2D representing the sum + * @see #add(Point2D) for the mutating version + */ + public Point2D withAdded(final Point2D other) { + return new Point2D(x + other.x, y + other.y); + } + + /** + * Returns a new point that is this point minus another. + * This point is not modified. + * + * @param other the point to subtract + * @return a new Point2D representing the difference + * @see #subtract(Point2D) for the mutating version + */ + public Point2D withSubtracted(final Point2D other) { + return new Point2D(x - other.x, y - other.y); + } + + /** + * Returns a new point with negated coordinates. + * This point is not modified. + * + * @return a new Point2D with negated coordinates + * @see #negate() for the mutating version + */ + public Point2D withNegated() { + return new Point2D(-x, -y); + } + + /** + * Returns a new point with coordinates multiplied by a factor. + * This point is not modified. + * + * @param factor the multiplier + * @return a new Point2D with multiplied coordinates + * @see #multiply(double) for the mutating version + */ + public Point2D withMultiplied(final double factor) { + return new Point2D(x * factor, y * factor); + } + + /** + * Returns a new point with coordinates divided by a factor. + * This point is not modified. + * + * @param factor the divisor + * @return a new Point2D with divided coordinates + * @see #divide(double) for the mutating version + */ + public Point2D withDivided(final double factor) { + return new Point2D(x / factor, y / factor); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point3D.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point3D.java new file mode 100755 index 0000000..8b227c6 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Point3D.java @@ -0,0 +1,586 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.geometry; + +import eu.svjatoslav.aukio.e3d.geometry.IntegerPoint; + +import static java.lang.Math.*; + +/** + * A mutable 3D point or vector with double-precision coordinates. + * + *

{@code Point3D} is the fundamental coordinate type used throughout the Aukio 3D engine. + * It represents either a position in 3D space or a directional vector, with public + * {@code x}, {@code y}, {@code z} fields for direct access.

+ * + *

All mutation methods return {@code this} for fluent chaining:

+ *
{@code
+ * Point3D p = new Point3D(10, 20, 30)
+ *     .multiply(2.0)
+ *     .translateX(5)
+ *     .add(new Point3D(1, 1, 1));
+ * // p is now (25, 41, 61)
+ * }
+ * + *

Common operations:

+ *
{@code
+ * // Create points
+ * Point3D origin = Point3D.origin();          // (0, 0, 0)
+ * Point3D pos = Point3D.point(100, 200, 300);
+ * Point3D copy = new Point3D(pos);            // clone
+ *
+ * // Measure distance
+ * double dist = pos.getDistanceTo(origin);
+ *
+ * // Rotation
+ * pos.rotate(origin, Math.PI / 4, 0);  // rotate 45 degrees on XZ plane
+ *
+ * // Scale
+ * pos.multiply(2.0);   // double all coordinates
+ * pos.divide(2.0);     // halve all coordinates
+ * }
+ * + *

Mutability convention:

+ *
    + *
  • Imperative verbs ({@code add}, {@code subtract}, {@code negate}, {@code multiply}, + * {@code divide}) mutate this point and return {@code this}
  • + *
  • {@code with}-prefixed methods ({@code withAdded}, {@code withSubtracted}, {@code withNegated}, + * {@code withMultiplied}, {@code withDivided}) return a new point without modifying this one
  • + *
+ * + *

Warning: This class is mutable with public fields. Clone before storing + * references that should not be shared:

+ *
{@code
+ * Point3D safeCopy = original.clone();
+ * }
+ * + * @see Point2D the 2D equivalent + * @see eu.svjatoslav.aukio.e3d.renderer.raster.Vertex wraps a Point3D with transform support + */ +public class Point3D implements Cloneable { + + /** X coordinate (horizontal axis). */ + public double x; + /** Y coordinate (vertical axis, positive = down in screen space). */ + public double y; + /** Z coordinate (depth axis, positive = into the screen / away from viewer). */ + public double z; + + /** + * Creates a point at the origin (0, 0, 0). + */ + public Point3D() { + } + + /** + * Creates a point with the specified double-precision coordinates. + * + * @param x the X coordinate + * @param y the Y coordinate + * @param z the Z coordinate + */ + public Point3D(final double x, final double y, final double z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Creates a point with the specified float coordinates (widened to double). + * + * @param x the X coordinate + * @param y the Y coordinate + * @param z the Z coordinate + */ + public Point3D(final float x, final float y, final float z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Creates a point with the specified integer coordinates (widened to double). + * + * @param x the X coordinate + * @param y the Y coordinate + * @param z the Z coordinate + */ + public Point3D(final int x, final int y, final int z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Creates a point from an {@link IntegerPoint} (used by octree voxel coordinates). + * + * @param point the integer point to convert + */ + public Point3D(IntegerPoint point) { + this.x = point.x; + this.y = point.y; + this.z = point.z; + } + + + /** + * Creates a new point by cloning coordinates from the parent point. + * + * @param parent the point to copy coordinates from + */ + public Point3D(final Point3D parent) { + x = parent.x; + y = parent.y; + z = parent.z; + } + + /** + * Returns a new point at the origin (0, 0, 0). + * + * @return a new Point3D at the origin + */ + public static Point3D origin() { + return new Point3D(); + } + + /** + * Returns a new point with the specified coordinates. + * + * @param x the X coordinate + * @param y the Y coordinate + * @param z the Z coordinate + * @return a new Point3D with the given coordinates + */ + public static Point3D point(final double x, final double y, final double z) { + return new Point3D(x, y, z); + } + + /** + * Adds another point to this point in place. + * This point is modified, the other point is not. + * + * @param otherPoint the point to add + * @return this point (for chaining) + * @see #withAdded(Point3D) for the non-mutating version that returns a new point + */ + public Point3D add(final Point3D otherPoint) { + x += otherPoint.x; + y += otherPoint.y; + z += otherPoint.z; + return this; + } + + /** + * Adds coordinates of current point to one or more other points. + * The current point's coordinates are added to each target point. + * + * @param otherPoints the points to add this point's coordinates to + * @return this point (for chaining) + */ + public Point3D addTo(final Point3D... otherPoints) { + for (final Point3D otherPoint : otherPoints) otherPoint.add(this); + return this; + } + + /** + * Create new point by cloning position of current point. + * + * @return newly created clone. + */ + public Point3D clone() { + return new Point3D(this); + } + + /** + * Copies coordinates from another point into this point. + * + * @param otherPoint the point to copy coordinates from + * @return this point (for chaining) + */ + public Point3D clone(final Point3D otherPoint) { + x = otherPoint.x; + y = otherPoint.y; + z = otherPoint.z; + return this; + } + + /** + * Set current point coordinates to the middle point between two other points. + * + * @param p1 first point. + * @param p2 second point. + * @return current point. + */ + public Point3D computeMiddlePoint(final Point3D p1, final Point3D p2) { + x = (p1.x + p2.x) / 2d; + y = (p1.y + p2.y) / 2d; + z = (p1.z + p2.z) / 2d; + return this; + } + + /** + * Checks if all coordinates are zero. + * + * @return {@code true} if current point coordinates are equal to zero + */ + public boolean isZero() { + return (x == 0) && (y == 0) && (z == 0); + } + + /** + * Computes the angle on the X-Z plane between this point and another point. + * + * @param anotherPoint the other point + * @return the angle in radians + */ + public double getAngleXZ(final Point3D anotherPoint) { + return Math.atan2(x - anotherPoint.x, z - anotherPoint.z); + } + + /** + * Computes the angle on the Y-Z plane between this point and another point. + * + * @param anotherPoint the other point + * @return the angle in radians + */ + public double getAngleYZ(final Point3D anotherPoint) { + return Math.atan2(y - anotherPoint.y, z - anotherPoint.z); + } + + /** + * Computes the angle on the X-Y plane between this point and another point. + * + * @param anotherPoint the other point + * @return the angle in radians + */ + public double getAngleXY(final Point3D anotherPoint) { + return Math.atan2(x - anotherPoint.x, y - anotherPoint.y); + } + + /** + * Compute distance to another point. + * + * @param anotherPoint point to compute distance to. + * @return distance to another point. + */ + public double getDistanceTo(final Point3D anotherPoint) { + final double xDelta = x - anotherPoint.x; + final double yDelta = y - anotherPoint.y; + final double zDelta = z - anotherPoint.z; + + return sqrt(((xDelta * xDelta) + (yDelta * yDelta) + (zDelta * zDelta))); + } + + /** + * Computes the length (magnitude) of this vector. + * + * @return the vector length + */ + public double getVectorLength() { + return sqrt(((x * x) + (y * y) + (z * z))); + } + + /** + * Negates this point's coordinates in place. + * This point is modified. + * + * @return this point (for chaining) + * @see #withNegated() for the non-mutating version that returns a new point + */ + public Point3D negate() { + x = -x; + y = -y; + z = -z; + return this; + } + + /** + * Rotates this point around a center point by the given XZ and YZ angles. + *

+ * See also: Let's remove Quaternions from every 3D Engine + * + * @param center the center point to rotate around + * @param angleXZ the angle in the XZ plane (yaw) in radians + * @param angleYZ the angle in the YZ plane (pitch) in radians + * @return this point (for chaining) + */ + public Point3D rotate(final Point3D center, final double angleXZ, + final double angleYZ) { + final double s1 = sin(angleXZ); + final double c1 = cos(angleXZ); + + final double s2 = sin(angleYZ); + final double c2 = cos(angleYZ); + + x -= center.x; + y -= center.y; + z -= center.z; + + final double y1 = (z * s2) + (y * c2); + final double z1 = (z * c2) - (y * s2); + + final double x1 = (z1 * s1) + (x * c1); + final double z2 = (z1 * c1) - (x * s1); + + x = x1 + center.x; + y = y1 + center.y; + z = z2 + center.z; + + return this; + } + + /** + * Rotate current point around the origin by the given angles. + * + * @param angleXZ angle around the XZ plane (yaw), in radians + * @param angleYZ angle around the YZ plane (pitch), in radians + * @return this point (mutated) + */ + public Point3D rotate(final double angleXZ, final double angleYZ) { + return rotate(new Point3D(0, 0, 0), angleXZ, angleYZ); + } + + /** + * Round current point coordinates to integer values. + */ + public void roundToInteger() { + x = (int) x; + y = (int) y; + z = (int) z; + } + + /** + * Divides all coordinates by a factor. + * This point is modified. + * + * @param factor the divisor + * @return this point (for chaining) + * @see #withDivided(double) for the non-mutating version that returns a new point + */ + public Point3D divide(final double factor) { + x /= factor; + y /= factor; + z /= factor; + return this; + } + + /** + * Multiplies all coordinates by a factor. + * This point is modified. + * + * @param factor the multiplier + * @return this point (for chaining) + * @see #withMultiplied(double) for the non-mutating version that returns a new point + */ + public Point3D multiply(final double factor) { + x *= factor; + y *= factor; + z *= factor; + return this; + } + + /** + * Set current point coordinates to given values. + * + * @param x X coordinate. + * @param y Y coordinate. + * @param z Z coordinate. + */ + public void setValues(final double x, final double y, final double z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Subtracts another point from this point in place. + * This point is modified, the other point is not. + * + * @param otherPoint the point to subtract + * @return this point (for chaining) + * @see #withSubtracted(Point3D) for the non-mutating version that returns a new point + */ + public Point3D subtract(final Point3D otherPoint) { + x -= otherPoint.x; + y -= otherPoint.y; + z -= otherPoint.z; + return this; + } + + @Override + public String toString() { + return "x:" + x + " y:" + y + " z:" + z; + } + + /** + * Translates this point along the X axis. + * + * @param xIncrement the amount to add to the X coordinate + * @return this point (for chaining) + */ + public Point3D translateX(final double xIncrement) { + x += xIncrement; + return this; + } + + /** + * Translates this point along the Y axis. + * + * @param yIncrement the amount to add to the Y coordinate + * @return this point (for chaining) + */ + public Point3D translateY(final double yIncrement) { + y += yIncrement; + return this; + } + + /** + * Translates this point along the Z axis. + * + * @param zIncrement the amount to add to the Z coordinate + * @return this point (for chaining) + */ + public Point3D translateZ(final double zIncrement) { + z += zIncrement; + return this; + } + + /** + * Here we assume that Z coordinate is distance to the viewer. + * If Z is positive, then point is in front of the viewer, and therefore it is visible. + * + * @return point visibility status. + */ + public boolean isVisible() { + return z > 0; + } + + /** + * Resets point coordinates to zero along all axes. + * + * @return current point. + */ + public Point3D zero() { + x = 0; + y = 0; + z = 0; + return this; + } + + /** + * Computes the dot product of this vector with another. + * + * @param other the other vector + * @return the dot product (scalar) + */ + public double dot(final Point3D other) { + return x * other.x + y * other.y + z * other.z; + } + + /** + * Computes the cross-product of this vector with another. + * Returns a new vector perpendicular to both input vectors. + * + * @param other the other vector + * @return a new Point3D representing the cross-product + */ + public Point3D cross(final Point3D other) { + return new Point3D( + y * other.z - z * other.y, + z * other.x - x * other.z, + x * other.y - y * other.x + ); + } + + /** + * Returns a new point that is the sum of this point and another. + * This point is not modified. + * + * @param other the point to add + * @return a new Point3D representing the sum + * @see #add(Point3D) for the mutating version + */ + public Point3D withAdded(final Point3D other) { + return new Point3D(x + other.x, y + other.y, z + other.z); + } + + /** + * Returns a new point that is this point minus another. + * This point is not modified. + * + * @param other the point to subtract + * @return a new Point3D representing the difference + * @see #subtract(Point3D) for the mutating version + */ + public Point3D withSubtracted(final Point3D other) { + return new Point3D(x - other.x, y - other.y, z - other.z); + } + + /** + * Returns a new point with negated coordinates. + * This point is not modified. + * + * @return a new Point3D with negated coordinates + * @see #negate() for the mutating version + */ + public Point3D withNegated() { + return new Point3D(-x, -y, -z); + } + + /** + * Returns a new unit vector (normalized) in the same direction. + * This point is not modified. + * + * @return a new Point3D with unit length + */ + public Point3D unit() { + final double len = getVectorLength(); + if (len == 0) { + return new Point3D(0, 0, 0); + } + return new Point3D(x / len, y / len, z / len); + } + + /** + * Returns a new point that is a linear interpolation between this point and another. + * When t=0, returns this point. When t=1, returns the other point. + * + * @param other the other point + * @param t the interpolation parameter (0 to 1) + * @return a new Point3D representing the interpolated position + */ + public Point3D interpolate(final Point3D other, final double t) { + return new Point3D( + x + (other.x - x) * t, + y + (other.y - y) * t, + z + (other.z - z) * t + ); + } + + /** + * Returns a new point with coordinates multiplied by a factor. + * This point is not modified. + * + * @param factor the multiplier + * @return a new Point3D with multiplied coordinates + * @see #multiply(double) for the mutating version + */ + public Point3D withMultiplied(final double factor) { + return new Point3D(x * factor, y * factor, z * factor); + } + + /** + * Returns a new point with coordinates divided by a factor. + * This point is not modified. + * + * @param factor the divisor + * @return a new Point3D with divided coordinates + * @see #divide(double) for the mutating version + */ + public Point3D withDivided(final double factor) { + return new Point3D(x / factor, y / factor, z / factor); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Polygon.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Polygon.java new file mode 100644 index 0000000..6c1b994 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Polygon.java @@ -0,0 +1,83 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.geometry; + +/** + * Utility class for polygon operations, primarily point-in-polygon testing. + * + *

Provides static methods for geometric computations on triangles and other polygons.

+ * + * @see Point2D + */ +public class Polygon { + + /** + * Creates a new Polygon utility instance. + */ + public Polygon() { + } + + + /** + * Checks if a point is on the right side of a directed line segment. + * Used internally for ray-casting in point-in-polygon tests. + * + * @param point the point to test + * @param lineP1 the start point of the line segment + * @param lineP2 the end point of the line segment + * @return {@code true} if the point is on the right side of the line + */ + private static boolean intersectsLine(final Point2D point, Point2D lineP1, + Point2D lineP2) { + + // Sort line points by y coordinate. + if (lineP1.y > lineP2.y) { + final Point2D tmp = lineP1; + lineP1 = lineP2; + lineP2 = tmp; + } + + // Check if point is within line y range. + if (point.y < lineP1.y || point.y > lineP2.y) + return false; + + // Check if point is on the line. + final double xp = lineP2.x - lineP1.x; + final double yp = lineP2.y - lineP1.y; + + final double crossX = lineP1.x + ((xp * (point.y - lineP1.y)) / yp); + + return point.x >= crossX; + } + + /** + * Tests whether a point lies inside a triangle using the ray-casting algorithm. + * + *

Casts a horizontal ray from the test point and counts intersections + * with the triangle edges. If the number of intersections is odd, the point is inside.

+ * + * @param point the point to test + * @param p1 the first vertex of the triangle + * @param p2 the second vertex of the triangle + * @param p3 the third vertex of the triangle + * @return {@code true} if the point is inside the triangle + */ + public static boolean pointWithinPolygon(final Point2D point, Point2D p1, Point2D p2, Point2D p3) { + + int intersectionCount = 0; + + if (intersectsLine(point, p1, p2)) + intersectionCount++; + + if (intersectsLine(point, p2, p3)) + intersectionCount++; + + if (intersectsLine(point, p3, p1)) + intersectionCount++; + + return intersectionCount == 1; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Rectangle.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Rectangle.java new file mode 100644 index 0000000..41b4195 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/Rectangle.java @@ -0,0 +1,83 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.geometry; + +import static java.lang.Math.abs; +import static java.lang.Math.min; + +/** + * A 2D axis-aligned rectangle defined by two corner points. + * + *

The rectangle is defined by two points ({@link #p1} and {@link #p2}) that represent + * opposite corners. The rectangle does not enforce ordering of these points.

+ * + * @see Point2D + * @see Box the 3D equivalent + */ +public class Rectangle { + + /** + * The corner points of the rectangle (opposite corners). + */ + public Point2D p1, p2; + + /** + * Creates a square rectangle centered at the origin with the specified size. + * + * @param size the width and height of the square + */ + public Rectangle(final double size) { + p2 = new Point2D(size / 2, size / 2); + p1 = p2.clone().negate(); + } + + /** + * Creates a rectangle with the specified corner points. + * + * @param p1 the first corner point + * @param p2 the second corner point (opposite corner) + */ + public Rectangle(final Point2D p1, final Point2D p2) { + this.p1 = p1; + this.p2 = p2; + } + + /** + * Returns the height of the rectangle (distance along the Y-axis). + * + * @return the height (always positive) + */ + public double getHeight() { + return abs(p1.y - p2.y); + } + + /** + * Returns the leftmost X coordinate of the rectangle. + * + * @return the minimum X value + */ + public double getLowerX() { + return min(p1.x, p2.x); + } + + /** + * Returns the topmost Y coordinate of the rectangle. + * + * @return the minimum Y value + */ + public double getLowerY() { + return min(p1.y, p2.y); + } + + /** + * Returns the width of the rectangle (distance along the X-axis). + * + * @return the width (always positive) + */ + public double getWidth() { + return abs(p1.x - p2.x); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/geometry/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/package-info.java new file mode 100644 index 0000000..a4db80e --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/geometry/package-info.java @@ -0,0 +1,7 @@ +/** + * Provides basic geometry classes for 2D and 3D coordinates and shapes. + * + * @see eu.svjatoslav.aukio.e3d.geometry.Point2D + * @see eu.svjatoslav.aukio.e3d.geometry.Point3D + */ +package eu.svjatoslav.aukio.e3d.geometry; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/BugReport.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/BugReport.java new file mode 100644 index 0000000..665b7cb --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/BugReport.java @@ -0,0 +1,233 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui; +import eu.svjatoslav.aukio.e3d.geometry.Camera; + +import eu.svjatoslav.aukio.e3d.diag.EngineConfig; +import eu.svjatoslav.aukio.e3d.diag.PersistentLog; +import eu.svjatoslav.aukio.e3d.diag.Telemetry; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; + +import javax.imageio.ImageIO; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +/** + * Writes a self-contained bug report directory the user can point a + * developer (or an AI assistant) at. + * + *

A new directory {@code bugreport-} is created under the + * configured {@code bugreport.dir} containing:

+ *
    + *
  • {@code description.txt} — what the user wrote in the popup
  • + *
  • {@code info.txt} — camera pose, view/screen resolution, heap, + * telemetry snapshot, java/os versions, effective config
  • + *
  • {@code screenshot.png} — a copy of the last rendered frame
  • + *
  • {@code aukio.log} / {@code aukio.log.1} — the persistent logs, + * including output from a session that froze before the report + * could be made
  • + *
  • {@code histogram.txt} — live-object histogram from + * {@code jcmd GC.class_histogram} (the OOM smoking gun; + * skipped when jcmd is unavailable)
  • + *
  • {@code threads.txt} — full thread dump (hang diagnosis)
  • + *
+ */ +public final class BugReport { + + private static final DateTimeFormatter DIR_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"); + + private BugReport() { + } + + /** + * Creates a bug report for the given view. + * + * @param viewPanel the view whose camera and last frame to capture + * (may be null — screenshot/camera are then + * skipped) + * @param description free-form user description of the problem + * @return the created report directory + */ + public static File create(final ViewPanel viewPanel, + final String description) throws IOException { + final File root = EngineConfig.getBugReportDir(); + File dir = new File(root, + "bugreport-" + LocalDateTime.now().format(DIR_FORMATTER)); + int suffix = 1; + while (dir.exists()) { + dir = new File(dir.getPath() + "-" + (++suffix)); + } + Files.createDirectories(dir.toPath()); + + Files.writeString(new File(dir, "description.txt").toPath(), + description == null ? "" : description); + writeInfo(new File(dir, "info.txt"), viewPanel); + writeScreenshot(dir, viewPanel); + copyLogs(dir); + writeHistogram(dir); + writeThreadDump(new File(dir, "threads.txt")); + + System.out.println("[BUGREPORT] written to " + + dir.getAbsolutePath()); + return dir; + } + + private static void writeInfo(final File file, + final ViewPanel viewPanel) + throws IOException { + final Runtime rt = Runtime.getRuntime(); + final long used = rt.totalMemory() - rt.freeMemory(); + final StringBuilder sb = new StringBuilder(); + sb.append("time = ").append(LocalDateTime.now()).append('\n'); + + if (viewPanel != null) { + final Camera camera = viewPanel.getCamera(); + final Point3D pos = camera.getTransform().getTranslation(); + final double[] angles = + camera.getTransform().getRotation().toAngles(); + sb.append(String.format( + "camera = (%.2f, %.2f, %.2f, %.2f, %.2f, %.2f)" + + " # x, y, z, yaw, pitch, roll%n", + pos.x, pos.y, pos.z, + angles[0], angles[1], angles[2])); + sb.append(String.format("view.size = %dx%d%n", + viewPanel.getWidth(), viewPanel.getHeight())); + sb.append("measured.fps = ").append(String.format("%.1f", + viewPanel.getMeasuredFPS())).append('\n'); + } + + try { + final GraphicsDevice device = GraphicsEnvironment + .getLocalGraphicsEnvironment() + .getDefaultScreenDevice(); + sb.append(String.format("screen.resolution = %dx%d@%dHz%n", + device.getDisplayMode().getWidth(), + device.getDisplayMode().getHeight(), + device.getDisplayMode().getRefreshRate())); + } catch (final Throwable t) { + sb.append("screen.resolution = unavailable (") + .append(t.getMessage()).append(")\n"); + } + + sb.append(String.format("heap.used = %d MB%nheap.max = %d MB%n", + used / (1024 * 1024), rt.maxMemory() / (1024 * 1024))); + sb.append("telemetry = ").append(Telemetry.snapshotLine()) + .append('\n'); + sb.append("java.version = ") + .append(System.getProperty("java.version")).append('\n'); + sb.append("os = ").append(System.getProperty("os.name")) + .append(' ').append(System.getProperty("os.version")) + .append('\n'); + sb.append("config.ipd.cm = ").append(EngineConfig.getIpdCm()) + .append('\n'); + sb.append("config.bugreport.dir = ") + .append(EngineConfig.getBugReportDir()).append('\n'); + sb.append("config.log.dir = ") + .append(EngineConfig.getLogDir()).append('\n'); + + Files.writeString(file.toPath(), sb.toString()); + } + + private static void writeScreenshot(final File dir, + final ViewPanel viewPanel) + throws IOException { + if (viewPanel == null) + return; + final BufferedImage lastFrame = viewPanel.getLastFrameImage(); + if (lastFrame == null) { + Files.writeString(new File(dir, "screenshot-missing.txt") + .toPath(), + "no frame had been rendered yet\n"); + return; + } + // the engine reuses frame buffers (triple buffering) — copy + final BufferedImage copy = new BufferedImage(lastFrame.getWidth(), + lastFrame.getHeight(), BufferedImage.TYPE_INT_RGB); + copy.getGraphics().drawImage(lastFrame, 0, 0, null); + ImageIO.write(copy, "png", new File(dir, "screenshot.png")); + } + + private static void copyLogs(final File dir) throws IOException { + final File log = PersistentLog.getLogFile(); + if (log == null || !log.isFile()) + return; + Files.copy(log.toPath(), + new File(dir, log.getName()).toPath(), + StandardCopyOption.REPLACE_EXISTING); + final File backup = new File(log.getParentFile(), + log.getName() + ".1"); + if (backup.isFile()) { + Files.copy(backup.toPath(), + new File(dir, backup.getName()).toPath(), + StandardCopyOption.REPLACE_EXISTING); + } + } + + /** + * Live-object histogram: forces a full GC and lists instance counts + * per class — the direct answer to "what is leaking". Best effort: + * needs the JDK's jcmd on the PATH. Bounded by a timeout: jcmd + * self-attach can hang in some environments, and a bug report must + * still complete without it. + */ + private static void writeHistogram(final File dir) { + final String pid = String.valueOf(ProcessHandle.current().pid()); + final File out = new File(dir, "histogram.txt"); + try { + final Process process = new ProcessBuilder("jcmd", pid, + "GC.class_histogram").redirectErrorStream(true) + .redirectOutput(out).start(); + final boolean done = process.waitFor(30, + java.util.concurrent.TimeUnit.SECONDS); + if (done && process.exitValue() == 0) { + return; + } + if (!done) { + process.destroyForcibly(); + } + out.delete(); + Files.writeString( + new File(dir, "histogram-unavailable.txt").toPath(), + "jcmd GC.class_histogram did not finish within 30s\n"); + } catch (final Throwable t) { + try { + Files.writeString( + new File(dir, "histogram-unavailable.txt").toPath(), + "jcmd GC.class_histogram failed: " + t + "\n"); + } catch (final IOException ignored) { + } + } + } + + private static void writeThreadDump(final File file) + throws IOException { + final StringBuilder sb = new StringBuilder(); + for (final Map.Entry entry + : Thread.getAllStackTraces().entrySet()) { + final Thread thread = entry.getKey(); + sb.append('"').append(thread.getName()).append('"') + .append(thread.isDaemon() ? " daemon" : "") + .append(" state=").append(thread.getState()) + .append('\n'); + for (final StackTraceElement element : entry.getValue()) { + sb.append(" at ").append(element).append('\n'); + } + sb.append('\n'); + } + sb.append("vm = ").append(ManagementFactory.getRuntimeMXBean() + .getVmName()).append('\n'); + Files.writeString(file.toPath(), sb.toString()); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperTools.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperTools.java new file mode 100644 index 0000000..4813daf --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperTools.java @@ -0,0 +1,46 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui; + +/** + * Per-ViewPanel developer tools that control diagnostic features. + * + *

Each {@link ViewPanel} has its own DeveloperTools instance, allowing + * different views to have independent debug configurations.

+ * + *

Settings can be toggled at runtime via the {@link DeveloperToolsPanel} + * (opened with F12 key).

+ * + * @see ViewPanel#getDeveloperTools() + * @see DeveloperToolsPanel + */ +public class DeveloperTools { + + /** + * If {@code true}, textured polygon borders are drawn in yellow. + * Useful for visualizing polygon slicing for perspective-correct rendering. + */ + public volatile boolean showPolygonBorders = false; + + /** + * If {@code true}, only render even-numbered horizontal segments (0, 2, 4, 6). + * Odd segments (1, 3, 5, 7) will remain black. Useful for detecting + * if threads render outside their allocated screen area (overdraw detection). + */ + public volatile boolean renderAlternateSegments = false; + + /** + * If {@code true}, draws red horizontal lines at segment boundaries. + * Useful for visualizing which thread renders which screen area. + * Each line marks the boundary between two adjacent rendering segments. + */ + public volatile boolean showSegmentBoundaries = false; + + /** + * Creates a new DeveloperTools instance with all debug features disabled. + */ + public DeveloperTools() { + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperToolsPanel.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperToolsPanel.java new file mode 100644 index 0000000..179a4f0 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeveloperToolsPanel.java @@ -0,0 +1,651 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui; +import eu.svjatoslav.aukio.e3d.diag.DebugLogBuffer; +import eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder; +import eu.svjatoslav.aukio.e3d.geometry.Camera; +import eu.svjatoslav.aukio.e3d.renderer.raster.CullingStatistics; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import java.awt.*; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.List; + +/** + * Developer tools panel for toggling diagnostic features and viewing logs. + * + *

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

+ *
    + *
  • Checkboxes to toggle debug settings
  • + *
  • Camera position display with copy button
  • + *
  • Composite shape frustum culling statistics
  • + *
  • A scrollable log viewer showing captured debug output
  • + *
  • A button to clear the log buffer
  • + *
  • Resizable window with native maximize support
  • + *
+ * + * @see DeveloperTools + * @see DebugLogBuffer + */ +public class DeveloperToolsPanel extends JFrame { + + private static final int UPDATE_INTERVAL_MS = 200; + + /** + * The view panel whose camera is being displayed. + */ + private final ViewPanel viewPanel; + /** + * The developer tools being controlled. + */ + private final DeveloperTools developerTools; + /** + * The log buffer being displayed. + */ + private final DebugLogBuffer debugLogBuffer; + /** + * The text area showing log messages. + */ + private final JTextArea logArea; + /** + * The label showing camera position. + */ + private final JLabel cameraLabel; + /** + * The label showing total composites count. + */ + private final JLabel totalCompositesLabel; + /** + * The label showing culled composites count. + */ + private final JLabel culledCompositesLabel; + /** + * The label showing culled percentage. + */ + private final JLabel culledPercentLabel; + /** + * The label showing current render thread count. + */ + private final JLabel renderThreadsLabel; + /** + * The label showing the current target FPS. + */ + private final JLabel targetFpsLabel; + /** + * The label showing the measured FPS. + */ + private final JLabel measuredFpsLabel; + /** + * Toggle button switching between target FPS and unlimited FPS. + */ + private final JToggleButton unlockFpsButton; + /** + * The per-thread activity timeline. + */ + private final ThreadTimelineComponent threadTimeline; + /** + * Toggle button enabling thread activity recording. + */ + private final JToggleButton recordTimelineButton; + /** + * Target FPS to restore when re-locking after an unlock. + */ + private int lockedFPS = 60; + /** + * Timer for periodic updates. + */ + private final Timer updateTimer; + /** + * Flag to prevent concurrent updates. + */ + private volatile boolean updating = false; + + /** + * Creates and displays a developer tools panel. + * + * @param parent the parent frame (for centering) + * @param viewPanel the view panel whose camera to display + * @param developerTools the developer tools to control + * @param debugLogBuffer the log buffer to display + */ + public DeveloperToolsPanel(final Frame parent, final ViewPanel viewPanel, + final DeveloperTools developerTools, + final DebugLogBuffer debugLogBuffer) { + super("Developer Tools"); + this.viewPanel = viewPanel; + this.developerTools = developerTools; + this.debugLogBuffer = debugLogBuffer; + + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + setLayout(new BorderLayout(8, 8)); + + cameraLabel = new JLabel(" "); + cameraLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + + // Initialize culling statistics labels + totalCompositesLabel = new JLabel("0"); + totalCompositesLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + culledCompositesLabel = new JLabel("0"); + culledCompositesLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + culledPercentLabel = new JLabel("0.0%"); + culledPercentLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + + // Initialize render threads label + renderThreadsLabel = new JLabel(String.valueOf(viewPanel.getNumRenderThreads())); + renderThreadsLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + + // Initialize frame rate display and unlock toggle + targetFpsLabel = new JLabel(" "); + targetFpsLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + measuredFpsLabel = new JLabel(" "); + measuredFpsLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + unlockFpsButton = new JToggleButton("Unlock FPS"); + unlockFpsButton.setToolTipText( + "Disable the frame rate cap so the application renders as fast as it can (benchmark mode)"); + unlockFpsButton.setSelected(viewPanel.getTargetFPS() <= 0); + unlockFpsButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + if (unlockFpsButton.isSelected()) { + final int current = viewPanel.getTargetFPS(); + if (current > 0) { + lockedFPS = current; + } + viewPanel.setFrameRate(0); + } else { + viewPanel.setFrameRate(lockedFPS); + } + updateFrameRateLabels(); + } + }); + + // Thread activity timeline with record toggle + threadTimeline = new ThreadTimelineComponent(); + recordTimelineButton = new JToggleButton("Record"); + recordTimelineButton.setToolTipText( + "Record per-thread activity (transform/paint/binning per frame, idle = black) for the timeline below"); + recordTimelineButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + ThreadActivityRecorder.setEnabled(recordTimelineButton.isSelected()); + } + }); + + final JPanel topPanel = new JPanel(); + topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.Y_AXIS)); + topPanel.add(createSettingsPanel()); + topPanel.add(createCameraPanel()); + topPanel.add(createCullingPanel()); + topPanel.add(createRenderThreadsPanel()); + topPanel.add(createFrameRatePanel()); + topPanel.add(createThreadTimelinePanel()); + add(topPanel, BorderLayout.NORTH); + + logArea = new JTextArea(15, 60); + logArea.setEditable(false); + logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + logArea.setBackground(Color.BLACK); + logArea.setForeground(Color.GREEN); + final JScrollPane scrollPane = new JScrollPane(logArea); + scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + add(scrollPane, BorderLayout.CENTER); + + final JPanel buttonPanel = createButtonPanel(); + add(buttonPanel, BorderLayout.SOUTH); + + pack(); + setLocationRelativeTo(parent); + + updateTimer = new Timer(UPDATE_INTERVAL_MS, new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + updateDisplay(); + } + }); + + addWindowListener(new WindowAdapter() { + @Override + public void windowOpened(final WindowEvent e) { + updateDisplay(); + updateTimer.start(); + } + + @Override + public void windowClosed(final WindowEvent e) { + updateTimer.stop(); + } + }); + } + + private JPanel createSettingsPanel() { + final JPanel panel = new JPanel(new GridLayout(0, 1, 0, 2)); + panel.setBorder(BorderFactory.createEmptyBorder(8, 8, 0, 8)); + + final JCheckBox showBordersCheckbox = new JCheckBox("Show polygon borders"); + showBordersCheckbox.setSelected(developerTools.showPolygonBorders); + showBordersCheckbox.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(final ChangeEvent e) { + developerTools.showPolygonBorders = showBordersCheckbox.isSelected(); + } + }); + + final JCheckBox alternateSegmentsCheckbox = new JCheckBox("Render alternate segments (overdraw debug)"); + alternateSegmentsCheckbox.setSelected(developerTools.renderAlternateSegments); + alternateSegmentsCheckbox.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(final ChangeEvent e) { + developerTools.renderAlternateSegments = alternateSegmentsCheckbox.isSelected(); + } + }); + + final JCheckBox segmentBoundariesCheckbox = new JCheckBox("Show segment boundaries"); + segmentBoundariesCheckbox.setSelected(developerTools.showSegmentBoundaries); + segmentBoundariesCheckbox.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(final ChangeEvent e) { + developerTools.showSegmentBoundaries = segmentBoundariesCheckbox.isSelected(); + } + }); + + panel.add(showBordersCheckbox); + panel.add(alternateSegmentsCheckbox); + panel.add(segmentBoundariesCheckbox); + + return panel; + } + + private JPanel createCameraPanel() { + final JPanel panel = new JPanel(new BorderLayout(4, 4)); + panel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(8, 8, 8, 8), + BorderFactory.createTitledBorder("Camera (x, y, z, yaw, pitch, roll)") + )); + + panel.add(cameraLabel, BorderLayout.CENTER); + + final JButton copyButton = new JButton("Copy"); + copyButton.setToolTipText("Copy camera position to clipboard"); + copyButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + final String text = cameraLabel.getText(); + if (text != null && !text.trim().isEmpty()) { + final StringSelection sel = new StringSelection(text); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(sel, null); + } + } + }); + + final JButton pasteButton = new JButton("Paste"); + pasteButton.setToolTipText( + "Set camera position from clipboard (six comma-separated" + + " numbers: x, y, z, yaw, pitch, roll); does" + + " nothing if the clipboard holds anything else"); + pasteButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + pasteCameraPosition(); + } + }); + + final JPanel buttons = new JPanel(new GridLayout(0, 1, 0, 4)); + buttons.add(copyButton); + buttons.add(pasteButton); + panel.add(buttons, BorderLayout.EAST); + + return panel; + } + + /** + * Strict decimal number: rejects NaN/Infinity/hex-float spellings + * that {@link Double#parseDouble(String)} would otherwise accept. + */ + private static final java.util.regex.Pattern NUMBER = + java.util.regex.Pattern.compile( + "[+-]?(\\d+\\.?\\d*|\\.\\d+)([eE][+-]?\\d+)?"); + + /** + * Reads the system clipboard and, if it holds exactly six + * comma-separated numbers (the format {@link #updateCameraLabel()} + * and the Copy button produce), teleports the camera to + * x, y, z, yaw, pitch, roll. Anything else in the clipboard is + * ignored silently. + */ + private void pasteCameraPosition() { + if (viewPanel == null) { + return; + } + final String text; + try { + final java.awt.datatransfer.Clipboard clipboard = + Toolkit.getDefaultToolkit().getSystemClipboard(); + if (!clipboard.isDataFlavorAvailable( + java.awt.datatransfer.DataFlavor.stringFlavor)) { + return; + } + text = (String) clipboard.getData( + java.awt.datatransfer.DataFlavor.stringFlavor); + } catch (final Exception ex) { + return; + } + if (text == null) { + return; + } + final String[] parts = text.trim().split(",", -1); + if (parts.length != 6) { + return; + } + final double[] values = new double[6]; + for (int i = 0; i < 6; i++) { + final String token = parts[i].trim(); + if (!NUMBER.matcher(token).matches()) { + return; + } + values[i] = Double.parseDouble(token); + } + viewPanel.getCamera().getTransform().set(values[0], values[1], + values[2], values[3], values[4], values[5]); + updateCameraLabel(); + } + + private JPanel createCullingPanel() { + final JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(0, 8, 8, 8), + BorderFactory.createTitledBorder("Composite shape frustum culling") + )); + + // Single row: total, culled, percent + final JPanel statsRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 2)); + statsRow.add(new JLabel("Total:")); + statsRow.add(totalCompositesLabel); + statsRow.add(new JLabel(" Culled:")); + statsRow.add(culledCompositesLabel); + statsRow.add(culledPercentLabel); + + panel.add(statsRow); + + return panel; + } + + private JPanel createRenderThreadsPanel() { + final JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(0, 8, 8, 8), + BorderFactory.createTitledBorder("Render Threads") + )); + + final JPanel statsRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 2)); + statsRow.add(new JLabel("Active:")); + statsRow.add(renderThreadsLabel); + statsRow.add(new JLabel(" Available cores:")); + statsRow.add(new JLabel(String.valueOf(Runtime.getRuntime().availableProcessors()))); + + panel.add(statsRow); + + return panel; + } + + private JPanel createFrameRatePanel() { + final JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(0, 8, 8, 8), + BorderFactory.createTitledBorder("Frame Rate") + )); + + final JPanel statsRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 2)); + statsRow.add(new JLabel("Target:")); + statsRow.add(targetFpsLabel); + statsRow.add(new JLabel(" Measured:")); + statsRow.add(measuredFpsLabel); + statsRow.add(unlockFpsButton); + + panel.add(statsRow); + + return panel; + } + + private JPanel createThreadTimelinePanel() { + final JPanel panel = new JPanel(new BorderLayout(4, 4)); + panel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(0, 8, 8, 8), + BorderFactory.createTitledBorder("Thread Timeline (idle = black; wheel = scroll, ctrl+wheel = zoom)") + )); + panel.add(recordTimelineButton, BorderLayout.NORTH); + panel.add(threadTimeline, BorderLayout.CENTER); + panel.add(threadTimeline.scrollBar, BorderLayout.SOUTH); + return panel; + } + + private JPanel createButtonPanel() { + final JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + + final JButton clearButton = new JButton("Clear Logs"); + clearButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + debugLogBuffer.clear(); + logArea.setText(""); + } + }); + + final JButton bugReportButton = new JButton("Make Bug Report..."); + bugReportButton.setToolTipText( + "Write a bug report directory: your description, a" + + " screenshot, camera position, logs and memory" + + " statistics"); + bugReportButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + showBugReportDialog(); + } + }); + + panel.add(clearButton); + panel.add(bugReportButton); + + return panel; + } + + /** + * Popup asking what happened, then writes a bug report directory + * (screenshot + description + logs + memory stats) and shows the + * resulting path. + */ + private void showBugReportDialog() { + final JTextArea descriptionArea = new JTextArea(8, 50); + descriptionArea.setLineWrap(true); + descriptionArea.setWrapStyleWord(true); + final JScrollPane scroll = new JScrollPane(descriptionArea); + scroll.setBorder(BorderFactory.createTitledBorder( + "What happened / what looks wrong?")); + + final int choice = JOptionPane.showConfirmDialog(this, scroll, + "Make Bug Report", JOptionPane.OK_CANCEL_OPTION, + JOptionPane.PLAIN_MESSAGE); + if (choice != JOptionPane.OK_OPTION) + return; + + try { + final java.io.File dir = BugReport.create(viewPanel, + descriptionArea.getText()); + showBugReportResultDialog(dir); + } catch (final Exception ex) { + JOptionPane.showMessageDialog(this, + "Bug report failed: " + ex.getMessage(), + "Bug Report", JOptionPane.ERROR_MESSAGE); + } + } + + /** + * Shows the created bug report's location in a selectable text + * field (copy-paste with Ctrl+C works) plus explicit Copy Path and + * Open Folder buttons — a plain JOptionPane message is neither + * selectable nor actionable. + */ + private void showBugReportResultDialog(final java.io.File dir) { + final JDialog dialog = new JDialog(this, "Bug Report", true); + dialog.setLayout(new BorderLayout(8, 8)); + + final JPanel centerPanel = new JPanel(new BorderLayout(4, 4)); + centerPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 0, 8)); + centerPanel.add(new JLabel( + "Bug report written to (point the developer at this" + + " directory):"), BorderLayout.NORTH); + final JTextField pathField = new JTextField( + dir.getAbsolutePath()); + pathField.setEditable(false); + pathField.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + centerPanel.add(pathField, BorderLayout.CENTER); + dialog.add(centerPanel, BorderLayout.CENTER); + + final JPanel buttonPanel = new JPanel( + new FlowLayout(FlowLayout.LEFT)); + + final JButton copyButton = new JButton("Copy Path"); + copyButton.setToolTipText("Copy the report path to the clipboard"); + copyButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + final StringSelection sel = new StringSelection( + dir.getAbsolutePath()); + Toolkit.getDefaultToolkit().getSystemClipboard() + .setContents(sel, null); + } + }); + buttonPanel.add(copyButton); + + final boolean openSupported = Desktop.isDesktopSupported() + && Desktop.getDesktop() + .isSupported(Desktop.Action.OPEN); + final JButton openButton = new JButton("Open Folder"); + openButton.setToolTipText(openSupported + ? "Open the report directory in the file manager" + : "Opening folders is not supported on this desktop"); + openButton.setEnabled(openSupported); + if (openSupported) { + openButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + try { + Desktop.getDesktop().open(dir); + } catch (final Exception ex) { + JOptionPane.showMessageDialog(dialog, + "Could not open folder: " + + ex.getMessage(), + "Bug Report", JOptionPane.ERROR_MESSAGE); + } + } + }); + } + buttonPanel.add(openButton); + + final JButton closeButton = new JButton("Close"); + closeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(final ActionEvent e) { + dialog.dispose(); + } + }); + buttonPanel.add(closeButton); + + dialog.add(buttonPanel, BorderLayout.SOUTH); + dialog.pack(); + dialog.setLocationRelativeTo(this); + dialog.setVisible(true); + } + + private void updateDisplay() { + if (updating) { + return; + } + updating = true; + try { + updateCameraLabel(); + updateCullingStatistics(); + updateRenderThreadsLabel(); + updateFrameRateLabels(); + updateLogDisplay(); + threadTimeline.repaint(); + } finally { + updating = false; + } + } + + private void updateCameraLabel() { + if (viewPanel == null) { + return; + } + + final Camera camera = viewPanel.getCamera(); + final Point3D pos = camera.getTransform().getTranslation(); + final double[] angles = camera.getTransform().getRotation().toAngles(); + + cameraLabel.setText(String.format("%.2f, %.2f, %.2f, %.2f, %.2f, %.2f", + pos.x, pos.y, pos.z, angles[0], angles[1], angles[2])); + } + + private void updateCullingStatistics() { + if (viewPanel == null) { + return; + } + + // Get the current rendering context from view panel's last render + final RenderingContext context = viewPanel.getRenderingContext(); + if (context == null || context.cullingStatistics == null) { + totalCompositesLabel.setText("-"); + culledCompositesLabel.setText("-"); + culledPercentLabel.setText("-"); + return; + } + + final CullingStatistics stats = context.cullingStatistics; + totalCompositesLabel.setText(String.valueOf(stats.totalComposites.get())); + culledCompositesLabel.setText(String.valueOf(stats.culledComposites.get())); + culledPercentLabel.setText(String.format(" (%.1f%%)", stats.getCulledPercentage())); + } + + private void updateRenderThreadsLabel() { + if (viewPanel == null) { + return; + } + renderThreadsLabel.setText(String.valueOf(viewPanel.getNumRenderThreads())); + } + + private void updateFrameRateLabels() { + if (viewPanel == null) { + return; + } + final int target = viewPanel.getTargetFPS(); + targetFpsLabel.setText(target > 0 ? String.valueOf(target) : "unlimited"); + measuredFpsLabel.setText(String.format("%.1f", viewPanel.getMeasuredFPS())); + } + + private void updateLogDisplay() { + final List entries = debugLogBuffer.getEntries(); + final StringBuilder sb = new StringBuilder(); + for (final String entry : entries) { + sb.append(entry).append('\n'); + } + logArea.setText(sb.toString()); + + final JScrollBar vertical = ((JScrollPane) logArea.getParent().getParent()) + .getVerticalScrollBar(); + vertical.setValue(vertical.getMaximum()); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeviceHotplug.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeviceHotplug.java new file mode 100644 index 0000000..ab8c26d --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/DeviceHotplug.java @@ -0,0 +1,78 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui; + +import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadTracker; +import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadTrackingManager; +import eu.svjatoslav.aukio.e3d.gui.spacemouse.SpaceMouseManager; +import eu.svjatoslav.aukio.e3d.gui.spacemouse.SpaceNavigatorHid; + +/** + * Optional input-device hot-plug for a {@link ViewPanel}: starts the + * RayNeo head-tracking manager and the SpaceNavigator 6DOF-mouse manager + * (each auto-detects its device even when plugged in after startup), and + * exposes the currently connected device, if any. + * + *

Extracted from ViewPanel to keep the panel focused on the render + * pipeline. Either device can be disabled with + * {@code -De3d.headtrack=false} / {@code -De3d.spacemouse=false}.

+ */ +final class DeviceHotplug { + + /** Head tracker hot-plug manager, unless disabled via e3d.headtrack=false. */ + private HeadTrackingManager headTrackingManager; + + /** SpaceNavigator hot-plug manager, unless disabled via e3d.spacemouse=false. */ + private SpaceMouseManager spaceMouseManager; + + /** + * Starts both hot-plug managers for the given panel (respecting the + * disable properties). + * + * @param viewPanel the panel whose camera the devices will drive + */ + DeviceHotplug(final ViewPanel viewPanel) { + if (!"false".equalsIgnoreCase( + System.getProperty("e3d.headtrack", "true"))) { + headTrackingManager = new HeadTrackingManager(viewPanel); + headTrackingManager.start(); + } + if (!"false".equalsIgnoreCase( + System.getProperty("e3d.spacemouse", "true"))) { + spaceMouseManager = new SpaceMouseManager(viewPanel); + spaceMouseManager.start(); + } + } + + /** + * Returns the active SpaceNavigator device, or null when no 6DOF + * mouse is currently connected. + */ + SpaceNavigatorHid getSpaceMouse() { + return spaceMouseManager == null ? null + : spaceMouseManager.getDevice(); + } + + /** + * Returns the active head tracker, or null when no glasses are + * currently connected. + */ + HeadTracker getHeadTracker() { + return headTrackingManager == null ? null + : headTrackingManager.getTracker(); + } + + /** Stops both hot-plug managers; safe to call more than once. */ + void stop() { + if (headTrackingManager != null) { + headTrackingManager.stop(); + headTrackingManager = null; + } + if (spaceMouseManager != null) { + spaceMouseManager.stop(); + spaceMouseManager = null; + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/FrameListener.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/FrameListener.java new file mode 100644 index 0000000..f7f12b9 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/FrameListener.java @@ -0,0 +1,52 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui; + +/** + * Listener interface for per-frame callbacks before the 3D scene is rendered. + * + *

Implement this interface and register it with + * {@link ViewPanel#addFrameListener(FrameListener)} to receive a callback + * before each frame. This is the primary mechanism for implementing animations, + * physics updates, and other time-dependent behavior.

+ * + *

Usage example - animating a shape:

+ *
{@code
+ * viewPanel.addFrameListener((panel, deltaMs) -> {
+ *     // Rotate the shape a little each frame
+ *     double angleIncrement = deltaMs * 0.001;  // radians per millisecond
+ *     myShape.setTransform(new Transform(
+ *         myShape.getLocation(),
+ *         currentAngle += angleIncrement, 0
+ *     ));
+ *     return true;  // request repaint since we changed something
+ * });
+ * }
+ * + *

The engine uses the return values to optimize rendering: if no listener + * returns {@code true} and no other changes occurred, the frame is skipped + * to save CPU and energy.

+ * + * @see ViewPanel#addFrameListener(FrameListener) + * @see ViewPanel#removeFrameListener(FrameListener) + */ +public interface FrameListener { + + /** + * Called before each frame render, allowing the listener to update state + * and indicate whether a repaint is needed. + * + *

Each registered listener is called exactly once per frame tick. + * The frame is only rendered if at least one listener returns {@code true} + * (or if the view was explicitly marked for repaint).

+ * + * @param viewPanel the view panel being rendered + * @param millisecondsSinceLastFrame time elapsed since the previous frame, + * for frame-rate-independent updates + * @return {@code true} if the view should be re-rendered this frame, + * {@code false} if this listener has no visual changes + */ + boolean onFrame(ViewPanel viewPanel, int millisecondsSinceLastFrame); +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/GuiComponent.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/GuiComponent.java new file mode 100644 index 0000000..1ee05df --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/GuiComponent.java @@ -0,0 +1,222 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui; + +import eu.svjatoslav.aukio.e3d.geometry.Box; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardFocusStack; +import eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardHelper; +import eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardInputHandler; +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent; +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController; +import eu.svjatoslav.aukio.e3d.math.Transform; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe.WireframeBox; + +import java.awt.event.KeyEvent; + +/** + * Base class for interactive GUI components rendered in 3D space. + * + *

{@code GuiComponent} combines a composite shape with keyboard and mouse interaction + * handling. When clicked, it acquires keyboard focus (via the {@link eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardFocusStack}), + * and a red wireframe border is displayed to indicate focus. Pressing ESC releases focus.

+ * + *

This class is the foundation for interactive widgets like the + * {@link eu.svjatoslav.aukio.e3d.gui.textEditorComponent.TextEditComponent}.

+ * + *

Usage example - creating a custom GUI component:

+ *
{@code
+ * GuiComponent myWidget = new GuiComponent(
+ *     new Transform(new Point3D(0, 0, 300)),
+ *     viewPanel,
+ *     new Point3D(400, 300, 0)  // width, height, depth
+ * );
+ *
+ * // Add visual content to the widget
+ * myWidget.addShape(someTextCanvas);
+ *
+ * // Add to the scene
+ * viewPanel.getRootShapeCollection().addShape(myWidget);
+ * }
+ * + * @see eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardFocusStack manages which component has keyboard focus + * @see eu.svjatoslav.aukio.e3d.gui.textEditorComponent.TextEditComponent a full text editor built on this class + */ +public class GuiComponent extends AbstractCompositeShape implements + KeyboardInputHandler, MouseInteractionController { + + private static final String GROUP_GUI_FOCUS = "gui.focus"; + + /** + * The view panel this component is attached to. + */ + public final ViewPanel viewPanel; + Box containingBox = new Box(); + private WireframeBox borders = null; + + private boolean borderShown = false; + + /** + * Creates a GUI component with the specified transform, view panel, and bounding box size. + * + * @param transform the position and orientation of the component in 3D space + * @param viewPanel the view panel this component belongs to + * @param size the bounding box dimensions (width, height, depth) + */ + public GuiComponent(final Transform transform, + final ViewPanel viewPanel, final Point3D size) { + super(transform); + this.viewPanel = viewPanel; + setDimensions(size); + } + + private WireframeBox createBorder() { + final LineAppearance appearance = new LineAppearance(10, + new eu.svjatoslav.aukio.e3d.renderer.raster.Color(255, 0, 0, 100)); + + final double borderSize = 10; + + final Box borderArea = containingBox.clone().enlarge(borderSize); + + return new WireframeBox(borderArea, appearance); + } + + @Override + public boolean focusLost(final ViewPanel viewPanel) { + hideBorder(); + return true; + } + + @Override + public boolean focusReceived(final ViewPanel viewPanel) { + showBorder(); + return true; + } + + /** + * Returns whether this component currently holds keyboard focus. + * + * @return {@code true} when focused (focus border is shown) + */ + public boolean hasKeyboardFocus() { + return borderShown; + } + + /** + * Returns the wireframe border box for this component. + * + * @return the border wireframe box + */ + public WireframeBox getBorders() { + if (borders == null) + borders = createBorder(); + return borders; + } + + /** + * Returns the depth of this component's bounding box. + * + * @return the depth in pixels + */ + public int getDepth() { + return (int) containingBox.getDepth(); + } + + /** + * Returns the height of this component's bounding box. + * + * @return the height in pixels + */ + public int getHeight() { + return (int) containingBox.getHeight(); + } + + /** + * Returns the width of this component's bounding box. + * + * @return the width in pixels + */ + public int getWidth() { + return (int) containingBox.getWidth(); + } + + /** + * Hides the focus border around this component. + */ + public void hideBorder() { + if (!borderShown) + return; + borderShown = false; + removeGroup(GROUP_GUI_FOCUS); + } + + @Override + public boolean keyPressed(final KeyEvent event, final ViewPanel viewPanel) { + if (event.getKeyChar() == KeyboardHelper.ESC) + viewPanel.getKeyboardFocusStack().popFocusOwner(); + return true; + } + + @Override + public boolean keyReleased(final KeyEvent event, final ViewPanel viewPanel) { + return false; + } + + @Override + public boolean mouseClicked(int button) { + // Direct-call path (no dispatch, no focus stack supplied). Only + // works for components constructed with a live panel; headless-built + // components (null panel) ignore the click instead of throwing. + if (viewPanel == null) + return false; + return mouseClicked(button, Double.NaN, Double.NaN, + viewPanel.getKeyboardFocusStack()); + } + + @Override + public boolean mouseClicked(final int button, final double textureU, + final double textureV, + final KeyboardFocusStack focusStack) { + if (button == MouseEvent.BUTTON_MIDDLE) { + // middle click releases keyboard focus, like ESC + focusStack.popFocusOwner(); + return true; + } + return focusStack.pushFocusOwner(this); + } + + @Override + public boolean mouseWheelMoved(final int verticalUnits, + final int horizontalUnits) { + // a focused GUI component owns the scroll wheel (the camera must + // not move while a component is focused); subclasses like the + // terminal and browser panels forward the scroll to their app + return true; + } + + @Override + public boolean mouseEntered() { + return false; + } + + @Override + public boolean mouseExited() { + return false; + } + + private void setDimensions(final Point3D size) { + containingBox.setBoxSize(size); + } + + private void showBorder() { + if (borderShown) + return; + borderShown = true; + addShape(getBorders(), GROUP_GUI_FOCUS); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/TextPointer.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/TextPointer.java new file mode 100755 index 0000000..fc057a5 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/TextPointer.java @@ -0,0 +1,123 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui; + +import static java.lang.Integer.compare; + +/** + * A pointer to a character in a text using row and column. + *

+ * It can be used to represent a cursor position in a text. + * Also, it can be used to represent beginning and end of a selection. + */ +public class TextPointer implements Comparable { + + /** + * The row of the character. Starts from 0. + */ + public int row; + + /** + * The column of the character. Starts from 0. + */ + public int column; + + /** + * Creates a text pointer at position (0, 0). + */ + public TextPointer() { + this(0, 0); + } + + /** + * Creates a text pointer at the specified row and column. + * + * @param row the row index (0-based) + * @param column the column index (0-based) + */ + public TextPointer(final int row, final int column) { + this.row = row; + this.column = column; + } + + /** + * Creates a text pointer by copying another text pointer. + * + * @param parent the text pointer to copy + */ + public TextPointer(final TextPointer parent) { + this(parent.row, parent.column); + } + + @Override + public boolean equals(final Object o) { + if (o == null) return false; + + return o instanceof TextPointer && compareTo((TextPointer) o) == 0; + } + + @Override + public int hashCode() { + int result = row; + result = 31 * result + column; + return result; + } + + /** + * Compares this pointer to another pointer. + * + * @param textPointer The pointer to compare to. + * @return

    + *
  • -1 if this pointer is smaller than the argument pointer.
  • + *
  • 0 if they are equal.
  • + *
  • 1 if this pointer is bigger than the argument pointer.
  • + *
+ */ + @Override + public int compareTo(final TextPointer textPointer) { + + if (row < textPointer.row) + return -1; + if (row > textPointer.row) + return 1; + + return compare(column, textPointer.column); + } + + /** + * Checks if this pointer is between the argument pointers. + *

+ * This pointer is considered to be between the pointers if it is bigger or equal to the start pointer + * and smaller than the end pointer. + * + * @param start The start pointer. + * @param end The end pointer. + * @return True if this pointer is between the specified pointers. + */ + public boolean isBetween(final TextPointer start, final TextPointer end) { + + if (start == null) + return false; + + if (end == null) + return false; + + // Make sure that start is smaller than end. + TextPointer smaller; + TextPointer bigger; + + if (end.compareTo(start) >= 0) { + smaller = start; + bigger = end; + } else { + smaller = end; + bigger = start; + } + + // Check if this pointer is between the specified pointers. + return (compareTo(smaller) >= 0) && (bigger.compareTo(this) > 0); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadTimelineComponent.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadTimelineComponent.java new file mode 100644 index 0000000..ad9bf35 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ThreadTimelineComponent.java @@ -0,0 +1,323 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui; +import eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; + +/** + * Per-thread activity timeline for the Developer Tools window — the + * software-renderer equivalent of a GPU frame profiler's occupancy view. + * + *

One row per worker thread, plus the render and present threads + * pinned to the top. Worker rows are labeled CPU 01..N in a stable + * order. Time on the X axis, interval kind as color. Black gaps are + * idle time — the spots + * where a core had no work, which is what this view exists to find. In a + * perfectly pipelined renderer every worker row is solid: when transform + * of frame N+1 (yellow-green) starts appearing inside paint of frame N + * (blue), the pipeline is overlapping as intended.

+ * + *

While recording, the view shows a live-scrolling window of the last + * ~100 ms. After Record is toggled off the capture freezes and can be + * scrolled back through the whole retained history (the ring buffer + * keeps roughly the last 10+ seconds at typical task rates) with the + * scrollbar or the mouse wheel.

+ * + *

Reads the {@link ThreadActivityRecorder} ring buffer directly on + * repaint; recording itself is toggled from the parent panel.

+ */ +public class ThreadTimelineComponent extends JComponent { + + /** Time window shown, in nanoseconds. Adjustable with ctrl+wheel. */ + private long windowNanos = 100_000_000L; + + /** Minimum and maximum zoom window, nanoseconds. */ + private static final long MIN_WINDOW_NANOS = 5_000_000L; + private static final long MAX_WINDOW_NANOS = 2_000_000_000L; + + private static final int ROW_HEIGHT = 11; + private static final int LABEL_WIDTH = 64; + private static final int LEGEND_HEIGHT = 16; + /** Rows the component reserves height for, so all workers + render fit. */ + private static final int RESERVED_ROWS = 26; + + /** Colors per recorded kind: three frame parities each for transform/paint/bin, then 9..11. */ + private static final Color[] KIND_COLORS = { + new Color(0x2E, 0xCC, 0x40), // transform, frame%3=0 — green + new Color(0xB8, 0xD9, 0x00), // transform, frame%3=1 — yellow-green + new Color(0x6B, 0x8E, 0x23), // transform, frame%3=2 — olive + new Color(0x00, 0x74, 0xD9), // paint, frame%3=0 — blue + new Color(0xF0, 0x12, 0xBE), // paint, frame%3=1 — magenta + new Color(0xB1, 0x0D, 0xC9), // paint, frame%3=2 — purple + new Color(0x39, 0xCC, 0xCC), // binning, frame%3=0 — cyan + new Color(0x00, 0x8B, 0x8B), // binning, frame%3=1 — dark cyan + new Color(0x00, 0x70, 0x70), // binning, frame%3=2 — teal + new Color(0xA0, 0xA0, 0xA0), // render-thread serial — gray + new Color(0x8B, 0x00, 0x00), // render-thread waiting— dark red + new Color(0xFF, 0xFF, 0xFF), // blit — white + new Color(0xFF, 0x85, 0x1B), // continuation (drain/sort/bin) — orange + new Color(0x8B, 0x45, 0x13), // drain+merge — brown + new Color(0xFF, 0xD7, 0x00), // depth sort — gold + }; + + private static final String[] LEGEND = { + "transform f0", "transform f1", "transform f2", + "paint f0", "paint f1", "paint f2", + "bin f0", "bin f1", "bin f2", + "render serial", "blocked", "blit", "sort+bin", "drain", "sort", + }; + + /** + * Scrollbar for navigating a frozen capture, in milliseconds scrolled + * back from the latest recorded interval. Owned here so the component + * can keep the model in sync with the capture length; the parent + * panel adds it below the timeline. + */ + public final JScrollBar scrollBar = new JScrollBar(JScrollBar.HORIZONTAL, 0, 100, 0, 100); + + /** + * How far back from the latest recorded interval the window ends, + * nanoseconds. Always 0 (live edge) while recording; user-adjustable + * on a frozen capture. + */ + private long scrollBackNanos = 0; + + /** True while the scrollbar model is being updated programmatically. */ + private boolean updatingScrollBar = false; + + public ThreadTimelineComponent() { + setBackground(Color.BLACK); + + scrollBar.addAdjustmentListener(new AdjustmentListener() { + @Override + public void adjustmentValueChanged(final AdjustmentEvent e) { + if (!updatingScrollBar) { + scrollBackNanos = scrollBar.getValue() * 1_000_000L; + repaint(); + } + } + }); + + addMouseWheelListener(new MouseWheelListener() { + @Override + public void mouseWheelMoved(final MouseWheelEvent e) { + if (e.isControlDown()) { + // Zoom around the current window, x1.25 per notch + for (int i = 0; i < Math.abs(e.getWheelRotation()); i++) { + windowNanos = e.getWheelRotation() < 0 + ? Math.max(MIN_WINDOW_NANOS, windowNanos * 4 / 5) + : Math.min(MAX_WINDOW_NANOS, windowNanos * 5 / 4); + } + } else { + if (ThreadActivityRecorder.isEnabled()) { + return; // live mode: nothing to scroll + } + scrollBackNanos = Math.max(0, scrollBackNanos + + e.getWheelRotation() * Math.max(1_000_000L, windowNanos / 10)); + } + repaint(); + } + }); + } + + @Override + public Dimension getPreferredSize() { + // Reserve full height up front: worker rows appear lazily as the + // pool starts, after the window has already been packed + return new Dimension(560, LEGEND_HEIGHT + RESERVED_ROWS * ROW_HEIGHT + 4); + } + + @Override + protected void paintComponent(final Graphics graphics) { + super.paintComponent(graphics); + final Graphics2D g = (Graphics2D) graphics; + final int width = getWidth(); + final int trackWidth = width - LABEL_WIDTH; + + g.setColor(Color.BLACK); + g.fillRect(0, 0, width, getHeight()); + + // Legend + g.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 9)); + int legendX = 2; + for (int kind = 0; kind < LEGEND.length; kind++) { + g.setColor(KIND_COLORS[kind]); + g.fillRect(legendX, 3, 8, 8); + g.setColor(Color.LIGHT_GRAY); + g.drawString(LEGEND[kind], legendX + 10, 11); + legendX += 10 + g.getFontMetrics().stringWidth(LEGEND[kind]) + 10; + } + + final boolean recording = ThreadActivityRecorder.isEnabled(); + if (!recording && ThreadActivityRecorder.cursor() == 0) { + g.setColor(Color.GRAY); + g.drawString("recording off — press Record to capture thread activity", + LABEL_WIDTH + 8, LEGEND_HEIGHT + ROW_HEIGHT); + syncScrollBar(0, 0, true); + return; + } + + final long[] starts = ThreadActivityRecorder.starts(); + final long[] ends = ThreadActivityRecorder.ends(); + final byte[] kinds = ThreadActivityRecorder.kinds(); + final byte[] rows = ThreadActivityRecorder.rows(); + final int capacity = ThreadActivityRecorder.capacity(); + + // Capture range: oldest retained interval to latest one + long latest = 0; + long oldest = Long.MAX_VALUE; + for (int i = 0; i < capacity; i++) { + if (starts[i] != 0) { + if (ends[i] > latest) { + latest = ends[i]; + } + if (starts[i] < oldest) { + oldest = starts[i]; + } + } + } + if (latest == 0) { + return; + } + if (oldest > latest - windowNanos) { + oldest = latest - windowNanos; + } + + // Live edge while recording; frozen offset afterwards + if (recording) { + scrollBackNanos = 0; + } + final long maxScrollBack = latest - oldest - windowNanos; + scrollBackNanos = Math.max(0, Math.min(scrollBackNanos, Math.max(0, maxScrollBack))); + + final long windowEnd = latest - scrollBackNanos; + final long windowStart = windowEnd - windowNanos; + final double nanosPerPixel = (double) windowNanos / trackWidth; + + final int rowCount = ThreadActivityRecorder.rowCount(); + + // Visual row order: "render" and "present" pinned to the top + // (they answer "where is the serial time" and "where is the + // display time"), workers below in recorder-row order. Recorder + // rows are assigned once per thread name, so this order is + // stable for the whole capture. + int renderRow = -1; + int presentRow = -1; + for (int row = 0; row < rowCount; row++) { + final String name = ThreadActivityRecorder.rowName(row); + if ("render".equals(name)) { + renderRow = row; + } else if ("present".equals(name)) { + presentRow = row; + } + } + final int[] visualOf = new int[rowCount]; + int nextVisual = 0; + if (renderRow >= 0) { + visualOf[renderRow] = nextVisual++; + } + if (presentRow >= 0) { + visualOf[presentRow] = nextVisual++; + } + for (int row = 0; row < rowCount; row++) { + if (row != renderRow && row != presentRow) { + visualOf[row] = nextVisual++; + } + } + + // Row labels and separators. Workers get sequential CPU numbers + // by visual position — the thread-name suffix (worker-27 etc.) + // is just a pool counter and carries no meaning. + int cpuNumber = 0; + final String[] labels = new String[rowCount]; + for (int row = 0; row < rowCount; row++) { + if (row == renderRow) { + labels[row] = "render"; + } else if (row == presentRow) { + labels[row] = "present"; + } else { + labels[row] = String.format("CPU %02d", ++cpuNumber); + } + } + g.setColor(Color.DARK_GRAY); + for (int row = 0; row < rowCount; row++) { + final int y = LEGEND_HEIGHT + visualOf[row] * ROW_HEIGHT; + g.drawLine(LABEL_WIDTH, y + ROW_HEIGHT - 1, width, y + ROW_HEIGHT - 1); + g.setColor(Color.GRAY); + g.drawString(labels[row], 4, y + ROW_HEIGHT - 3); + g.setColor(Color.DARK_GRAY); + } + + // Intervals + for (int i = 0; i < capacity; i++) { + final long t0 = starts[i]; + if (t0 == 0 || ends[i] < windowStart || t0 > windowEnd) { + continue; + } + final int kind = kinds[i]; + if (kind < 0 || kind >= KIND_COLORS.length) { + continue; + } + final int x0 = LABEL_WIDTH + (int) ((Math.max(t0, windowStart) - windowStart) / nanosPerPixel); + final int x1 = LABEL_WIDTH + (int) ((Math.min(ends[i], windowEnd) - windowStart) / nanosPerPixel); + g.setColor(KIND_COLORS[kind]); + g.fillRect(x0, LEGEND_HEIGHT + visualOf[rows[i]] * ROW_HEIGHT + 1, + Math.max(1, x1 - x0), ROW_HEIGHT - 2); + } + + // Time grid: pick a tick spacing that keeps ~10 ticks in view + long tickSpacing = 1_000_000L; // 1 ms + while (windowNanos / tickSpacing > 20) { + tickSpacing *= 10; + } + while (windowNanos / tickSpacing < 5 && tickSpacing > 100_000L) { + tickSpacing /= 10; + } + g.setColor(new Color(40, 40, 40)); + final long firstTick = (windowStart / tickSpacing + 1) * tickSpacing; + for (long tick = firstTick; tick < windowEnd; tick += tickSpacing) { + final int x = LABEL_WIDTH + (int) ((tick - windowStart) / nanosPerPixel); + g.drawLine(x, LEGEND_HEIGHT, x, LEGEND_HEIGHT + rowCount * ROW_HEIGHT); + } + + // Window size + frozen-offset readout, bottom right of the track + g.setColor(Color.YELLOW); + final String readout = (windowNanos >= 1_000_000_000L + ? String.format("%.1f s", windowNanos / 1e9) + : String.format("%.0f ms", windowNanos / 1e6)) + + (recording ? " live" : String.format(" -%.2f s", scrollBackNanos / 1e9)); + g.drawString(readout, width - g.getFontMetrics().stringWidth(readout) - 6, + LEGEND_HEIGHT + rowCount * ROW_HEIGHT - 4); + + syncScrollBar((int) (scrollBackNanos / 1_000_000L), + (int) (Math.max(0, maxScrollBack) / 1_000_000L) + (int) (windowNanos / 1_000_000L), + recording); + } + + /** + * Pushes the current scroll state into the scrollbar model without + * feeding back into the adjustment listener. + * + * @param valueMs current scroll-back offset in milliseconds + * @param maxMs maximum scroll-back offset plus window size + * @param disabled true to grey out the scrollbar (live recording) + */ + private void syncScrollBar(final int valueMs, final int maxMs, final boolean disabled) { + final int visibleMs = Math.max(1, (int) (windowNanos / 1_000_000L)); + updatingScrollBar = true; + try { + scrollBar.setEnabled(!disabled && maxMs > visibleMs); + scrollBar.setValues(Math.min(valueMs, maxMs), visibleMs, 0, Math.max(visibleMs, maxMs)); + } finally { + updatingScrollBar = false; + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewFrame.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewFrame.java new file mode 100755 index 0000000..2dfe340 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewFrame.java @@ -0,0 +1,345 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; + +/** + * Convenience window (JFrame) that creates and hosts a {@link ViewPanel} for 3D rendering. + * + *

This is the simplest way to get a 3D view up and running. The frame starts + * maximized, enforces a minimum size of 400x400, and handles window lifecycle + * events (minimizing, restoring, closing) automatically.

+ * + *

Quick start:

+ *
{@code
+ * // Create a window with a 3D view
+ * ViewFrame frame = new ViewFrame();
+ *
+ * // Access the view panel to add shapes and configure the scene
+ * ViewPanel viewPanel = frame.getViewPanel();
+ * viewPanel.getRootShapeCollection().addShape(
+ *     new WireframeCube(new Point3D(0, 0, 200), 50,
+ *         new LineAppearance(5, Color.GREEN))
+ * );
+ *
+ * // To close programmatically:
+ * frame.exit();
+ * }
+ * + * @see ViewPanel the embedded 3D rendering panel + */ +public class ViewFrame extends JFrame implements WindowListener { + + private static final long serialVersionUID = -7037635097739548470L; + + /** The embedded 3D view panel. */ + private final ViewPanel viewPanel; + + /** Whether the frame is currently in fullscreen exclusive mode. */ + private boolean fullscreen = false; + + /** Saved window bounds before entering fullscreen, used to restore on exit. */ + private Rectangle previousBounds; + + /** Saved extended state before entering fullscreen. */ + private int previousExtendedState; + + /** + * Creates a new maximized window with a 3D view. + */ + public ViewFrame() { + this("3D engine", -1, -1, true); + } + + /** + * Creates a new maximized window with a 3D view and custom title. + * + * @param title the window title to display + */ + public ViewFrame(final String title) { + this(title, -1, -1, true); + } + + /** + * Creates a new window with a 3D view at the specified size. + * + * @param width window width in pixels, or -1 for default + * @param height window height in pixels, or -1 for default + */ + public ViewFrame(final int width, final int height) { + this("3D engine", width, height, false); + } + + /** + * Creates a new window with a 3D view at the specified size with a custom title. + * + * @param title the window title to display + * @param width window width in pixels, or -1 for default + * @param height window height in pixels, or -1 for default + */ + public ViewFrame(final String title, final int width, final int height) { + this(title, width, height, false); + } + + private ViewFrame(final String title, final int width, final int height, final boolean maximize) { + setTitle(title); + + addWindowListener(new java.awt.event.WindowAdapter() { + @Override + public void windowClosing(final java.awt.event.WindowEvent e) { + exit(); + } + }); + + viewPanel = new ViewPanel(); + + add(getViewPanel()); + + if (width > 0 && height > 0) { + setSize(width, height); + } else { + setSize(800, 600); + } + + if (maximize) { + setExtendedState(JFrame.MAXIMIZED_BOTH); + } + setVisible(true); + validate(); + + addResizeListener(); + addWindowListener(this); + } + + private void addResizeListener() { + addComponentListener(new ComponentListener() { + // This method is called after the component's size changes + @Override + public void componentHidden(final ComponentEvent e) { + } + + @Override + public void componentMoved(final ComponentEvent e) { + } + + @Override + public void componentResized(final ComponentEvent evt) { + + final Component c = (Component) evt.getSource(); + + // Get new size + final Dimension newSize = c.getSize(); + + boolean sizeFixed = false; + + if (newSize.width < 400) { + newSize.width = 400; + sizeFixed = true; + } + + if (newSize.height < 400) { + newSize.height = 400; + sizeFixed = true; + } + + if (sizeFixed) + setSize(newSize); + + } + + @Override + public void componentShown(final ComponentEvent e) { + viewPanel.repaintDuringNextViewUpdate(); + } + + }); + } + + /** + * Exit the application. + */ + public void exit() { + if (getViewPanel() != null) { + getViewPanel().stop(); + getViewPanel().setEnabled(false); + getViewPanel().setVisible(false); + } + dispose(); + } + + @Override + public java.awt.Dimension getPreferredSize() { + return new java.awt.Dimension(640, 480); + } + + /** + * Returns the embedded {@link ViewPanel} for adding shapes and configuring the scene. + * + * @return the view panel contained in this frame + */ + public ViewPanel getViewPanel() { + return viewPanel; + } + + /** + * Returns whether the frame is currently in fullscreen exclusive mode. + * + * @return {@code true} if the frame is in fullscreen exclusive mode + */ + public boolean isFullscreen() { + return fullscreen; + } + + /** + * Enters or exits fullscreen exclusive mode (FSEM). + * + *

Fullscreen always targets the screen the window is currently + * placed on (its {@link GraphicsConfiguration#getDevice()}), so a + * window dragged onto e.g. XR glasses goes fullscreen there, not on + * the primary monitor. The default screen device is only a fallback + * for a not-yet-displayed frame.

+ * + *

When entering fullscreen, the current window bounds and extended state are + * saved so they can be restored on exit. The frame is passed to the screen + * device as the fullscreen window, which automatically removes decorations and + * covers the entire screen. No explicit {@code setUndecorated()} call is needed — + * in fact, calling it would be harmful because it requires {@code dispose()} which + * destroys the Canvas and its BufferStrategy.

+ * + *

When exiting fullscreen, the frame is released from the screen device, + * decorations are restored automatically, and previous bounds/extended state + * are restored.

+ * + *

This method is idempotent: setting fullscreen to its current value + * is a no-op and returns {@code true}.

+ * + * @param on {@code true} to enter fullscreen, {@code false} to exit fullscreen + * @return {@code true} if the state changed or was already as requested, {@code false} + * if the transition could not be performed (e.g. unsupported graphics device) + */ + public boolean setFullscreen(final boolean on) { + if (this.fullscreen == on) + return true; + + GraphicsDevice device = null; + final GraphicsConfiguration configuration = getGraphicsConfiguration(); + if (configuration != null) + device = configuration.getDevice(); + if (device == null) + device = GraphicsEnvironment.getLocalGraphicsEnvironment() + .getDefaultScreenDevice(); + + if (!device.isFullScreenSupported()) + return false; + + try { + if (on) { + // Save current state before entering fullscreen + previousBounds = getBounds(); + previousExtendedState = getExtendedState(); + + // GraphicsDevice.setFullScreenWindow() automatically removes + // decorations when entering FSEM and restores them on exit. + // Do NOT call setUndecorated() here — it requires the frame to be + // non-displayable (dispose first) which destroys the Canvas and + // its BufferStrategy, breaking the render thread. + device.setFullScreenWindow(this); + fullscreen = true; + } else { + // Exit fullscreen exclusive mode. + // Decorations are restored automatically by the graphics device. + device.setFullScreenWindow(null); + + // Restore previous bounds and extended state + if (previousBounds != null) { + setBounds(previousBounds); + } + setExtendedState(previousExtendedState); + + fullscreen = false; + + // Trigger a repaint since the rendering surface changed + viewPanel.repaintDuringNextViewUpdate(); + } + } catch (final Exception e) { + // Something went wrong — revert if we were trying to enter + if (on) { + try { + device.setFullScreenWindow(null); + if (previousBounds != null) + setBounds(previousBounds); + setExtendedState(previousExtendedState); + } catch (final Exception ignored) { + } + } + return false; + } + + return true; + } + + /** + * Toggles between fullscreen exclusive mode and windowed mode. + * + *

Equivalent to {@code setFullscreen(!isFullscreen())}.

+ * + * @return {@code true} if the toggle succeeded + * @see #setFullscreen(boolean) + * @see #isFullscreen() + */ + public boolean toggleFullscreen() { + return setFullscreen(!fullscreen); + } + + @Override + public void windowActivated(final WindowEvent e) { + viewPanel.repaintDuringNextViewUpdate(); + viewPanel.requestFocus(); + } + + @Override + public void windowClosed(final WindowEvent e) { + } + + @Override + public void windowClosing(final WindowEvent e) { + } + + @Override + public void windowDeactivated(final WindowEvent e) { + } + + /** + * Repaint the view when the window is deiconified. + * + * Deiconified means that the window is restored from minimized state. + */ + @Override + public void windowDeiconified(final WindowEvent e) { + viewPanel.repaintDuringNextViewUpdate(); + } + + /** + * Do nothing when the window is iconified. + * + * Iconified means that the window is minimized. + * @param e the event to be processed + */ + @Override + public void windowIconified(final WindowEvent e) { + } + + @Override + public void windowOpened(final WindowEvent e) { + viewPanel.repaintDuringNextViewUpdate(); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java new file mode 100755 index 0000000..9203394 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewPanel.java @@ -0,0 +1,1575 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui; +import eu.svjatoslav.aukio.e3d.diag.DebugLogBuffer; +import eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder; +import eu.svjatoslav.aukio.e3d.geometry.Camera; +import eu.svjatoslav.aukio.e3d.renderer.raster.StereoEye; +import eu.svjatoslav.aukio.e3d.renderer.raster.SegmentRenderingContext; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; + +import eu.svjatoslav.aukio.e3d.diag.Diagnostics; +import eu.svjatoslav.aukio.e3d.diag.EngineConfig; +import eu.svjatoslav.aukio.e3d.diag.Telemetry; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadLookController; +import eu.svjatoslav.aukio.e3d.gui.headtrack.HeadTracker; +import eu.svjatoslav.aukio.e3d.gui.headtrack.RayNeoHid; +import eu.svjatoslav.aukio.e3d.gui.spacemouse.SpaceNavigatorHid; +import eu.svjatoslav.aukio.e3d.gui.humaninput.InputManager; +import eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardFocusStack; +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent; +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection; +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager; + +import java.awt.*; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.image.BufferStrategy; +import java.util.Arrays; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * AWT Canvas that provides a 3D rendering surface with built-in camera navigation. + * + *

{@code ViewPanel} is the primary entry point for embedding the Aukio 3D engine into + * a Java application. It manages the render loop, maintains a scene graph + * ({@link ShapeCollection}), and handles user input for camera navigation.

+ * + *

Uses {@link BufferStrategy} for efficient page-flipping and tear-free rendering.

+ * + *

Quick start - creating a 3D view in a window:

+ *
{@code
+ * // Option 1: Use ViewFrame (creates a maximized JFrame for you)
+ * ViewFrame frame = new ViewFrame();
+ * ViewPanel viewPanel = frame.getViewPanel();
+ *
+ * // Option 2: Embed ViewPanel in your own window
+ * JFrame frame = new JFrame("My 3D App");
+ * ViewPanel viewPanel = new ViewPanel();
+ * frame.add(viewPanel);
+ * frame.setSize(800, 600);
+ * frame.setVisible(true);
+ *
+ * // Add shapes to the scene
+ * ShapeCollection scene = viewPanel.getRootShapeCollection();
+ * scene.addShape(new WireframeCube(
+ *     new Point3D(0, 0, 200), 50,
+ *     new LineAppearance(5, Color.GREEN)
+ * ));
+ *
+ * // Position the camera
+ * viewPanel.getCamera().setLocation(new Point3D(0, 0, -100));
+ *
+ * // Listen for frame updates (e.g., for animations)
+ * viewPanel.addFrameListener((panel, deltaMs) -> {
+ *     // Called before each frame. Return true to force repaint.
+ *     return false;
+ * });
+ * }
+ * + *

Architecture:

+ *
    + *
  • A background render thread continuously generates frames at the target FPS
  • + *
  • The engine intelligently skips rendering when no visual changes are detected
  • + *
  • {@link FrameListener}s are notified before each potential frame, enabling animations
  • + *
  • Mouse/keyboard input is managed by {@link InputManager}
  • + *
  • Keyboard focus is managed by {@link KeyboardFocusStack}
  • + *
+ * + * @see ViewFrame convenience window wrapper + * @see ShapeCollection the scene graph + * @see Camera the camera/viewer + * @see FrameListener for per-frame callbacks + */ +public class ViewPanel extends Canvas { + private static final long serialVersionUID = 1683277888885045387L; + private static final int NUM_BUFFERS = 2; + + /** The input manager handling mouse and keyboard events. */ + private final InputManager inputManager = new InputManager(this); + /** The stack managing keyboard focus for GUI components. */ + private final KeyboardFocusStack keyboardFocusStack; + /** The camera representing the viewer's position and orientation. */ + private final Camera camera = new Camera(); + /** Optional input devices (head tracker, 6DOF mouse) with hot-plug. */ + private DeviceHotplug deviceHotplug; + /** The root shape collection containing all 3D shapes in the scene. */ + private final ShapeCollection rootShapeCollection = new ShapeCollection(); + /** The set of frame listeners notified before each frame. */ + private final Set frameListeners = ConcurrentHashMap.newKeySet(); + /** + * Number of paint tiles per render thread. Tiles (not threads) are the + * unit of work: threads steal tiles off a shared ticket, so more tiles + * than threads lets fast threads pick up remaining tiles instead of + * idling behind a busy one. Measured 2026-09-04 (400-sphere scene, + * 1920x1080, HX 370): 10x oversampling beats 4x by ~15% and also + * beats pure horizontal bands; 16x adds a further win only on + * spatially clustered scenes. + */ + private static final int TILES_PER_THREAD = 10; + + /** Number of render threads. Can be changed at runtime via {@link #setNumRenderThreads(int)}. */ + private volatile int numRenderThreads = defaultRenderThreadCount(); + /** + * Executor for the parallel transform phase, sized to all available cores. + * Created lazily on the render thread; daemon threads so it never blocks JVM exit. + */ + private ExecutorService transformExecutor = null; + /** The background color of the view. */ + public Color backgroundColor = Color.BLACK; + + /** Developer tools for this view panel. */ + private final DeveloperTools developerTools = new DeveloperTools(); + /** Debug log buffer for capturing diagnostic output. */ + private final DebugLogBuffer debugLogBuffer = new DebugLogBuffer(10000); + /** 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(); + + /** Progressive GI system, created by {@link #enableGlobalIllumination()}. */ + private eu.svjatoslav.aukio.e3d.renderer.raster.gi.GlobalIllumination globalIllumination; + + /** + * 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. + */ + private long lastUpdateMillis = 0; + + /** The current rendering context for the active frame. */ + private RenderingContext renderingContext = null; + + /** + * Triple-buffered frame contexts, indexed by pass counter modulo 3. + * While frame N is still being painted from one buffer, frame N+1 + * already transforms into another, so paint threads never idle waiting + * for the transform phase and transform threads never wait for the + * last tile. + */ + private final RenderingContext[] frameContexts = new RenderingContext[3]; + + /** Slot index (0..2) of the frame context currently being prepared. */ + private int frameParity = 0; + + /** + * Counts render passes (one per eye in stereo). A pass's projection + * buffer slot is {@code passCounter % 3}: with three slots, transform + * of pass P only conflicts with paint of pass P-3, so it can run + * while the two previous passes' paints are still in flight. + */ + private long passCounter = 0; + + /** + * Paint passes that were submitted to the shared executor but not yet + * awaited, oldest first. Depth stays at most 2: the pass being + * transformed now overlaps the previously submitted one, and freed + * workers flow from the tail of the older pass's tiles straight into + * the newer pass's tiles — consecutive passes' paints overlap on + * purpose (they write different parity framebuffers and read + * different parity vertex slots, so no ordering between them is + * required). + */ + private final java.util.ArrayDeque pendingPaints = new java.util.ArrayDeque<>(); + + /** + * Mailbox holding the newest completed frame awaiting presentation. + * Only the latest frame is kept: when the display path (X server at + * 4K, tens of MB per blit) is slower than production, stale frames + * are dropped instead of piling up latency — swapchain "mailbox + * mode". The render thread never blocks on the display: it deposits + * here and the dedicated present thread does the blitting. + */ + private final java.util.concurrent.atomic.AtomicReference mailboxFrame = + new java.util.concurrent.atomic.AtomicReference<>(); + + /** Signals the present thread that a frame was deposited. */ + private final java.util.concurrent.Semaphore presentSignal = new java.util.concurrent.Semaphore(0); + + /** Daemon thread performing all BufferStrategy blits. */ + private Thread presentThread; + + /** Run flag for the present thread. */ + private volatile boolean presentThreadRunning = false; + + /** + * Maximum blits per second the present thread issues IN CAPPED-FPS + * MODE. Exists because the X server also dispatches input: flooding + * it with blits causes desktop-wide mouse/keyboard jitter. Override + * with {@code -Daukio3d.presentRate=N} for high-refresh displays. + * + *

In benchmark mode (targetFPS <= 0) pacing is DISABLED: the + * pacing sleep delays the presented frame's gate release, and since + * paint(F+3) awaits that gate before reusing F's framebuffer, pacing + * the present thread used to cap PRODUCTION to roughly + * 2×PRESENT_RATE_LIMIT_FPS (measured: unlimited-FPS mode + * locked to ~120 FPS with 18 workers 70% idle, 2026-09-06).

+ */ + private static final int PRESENT_RATE_LIMIT_FPS = + Integer.parseInt(System.getProperty("aukio3d.presentRate", "60")); + + /** + * When false, each paint pass is awaited immediately after submission, + * restoring the old strictly-sequential phase order. Kill switch for + * benchmarking and regression hunting; also toggled at runtime from + * developer tools if needed. + */ + private volatile boolean pipelineEnabled = + !"false".equalsIgnoreCase(System.getProperty("aukio3d.pipeline", "true")); + + /** + * A paint pass whose tiles are still being worked on by the render + * executor. Awaited one pass later (software pipeline). + */ + /** A completed frame plus the gate to fire once it is presented or dropped. */ + private static final class PresentJob { + RenderingContext context; + java.util.concurrent.CountDownLatch gate; + } + + private static class PendingPaint { + volatile CountDownLatch latch; + RenderingContext context; + volatile SegmentRenderingContext[] segmentContexts; + int eyeOffsetX; + int eyeWidth; + /** True when this pass completes its frame (blit becomes due). */ + boolean lastPassOfFrame; + + /** Global pass index at submission time (slot = index mod 3). */ + long passIndex; + + /** The frame's own present gate, fired when it is presented or dropped. */ + java.util.concurrent.CountDownLatch frameGate; + + /** + * Counted down by the pass's asynchronous continuation once the + * paint tasks have been submitted and {@link #latch} / + * {@link #segmentContexts} are published. + */ + final java.util.concurrent.CountDownLatch ready = new java.util.concurrent.CountDownLatch(1); + } + + /** + * Currently target frames per second rate for this view. Target FPS can be changed at runtime. + * 3D engine tries to be smart and only repaints screen when there are visible changes. + * A value of 0 or less means unlimited FPS (benchmark mode). + */ + private volatile int targetFPS = 60; + + /** Frames blitted in the current FPS measurement window (render thread only). */ + private int fpsWindowFrames = 0; + + /** Start of the current FPS measurement window, nanoseconds (render thread only). */ + private long fpsWindowStartNanos = 0; + + /** Most recently measured display rate (frames blitted per second). */ + private volatile double measuredFPS = 0; + + /** + * Set to true if it is known than next frame needs to be painted. Flag is cleared + * immediately after frame got updated. + */ + private boolean viewRepaintNeeded = true; + + /** + * Render thread that runs the continuous frame generation loop. + */ + private Thread renderThread; + + /** + * Flag to control whether the render thread should keep running. + */ + private volatile boolean renderThreadRunning = false; + + /** Timestamp for the next scheduled frame. */ + private long nextFrameTime; + + /** + * The most recently completed frame, retained so a bug report can + * include a screenshot of what the user was seeing. Set by + * {@link #presentFrame}; the buffer is reused by the pipeline, so + * consumers must copy it. + */ + private volatile java.awt.image.BufferedImage lastFrameImage; + + /** The buffer strategy for page-flipping rendering (also read by the present thread). */ + private BufferStrategy bufferStrategy; + + /** Whether the buffer strategy has been initialized. */ + private boolean bufferStrategyInitialized = false; + + /** + * Creates a new view panel with default settings. + */ + public ViewPanel() { + frameListeners.add((panel, deltaMs) -> camera.onFrame(deltaMs)); + frameListeners.add(inputManager); + + // persistent log + telemetry (idempotent); the view telemetry + // source reports frame production stats + Diagnostics.install(); + Telemetry.registerSource("view", () -> String.format( + "fps=%.1f targetFps=%d size=%dx%d renderThreads=%d", + getMeasuredFPS(), getTargetFPS(), getWidth(), getHeight(), + getNumRenderThreads())); + + keyboardFocusStack = new KeyboardFocusStack(this); + + initializeCanvas(); + deviceHotplug = new DeviceHotplug(this); + + // Set default ambient light for the scene + lightingManager.setAmbientLight(new Color(50, 50, 50)); + addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(final ComponentEvent e) { + viewRepaintNeeded = true; + startRenderThreadIfReady(); + } + + @Override + public void componentShown(final ComponentEvent e) { + viewRepaintNeeded = true; + startRenderThreadIfReady(); + } + }); + } + + private void startRenderThreadIfReady() { + if (isShowing() && getWidth() > 0 && getHeight() > 0) + startRenderThread(); + } + + /** + * Returns the camera representing the viewer's position and orientation. + * + * @return the camera + */ + public Camera getCamera() { + return camera; + } + + /** + * Returns the keyboard focus stack, which manages which component receives + * keyboard input. + * + * @return the keyboard focus stack + */ + public KeyboardFocusStack getKeyboardFocusStack() { + return keyboardFocusStack; + } + + /** + * Returns the root shape collection (scene graph). Add your 3D shapes here + * to make them visible in the view. + * + *
{@code
+     * viewPanel.getRootShapeCollection().addShape(myShape);
+     * }
+ * + * @return the root shape collection + */ + public ShapeCollection getRootShapeCollection() { + return rootShapeCollection; + } + + /** + * Returns the human input device (mouse/keyboard) event tracker. + * + * @return the HID event tracker + */ + /** + * Returns the input manager handling mouse and keyboard events for this view. + * + * @return the input manager + */ + public InputManager getInputManager() { + return inputManager; + } + + /** + * Registers a listener that will be notified before each frame render. + * Listeners can trigger repaints by returning {@code true} from + * {@link FrameListener#onFrame}. + * + * @param listener the listener to add + * @see #removeFrameListener(FrameListener) + */ + public void addFrameListener(final FrameListener listener) { + frameListeners.add(listener); + } + + @Override + public Dimension getPreferredSize() { + return new Dimension(640, 480); + } + + @Override + public Dimension getMinimumSize() { + return getPreferredSize(); + } + + @Override + public Dimension getMaximumSize() { + return getPreferredSize(); + } + + /** + * Returns the current rendering context for the active frame. + * + * @return the rendering context, or null if no frame is being rendered + */ + public RenderingContext getRenderingContext() { + return renderingContext; + } + + /** + * Returns the developer tools for this view panel. + * + * @return the developer tools + */ + public DeveloperTools getDeveloperTools() { + return developerTools; + } + + /** + * Returns the debug log buffer for this view panel. + * + * @return the debug log buffer + */ + public DebugLogBuffer getDebugLogBuffer() { + return debugLogBuffer; + } + + /** + * Returns the most recently completed frame, for bug report + * screenshots. The buffer belongs to the render pipeline and is + * reused — copy it before use. Null until the first frame completes. + * + * @return the last presented frame image, or null + */ + public java.awt.image.BufferedImage getLastFrameImage() { + return lastFrameImage; + } + + /** + * 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; + } + + /** + * Enables progressive global illumination with the default of 2 + * dedicated low-priority CPU threads. GI is opt-in: without this call + * lighting behaves exactly as before. Shadows and bounced light fade in + * over the first seconds and keep adapting to scene changes. + * + * @return the running GI system + */ + public eu.svjatoslav.aukio.e3d.renderer.raster.gi.GlobalIllumination enableGlobalIllumination() { + return enableGlobalIllumination(2); + } + + /** + * Enables progressive global illumination with the given number of + * dedicated low-priority CPU threads. Calling again returns the + * already-running system. + * + * @param threadCount worker threads for ray tracing + * @return the running GI system + */ + public synchronized eu.svjatoslav.aukio.e3d.renderer.raster.gi.GlobalIllumination enableGlobalIllumination( + final int threadCount) { + if (globalIllumination == null) { + globalIllumination = new eu.svjatoslav.aukio.e3d.renderer.raster.gi.GlobalIllumination( + rootShapeCollection, lightingManager, threadCount); + globalIllumination.start(); + } + return globalIllumination; + } + + /** + * Shows the developer tools panel, toggling it if already open. + * Called when F12 is pressed. + */ + public void showDeveloperToolsPanel() { + if (developerToolsPanel != null && developerToolsPanel.isVisible()) { + developerToolsPanel.dispose(); + developerToolsPanel = null; + return; + } + + Frame parentFrame = null; + Container parent = getParent(); + while (parent != null) { + if (parent instanceof Frame) { + parentFrame = (Frame) parent; + break; + } + parent = parent.getParent(); + } + + developerToolsPanel = new DeveloperToolsPanel(parentFrame, this, developerTools, debugLogBuffer); + developerToolsPanel.setVisible(true); + } + + @Override + public void paint(final Graphics g) { + } + + @Override + public void update(final Graphics g) { + } + + private void initializeCanvas() { + setBackground(java.awt.Color.BLACK); + setFocusable(true); + // The canvas is the only focusable component in the frame; AWT + // focus traversal is meaningless here. Without this, AWT consumes + // Tab/Shift+Tab (and Ctrl+Tab) as traversal keys BEFORE they reach + // KeyListeners, so widgets (terminal, text editor Tab-indent) + // never see them. + setFocusTraversalKeysEnabled(false); + setIgnoreRepaint(true); + setVisible(true); + } + + @Override + public void addNotify() { + super.addNotify(); + requestFocus(); + } + + private void ensureBufferStrategy() { + if (bufferStrategyInitialized && bufferStrategy != null) + return; + + if (!isDisplayable() || getWidth() <= 0 || getHeight() <= 0) + return; + + try { + createBufferStrategy(NUM_BUFFERS); + bufferStrategy = getBufferStrategy(); + if (bufferStrategy != null) { + bufferStrategyInitialized = true; + // Prime the buffer strategy with an initial show() to ensure it's ready + Graphics2D g = null; + try { + g = (Graphics2D) bufferStrategy.getDrawGraphics(); + if (g != null) { + g.setColor(java.awt.Color.BLACK); + g.fillRect(0, 0, getWidth(), getHeight()); + } + } finally { + if (g != null) g.dispose(); + } + bufferStrategy.show(); + java.awt.Toolkit.getDefaultToolkit().sync(); + } + } catch (final Exception e) { + bufferStrategy = null; + bufferStrategyInitialized = false; + } + } + + + private void renderFrame() { + ensureBufferStrategy(); + + if (bufferStrategy == null || renderingContext == null) { + debugLogBuffer.log("[VIEWPANEL] renderFrame ABORT: bufferStrategy=" + bufferStrategy + ", renderingContext=" + renderingContext); + return; + } + + ThreadActivityRecorder.setFrameParity(frameParity); + + // Install this frame-use's present gate and capture the previous + // one: this frame's paint continuation will await the previous + // gate before writing pixels, so painting never overwrites a + // buffer the present thread is still blitting from. The frame's + // OWN gate travels with the PendingPaint to the deposit — the + // context field is reset again by the next frame reusing this + // context, long before this frame's flush reads it. + final java.util.concurrent.CountDownLatch previousGate = renderingContext.presentGate; + final java.util.concurrent.CountDownLatch frameGate = new java.util.concurrent.CountDownLatch(1); + renderingContext.presentGate = frameGate; + + try { + // Triple-buffered software pipeline, one render pass per eye. + // Vertex state, aggregators and framebuffers cycle through 3 + // slots, so transform(pass P) only needs paint(P-3) to be + // complete — it can run while the two previous passes' paints + // are still on the executor. Before each transform, completed + // paints are flushed (mouse hits, frame blit) and the render + // thread blocks ONLY if the pass P-3 paint is still running, + // which steady-state worker throughput prevents. Workers flow + // from one pass's tiles straight into the next pass's tiles + // with no gap: the next paint is always already queued. + if (stereoModeEnabled) { + final int eyeWidth = renderingContext.width / 2; + flushCompletedPasses(passCounter - 3); + final RenderingContext leftPass = transformPass(StereoEye.LEFT, eyeWidth, 0); + submitPaintPass(StereoEye.LEFT, eyeWidth, 0, false, leftPass, previousGate, frameGate); + flushCompletedPasses(passCounter - 3); + final RenderingContext rightPass = transformPass(StereoEye.RIGHT, eyeWidth, eyeWidth); + submitPaintPass(StereoEye.RIGHT, eyeWidth, eyeWidth, true, rightPass, previousGate, frameGate); + } else { + flushCompletedPasses(passCounter - 3); + final RenderingContext pass = transformPass(StereoEye.NONE, renderingContext.width, 0); + submitPaintPass(StereoEye.NONE, renderingContext.width, 0, true, pass, previousGate, frameGate); + } + frameParity = (frameParity + 1) % 3; + } catch (final Exception e) { + debugLogBuffer.log("[VIEWPANEL] renderFrame exception: " + e.getMessage()); + e.printStackTrace(); + bufferStrategyInitialized = false; + bufferStrategy = null; + } + } + + /** + * Blits a finished frame buffer to the screen via the buffer strategy. + * The re-blit loop handles OS back-buffer recreation: contentsRestored() + * triggers when the OS recreates the back buffer (common during window + * creation); since the offscreen bufferedImage still contains the + * correct frame data, only a re-blit is needed, never a re-render. + * + * @param context the frame context whose bufferedImage is complete + */ + /** + * Deposits a completed frame into the presentation mailbox and wakes + * the present thread. If the previous deposited frame has not been + * shown yet, it is dropped: displaying a stale frame when a newer one + * exists only adds latency. + * + * @param context the frame context whose buffer is complete + */ + private void presentFrame(final RenderingContext context, + final java.util.concurrent.CountDownLatch frameGate) { + noteFrameBlitted(); // counts PRODUCED frames (benchmark rate) + lastFrameImage = context.bufferedImage; + final PresentJob job = new PresentJob(); + job.context = context; + job.gate = frameGate; + final PresentJob dropped = mailboxFrame.getAndSet(job); + if (dropped != null) { + // Never shown: release its framebuffer for reuse immediately + dropped.gate.countDown(); + } + presentSignal.release(); + } + + /** + * Present thread loop: takes the newest mailbox frame and blits it. + * All slow display-path work (33 MB drawImage at 4K, BufferStrategy + * show, Toolkit.sync round-trip to the X server) happens here, never + * on the render thread that feeds the worker pool. + * + *

Paced to the display rate IN CAPPED-FPS MODE: without a cap, an + * unlimited-FPS pipeline floods the X server with hundreds of 33 MB + * blits per second, and since the X server also processes mouse and + * keyboard, the whole desktop gets input jitter. In benchmark mode + * (targetFPS <= 0) pacing is skipped: the sleep would delay the + * presented frame's gate release, and gate backpressure + * (paint(F+3) awaits blit/drop of F) would cap production to the + * present rate — the opposite of what benchmark mode is for. Frames + * the present thread cannot keep up with are dropped from the + * mailbox at deposit time and their gates fire immediately.

+ */ + private void presentLoop() { + final long presentIntervalNanos = 1_000_000_000L / PRESENT_RATE_LIMIT_FPS; + long nextPresent = System.nanoTime(); + while (presentThreadRunning) { + try { + presentSignal.acquire(); + } catch (final InterruptedException e) { + break; + } + final PresentJob job = mailboxFrame.getAndSet(null); + if (job != null) { + // Only pace when the user asked for a capped frame rate. + if (targetFPS > 0) { + nextPresent += presentIntervalNanos; + final long sleep = nextPresent - System.nanoTime(); + if (sleep > 0) { + try { + Thread.sleep(sleep / 1_000_000L, (int) (sleep % 1_000_000L)); + } catch (final InterruptedException e) { + break; + } + } else if (sleep < -presentIntervalNanos) { + nextPresent = System.nanoTime(); // fell behind: resync + } + } + try { + blitFrame(job.context, job.gate); + } catch (final Throwable t) { + // A blit failure must NEVER kill this thread: every + // frame's present gate depends on it. Drop the buffer + // strategy; the render thread recreates it next frame. + debugLogBuffer.log("[VIEWPANEL] present failed: " + t); + bufferStrategyInitialized = false; + bufferStrategy = null; + } finally { + job.gate.countDown(); + } + } + } + } + + private void blitFrame(final RenderingContext context, + final java.util.concurrent.CountDownLatch gate) { + if (bufferStrategy == null) + return; + final boolean trace = ThreadActivityRecorder.isEnabled(); + final long t0 = trace ? System.nanoTime() : 0; + try { + do { + Graphics2D g = null; + try { + g = (Graphics2D) bufferStrategy.getDrawGraphics(); + if (g != null) { + // Use image observer to ensure proper image loading + g.drawImage(context.bufferedImage, 0, 0, this); + } + } catch (final Exception e) { + debugLogBuffer.log("[VIEWPANEL] Blit exception: " + e.getMessage()); + break; + } finally { + if (g != null) g.dispose(); + } + } while (bufferStrategy.contentsRestored()); + + // Release the framebuffer for reuse NOW: the draw loop above is + // the only part that reads context.bufferedImage. show() and + // Toolkit.sync() below touch only the BufferStrategy's own back + // buffer and the X connection — at 4920x2960 they cost ~16 ms + // (vs ~8 ms for the draw), and holding the frame's gate across + // them used to stall the paint pass waiting to reuse this + // buffer (measured 2026-09-06: gate hold ~24 ms, production + // capped at ~52 FPS). The present thread's finally-block still + // fires the gate as a backstop (countDown is idempotent). + gate.countDown(); + + if (bufferStrategy.contentsLost()) { + debugLogBuffer.log("[VIEWPANEL] Buffer contents LOST, reinitializing"); + bufferStrategyInitialized = false; + bufferStrategy = null; + } else { + bufferStrategy.show(); + java.awt.Toolkit.getDefaultToolkit().sync(); + } + } finally { + if (trace) { + ThreadActivityRecorder.record(ThreadActivityRecorder.KIND_BLIT, t0, System.nanoTime()); + } + } + } + + /** + * Awaits the oldest pending paint pass (if any), processes its mouse + * hits, and schedules its frame buffer for blitting when it was the + * frame's last pass. Newer paint passes keep running meanwhile — the + * await only enforces the constraints that actually exist: vertex + * slot reuse (transform P+2 needs paint P done) and blit ordering. + */ + private void flushPendingPaint() { + final PendingPaint pending = pendingPaints.poll(); + if (pending == null) + return; + + try { + final boolean trace = ThreadActivityRecorder.isEnabled(); + final long t0 = trace ? System.nanoTime() : 0; + pending.ready.await(); + final CountDownLatch paintLatch = pending.latch; + if (paintLatch != null) { + paintLatch.await(); + } + if (trace) { + ThreadActivityRecorder.record(ThreadActivityRecorder.KIND_AWAIT, t0, System.nanoTime()); + } + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + // Hi-Z: fold this frame's depth into the occlusion pyramid the + // next frame's block culling runs against. Only on successful + // paint (segmentContexts null = the pass failed — keep the + // previous pyramid rather than folding in partial depth). + if (pending.segmentContexts != null + && pending.context.occlusionPyramid != null) + pending.context.occlusionPyramid.buildFrom( + pending.context.depth, + pending.context.width, pending.context.height); + + // Only process mouse hits for the eye whose viewport contains the + // cursor. In stereo mode each eye sees a different camera position, + // so the same object may appear at different screen X in each eye. + // The cursor is in absolute screen coordinates and can only be in + // one eye's viewport; combining for the wrong eye would overwrite + // the correct hit with null. + final MouseEvent mouseEvent = pending.context.getMouseEvent(); + final boolean mouseInThisEye = (mouseEvent == null) + || (mouseEvent.coordinate.x >= pending.eyeOffsetX + && mouseEvent.coordinate.x < pending.eyeOffsetX + pending.eyeWidth); + // segmentContexts is null when the pass's paint continuation + // failed (e.g. a transform chunk threw): the frame is skipped, + // but that must not kill the render loop with an NPE here. + if (mouseInThisEye && pending.segmentContexts != null) { + combineMouseResults(pending.segmentContexts, pending.context); + viewRepaintNeeded = pending.context.handlePossibleComponentMouseEvent( + getKeyboardFocusStack()); + } + + if (developerTools.showSegmentBoundaries) { + final RenderingContext context = pending.context; + final int tilesX = context.tilesX; + final int tilesY = context.tilesY; + final int tileH = context.height / tilesY; + final int tileW = pending.eyeWidth / tilesX; + final int[] pixels = context.pixels; + final int width = context.width; + final int height = context.height; + final int red = (255 << 16); + for (int ty = 1; ty < tilesY; ty++) { + final int offset = ty * tileH * width; + Arrays.fill(pixels, offset + pending.eyeOffsetX, + offset + pending.eyeOffsetX + pending.eyeWidth, red); + } + for (int tx = 1; tx < tilesX; tx++) { + final int x = pending.eyeOffsetX + tx * tileW; + for (int y = 0; y < height; y++) + pixels[y * width + x] = red; + } + } + + if (pending.lastPassOfFrame) { + // Hand the completed frame to the present thread (mailbox: + // only the newest is shown). The render thread never blocks + // on the display; workers stay fed from the queue meanwhile. + presentFrame(pending.context, pending.frameGate); + } + } + + /** + * Flushes every pending paint that is already complete, plus — when + * correctness demands — awaits older passes. Transform of pass P may + * only start once paint(P-3) is complete (vertex slot and aggregator + * cycle of 3); callers pass P-3 as {@code maxPassIndex}. Passes newer + * than that are flushed without blocking when their latch already + * reached zero, so completed frames are blitted as early as possible. + * + * @param maxPassIndex passes up to this index MUST be complete on return + */ + private void flushCompletedPasses(final long maxPassIndex) { + while (true) { + final PendingPaint head = pendingPaints.peek(); + if (head == null) + return; + if (head.passIndex > maxPassIndex + && (head.ready.getCount() > 0 + || head.latch == null + || head.latch.getCount() > 0)) + return; // still being prepared or painted; leave it queued + flushPendingPaint(); + } + } + + /** + * Clears a single tile's pixel area to the background color. + * Called by each render thread before painting shapes. + * The tile context carries exact X and Y bounds (X bounds also cover + * the stereo per-eye viewport). + * + * @param ctx the tile rendering context with X/Y bounds + */ + private void clearSegmentPixels(final SegmentRenderingContext ctx) { + final int rgb = (backgroundColor.r << 16) | (backgroundColor.g << 8) | backgroundColor.b; + final int width = ctx.width; + final int[] pixels = ctx.pixels; + + final int minX = ctx.renderMinX; + final int maxX = ctx.renderMaxX; + + final float[] depth = ctx.depth; + for (int y = ctx.renderMinY; y < ctx.renderMaxY; y++) { + final int rowOffset = y * width; + Arrays.fill(pixels, rowOffset + minX, rowOffset + maxX, rgb); + Arrays.fill(depth, rowOffset + minX, rowOffset + maxX, + Float.NEGATIVE_INFINITY); + } + } + + private void combineMouseResults(final SegmentRenderingContext[] segmentContexts, + final RenderingContext context) { + // All segments paint shapes back-to-front, and mouse hit detection + // happens before Y-bound clipping. So each segment should report the + // same "last hit" (frontmost shape under mouse). Just take the first non-null. + for (final SegmentRenderingContext ctx : segmentContexts) { + final MouseInteractionController hit = ctx.getSegmentMouseHit(); + if (hit != null) { + context.setCurrentObjectUnderMouseCursor(hit, + ctx.getSegmentMouseHitU(), ctx.getSegmentMouseHitV()); + return; + } + } + } + + /** + * Calling these methods tells 3D engine that current 3D view needs to be + * repainted on first opportunity. + */ + public void repaintDuringNextViewUpdate() { + viewRepaintNeeded = true; + } + + /** + * Set target frames per second rate for this view. Target FPS can be changed at runtime. + * Use 0 or negative value for unlimited FPS (max performance mode for benchmarking). + * + * @param frameRate target frames per second rate for this view. + */ + public void setFrameRate(final int frameRate) { + targetFPS = frameRate; + } + + /** + * Returns the current target frames per second rate. + * + * @return target FPS; 0 or less means unlimited + */ + public int getTargetFPS() { + return targetFPS; + } + + /** + * Returns the measured production rate: frames completed per second, + * averaged over the last ~500 ms window. This is the benchmark number: + * how fast the pipeline produces frames, regardless of how quickly + * the display path presents them (the mailbox present thread may + * drop stale frames when the display is slower than production). + * + * @return measured frames per second + */ + public double getMeasuredFPS() { + return measuredFPS; + } + + /** + * Counts one blitted frame into the FPS measurement window. + * Called by the render thread after each completed blit. + */ + private void noteFrameBlitted() { + fpsWindowFrames++; + final long now = System.nanoTime(); + if (fpsWindowStartNanos == 0) { + fpsWindowStartNanos = now; + fpsWindowFrames = 0; + return; + } + final long elapsed = now - fpsWindowStartNanos; + if (elapsed >= 500_000_000L) { + measuredFPS = fpsWindowFrames * 1e9 / elapsed; + fpsWindowStartNanos = now; + fpsWindowFrames = 0; + } + } + + /** + * Returns the current number of render threads. + * + * @return the number of render threads + */ + public int getNumRenderThreads() { + return numRenderThreads; + } + + /** + * Sets the number of render threads. Takes effect on the next frame. + * The executor service is recreated lazily when the render loop detects the change. + * + * @param count number of render threads (must be at least 1) + */ + public void setNumRenderThreads(final int count) { + if (count < 1) + throw new IllegalArgumentException("Render thread count must be at least 1, got: " + count); + numRenderThreads = count; + viewRepaintNeeded = true; + } + + // ------------------------------------------------------------------ + // Stereo rendering + // ------------------------------------------------------------------ + + /** + * Default inter-pupillary distance in world units (centimeters). + * Human IPD ranges from ~5.5 to ~7.5 cm; 6.5 cm is the population median. + */ + private static final double DEFAULT_STEREO_IPD = 6.5; + + /** Inter-pupillary distance in world units, configurable at runtime. */ + private double stereoIPD = EngineConfig.getIpdCm(); + + /** Whether side-by-side stereoscopic rendering is enabled. */ + private boolean stereoModeEnabled = false; + + /** + * Returns whether side-by-side stereoscopic rendering is currently enabled. + * + * @return {@code true} if stereo mode is active + */ + public boolean isStereoModeEnabled() { + return stereoModeEnabled; + } + + /** + * Enables or disables side-by-side stereoscopic rendering. + * When enabled, each frame renders two eye views side-by-side. + * + * @param enabled {@code true} to enable stereo mode, {@code false} to disable + */ + public void setStereoModeEnabled(final boolean enabled) { + this.stereoModeEnabled = enabled; + viewRepaintNeeded = true; + } + + /** + * Returns the current inter-pupillary distance used for stereo rendering. + * + * @return IPD in world units (centimeters) + */ + public double getStereoIPD() { + return stereoIPD; + } + + /** + * Sets the inter-pupillary distance for stereo rendering. + * Human IPD ranges from ~5.5 to ~7.5 cm; for XR glasses the optical + * IPD may differ from the user's anatomical IPD. + * + * @param ipd the inter-pupillary distance in world units (centimeters) + */ + public void setStereoIPD(final double ipd) { + this.stereoIPD = ipd; + viewRepaintNeeded = true; + } + + /** + * Runs the transform phase of a single eye pass: offsets the camera for + * the eye, updates the frame context viewport fields, transforms, sorts + * and tile-bins the scene. The pass's projection slot is + * {@code passCounter & 1}; its paint (submitted later by + * {@link #submitPaintPass}) reads the same slot from copies taken while + * it is still current. + * + * @param eye which eye to render + * @param eyeWidth width of the eye viewport in pixels + * @param eyeOffsetX X offset of the eye viewport within the full buffer + */ + private RenderingContext transformPass(final StereoEye eye, final int eyeWidth, final int eyeOffsetX) { + final boolean trace = ThreadActivityRecorder.isEnabled(); + final long t0 = trace ? System.nanoTime() : 0; + final Camera camera = getCamera(); + final Point3D location = camera.getTransform().getTranslation(); + + final double originalX = location.x; + if (eye != StereoEye.NONE) { + final double ipdOffset = (eye == StereoEye.LEFT) ? -stereoIPD / 2.0 : stereoIPD / 2.0; + location.x += ipdOffset; + } + + try { + // Independent per-pass context: the walk, its forked chunk + // tasks and the asynchronous sort/bin/paint continuation all + // read this copy, so the NEXT pass's setup (a new copy) + // cannot disturb work that is still in flight. + final RenderingContext passContext = new RenderingContext(renderingContext); + passContext.stereoEye = eye; + passContext.stereoViewportWidth = eyeWidth; + passContext.stereoViewportOffsetX = eyeOffsetX; + passContext.renderMinX = eyeOffsetX; + passContext.renderMaxX = eyeOffsetX + eyeWidth; + passContext.centerCoordinate.x = eyeWidth / 2.0; + passContext.projectionScale = eyeWidth / 3.0; + passContext.vertexSlot = (int) (passCounter % 3); + + // Walks the tree and forks heavy composites into chunk tasks, + // but does NOT wait for them: draining happens inside the + // pass's continuation on a worker thread. + rootShapeCollection.transformShapesBegin(getCamera(), passContext); + return passContext; + } finally { + location.x = originalX; + if (trace) { + ThreadActivityRecorder.record(ThreadActivityRecorder.KIND_RENDER, t0, System.nanoTime()); + } + } + } + + /** + * Submits the paint phase of a single eye pass to the render executor + * and returns immediately (unless the pipeline kill switch is off). + * Tiles are work-stolen off a shared ticket; output is unaffected — + * every tile is still painted by exactly one thread, in fixed + * (Z, shapeId) order. + * + * @param eye which eye this pass renders + * @param eyeWidth width of the eye viewport in pixels + * @param eyeOffsetX X offset of the eye viewport within the buffer + * @param lastPassOfFrame true when this pass completes its frame + */ + private void submitPaintPass(final StereoEye eye, final int eyeWidth, final int eyeOffsetX, + final boolean lastPassOfFrame, final RenderingContext passContext, + final java.util.concurrent.CountDownLatch previousGate, + final java.util.concurrent.CountDownLatch frameGate) { + final RenderingContext frameContext = renderingContext; + final int tilesX = frameContext.tilesX; + final int tilesY = frameContext.tilesY; + final int height = frameContext.height; + final int slot = passContext.vertexSlot; + final ExecutorService executor = getOrCreateTransformExecutor(); + + // Enqueue the pending-paint shell synchronously so pendingPaints + // stays in pass order; the continuation fills in the rest. + final PendingPaint pending = new PendingPaint(); + pending.context = frameContext; + pending.eyeOffsetX = eyeOffsetX; + pending.eyeWidth = eyeWidth; + pending.lastPassOfFrame = lastPassOfFrame; + pending.passIndex = passCounter; + pending.frameGate = frameGate; + pendingPaints.addLast(pending); + + passCounter++; + + final int tracePaintKind = ThreadActivityRecorder.KIND_PAINT + ThreadActivityRecorder.frameParity(); + + // The whole rest of the pass is one asynchronous continuation on + // the shared executor: drain the transform chunks (a ForkJoinTask + // get() here work-steals instead of blocking), depth-sort on the + // same pool, bin per tile, then submit the paint ticket tasks. + // The render thread never waits for any of it. + executor.submit(() -> { + final boolean trace = ThreadActivityRecorder.isEnabled(); + final long t0 = trace ? System.nanoTime() : 0; + try { + long ts = trace ? System.nanoTime() : 0; + rootShapeCollection.drainTransformShapes(passContext); + if (trace) { + ThreadActivityRecorder.record(ThreadActivityRecorder.KIND_DRAIN, ts, System.nanoTime()); + ts = System.nanoTime(); + } + // Present gate: never overwrite a buffer the present + // thread is still blitting from (frame F-3's contents). + // Fires instantly unless the display is >=3 frames behind. + // The timeout is insurance, not control flow: a present + // path failure must degrade to a torn frame, never to a + // frozen pipeline. + if (!previousGate.await(2, java.util.concurrent.TimeUnit.SECONDS)) { + debugLogBuffer.log("[VIEWPANEL] present gate timeout — presenting may be stuck"); + } + if (trace) { + ThreadActivityRecorder.record(ThreadActivityRecorder.KIND_AWAIT, ts, System.nanoTime()); + } + ts = trace ? System.nanoTime() : 0; + rootShapeCollection.sortShapes(slot, executor); + if (trace) { + ThreadActivityRecorder.record(ThreadActivityRecorder.KIND_SORT, ts, System.nanoTime()); + } + rootShapeCollection.binShapesForTiles(slot, tilesX, tilesY, + eyeOffsetX, eyeWidth, height, executor); + + final int tileW = eyeWidth / tilesX; + final int tileH = height / tilesY; + final int segments = tilesX * tilesY; + // In stereo the right eye's segment indices follow the left eye's + final int eyeBase = (eye == StereoEye.RIGHT) ? segments : 0; + + final SegmentRenderingContext[] segmentContexts = new SegmentRenderingContext[segments]; + for (int ty = 0; ty < tilesY; ty++) { + final int minY = ty * tileH; + final int maxY = (ty == tilesY - 1) ? height : (ty + 1) * tileH; + for (int tx = 0; tx < tilesX; tx++) { + final int minX = eyeOffsetX + tx * tileW; + final int maxX = (tx == tilesX - 1) ? eyeOffsetX + eyeWidth : minX + tileW; + final int index = ty * tilesX + tx; + final SegmentRenderingContext tileContext = new SegmentRenderingContext( + passContext, minY, maxY, eyeBase + index); + tileContext.vertexSlot = slot; + tileContext.renderMinX = minX; + tileContext.renderMaxX = maxX; + segmentContexts[index] = tileContext; + } + } + + // Per-tile paint tasks on the fork/join pool: a worker + // finishing one tile immediately pulls ANY next queued + // work — another tile (of this or an adjacent frame's + // pass), a transform chunk, a continuation — so cores + // never idle waiting for a ticket-loop task to end. + final CountDownLatch paintLatch = new CountDownLatch(segments); + + for (int s = 0; s < segments; s++) { + final int segmentIndex = s; + if (developerTools.renderAlternateSegments && (segmentIndex % 2 == 1)) { + paintLatch.countDown(); + continue; + } + executor.submit(() -> { + final long pt0 = trace ? System.nanoTime() : 0; + try { + clearSegmentPixels(segmentContexts[segmentIndex]); + rootShapeCollection.paintShapes(segmentContexts[segmentIndex]); + } finally { + if (trace) { + ThreadActivityRecorder.record(tracePaintKind, pt0, System.nanoTime()); + } + paintLatch.countDown(); + } + }); + } + + pending.segmentContexts = segmentContexts; + pending.latch = paintLatch; + } catch (final Throwable t) { + debugLogBuffer.log("[VIEWPANEL] paint continuation failed: " + t); + t.printStackTrace(); + } finally { + if (trace) { + ThreadActivityRecorder.record(ThreadActivityRecorder.KIND_PREP, t0, System.nanoTime()); + } + pending.ready.countDown(); + } + }); + + if (!pipelineEnabled) { + flushCompletedPasses(Long.MAX_VALUE); + } + } + + // ------------------------------------------------------------------ + + /** + * Default number of paint segment threads: 75% of available CPU + * threads, clamped to at least 1 and at most (CPU threads - 1), so + * one thread always stays free for the rest of the system. + * + * @return the default render thread count + */ + private static int defaultRenderThreadCount() { + final int cores = Runtime.getRuntime().availableProcessors(); + return Math.max(1, Math.min((int) Math.round(cores * 0.75), cores - 1)); + } + + /** + * Returns the shared transform executor, creating it on first use. + * Called from the render thread only (no synchronization needed). + * + * @return the transform executor + */ + private ExecutorService getOrCreateTransformExecutor() { + if (transformExecutor == null || transformExecutor.isShutdown()) { + final int threads = numRenderThreads; + final AtomicInteger workerCounter = new AtomicInteger(); + // ForkJoinPool, not a fixed thread pool: the instrumented + // parallel merge sort and bin/merge copy tasks fork onto THIS + // pool (never the common pool), and a worker blocked in + // ForkJoinTask.get() (continuation draining transform chunks) + // work-steals other tasks instead of idling. asyncMode = FIFO + // submission queues, so older frames' work is preferred. + // Sized to the ALLOCATED thread count (75% of cores), not all + // cores: measured 2026-09-05 (UtilizationBench) that 24/24 + // threads run only ~70% busy — GC/JIT/OS threads displace + // workers and stretch frame tails — while 18/18 stay ~83%+ + // busy AND deliver higher FPS. + transformExecutor = new java.util.concurrent.ForkJoinPool(threads, + pool -> { + final java.util.concurrent.ForkJoinWorkerThread thread = + java.util.concurrent.ForkJoinPool.defaultForkJoinWorkerThreadFactory + .newThread(pool); + thread.setName("e3d-worker-" + workerCounter.getAndIncrement()); + thread.setDaemon(true); + return thread; + }, null, true); + } + return transformExecutor; + } + + /** + * Returns the active SpaceNavigator device, or null when no 6DOF + * mouse is currently connected. + */ + public SpaceNavigatorHid getSpaceMouse() { + return deviceHotplug == null ? null : deviceHotplug.getSpaceMouse(); + } + + /** + * Returns the active head tracker, or null when no glasses are + * currently connected. + */ + public HeadTracker getHeadTracker() { + return deviceHotplug == null ? null : deviceHotplug.getHeadTracker(); + } + + /** + * Stops rendering of this view. + */ + public void stop() { + if (deviceHotplug != null) { + deviceHotplug.stop(); + deviceHotplug = null; + } + if (globalIllumination != null) { + // Stop the GI sweep with the view: the worker threads trace + // rays against THIS view's scene, so after close they are pure + // CPU burn (observed 2026-09-20: 4 leaked threads at ~70% duty + // minutes after the demo window was gone). + globalIllumination.stop(); + globalIllumination = null; + } + renderThreadRunning = false; + presentThreadRunning = false; + presentSignal.release(); + final PresentJob dropped = mailboxFrame.getAndSet(null); + if (dropped != null) { + dropped.gate.countDown(); + } + if (renderThread != null) { + // Interrupt BEFORE any executor teardown: the render thread may + // be parked in flushPendingPaint's paintLatch.await(), and + // shutdownNow() cancels queued paint tasks — a cancelled task + // never runs its finally countDown(), so the latch never fires + // and an uninterrupted join here hangs forever (observed + // 2026-09-20: close button dead, EDT stuck in this join). + renderThread.interrupt(); + try { + renderThread.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + renderThread = null; + } + // Tear down the pipeline only with the render thread dead: clearing + // the deque and cancelling queued tasks while it was still flushing + // raced its peek/poll and orphaned the latches it awaited. + pendingPaints.clear(); + if (transformExecutor != null) { + transformExecutor.shutdownNow(); + } + } + + /** + * Starts the render thread that continuously generates frames. + */ + private synchronized void startRenderThread() { + if (renderThread != null) + return; + + renderThreadRunning = true; + renderThread = new Thread(this::renderLoop, "e3d-render"); + renderThread.setDaemon(true); + renderThread.start(); + + if (!presentThreadRunning) { + presentThreadRunning = true; + presentThread = new Thread(this::presentLoop, "e3d-present"); + presentThread.setDaemon(true); + presentThread.start(); + } + } + + /** + * Main render loop that generates frames continuously. + * Supports both unlimited FPS and fixed FPS modes with dynamic sleep adjustment. + */ + private void renderLoop() { + nextFrameTime = System.currentTimeMillis(); + + while (renderThreadRunning) { + try { + ensureThatViewIsUpToDate(); + } catch (final Exception e) { + e.printStackTrace(); + } + + if (maintainTargetFps()) break; + } + } + + /** + * Ensures that the rendering process maintains the target frames per second (FPS) + * by dynamically adjusting the thread sleep duration. + * + * @return {@code true} if the thread was interrupted while sleeping, otherwise {@code false}. + */ + private boolean maintainTargetFps() { + if (targetFPS <= 0) return false; + + long now = System.currentTimeMillis(); + + nextFrameTime += 1000L / targetFPS; + + // If we've fallen behind, reset to now instead of trying to catch up + if (nextFrameTime < now) + nextFrameTime = now; + + long sleepTime = nextFrameTime - now; + if (sleepTime > 0) { + try { + Thread.sleep(sleepTime); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return true; + } + } + return false; + } + + /** + * This method is executed by periodic timer task, in frequency according to + * defined frame rate. + *

+ * It tells view to update itself. View can decide if actual re-rendering of + * graphics is needed. + */ + void ensureThatViewIsUpToDate() { + maintainRenderingContext(); + + final int millisecondsPassedSinceLastUpdate = getMillisecondsPassedSinceLastUpdate(); + + boolean renderFrame = notifyFrameListeners(millisecondsPassedSinceLastUpdate); + + // Unlimited FPS is benchmark mode: render continuously even when + // the scene reports no changes, so the measured rate reflects + // maximum achievable throughput. + if (targetFPS <= 0) { + renderFrame = true; + } + + if (viewRepaintNeeded) { + viewRepaintNeeded = false; + renderFrame = true; + } + + // abort rendering if window size is invalid + if ((getWidth() > 0) && (getHeight() > 0) && renderFrame) { + renderFrame(); + } else { + // No new frame: make sure the last submitted paint passes + // still get awaited, mouse-processed and blitted. + flushCompletedPasses(Long.MAX_VALUE); + } + } + + private void maintainRenderingContext() { + int panelWidth = getWidth(); + int panelHeight = getHeight(); + + if (panelWidth <= 0 || panelHeight <= 0) { + if (frameContexts[0] != null) { + frameContexts[0].dispose(); + frameContexts[1].dispose(); + frameContexts[2].dispose(); + frameContexts[0] = null; + frameContexts[1] = null; + frameContexts[2] = null; + renderingContext = null; + pendingPaints.clear(); + } + return; + } + + // create new rendering contexts if window size has changed OR the + // tile grid has changed (thread count, stereo toggle) + final int viewportCount = stereoModeEnabled ? 2 : 1; + final int viewportWidth = panelWidth / viewportCount; + // Total tiles ~= 10x paint threads (work-stealing granularity); + // split into roughly square tiles: squares minimize boundary + // crossings, i.e. how many tiles each shape overlaps + final int targetTiles = Math.max(1, numRenderThreads * TILES_PER_THREAD); + final int tilesX = Math.max(1, Math.min(viewportWidth, (int) Math.round( + Math.sqrt(targetTiles * (double) viewportWidth / panelHeight)))); + final int tilesY = Math.max(1, Math.min(panelHeight, + (int) Math.round((double) targetTiles / tilesX))); + if ((frameContexts[0] == null) + || (frameContexts[0].width != panelWidth) + || (frameContexts[0].height != panelHeight) + || (frameContexts[0].tilesX != tilesX) + || (frameContexts[0].tilesY != tilesY) + || (frameContexts[0].viewportCount != viewportCount)) { + // The pending paints still work on the old buffers; let them + // finish before disposing the contexts they write into. + flushCompletedPasses(Long.MAX_VALUE); + final PresentJob droppedJob = mailboxFrame.getAndSet(null); + if (droppedJob != null) { + droppedJob.gate.countDown(); + } + for (int parity = 0; parity < 3; parity++) { + if (frameContexts[parity] != null) { + frameContexts[parity].dispose(); + } + final RenderingContext context = new RenderingContext(panelWidth, panelHeight, + tilesX, tilesY, viewportCount); + context.developerTools = developerTools; + context.debugLogBuffer = debugLogBuffer; + context.lightingManager = lightingManager; + frameContexts[parity] = context; + } + } + + renderingContext = frameContexts[frameParity]; + renderingContext.transformExecutor = getOrCreateTransformExecutor(); + renderingContext.prepareForNewFrameRendering(); + } + + private boolean notifyFrameListeners(int millisecondsPassedSinceLastUpdate) { + boolean reRenderFrame = false; + for (final FrameListener listener : frameListeners) + if (listener.onFrame(this, millisecondsPassedSinceLastUpdate)) + reRenderFrame = true; + return reRenderFrame; + } + + private int getMillisecondsPassedSinceLastUpdate() { + final long currentTime = System.currentTimeMillis(); + + if (lastUpdateMillis == 0) + lastUpdateMillis = currentTime; + + final int millisecondsPassedSinceLastUpdate = (int) (currentTime - lastUpdateMillis); + lastUpdateMillis = currentTime; + return millisecondsPassedSinceLastUpdate; + } + + /** + * Removes a previously registered frame listener. + * + * @param frameListener the listener to remove + */ + public void removeFrameListener(FrameListener frameListener) { + frameListeners.remove(frameListener); + } + +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewSpaceTracker.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewSpaceTracker.java new file mode 100644 index 0000000..24ddb36 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/ViewSpaceTracker.java @@ -0,0 +1,112 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.math.TransformStack; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; + +/** + * Tracks an object's position in view/camera space for distance and angle calculations. + * + *

Used primarily for level-of-detail (LOD) decisions based on how far and at what + * angle the viewer is from an object. The tracker maintains the object's center point + * transformed into view space, and optionally orientation axes for angle calculations.

+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape + */ +public class ViewSpaceTracker { + + /** + * The object's center point (0,0,0 in object space) transformed to view space. + */ + public Vertex center = new Vertex(); + + /** + * Point at (10,0,0) in object space, used for XZ angle calculation. + * Only initialized if orientation tracking is enabled. + */ + public Vertex right; + + /** + * Point at (0,10,0) in object space, used for YZ angle calculation. + * Only initialized if orientation tracking is enabled. + */ + public Vertex down; + + /** + * Creates a new view space tracker. + */ + public ViewSpaceTracker() { + } + + /** + * Transforms the tracked points from object space to view space. + * + * @param transformPipe the current transform stack + * @param renderingContext the rendering context for frame info + */ + public void analyze(final TransformStack transformPipe, + final RenderingContext renderingContext) { + + center.calculateLocationRelativeToViewer(transformPipe, renderingContext); + + if (right != null) { + right.calculateLocationRelativeToViewer(transformPipe, renderingContext); + down.calculateLocationRelativeToViewer(transformPipe, renderingContext); + } + } + + /** + * Enables tracking of orientation axes for angle calculations. + * Disabled by default to save computation when angles are not needed. + */ + public void enableOrientationTracking() { + right = new Vertex(new Point3D(10, 0, 0)); + down = new Vertex(new Point3D(0, 10, 0)); + } + + /** + * Returns the angle between the viewer and object in the XY plane. + * + * @return the XY angle in radians + */ + public double getAngleXY(final RenderingContext renderingContext) { + return center.transformedCoordinate(renderingContext) + .getAngleXY(down.transformedCoordinate(renderingContext)); + } + + /** + * Returns the angle between the viewer and object in the XZ plane. + * + * @return the XZ angle in radians + */ + public double getAngleXZ(final RenderingContext renderingContext) { + return center.transformedCoordinate(renderingContext) + .getAngleXZ(right.transformedCoordinate(renderingContext)); + } + + /** + * Returns the angle between the viewer and object in the YZ plane. + * + * @return the YZ angle in radians + */ + public double getAngleYZ(final RenderingContext renderingContext) { + return center.transformedCoordinate(renderingContext) + .getAngleYZ(down.transformedCoordinate(renderingContext)); + } + + /** + * Returns the distance from the camera to the object's center. + * Used for level-of-detail calculations. + * + * @return the distance in world units + */ + public double getDistanceToCamera(final RenderingContext renderingContext) { + return center.transformedCoordinate(renderingContext).getVectorLength(); + } + +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/HeadLookController.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/HeadLookController.java new file mode 100644 index 0000000..dab2fc8 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/HeadLookController.java @@ -0,0 +1,144 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.headtrack; + +import eu.svjatoslav.aukio.e3d.gui.FrameListener; +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; +import eu.svjatoslav.aukio.e3d.math.Quaternion; + +import java.awt.event.KeyEvent; + +/** + * Applies head orientation from XR glasses to the camera, every frame: + * turn your head left and the view pans left, look up and the view + * tilts up. The virtual world stays fixed in space while the display + * moves with your head. + * + *

Complementary mouse look: mouse drag and head tracking + * compose instead of fighting. Every frame the controller compares the + * camera rotation against what it wrote last frame; a mismatch means + * the mouse moved the view, and the difference is folded into the base + * yaw/pitch so the view stays exactly where the mouse put it — + * subsequent head motion then composes on top. Head movement during a + * drag is not lost: it re-applies as a relative delta once the drag + * ends.

+ * + *

Press Scroll Lock to recenter: the current head pose becomes + * "straight ahead" and the camera returns to its base pose. Needed + * occasionally because gyro-only yaw drifts slowly.

+ * + *

Arrow-key movement and wheel vertical movement are unaffected + * (they translate, not rotate).

+ * + *

Head roll is measured but deliberately not applied — a constant + * world horizon reads better than a world that tilts with your neck.

+ */ +public final class HeadLookController implements FrameListener { + + /** + * Sign mapping, confirmed by live user testing: positive engine yaw + * turns the view left, so head-yaw is added directly; head-down + * integrates to positive pitch while negative engine pitch looks + * down, so pitch is subtracted. + */ + private static final double YAW_SIGN = 1.0; + private static final double PITCH_SIGN = -1.0; + + /** Yaw sensitivity multiplier; 2.0 = the view turns twice as far as + * the head (live-tuned 2026-09-10: head yaw was under-responsive). */ + private static final double YAW_GAIN = 2.0; + + /** Pitch sensitivity multiplier; 1.0 = the world stays fixed in space. */ + private static final double PITCH_GAIN = 1.0; + + /** + * Two rotations are "the same" below this per-component distance. + * A rotation this controller wrote itself compares bitwise identical; + * anything past this epsilon came from another writer (mouse drag). + */ + private static final double SAME_ROTATION_EPSILON = 1e-9; + + private final HeadTracker tracker; + private final ViewPanel viewPanel; + + private double baseYaw, basePitch; + + /** Rotation this controller wrote to the camera last frame. */ + private Quaternion lastApplied; + + private boolean recenteredOnce; + private boolean scrollLockWasPressed; + + public HeadLookController(final HeadTracker tracker, + final ViewPanel viewPanel) { + this.tracker = tracker; + this.viewPanel = viewPanel; + } + + @Override + public boolean onFrame(final ViewPanel viewPanel, + final int millisecondsSinceLastFrame) { + if (!tracker.isCalibrated()) + return false; + + final boolean scrollLockPressed = viewPanel.getInputManager() + .isKeyPressed(KeyEvent.VK_SCROLL_LOCK); + if (scrollLockPressed && !scrollLockWasPressed) { + captureBasePose(); + tracker.recenter(); + recenteredOnce = true; + lastApplied = null; + } + scrollLockWasPressed = scrollLockPressed; + + if (!recenteredOnce) { + // Boot recenter: capture the camera pose the application + // configured (not the constructor-time identity) and make + // the current head pose "straight ahead". + captureBasePose(); + tracker.recenter(); + recenteredOnce = true; + } + + final double lookYaw = tracker.getLookYaw(); + final double lookPitch = tracker.getLookPitch(); + + if (lastApplied != null) { + final Quaternion current = viewPanel.getCamera() + .getTransform().getRotation(); + if (!nearlyEqual(current, lastApplied)) { + // An external writer (mouse drag) moved the camera. + // Re-derive base yaw/pitch so the view stays exactly + // where the mouse left it — no snap-back. + final double[] angles = current.toAngles(); + baseYaw = angles[0] - YAW_SIGN * YAW_GAIN * lookYaw; + basePitch = angles[1] - PITCH_SIGN * PITCH_GAIN * lookPitch; + } + } + + final double yaw = baseYaw + YAW_SIGN * YAW_GAIN * lookYaw; + final double pitch = basePitch + PITCH_SIGN * PITCH_GAIN * lookPitch; + final Quaternion applied = Quaternion.fromAngles(yaw, pitch, 0); + viewPanel.getCamera().getTransform().getRotation().set(applied); + lastApplied = applied; + + return true; + } + + private void captureBasePose() { + final double[] angles = viewPanel.getCamera().getTransform() + .getRotation().toAngles(); + baseYaw = angles[0]; + basePitch = angles[1]; + } + + private static boolean nearlyEqual(final Quaternion a, + final Quaternion b) { + return Math.abs(a.w - b.w) < SAME_ROTATION_EPSILON + && Math.abs(a.x - b.x) < SAME_ROTATION_EPSILON + && Math.abs(a.y - b.y) < SAME_ROTATION_EPSILON + && Math.abs(a.z - b.z) < SAME_ROTATION_EPSILON; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/HeadTracker.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/HeadTracker.java new file mode 100644 index 0000000..b822dcc --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/HeadTracker.java @@ -0,0 +1,308 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.headtrack; + +import com.sun.jna.Memory; + +/** + * Head orientation tracker built on the RayNeo glasses IMU. + * + *

A daemon thread reads 500 Hz IMU frames and fuses them into yaw, + * pitch and roll angles with a complementary filter: gyroscope + * integration for responsiveness, accelerometer gravity as the long-term + * pitch/roll reference. Yaw has no absolute reference (the magnetometer + * is too noisy indoors near a laptop), so it drifts slowly — call + * {@link #recenter()} (Scroll Lock in {@link HeadLookController}) + * whenever the view feels off-center.

+ * + *

Sensor axes, established by calibration capture: gyro[0]/X = pitch + * (nod), gyro[1]/Y = yaw (left/right turn), gyro[2]/Z = roll + * (ear-to-shoulder tilt).

+ * + *

Drift control ("standstill perceived as slow motion"):

+ *
    + *
  • Startup calibration only accepts samples taken while the + * glasses are stationary — moving samples would poison the bias + * estimate with phantom rotation. Putting the glasses on a desk + * for the first second gives the cleanest estimate, but a held + * head passes the stillness gate too.
  • + *
  • While running, the bias is continuously re-estimated whenever + * the head is stationary (fast EMA, ~0.5s time constant).
  • + *
  • Residual rates below {@value #DEADBAND_DPS} dps after bias + * subtraction are integrated as exactly zero — sensor noise can + * never accumulate into visible rotation.
  • + *
+ */ +public final class HeadTracker { + + /** Sensor axis indices. */ + private static final int AXIS_PITCH = 0; + private static final int AXIS_YAW = 1; + private static final int AXIS_ROLL = 2; + + private static final double DPS_TO_RAD = Math.PI / 180.0; + + /** Stationary samples needed for the initial bias estimate. */ + private static final int CALIBRATION_SAMPLES = 500; + + /** Give up waiting for stillness after this long and calibrate anyway. */ + private static final int CALIBRATION_TIMEOUT_SAMPLES = 5000; + + /** Gyro magnitude below which the head counts as stationary (dps). */ + private static final double STATIONARY_THRESHOLD_DPS = 0.5; + + /** + * Rates below this after bias subtraction are treated as zero. + * Kills the slow phantom rotation that gyro noise would otherwise + * integrate into. + */ + private static final double DEADBAND_DPS = 0.07; + + /** Complementary filter weight: accel correction per sample. */ + private static final double ACCEL_BLEND = 0.02; + + /** Bias re-estimation speed while stationary (~0.5s time constant). */ + private static final double BIAS_BLEND = 0.004; + + /** Output smoothing (exponential moving average per sample). */ + private static final double OUTPUT_SMOOTHING = 0.35; + + private final RayNeoHid device; + private final Thread readerThread; + private final Memory frame = new Memory(RayNeoHid.FRAME_SIZE); + + private final double[] gyroBias = new double[3]; + private int calibrationSamples; + private int calibrationAttempts; + + private double fusedYaw, fusedPitch, fusedRoll; + private double centerYaw, centerPitch; + private double smoothYaw, smoothPitch; + private int lastTick; + private long lastSampleNanos; + private boolean hasTick; + + private volatile boolean running = true; + private volatile boolean calibrated; + + // diagnostics (all written on the reader thread, read anywhere) + private volatile int framesReceived; + private volatile int lastTickValue; + private volatile double lastDtMillis; + private volatile double maxAbsGyroDps; + + public HeadTracker(final RayNeoHid device) { + this.device = device; + readerThread = new Thread(this::readLoop, "rayneo-head-tracker"); + readerThread.setDaemon(true); + } + + public void start() { + device.startImu(); + readerThread.start(); + final Thread watchdog = new Thread(this::watchdogLoop, + "rayneo-imu-watchdog"); + watchdog.setDaemon(true); + watchdog.start(); + } + + /** + * The glasses occasionally answer IMU-ON with a stream of frozen + * frames (tick not advancing). If frames stall or the tick freezes, + * re-send the enable sequence. + */ + private void watchdogLoop() { + int lastFrames = 0; + int lastTickSeen = 0; + while (running) { + try { + Thread.sleep(2000); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + final int frames = framesReceived; + final int tick = lastTickValue; + if (frames > 10 && frames == lastFrames) { + System.err.println("head tracker: frame stream stalled, " + + "re-enabling IMU"); + device.startImu(); + } else if (frames > 1000 && tick == lastTickSeen) { + System.err.println("head tracker: tick frozen (stale " + + "sensor data), re-enabling IMU"); + device.startImu(); + } + lastFrames = frames; + lastTickSeen = tick; + } + } + + /** + * One-line diagnostic snapshot: frame flow, tick, integration health. + */ + public String getDebugString() { + return String.format( + "frames=%d tick=%d dt=%.2fms maxGyro=%.1fdps " + + "calibrated=%b yaw=%.2fdeg pitch=%.2fdeg", + framesReceived, lastTickValue, lastDtMillis, + maxAbsGyroDps, calibrated, Math.toDegrees(getLookYaw()), + Math.toDegrees(getLookPitch())); + } + + /** + * Makes the current head orientation the new "straight ahead". + */ + public synchronized void recenter() { + centerYaw = fusedYaw; + centerPitch = fusedPitch; + smoothYaw = 0; + smoothPitch = 0; + } + + /** Head yaw relative to the last recenter, radians. */ + public synchronized double getLookYaw() { + return smoothYaw; + } + + /** Head pitch relative to the last recenter, radians. */ + public synchronized double getLookPitch() { + return smoothPitch; + } + + /** False until the initial gyro bias calibration has finished. */ + public boolean isCalibrated() { + return calibrated; + } + + /** True while the reader thread is alive and frames may be flowing. */ + public boolean isRunning() { + return running && readerThread.isAlive(); + } + + public void stop() { + running = false; + // Close first: the reader thread is parked in a native read that no + // interrupt can wake; closing the fd fails the read instantly. + device.close(); + try { + readerThread.join(500); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private void readLoop() { + while (running) { + final int type = device.readFrame(frame); + if (type < 0) { + if (running) + System.err.println("head tracker: device read failed " + + "(unplugged?), stopping"); + running = false; + return; + } + if (type == RayNeoHid.TYPE_IMU) + onSample(); + } + running = false; + } + + private void onSample() { + final float gx = frame.getFloat(16); + final float gy = frame.getFloat(20); + final float gz = frame.getFloat(24); + final float ax = frame.getFloat(4); + final float ay = frame.getFloat(8); + final float az = frame.getFloat(12); + final int tick = frame.getInt(40); + + framesReceived++; + lastTickValue = tick; + final double absGyro = Math.sqrt(gx * gx + gy * gy + gz * gz); + if (absGyro > maxAbsGyroDps) + maxAbsGyroDps = absGyro; + + if (!calibrated) { + calibrate(gx, gy, gz, ax, ay, az, tick, absGyro); + return; + } + + final double dt; + if (hasTick) { + // The device tick is NOT microseconds (measured: ~50µs per + // unit) — useless for integration timing, kept only as a + // liveness signal for the watchdog. Wall clock at 500Hz is + // fine; jitter is smoothed by the output EMA. + dt = Math.min(Math.max((System.nanoTime() - lastSampleNanos) + / 1_000_000_000.0, 0), 0.1); + } else { + dt = 0.002; + } + lastDtMillis = dt * 1000; + lastSampleNanos = System.nanoTime(); + lastTick = tick; + hasTick = true; + + if (absGyro < STATIONARY_THRESHOLD_DPS) + for (int i = 0; i < 3; i++) { + final double g = i == AXIS_PITCH ? gx : i == AXIS_YAW ? gy : gz; + gyroBias[i] += (g - gyroBias[i]) * BIAS_BLEND; + } + + synchronized (this) { + fusedPitch += deadband(gx - gyroBias[AXIS_PITCH]) * DPS_TO_RAD * dt; + fusedYaw += deadband(gy - gyroBias[AXIS_YAW]) * DPS_TO_RAD * dt; + fusedRoll += deadband(gz - gyroBias[AXIS_ROLL]) * DPS_TO_RAD * dt; + + // gravity reference: pitch rotates around sensor X (mixes the + // Y/Z gravity components), roll around sensor Z (mixes X/Y) + final double accelPitch = Math.atan2(az, ay); + final double accelRoll = Math.atan2(-ax, ay); + fusedPitch += (accelPitch - fusedPitch) * ACCEL_BLEND; + fusedRoll += (accelRoll - fusedRoll) * ACCEL_BLEND; + + final double yaw = fusedYaw - centerYaw; + final double pitch = fusedPitch - centerPitch; + smoothYaw += (yaw - smoothYaw) * OUTPUT_SMOOTHING; + smoothPitch += (pitch - smoothPitch) * OUTPUT_SMOOTHING; + } + } + + /** + * Gyro bias calibration: only stationary samples contribute — if + * the user is already turning, those samples would poison the bias + * with several dps of phantom drift. Give up after ~10s and accept + * whatever we have. + */ + private void calibrate(final float gx, final float gy, final float gz, + final float ax, final float ay, final float az, + final int tick, final double absGyro) { + calibrationAttempts++; + if (absGyro < STATIONARY_THRESHOLD_DPS + || calibrationAttempts > CALIBRATION_TIMEOUT_SAMPLES) { + gyroBias[AXIS_PITCH] += gx; + gyroBias[AXIS_YAW] += gy; + gyroBias[AXIS_ROLL] += gz; + calibrationSamples++; + } + if (calibrationSamples >= CALIBRATION_SAMPLES) { + for (int i = 0; i < 3; i++) + gyroBias[i] /= calibrationSamples; + // jump-start the tilt estimate from gravity; otherwise the + // filter would take a second to converge and the boot + // recenter would capture the unconverged zero + fusedPitch = Math.atan2(az, ay); + fusedRoll = Math.atan2(-ax, ay); + calibrated = true; + lastTick = tick; + lastSampleNanos = System.nanoTime(); + hasTick = true; + } + } + + private static double deadband(final double rateDps) { + return Math.abs(rateDps) < DEADBAND_DPS ? 0 : rateDps; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/HeadTrackingManager.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/HeadTrackingManager.java new file mode 100644 index 0000000..3808541 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/HeadTrackingManager.java @@ -0,0 +1,117 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.headtrack; + +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; + +/** + * Hot-plug manager for XR glasses head tracking. Polls for RayNeo + * glasses every two seconds: + * + *
    + *
  • glasses plugged in → open the HID pipe, start a + * {@link HeadTracker}, attach a {@link HeadLookController} to the + * view's frame listeners
  • + *
  • glasses unplugged (tracker read fails) → stop the tracker and + * detach the controller, so the camera is no longer driven by a + * dead device
  • + *
  • plugged back in → fresh tracker, fresh boot recenter
  • + *
+ * + *

Permission failures are reported once, then retried silently — + * the user may install the udev rule while the application is running.

+ */ +public final class HeadTrackingManager { + + private static final long POLL_INTERVAL_MS = 2000; + + private final ViewPanel viewPanel; + private final Thread thread; + + private volatile boolean running = true; + private HeadTracker tracker; + private HeadLookController controller; + private boolean failureReported; + + public HeadTrackingManager(final ViewPanel viewPanel) { + this.viewPanel = viewPanel; + thread = new Thread(this::pollLoop, "head-tracking-hotplug"); + thread.setDaemon(true); + } + + public void start() { + thread.start(); + } + + /** Active tracker, or null when no glasses are connected. */ + public HeadTracker getTracker() { + return tracker; + } + + public void stop() { + running = false; + // Wake the 2 s poll sleep; without the interrupt the join below + // burns its full 1 s timeout on the EDT at every window close. + thread.interrupt(); + try { + thread.join(1000); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + disconnect(); + } + + private void pollLoop() { + while (running) { + poll(); + try { + Thread.sleep(POLL_INTERVAL_MS); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + + private void poll() { + if (tracker != null && !tracker.isRunning()) { + System.out.println("head tracking: glasses disconnected"); + disconnect(); + } + if (tracker != null) + return; + + try { + final RayNeoHid glasses = RayNeoHid.open(); + if (glasses == null) + return; + tracker = new HeadTracker(glasses); + tracker.start(); + controller = new HeadLookController(tracker, viewPanel); + viewPanel.addFrameListener(controller); + failureReported = false; + System.out.println("head tracking: RayNeo glasses detected, " + + "calibrating (hold still for a second)"); + } catch (final Exception e) { + tracker = null; + if (!failureReported) { + failureReported = true; + System.err.println("head tracking unavailable: " + + e.getMessage()); + } + } + } + + private void disconnect() { + if (controller != null) { + viewPanel.removeFrameListener(controller); + controller = null; + } + if (tracker != null) { + tracker.stop(); + tracker = null; + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/RayNeoHid.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/RayNeoHid.java new file mode 100644 index 0000000..c7a47a8 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/headtrack/RayNeoHid.java @@ -0,0 +1,149 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.headtrack; + +import com.sun.jna.Library; +import com.sun.jna.Memory; +import com.sun.jna.Native; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Raw HID transport for RayNeo AR glasses (T&A Mobile Phones, + * VID 1BBB, PID AF50) via Linux hidraw. + * + *

IMU sample layout inside a 0x65 frame (all little-endian):

+ *
    + *
  • float acc[3] at offset 4 (m/s²)
  • + *
  • float gyro[3] at offset 16 (degrees/second)
  • + *
  • float temperature at 28, float magnet[0..1] at 32
  • + *
  • uint32 tick at 40 (NOT microseconds — ~50µs per unit; use only + * as a liveness signal, never for integration timing)
  • + *
  • float psensor at 44, float lsensor at 48, float magnet[2] at 52
  • + *
+ * + *

Startup sequence that yields live data: psensor enable (0x38) THEN + * IMU on (0x01). Without the psensor enable the glasses may stream + * frozen sensor values.

+ * + *

Permissions: /dev/hidraw* is root-only by default. Install a udev + * rule (e.g. {@code 99-rayneo-glasses.rules} with MODE="0666" for + * VID 1BBB PID AF50) to allow user access.

+ */ +public final class RayNeoHid implements AutoCloseable { + + public static final int FRAME_SIZE = 64; + + private static final int O_RDWR = 0x02; + private static final byte MAGIC_OUT = 0x66; + private static final byte MAGIC_IN = (byte) 0x99; + + /** Frame type: IMU sample. */ + public static final int TYPE_IMU = 0x65; + + private static final int CMD_DEVICE_INFO = 0x00; + private static final int CMD_IMU_ON = 0x01; + private static final int CMD_IMU_OFF = 0x02; + private static final int CMD_PSENSOR_ENABLE = 0x38; + + private interface CLib extends Library { + CLib INSTANCE = Native.load("c", CLib.class); + + int open(String path, int flags); + + int close(int fd); + + int read(int fd, Memory buffer, int count); + + int write(int fd, Memory buffer, int count); + } + + private final int fd; + private final Memory frameBuffer = new Memory(FRAME_SIZE); + + private RayNeoHid(final int fd) { + this.fd = fd; + } + + /** + * Finds the hidraw node of the glasses by scanning sysfs uevent data, + * then opens it. Returns null when the glasses are not connected. + */ + public static RayNeoHid open() throws IOException { + final Path hidrawDir = Path.of("/sys/class/hidraw"); + if (!Files.isDirectory(hidrawDir)) + return null; + + try (DirectoryStream nodes = Files.newDirectoryStream(hidrawDir)) { + for (final Path node : nodes) { + final Path uevent = node.resolve("device/uevent"); + if (!Files.isRegularFile(uevent)) + continue; + final String content = Files.readString(uevent); + if (content.contains("00001BBB") && content.contains("0000AF50")) { + final Path dev = Path.of("/dev", node.getFileName().toString()); + final int fd = CLib.INSTANCE.open(dev.toString(), O_RDWR); + if (fd < 0) + throw new IOException("cannot open " + dev + + " (permissions? install udev rule " + + "99-rayneo-glasses.rules)"); + return new RayNeoHid(fd); + } + } + } + return null; + } + + /** + * Enables the proximity sensor pipeline and starts IMU streaming. + * Both commands are required; psensor-first ordering matters. + */ + public void startImu() { + sendCommand(CMD_PSENSOR_ENABLE, 0); + sendCommand(CMD_IMU_ON, 0); + } + + /** + * Stops IMU streaming. + */ + public void stopImu() { + sendCommand(CMD_IMU_OFF, 0); + } + + private void sendCommand(final int command, final int value) { + synchronized (frameBuffer) { + frameBuffer.clear(); + frameBuffer.setByte(0, MAGIC_OUT); + frameBuffer.setByte(1, (byte) command); + frameBuffer.setByte(2, (byte) value); + CLib.INSTANCE.write(fd, frameBuffer, FRAME_SIZE); + } + } + + /** + * Blocking read of one 64-byte frame. Returns the frame type byte + * (e.g. {@link #TYPE_IMU}) or -1 on error/closed device. The frame + * bytes are left in the given buffer for the caller to parse. + */ + public int readFrame(final Memory out) { + final int n = CLib.INSTANCE.read(fd, out, FRAME_SIZE); + if (n != FRAME_SIZE || out.getByte(0) != MAGIC_IN) + return -1; + return out.getByte(1) & 0xFF; + } + + @Override + public void close() { + try { + stopImu(); + } catch (final Exception ignored) { + // device may already be unplugged + } + CLib.INSTANCE.close(fd); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.java new file mode 100644 index 0000000..b0b7d29 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/InputManager.java @@ -0,0 +1,378 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.humaninput; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Camera; +import eu.svjatoslav.aukio.e3d.gui.FrameListener; +import eu.svjatoslav.aukio.e3d.gui.ViewFrame; +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; +import eu.svjatoslav.aukio.e3d.math.Quaternion; + +import java.awt.*; +import java.awt.event.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Manages mouse and keyboard input for the 3D view. + * + *

Handles mouse/keyboard events, tracks pressed keys and mouse state, + * and forwards events to the appropriate handlers. Also provides default camera + * control via mouse dragging (look around) and mouse wheel (vertical movement).

+ * + * @see ViewPanel#getInputManager() + */ +public class InputManager implements + MouseMotionListener, KeyListener, MouseListener, MouseWheelListener, FrameListener { + + private final Map pressedKeysToPressedTimeMap = new HashMap<>(); + private final List detectedMouseEvents = new ArrayList<>(); + private final List detectedKeyEvents = new ArrayList<>(); + private final Point2D mouseDelta = new Point2D(); + private final MouseEvent reusableHoverEvent = new MouseEvent(new Point2D(), 0); + private final Point2D reusableMouseLocation = new Point2D(); + private final ViewPanel viewPanel; + private int wheelVerticalUnits = 0; + private int wheelHorizontalUnits = 0; + private Point2D oldMouseCoordinatesWhenDragging; + private Point2D currentMouseLocation; + private boolean mouseMoved; + private boolean mouseWithinWindow = false; + private double cameraYaw = 0; + private double cameraPitch = 0; + private double cameraRoll = 0; + + /** + * Creates an input manager attached to the given view panel. + * + * @param viewPanel the view panel to receive input from + */ + public InputManager(final ViewPanel viewPanel) { + this.viewPanel = viewPanel; + bind(viewPanel); + } + + /** + * Processes accumulated input events and updates camera based on mouse drag/wheel. + * + * @param viewPanel the view panel + * @param millisecondsSinceLastFrame time since last frame (unused) + * @return {@code true} if a view repaint is needed + */ + @Override + public boolean onFrame(final ViewPanel viewPanel, final int millisecondsSinceLastFrame) { + boolean viewUpdateNeeded = handleKeyboardEvents(); + viewUpdateNeeded |= handleMouseClicksAndHover(viewPanel); + viewUpdateNeeded |= handleMouseDragging(); + viewUpdateNeeded |= handleMouseScrolling(); + return viewUpdateNeeded; + } + + /** + * Binds this input manager to listen for events on the given component. + * Also registers a global {@link KeyEventDispatcher} so keyboard shortcuts + * (F11, F12, SHIFT+F11) work even when the ViewPanel does not have focus. + * + * @param component the component to attach listeners to + */ + private void bind(final Component component) { + component.addMouseMotionListener(this); + component.addKeyListener(this); + component.addMouseListener(this); + component.addMouseWheelListener(this); + } + + /** + * Processes all accumulated keyboard events and forwards them to the current focus owner. + * + * @return {@code true} if any event handler requested a repaint + */ + private boolean handleKeyboardEvents() { + final KeyboardInputHandler currentFocusOwner = viewPanel.getKeyboardFocusStack().getCurrentFocusOwner(); + + if (currentFocusOwner == null) + return false; + + boolean viewUpdateNeeded = false; + synchronized (detectedKeyEvents) { + for (int i = 0; i < detectedKeyEvents.size(); i++) + viewUpdateNeeded |= processKeyEvent(currentFocusOwner, detectedKeyEvents.get(i)); + detectedKeyEvents.clear(); + } + return viewUpdateNeeded; + } + + /** + * Processes a single keyboard event by dispatching to the focus owner. + * + * @param currentFocusOwner the component that currently has keyboard focus + * @param keyEvent the keyboard event to process + * @return {@code true} if the handler requested a repaint + */ + private boolean processKeyEvent(KeyboardInputHandler currentFocusOwner, KeyEvent keyEvent) { + switch (keyEvent.getID()) { + case KeyEvent.KEY_PRESSED: + return currentFocusOwner.keyPressed(keyEvent, viewPanel); + + case KeyEvent.KEY_RELEASED: + return currentFocusOwner.keyReleased(keyEvent, viewPanel); + } + return false; + } + + /** + * Handles mouse clicks and hover detection. + * Sets up the mouse event in the rendering context for shape hit testing. + * + * @param viewPanel the view panel + * @return {@code true} if a repaint is needed + */ + private synchronized boolean handleMouseClicksAndHover(final ViewPanel viewPanel) { + boolean rerenderNeeded = false; + MouseEvent event = findClickLocationToTrace(); + if (event != null) { + rerenderNeeded = true; + } else { + if (mouseMoved) { + mouseMoved = false; + rerenderNeeded = true; + } + + if (currentMouseLocation != null) { + reusableHoverEvent.coordinate.x = currentMouseLocation.x; + reusableHoverEvent.coordinate.y = currentMouseLocation.y; + event = reusableHoverEvent; + } + } + + if (viewPanel.getRenderingContext() != null) + viewPanel.getRenderingContext().setMouseEvent(event); + + return rerenderNeeded; + } + + private MouseEvent findClickLocationToTrace() { + synchronized (detectedMouseEvents) { + if (detectedMouseEvents.isEmpty()) + return null; + + return detectedMouseEvents.remove(0); + } + } + + /** + * Returns whether the specified key is currently pressed. + * + * @param keyCode the key code (from {@link java.awt.event.KeyEvent}) + * @return {@code true} if the key is currently pressed + */ + public boolean isKeyPressed(final int keyCode) { + return pressedKeysToPressedTimeMap.containsKey(keyCode); + } + + @Override + public void keyPressed(final KeyEvent evt) { + if (evt.getKeyCode() == java.awt.event.KeyEvent.VK_F12) { + viewPanel.showDeveloperToolsPanel(); + return; + } + if (evt.getKeyCode() == java.awt.event.KeyEvent.VK_F11) { + final ViewFrame frame = findParentViewFrame(); + if (frame != null) { + if (evt.isShiftDown()) { + // SHIFT+F11: toggle stereo + fullscreen together + final boolean stereoOn = !viewPanel.isStereoModeEnabled(); + viewPanel.setStereoModeEnabled(stereoOn); + frame.setFullscreen(stereoOn); + } else { + // F11: toggle fullscreen only + frame.toggleFullscreen(); + } + } + return; + } + // +/- adjusts stereo IPD (inter-pupillary distance) when stereo is active + if (viewPanel.isStereoModeEnabled()) { + final int code = evt.getKeyCode(); + if (code == java.awt.event.KeyEvent.VK_PLUS + || code == java.awt.event.KeyEvent.VK_EQUALS + || code == java.awt.event.KeyEvent.VK_ADD) { + viewPanel.setStereoIPD(viewPanel.getStereoIPD() + 0.5); + return; + } + if (code == java.awt.event.KeyEvent.VK_MINUS + || code == java.awt.event.KeyEvent.VK_SUBTRACT) { + viewPanel.setStereoIPD(Math.max(0.5, viewPanel.getStereoIPD() - 0.5)); + return; + } + } + synchronized (detectedKeyEvents) { + pressedKeysToPressedTimeMap.put(evt.getKeyCode(), System.currentTimeMillis()); + detectedKeyEvents.add(evt); + } + } + + @Override + public void keyReleased(final KeyEvent evt) { + synchronized (detectedKeyEvents) { + pressedKeysToPressedTimeMap.remove(evt.getKeyCode()); + detectedKeyEvents.add(evt); + } + } + + @Override + public void keyTyped(final KeyEvent e) { + } + + @Override + public void mouseClicked(final java.awt.event.MouseEvent e) { + synchronized (detectedMouseEvents) { + detectedMouseEvents.add(new MouseEvent(e.getX(), e.getY(), e.getButton())); + } + } + + @Override + public void mouseDragged(final java.awt.event.MouseEvent evt) { + reusableMouseLocation.x = evt.getX(); + reusableMouseLocation.y = evt.getY(); + + if (oldMouseCoordinatesWhenDragging == null) { + oldMouseCoordinatesWhenDragging = new Point2D(reusableMouseLocation.x, reusableMouseLocation.y); + return; + } + + mouseDelta.x += reusableMouseLocation.x - oldMouseCoordinatesWhenDragging.x; + mouseDelta.y += reusableMouseLocation.y - oldMouseCoordinatesWhenDragging.y; + + oldMouseCoordinatesWhenDragging.x = reusableMouseLocation.x; + oldMouseCoordinatesWhenDragging.y = reusableMouseLocation.y; + } + + @Override + public void mouseEntered(final java.awt.event.MouseEvent e) { + mouseWithinWindow = true; + } + + @Override + public synchronized void mouseExited(final java.awt.event.MouseEvent e) { + mouseWithinWindow = false; + currentMouseLocation = null; + } + + @Override + public synchronized void mouseMoved(final java.awt.event.MouseEvent e) { + if (currentMouseLocation == null) + currentMouseLocation = new Point2D(e.getX(), e.getY()); + else { + currentMouseLocation.x = e.getX(); + currentMouseLocation.y = e.getY(); + } + mouseMoved = true; + } + + @Override + public void mousePressed(final java.awt.event.MouseEvent e) { + // Extra buttons (mouse back/forward) never produce an AWT CLICKED + // event on Linux (only PRESSED/RELEASED), so they are delivered + // to the component already on press. + if (e.getButton() > 3) { + synchronized (detectedMouseEvents) { + detectedMouseEvents.add( + new MouseEvent(e.getX(), e.getY(), e.getButton())); + } + } + // Initialize camera rotation state from current camera orientation. + // This prevents a jump when the camera was programmatically positioned + // with a non-default rotation before the user started dragging. + final Camera camera = viewPanel.getCamera(); + final double[] angles = camera.getTransform().getRotation().toAngles(); + cameraYaw = angles[0]; + cameraPitch = angles[1]; + cameraRoll = angles[2]; + } + + @Override + public void mouseReleased(final java.awt.event.MouseEvent evt) { + oldMouseCoordinatesWhenDragging = null; + } + + @Override + public void mouseWheelMoved(final java.awt.event.MouseWheelEvent evt) { + // horizontal wheel input arrives as shift-modified vertical + // rotation (AWT convention on all platforms) + if (evt.isShiftDown()) + wheelHorizontalUnits += evt.getWheelRotation(); + else + wheelVerticalUnits += evt.getWheelRotation(); + } + + /** + * Routes scroll wheel input: to the focused GUI component when it + * consumes it, otherwise to the camera (vertical = up/down, + * horizontal = strafe left/right). + */ + private boolean handleMouseScrolling() { + final int vertical = wheelVerticalUnits; + final int horizontal = wheelHorizontalUnits; + wheelVerticalUnits = 0; + wheelHorizontalUnits = 0; + if (vertical == 0 && horizontal == 0) + return false; + + final KeyboardInputHandler focusOwner = viewPanel + .getKeyboardFocusStack().getCurrentFocusOwner(); + if (focusOwner instanceof MouseInteractionController component + && component.mouseWheelMoved(vertical, horizontal)) + // consumed by the focused component + return true; + + final Camera camera = viewPanel.getCamera(); + final double actualAcceleration = 50 * camera.cameraAcceleration * (1 + (camera.getMovementSpeed() / 10)); + camera.getMovementVector().y += (vertical * actualAcceleration); + camera.getMovementVector().x += (horizontal * actualAcceleration); + camera.enforceSpeedLimit(); + return true; + } + + private boolean handleMouseDragging() { + if (mouseDelta.isZero()) { + return false; + } + + cameraYaw -= mouseDelta.x / 50.0; + cameraPitch -= mouseDelta.y / 50.0; + + cameraPitch = Math.max(-Math.PI / 2 + 0.001, + Math.min( Math.PI / 2 - 0.001, cameraPitch)); + + final Camera camera = viewPanel.getCamera(); + camera.getTransform().getRotation().set( + Quaternion.fromAngles(cameraYaw, cameraPitch, cameraRoll)); + + mouseDelta.zero(); + return true; + } + + /** + * Walks up the component hierarchy from the view panel to find the + * parent {@link ViewFrame}, if any. + * + * @return the parent ViewFrame, or {@code null} if the ViewPanel is + * not embedded in a ViewFrame + */ + private ViewFrame findParentViewFrame() { + java.awt.Container parent = viewPanel.getParent(); + while (parent != null) { + if (parent instanceof ViewFrame) + return (ViewFrame) parent; + parent = parent.getParent(); + } + return null; + } + +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardFocusStack.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardFocusStack.java new file mode 100644 index 0000000..f89d60e --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardFocusStack.java @@ -0,0 +1,103 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.humaninput; +import eu.svjatoslav.aukio.e3d.geometry.Camera; + +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; + +/** + * Manages keyboard focus for interactive 3D components. + * + *

Exactly one {@link KeyboardInputHandler} has keyboard focus at a time. + * When a component gains focus (e.g., by being clicked), the previous focus + * owner is notified and forgotten. When the component releases focus (ESC or + * middle mouse button), focus falls back to the default handler — never to a + * previously focused component (there is no focus stack).

+ * + *

The default handler is a {@link WorldNavigationUserInputTracker}, which + * handles WASD/arrow-key camera movement while no component has focus.

+ * + *

Focus flow example:

+ *
{@code
+ * // Initial state: WorldNavigationUserInputTracker has focus (camera movement)
+ * // User clicks on a text editor:
+ * focus.pushFocusOwner(textEditor);
+ * // Now textEditor receives keyboard events
+ *
+ * // User presses ESC or middle-clicks the editor:
+ * focus.popFocusOwner();
+ * // Camera movement is active again
+ * }
+ * + * @see KeyboardInputHandler the interface that focus owners must implement + * @see WorldNavigationUserInputTracker default handler for camera navigation + */ +public class KeyboardFocusStack { + + private final ViewPanel viewPanel; + private final WorldNavigationUserInputTracker defaultInputHandler = new WorldNavigationUserInputTracker(); + private KeyboardInputHandler currentUserInputHandler; + + /** + * Creates a new focus manager for the given view panel, with + * {@link WorldNavigationUserInputTracker} as the default focus owner. + * + * @param viewPanel the view panel this focus manager belongs to + */ + public KeyboardFocusStack(final ViewPanel viewPanel) { + this.viewPanel = viewPanel; + currentUserInputHandler = defaultInputHandler; + currentUserInputHandler.focusReceived(viewPanel); + } + + /** + * Returns the handler that currently has keyboard focus. When no + * component is focused, this is the default camera navigation handler. + * + * @return the current focus owner + */ + public KeyboardInputHandler getCurrentFocusOwner() { + return currentUserInputHandler; + } + + /** + * Releases focus from the current focus owner; focus falls back to the + * default camera navigation handler. No previously focused component is + * restored — focus is single-level, not a stack. + */ + public void popFocusOwner() { + if (currentUserInputHandler == defaultInputHandler) + return; + + currentUserInputHandler.focusLost(viewPanel); + currentUserInputHandler = defaultInputHandler; + currentUserInputHandler.focusReceived(viewPanel); + } + + /** + * Gives keyboard focus to the given handler. The previous focus owner is + * notified via {@link KeyboardInputHandler#focusLost} and forgotten. + * + *

If the given handler is already the current focus owner, this method + * does nothing and returns {@code false}.

+ * + * @param newInputHandler the handler to receive keyboard focus + * @return {@code true} if the view needs to be repainted as a result + */ + public boolean pushFocusOwner(final KeyboardInputHandler newInputHandler) { + boolean updateNeeded = false; + + if (currentUserInputHandler == newInputHandler) + return false; + + if (currentUserInputHandler != null) + updateNeeded = currentUserInputHandler.focusLost(viewPanel); + + currentUserInputHandler = newInputHandler; + updateNeeded |= currentUserInputHandler.focusReceived(viewPanel); + + return updateNeeded; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardHelper.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardHelper.java new file mode 100644 index 0000000..a2d32a2 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardHelper.java @@ -0,0 +1,124 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.humaninput; + +import java.awt.event.InputEvent; +import java.util.HashSet; +import java.util.Set; + +/** + * Utility class providing keyboard key code constants and modifier detection methods. + * + *

Provides named constants for common key codes and static helper methods + * to check whether modifier keys (Ctrl, Alt, Shift) are pressed in a given + * event modifier mask.

+ * + *

Usage example:

+ *
{@code
+ * public boolean keyPressed(KeyEvent event, ViewPanel viewPanel) {
+ *     if (event.getKeyCode() == KeyboardHelper.ENTER) {
+ *         // Handle Enter key
+ *     }
+ *     if (KeyboardHelper.isCtrlPressed(event.getModifiersEx())) {
+ *         // Handle Ctrl+key combination
+ *     }
+ *     return true;
+ * }
+ * }
+ * + * @see KeyboardInputHandler the interface for receiving keyboard events + */ +public class KeyboardHelper { + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private KeyboardHelper() { + } + + /** Key code for the Tab key. */ + public static final int TAB = 9; + /** Key code for the Down arrow key. */ + public static final int DOWN = 40; + /** Key code for the Up arrow key. */ + public static final int UP = 38; + /** Key code for the Right arrow key. */ + public static final int RIGHT = 39; + /** Key code for the Left arrow key. */ + public static final int LEFT = 37; + /** Key code for the Page Down key. */ + public static final int PGDOWN = 34; + /** Key code for the Page Up key. */ + public static final int PGUP = 33; + /** Key code for the Home key. */ + public static final int HOME = 36; + /** Key code for the End key. */ + public static final int END = 35; + /** Key code for the Delete key. */ + public static final int DEL = 127; + /** Key code for the Enter/Return key. */ + public static final int ENTER = 10; + /** Key code for the Backspace key. */ + public static final int BACKSPACE = 8; + /** Key code for the Escape key. */ + public static final int ESC = 27; + /** Key code for the Shift key. */ + public static final int SHIFT = 16; + + private static final Set nonText; + + static { + nonText = new HashSet<>(); + nonText.add(DOWN); + nonText.add(UP); + nonText.add(LEFT); + nonText.add(RIGHT); + + nonText.add(SHIFT); + nonText.add(ESC); + } + + /** + * Checks if the Alt key is pressed in the given modifier mask. + * + * @param modifiersEx the extended modifier mask from {@link java.awt.event.KeyEvent#getModifiersEx()} + * @return {@code true} if Alt is pressed + */ + public static boolean isAltPressed(final int modifiersEx) { + return (modifiersEx | InputEvent.ALT_DOWN_MASK) == modifiersEx; + } + + /** + * Checks if the Ctrl key is pressed in the given modifier mask. + * + * @param modifiersEx the extended modifier mask from {@link java.awt.event.KeyEvent#getModifiersEx()} + * @return {@code true} if Ctrl is pressed + */ + public static boolean isCtrlPressed(final int modifiersEx) { + return (modifiersEx | InputEvent.CTRL_DOWN_MASK) == modifiersEx; + } + + /** + * Checks if the Shift key is pressed in the given modifier mask. + * + * @param modifiersEx the extended modifier mask from {@link java.awt.event.KeyEvent#getModifiersEx()} + * @return {@code true} if Shift is pressed + */ + public static boolean isShiftPressed(final int modifiersEx) { + return (modifiersEx | InputEvent.SHIFT_DOWN_MASK) == modifiersEx; + } + + /** + * Determines whether the given key code represents a text-producing key + * (as opposed to navigation or modifier keys like arrows, Shift, Escape). + * + * @param keyCode the key code to check + * @return {@code true} if the key produces text input + */ + public static boolean isText(final int keyCode) { + return !nonText.contains(keyCode); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardInputHandler.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardInputHandler.java new file mode 100644 index 0000000..e6ed27b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/KeyboardInputHandler.java @@ -0,0 +1,54 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.humaninput; + +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; + +import java.awt.event.KeyEvent; + +/** + * This is the process: + *

+ * 1. Component receives focus, perhaps because user clicked on it with the mouse. + * 2. Now component will receive user key press and release events from the keyboard. + * 3. Component loses focus. Perhaps user chose another component to interact with. + */ +public interface KeyboardInputHandler { + + /** + * Called when the component loses keyboard focus. + * + * @param viewPanel the view panel that owns this handler + * @return {@code true} if view needs to be re-rendered + */ + boolean focusLost(ViewPanel viewPanel); + + /** + * Called when the component receives keyboard focus. + * + * @param viewPanel the view panel that owns this handler + * @return {@code true} if view needs to be re-rendered + */ + boolean focusReceived(ViewPanel viewPanel); + + /** + * Called when a key is pressed while the component has focus. + * + * @param event the key event + * @param viewPanel the view panel that owns this handler + * @return {@code true} if view needs to be re-rendered + */ + boolean keyPressed(KeyEvent event, ViewPanel viewPanel); + + /** + * Called when a key is released while the component has focus. + * + * @param event the key event + * @param viewPanel the view panel that owns this handler + * @return {@code true} if view needs to be re-rendered + */ + boolean keyReleased(KeyEvent event, ViewPanel viewPanel); + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/MouseEvent.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/MouseEvent.java new file mode 100644 index 0000000..819d9b0 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/MouseEvent.java @@ -0,0 +1,59 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.humaninput; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; + +/** + * Represents mouse event. + */ +public class MouseEvent { + + /** Mouse over (no button pressed). */ + public static final int BUTTON_HOVER = 0; + /** Left mouse button. */ + public static final int BUTTON_LEFT = 1; + /** Middle mouse button. */ + public static final int BUTTON_MIDDLE = 2; + /** Right mouse button. */ + public static final int BUTTON_RIGHT = 3; + /** + * Mouse back button. AWT on Linux reports X buttons 8/9 as 6/7 and + * delivers them only as PRESSED/RELEASED, never CLICKED; other + * platforms may use 4/5 — handle both. + */ + public static final int BUTTON_BACK = 6; + /** Mouse forward button (AWT 7 on Linux = X button 9). */ + public static final int BUTTON_FORWARD = 7; + + /** + * Mouse coordinate in screen space (pixels) relative to top left corner of the screen + * when mouse button was clicked. + */ + public Point2D coordinate; + + /** + * One of {@link #BUTTON_HOVER}, {@link #BUTTON_LEFT}, + * {@link #BUTTON_MIDDLE}, {@link #BUTTON_RIGHT}. + */ + public int button; + + MouseEvent(final int x, final int y, final int button) { + this(new Point2D(x, y), button); + } + + MouseEvent(final Point2D coordinate, final int button) { + this.coordinate = coordinate; + this.button = button; + } + + @Override + public String toString() { + return "MouseEvent{" + + "coordinate=" + coordinate + + ", button=" + button + + '}'; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/MouseInteractionController.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/MouseInteractionController.java new file mode 100644 index 0000000..cd2b597 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/MouseInteractionController.java @@ -0,0 +1,110 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.humaninput; + +/** + * Interface that allows to handle mouse events. + */ +public interface MouseInteractionController { + + /** + * Called when mouse is clicked on component. + * + * @param button the mouse button that was clicked (1 = left, 2 = middle, 3 = right) + * @return {@code true} if view update is needed as a consequence of this mouse click + */ + boolean mouseClicked(int button); + + /** + * Called when mouse is clicked on component, with the exact texture + * coordinates of the clicked point when the hit shape is textured. + * + *

The default implementation ignores the texture coordinates and + * delegates to {@link #mouseClicked(int)}. Components that need to know + * WHERE on their surface the click landed (e.g. to forward the click + * into a captured application window) override this method.

+ * + * @param button the mouse button that was clicked (1 = left, 2 = middle, 3 = right) + * @param textureU texture-space X of the clicked point in primary-texture + * pixels, or {@link Double#NaN} when the hit shape has no texture + * @param textureV texture-space Y of the clicked point in primary-texture + * pixels, or {@link Double#NaN} when the hit shape has no texture + * @return {@code true} if view update is needed as a consequence of this mouse click + */ + default boolean mouseClicked(final int button, final double textureU, + final double textureV) { + return mouseClicked(button); + } + + /** + * Called when mouse is clicked on component, additionally carrying the + * keyboard focus stack of the dispatching view. + * + *

Components that take keyboard focus on click should override THIS + * method rather than reaching for a stored ViewPanel reference: scenes + * built headlessly (golden-image harness) construct components with a + * null panel, but the dispatching view always has a focus stack.

+ * + *

The default implementation ignores the focus stack and delegates + * to {@link #mouseClicked(int, double, double)}.

+ * + * @param button the mouse button that was clicked (1 = left, 2 = middle, 3 = right) + * @param textureU texture-space X of the clicked point, or {@link Double#NaN} + * @param textureV texture-space Y of the clicked point, or {@link Double#NaN} + * @param focusStack keyboard focus stack of the dispatching view + * @return {@code true} if view update is needed as a consequence of this mouse click + */ + default boolean mouseClicked(final int button, final double textureU, + final double textureV, + final KeyboardFocusStack focusStack) { + return mouseClicked(button, textureU, textureV); + } + + /** + * Called when the mouse wheel is turned while this component has + * keyboard focus. Both axes are reported: horizontal wheel input + * arrives from AWT as shift-modified vertical rotation and is + * separated by the input manager. + * + *

Returning {@code false} lets the wheel fall through to the + * default camera movement.

+ * + * @param verticalUnits wheel notches; positive = wheel away from + * the user (content scrolls down) + * @param horizontalUnits wheel notches; positive = scroll right + * @return {@code true} if the component consumed the scroll + */ + default boolean mouseWheelMoved(final int verticalUnits, + final int horizontalUnits) { + return false; + } + + /** + * Called when the mouse hovers over this component, with the exact + * texture coordinates of the hovered point when the hit shape is + * textured (NaN otherwise). Allows components that forward input into + * a captured application window to track the pointer position. + * + * @return {@code true} if view update is needed + */ + default boolean mouseHover(final double textureU, final double textureV) { + return false; + } + + /** + * Called when mouse gets over given component. + * + * @return true if view update is needed as a consequence of this mouse enter. + */ + boolean mouseEntered(); + + /** + * Called when mouse leaves screen area occupied by component. + * + * @return true if view update is needed as a consequence of this mouse exit. + */ + boolean mouseExited(); + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/WorldNavigationUserInputTracker.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/WorldNavigationUserInputTracker.java new file mode 100644 index 0000000..152e69c --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/WorldNavigationUserInputTracker.java @@ -0,0 +1,93 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.humaninput; + +import eu.svjatoslav.aukio.e3d.geometry.Camera; +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; +import eu.svjatoslav.aukio.e3d.gui.FrameListener; + +import java.awt.event.KeyEvent; + +/** + * Default keyboard input handler that translates arrow key presses into camera (avatar) + * movement through the 3D world. + * + *

This handler is automatically registered as the default focus owner in the + * {@link KeyboardFocusStack}. It listens for arrow key presses on each frame and + * applies acceleration to the avatar's movement vector accordingly:

+ *
    + *
  • Up arrow - move forward (positive Z)
  • + *
  • Down arrow - move backward (negative Z)
  • + *
  • Right arrow - move right (positive X)
  • + *
  • Left arrow - move left (negative X)
  • + *
+ * + *

Movement acceleration scales with the time delta between frames for smooth, + * frame-rate-independent navigation. It also scales with current speed for a natural + * acceleration curve.

+ * + * @see KeyboardFocusStack the focus system that manages this handler + * @see Camera the camera/viewer that this handler moves + */ +public class WorldNavigationUserInputTracker implements KeyboardInputHandler, FrameListener { + + /** + * Creates a new world navigation input tracker. + */ + public WorldNavigationUserInputTracker() { + } + + @Override + public boolean onFrame(final ViewPanel viewPanel, + final int millisecondsSinceLastFrame) { + + final InputManager inputManager = viewPanel.getInputManager(); + + final Camera camera = viewPanel.getCamera(); + + final double actualAcceleration = (long) millisecondsSinceLastFrame + * camera.cameraAcceleration + * (1 + (camera.getMovementSpeed() / 10)); + + if (inputManager.isKeyPressed(KeyboardHelper.UP)) + camera.getMovementVector().z += actualAcceleration; + + if (inputManager.isKeyPressed(KeyboardHelper.DOWN)) + camera.getMovementVector().z -= actualAcceleration; + + if (inputManager.isKeyPressed(KeyboardHelper.RIGHT)) + camera.getMovementVector().x += actualAcceleration; + + if (inputManager.isKeyPressed(KeyboardHelper.LEFT)) + camera.getMovementVector().x -= actualAcceleration; + + camera.enforceSpeedLimit(); + + return false; + } + + @Override + public boolean focusLost(final ViewPanel viewPanel) { + viewPanel.removeFrameListener(this); + return false; + } + + @Override + public boolean focusReceived(final ViewPanel viewPanel) { + viewPanel.addFrameListener(this); + return false; + } + + @Override + public boolean keyPressed(final KeyEvent event, final ViewPanel viewContext) { + return false; + } + + @Override + public boolean keyReleased(final KeyEvent event, final ViewPanel viewContext) { + return false; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/package-info.java new file mode 100644 index 0000000..041b06a --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/humaninput/package-info.java @@ -0,0 +1,7 @@ +/** + * Provides input device tracking (keyboard, mouse) and event forwarding to virtual components. + * + * @see eu.svjatoslav.aukio.e3d.gui.humaninput.InputManager + * @see eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardFocusStack + */ +package eu.svjatoslav.aukio.e3d.gui.humaninput; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/package-info.java new file mode 100644 index 0000000..1bb7041 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/package-info.java @@ -0,0 +1,25 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Graphical user interface components for the Aukio 3D engine. + * + *

This package provides the primary integration points for embedding 3D rendering + * into Java applications using Swing/AWT.

+ * + *

Key classes:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.gui.ViewPanel} - The main rendering surface (JPanel)
  • + *
  • {@link eu.svjatoslav.aukio.e3d.gui.ViewFrame} - A JFrame with embedded ViewPanel
  • + *
  • {@link eu.svjatoslav.aukio.e3d.geometry.Camera} - Represents the viewer's position and orientation
  • + *
  • {@link eu.svjatoslav.aukio.e3d.gui.DeveloperTools} - Debugging and profiling utilities
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.gui.ViewPanel + * @see eu.svjatoslav.aukio.e3d.geometry.Camera + */ + +package eu.svjatoslav.aukio.e3d.gui; +import eu.svjatoslav.aukio.e3d.geometry.Camera; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseController.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseController.java new file mode 100644 index 0000000..1951873 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseController.java @@ -0,0 +1,161 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.spacemouse; + +import eu.svjatoslav.aukio.e3d.geometry.Camera; +import eu.svjatoslav.aukio.e3d.gui.FrameListener; +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; +import eu.svjatoslav.aukio.e3d.math.Matrix3x3; +import eu.svjatoslav.aukio.e3d.math.Quaternion; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; + +/** + * Applies SpaceNavigator 6DOF input to the camera, every frame: + * + *
    + *
  • push/pull/slide the cap → camera moves DIRECTLY, proportional + * to deflection (the cap senses pressure, unlike a keyboard): + * velocity = deflection × top speed, applied to the camera + * position every frame. Release the cap and movement stops + * instantly — no acceleration ramp, no coasting
  • + *
  • tilt/twist the cap → camera yaw/pitch. Applied as an + * incremental delta on top of the current orientation, so it + * composes with head tracking (HeadLookController folds the + * delta into its base pose, same as mouse drags) and with mouse + * drag look
  • + *
  • left button → brake (zero the keyboard/wheel movement vector)
  • + *
+ * + *

Cap roll is measured but deliberately not applied — consistent + * with head tracking, the world horizon stays level.

+ */ +public final class SpaceMouseController implements FrameListener { + + /** + * Raw-axis → camera sign mapping. Hardware conventions are + * documented in {@link SpaceNavigatorHid}. Camera conventions: + * movementVector.x+ = strafe right, .z+ = forward, .y+ = up; + * yaw+ = turn left, pitch+ = look up. + */ + private static final double STRAFE_SIGN = 1.0; // push right → strafe right + private static final double FORWARD_SIGN = -1.0; // push away → forward (live-verified) + private static final double VERTICAL_SIGN = 1.0; // press down → move down (live-verified) + private static final double YAW_SIGN = -1.0; // twist clockwise → turn right + private static final double PITCH_SIGN = 1.0; // tilt top away → look up (live-verified) + + /** Raw counts below this are treated as "hands off". */ + private static final int DEADBAND = 1; + + /** Camera speed at full cap deflection, as a multiple of the + * keyboard top speed ({@link Camera#SPEED_LIMIT}). 3.0 = direct + * drive tops out at 3x the keyboard's capped speed (2026-09-10 — + * keyboard cap was the hidden ceiling behind "still too slow"). */ + private static final double TRANSLATION_SPEED_FACTOR = 5.0; + + /** Degrees of view yaw per frame at full cap twist + * (2x pitch 2026-09-10 — twist felt under-responsive). */ + private static final double ROTATION_YAW_DEGREES = 3.0; + + /** Degrees of view pitch per frame at full cap tilt. */ + private static final double ROTATION_PITCH_DEGREES = 1.5; + + private final SpaceNavigatorHid device; + private final ViewPanel viewPanel; + + private double sensitivity = 5.0; + private boolean leftButtonWasPressed; + + public SpaceMouseController(final SpaceNavigatorHid device, + final ViewPanel viewPanel) { + this.device = device; + this.viewPanel = viewPanel; + } + + public double getSensitivity() { + return sensitivity; + } + + public void setSensitivity(final double sensitivity) { + this.sensitivity = sensitivity; + } + + @Override + public boolean onFrame(final ViewPanel viewPanel, + final int millisecondsSinceLastFrame) { + if (!device.isRunning()) + return false; + + final Camera camera = viewPanel.getCamera(); + boolean changed = false; + + // Frame-rate independent scaling, 60 fps baseline. + final double dt = millisecondsSinceLastFrame / 16.6667; + + final double strafe = shaped(device.getTx()) * STRAFE_SIGN; + final double forward = shaped(device.getTy()) * FORWARD_SIGN; + final double vertical = shaped(device.getTz()) * VERTICAL_SIGN; + if (strafe != 0 || forward != 0 || vertical != 0) { + // Direct proportional drive: deflection maps to velocity + // (full deflection = keyboard top speed), applied straight + // to the camera position. No movement vector, no friction — + // releasing the cap stops the camera this very frame. + final Matrix3x3 m = camera.getTransform().getRotation() + .toMatrix(); + final Point3D location = camera.getTransform() + .getTranslation(); + final double step = TRANSLATION_SPEED_FACTOR + * Camera.SPEED_LIMIT * Camera.SPEED_MULTIPLIER + * millisecondsSinceLastFrame * sensitivity; + location.x += (m.m20 * forward + m.m00 * strafe) * step; + location.y += (m.m21 * forward + m.m01 * strafe) * step; + location.z += (m.m22 * forward + m.m02 * strafe) * step; + location.y += vertical * step; + changed = true; + } + + final double yawDelta = shaped(device.getRz()) * YAW_SIGN; + final double pitchDelta = shaped(device.getRx()) * PITCH_SIGN; + if (yawDelta != 0 || pitchDelta != 0) { + final double[] angles = camera.getTransform().getRotation() + .toAngles(); + double yaw = angles[0] + yawDelta + * Math.toRadians(ROTATION_YAW_DEGREES) + * sensitivity * dt; + double pitch = angles[1] + pitchDelta + * Math.toRadians(ROTATION_PITCH_DEGREES) + * sensitivity * dt; + pitch = Math.max(-Math.PI / 2 + 0.001, + Math.min(Math.PI / 2 - 0.001, pitch)); + camera.getTransform().getRotation().set( + Quaternion.fromAngles(yaw, pitch, 0)); + changed = true; + } + + final boolean leftPressed = (device.getButtons() & 1) != 0; + if (leftPressed && !leftButtonWasPressed) { + camera.getMovementVector().x = 0; + camera.getMovementVector().y = 0; + camera.getMovementVector().z = 0; + changed = true; + } + leftButtonWasPressed = leftPressed; + + return changed; + } + + /** + * Deadband + quadratic response: raw axis (−350..350) → −1..1. + * Quadratic keeps fine control near the center while preserving + * full speed at the rim. + */ + private static double shaped(final int raw) { + final int magnitude = Math.abs(raw); + if (magnitude < DEADBAND) + return 0; + final double v = (magnitude - DEADBAND) + / (SpaceNavigatorHid.FULL_DEFLECTION - DEADBAND); + return Math.copySign(v * v, raw); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseManager.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseManager.java new file mode 100644 index 0000000..d2a5cd9 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceMouseManager.java @@ -0,0 +1,119 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.spacemouse; + +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; + +/** + * Hot-plug manager for the SpaceNavigator 6DOF mouse. Polls for the + * device every two seconds: + * + *
    + *
  • plugged in → open the HID pipe, attach a + * {@link SpaceMouseController} to the view's frame listeners
  • + *
  • unplugged (read error) → stop and detach
  • + *
  • plugged back in → fresh device instance
  • + *
+ * + *

Permission failures are reported once, then retried silently — + * the user may install the udev rule while the application runs.

+ */ +public final class SpaceMouseManager { + + private static final long POLL_INTERVAL_MS = 2000; + + private final ViewPanel viewPanel; + private final Thread thread; + + private volatile boolean running = true; + private SpaceNavigatorHid device; + private SpaceMouseController controller; + private boolean failureReported; + + public SpaceMouseManager(final ViewPanel viewPanel) { + this.viewPanel = viewPanel; + thread = new Thread(this::pollLoop, "spacemouse-hotplug"); + thread.setDaemon(true); + } + + public void start() { + thread.start(); + } + + /** Active device, or null when no SpaceNavigator is connected. */ + public SpaceNavigatorHid getDevice() { + return device; + } + + /** Active controller, or null when no SpaceNavigator is connected. */ + public SpaceMouseController getController() { + return controller; + } + + public void stop() { + running = false; + // Wake the 2 s poll sleep; without the interrupt the join below + // burns its full 1 s timeout on the EDT at every window close. + thread.interrupt(); + try { + thread.join(1000); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + disconnect(); + } + + private void pollLoop() { + while (running) { + poll(); + try { + Thread.sleep(POLL_INTERVAL_MS); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + + private void poll() { + if (device != null && !device.isRunning()) { + System.out.println("spacemouse: disconnected"); + disconnect(); + } + if (device != null) + return; + + try { + final SpaceNavigatorHid opened = SpaceNavigatorHid.open(); + if (opened == null) + return; + opened.start(); + device = opened; + controller = new SpaceMouseController(opened, viewPanel); + viewPanel.addFrameListener(controller); + failureReported = false; + System.out.println("spacemouse: SpaceNavigator detected — " + + "cap moves the camera, left button brakes"); + } catch (final Exception e) { + device = null; + if (!failureReported) { + failureReported = true; + System.err.println("spacemouse unavailable: " + + e.getMessage()); + } + } + } + + private void disconnect() { + if (controller != null) { + viewPanel.removeFrameListener(controller); + controller = null; + } + if (device != null) { + device.stop(); + device = null; + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceNavigatorHid.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceNavigatorHid.java new file mode 100644 index 0000000..7709c5c --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/spacemouse/SpaceNavigatorHid.java @@ -0,0 +1,214 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.spacemouse; + +import com.sun.jna.Library; +import com.sun.jna.Memory; +import com.sun.jna.Native; + +import java.io.File; + +/** + * HID transport for the 3Dconnexion SpaceNavigator 6DOF mouse + * (USB 046D:C626), read via the Linux hidraw interface using JNA — + * same approach as the RayNeo glasses transport. + * + *

Protocol (from libspnav/spacenavd documentation): the device + * sends 7-byte input reports, ONLY while the cap is deflected or a + * button changes — at rest the pipe is silent (so unlike the XR + * glasses there is no stream watchdog; silence is normal):

+ * + *
    + *
  • report[0] = 1 — translation; int16 LE at [1,2]=X, [3,4]=Y, + * [5,6]=Z; full deflection ≈ ±350
  • + *
  • report[0] = 2 — rotation; int16 LE at [1,2]=RX, [3,4]=RY, + * [5,6]=RZ; full deflection ≈ ±350
  • + *
  • report[0] = 3 — buttons; report[1] bit0 = left, bit1 = right
  • + *
+ * + *

Raw axis sign conventions (SpaceNavigator hardware):

+ *
    + *
  • TX: push cap right → positive
  • + *
  • TY: pull cap toward you → positive (live-verified)
  • + *
  • TZ: pull cap up → positive (live-verified)
  • + *
  • RX: tilt cap top away → positive
  • + *
  • RY: tilt cap top right → positive
  • + *
  • RZ: twist cap clockwise (seen from above) → positive
  • + *
+ * + *

Detection scans /sys/class/hidraw for HID_ID + * 0003:0000046D:0000C626 — the hidraw node changes on replug, so never + * cache the path. Unplug is detected by a read error (read returns + * <0).

+ */ +public final class SpaceNavigatorHid { + + private static final String HID_ID = "0003:0000046D:0000C626"; + + /** Full deflection of any axis, per the HID descriptor. */ + public static final double FULL_DEFLECTION = 350.0; + + /** + * Same JNA calling convention as the proven RayNeo glasses + * transport: static singleton mapping, Memory buffers (a raw + * byte[] mapping correlated with native heap corruption — + * "free(): invalid pointer" — under active report streaming). + */ + private interface CLib extends Library { + CLib INSTANCE = Native.load("c", CLib.class); + + int open(String path, int flags); + + int close(int fd); + + int read(int fd, Memory buffer, int count); + + /** poll(2) on a single-element struct pollfd (8 bytes: fd, events, revents). */ + int poll(Memory fds, int nfds, int timeout); + } + + private final File deviceNode; + private final Memory readBuffer = new Memory(64); + + private int fd = -1; + private Thread reader; + private volatile boolean running; + + /** Latest axis state, in raw device units (±350). */ + private volatile int tx, ty, tz, rx, ry, rz; + private volatile int buttons; + /** Set when a read error (typically unplug) killed the reader. */ + private volatile boolean broken; + + private SpaceNavigatorHid(final File deviceNode) { + this.deviceNode = deviceNode; + } + + /** + * Finds and opens the first SpaceNavigator on the system, or null + * when none is plugged in. Throws when the device is present but + * cannot be opened (permissions — see + * /etc/udev/rules.d/99-spacenavigator.rules). + */ + public static SpaceNavigatorHid open() { + final File hidrawDir = new File("/sys/class/hidraw"); + final File[] entries = hidrawDir.listFiles(); + if (entries == null) + return null; + for (final File entry : entries) { + final File uevent = new File(entry, "device/uevent"); + if (!uevent.isFile()) + continue; + try { + final String content = new String( + java.nio.file.Files.readAllBytes(uevent.toPath())); + if (!content.contains(HID_ID)) + continue; + final File node = new File("/dev", entry.getName()); + final SpaceNavigatorHid hid = new SpaceNavigatorHid(node); + hid.openNode(); + return hid; + } catch (final Exception e) { + throw new RuntimeException("SpaceNavigator found at " + + entry.getName() + " but cannot be opened: " + + e.getMessage()); + } + } + return null; + } + + private void openNode() { + fd = CLib.INSTANCE.open(deviceNode.getAbsolutePath(), + 2 /* O_RDWR */); + if (fd < 0) + throw new RuntimeException("open(" + deviceNode + + ") failed: " + Native.getLastError()); + } + + public void start() { + running = true; + reader = new Thread(this::readLoop, "spacenavigator-hid"); + reader.setDaemon(true); + reader.start(); + } + + private void readLoop() { + // Poll before reading: the device is SILENT at rest, so a plain + // blocking read never returns when idle — and close() from stop() + // does not wake a blocked read on Linux, which made every window + // close wait out the full 1 s join timeout. + final Memory pollfd = new Memory(8); + while (running) { + pollfd.clear(); + pollfd.setInt(0, fd); + pollfd.setShort(4, (short) 0x1); // events = POLLIN + final int ready = CLib.INSTANCE.poll(pollfd, 1, 250); + if (ready == 0) + continue; // idle device: re-check running + if (ready < 0) { + if (Native.getLastError() == 4) // EINTR + continue; + broken = true; + return; + } + final short revents = pollfd.getShort(6); + if ((revents & 0x1) == 0) { // no POLLIN: ERR/HUP/NVAL = unplugged + broken = true; + return; + } + final int n = CLib.INSTANCE.read(fd, readBuffer, 64); + if (n < 0) { + broken = true; // unplugged + return; + } + if (n < 7) + continue; + final int reportId = readBuffer.getByte(0) & 0xFF; + if (reportId == 1) { + tx = readBuffer.getShort(1); + ty = readBuffer.getShort(3); + tz = readBuffer.getShort(5); + } else if (reportId == 2) { + rx = readBuffer.getShort(1); + ry = readBuffer.getShort(3); + rz = readBuffer.getShort(5); + } else if (reportId == 3) { + buttons = readBuffer.getByte(1) & 0xFF; + } + } + } + + /** True while the device is connected and the reader is alive. */ + public boolean isRunning() { + return running && !broken; + } + + public int getTx() { return tx; } + public int getTy() { return ty; } + public int getTz() { return tz; } + public int getRx() { return rx; } + public int getRy() { return ry; } + public int getRz() { return rz; } + public int getButtons() { return buttons; } + + public void stop() { + running = false; + // Join first: the poll loop notices the flag within 250 ms. Closing + // the fd before the reader is dead risks the fd number being reused + // under a concurrent open while the reader is mid-read. + if (reader != null) { + try { + reader.join(1000); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + reader = null; + } + if (fd >= 0) { + CLib.INSTANCE.close(fd); + fd = -1; + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/Character.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/Character.java new file mode 100644 index 0000000..5d86f67 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/Character.java @@ -0,0 +1,29 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.textEditorComponent; + +/** + * A character in a text editor. + */ +public class Character { + + /** + * The character value. + */ + char value; + + /** + * Creates a character with the given value. + * + * @param value the character value + */ + public Character(final char value) { + this.value = value; + } + + boolean hasValue() { + return value != ' '; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/LookAndFeel.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/LookAndFeel.java new file mode 100644 index 0000000..aebf041 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/LookAndFeel.java @@ -0,0 +1,41 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.textEditorComponent; + +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; + +/** + * A look and feel of a text editor. + */ +public class LookAndFeel { + + /** Default foreground (text) color. */ + public Color foreground = new Color(255, 255, 255); + + /** Default background color. */ + public Color background = new Color(20, 20, 20, 255); + + /** Background color for tab stop positions. */ + public Color tabStopBackground = new Color(25, 25, 25, 255); + + /** Cursor foreground color. */ + public Color cursorForeground = new Color(255, 255, 255); + + /** Cursor background color. */ + public Color cursorBackground = new Color(255, 0, 0); + + /** Selection foreground color. */ + public Color selectionForeground = new Color(255, 255, 255); + + /** Selection background color. */ + public Color selectionBackground = new Color(0, 80, 80); + + /** + * Creates a look and feel with default colors. + */ + public LookAndFeel() { + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/Page.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/Page.java new file mode 100644 index 0000000..06e94b8 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/Page.java @@ -0,0 +1,162 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.textEditorComponent; + +import java.util.ArrayList; +import java.util.List; + +/** + * A page in a text editor. + */ +public class Page { + + /** + * The text lines. + */ + public List rows = new ArrayList<>(); + + /** + * Creates a new empty page. + */ + public Page() { + } + + /** + * Ensures that the page has at least the specified number of lines. + * + * @param row the minimum number of lines required + */ + public void ensureMaxTextLine(final int row) { + while (rows.size() <= row) + rows.add(new TextLine()); + } + + /** + * Returns the character at the specified location. + * If the location is out of bounds, returns a space. + * + * @param row the row index + * @param column the column index + * @return the character at the specified location + */ + public char getChar(final int row, final int column) { + if (rows.size() <= row) + return ' '; + return rows.get(row).getCharForLocation(column); + } + + /** + * Returns the specified line. + * + * @param row The line number. + * @return The line. + */ + public TextLine getLine(final int row) { + ensureMaxTextLine(row); + return rows.get(row); + } + + /** + * Returns the length of the specified line. + * + * @param row The line number. + * @return The length of the line. + */ + public int getLineLength(final int row) { + if (rows.size() <= row) + return 0; + return rows.get(row).getLength(); + } + + /** + * Returns the number of lines in the page. + * + * @return The number of lines in the page. + */ + public int getLinesCount() { + pack(); + return rows.size(); + } + + /** + * Returns the text of the page. + * + * @return The text of the page. + */ + public String getText() { + pack(); + + final StringBuilder result = new StringBuilder(); + for (final TextLine textLine : rows) { + if (result.length() > 0) + result.append("\n"); + result.append(textLine.toString()); + } + return result.toString(); + } + + /** + * Inserts a character at the specified position. + * + * @param row the row index + * @param col the column index + * @param value the character to insert + */ + public void insertCharacter(final int row, final int col, final char value) { + getLine(row).insertCharacter(col, value); + } + + /** + * Inserts a line at the specified row. + * + * @param row the row index where to insert + * @param textLine the text line to insert + */ + public void insertLine(final int row, final TextLine textLine) { + rows.add(row, textLine); + } + + /** + * Removes empty lines from the end of the page. + */ + private void pack() { + int newLength = 0; + + for (int i = rows.size() - 1; i >= 0; i--) + if (!rows.get(i).isEmpty()) { + newLength = i + 1; + break; + } + + if (newLength == rows.size()) + return; + + rows = rows.subList(0, newLength); + } + + /** + * Removes the specified character from the page. + * + * @param row The line number. + * @param col The character number. + */ + public void removeCharacter(final int row, final int col) { + if (rows.size() <= row) + return; + getLine(row).removeCharacter(col); + } + + /** + * Removes the specified line from the page. + * + * @param row The line number. + */ + public void removeLine(final int row) { + if (rows.size() <= row) + return; + rows.remove(row); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/TextEditComponent.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/TextEditComponent.java new file mode 100755 index 0000000..8e62ff5 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/TextEditComponent.java @@ -0,0 +1,915 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.textEditorComponent; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.gui.GuiComponent; +import eu.svjatoslav.aukio.e3d.gui.TextPointer; +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; +import eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardHelper; +import eu.svjatoslav.aukio.e3d.math.Transform; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas; + +import java.awt.*; +import java.awt.datatransfer.*; +import java.awt.event.KeyEvent; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +/** + * A full-featured text editor component rendered in 3D space. + * + *

Extends {@link GuiComponent} to integrate keyboard focus management and mouse + * interaction with a multi-line text editing surface. The editor is backed by a + * {@link Page} model containing {@link TextLine} instances and rendered via a + * {@link TextCanvas}.

+ * + *

Supported editing features:

+ *
    + *
  • Cursor navigation with arrow keys, Home, End, Page Up, and Page Down
  • + *
  • Text selection via Shift + arrow keys
  • + *
  • Clipboard operations: Ctrl+C (copy), Ctrl+X (cut), Ctrl+V (paste), Ctrl+A (select all)
  • + *
  • Word-level cursor movement with Ctrl+Left and Ctrl+Right
  • + *
  • Tab indentation and Shift+Tab dedentation for single lines and block selections
  • + *
  • Backspace dedentation of selected blocks (removes 4 spaces of indentation)
  • + *
  • Automatic scrolling when the cursor moves beyond the visible area
  • + *
+ * + *

Usage example:

+ *
{@code
+ * // Create a look and feel (or use defaults)
+ * LookAndFeel lookAndFeel = new LookAndFeel();
+ *
+ * // Create the text editor at a position in 3D space
+ * TextEditComponent editor = new TextEditComponent(
+ *     new Transform(new Point3D(0, 0, 500)),  // position in world
+ *     viewPanel,                                // the active ViewPanel
+ *     new Point2D(800, 600),                    // size in world coordinates
+ *     lookAndFeel
+ * );
+ *
+ * // Set initial content
+ * editor.setText("Hello, World!\nSecond line of text.");
+ *
+ * // Add to the scene
+ * viewPanel.getRootShapeCollection().addShape(editor);
+ * }
+ * + * @see GuiComponent the base class providing keyboard focus and mouse click handling + * @see Page the underlying text model holding all lines + * @see TextCanvas the rendering surface for character-based output + * @see LookAndFeel configurable colors for the editor's visual appearance + * @see TextPointer row/column pointer used for cursor and selection positions + */ +public class TextEditComponent extends GuiComponent implements ClipboardOwner { + + private static final long serialVersionUID = -7118833957783600630L; + + /** + * Text rows that need to be repainted. + */ + private final Set dirtyRows = new HashSet<>(); + + + /** + * The text canvas used to render characters on screen. + */ + private final TextCanvas textCanvas; + + /** + * The number of characters the view is scrolled horizontally. + */ + public int scrolledCharacters = 0; + + /** + * The number of lines the view is scrolled vertically. + */ + public int scrolledLines = 0; + + /** + * Whether the user is currently in selection mode (Shift key held during navigation). + */ + public boolean selecting = false; + + /** + * Selection start and end pointers. + */ + public TextPointer selectionStart = new TextPointer(0, 0); + + /** + * The end position of the text selection. + */ + public TextPointer selectionEnd = new TextPointer(0, 0); + + /** + * The current cursor position in the text (row and column). + */ + public TextPointer cursorLocation = new TextPointer(0, 0); + + /** + * The page model holding all text lines. + */ + Page page = new Page(); + + /** + * The look and feel configuration controlling editor colors. + */ + LookAndFeel lookAndFeel; + + /** + * If true, the page will be repainted on the next update. + */ + boolean repaintPage = false; + + /** + * Creates a new text editor component positioned in 3D space. + * + *

The editor dimensions in rows and columns are computed from the given world-coordinate + * size and the font character dimensions defined in {@link TextCanvas}. A {@link TextCanvas} + * is created internally and added as a child shape.

+ * + * @param transform the position and orientation of the editor in 3D space + * @param viewPanel the view panel this editor belongs to + * @param sizeInWorldCoordinates the editor size in world coordinates (width, height); + * determines the number of visible columns and rows + * @param lookAndFeel the color configuration for the editor's visual appearance + */ + public TextEditComponent(final Transform transform, + final ViewPanel viewPanel, + final Point2D sizeInWorldCoordinates, + LookAndFeel lookAndFeel) { + super(transform, viewPanel, sizeInWorldCoordinates.to3D()); + + this.lookAndFeel = lookAndFeel; + final int columns = (int) (sizeInWorldCoordinates.x / TextCanvas.FONT_CHAR_WIDTH); + final int rows = (int) (sizeInWorldCoordinates.y / TextCanvas.FONT_CHAR_HEIGHT); + + textCanvas = new TextCanvas( + new Transform(), + new TextPointer(rows, columns), + lookAndFeel.foreground, lookAndFeel.background); + + textCanvas.setMouseInteractionController(this); + + repaintPage(); + addShape(textCanvas); + } + + /** + * Ensures the cursor stays within the visible editor area by adjusting + * scroll offsets when the cursor moves beyond the visible boundaries. + * Also clamps the cursor position so that row and column are never negative. + */ + private void checkCursorBoundaries() { + if (cursorLocation.column < 0) + cursorLocation.column = 0; + if (cursorLocation.row < 0) + cursorLocation.row = 0; + + // ensure chat cursor stays within vertical editor boundaries by + // vertical scrolling + if ((cursorLocation.row - scrolledLines) < 0) + scroll(0, cursorLocation.row - scrolledLines); + + if ((((cursorLocation.row - scrolledLines) + 1)) > textCanvas.getSize().row) + scroll(0, + ((((((cursorLocation.row - scrolledLines) + 1) - textCanvas + .getSize().row))))); + + // ensure chat cursor stays within horizontal editor boundaries by + // horizontal scrolling + if ((cursorLocation.column - scrolledCharacters) < 0) + scroll(cursorLocation.column - scrolledCharacters, 0); + + if ((((cursorLocation.column - scrolledCharacters) + 1)) > textCanvas + .getSize().column) + scroll((((((cursorLocation.column - scrolledCharacters) + 1) - textCanvas + .getSize().column))), 0); + } + + /** + * Clears the current text selection by setting the selection end to match + * the selection start, effectively making the selection empty. + * + *

A full page repaint is scheduled to remove the visual selection highlight.

+ */ + public void clearSelection() { + selectionEnd = new TextPointer(selectionStart); + repaintPage = true; + } + + /** + * Copies the currently selected text to the system clipboard. + * + *

If no text is selected (i.e., selection start equals selection end), + * this method does nothing. Multi-line selections are joined with newline + * characters.

+ * + * @see #setClipboardContents(String) + * @see #cutToClipboard() + */ + public void copyToClipboard() { + if (selectionStart.compareTo(selectionEnd) == 0) + return; + // System.out.println("Copy action."); + final StringBuilder msg = new StringBuilder(); + + ensureSelectionOrder(); + + for (int row = selectionStart.row; row <= selectionEnd.row; row++) { + final TextLine textLine = page.getLine(row); + + if (row == selectionStart.row) { + if (row == selectionEnd.row) + msg.append(textLine.getSubString(selectionStart.column, + selectionEnd.column + 1)); + else + msg.append(textLine.getSubString(selectionStart.column, + textLine.getLength())); + } else { + msg.append('\n'); + if (row == selectionEnd.row) + msg.append(textLine + .getSubString(0, selectionEnd.column + 1)); + else + msg.append(textLine.toString()); + } + } + + setClipboardContents(msg.toString()); + } + + /** + * Cuts the currently selected text to the system clipboard. + * + *

This copies the selected text to the clipboard via {@link #copyToClipboard()}, + * then deletes the selection from the page and triggers a full repaint.

+ * + * @see #copyToClipboard() + * @see #deleteSelection() + */ + public void cutToClipboard() { + copyToClipboard(); + deleteSelection(); + repaintPage(); + } + + /** + * Deletes the currently selected text from the page. + * + *

After deletion, the selection is cleared and the cursor is moved to + * the position where the selection started.

+ * + * @see #ensureSelectionOrder() + */ + public void deleteSelection() { + ensureSelectionOrder(); + int ym = 0; + + for (int line = selectionStart.row; line <= selectionEnd.row; line++) { + final TextLine currentLine = page.getLine(line - ym); + + if (line == selectionStart.row) { + if (line == selectionEnd.row) + + currentLine.cutSubString(selectionStart.column, + selectionEnd.column); + else if (selectionStart.column == 0) { + page.removeLine(line - ym); + ym++; + } else + currentLine.cutSubString(selectionStart.column, + currentLine.getLength() + 1); + } else if (line == selectionEnd.row) + currentLine.cutSubString(0, selectionEnd.column); + else { + page.removeLine(line - ym); + ym++; + } + } + + clearSelection(); + cursorLocation = new TextPointer(selectionStart); + } + + /** + * Ensures that {@link #selectionStart} is smaller than + * {@link #selectionEnd}. + * + *

If the start pointer is after the end pointer (e.g., when the user + * selected text backwards), the two pointers are swapped so that + * subsequent operations can iterate from start to end.

+ */ + public void ensureSelectionOrder() { + if (selectionStart.compareTo(selectionEnd) > 0) { + final TextPointer temp = selectionEnd; + selectionEnd = selectionStart; + selectionStart = temp; + } + } + + /** + * Retrieves the current text contents of the system clipboard. + * + * @return the clipboard text content, or an empty string if the clipboard + * is empty or does not contain text + */ + public String getClipboardContents() { + String result = ""; + final Clipboard clipboard = Toolkit.getDefaultToolkit() + .getSystemClipboard(); + // odd: the Object param of getContents is not currently used + final Transferable contents = clipboard.getContents(null); + final boolean hasTransferableText = (contents != null) + && contents.isDataFlavorSupported(DataFlavor.stringFlavor); + if (hasTransferableText) + try { + result = (String) contents + .getTransferData(DataFlavor.stringFlavor); + } catch (final UnsupportedFlavorException | IOException ex) { + // highly unlikely since we are using a standard DataFlavor + System.out.println(ex); + } + // System.out.println(result); + return result; + } + + /** + * Places the given string into the system clipboard so that it can be + * pasted into other applications. + * + * @param contents the text to place on the clipboard + * @see #getClipboardContents() + * @see #copyToClipboard() + */ + public void setClipboardContents(final String contents) { + final StringSelection stringSelection = new StringSelection(contents); + final Clipboard clipboard = Toolkit.getDefaultToolkit() + .getSystemClipboard(); + clipboard.setContents(stringSelection, stringSelection); + } + + /** + * Scrolls to and positions the cursor at the beginning of the specified line. + * + *

The view is scrolled so the target line is visible, the cursor is placed + * at the start of that line (column 0), and a full repaint is triggered.

+ * + * @param Line the zero-based line number to navigate to + */ + public void goToLine(final int Line) { + // markNavigationLocation(Line); + scrolledLines = Line + 1; + cursorLocation.row = Line + 1; + cursorLocation.column = 0; + repaintPage(); + } + + /** + * Inserts the given text string at the current cursor position. + * + *

The text is processed character by character. Special characters are + * handled as editing operations:

+ *
    + *
  • {@code DEL} -- deletes the character at the cursor
  • + *
  • {@code ENTER} -- splits the current line at the cursor
  • + *
  • {@code BACKSPACE} -- deletes the character before the cursor
  • + *
+ *

All other printable characters are inserted at the cursor position, + * advancing the cursor column by one for each character.

+ * + * @param txt the text to insert; {@code null} values are silently ignored + */ + public void insertText(final String txt) { + if (txt == null) + return; + + for (final char c : txt.toCharArray()) { + + if (c == KeyboardHelper.DEL) { + processDel(); + continue; + } + + if (c == KeyboardHelper.ENTER) { + processEnter(); + continue; + } + + if (c == KeyboardHelper.BACKSPACE) { + processBackspace(); + continue; + } + + // type character + if (KeyboardHelper.isText(c)) { + page.insertCharacter(cursorLocation.row, cursorLocation.column, + c); + cursorLocation.column++; + } + } + } + + /** + * Handles a key press event by routing it through the editor's input processing + * pipeline. + * + *

This method delegates to the parent {@link GuiComponent#keyPressed(KeyEvent, ViewPanel)} + * (which handles ESC for focus release), then processes the key event for text editing, + * marks the affected row as dirty, adjusts scroll boundaries, and repaints as needed.

+ * + * @param event the keyboard event + * @param viewPanel the view panel that dispatched this event + * @return always {@code true}, indicating the event was consumed + */ + @Override + public boolean keyPressed(final KeyEvent event, final ViewPanel viewPanel) { + super.keyPressed(event, viewPanel); + + processKeyEvent(event); + + markRowDirty(); + + checkCursorBoundaries(); + + repaintWhatNeeded(); + return true; + } + + /** + * Called when this editor loses ownership of the system clipboard. + * + *

This is an empty implementation of the {@link ClipboardOwner} interface; + * no action is taken when clipboard ownership is lost.

+ * + * @param aClipboard the clipboard that this editor previously owned + * @param aContents the contents that were previously placed on the clipboard + */ + @Override + public void lostOwnership(final Clipboard aClipboard, + final Transferable aContents) { + // do nothing + } + + /** + * Marks the current cursor row as dirty, scheduling it for repaint on the + * next rendering cycle. + */ + public void markRowDirty() { + dirtyRows.add(cursorLocation.row); + } + + /** + * Pastes text from the system clipboard at the current cursor position. + * + * @see #getClipboardContents() + * @see #insertText(String) + */ + public void pasteFromClipboard() { + insertText(getClipboardContents()); + } + + /** + * Processes the backspace key action. + * + *

If there is no active selection, deletes the character before the cursor. + * If the cursor is at the beginning of a line, merges the current line with the + * previous one. If there is an active selection, dedents the selected lines by + * removing up to 4 leading spaces (block dedentation).

+ */ + private void processBackspace() { + if (selectionStart.compareTo(selectionEnd) == 0) { + // erase single character + if (cursorLocation.column > 0) { + cursorLocation.column--; + page.removeCharacter(cursorLocation.row, cursorLocation.column); + // System.out.println(lines.get(currentCursor.line).toString()); + } else if (cursorLocation.row > 0) { + cursorLocation.row--; + final int currentLineLength = page + .getLineLength(cursorLocation.row); + cursorLocation.column = currentLineLength; + page.getLine(cursorLocation.row) + .insertTextLine(currentLineLength, + page.getLine(cursorLocation.row + 1)); + page.removeLine(cursorLocation.row + 1); + repaintPage = true; + } + } else { + // dedent multiple lines + ensureSelectionOrder(); + // scan if enough space exists + for (int y = selectionStart.row; y < selectionEnd.row; y++) + if (page.getLine(y).getIndent() < 4) + return; + + for (int y = selectionStart.row; y < selectionEnd.row; y++) + page.getLine(y).cutFromBeginning(4); + + repaintPage = true; + } + } + + /** + * Processes keyboard shortcuts involving the Ctrl modifier key. + * + *

Supported combinations:

+ *
    + *
  • Ctrl+A -- select all text
  • + *
  • Ctrl+X -- cut selected text to clipboard
  • + *
  • Ctrl+C -- copy selected text to clipboard
  • + *
  • Ctrl+V -- paste from clipboard
  • + *
  • Ctrl+Right -- skip to the beginning of the next word
  • + *
  • Ctrl+Left -- skip to the beginning of the previous word
  • + *
+ * + * @param keyCode the key code of the pressed key (combined with Ctrl) + */ + private void processCtrlCombinations(final int keyCode) { + + if ((char) keyCode == 'A') { // CTRL + A -- select all + final int lastLineIndex = page.getLinesCount() - 1; + selectionStart = new TextPointer(0, 0); + selectionEnd = new TextPointer(lastLineIndex, + page.getLineLength(lastLineIndex)); + repaintPage(); + } + + // CTRL + X -- cut + if ((char) keyCode == 'X') + cutToClipboard(); + + // CTRL + C -- copy + if ((char) keyCode == 'C') + copyToClipboard(); + + // CTRL + V -- paste + if ((char) keyCode == 'V') + pasteFromClipboard(); + + if (keyCode == 39) { // RIGHT + // skip to the beginning of the next word + + for (int x = cursorLocation.column; x < (page + .getLineLength(cursorLocation.row) - 1); x++) + if ((page.getChar(cursorLocation.row, x) == ' ') + && (page.getChar(cursorLocation.row, x + 1) != ' ')) { + // beginning of the next word is found + cursorLocation.column = x + 1; + return; + } + + cursorLocation.column = page.getLineLength(cursorLocation.row); + return; + } + + if (keyCode == 37) { // Left + + // skip to the beginning of the previous word + for (int x = cursorLocation.column - 2; x >= 0; x--) + if ((page.getChar(cursorLocation.row, x) == ' ') + & (page.getChar(cursorLocation.row, x + 1) != ' ')) { + cursorLocation.column = x + 1; + return; + } + + cursorLocation.column = 0; + } + } + + /** + * Processes the Delete key action. + * + *

If there is no active selection, deletes the character at the cursor position. + * If the cursor is at the end of the line, the next line is merged into the current one. + * If there is an active selection, the entire selection is deleted.

+ */ + public void processDel() { + if (selectionStart.compareTo(selectionEnd) == 0) { + // is there still some text right to the cursor ? + if (cursorLocation.column < page.getLineLength(cursorLocation.row)) + page.removeCharacter(cursorLocation.row, cursorLocation.column); + else { + page.getLine(cursorLocation.row).insertTextLine( + cursorLocation.column, + page.getLine(cursorLocation.row + 1)); + page.removeLine(cursorLocation.row + 1); + repaintPage = true; + } + } else { + deleteSelection(); + repaintPage = true; + } + } + + /** + * Processes the Enter key action by splitting the current line at the cursor position. + * + *

Everything to the right of the cursor is moved to a new line inserted + * below. The cursor moves to the beginning of the new line.

+ */ + private void processEnter() { + final TextLine currentLine = page.getLine(cursorLocation.row); + // move everything right to the cursor into new line + final TextLine newLine = currentLine.getSubLine(cursorLocation.column, + currentLine.getLength()); + page.insertLine(cursorLocation.row + 1, newLine); + + // trim existing line + page.getLine(cursorLocation.row).cutUntilEnd(cursorLocation.column); + repaintPage = true; + + cursorLocation.row++; + cursorLocation.column = 0; + } + + /** + * Routes a keyboard event to the appropriate handler based on modifier keys + * and key codes. + * + *

Handles Ctrl combinations, Tab/Shift+Tab, text input, Shift-based selection, + * and cursor navigation keys (Home, End, arrows, Page Up/Down). Alt key events + * are ignored.

+ * + * @param event the keyboard event to process + */ + private void processKeyEvent(final KeyEvent event) { + final int modifiers = event.getModifiersEx(); + final int keyCode = event.getKeyCode(); + final char keyChar = event.getKeyChar(); + + // System.out.println("Keycode:" + keyCode s+ ", keychar:" + keyChar); + + if (KeyboardHelper.isAltPressed(modifiers)) + return; + + if (KeyboardHelper.isCtrlPressed(modifiers)) { + processCtrlCombinations(keyCode); + return; + } + + if (keyCode == KeyboardHelper.TAB) { + processTab(modifiers); + return; + } + + clearSelection(); + + if (KeyboardHelper.isText(keyCode)) { + insertText(String.valueOf(keyChar)); + return; + } + + if (KeyboardHelper.isShiftPressed(modifiers)) { + if (!selecting) + attemptSelectionStart:{ + + if (keyChar == 65535) + if (keyCode == 16) + break attemptSelectionStart; + if (((keyChar >= 32) & (keyChar <= 128)) | (keyChar == 10) + | (keyChar == 8) | (keyChar == 9)) + break attemptSelectionStart; + + selectionStart = new TextPointer(cursorLocation); + selectionEnd = selectionStart; + selecting = true; + repaintPage(); + } + } else + selecting = false; + + if (keyCode == KeyboardHelper.HOME) { + cursorLocation.column = 0; + return; + } + if (keyCode == KeyboardHelper.END) { + cursorLocation.column = page.getLineLength(cursorLocation.row); + return; + } + + // process cursor keys + if (keyCode == KeyboardHelper.DOWN) { + markRowDirty(); + cursorLocation.row++; + return; + } + + if (keyCode == KeyboardHelper.UP) { + markRowDirty(); + cursorLocation.row--; + return; + } + + if (keyCode == KeyboardHelper.RIGHT) { + cursorLocation.column++; + return; + } + + if (keyCode == KeyboardHelper.LEFT) { + cursorLocation.column--; + return; + } + + if (keyCode == KeyboardHelper.PGDOWN) { + cursorLocation.row += textCanvas.getSize().row; + repaintPage(); + return; + } + + if (keyCode == KeyboardHelper.PGUP) { + cursorLocation.row -= textCanvas.getSize().row; + repaintPage = true; + } + + } + + /** + * Processes the Tab key action for indentation and dedentation. + * + *

Behavior depends on modifiers and selection state:

+ *
    + *
  • Shift+Tab with selection: dedents all selected lines by + * removing up to 4 leading spaces, if all lines have sufficient indentation
  • + *
  • Shift+Tab without selection: dedents the current line by + * removing 4 leading spaces and moving the cursor back
  • + *
  • Tab with selection: indents all selected lines by adding + * 4 leading spaces
  • + *
+ * + * @param modifiers the keyboard modifier flags from the key event + */ + private void processTab(final int modifiers) { + if (KeyboardHelper.isShiftPressed(modifiers)) { + if (selectionStart.compareTo(selectionEnd) != 0) { + // dedent multiple lines + ensureSelectionOrder(); + + identSelection: + { + // check that indentation is possible + for (int y = selectionStart.row; y < selectionEnd.row; y++) { + final TextLine textLine = page.getLine(y); + + if (!textLine.isEmpty()) + if (textLine.getIndent() < 4) + break identSelection; + } + + for (int y = selectionStart.row; y < selectionEnd.row; y++) + page.getLine(y).cutFromBeginning(4); + } + } else { + // dedent current line + final TextLine textLine = page.getLine(cursorLocation.row); + + if (cursorLocation.column >= 4) + if (textLine.isEmpty()) + cursorLocation.column -= 4; + else if (textLine.getIndent() >= 4) { + cursorLocation.column -= 4; + textLine.cutFromBeginning(4); + } + + } + + repaintPage(); + + } else if (selectionStart.compareTo(selectionEnd) != 0) { + // indent multiple lines + ensureSelectionOrder(); + for (int y = selectionStart.row; y < selectionEnd.row; y++) + page.getLine(y).addIndent(4); + + repaintPage(); + } + } + + /** + * Repaints the entire visible page area onto the text canvas. + * + *

Iterates over every visible cell (row and column), applying the appropriate + * foreground and background colors based on whether the cell is the cursor position, + * part of a selection, or a tab stop margin. Characters are read from the underlying + * {@link Page} model with scroll offsets applied.

+ */ + public void repaintPage() { + + final int columnCount = textCanvas.getSize().column + 2; + final int rowCount = textCanvas.getSize().row + 2; + + for (int row = 0; row < rowCount; row++) + for (int column = 0; column < columnCount; column++) { + final boolean isTabMargin = ((column + scrolledCharacters) % 4) == 0; + + if ((column == (cursorLocation.column - scrolledCharacters)) + & (row == (cursorLocation.row - scrolledLines))) { + // cursor + textCanvas.setBackgroundColor(lookAndFeel.cursorBackground); + textCanvas.setForegroundColor(lookAndFeel.cursorForeground); + } else if (new TextPointer(row + scrolledLines, column).isBetween( + selectionStart, selectionEnd)) { + // selected text + textCanvas.setBackgroundColor(lookAndFeel.selectionBackground); + textCanvas.setForegroundColor(lookAndFeel.selectionForeground); + } else { + // normal text + textCanvas.setBackgroundColor(lookAndFeel.background); + textCanvas.setForegroundColor(lookAndFeel.foreground); + + if (isTabMargin) + textCanvas + .setBackgroundColor(lookAndFeel.tabStopBackground); + + } + + final char charUnderCursor = page.getChar(row + scrolledLines, + column + scrolledCharacters); + + textCanvas.putChar(row, column, charUnderCursor); + } + + } + + /** + * Repaints a single row of the editor. + * + *

Note: the current implementation delegates to + * {@link #repaintPage()} and repaints the entire page. This is a candidate + * for optimization.

+ * + * @param rowNumber the zero-based row index to repaint + */ + public void repaintRow(final int rowNumber) { + // TODO: Optimize this. No need to repaint entire page. + repaintPage(); + } + + /** + * Repaints only the portions of the editor that have been marked as dirty. + * + *

If {@link #repaintPage} is set, the entire page is repainted and all + * dirty row tracking is cleared. Otherwise, only the individually dirty rows + * are repainted.

+ */ + private void repaintWhatNeeded() { + if (repaintPage) { + dirtyRows.clear(); + repaintPage(); + return; + } + + dirtyRows.forEach(this::repaintRow); + dirtyRows.clear(); + } + + /** + * Scrolls the visible editor area by the specified number of characters and lines. + * + *

Scroll offsets are clamped so they never go below zero. A full page + * repaint is scheduled after scrolling.

+ * + * @param charactersToScroll the number of characters to scroll horizontally + * (positive = right, negative = left) + * @param linesToScroll the number of lines to scroll vertically + * (positive = down, negative = up) + */ + public void scroll(final int charactersToScroll, final int linesToScroll) { + scrolledLines += linesToScroll; + scrolledCharacters += charactersToScroll; + + if (scrolledLines < 0) + scrolledLines = 0; + + if (scrolledCharacters < 0) + scrolledCharacters = 0; + + repaintPage = true; + } + + /** + * Replaces the entire editor content with the given text. + * + *

Resets the cursor to position (0, 0), clears all scroll offsets and + * selections, creates a fresh {@link Page}, inserts the text, and triggers + * a full repaint.

+ * + * @param text the new text content for the editor; may contain newline + * characters to create multiple lines + */ + public void setText(final String text) { + // System.out.println("Set text:" + text); + cursorLocation = new TextPointer(0, 0); + scrolledCharacters = 0; + scrolledLines = 0; + selectionStart = new TextPointer(0, 0); + selectionEnd = new TextPointer(0, 0); + page = new Page(); + insertText(text); + repaintPage(); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/TextLine.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/TextLine.java new file mode 100755 index 0000000..671fa0f --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/TextLine.java @@ -0,0 +1,410 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.gui.textEditorComponent; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a single line of text in the text editor. + * + *

Internally stores a mutable list of {@link Character} objects, one per character in + * the line. Provides operations for inserting, cutting, and copying substrings, as well + * as indentation manipulation (adding or removing leading spaces).

+ * + *

Lines automatically trim trailing whitespace via the internal {@code pack()} method, + * which is invoked after most mutating operations. This ensures that lines never store + * unnecessary trailing space characters.

+ * + * @see Character the wrapper for individual character values in a line + * @see Page the container that holds multiple {@code TextLine} instances + * @see TextEditComponent the text editor component that uses lines for editing + */ +public class TextLine { + + private List chars = new ArrayList<>(); + + /** + * Creates an empty text line with no characters. + */ + public TextLine() { + } + + /** + * Creates a text line from an existing list of {@link Character} objects. + * + *

Trailing whitespace is automatically trimmed via {@code pack()}.

+ * + * @param value the list of characters to initialize this line with + */ + public TextLine(final List value) { + chars = value; + pack(); + } + + /** + * Creates a text line initialized with the given string. + * + *

Each character in the string is converted to a {@link Character} object. + * Trailing whitespace is automatically trimmed.

+ * + * @param value the string to initialize this line with + */ + public TextLine(final String value) { + setValue(value); + } + + /** + * Adds indentation (leading spaces) to the beginning of this line. + * + *

If the line is empty, no indentation is added. Otherwise, the specified + * number of space characters are prepended to the line.

+ * + * @param amount the number of space characters to prepend + */ + public void addIndent(final int amount) { + if (isEmpty()) + return; + + for (int i = 0; i < amount; i++) + chars.add(0, new Character(' ')); + } + + /** + * Removes characters from the specified range and returns them as a string. + * + *

This is a destructive operation: the characters in the range + * [{@code from}, {@code until}) are removed from this line. If the line is + * shorter than {@code until}, it is padded with spaces before extraction. + * Trailing whitespace is trimmed after removal.

+ * + * @param from the start index (inclusive) of the range to extract + * @param until the end index (exclusive) of the range to extract + * @return the extracted characters as a string + */ + public String copySubString(final int from, final int until) { + final StringBuilder result = new StringBuilder(); + + ensureLength(until); + + for (int i = from; i < until; i++) + result.append(chars.remove(from).value); + + pack(); + return result.toString(); + } + + + /** + * Removes the specified number of characters from the beginning of this line. + * + *

If {@code charactersToCut} exceeds the line length, the entire line is cleared. + * If {@code charactersToCut} is zero, no changes are made.

+ * + * @param charactersToCut the number of leading characters to remove + */ + public void cutFromBeginning(int charactersToCut) { + + if (charactersToCut > chars.size()) + charactersToCut = chars.size(); + + if (charactersToCut == 0) + return; + + chars = chars.subList(charactersToCut, chars.size()); + } + + /** + * Extracts a substring from this line, removing those characters and returning them. + * + *

Characters in the range [{@code from}, {@code until}) are removed from this + * line and returned as a string. Characters outside the range are retained. If the + * line is shorter than {@code until}, it is padded with spaces before extraction. + * Trailing whitespace is trimmed after the cut.

+ * + * @param from the start index (inclusive) of the range to cut + * @param until the end index (exclusive) of the range to cut + * @return the cut characters as a string + */ + public String cutSubString(final int from, final int until) { + final StringBuilder result = new StringBuilder(); + + final List reminder = new ArrayList<>(); + + ensureLength(until); + + for (int i = 0; i < chars.size(); i++) + if ((i >= from) && (i < until)) + result.append(chars.get(i).value); + else + reminder.add(chars.get(i)); + + chars = reminder; + + pack(); + return result.toString(); + } + + /** + * Truncates this line at the specified column, discarding all characters from + * that position to the end. + * + *

If {@code col} is greater than or equal to the current line length, + * no changes are made.

+ * + * @param col the column index at which to truncate (exclusive; characters at + * indices 0 through {@code col - 1} are kept) + */ + public void cutUntilEnd(final int col) { + if (col >= chars.size()) + return; + + chars = chars.subList(0, col); + } + + /** + * Ensures the internal character list is at least the given length, + * padding with space characters as needed. + */ + private void ensureLength(final int length) { + while (chars.size() < length) + chars.add(new Character(' ')); + } + + /** + * Returns the character at the specified column position. + * + *

If the column is beyond the end of this line, a space character is returned.

+ * + * @param col the zero-based column index + * @return the character at the given column, or {@code ' '} if out of bounds + */ + public char getCharForLocation(final int col) { + + if (col >= chars.size()) + return ' '; + + return chars.get(col).value; + } + + /** + * Returns the internal list of {@link Character} objects backing this line. + * + *

Note: the returned list is the live internal list. Modifications + * to the returned list will directly affect this line.

+ * + * @return the mutable list of characters in this line + */ + public List getChars() { + return chars; + } + + /** + * Returns the indentation level of this line, measured as the number of + * leading space characters before the first non-space character. + * + *

If the line is empty, returns {@code 0}.

+ * + * @return the number of leading space characters + * @throws RuntimeException if the line is non-empty but contains only spaces + * (should not occur due to trailing whitespace trimming by {@code pack()}) + */ + public int getIndent() { + if (isEmpty()) + return 0; + + for (int i = 0; i < chars.size(); i++) + if (chars.get(i).hasValue()) + return i; + + throw new RuntimeException("This code shall never execute"); + } + + /** + * Returns the length of this line (number of characters, excluding trimmed + * trailing whitespace). + * + * @return the number of characters in this line + */ + public int getLength() { + return chars.size(); + } + + /** + * Returns a new {@code TextLine} containing the characters from this line + * in the range [{@code from}, {@code until}). + * + *

If {@code until} exceeds the line length, only the available characters + * are included. The returned line is an independent copy.

+ * + * @param from the start index (inclusive) + * @param until the end index (exclusive) + * @return a new {@code TextLine} with the specified sub-range of characters + */ + public TextLine getSubLine(final int from, final int until) { + final List result = new ArrayList<>(); + + for (int i = from; i < until; i++) { + if (i >= chars.size()) + break; + result.add(chars.get(i)); + } + + return new TextLine(result); + } + + /** + * Returns a substring of this line from column {@code from} (inclusive) to + * column {@code until} (exclusive). + * + *

If the requested range extends beyond the line length, space characters + * are used for positions past the end of the line.

+ * + * @param from the start column (inclusive) + * @param until the end column (exclusive) + * @return the substring in the specified range + */ + public String getSubString(final int from, final int until) { + final StringBuilder result = new StringBuilder(); + + for (int i = from; i < until; i++) + result.append(getCharForLocation(i)); + + return result.toString(); + } + + /** + * Inserts a single character at the specified column position. + * + *

If the column is beyond the current line length, the line is padded + * with spaces up to that position. Trailing whitespace is trimmed after + * insertion.

+ * + * @param col the zero-based column at which to insert + * @param value the character to insert + */ + public void insertCharacter(final int col, final char value) { + ensureLength(col); + chars.add(col, new Character(value)); + pack(); + } + + /** + * Inserts a string at the specified column position. + * + *

Each character in the string is inserted sequentially starting at + * {@code col}. If the column is beyond the current line length, the line + * is padded with spaces. Trailing whitespace is trimmed after insertion.

+ * + * @param col the zero-based column at which to start inserting + * @param value the string to insert + */ + public void insertString(final int col, final String value) { + ensureLength(col); + int i = 0; + for (final char c : value.toCharArray()) { + chars.add(col + i, new Character(c)); + i++; + } + pack(); + } + + /** + * Inserts all characters from another {@code TextLine} at the specified column. + * + *

If the column is beyond the current line length, the line is padded with + * spaces. Trailing whitespace is trimmed after insertion.

+ * + * @param col the zero-based column at which to start inserting + * @param textLine the text line whose characters will be inserted + */ + public void insertTextLine(final int col, final TextLine textLine) { + ensureLength(col); + int i = 0; + for (final Character c : textLine.getChars()) { + chars.add(col + i, c); + i++; + } + pack(); + } + + /** + * Returns whether this line contains no characters. + * + *

Because trailing whitespace is trimmed, an empty line means there are + * no visible characters on this line.

+ * + * @return {@code true} if the line has no characters, {@code false} otherwise + */ + public boolean isEmpty() { + return chars.isEmpty(); + } + + /** + * Trims trailing whitespace from this line by removing trailing space + * characters that have no visible content. + */ + private void pack() { + int newLength = 0; + + for (int i = chars.size() - 1; i >= 0; i--) + if (chars.get(i).hasValue()) { + newLength = i + 1; + break; + } + + if (newLength == chars.size()) + return; + + chars = chars.subList(0, newLength); + } + + /** + * Removes the character at the specified column position. + * + *

If the column is beyond the end of the line, no changes are made.

+ * + * @param col the zero-based column of the character to remove + */ + public void removeCharacter(final int col) { + if (col >= chars.size()) + return; + + chars.remove(col); + } + + /** + * Replaces the entire contents of this line with the given string. + * + *

The existing characters are cleared, and each character from the string + * is added as a new {@link Character} object. Trailing whitespace is trimmed.

+ * + * @param string the new text content for this line + */ + public void setValue(final String string) { + chars.clear(); + for (final char c : string.toCharArray()) + chars.add(new Character(c)); + + pack(); + } + + /** + * Returns the string representation of this line by concatenating + * all character values. + * + * @return the text content of this line as a {@code String} + */ + @Override + public String toString() { + final StringBuilder buffer = new StringBuilder(); + + for (final Character character : chars) + buffer.append(character.value); + + return buffer.toString(); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/package-info.java new file mode 100644 index 0000000..53ea2a1 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/package-info.java @@ -0,0 +1,6 @@ +/** + * Provides a simple text editor component rendered in 3D space. + * + * @see eu.svjatoslav.aukio.e3d.gui.textEditorComponent.TextEditComponent + */ +package eu.svjatoslav.aukio.e3d.gui.textEditorComponent; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/headless/GoldenImage.java b/src/main/java/eu/svjatoslav/aukio/e3d/headless/GoldenImage.java new file mode 100644 index 0000000..8bbd7e5 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/headless/GoldenImage.java @@ -0,0 +1,168 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.headless; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +/** + * Golden-image comparison: render a scene, compare against a committed + * reference PNG, fail when the picture drifts. + * + *

A pixel counts as different when any RGB channel differs by more than + * a per-channel tolerance; a comparison fails when the fraction of + * differing pixels exceeds a threshold. Both are parameters — shading and + * antialiasing make exact matches fragile, but "the floor lost 30% of its + * pixels" must not pass.

+ * + *

Example:

+ *
{@code
+ * BufferedImage actual = Snapshot.render(scene, lighting, pose, 640, 480);
+ * GoldenImage.Result r = GoldenImage.compare(actual, new File("goldens/house.png"), 8, 0.01);
+ * if (!r.passed) {
+ *     GoldenImage.saveDiff(actual, new File("goldens/house.png"), "/tmp/diff.png");
+ *     throw new AssertionError(r.toString());
+ * }
+ * }
+ * + *

Command line (for scripts):

+ *
+ * java eu.svjatoslav.aukio.e3d.headless.GoldenImage actual.png golden.png [tolerance] [maxDiffFraction]
+ * exit code 0 = match, 1 = differ, 2 = usage/io error
+ * 
+ * + * @see Snapshot + */ +public final class GoldenImage { + + private GoldenImage() { + // utility class + } + + /** Outcome of one comparison. */ + public static final class Result { + /** True when the images match within tolerance. */ + public final boolean passed; + /** Fraction of pixels that differ (0..1). */ + public final double diffFraction; + /** Absolute count of differing pixels. */ + public final int diffPixels; + /** Total pixels compared. */ + public final int totalPixels; + + Result(final boolean passed, final double diffFraction, + final int diffPixels, final int totalPixels) { + this.passed = passed; + this.diffFraction = diffFraction; + this.diffPixels = diffPixels; + this.totalPixels = totalPixels; + } + + @Override + public String toString() { + return String.format(Locale.ROOT, "%s: %d/%d pixels differ (%.4f)", + passed ? "PASS" : "FAIL", diffPixels, totalPixels, diffFraction); + } + } + + /** + * Compares an image against a golden PNG file. + * + * @param actual the rendered image + * @param goldenFile the reference PNG + * @param channelTolerance per-channel (R/G/B) tolerance, 0 = exact + * @param maxDiffFraction maximum allowed fraction of differing pixels (0..1) + * @return the comparison result + * @throws IOException on read failure or size mismatch + */ + public static Result compare(final BufferedImage actual, final File goldenFile, + final int channelTolerance, final double maxDiffFraction) + throws IOException { + final BufferedImage golden = ImageIO.read(goldenFile); + if (golden == null) + throw new IOException("cannot read golden image: " + goldenFile); + if (golden.getWidth() != actual.getWidth() || golden.getHeight() != actual.getHeight()) + throw new IOException("size mismatch: actual " + actual.getWidth() + "x" + actual.getHeight() + + " vs golden " + golden.getWidth() + "x" + golden.getHeight()); + + int diff = 0; + final int width = actual.getWidth(); + final int height = actual.getHeight(); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (differs(actual.getRGB(x, y), golden.getRGB(x, y), channelTolerance)) + diff++; + } + } + final int total = width * height; + final double fraction = (double) diff / total; + return new Result(fraction <= maxDiffFraction, fraction, diff, total); + } + + /** + * Writes a visual diff image: matching pixels dimmed, differing pixels + * highlighted red. Handy for inspecting a failed comparison. + * + * @param actual the rendered image + * @param goldenFile the reference PNG + * @param outPath where to write the diff PNG + * @throws IOException on read/write failure or size mismatch + */ + public static void saveDiff(final BufferedImage actual, final File goldenFile, + final String outPath) throws IOException { + final BufferedImage golden = ImageIO.read(goldenFile); + final BufferedImage diff = new BufferedImage(actual.getWidth(), actual.getHeight(), + BufferedImage.TYPE_INT_RGB); + for (int y = 0; y < actual.getHeight(); y++) { + for (int x = 0; x < actual.getWidth(); x++) { + final int a = actual.getRGB(x, y); + if (differs(a, golden.getRGB(x, y), 0)) { + diff.setRGB(x, y, 0xFF0000); + } else { + // dimmed original: quarter brightness + diff.setRGB(x, y, ((a >> 2) & 0x3F3F3F)); + } + } + } + ImageIO.write(diff, "png", new File(outPath)); + } + + /** True when any channel of the two RGB values differs by more than tolerance. */ + private static boolean differs(final int rgb1, final int rgb2, final int tolerance) { + for (int shift = 16; shift >= 0; shift -= 8) { + final int c1 = (rgb1 >> shift) & 0xFF; + final int c2 = (rgb2 >> shift) & 0xFF; + if (Math.abs(c1 - c2) > tolerance) + return true; + } + return false; + } + + /** + * CLI: compares two PNGs. Exit 0 = match, 1 = differ, 2 = error. + * + * @param args actual.png golden.png [channelTolerance] [maxDiffFraction] + */ + public static void main(final String[] args) { + if (args.length < 2) { + System.err.println("usage: GoldenImage actual.png golden.png [channelTolerance] [maxDiffFraction]"); + System.exit(2); + } + try { + final int tolerance = args.length > 2 ? Integer.parseInt(args[2]) : 0; + final double maxFraction = args.length > 3 ? Double.parseDouble(args[3]) : 0.0; + final BufferedImage actual = ImageIO.read(new File(args[0])); + final Result result = compare(actual, new File(args[1]), tolerance, maxFraction); + System.out.println(result); + System.exit(result.passed ? 0 : 1); + } catch (final Exception e) { + System.err.println("error: " + e.getMessage()); + System.exit(2); + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/headless/PixelAssertions.java b/src/main/java/eu/svjatoslav/aukio/e3d/headless/PixelAssertions.java new file mode 100644 index 0000000..21286d1 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/headless/PixelAssertions.java @@ -0,0 +1,146 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.headless; + +import java.awt.image.BufferedImage; + +/** + * Pixel-level assertions for headless render verification. + * + *

The recurring question in rasterizer debugging is "did this region of + * the screen actually get painted?" These helpers answer it against a + * {@link BufferedImage} produced by {@link Snapshot}, treating one chosen + * background color as "unpainted". All methods are static and return data + * rather than throwing, so callers decide how to report failures.

+ * + *

Example — assert the floor rendered (no holes from clipping):

+ *
{@code
+ * BufferedImage image = Snapshot.render(scene, lighting, pose, 640, 480);
+ * double holes = PixelAssertions.unpaintedFraction(image, 0x00000000,
+ *         0.15, 0.45, 0.85, 1.0);  // lower-center band
+ * if (holes > 0.05)
+ *     throw new AssertionError("floor has holes: " + holes);
+ * }
+ * + * @see Snapshot + * @see GoldenImage + */ +public final class PixelAssertions { + + private PixelAssertions() { + // utility class + } + + /** + * Fraction of pixels in the whole image that still equal the background + * color (i.e. nothing was painted there). + * + * @param image the rendered image + * @param backgroundRgb the background color (RGB, alpha ignored) + * @return fraction in [0, 1] + */ + public static double unpaintedFraction(final BufferedImage image, final int backgroundRgb) { + return unpaintedFraction(image, backgroundRgb, 0, 0, 1, 1); + } + + /** + * Fraction of pixels inside a relative rectangle that still equal the + * background color. + * + * @param image the rendered image + * @param backgroundRgb the background color (RGB, alpha ignored) + * @param x0 left edge, fraction of width (0..1) + * @param y0 top edge, fraction of height (0..1) + * @param x1 right edge, fraction of width (0..1) + * @param y1 bottom edge, fraction of height (0..1) + * @return fraction in [0, 1] + */ + public static double unpaintedFraction(final BufferedImage image, final int backgroundRgb, + final double x0, final double y0, + final double x1, final double y1) { + final int width = image.getWidth(); + final int height = image.getHeight(); + final int bg = backgroundRgb & 0xFFFFFF; + + int total = 0; + int unpainted = 0; + for (int y = (int) (height * y0); y < (int) (height * y1); y++) { + for (int x = (int) (width * x0); x < (int) (width * x1); x++) { + total++; + if ((image.getRGB(x, y) & 0xFFFFFF) == bg) + unpainted++; + } + } + return total == 0 ? 0 : (double) unpainted / total; + } + + /** + * Counts pixels in the whole image that differ from the background color. + * + * @param image the rendered image + * @param backgroundRgb the background color (RGB, alpha ignored) + * @return number of painted pixels + */ + public static long countPainted(final BufferedImage image, final int backgroundRgb) { + final int bg = backgroundRgb & 0xFFFFFF; + long painted = 0; + for (int y = 0; y < image.getHeight(); y++) + for (int x = 0; x < image.getWidth(); x++) + if ((image.getRGB(x, y) & 0xFFFFFF) != bg) + painted++; + return painted; + } + + /** + * Counts pixels equal (in RGB) to the given color. Useful with flat + * test colors: "how much of the red triangle made it to the screen?" + * + * @param image the rendered image + * @param rgb the color to count (alpha ignored) + * @return number of matching pixels + */ + public static long countColor(final BufferedImage image, final int rgb) { + final int wanted = rgb & 0xFFFFFF; + long count = 0; + for (int y = 0; y < image.getHeight(); y++) + for (int x = 0; x < image.getWidth(); x++) + if ((image.getRGB(x, y) & 0xFFFFFF) == wanted) + count++; + return count; + } + + /** + * Dumps a grid of pixel colors around a point as text, for eyeballing + * gradients/edges in a failing render. Samples every {@code stride} + * pixels, formatted as RRGGBB hex, one row per line. + * + * @param image the rendered image + * @param cx center X in pixels + * @param cy center Y in pixels + * @param radius grid extends this many samples in each direction + * @param stride pixels between samples + * @return multi-line hex grid string + */ + public static String dumpPixelGrid(final BufferedImage image, + final int cx, final int cy, + final int radius, final int stride) { + final StringBuilder sb = new StringBuilder(); + for (int dy = -radius; dy <= radius; dy++) { + for (int dx = -radius; dx <= radius; dx++) { + final int x = cx + dx * stride; + final int y = cy + dy * stride; + if (x < 0 || y < 0 || x >= image.getWidth() || y >= image.getHeight()) { + sb.append(" ---- "); + } else { + sb.append(String.format("%06X", image.getRGB(x, y) & 0xFFFFFF)); + } + if (dx < radius) + sb.append(' '); + } + sb.append('\n'); + } + return sb.toString(); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/headless/SceneDump.java b/src/main/java/eu/svjatoslav/aukio/e3d/headless/SceneDump.java new file mode 100644 index 0000000..5c4d781 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/headless/SceneDump.java @@ -0,0 +1,103 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.headless; + +import eu.svjatoslav.aukio.e3d.renderer.raster.gi.GlobalIllumination; +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource; +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection; +import eu.svjatoslav.aukio.e3d.geometry.Camera; + +import java.util.Locale; + +/** + * Text dump of everything that determines what a frame looks like: shape + * counts, lights, camera pose, GI status. One call, one string — paste it + * into a bug report and the scene is reproducible. + * + *

Example:

+ *
{@code
+ * System.out.println(SceneDump.dump(scene, lighting, camera, gi));
+ * }
+ * + *

Sample output:

+ *
+ * == SceneDump ==
+ * shapes: 214 top-level, 1892 queued for rendering
+ * lights: 4 (ambient #181818)
+ *   [0] pos=(-800.0, -240.0, 0.0) color=FFD890 intensity=6.0
+ *   ...
+ * camera: 290.31, -35.59, -2.10, -0.58, -0.15, -0.00
+ * GI: running, 152034 work items, converged
+ * 
+ * + * @see Snapshot#poseString(Camera) + */ +public final class SceneDump { + + private SceneDump() { + // utility class + } + + /** + * Dumps scene state to a human-readable string. + * + * @param scene the scene (may be null to skip shape counts) + * @param lighting the lighting manager (may be null to skip lights) + * @param camera the camera (may be null to skip the pose) + * @param gi the GI system, or null when GI is not in use + * @return the dump, one fact per line + */ + public static String dump(final ShapeCollection scene, + final LightingManager lighting, + final Camera camera, + final GlobalIllumination gi) { + final StringBuilder sb = new StringBuilder("== SceneDump ==\n"); + + if (scene != null) { + int topLevel = 0; + for (final AbstractShape ignored : scene.getShapes()) + topLevel++; + sb.append("shapes: ").append(topLevel) + .append(" top-level, ").append(scene.getQueuedShapeCount()) + .append(" queued for rendering\n"); + } + + if (lighting != null) { + sb.append("lights: ").append(lighting.getLights().size()) + .append(" (ambient #") + .append(String.format("%06X", rgbOf(lighting.getAmbientLight()))) + .append(")\n"); + int i = 0; + for (final LightSource light : lighting.getLights()) { + sb.append(String.format(Locale.ROOT, + " [%d] pos=(%.1f, %.1f, %.1f) color=%06X intensity=%.1f%n", + i++, light.getPosition().x, light.getPosition().y, light.getPosition().z, + rgbOf(light.getColor()), light.getIntensity())); + } + } + + if (camera != null) + sb.append("camera: ").append(Snapshot.poseString(camera)).append('\n'); + + if (gi != null) { + sb.append("GI: "); + if (!gi.isRunning()) { + sb.append("stopped\n"); + } else { + sb.append("running, ").append(gi.getWorkItemCount()).append(" work items, ") + .append(gi.isConverged() ? "converged" : "converging").append('\n'); + } + } + + return sb.toString(); + } + + /** Packs an engine Color's r/g/b fields into a 0xRRGGBB int. */ + private static int rgbOf(final eu.svjatoslav.aukio.e3d.renderer.raster.Color color) { + return ((color.r & 0xFF) << 16) | ((color.g & 0xFF) << 8) | (color.b & 0xFF); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/headless/Snapshot.java b/src/main/java/eu/svjatoslav/aukio/e3d/headless/Snapshot.java new file mode 100644 index 0000000..d91db0a --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/headless/Snapshot.java @@ -0,0 +1,200 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.headless; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.geometry.Camera; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection; +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +/** + * One-call facade for rendering a scene to an image without a window. + * + *

Assembles the same transform → sort → paint pipeline that + * {@code ViewPanel} drives on screen, but into an off-screen + * {@link RenderingContext} pixel buffer. No Swing frame, no X display, no + * render thread — safe to use from tests, doc tooling and batch jobs.

+ * + *

Example:

+ *
{@code
+ * ShapeCollection scene = new ShapeCollection();
+ * scene.addShape(myShape);
+ *
+ * LightingManager lighting = new LightingManager();
+ * lighting.setAmbientLight(Color.hex("181818"));
+ *
+ * BufferedImage image = Snapshot.render(scene, lighting,
+ *         "290.31, -35.59, -2.10, -0.58, -0.15, 0.0", 640, 480);
+ * Snapshot.save(image, "/tmp/snapshot.png");
+ * }
+ * + *

The pose string is the same "x, y, z, yaw, pitch, roll" format the + * demos print for camera positions, so a pose copy-pasted from a bug + * report reproduces the exact view.

+ * + * @see PixelAssertions for checking what got painted + * @see GoldenImage for comparing against a reference PNG + * @see SceneDump for inspecting the scene state behind an image + */ +public final class Snapshot { + + private Snapshot() { + // utility class + } + + /** + * Renders the scene once from the given pose string. + * + * @param scene the scene to render + * @param lighting lighting for the frame (may be null for unlit scenes) + * @param pose "x, y, z, yaw, pitch, roll" — same format demos print + * @param width image width in pixels + * @param height image height in pixels + * @return the rendered frame (background is black) + */ + public static BufferedImage render(final ShapeCollection scene, + final LightingManager lighting, + final String pose, + final int width, final int height) { + return render(scene, lighting, cameraFromPose(pose), width, height); + } + + /** + * Renders the scene once from the given camera. + * + * @param scene the scene to render + * @param lighting lighting for the frame (may be null for unlit scenes) + * @param camera positioned camera + * @param width image width in pixels + * @param height image height in pixels + * @return the rendered frame (background is black) + */ + public static BufferedImage render(final ShapeCollection scene, + final LightingManager lighting, + final Camera camera, + final int width, final int height) { + final RenderingContext ctx = new RenderingContext(width, height, 1); + ctx.lightingManager = lighting; + renderInto(scene, camera, ctx, 0); + return ctx.getImage(); + } + + /** + * Renders one frame into an existing context, filling the background + * with the given ARGB color first. Use a unique sentinel color when the + * caller needs to distinguish "nothing painted here" from "painted + * black" (e.g. hole detection in clipping tests). + * + * @param scene the scene to render + * @param camera positioned camera + * @param ctx target context (its {@code pixels} buffer is painted) + * @param backgroundArgb background fill applied before painting + */ + public static void renderInto(final ShapeCollection scene, + final Camera camera, + final RenderingContext ctx, + final int backgroundArgb) { + final Point3D location = camera.getTransform().getTranslation(); + ctx.viewerPosition.x = location.x; + ctx.viewerPosition.y = location.y; + ctx.viewerPosition.z = location.z; + + Arrays.fill(ctx.pixels, backgroundArgb); + Arrays.fill(ctx.depth, Float.NEGATIVE_INFINITY); + scene.transformShapes(camera, ctx); + scene.sortShapes(ctx.vertexSlot); + scene.paintShapes(ctx); + if (System.getProperty("aukio.zbuffer.dumpDepth") != null) + dumpDepth(ctx, System.getProperty("aukio.zbuffer.dumpDepth")); + ctx.frameNumber++; + } + + /** + * Debug helper: saves the depth buffer as a grayscale PNG (z = 1/w, + * log-scaled; untouched pixels black). Enabled per render via + * {@code -Daukio.zbuffer.dumpDepth=/path.png}. + */ + private static void dumpDepth(final RenderingContext ctx, + final String path) { + final int w = ctx.width; + final int h = ctx.height; + final BufferedImage img = new BufferedImage(w, h, + BufferedImage.TYPE_BYTE_GRAY); + final byte[] out = ((java.awt.image.DataBufferByte) + img.getRaster().getDataBuffer()).getData(); + double maxLog = 1; + for (int i = 0; i < w * h; i++) { + final float dw = ctx.depth[i]; + if (dw > 0) + maxLog = Math.max(maxLog, Math.log(1d / dw)); + } + for (int i = 0; i < w * h; i++) { + final float dw = ctx.depth[i]; + if (dw > 0) { + final double z = 1d / dw; + out[i] = (byte) (255 * Math.log(z) / maxLog); + } + } + try { + javax.imageio.ImageIO.write(img, "png", + new java.io.File(path)); + } catch (final java.io.IOException e) { + System.out.println("depth dump failed: " + e.getMessage()); + } + } + + /** + * Builds a camera from a "x, y, z, yaw, pitch, roll" pose string, as + * printed by demos (see {@link #poseString(Camera)}). + * + * @param pose the pose string; commas and extra whitespace tolerated + * @return a camera at that pose + */ + public static Camera cameraFromPose(final String pose) { + final String[] parts = pose.split(","); + if (parts.length != 6) + throw new IllegalArgumentException( + "pose must have 6 comma-separated values (x, y, z, yaw, pitch, roll), got: " + pose); + final double[] v = new double[6]; + for (int i = 0; i < 6; i++) + v[i] = Double.parseDouble(parts[i].trim()); + final Camera camera = new Camera(); + camera.getTransform().set(v[0], v[1], v[2], v[3], v[4], v[5]); + return camera; + } + + /** + * Formats a camera's pose as "x, y, z, yaw, pitch, roll" — the exact + * string {@link #cameraFromPose(String)} parses back. Intended for bug + * reports: paste the pose, reproduce the view. + * + * @param camera the camera to describe + * @return the pose string + */ + public static String poseString(final Camera camera) { + final Point3D p = camera.getTransform().getTranslation(); + final double[] angles = camera.getTransform().getRotation().toAngles(); + return String.format(java.util.Locale.ROOT, "%.2f, %.2f, %.2f, %.2f, %.2f, %.2f", + p.x, p.y, p.z, angles[0], angles[1], angles[2]); + } + + /** + * Saves an image as PNG. + * + * @param image the image to save + * @param path target file path + * @throws IOException on write failure + */ + public static void save(final BufferedImage image, final String path) throws IOException { + ImageIO.write(image, "png", new File(path)); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/headless/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/headless/package-info.java new file mode 100644 index 0000000..59fae56 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/headless/package-info.java @@ -0,0 +1,22 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Headless rendering toolkit: windowless snapshots, pixel assertions, + * golden-image comparison and scene-state dumps. + * + *

Everything here works without a display, a Swing frame or a render + * thread, driving the same transform/sort/paint pipeline the on-screen + * path uses. Built for automated verification (tests, doc tooling, AI + * agents), but equally useful for batch thumbnail generation.

+ * + *
    + *
  • {@link eu.svjatoslav.aukio.e3d.headless.Snapshot} — render a scene to a BufferedImage; pose strings
  • + *
  • {@link eu.svjatoslav.aukio.e3d.headless.PixelAssertions} — "did this region get painted?"
  • + *
  • {@link eu.svjatoslav.aukio.e3d.headless.GoldenImage} — compare against a reference PNG
  • + *
  • {@link eu.svjatoslav.aukio.e3d.headless.SceneDump} — shapes/lights/camera/GI state as text
  • + *
+ */ +package eu.svjatoslav.aukio.e3d.headless; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/math/DiamondSquare.java b/src/main/java/eu/svjatoslav/aukio/e3d/math/DiamondSquare.java new file mode 100644 index 0000000..500f96b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/math/DiamondSquare.java @@ -0,0 +1,171 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +package eu.svjatoslav.aukio.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/aukio/e3d/math/Matrix3x3.java b/src/main/java/eu/svjatoslav/aukio/e3d/math/Matrix3x3.java new file mode 100644 index 0000000..b7ca82d --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/math/Matrix3x3.java @@ -0,0 +1,67 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.math; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; + +/** + * A 3x3 matrix for 3D transformations. + * + *

Matrix elements are stored in row-major order:

+ *
+ * | m00 m01 m02 |
+ * | m10 m11 m12 |
+ * | m20 m21 m22 |
+ * 
+ * + * @see Point3D + */ +public class Matrix3x3 { + + public double m00; + public double m01; + public double m02; + public double m10; + public double m11; + public double m12; + public double m20; + public double m21; + public double m22; + + /** + * Creates a zero matrix. + */ + public Matrix3x3() { + } + + /** + * Returns an identity matrix. + * + * @return a new identity matrix + */ + public static Matrix3x3 identity() { + final Matrix3x3 m = new Matrix3x3(); + m.m00 = 1; + m.m11 = 1; + m.m22 = 1; + return m; + } + + /** + * Applies this matrix transformation to a point. + * + * @param in the input point (not modified) + * @param out the output point (will be modified) + */ + public void transform(final Point3D in, final Point3D out) { + final double x = m00 * in.x + m01 * in.y + m02 * in.z; + final double y = m10 * in.x + m11 * in.y + m12 * in.z; + final double z = m20 * in.x + m21 * in.y + m22 * in.z; + out.x = x; + out.y = y; + out.z = z; + } + +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/math/Quaternion.java b/src/main/java/eu/svjatoslav/aukio/e3d/math/Quaternion.java new file mode 100644 index 0000000..c5d9fd3 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/math/Quaternion.java @@ -0,0 +1,281 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.math; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; + +import static java.lang.Math.cos; +import static java.lang.Math.sin; + +/** + * A unit quaternion representing a 3D rotation. + * + *

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 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. + * + * @param w the scalar component + * @param x the i component + * @param y the j component + * @param z the k component + */ + public Quaternion(final double w, final double x, final double y, final double z) { + this.w = w; + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Returns the identity quaternion representing no rotation. + * + * @return the identity quaternion (1, 0, 0, 0) + */ + public static Quaternion identity() { + return new Quaternion(1, 0, 0, 0); + } + + /** + * Creates a quaternion from an axis-angle representation. + * + * @param axis the rotation axis (must be normalized) + * @param angle the rotation angle in radians + * @return a quaternion representing the rotation + */ + public static Quaternion fromAxisAngle(final Point3D axis, final double angle) { + final double halfAngle = angle / 2; + final double s = sin(halfAngle); + final double c = cos(halfAngle); + return new Quaternion(c, axis.x * s, axis.y * s, axis.z * s); + } + + /** + * Creates a quaternion from XZ (yaw) and YZ (pitch) Euler angles. + * + *

The rotation is composed as yaw (around Y axis) followed by + * pitch (around X axis). No roll rotation is applied.

+ * + *

For full 3-axis rotation, use {@link #fromAngles(double, double, double)}.

+ * + * @param angleXZ the angle around the XZ axis (yaw) in radians + * @param angleYZ the angle around the YZ axis (pitch) in radians + * @return a quaternion representing the combined rotation + */ + public static Quaternion fromAngles(final double angleXZ, final double angleYZ) { + return fromAngles(angleXZ, angleYZ, 0); + } + + /** + * Creates a quaternion from full Euler angles (yaw, pitch, roll). + * + *

Rotation order: yaw (Y) → pitch (X) → roll (Z). This is the standard + * Y-X-Z Euler order commonly used for object placement in 3D scenes.

+ * + *

Performance note: This method uses a direct Euler-to-quaternion + * formula to avoid intermediate allocations.

+ * + * @param yaw rotation around Y axis (horizontal heading) in radians + * @param pitch rotation around X axis (vertical tilt) in radians; + * positive values tilt upward + * @param roll rotation around Z axis (bank/tilt) in radians; + * positive values rotate clockwise when looking along +Z + * @return a quaternion representing the combined rotation + */ + public static Quaternion fromAngles(final double yaw, final double pitch, final double roll) { + // Half angles for the Euler-to-quaternion conversion + final double cy = cos(yaw * 0.5); + final double sy = sin(yaw * 0.5); + final double cp = cos(pitch * 0.5); + final double sp = sin(pitch * 0.5); + final double cr = cos(roll * 0.5); + final double sr = sin(roll * 0.5); + + // Direct formula for Y-X-Z Euler order with negated pitch + // Equivalent to: qRoll * qPitch(−pitch) * qYaw + return new Quaternion( + cr * cp * cy + sr * sp * sy, // w + -cr * sp * cy - sr * cp * sy, // x + cr * cp * sy - sr * sp * cy, // y + -cr * sp * sy + sr * cp * cy // 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; + } + + /** + * Multiplies this quaternion by another (Hamilton product). + * + * @param other the quaternion to multiply by + * @return a new quaternion representing the combined rotation + */ + public Quaternion multiply(final Quaternion other) { + return new Quaternion( + w * other.w - x * other.x - y * other.y - z * other.z, + w * other.x + x * other.w + y * other.z - z * other.y, + w * other.y - x * other.z + y * other.w + z * other.x, + w * other.z + x * other.y - y * other.x + z * other.w + ); + } + + /** + * Normalizes this quaternion to unit length. + * + * @return this quaternion (for chaining) + */ + public Quaternion normalize() { + final double len = Math.sqrt(w * w + x * x + y * y + z * z); + if (len > 0) { + w /= len; + x /= len; + y /= len; + z /= len; + } + 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. + * + * @return a new matrix representing this rotation + */ + public Matrix3x3 toMatrix3x3() { + final Matrix3x3 m = new Matrix3x3(); + copyToMatrix(m); + return m; + } + + /** + * Copies this quaternion's rotation to an existing 3x3 matrix. + * + *

This method avoids allocation by reusing an existing Matrix3x3 instance. + * Used by Transform to avoid per-vertex allocation during rotation.

+ * + * @param m the matrix to receive the rotation (modified in place) + */ + public void copyToMatrix(final Matrix3x3 m) { + m.m00 = 1 - 2 * (y * y + z * z); + m.m01 = 2 * (x * y - w * z); + m.m02 = 2 * (x * z + w * y); + + m.m10 = 2 * (x * y + w * z); + m.m11 = 1 - 2 * (x * x + z * z); + m.m12 = 2 * (y * z - w * x); + + m.m20 = 2 * (x * z - w * y); + m.m21 = 2 * (y * z + w * x); + m.m22 = 1 - 2 * (x * x + y * y); + } + + /** + * 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(); + } + + /** + * Extracts Euler angles (yaw, pitch, roll) from this quaternion. + * + *

This is the inverse of {@link #fromAngles(double, double, double)}. + * Returns angles in the Y-X-Z Euler order used by this engine.

+ * + * @return array of {yaw, pitch, roll} in radians + */ + public double[] toAngles() { + final Matrix3x3 m = toMatrix3x3(); + + final double pitch = -Math.asin(Math.max(-1, Math.min(1, m.m21))); + final double yaw = -Math.atan2(m.m20, m.m22); + final double roll = -Math.atan2(m.m01, m.m11); + + return new double[]{yaw, pitch, roll}; + } + +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/math/Transform.java b/src/main/java/eu/svjatoslav/aukio/e3d/math/Transform.java new file mode 100755 index 0000000..851c6c8 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/math/Transform.java @@ -0,0 +1,257 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.math; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; + +/** + * Represents a transformation in 3D space combining translation and rotation. + * + *

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

+ * + *

Performance optimization: The rotation matrix is cached and only + * recomputed when the rotation quaternion changes. This avoids allocating a + * new Matrix3x3 on every transform() call and avoids redundant quaternion-to-matrix + * conversions for vertices sharing the same transform.

+ * + *

Mutability convention:

+ *
    + *
  • Imperative verbs ({@code set}, {@code setTranslation}, {@code transform}) + * mutate this transform or the input point
  • + *
  • {@code with}-prefixed methods ({@code withTransformed}) + * return a new instance without modifying the original
  • + *
+ * + *

Thread safety: The transform phase is single-threaded (synchronized in + * ShapeCollection.transformShapes()), so no synchronization is needed for the cached matrix. + * The matrix is computed once per Transform per frame and reused for all vertices.

+ * + * @see Quaternion + * @see Point3D + */ +public class Transform implements Cloneable { + + /** + * The translation applied after rotation. + */ + private final Point3D translation; + + /** + * The rotation applied before translation. + */ + private final Quaternion rotation; + + /** + * Cached rotation matrix for performance. + * Lazily computed when first needed and reused for subsequent transform() calls. + */ + private Matrix3x3 cachedMatrix; + + /** + * Flag indicating whether the cached matrix needs to be recomputed. + * Set to true when rotation is modified via set() or invalidateCache(). + */ + private boolean matrixDirty = true; + + /** + * Creates a transform with no translation or rotation (identity transform). + */ + public Transform() { + translation = new Point3D(); + rotation = new Quaternion(); + } + + /** + * Creates a transform with the specified translation and no rotation. + * + * @param translation the translation + */ + public Transform(final Point3D translation) { + this.translation = translation; + rotation = new Quaternion(); + } + + /** + * Creates a transform with the specified translation and rotation from Euler angles. + * + * @param translation the translation + * @param yaw the angle around the Y axis (horizontal heading) in radians + * @param pitch the angle around the X axis (vertical tilt) in radians + * @return a new transform with the specified translation and rotation + */ + public static Transform fromAngles(final Point3D translation, final double yaw, final double pitch) { + return fromAngles(translation.x, translation.y, translation.z, yaw, pitch, 0); + } + + /** + * Creates a transform with translation and full Euler rotation. + * + *

Rotation order: yaw (Y) → pitch (X) → roll (Z). This is the standard + * Y-X-Z Euler order commonly used for object placement in 3D scenes.

+ * + * @param x translation X coordinate + * @param y translation Y coordinate + * @param z translation Z coordinate + * @param yaw rotation around Y axis (horizontal heading) in radians + * @param pitch rotation around X axis (vertical tilt) in radians + * @param roll rotation around Z axis (bank/tilt) in radians + * @return a new transform with the specified translation and rotation + */ + public static Transform fromAngles(final double x, final double y, final double z, + final double yaw, final double pitch, final double roll) { + final Transform t = new Transform(new Point3D(x, y, z)); + t.rotation.set(Quaternion.fromAngles(yaw, pitch, roll)); + return t; + } + + /** + * Creates a transform with the specified translation and rotation. + * + * @param translation the translation + * @param rotation the rotation (will be cloned) + */ + public Transform(final Point3D translation, final Quaternion rotation) { + this.translation = translation; + this.rotation = rotation.clone(); + } + + /** + * Creates a copy of this transform with cloned translation and rotation. + * + * @return a new transform with the same translation and rotation values + */ + @Override + public Transform clone() { + return new Transform(translation, rotation); + } + + /** + * Returns the rotation component of this transform. + * + *

Warning: If you modify the returned quaternion directly, you must + * call {@link #invalidateCache()} afterwards to ensure the cached rotation matrix + * is recomputed on the next call to {@link #transform(Point3D)}.

+ * + * @return the rotation quaternion (mutable reference) + */ + public Quaternion getRotation() { + return rotation; + } + + /** + * Invalidates the cached rotation matrix. + * + *

Call this method after directly modifying the rotation quaternion + * (obtained via {@link #getRotation()}) to ensure the matrix is recomputed + * on the next call to {@link #transform(Point3D)}.

+ * + *

This method is automatically called by {@link #set(double, double, double, double, double, double)}.

+ * + * @return this transform (for chaining) + */ + public Transform invalidateCache() { + matrixDirty = true; + return this; + } + + /** + * Returns the translation component of this transform. + * + * @return the translation point (mutable reference) + */ + public Point3D getTranslation() { + return translation; + } + + /** + * Applies this transform to a point: rotation followed by translation. + * + *

Uses a cached rotation matrix to avoid allocation and redundant computation. + * The matrix is computed once (lazily) and reused for all subsequent calls + * until {@link #invalidateCache()} is called.

+ * + * @param point the point to transform (modified in place) + * @see #withTransformed(Point3D) for the non-mutating version that returns a new point + */ + public void transform(final Point3D point) { + getRotationMatrix().transform(point, point); + point.add(translation); + } + + /** + * Returns the cached rotation matrix, computing it first if the cache + * is dirty or not yet initialized. + * + *

Package-private for internal use by {@link TransformStack}. + * Callers must not modify the returned matrix.

+ * + * @return the cached rotation matrix + */ + Matrix3x3 getRotationMatrix() { + // Lazily create and cache the rotation matrix + if (matrixDirty || cachedMatrix == null) { + if (cachedMatrix == null) { + cachedMatrix = new Matrix3x3(); + } + rotation.copyToMatrix(cachedMatrix); + matrixDirty = false; + } + return cachedMatrix; + } + + /** + * Returns a new point with this transform applied. + * The original point is not modified. + * + * @param point the point to transform + * @return a new Point3D with the transform applied + * @see #transform(Point3D) for the mutating version + */ + public Point3D withTransformed(final Point3D point) { + final Point3D result = new Point3D(point); + transform(result); + return result; + } + + /** + * Sets the translation for this transform by copying the values from the given point. + * + * @param translation the translation values to copy + * @return this transform (for chaining) + */ + public Transform setTranslation(final Point3D translation) { + this.translation.x = translation.x; + this.translation.y = translation.y; + this.translation.z = translation.z; + return this; + } + +/** + * Sets both translation and rotation from Euler angles. + * + *

Rotation order: yaw (Y) → pitch (X) → roll (Z). This is the standard + * Y-X-Z Euler order commonly used for object placement in 3D scenes.

+ * + *

This method invalidates the cached rotation matrix.

+ * + * @param x translation X coordinate + * @param y translation Y coordinate + * @param z translation Z coordinate + * @param yaw rotation around Y axis (horizontal heading) in radians + * @param pitch rotation around X axis (vertical tilt) in radians + * @param roll rotation around Z axis (bank/tilt) in radians + * @return this transform for chaining + */ + public Transform set(final double x, final double y, final double z, + final double yaw, final double pitch, final double roll) { + translation.x = x; + translation.y = y; + translation.z = z; + rotation.set(Quaternion.fromAngles(yaw, pitch, roll)); + matrixDirty = true; + return this; + } + +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/math/TransformStack.java b/src/main/java/eu/svjatoslav/aukio/e3d/math/TransformStack.java new file mode 100644 index 0000000..58023c4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/math/TransformStack.java @@ -0,0 +1,222 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.math; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; + +/** + * Stack of transforms applied to points during rendering. + * + *

Transforms are applied in reverse order (last added is applied first). + * This supports hierarchical scene graphs where child objects are positioned + * relative to their parent objects.

+ * + *

Example:

+ *
+ * There is a ship in the sea. The ship moves along the sea, and every object
+ * on the ship moves with it. Inside the ship there is a car. The car moves
+ * along the ship, and every object on the car moves with it.
+ *
+ * To calculate the world position of an object inside the car:
+ * 1. Apply an object's position relative to the car
+ * 2. Apply the car's position relative to the ship
+ * 3. Apply ship's position relative to the world
+ * 
+ * + *

Implementation: eager composition. Every transform is a rigid + * motion (rotation then translation), and compositions of rigid motions are + * closed and associative, so the whole stack collapses into a single + * equivalent transform. The stack maintains the composed rotation matrix and + * translation for each level: {@link #addTransform} composes the pushed + * transform with the previous level's composite, and {@link #transform} + * applies only the top-level composite. Per-point cost is one 3x3 + * matrix-vector multiply plus one addition, independent of stack depth. + * {@link #dropTransform} restores the parent composite for free.

+ * + *

Contract: composition is a snapshot taken at push time. Mutating + * a transform after pushing it has no effect on the stack until it is + * dropped and pushed again. The traversal rebuilds the stack every frame, + * so frame-to-frame changes are always picked up.

+ * + * @see Transform + */ +public class TransformStack { + + /** + * Maximum nesting depth of transforms. + * Fixed size for efficiency to avoid memory allocation during rendering. + */ + private static final int MAX_DEPTH = 100; + + /** + * Composed row-major 3x3 rotation matrices, 9 doubles per stack level. + * Level i holds the composition of all transforms pushed so far, in + * application order (level i's own transform applied first, level 0 last). + */ + private final double[] rotations = new double[MAX_DEPTH * 9]; + + /** + * Composed translations, 3 doubles per stack level. + */ + private final double[] translations = new double[MAX_DEPTH * 3]; + + /** + * The current number of transforms in the stack. + */ + private int transformsCount = 0; + + /** + * Creates a new empty transform stack. + */ + public TransformStack() { + } + + /** + * Creates a copy of another transform stack, duplicating its composed + * per-level transforms. Used to give each parallel transform worker its + * own stack preloaded with the same camera/parent state. + * + * @param source the stack to copy + */ + public TransformStack(final TransformStack source) { + loadFrom(source); + } + + /** + * Reinitializes this stack as a copy of {@code source} without + * allocating: the fixed-size arrays are reused. Used by the parallel + * transform coordinator to hand pooled stacks to chunk tasks. + * + * @param source the stack to copy + */ + public void loadFrom(final TransformStack source) { + transformsCount = source.transformsCount; + System.arraycopy(source.rotations, 0, rotations, 0, transformsCount * 9); + System.arraycopy(source.translations, 0, translations, 0, transformsCount * 3); + } + + /** + * Pushes a transform onto the stack, composing it with the current + * top-level composite. + * + *

If the previous composite is (Rp, tp) and the pushed transform is + * (Rn, tn), the new composite is R' = Rp * Rn, t' = Rp * tn + tp — + * the pushed transform applies first, then the previous composite.

+ * + * @param transform the transform to push (snapshotted at push time) + */ + public void addTransform(final Transform transform) { + final int i = transformsCount; + final int r = i * 9; + final int v = i * 3; + + final Matrix3x3 rm = transform.getRotationMatrix(); + final Point3D t = transform.getTranslation(); + + if (i == 0) { + rotations[r] = rm.m00; + rotations[r + 1] = rm.m01; + rotations[r + 2] = rm.m02; + rotations[r + 3] = rm.m10; + rotations[r + 4] = rm.m11; + rotations[r + 5] = rm.m12; + rotations[r + 6] = rm.m20; + rotations[r + 7] = rm.m21; + rotations[r + 8] = rm.m22; + translations[v] = t.x; + translations[v + 1] = t.y; + translations[v + 2] = t.z; + } else { + final int pr = r - 9; + final int pv = v - 3; + + final double a00 = rotations[pr]; + final double a01 = rotations[pr + 1]; + final double a02 = rotations[pr + 2]; + final double a10 = rotations[pr + 3]; + final double a11 = rotations[pr + 4]; + final double a12 = rotations[pr + 5]; + final double a20 = rotations[pr + 6]; + final double a21 = rotations[pr + 7]; + final double a22 = rotations[pr + 8]; + + rotations[r] = a00 * rm.m00 + a01 * rm.m10 + a02 * rm.m20; + rotations[r + 1] = a00 * rm.m01 + a01 * rm.m11 + a02 * rm.m21; + rotations[r + 2] = a00 * rm.m02 + a01 * rm.m12 + a02 * rm.m22; + rotations[r + 3] = a10 * rm.m00 + a11 * rm.m10 + a12 * rm.m20; + rotations[r + 4] = a10 * rm.m01 + a11 * rm.m11 + a12 * rm.m21; + rotations[r + 5] = a10 * rm.m02 + a11 * rm.m12 + a12 * rm.m22; + rotations[r + 6] = a20 * rm.m00 + a21 * rm.m10 + a22 * rm.m20; + rotations[r + 7] = a20 * rm.m01 + a21 * rm.m11 + a22 * rm.m21; + rotations[r + 8] = a20 * rm.m02 + a21 * rm.m12 + a22 * rm.m22; + + translations[v] = a00 * t.x + a01 * t.y + a02 * t.z + translations[pv]; + translations[v + 1] = a10 * t.x + a11 * t.y + a12 * t.z + translations[pv + 1]; + translations[v + 2] = a20 * t.x + a21 * t.y + a22 * t.z + translations[pv + 2]; + } + transformsCount++; + } + + /** + * Clears all transforms from the stack. + */ + public void clear() { + transformsCount = 0; + } + + /** + * Pops the most recently added transform from the stack. The parent + * level's composite is restored automatically. + */ + public void dropTransform() { + transformsCount--; + } + + /** + * Transforms a point through the whole stack by applying the top-level + * composed transform. Cost is independent of stack depth. + * + * @param coordinate the input coordinate (not modified) + * @param result the output coordinate (receives transformed result) + */ + public void transform(final Point3D coordinate, final Point3D result) { + if (transformsCount == 0) { + result.clone(coordinate); + return; + } + final int r = (transformsCount - 1) * 9; + final int v = (transformsCount - 1) * 3; + final double x = coordinate.x; + final double y = coordinate.y; + final double z = coordinate.z; + result.x = rotations[r] * x + rotations[r + 1] * y + rotations[r + 2] * z + translations[v]; + result.y = rotations[r + 3] * x + rotations[r + 4] * y + rotations[r + 5] * z + translations[v + 1]; + result.z = rotations[r + 6] * x + rotations[r + 7] * y + rotations[r + 8] * z + translations[v + 2]; + } + + /** + * Copies the fully composed top-level transform (rotations 0..8, then + * translations 0..2) into {@code out} (length ≥ 12), for bulk + * loops that apply the same matrix to thousands of vertices + * ({@code TriangleMeshBlock}). Identity when the stack is empty. + * Applying it with the same expression order as + * {@link #transform(Point3D, Point3D)} yields bit-identical results. + * + * @param out destination array, length at least 12 + */ + public void getTopTransform(final double[] out) { + if (transformsCount == 0) { + out[0] = 1; out[1] = 0; out[2] = 0; + out[3] = 0; out[4] = 1; out[5] = 0; + out[6] = 0; out[7] = 0; out[8] = 1; + out[9] = 0; out[10] = 0; out[11] = 0; + return; + } + final int r = (transformsCount - 1) * 9; + final int v = (transformsCount - 1) * 3; + System.arraycopy(rotations, r, out, 0, 9); + System.arraycopy(translations, v, out, 9, 3); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/math/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/math/package-info.java new file mode 100644 index 0000000..3d7ab5e --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/math/package-info.java @@ -0,0 +1,9 @@ +/** + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + *

+ * Math that is needed for the project. + */ + +package eu.svjatoslav.aukio.e3d.math; + diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/package-info.java new file mode 100644 index 0000000..b8092d1 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/package-info.java @@ -0,0 +1,7 @@ +/** + * This is root package for 3D engine. Since package name cannot start with a digit, it is named "e3d" instead, + * which stands for "Engine 3D". + */ + +package eu.svjatoslav.aukio.e3d; + diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolume.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolume.java new file mode 100755 index 0000000..377192f --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolume.java @@ -0,0 +1,1137 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.octree; +import eu.svjatoslav.aukio.e3d.geometry.IntegerPoint; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.octree.raytracer.Ray; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; + +import static java.lang.Integer.max; +import static java.lang.Integer.min; + +/** + * Sparse voxel octree for 3D volume storage and ray tracing. + * + *

The octree represents a 3D volume with three cell types:

+ *
    + *
  • UNUSED - Empty cell, not yet allocated
  • + *
  • SOLID - Contains color and illumination data
  • + *
  • CLUSTER - Contains pointers to 8 child cells (for subdivision)
  • + *
+ * + *

Cell data is stored in parallel arrays ({@code cell1} through {@code cell8}) + * for memory efficiency. Each array stores different aspects of cell data.

+ * + *

Status: demo-only subsystem (used by {@code OctreeDemo}). + * The ray-traversal core ({@code traceCell}, ~590 lines of per-octant + * code) has no unit coverage and is exercised only through the demo's + * golden image; treat changes there as unverified by anything except the + * golden. The cell-pool capacity is fixed at construction — + * {@link #getNewCellPointer()} throws {@link IllegalStateException} when + * the pool is exhausted (it used to hang in an infinite rescan loop).

+ * + * @see eu.svjatoslav.aukio.e3d.renderer.octree.raytracer.RayTracer + * @see eu.svjatoslav.aukio.e3d.renderer.octree.raytracer.Ray + */ +public class OctreeVolume { + + /** Return value indicating no intersection during ray tracing. */ + public static final int TRACE_NO_HIT = -1; + + /** Cell state marker for solid cells. */ + private static final int CELL_STATE_SOLID = -2; + + /** Cell state marker for unused/empty cells. */ + private static final int CELL_STATE_UNUSED = -1; + + /** Cell data array 1: stores cell state and first child pointer. */ + public int[] cell1; + /** Cell data array 2: stores color values. */ + public int[] cell2; + /** Cell data array 3: stores illumination values. */ + public int[] cell3; + /** Cell data array 4: stores child pointer 4. */ + public int[] cell4; + /** Cell data array 5: stores child pointer 5. */ + public int[] cell5; + /** Cell data array 6: stores child pointer 6. */ + public int[] cell6; + /** Cell data array 7: stores child pointer 7. */ + public int[] cell7; + /** Cell data array 8: stores child pointer 8. */ + public int[] cell8; + + /** + * Pointer to the next unused cell in the allocation buffer. + */ + public int cellAllocationPointer = 0; + + /** Number of currently allocated cells. */ + public int usedCellsCount = 0; + + /** Size of the root (master) cell in world units. */ + public int masterCellSize; + + /** + * Creates a new octree volume with default buffer size (1.5M cells) + * and master cell size of 256*64 units. + */ + public OctreeVolume() { + initWorld(1500000, 256 * 64); + } + + /** + * Subdivides a solid cell into 8 child cells, each with the same color and illumination. + * + * @param pointer the cell to break up + */ + public void breakSolidCell(final int pointer) { + final int color = getCellColor(pointer); + final int illumination = getCellIllumination(pointer); + + cell1[pointer] = makeNewCell(color, illumination); + cell2[pointer] = makeNewCell(color, illumination); + cell3[pointer] = makeNewCell(color, illumination); + cell4[pointer] = makeNewCell(color, illumination); + cell5[pointer] = makeNewCell(color, illumination); + cell6[pointer] = makeNewCell(color, illumination); + cell7[pointer] = makeNewCell(color, illumination); + cell8[pointer] = makeNewCell(color, illumination); + } + + /** + * Clears the cell. + * @param pointer Pointer to the cell. + */ + public void clearCell(final int pointer) { + cell1[pointer] = 0; + cell2[pointer] = 0; + cell3[pointer] = 0; + cell4[pointer] = 0; + + cell5[pointer] = 0; + cell6[pointer] = 0; + cell7[pointer] = 0; + cell8[pointer] = 0; + } + + /** + * Marks a cell as deleted and returns it to the unused pool. + * + * @param cellPointer the cell to delete + */ + public void deleteCell(final int cellPointer) { + clearCell(cellPointer); + cell1[cellPointer] = CELL_STATE_UNUSED; + usedCellsCount--; + } + + /** + * Tests whether a ray intersects with a cubic region. + * + * @param cubeX the X center of the cube + * @param cubeY the Y center of the cube + * @param cubeZ the Z center of the cube + * @param cubeSize the half-size of the cube + * @param r the ray to test + * @return intersection type code, or 0 if no intersection + */ + public int doesIntersect(final int cubeX, final int cubeY, final int cubeZ, + final int cubeSize, final Ray r) { + + // ray starts inside the cube + if ((cubeX - cubeSize) < r.origin.x) + if ((cubeX + cubeSize) > r.origin.x) + if ((cubeY - cubeSize) < r.origin.y) + if ((cubeY + cubeSize) > r.origin.y) + if ((cubeZ - cubeSize) < r.origin.z) + if ((cubeZ + cubeSize) > r.origin.z) { + r.hitPoint = r.origin.clone(); + return 1; + } + // back face + if (r.direction.z > 0) + if ((cubeZ - cubeSize) > r.origin.z) { + final double mult = ((cubeZ - cubeSize) - r.origin.z) / r.direction.z; + final double hitX = (r.direction.x * mult) + r.origin.x; + if ((cubeX - cubeSize) < hitX) + if ((cubeX + cubeSize) > hitX) { + final double hitY = (r.direction.y * mult) + r.origin.y; + if ((cubeY - cubeSize) < hitY) + if ((cubeY + cubeSize) > hitY) { + r.hitPoint = new Point3D(hitX, hitY, cubeZ + - cubeSize); + return 2; + } + } + } + + // up face + if (r.direction.y > 0) + if ((cubeY - cubeSize) > r.origin.y) { + final double mult = ((cubeY - cubeSize) - r.origin.y) / r.direction.y; + final double hitX = (r.direction.x * mult) + r.origin.x; + if ((cubeX - cubeSize) < hitX) + if ((cubeX + cubeSize) > hitX) { + final double hitZ = (r.direction.z * mult) + r.origin.z; + if ((cubeZ - cubeSize) < hitZ) + if ((cubeZ + cubeSize) > hitZ) { + r.hitPoint = new Point3D(hitX, cubeY - cubeSize, + hitZ); + return 3; + } + } + } + + // left face + if (r.direction.x > 0) + if ((cubeX - cubeSize) > r.origin.x) { + final double mult = ((cubeX - cubeSize) - r.origin.x) / r.direction.x; + final double hitY = (r.direction.y * mult) + r.origin.y; + if ((cubeY - cubeSize) < hitY) + if ((cubeY + cubeSize) > hitY) { + final double hitZ = (r.direction.z * mult) + r.origin.z; + if ((cubeZ - cubeSize) < hitZ) + if ((cubeZ + cubeSize) > hitZ) { + r.hitPoint = new Point3D(cubeX - cubeSize, hitY, + hitZ); + return 4; + } + } + } + + // front face + if (r.direction.z < 0) + if ((cubeZ + cubeSize) < r.origin.z) { + final double mult = ((cubeZ + cubeSize) - r.origin.z) / r.direction.z; + final double hitX = (r.direction.x * mult) + r.origin.x; + if ((cubeX - cubeSize) < hitX) + if ((cubeX + cubeSize) > hitX) { + final double hitY = (r.direction.y * mult) + r.origin.y; + if ((cubeY - cubeSize) < hitY) + if ((cubeY + cubeSize) > hitY) { + r.hitPoint = new Point3D(hitX, hitY, cubeZ + + cubeSize); + return 5; + } + } + } + + // down face + if (r.direction.y < 0) + if ((cubeY + cubeSize) < r.origin.y) { + final double mult = ((cubeY + cubeSize) - r.origin.y) / r.direction.y; + final double hitX = (r.direction.x * mult) + r.origin.x; + if ((cubeX - cubeSize) < hitX) + if ((cubeX + cubeSize) > hitX) { + final double hitZ = (r.direction.z * mult) + r.origin.z; + if ((cubeZ - cubeSize) < hitZ) + if ((cubeZ + cubeSize) > hitZ) { + r.hitPoint = new Point3D(hitX, cubeY + cubeSize, + hitZ); + return 6; + } + } + } + + // right face + if (r.direction.x < 0) + if ((cubeX + cubeSize) < r.origin.x) { + final double mult = ((cubeX + cubeSize) - r.origin.x) / r.direction.x; + final double hitY = (r.direction.y * mult) + r.origin.y; + if ((cubeY - cubeSize) < hitY) + if ((cubeY + cubeSize) > hitY) { + final double hitZ = (r.direction.z * mult) + r.origin.z; + if ((cubeZ - cubeSize) < hitZ) + if ((cubeZ + cubeSize) > hitZ) { + r.hitPoint = new Point3D(cubeX + cubeSize, hitY, + hitZ); + return 7; + } + } + } + return 0; + } + + /** + * Fills a 3D rectangular region with solid cells of the given color. + * + * @param p1 one corner of the rectangle + * @param p2 the opposite corner of the rectangle + * @param color the color to fill with + */ + public void fillRectangle(IntegerPoint p1, IntegerPoint p2, Color color) { + + int x1 = min(p1.x, p2.x); + int x2 = max(p1.x, p2.x); + int y1 = min(p1.y, p2.y); + int y2 = max(p1.y, p2.y); + int z1 = min(p1.z, p2.z); + int z2 = max(p1.z, p2.z); + + for (int x = x1; x <= x2; x++) + for (int y = y1; y <= y2; y++) + for (int z = z1; z <= z2; z++) + putCell(x, y, z, 0, 0, 0, masterCellSize, 0, color); + } + + /** + * Returns the color value stored in a solid cell. + * + * @param pointer the cell pointer + * @return the packed RGB color value + */ + public int getCellColor(final int pointer) { + return cell2[pointer]; + } + + /** + * Returns the illumination value stored in a solid cell. + * + * @param pointer the cell pointer + * @return the packed RGB illumination value + */ + public int getCellIllumination(final int pointer) { + return cell3[pointer]; + } + + /** + * Initializes the octree storage arrays with the specified buffer size and root cell size. + * + * @param bufferLength the number of cells to allocate space for + * @param masterCellSize the size of the root cell in world units + */ + public void initWorld(final int bufferLength, final int masterCellSize) { + // System.out.println("Initializing new world"); + + // initialize world storage buffer + this.masterCellSize = masterCellSize; + + cell1 = new int[bufferLength]; + cell2 = new int[bufferLength]; + cell3 = new int[bufferLength]; + cell4 = new int[bufferLength]; + + cell5 = new int[bufferLength]; + cell6 = new int[bufferLength]; + cell7 = new int[bufferLength]; + cell8 = new int[bufferLength]; + + for (int i = 0; i < bufferLength; i++) + cell1[i] = CELL_STATE_UNUSED; + + // initialize master cell (occupies index 0 without being + // CELL_STATE_UNUSED — count it so usedCellsCount stays honest) + clearCell(0); + usedCellsCount = 1; + } + + /** + * Checks if the cell at the given pointer is a solid (leaf) cell. + * + * @param pointer the cell pointer to check + * @return {@code true} if the cell is solid + */ + public boolean isCellSolid(final int pointer) { + return cell1[pointer] == CELL_STATE_SOLID; + } + + /** + * Scans cells arrays and returns pointer to found unused cell. + * + *

Fails loudly on pool exhaustion — the previous version looped + * forever, rescanning the full pool on every wrap. The authoritative + * check is a full scan cycle back to the start position (the + * {@code usedCellsCount} fast path alone is insufficient: the master + * cell at index 0 occupies a slot without being counted).

+ * + * @return pointer to found unused cell + * @throws IllegalStateException when the cell pool is full + */ + public int getNewCellPointer() { + if (usedCellsCount >= cell1.length) + throw poolExhausted(); + + final int start = cellAllocationPointer; + while (true) { + // ensure that cell allocation pointer is in bounds + if (cellAllocationPointer >= cell1.length) + cellAllocationPointer = 0; + + if (cell1[cellAllocationPointer] == CELL_STATE_UNUSED) { + // unused cell found + clearCell(cellAllocationPointer); + + usedCellsCount++; + return cellAllocationPointer; + } + + cellAllocationPointer++; + if (cellAllocationPointer == start) + throw poolExhausted(); + } + } + + /** Builds the pool-exhaustion exception (shared by both guards). */ + private IllegalStateException poolExhausted() { + return new IllegalStateException( + "Octree cell pool exhausted: all " + cell1.length + + " cells are in use. Increase the pool size" + + " at OctreeVolume construction or reduce" + + " scene voxel density."); + } + + /** + * Allocates a new solid cell with the given color and illumination. + * + * @param color the color value for the new cell + * @param illumination the illumination value for the new cell + * @return the pointer to the newly allocated cell + */ + public int makeNewCell(final int color, final int illumination) { + final int pointer = getNewCellPointer(); + markCellAsSolid(pointer); + setCellColor(pointer, color); + setCellIllumination(pointer, illumination); + return pointer; + } + + /** + * Mark cell as solid. + * + * @param pointer pointer to cell + */ + public void markCellAsSolid(final int pointer) { + cell1[pointer] = CELL_STATE_SOLID; + } + + /** + * Stores a voxel at the given world coordinates with the specified color. + * + * @param x the X coordinate + * @param y the Y coordinate + * @param z the Z coordinate + * @param color the color of the voxel + */ + public void putCell(final int x, final int y, final int z, final Color color) { + putCell(x, y, z, 0, 0, 0, masterCellSize, 0, color); + } + + private void putCell(final int x, final int y, final int z, + final int cellX, final int cellY, final int cellZ, + final int cellSize, final int cellPointer, final Color color) { + + if (cellSize > 1) { + + // if case of big cell + if (isCellSolid(cellPointer)) { + + // if cell is already a needed color, do nothing + if (getCellColor(cellPointer) == color.toInt()) + return; + + // otherwise break cell up + breakSolidCell(cellPointer); + + // continue, as if it is cluster now + } + + // decide which subcube to use + int[] subCubeArray; + int subX, subY, subZ; + + if (x > cellX) { + subX = (cellSize / 2) + cellX; + if (y > cellY) { + subY = (cellSize / 2) + cellY; + if (z > cellZ) { + subZ = (cellSize / 2) + cellZ; + // 7 + subCubeArray = cell7; + } else { + subZ = (-cellSize / 2) + cellZ; + // 3 + subCubeArray = cell3; + } + } else { + subY = (-cellSize / 2) + cellY; + if (z > cellZ) { + subZ = (cellSize / 2) + cellZ; + // 6 + subCubeArray = cell6; + } else { + subZ = (-cellSize / 2) + cellZ; + // 2 + subCubeArray = cell2; + } + } + } else { + subX = (-cellSize / 2) + cellX; + if (y > cellY) { + subY = (cellSize / 2) + cellY; + if (z > cellZ) { + subZ = (cellSize / 2) + cellZ; + // 8 + subCubeArray = cell8; + } else { + subZ = (-cellSize / 2) + cellZ; + // 4 + subCubeArray = cell4; + } + } else { + subY = (-cellSize / 2) + cellY; + if (z > cellZ) { + subZ = (cellSize / 2) + cellZ; + // 5 + subCubeArray = cell5; + } else { + subZ = (-cellSize / 2) + cellZ; + // 1 + subCubeArray = cell1; + } + } + } + + int subCubePointer; + if (subCubeArray[cellPointer] == 0) { + // create empty cluster + subCubePointer = getNewCellPointer(); + subCubeArray[cellPointer] = subCubePointer; + } else + subCubePointer = subCubeArray[cellPointer]; + + putCell(x, y, z, subX, subY, subZ, cellSize / 2, subCubePointer, + color); + } else { + cell1[cellPointer] = CELL_STATE_SOLID; + cell2[cellPointer] = color.toInt(); + cell3[cellPointer] = CELL_STATE_UNUSED; + // System.out.println("Cell written!"); + } + } + + /** + * Sets the color value for the cell at the given pointer. + * + * @param pointer the cell pointer + * @param color the color value to set + */ + public void setCellColor(final int pointer, final int color) { + cell2[pointer] = color; + } + + /** + * Sets the illumination value for the cell at the given pointer. + * + * @param pointer the cell pointer + * @param illumination the illumination value to set + */ + public void setCellIllumination(final int pointer, final int illumination) { + cell3[pointer] = illumination; + } + + /** + * Traces a ray through the octree to find an intersecting solid cell. + * + * @param cellX the X coordinate of the current cell center + * @param cellY the Y coordinate of the current cell center + * @param cellZ the Z coordinate of the current cell center + * @param cellSize the size of the current cell + * @param pointer the pointer to the current cell + * @param ray the ray to trace + * @return pointer to intersecting cell or TRACE_NO_HIT if no intersection + */ + public int traceCell(final int cellX, final int cellY, final int cellZ, + final int cellSize, final int pointer, final Ray ray) { + if (isCellSolid(pointer)) { + // solid cell + if (doesIntersect(cellX, cellY, cellZ, cellSize, ray) != 0) { + ray.hitCellSize = cellSize; + ray.hitCellX = cellX; + ray.hitCellY = cellY; + ray.hitCellZ = cellZ; + return pointer; + } + return TRACE_NO_HIT; + } else // cluster + if (doesIntersect(cellX, cellY, cellZ, cellSize, ray) != 0) { + final int halfOfCellSize = cellSize / 2; + int rayIntersectionResult; + + if (ray.origin.x > cellX) { + if (ray.origin.y > cellY) { + if (ray.origin.z > cellZ) { + // 7 + // 6 8 3 5 2 4 1 + + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell7[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell6[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell8[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell3[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell2[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell4[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell5[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell1[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } else { + // 3 + // 2 4 7 1 6 8 5 + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell3[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell2[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell4[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell7[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell6[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell8[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, + cellZ - halfOfCellSize, halfOfCellSize, + cell1[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, + cellZ + halfOfCellSize, halfOfCellSize, + cell5[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } + } else if (ray.origin.z > cellZ) { + // 6 + // 5 2 7 8 1 3 4 + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell6[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell7[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell2[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell5[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell8[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell3[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell1[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell4[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } else { + // 2 + // 1 3 6 5 4 7 8 + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell2[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell3[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell1[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell6[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell7[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell5[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell4[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell8[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } + } else if (ray.origin.y > cellY) { + if (ray.origin.z > cellZ) { + // 8 + // 5 7 4 1 6 3 2 + + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell8[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell7[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell5[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell4[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell3[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell1[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell6[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell2[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } else { + // 4 + // 1 3 8 5 7 2 6 + + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell4[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell8[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell3[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell1[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY + halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell7[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + - halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell5[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + - halfOfCellSize, halfOfCellSize, cell2[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + + halfOfCellSize, cellY - halfOfCellSize, cellZ + + halfOfCellSize, halfOfCellSize, cell6[pointer], + ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } + } else if (ray.origin.z > cellZ) { + // 5 + // 1 6 8 4 2 7 3 + + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY - halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell5[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY - halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell1[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY - halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell6[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY + halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell8[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY + halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell4[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY + halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell7[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY - halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell2[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY + halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell3[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + } else { + // 1 + // 5 2 4 8 6 3 7 + + if (cell1[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY - halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell1[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell5[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY - halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell5[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell2[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY - halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell2[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell4[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY + halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell4[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell6[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY - halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell6[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell8[pointer] != 0) { + rayIntersectionResult = traceCell(cellX - halfOfCellSize, + cellY + halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell8[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + + if (cell3[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY + halfOfCellSize, cellZ - halfOfCellSize, + halfOfCellSize, cell3[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + if (cell7[pointer] != 0) { + rayIntersectionResult = traceCell(cellX + halfOfCellSize, + cellY + halfOfCellSize, cellZ + halfOfCellSize, + halfOfCellSize, cell7[pointer], ray); + if (rayIntersectionResult >= 0) + return rayIntersectionResult; + } + } + } + return TRACE_NO_HIT; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/package-info.java new file mode 100755 index 0000000..f79029e --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/package-info.java @@ -0,0 +1,21 @@ +/** + * Octree-based voxel volume representation and rendering for the Aukio 3D engine. + * + *

This package provides a volumetric data structure based on an octree, which enables + * efficient storage and rendering of voxel data. The octree recursively subdivides 3D space + * into eight octants, achieving significant data compression for sparse or repetitive volumes.

+ * + *

Key classes:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.octree.OctreeVolume} - the main octree data structure + * for storing and querying voxel cells
  • + *
  • {@link eu.svjatoslav.aukio.e3d.geometry.IntegerPoint} - integer 3D coordinate used + * for voxel addressing
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.octree.raytracer ray tracing through octree volumes + */ + +package eu.svjatoslav.aukio.e3d.renderer.octree; +import eu.svjatoslav.aukio.e3d.geometry.IntegerPoint; + diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/CameraView.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/CameraView.java new file mode 100644 index 0000000..5813889 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/CameraView.java @@ -0,0 +1,55 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.octree.raytracer; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.geometry.Camera; +import eu.svjatoslav.aukio.e3d.math.Matrix3x3; + +import static eu.svjatoslav.aukio.e3d.renderer.octree.raytracer.RaytracingCamera.SIZE; + +/** + * Represents camera view. Used to compute direction of rays during ray tracing. + */ +public class CameraView { + + /** + * Camera view coordinates. + */ + Point3D cameraCenter, topLeft, topRight, bottomLeft, bottomRight; + + /** + * Creates a camera view for ray tracing from the given camera and zoom level. + * + * @param camera the camera to create a view for + * @param zoom the zoom level (scales the view frustum) + */ + public CameraView(final Camera camera, final double zoom) { + final float viewAngle = (float) .6; + cameraCenter = new Point3D(); + topLeft = new Point3D(0, 0, SIZE).rotate(-viewAngle, -viewAngle); + topRight = new Point3D(0, 0, SIZE).rotate(viewAngle, -viewAngle); + bottomLeft = new Point3D(0, 0, SIZE).rotate(-viewAngle, viewAngle); + bottomRight = new Point3D(0, 0, SIZE).rotate(viewAngle, viewAngle); + + final Matrix3x3 m = camera.getTransform().getRotation().invert().toMatrix3x3(); + final Point3D temp = new Point3D(); + + temp.clone(topLeft); + m.transform(temp, topLeft); + + temp.clone(topRight); + m.transform(temp, topRight); + + temp.clone(bottomLeft); + m.transform(temp, bottomLeft); + + temp.clone(bottomRight); + m.transform(temp, bottomRight); + + camera.getTransform().getTranslation().clone().divide(zoom).addTo(cameraCenter, topLeft, topRight, bottomLeft, bottomRight); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/LightSource.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/LightSource.java new file mode 100755 index 0000000..dbfcc80 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/LightSource.java @@ -0,0 +1,42 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.octree.raytracer; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; + +/** + * Represents light source. + */ +public class LightSource { + + /** + * Light source color. + */ + public Color color; + /** + * Light source brightness. + */ + public float brightness; + /** + * Light source location. + */ + Point3D location; + + /** + * Creates a light source at the given location with the specified color and brightness. + * + * @param location the position of the light source in world space + * @param color the color of the light + * @param Brightness the brightness multiplier (0.0 = off, 1.0 = full) + */ + public LightSource(final Point3D location, final Color color, + final float Brightness) { + this.location = location; + this.color = color; + brightness = Brightness; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/Ray.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/Ray.java new file mode 100755 index 0000000..84d7168 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/Ray.java @@ -0,0 +1,71 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.octree.raytracer; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; + +/** + * Represents a ray used for tracing through an {@link eu.svjatoslav.aukio.e3d.renderer.octree.OctreeVolume}. + * + *

A ray is defined by an {@link #origin} point and a {@link #direction} vector. + * After tracing through the octree, the intersection results are stored in the + * {@link #hitPoint}, {@link #hitCellSize}, and {@link #hitCellX}/{@link #hitCellY}/{@link #hitCellZ} + * fields, which are populated by the octree traversal algorithm.

+ * + * @see RayTracer + * @see eu.svjatoslav.aukio.e3d.renderer.octree.OctreeVolume#traceCell(int, int, int, int, int, Ray) + */ +public class Ray { + + /** + * The origin point of the ray (the starting position in world space). + */ + public Point3D origin; + + /** + * The direction vector of the ray. Does not need to be normalized; + * the octree traversal handles arbitrary direction magnitudes. + */ + public Point3D direction; + + /** + * The point in world space where the ray intersected an octree cell. + * Set by the octree traversal algorithm after a successful intersection. + */ + public Point3D hitPoint; + + /** + * The size (side length) of the octree cell that was hit. + * A value of 1 indicates a leaf cell at the finest resolution. + */ + public int hitCellSize; + + /** + * The x coordinate of the octree cell that was hit. + */ + public int hitCellX; + + /** + * The y coordinate of the octree cell that was hit. + */ + public int hitCellY; + + /** + * The z coordinate of the octree cell that was hit. + */ + public int hitCellZ; + + /** + * Creates a new ray with the specified origin and direction. + * + * @param origin the starting point of the ray + * @param direction the direction vector of the ray + */ + public Ray(Point3D origin, Point3D direction) { + this.origin = origin; + this.direction = direction; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RayTracer.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RayTracer.java new file mode 100755 index 0000000..e9a41b0 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RayTracer.java @@ -0,0 +1,411 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.octree.raytracer; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; +import eu.svjatoslav.aukio.e3d.renderer.octree.OctreeVolume; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture; + +import java.util.Vector; + +/** + * Ray tracing engine for rendering {@link OctreeVolume} scenes onto a {@link Texture}. + * + *

{@code RayTracer} implements {@link Runnable} and is designed to execute as a background + * task. It casts one ray per pixel through the camera's view frustum, tracing each ray + * into the octree volume to find intersections with solid cells. When a hit is found, the + * ray tracer computes per-pixel lighting by casting shadow rays from the hit point toward + * each {@link LightSource} along multiple surface-normal-offset directions (6 directions: + * +X, -X, +Y, -Y, +Z, -Z) to approximate diffuse illumination with soft shadows.

+ * + *

Rendering pipeline

+ *
    + *
  1. The camera's view frustum corners are obtained via {@link RaytracingCamera#getCameraView()}.
  2. + *
  3. For each pixel, a primary ray is constructed from the camera center through the + * interpolated position on the view plane.
  4. + *
  5. The ray is traced through the octree using + * {@link OctreeVolume#traceCell(int, int, int, int, int, Ray)}.
  6. + *
  7. If a solid cell is hit, up to 6 shadow rays are cast toward each light source. + * If no shadow ray is occluded, the light's contribution is accumulated.
  8. + *
  9. The final pixel color is the cell's base color modulated by the accumulated light.
  10. + *
  11. Computed lighting is cached in the octree cell data ({@code cell3}) for reuse.
  12. + *
+ * + *

Progress is reported periodically by invalidating the texture's mipmap cache and + * requesting a repaint on the {@link ViewPanel}, allowing partial results to be displayed + * while rendering continues.

+ * + * @see OctreeVolume + * @see Ray + * @see LightSource + * @see RaytracingCamera + */ +public class RayTracer implements Runnable { + + /** + * Minimum interval in milliseconds between progress updates (texture refresh and repaint). + */ + private static final int PROGRESS_UPDATE_FREQUENCY_MILLIS = 1000; + + /** + * The raytracing camera defining the viewpoint and view frustum for ray generation. + */ + private final RaytracingCamera raytracingCamera; + + /** + * The target texture where rendered pixels are written. + */ + private final Texture texture; + + /** + * The view panel used for triggering display repaints during progressive rendering. + */ + private final ViewPanel viewPanel; + + /** + * The octree volume to be ray-traced. + */ + private final OctreeVolume octreeVolume; + + /** + * The list of light sources used for illumination calculations. + */ + private final Vector lights; + + /** + * Counter tracking the number of light computations performed during the current render pass. + */ + private int computedLights; + + /** + * Creates a new ray tracer for the given scene configuration. + * + * @param texture the texture to render into; its primary bitmap dimensions + * determine the output resolution + * @param octreeVolume the octree volume containing the scene geometry + * @param lights the light sources to use for illumination + * @param raytracingCamera the raytracing camera defining the viewpoint + * @param viewPanel the view panel for triggering progress repaints + */ + public RayTracer(final Texture texture, final OctreeVolume octreeVolume, + final Vector lights, final RaytracingCamera raytracingCamera, + final ViewPanel viewPanel) { + + this.texture = texture; + this.octreeVolume = octreeVolume; + this.lights = lights; + this.raytracingCamera = raytracingCamera; + this.viewPanel = viewPanel; + } + + /** + * Executes the ray tracing render pass. + * + *

Iterates over every pixel of the target texture, constructs a primary ray + * from the camera center through the view plane, traces it into the octree volume, + * and writes the resulting color. The texture is periodically refreshed to show + * progressive results.

+ */ + @Override + public void run() { + computedLights = 0; + + // create camera + + // Camera cam = new Camera(camCenter, upLeft, upRight, downLeft, + // downRight); + + // add camera to the raytracing point + // Main.mainWorld.geometryCollection.addObject(cam); + // Main.mainWorld.compiledGeometry.compileGeometry(Main.mainWorld.geometryCollection); + + final int width = texture.primaryBitmap.width; + final int height = texture.primaryBitmap.height; + + final CameraView cameraView = raytracingCamera.getCameraView(); + + // calculate vertical vectors + final double x1p = cameraView.bottomLeft.x - cameraView.topLeft.x; + final double y1p = cameraView.bottomLeft.y - cameraView.topLeft.y; + final double z1p = cameraView.bottomLeft.z - cameraView.topLeft.z; + + final double x2p = cameraView.bottomRight.x - cameraView.topRight.x; + final double y2p = cameraView.bottomRight.y - cameraView.topRight.y; + final double z2p = cameraView.bottomRight.z - cameraView.topRight.z; + + long nextBitmapUpdate = System.currentTimeMillis() + + PROGRESS_UPDATE_FREQUENCY_MILLIS; + + for (int y = 0; y < height; y++) { + final double cx1 = cameraView.topLeft.x + ((x1p * y) / height); + final double cy1 = cameraView.topLeft.y + ((y1p * y) / height); + final double cz1 = cameraView.topLeft.z + ((z1p * y) / height); + + final double cx2 = cameraView.topRight.x + ((x2p * y) / height); + final double cy2 = cameraView.topRight.y + ((y2p * y) / height); + final double cz2 = cameraView.topRight.z + ((z2p * y) / height); + + // calculate horizontal vector + final double x3p = cx2 - cx1; + final double y3p = cy2 - cy1; + final double z3p = cz2 - cz1; + + for (int x = 0; x < width; x++) { + final double cx3 = cx1 + ((x3p * x) / width); + final double cy3 = cy1 + ((y3p * x) / width); + final double cz3 = cz1 + ((z3p * x) / width); + + final Ray r = new Ray( + new Point3D(cameraView.cameraCenter.x, + cameraView.cameraCenter.y, + cameraView.cameraCenter.z), + new Point3D( + cx3 - cameraView.cameraCenter.x, cy3 + - cameraView.cameraCenter.y, cz3 + - cameraView.cameraCenter.z) + ); + final int c = traceRay(r); + + final Color color = new Color(c); + texture.primaryBitmap.drawPixel(x, y, color); + } + + if (System.currentTimeMillis() > nextBitmapUpdate) { + nextBitmapUpdate = System.currentTimeMillis() + + PROGRESS_UPDATE_FREQUENCY_MILLIS; + texture.resetResampledBitmapCache(); + viewPanel.repaintDuringNextViewUpdate(); + } + } + + texture.resetResampledBitmapCache(); + viewPanel.repaintDuringNextViewUpdate(); + } + + /** + * Traces a single ray into the octree volume and computes the resulting pixel color. + * + *

If the ray intersects a solid cell, the method computes diffuse lighting by + * casting shadow rays from 6 surface-offset positions toward each light source. + * The lighting result is cached in the octree's {@code cell3} array to avoid + * redundant computation for the same cell.

+ * + * @param ray the ray to trace (origin and direction must be set) + * @return the packed RGB color value (0xRRGGBB), or 0 if the ray hits nothing + */ + private int traceRay(final Ray ray) { + + final int intersectingCell = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, ray); + + if (intersectingCell != -1) { + // if lighting not computed, compute it + if (octreeVolume.cell3[intersectingCell] == -1) + // if cell is larger than 1 + if (ray.hitCellSize > 1) { + // break it up + octreeVolume.breakSolidCell(intersectingCell); + return traceRay(ray); + } else { + computedLights++; + float red = 30, green = 30, blue = 30; + + for (final LightSource l : lights) { + final double xDist = (l.location.x - ray.hitCellX); + final double yDist = (l.location.y - ray.hitCellY); + final double zDist = (l.location.z - ray.hitCellZ); + + double newRed = 0, newGreen = 0, newBlue = 0; + double tempRed, tempGreen, tempBlue; + + double distance = Math.sqrt((xDist * xDist) + + (yDist * yDist) + (zDist * zDist)); + distance = (distance / 3) + 1; + + final Ray r1 = new Ray( + new Point3D( + ray.hitCellX, + ray.hitCellY - (float) 1.5, + ray.hitCellZ), + + new Point3D((float) l.location.x - (float) ray.hitCellX, l.location.y + - (ray.hitCellY - (float) 1.5), (float) l.location.z + - (float) ray.hitCellZ) + ); + + final int rt1 = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, r1); + + if (rt1 == -1) { + newRed = (l.color.r * l.brightness) / distance; + newGreen = (l.color.g * l.brightness) / distance; + newBlue = (l.color.b * l.brightness) / distance; + } + + final Ray r2 = new Ray( + new Point3D( + ray.hitCellX - (float) 1.5, + ray.hitCellY, ray.hitCellZ), + + new Point3D( + l.location.x - (ray.hitCellX - (float) 1.5), (float) l.location.y + - (float) ray.hitCellY, (float) l.location.z + - (float) ray.hitCellZ) + ); + + final int rt2 = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, r2); + + if (rt2 == -1) { + tempRed = (l.color.r * l.brightness) / distance; + tempGreen = (l.color.g * l.brightness) / distance; + tempBlue = (l.color.b * l.brightness) / distance; + + if (tempRed > newRed) + newRed = tempRed; + if (tempGreen > newGreen) + newGreen = tempGreen; + if (tempBlue > newBlue) + newBlue = tempBlue; + } + + final Ray r3 = new Ray( + new Point3D( + ray.hitCellX, ray.hitCellY, + ray.hitCellZ - (float) 1.5), + new Point3D( + (float) l.location.x - (float) ray.hitCellX, (float) l.location.y + - (float) ray.hitCellY, l.location.z + - (ray.hitCellZ - (float) 1.5)) + ); + + final int rt3 = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, r3); + + if (rt3 == -1) { + tempRed = (l.color.r * l.brightness) / distance; + tempGreen = (l.color.g * l.brightness) / distance; + tempBlue = (l.color.b * l.brightness) / distance; + if (tempRed > newRed) + newRed = tempRed; + if (tempGreen > newGreen) + newGreen = tempGreen; + if (tempBlue > newBlue) + newBlue = tempBlue; + } + + final Ray r4 = new Ray( + new Point3D( + ray.hitCellX, + ray.hitCellY + (float) 1.5, + ray.hitCellZ), + + new Point3D( + (float) l.location.x - (float) ray.hitCellX, l.location.y + - (ray.hitCellY + (float) 1.5), (float) l.location.z + - (float) ray.hitCellZ) + ); + + final int rt4 = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, r4); + + if (rt4 == -1) { + tempRed = (l.color.r * l.brightness) / distance; + tempGreen = (l.color.g * l.brightness) / distance; + tempBlue = (l.color.b * l.brightness) / distance; + if (tempRed > newRed) + newRed = tempRed; + if (tempGreen > newGreen) + newGreen = tempGreen; + if (tempBlue > newBlue) + newBlue = tempBlue; + } + + final Ray r5 = new Ray( + new Point3D( + ray.hitCellX + (float) 1.5, + ray.hitCellY, ray.hitCellZ), + + new Point3D( + l.location.x - (ray.hitCellX + (float) 1.5), (float) l.location.y + - (float) ray.hitCellY, (float) l.location.z + - (float) ray.hitCellZ) + ); + + final int rt5 = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, r5); + + if (rt5 == -1) { + tempRed = (l.color.r * l.brightness) / distance; + tempGreen = (l.color.g * l.brightness) / distance; + tempBlue = (l.color.b * l.brightness) / distance; + if (tempRed > newRed) + newRed = tempRed; + if (tempGreen > newGreen) + newGreen = tempGreen; + if (tempBlue > newBlue) + newBlue = tempBlue; + } + + final Ray r6 = new Ray( + new Point3D( + ray.hitCellX, ray.hitCellY, + ray.hitCellZ + (float) 1.5), + + new Point3D( + + (float) l.location.x - (float) ray.hitCellX, (float) l.location.y + - (float) ray.hitCellY, l.location.z + - (ray.hitCellZ + (float) 1.5))); + + final int rt6 = octreeVolume.traceCell(0, 0, 0, + octreeVolume.masterCellSize, 0, r6); + + if (rt6 == -1) { + tempRed = (l.color.r * l.brightness) / distance; + tempGreen = (l.color.g * l.brightness) / distance; + tempBlue = (l.color.b * l.brightness) / distance; + if (tempRed > newRed) + newRed = tempRed; + if (tempGreen > newGreen) + newGreen = tempGreen; + if (tempBlue > newBlue) + newBlue = tempBlue; + } + red += newRed; + green += newGreen; + blue += newBlue; + + } + + final int cellColor = octreeVolume.cell2[intersectingCell]; + + red = (red * ((cellColor & 0xFF0000) >> 16)) / 255; + green = (green * ((cellColor & 0xFF00) >> 8)) / 255; + blue = (blue * (cellColor & 0xFF)) / 255; + + if (red > 255) + red = 255; + if (green > 255) + green = 255; + if (blue > 255) + blue = 255; + + octreeVolume.cell3[intersectingCell] = (((int) red) << 16) + + (((int) green) << 8) + ((int) blue); + + } + if (octreeVolume.cell3[intersectingCell] == 0) + return octreeVolume.cell2[intersectingCell]; + return octreeVolume.cell3[intersectingCell]; + } + + // return (200 << 16) + (200 << 8) + 255; + return 0; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RaytracingCamera.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RaytracingCamera.java new file mode 100755 index 0000000..d4721d0 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/RaytracingCamera.java @@ -0,0 +1,136 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.octree.raytracer; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.geometry.Camera; +import eu.svjatoslav.aukio.e3d.math.Matrix3x3; +import eu.svjatoslav.aukio.e3d.math.Transform; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.TexturedRectangle; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URL; + +/** + * Raytracing camera that renders a scene to a texture. + * It is represented on the scene as a textured rectangle showing the raytraced view. + */ +public class RaytracingCamera extends TexturedRectangle { + + /** Size of the camera view in world units. */ + public static final int SIZE = 100; + /** Size of the rendered image in pixels. */ + public static final int IMAGE_SIZE = 500; + private final CameraView cameraView; + + /** + * Creates a raytracing camera at the specified camera position. + * + * @param camera the camera to use for the view + * @param zoom the zoom level + */ + public RaytracingCamera(final Camera camera, final double zoom) { + super(new Transform(camera.getTransform().getTranslation().clone())); + cameraView = new CameraView(camera, zoom); + + computeCameraCoordinates(camera); + + addWaitNotification(getTexture()); + } + + private void addWaitNotification(final Texture texture) { + // add hourglass icon + try { + final BufferedImage sprite = getSprite("eu/svjatoslav/aukio/e3d/examples/hourglass.png"); + texture.graphics.drawImage(sprite, IMAGE_SIZE / 2, + (IMAGE_SIZE / 2) - 30, null); + } catch (final Exception ignored) { + } + + // add "Please wait..." message + texture.graphics.setColor(java.awt.Color.WHITE); + texture.graphics.setFont(new Font("Monospaced", Font.PLAIN, 10)); + texture.graphics.drawString("Please wait...", (IMAGE_SIZE / 2) - 20, + (IMAGE_SIZE / 2) + 30); + } + + private void computeCameraCoordinates(final Camera camera) { + initialize(SIZE, SIZE, IMAGE_SIZE, IMAGE_SIZE, 3); + + Point3D cameraCenter = new Point3D(); + + topLeft.setValues(cameraCenter.x, cameraCenter.y, cameraCenter.z + SIZE); + topRight.clone(topLeft); + bottomLeft.clone(topLeft); + bottomRight.clone(topLeft); + + final float viewAngle = (float) .6; + + topLeft.rotate(cameraCenter, -viewAngle, -viewAngle); + topRight.rotate(cameraCenter, viewAngle, -viewAngle); + bottomLeft.rotate(cameraCenter, -viewAngle, viewAngle); + bottomRight.rotate(cameraCenter, viewAngle, viewAngle); + + final Matrix3x3 m = camera.getTransform().getRotation().invert().toMatrix3x3(); + final Point3D temp = new Point3D(); + + temp.clone(topLeft); + temp.subtract(cameraCenter); + m.transform(temp, topLeft); + topLeft.add(cameraCenter); + + temp.clone(topRight); + temp.subtract(cameraCenter); + m.transform(temp, topRight); + topRight.add(cameraCenter); + + temp.clone(bottomLeft); + temp.subtract(cameraCenter); + m.transform(temp, bottomLeft); + bottomLeft.add(cameraCenter); + + temp.clone(bottomRight); + temp.subtract(cameraCenter); + m.transform(temp, bottomRight); + bottomRight.add(cameraCenter); + + final Color cameraColor = new Color(255, 255, 0, 255); + final LineAppearance appearance = new LineAppearance(2, cameraColor); + + addShape(appearance.getLine(topLeft, topRight)); + addShape(appearance.getLine(bottomLeft, bottomRight)); + addShape(appearance.getLine(topLeft, bottomLeft)); + addShape(appearance.getLine(topRight, bottomRight)); + + } + + /** + * Returns the camera view used for ray tracing. + * + * @return the camera view + */ + public CameraView getCameraView() { + return cameraView; + } + + /** + * Loads a sprite image from the classpath. + * + * @param ref the resource path + * @return the loaded image + * @throws IOException if the image cannot be loaded + */ + public BufferedImage getSprite(final String ref) throws IOException { + final URL url = this.getClass().getClassLoader().getResource(ref); + return ImageIO.read(url); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/package-info.java new file mode 100755 index 0000000..e49132c --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/octree/raytracer/package-info.java @@ -0,0 +1,21 @@ +/** + * Ray tracer for rendering voxel data stored in an octree structure. + * + *

This package implements a ray tracing renderer that casts rays through an + * {@link eu.svjatoslav.aukio.e3d.renderer.octree.OctreeVolume} to produce rendered images + * of volumetric data. The ray tracer traverses the octree hierarchy for efficient + * intersection testing, skipping empty regions of space.

+ * + *

Key classes:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.octree.raytracer.RayTracer} - main ray tracing engine
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.octree.raytracer.RaytracingCamera} - camera configuration for ray generation
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.octree.raytracer.Ray} - represents a single ray cast through the volume
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.octree.raytracer.LightSource} - defines a light source for shading
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.octree.OctreeVolume the voxel data structure + */ + +package eu.svjatoslav.aukio.e3d.renderer.octree.raytracer; + diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/package-info.java new file mode 100755 index 0000000..2c18a77 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/package-info.java @@ -0,0 +1,11 @@ +/** + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + *

+ * + * Various 3D renderers utilizing different rendering approaches. + * + */ + +package eu.svjatoslav.aukio.e3d.renderer; + diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Color.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Color.java new file mode 100644 index 0000000..93e80ec --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Color.java @@ -0,0 +1,353 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster; + +/** + * RGBA color representation for the Aukio 3D engine. + * + *

This is the engine's own color class (not {@link java.awt.Color}). All color values + * use integer components in the range 0-255. The class provides predefined constants + * for common colors and several constructors for creating colors from different formats.

+ * + *

Mutability: Color fields are mutable to enable reuse during rendering + * (e.g., lighting calculations). This avoids allocating new Color instances per polygon.

+ * + *

Usage examples:

+ *
{@code
+ * // Use predefined color constants
+ * Color red = Color.RED;
+ * Color semiTransparent = Color.hex("FF000080");
+ *
+ * // Create from hex string (recommended)
+ * Color hex6 = Color.hex("FF8800");     // RGB, fully opaque
+ * Color hex8 = Color.hex("FF880080");   // RGBA with alpha
+ * Color hex3 = Color.hex("F80");        // Short RGB format
+ *
+ * // Create from integer RGBA components (0-255)
+ * Color custom = new Color(100, 200, 50, 255);
+ *
+ * // Create from packed RGB integer
+ * Color packed = new Color(0xFF8800);
+ *
+ * // Modify existing color (avoids allocation)
+ * color.set(255, 128, 0, 255);
+ * }
+ * + *

Important: Always use this class instead of {@link java.awt.Color} when + * working with the Aukio 3D engine's rendering pipeline.

+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.Line + */ +public final class Color { + + /** + * Fully opaque red (255, 0, 0). + */ + public static final Color RED = new Color(255, 0, 0, 255); + /** + * Fully opaque green (0, 255, 0). + */ + public static final Color GREEN = new Color(0, 255, 0, 255); + /** + * Fully opaque blue (0, 0, 255). + */ + public static final Color BLUE = new Color(0, 0, 255, 255); + /** + * Fully opaque yellow (255, 255, 0). + */ + public static final Color YELLOW = new Color(255, 255, 0, 255); + /** + * Fully opaque cyan (0, 255, 255). + */ + public static final Color CYAN = new Color(0, 255, 255, 255); + /** + * Fully opaque magenta/purple (255, 0, 255). + */ + public static final Color MAGENTA = new Color(255, 0, 255, 255); + /** + * Fully opaque white (255, 255, 255). + */ + public static final Color WHITE = new Color(255, 255, 255, 255); + /** + * Fully opaque black (0, 0, 0). + */ + public static final Color BLACK = new Color(0, 0, 0, 255); + /** + * Fully opaque purple/magenta (255, 0, 255). + */ + public static final Color PURPLE = new Color(255, 0, 255, 255); + /** + * Fully transparent (alpha = 0). + */ + public static final Color TRANSPARENT = new Color(0, 0, 0, 0); + /** + * Red component. 0-255. + */ + public int r; + /** + * Green component. 0-255. + */ + public int g; + /** + * Blue component. 0-255. + */ + public int b; + /** + * Alpha component. + * 0 - transparent. + * 255 - opaque. + */ + public int a; + private java.awt.Color cachedAwtColor; + + /** + * Creates a black, fully opaque color (0, 0, 0, 255). + */ + public Color() { + this.r = 0; + this.g = 0; + this.b = 0; + this.a = 255; + } + + /** + * Creates a copy of the given color. + * + * @param parentColor the color to copy + */ + public Color(final Color parentColor) { + r = parentColor.r; + g = parentColor.g; + b = parentColor.b; + a = parentColor.a; + } + + /** + * Creates a color from floating-point RGBA components in the range 0.0 to 1.0. + * Values are internally converted to 0-255 integer range and clamped. + * + * @param r red component (0.0 = none, 1.0 = full) + * @param g green component (0.0 = none, 1.0 = full) + * @param b blue component (0.0 = none, 1.0 = full) + * @param a alpha component (0.0 = transparent, 1.0 = opaque) + */ + public Color(final double r, final double g, final double b, final double a) { + this.r = clamp((int) (r * 255d)); + this.g = clamp((int) (g * 255d)); + this.b = clamp((int) (b * 255d)); + this.a = clamp((int) (a * 255d)); + } + + /** + * Creates a color from a hexadecimal string. + * + * @param colorHexCode color code in hex format. + * Supported formats are: + *
+     *                                         RGB
+     *                                         RGBA
+     *                                         RRGGBB
+     *                                         RRGGBBAA
+     *                                         
+ */ + public Color(String colorHexCode) { + switch (colorHexCode.length()) { + case 3: + r = parseHexSegment(colorHexCode, 0, 1) * 16; + g = parseHexSegment(colorHexCode, 1, 1) * 16; + b = parseHexSegment(colorHexCode, 2, 1) * 16; + a = 255; + return; + + case 4: + r = parseHexSegment(colorHexCode, 0, 1) * 16; + g = parseHexSegment(colorHexCode, 1, 1) * 16; + b = parseHexSegment(colorHexCode, 2, 1) * 16; + a = parseHexSegment(colorHexCode, 3, 1) * 16; + return; + + case 6: + r = parseHexSegment(colorHexCode, 0, 2); + g = parseHexSegment(colorHexCode, 2, 2); + b = parseHexSegment(colorHexCode, 4, 2); + a = 255; + return; + + case 8: + r = parseHexSegment(colorHexCode, 0, 2); + g = parseHexSegment(colorHexCode, 2, 2); + b = parseHexSegment(colorHexCode, 4, 2); + a = parseHexSegment(colorHexCode, 6, 2); + return; + default: + throw new IllegalArgumentException("Unsupported color code: " + colorHexCode); + } + } + + /** + * Creates a fully opaque color from a packed RGB integer. + * + *

The integer is interpreted as {@code 0xRRGGBB}, where the upper 8 bits + * are the red channel, the middle 8 bits are green, and the lower 8 bits are blue.

+ * + * @param rgb packed RGB value (e.g. {@code 0xFF8800} for orange) + */ + public Color(final int rgb) { + r = (rgb & 0xFF0000) >> 16; + g = (rgb & 0xFF00) >> 8; + b = rgb & 0xFF; + a = 255; + } + + /** + * Creates a fully opaque color from RGB integer components (0-255). + * + * @param r red component (0-255) + * @param g green component (0-255) + * @param b blue component (0-255) + */ + public Color(final int r, final int g, final int b) { + this(r, g, b, 255); + } + + /** + * Creates a color from RGBA integer components (0-255). + * Values outside 0-255 are clamped. + * + * @param r red component (0-255) + * @param g green component (0-255) + * @param b blue component (0-255) + * @param a alpha component (0 = transparent, 255 = opaque) + */ + public Color(final int r, final int g, final int b, final int a) { + this.r = clamp(r); + this.g = clamp(g); + this.b = clamp(b); + this.a = clamp(a); + } + + /** + * Creates a color from a hexadecimal string. + * + *

Supported formats:

+ *
    + *
  • {@code RGB} - 3 hex digits, fully opaque
  • + *
  • {@code RGBA} - 4 hex digits
  • + *
  • {@code RRGGBB} - 6 hex digits, fully opaque
  • + *
  • {@code RRGGBBAA} - 8 hex digits
  • + *
+ * + * @param hex hex color code + * @return a new Color instance + */ + public static Color hex(final String hex) { + return new Color(hex); + } + + /** + * Clamps a value to the valid color component range (0-255). + * + * @param value the value to clamp + * @return the clamped value + */ + public static int clamp(final int value) { + if (value < 0) return 0; + if (value > 255) return 255; + return value; + } + + private int parseHexSegment(String hexString, int start, int length) { + return Integer.parseInt(hexString.substring(start, start + length), 16); + } + + /** + * Returns {@code true} if this color is fully transparent (alpha = 0). + * + * @return {@code true} if the alpha component is zero + */ + public boolean isTransparent() { + return a == 0; + } + + /** + * Sets all color components at once. + * + *

Values outside 0-255 are clamped. This method invalidates any cached + * AWT color, so the next call to {@link #toAwtColor()} will create a new one.

+ * + * @param r red component (0-255) + * @param g green component (0-255) + * @param b blue component (0-255) + * @param a alpha component (0-255) + * @return this Color for chaining + */ + public Color set(final int r, final int g, final int b, final int a) { + this.r = clamp(r); + this.g = clamp(g); + this.b = clamp(b); + this.a = clamp(a); + cachedAwtColor = null; + return this; + } + + /** + * Copies values from another color. + * + * @param other the color to copy from + * @return this Color for chaining + */ + public Color set(final Color other) { + this.r = other.r; + this.g = other.g; + this.b = other.b; + this.a = other.a; + cachedAwtColor = null; + return this; + } + + /** + * Converts this color to a {@link java.awt.Color} instance for use with + * Java AWT/Swing graphics APIs. + * + * @return the equivalent {@link java.awt.Color} + */ + public java.awt.Color toAwtColor() { + if (cachedAwtColor == null) + cachedAwtColor = new java.awt.Color(r, g, b, a); + return cachedAwtColor; + } + + /** + * Converts this color to a packed ARGB integer as used by {@link java.awt.Color#getRGB()}. + * + * @return packed ARGB integer representation + */ + public int toInt() { + return (a << 24) | (r << 16) | (g << 8) | b; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Color color = (Color) o; + + if (r != color.r) return false; + if (g != color.g) return false; + if (b != color.b) return false; + return a == color.a; + } + + @Override + public int hashCode() { + int result = r; + result = 31 * result + g; + result = 31 * result + b; + result = 31 * result + a; + return result; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/CullingStatistics.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/CullingStatistics.java new file mode 100644 index 0000000..95bfc4e --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/CullingStatistics.java @@ -0,0 +1,64 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Statistics for frustum culling, tracking composite-level culling efficiency. + * + *

Updated each frame during the rendering pipeline:

+ *
    + *
  • {@link #totalComposites} - incremented before each composite's frustum test
  • + *
  • {@link #culledComposites} - incremented when a composite fails the frustum test
  • + *
+ * + *

Thread safety: counters are {@link AtomicInteger} because the parallel + * transform phase increments them from multiple worker threads.

+ * + *

Displayed in the {@link eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel} to help developers understand + * culling efficiency and optimize scene graphs.

+ * + * @see eu.svjatoslav.aukio.e3d.gui.DeveloperToolsPanel + * @see eu.svjatoslav.aukio.e3d.renderer.raster.Frustum + */ +public class CullingStatistics { + + /** + * Total number of composite shapes tested against the frustum this frame. + * Incremented before each composite's AABB frustum test. + * Does not include the root composite (which is never frustum-tested). + */ + public final AtomicInteger totalComposites = new AtomicInteger(0); + + /** + * Number of composite shapes that were entirely outside the frustum and skipped. + * When a composite is culled, all its children (shapes and nested composites) + * are skipped without individual testing. + */ + public final AtomicInteger culledComposites = new AtomicInteger(0); + + /** + * Resets all statistics to zero. + * Called at the start of each frame before computing new statistics. + */ + public void reset() { + totalComposites.set(0); + culledComposites.set(0); + } + + /** + * Returns the percentage of composites that were culled. + * + * @return the culled percentage (0-100), or 0 if there are no composites + */ + public double getCulledPercentage() { + final int total = totalComposites.get(); + if (total == 0) { + return 0.0; + } + return 100.0 * culledComposites.get() / total; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Frustum.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Frustum.java new file mode 100644 index 0000000..47f7f39 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Frustum.java @@ -0,0 +1,269 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.geometry.Box; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.Plane; + +import eu.svjatoslav.aukio.e3d.geometry.Camera; + +/** + * View frustum for frustum culling - eliminates objects outside the camera's view. + * + *

The frustum is a truncated pyramid-shaped volume that represents everything + * the camera can see. Objects completely outside this volume can be skipped + * during rendering, significantly improving performance for large scenes.

+ * + *

Frustum planes:

+ *
    + *
  • Left, Right, Top, Bottom - define the viewport edges
  • + *
  • Near - closest visible distance from camera
  • + *
  • Far - farthest visible distance from camera
  • + *
+ * + *

Usage:

+ *
{@code
+ * Frustum frustum = new Frustum();
+ * frustum.update(camera, screenWidth, screenHeight);
+ *
+ * Box objectBounds = shape.getBoundingBox();
+ * if (frustum.intersectsAABB(objectBounds)) {
+ *     // Object is potentially visible - render it
+ * } else {
+ *     // Object outside frustum - skip rendering
+ * }
+ * }
+ * + *

AABB intersection algorithm:

+ *

Uses the optimized "P-vertex" approach: for each plane, we test only + * the AABB corner most aligned with the plane normal. If this corner is + * behind the plane, the entire AABB is outside the frustum.

+ * + * @see Box axis-aligned bounding box for culling tests + * @see Camera provides position and orientation for frustum computation + */ +public class Frustum { + + /** + * Index for the left clipping plane. + */ + public static final int LEFT = 0; + /** + * Index for the right clipping plane. + */ + public static final int RIGHT = 1; + /** + * Index for the top clipping plane. + */ + public static final int TOP = 2; + /** + * Index for the bottom clipping plane. + */ + public static final int BOTTOM = 3; + /** + * Index for the near clipping plane. + */ + public static final int NEAR = 4; + /** + * Index for the far clipping plane. + */ + public static final int FAR = 5; + + /** + * The six clipping planes defining the frustum volume. + * Each plane is stored as (normal, distance) in Hesse normal form. + * Planes are in world space coordinates. + */ + private final Plane[] planes = new Plane[6]; + + /** + * Default near plane distance from camera (in world units). + * Objects closer than this are culled. + */ + private double nearDistance = 1.0; + + /** + * Default far plane distance from camera (in world units). + * Objects farther than this are culled. + */ + private double farDistance = 10000.0; + + /** + * Creates a new frustum with uninitialized planes. + * Call {@link #update} before using for culling. + */ + public Frustum() { + for (int i = 0; i < 6; i++) { + planes[i] = new Plane(new Point3D(0, 0, 1), 0); + } + } + + /** + * Updates the frustum planes in view space (camera at origin, looking along +Z). + * + *

This method should be called once per frame before rendering, after the + * camera position and orientation have been updated.

+ * + *

View space coordinate system:

+ *
    + *
  • Camera at origin (0, 0, 0)
  • + *
  • Forward = +Z axis (looking into the screen)
  • + *
  • Right = +X axis
  • + *
  • Up = -Y axis (since Y-down means smaller Y is higher visually)
  • + *
+ * + *

Plane normals point INTO the frustum (toward the visible volume). + * A point is inside if dot(normal, point) >= distance for all planes.

+ * + *

FOV calculation: The Aukio 3D engine uses projectionScale = width/3. + * This means tan(halfHFOV) = (width/2) / projectionScale = 1.5, giving a + * horizontal FOV of approximately 112 degrees.

+ * + * @param camera the camera (used only for aspect ratio derivation from width/height) + * @param width the viewport width in pixels (defines projectionScale) + * @param height the viewport height in pixels (used for vertical FOV) + */ + public void update(final Camera camera, final int width, final int height) { + // Frustum is computed in VIEW SPACE (camera at origin, looking along +Z) + // This matches the coordinate system after applying camera transforms + + // Aukio 3D uses projectionScale = width/3 + // tan(halfFOV) = (halfSize) / projectionScale + final double projectionScale = width / 3.0; + final double tanHalfHFOV = (width / 2.0) / projectionScale; // = 1.5 (very wide FOV) + final double tanHalfVFOV = (height / 2.0) / projectionScale; // depends on aspect ratio + + // Compute cosine and sine of half-FOV angles + // cosHalfFOV = 1 / sqrt(1 + tanHalfFOV^2) + // sinHalfFOV = tanHalfFOV * cosHalfFOV + final double cosHalfHFOV = 1.0 / Math.sqrt(1.0 + tanHalfHFOV * tanHalfHFOV); + final double sinHalfHFOV = tanHalfHFOV * cosHalfHFOV; + final double cosHalfVFOV = 1.0 / Math.sqrt(1.0 + tanHalfVFOV * tanHalfVFOV); + final double sinHalfVFOV = tanHalfVFOV * cosHalfVFOV; + + // Near and far distances + nearDistance = 1.0; + farDistance = 10000.0; + + // All side planes pass through origin (camera position in view space) + // Plane equation: dot(normal, point) >= distance means inside + + // Left plane: inward normal pointing right-forward + // Bounds: x >= -tanHalfHFOV * z (to the right of left edge) + planes[LEFT].normal = new Point3D(cosHalfHFOV, 0, sinHalfHFOV); + planes[LEFT].distance = 0; + + // Right plane: inward normal pointing left-forward + // Bounds: x <= tanHalfHFOV * z (to the left of right edge) + planes[RIGHT].normal = new Point3D(-cosHalfHFOV, 0, sinHalfHFOV); + planes[RIGHT].distance = 0; + + // Top plane: inward normal pointing down-forward (Y-down system, top is smaller Y) + // Bounds: y <= tanHalfVFOV * z (below top edge, smaller Y) + planes[TOP].normal = new Point3D(0, -cosHalfVFOV, sinHalfVFOV); + planes[TOP].distance = 0; + + // Bottom plane: inward normal pointing up-forward (larger Y is below) + // Bounds: y >= -tanHalfVFOV * z (above bottom edge, larger Y) + planes[BOTTOM].normal = new Point3D(0, cosHalfVFOV, sinHalfVFOV); + planes[BOTTOM].distance = 0; + + // Near plane: inward normal pointing forward (+Z) + // Bounds: z >= nearDistance (in front of near plane) + planes[NEAR].normal = new Point3D(0, 0, 1); + planes[NEAR].distance = nearDistance; + + // Far plane: inward normal pointing backward (-Z) + // Bounds: z <= farDistance (behind far plane) + planes[FAR].normal = new Point3D(0, 0, -1); + planes[FAR].distance = -farDistance; + } + + /** + * Tests whether an axis-aligned bounding box intersects the frustum. + * + *

This is a conservative test: returns {@code true} if the box is + * potentially visible (inside or partially inside the frustum), and + * {@code false} only if the box is completely outside all frustum planes.

+ * + *

Optimized algorithm:

+ *

For each plane, we test only the AABB corner most aligned with the + * plane normal (the "P-vertex"). If this corner is behind the plane, + * the entire AABB must be outside the frustum.

+ * + * @param box the axis-aligned bounding box to test (in view space coordinates) + * @return {@code true} if the box intersects or is inside the frustum, + * {@code false} if completely outside + */ + public boolean intersectsAABB(final Box box) { + // Get box min/max for each axis + final double minX = box.getMinX(); + final double maxX = box.getMaxX(); + final double minY = box.getMinY(); + final double maxY = box.getMaxY(); + final double minZ = box.getMinZ(); + final double maxZ = box.getMaxZ(); + + for (int i = 0; i < 6; i++) { + final Plane plane = planes[i]; + final Point3D n = plane.normal; + final double d = plane.distance; + + // Find the P-vertex: the corner most aligned with the plane normal + // If normal component is positive, use max; if negative, use min + final double px = (n.x > 0) ? maxX : minX; + final double py = (n.y > 0) ? maxY : minY; + final double pz = (n.z > 0) ? maxZ : minZ; + + // Test if P-vertex is outside the frustum (behind the plane) + // For inward-pointing normals: inside = dot(N,P) >= distance + // So outside = dot(N,P) < distance + if (n.x * px + n.y * py + n.z * pz < d) { + return false; // AABB entirely outside this plane + } + } + + return true; // AABB intersects or inside all planes + } + + /** + * Returns the near clipping plane distance. + * + * @return the near distance in world units + */ + public double getNearDistance() { + return nearDistance; + } + + /** + * Returns the far clipping plane distance. + * + * @return the far distance in world units + */ + public double getFarDistance() { + return farDistance; + } + + /** + * Sets the near and far clipping distances. + * + * @param near the near plane distance (objects closer are culled) + * @param far the far plane distance (objects farther are culled) + */ + public void setClipDistances(final double near, final double far) { + this.nearDistance = near; + this.farDistance = far; + } + + /** + * Returns a specific frustum plane for debugging or advanced usage. + * + * @param planeIndex one of LEFT, RIGHT, TOP, BOTTOM, NEAR, FAR + * @return the plane at the specified index + */ + public Plane getPlane(final int planeIndex) { + return planes[planeIndex]; + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/HiZPyramid.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/HiZPyramid.java new file mode 100644 index 0000000..e04e579 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/HiZPyramid.java @@ -0,0 +1,198 @@ +package eu.svjatoslav.aukio.e3d.renderer.raster; + +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Hierarchical depth pyramid for whole-block occlusion culling + * (Hi-Z). Built from the just-painted frame's depth buffer; queried + * during the NEXT frame's transform to skip blocks that are fully + * hidden behind what was drawn last frame. + * + *

Semantics: each tile stores the MINIMUM w (= 1/z, i.e. the + * FARTHEST written depth) over its pixels. A block whose nearest + * possible point (max w over its AABB corners) is farther than a + * tile's farthest written depth is behind something at every written + * pixel of that tile — and tiles with any unwritten (sky) pixel hold + * -infinity and never occlude. That makes the test conservative: it + * may keep a hidden block, it never culls a visible one — under a + * static camera. (The first version max-pooled, storing the NEAREST + * depth per tile; that culled houses visible BETWEEN nearer tree + * trunks — per-pixel gaps inside a tile are invisible to a max.) + * Camera motion reuses the stale pyramid, which can cull a + * newly-visible block wrongly; the block's pixels then contain far + * background depth, so the next frame's pyramid no longer occludes + * it — wrong culls self-heal in one frame (sanctioned design + * concession), and the query margin absorbs small-motion + * parallax.

+ * + *

Empty tiles (never depth-written) store -infinity and never + * occlude, so the first frame after startup (or after any pass that + * leaves the pyramid unbuilt) culls nothing.

+ * + *

Knobs: {@code -Daukio.hiz=false} disables culling (build still + * happens — it is cheap — so flipping the flag needs no warm-up); + * {@code -Daukio.hiz.margin=0.02} sets the relative w-space safety + * margin against parallax between frames.

+ */ +public final class HiZPyramid { + + /** Level-0 tile edge in pixels; level i tiles cover TILE*2^i px. */ + private static final int TILE = 8; + + private static final boolean ENABLED = Boolean.parseBoolean( + System.getProperty("aukio.hiz", "true")); + private static final double MARGIN = Double.parseDouble( + System.getProperty("aukio.hiz.margin", "0.02")); + + /** Blocks occlusion-tested this process (telemetry). */ + public final AtomicLong blocksTested = new AtomicLong(); + /** Blocks culled as fully occluded (telemetry). */ + public final AtomicLong blocksCulled = new AtomicLong(); + + private float[] tiles = new float[0]; + private int[] levelOff = new int[0]; + private int[] levelW = new int[0]; + private int[] levelH = new int[0]; + private int levels; + private int bufW = -1, bufH = -1; + + /** Rebuilds the pyramid from a freshly painted depth buffer. */ + public synchronized void buildFrom(final float[] depth, + final int width, final int height) { + if (width != bufW || height != bufH) { + allocate(width, height); + } + + // Level 0: min-pool depth into TILE x TILE tiles. Depth + // outside the painted area is -infinity (cleared), so a tile + // with any unpainted pixel never occludes. + final int w0 = levelW[0], h0 = levelH[0]; + final int off0 = levelOff[0]; + for (int ty = 0; ty < h0; ty++) { + final int yEnd = Math.min((ty + 1) * TILE, height); + for (int tx = 0; tx < w0; tx++) { + final int xEnd = Math.min((tx + 1) * TILE, width); + float m = Float.POSITIVE_INFINITY; + for (int y = ty * TILE; y < yEnd; y++) { + final int row = y * width; + for (int x = tx * TILE; x < xEnd; x++) + if (depth[row + x] < m) + m = depth[row + x]; + } + tiles[off0 + ty * w0 + tx] = m; + } + } + + // Higher levels: min of 2x2 children. + for (int l = 1; l < levels; l++) { + final int pw = levelW[l - 1], ph = levelH[l - 1]; + final int poff = levelOff[l - 1]; + final int cw = levelW[l], ch = levelH[l]; + final int coff = levelOff[l]; + for (int ty = 0; ty < ch; ty++) + for (int tx = 0; tx < cw; tx++) { + float m = Float.POSITIVE_INFINITY; + for (int dy = 0; dy < 2; dy++) + for (int dx = 0; dx < 2; dx++) { + final int sx = tx * 2 + dx, sy = ty * 2 + dy; + if (sx < pw && sy < ph) { + final float v = tiles[poff + sy * pw + sx]; + if (v < m) + m = v; + } + } + tiles[coff + ty * cw + tx] = m; + } + } + } + + /** + * Conservative whole-block occlusion test. + * + * @param x1..y2 screen-space AABB of the block (will be clamped + * to the buffer; a fully off-screen box returns + * false) + * @param nearestW the block's nearest possible depth = MAX 1/z + * over its corners + * @return true when the block is certainly hidden behind last + * frame's occluders (within the parallax margin) + */ + public synchronized boolean occluded(final double x1, final double y1, + final double x2, final double y2, + final double nearestW) { + if (!ENABLED || levels == 0) + return false; + + int bx1 = (int) Math.floor(x1), by1 = (int) Math.floor(y1); + int bx2 = (int) Math.ceil(x2), by2 = (int) Math.ceil(y2); + if (bx1 < 0) bx1 = 0; + if (by1 < 0) by1 = 0; + if (bx2 >= bufW) bx2 = bufW - 1; + if (by2 >= bufH) by2 = bufH - 1; + if (bx1 > bx2 || by1 > by2) + return false; + + // Coarsest level where the box still covers <= 2 tiles per axis. + int level = 0; + while (level + 1 < levels) { + final int s = TILE << (level + 1); + final int tw = (bx2 / s) - (bx1 / s) + 1; + final int th = (by2 / s) - (by1 / s) + 1; + if (tw > 2 || th > 2) + break; + level++; + } + + final int s = TILE << level; + final int tx1 = bx1 / s, ty1 = by1 / s; + final int tx2 = bx2 / s, ty2 = by2 / s; + final int w = levelW[level], off = levelOff[level]; + + float minStored = Float.POSITIVE_INFINITY; + for (int ty = ty1; ty <= ty2; ty++) + for (int tx = tx1; tx <= tx2; tx++) { + final float v = tiles[off + ty * w + tx]; + if (v < minStored) + minStored = v; + } + + // Occluded only when the block's nearest point is clearly + // behind the farthest written depth in the range; the + // relative margin absorbs parallax between frames. + return nearestW < minStored * (1.0 - MARGIN); + } + + private void allocate(final int width, final int height) { + bufW = width; + bufH = height; + int lw = (width + TILE - 1) / TILE; + int lh = (height + TILE - 1) / TILE; + int count = 0; + levels = 0; + while (true) { + levels++; + count += lw * lh; + if (lw == 1 && lh == 1) + break; + lw = Math.max(1, (lw + 1) / 2); + lh = Math.max(1, (lh + 1) / 2); + } + tiles = new float[count]; + levelOff = new int[levels]; + levelW = new int[levels]; + levelH = new int[levels]; + lw = (width + TILE - 1) / TILE; + lh = (height + TILE - 1) / TILE; + int off = 0; + for (int l = 0; l < levels; l++) { + levelOff[l] = off; + levelW[l] = lw; + levelH[l] = lh; + off += lw * lh; + lw = Math.max(1, (lw + 1) / 2); + lh = Math.max(1, (lh + 1) / 2); + } + Arrays.fill(tiles, Float.NEGATIVE_INFINITY); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformCoordinator.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformCoordinator.java new file mode 100644 index 0000000..54f544f --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformCoordinator.java @@ -0,0 +1,204 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster; + +import java.util.Queue; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Coordinates the non-blocking parallel transform fork for one frame. + * + *

Every composite whose render list exceeds the parallel threshold (at + * ANY nesting level, not just the root) splits its children into chunks and + * submits chunk tasks here instead of transforming them serially. Chunk + * tasks never block on other tasks, so a fixed-size pool cannot starve; + * only the orchestrating render thread waits, in {@link #drainAndMergeInto}. + * This is what makes recursive decomposition safe: a nested heavy composite + * reached inside a chunk task forks its own children into the same queue + * and returns immediately.

+ * + *

Merge order is irrelevant: the depth sort that follows the transform + * phase is deterministic on (Z, shapeId).

+ * + *

Lifecycle: created by {@code ShapeCollection.transformShapes()} when a + * transform executor is available, published on the rendering context for + * the duration of the root transform, drained and discarded before the + * sort phase.

+ */ +public final class ParallelTransformCoordinator { + + private final ExecutorService executor; + private final Queue> futures = new ConcurrentLinkedQueue<>(); + private final AtomicInteger submittedTaskCount = new AtomicInteger(); + + /** + * Shared pools of chunk-task scratch objects. Coordinators are + * created per render pass, so the pools are static — otherwise + * reuse would never survive the next pass. A pooled aggregator's + * queue list keeps its capacity (reset() does not shrink), and a + * pooled TransformStack keeps its fixed arrays, so steady-state + * frames allocate neither the 9.6 KB stack per chunk nor the + * doubling-copy chain of every chunk's queue. + */ + private static final Queue + STACK_POOL = new ConcurrentLinkedQueue<>(); + private static final Queue + AGGREGATOR_POOL = new ConcurrentLinkedQueue<>(); + + /** + * Borrows a pooled transform stack preloaded with {@code source}'s + * composed state. Must be returned with {@link #returnStack} when + * the chunk task finishes. + * + * @param source stack to copy into the borrowed instance + * @return a stack, possibly reused from a previous frame + */ + public eu.svjatoslav.aukio.e3d.math.TransformStack borrowStack( + final eu.svjatoslav.aukio.e3d.math.TransformStack source) { + eu.svjatoslav.aukio.e3d.math.TransformStack stack = STACK_POOL.poll(); + if (stack == null) + stack = new eu.svjatoslav.aukio.e3d.math.TransformStack(); + stack.loadFrom(source); + return stack; + } + + /** + * Returns a borrowed stack to the pool. The caller must not touch + * it afterwards. + * + * @param stack the borrowed stack + */ + public void returnStack( + final eu.svjatoslav.aukio.e3d.math.TransformStack stack) { + STACK_POOL.offer(stack); + } + + /** + * Borrows a pooled chunk aggregator (reset, but with its queue + * capacity intact from previous frames). + * + * @return an aggregator, possibly reused from a previous frame + */ + public RenderAggregator borrowAggregator() { + final RenderAggregator aggregator = AGGREGATOR_POOL.poll(); + return aggregator != null ? aggregator : new RenderAggregator(); + } + + /** + * Frame parity captured at construction, stamped onto recorded + * timeline intervals so overlapping frames keep distinct colors. + */ + private final int traceParity = eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.frameParity(); + + /** + * Frame-wide cap on chunk tasks. Deep hierarchies of heavy composites + * would otherwise fork geometrically (each fork targets cores*4 tasks); + * past the cap, composites transform serially inline. The cap is + * generous enough that realistic scenes never hit it. + */ + private final int maxTasks = Runtime.getRuntime().availableProcessors() * 64; + + public ParallelTransformCoordinator(final ExecutorService executor) { + this.executor = executor; + } + + /** + * Reserves budget for {@code count} chunk tasks. Returns false when the + * frame-wide cap would be exceeded; the caller must then transform + * serially instead of forking. + * + * @param count number of chunk tasks the caller intends to submit + * @return true when the reservation was granted + */ + public boolean tryReserveTasks(final int count) { + if (submittedTaskCount.addAndGet(count) > maxTasks) { + submittedTaskCount.addAndGet(-count); + return false; + } + return true; + } + + /** + * Submits one chunk task. May be called from the orchestrating thread + * (root fork) or from inside a running chunk task (nested fork). + * Callers must have reserved budget via {@link #tryReserveTasks(int)}. + * + * @param task transforms a chunk of children into a private aggregator + */ + public void submit(final Callable task) { + if (eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.isEnabled()) { + final int parity = traceParity; + futures.add(executor.submit(() -> { + final long t0 = System.nanoTime(); + try { + return task.call(); + } finally { + eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record( + eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.KIND_TRANSFORM + parity, + t0, System.nanoTime()); + } + })); + return; + } + futures.add(executor.submit(task)); + } + + /** + * Total chunk tasks submitted to this coordinator, across all nesting + * levels. Diagnostics for tests and profiling. + * + * @return number of submitted chunk tasks + */ + public int getSubmittedTaskCount() { + return submittedTaskCount.get(); + } + + /** + * Waits for all submitted chunk tasks, including tasks submitted by + * other tasks (nested forks), and merges their aggregators into + * {@code target}. Must be called from the single orchestrating render + * thread after the root composite's {@code transform()} returns. + * + *

Termination is guaranteed: a task enqueues all its own submissions + * before completing, so once every future polled so far has completed + * and the queue is empty, no further submissions can arrive.

+ * + * @param target the root aggregator to merge results into + */ + public void drainAndMergeInto(final RenderAggregator target) { + final boolean trace = eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.isEnabled(); + final java.util.List parts = new java.util.ArrayList<>(); + Future future; + while ((future = futures.poll()) != null) { + try { + final long t0 = trace ? System.nanoTime() : 0; + parts.add(future.get()); + if (trace) { + eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record( + eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.KIND_AWAIT, + t0, System.nanoTime()); + } + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted during parallel transform", e); + } catch (final ExecutionException e) { + throw new RuntimeException("Parallel transform task failed", e.getCause()); + } + } + target.mergeAllParallel(parts, executor); + // Return chunk aggregators to the pool: reset() keeps their + // queue capacity, so next frame's chunks start at steady-state + // size instead of re-growing by doubling copies. + for (final RenderAggregator part : parts) { + part.reset(); + AGGREGATOR_POOL.offer(part); + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSort.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSort.java new file mode 100644 index 0000000..f8701ba --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSort.java @@ -0,0 +1,220 @@ +package eu.svjatoslav.aukio.e3d.renderer.raster; + +/** + * Stable LSD radix sort over parallel (key, index) arrays, plus the + * Z-to-sortable-key mapping used by {@link RenderAggregator}'s fast sort + * path. Byte-wise LSD passes make the sort stable, so equal keys keep + * their original relative order; the caller resolves remaining ties + * explicitly (by shapeId) afterwards. + * + *

Keys are interpreted as unsigned 64-bit values (digit extraction is + * unsigned); {@link #zSortKey(double)} returns keys pre-biased so that + * unsigned key order equals the comparator's paint order.

+ */ +final class RadixLongSort { + + private RadixLongSort() { + } + + /** + * Maps a camera-space Z (already depth-bias adjusted) to a 64-bit key + * whose unsigned order is DESCENDING in z — i.e. the painter's + * back-to-front order. Matches the comparator's contract: + * + *
    + *
  • {@code z1 > z2} (double semantics) iff key1 unsigned< key2;
  • + *
  • {@code -0.0} is normalized to {@code +0.0} — the comparator + * compares with {@code <}/{@code >}, which treats them equal, + * so they must share one key;
  • + *
  • NaN (never expected: queued shapes passed the near-plane + * cull) maps to one canonical key, making all-NaN ties resolve + * deterministically by shapeId.
  • + *
+ */ + static long zSortKey(final double zIn) { + double z = zIn; + if (z == 0.0d) + z = 0.0d; // normalize -0.0 -> +0.0 + long bits = Double.doubleToRawLongBits(z); + // IEEE 754 -> monotone signed long (negatives flip magnitude bits) + bits ^= (bits >> 63) & 0x7fffffffffffffffL; + // descending order + unsigned-digit friendliness + return ~bits ^ Long.MIN_VALUE; + } + + /** + * Ascending-Z variant for z-buffer mode: the opaque pass paints + * front-to-back for early-z rejection (correctness comes from the + * depth test, so order is purely a performance heuristic). Ties map + * to equal keys exactly like {@link #zSortKey}. + */ + static long zSortKeyAscending(final double zIn) { + double z = zIn; + if (z == 0.0d) + z = 0.0d; + long bits = Double.doubleToRawLongBits(z); + bits ^= (bits >> 63) & 0x7fffffffffffffffL; + return bits ^ Long.MIN_VALUE; + } + + /** + * Stable LSD radix sort of pairs {@code (keys[i], idx[i])}, ascending + * by unsigned key interpretation. Eight 8-bit counting passes; the + * even pass count leaves the result back in {@code keys}/{@code idx} + * (no final copy). Scratch arrays must be at least n long. + */ + static void sortPairs(final long[] keys, final int[] idx, final int n, + final long[] keyTmp, final int[] idxTmp) { + long[] srcK = keys; + long[] dstK = keyTmp; + int[] srcI = idx; + int[] dstI = idxTmp; + final int[] count = new int[256]; + for (int shift = 0; shift < 64; shift += 8) { + java.util.Arrays.fill(count, 0); + for (int i = 0; i < n; i++) + count[(int) ((srcK[i] >>> shift) & 0xFF)]++; + int sum = 0; + for (int d = 0; d < 256; d++) { + final int c = count[d]; + count[d] = sum; + sum += c; + } + for (int i = 0; i < n; i++) { + final int d = (int) ((srcK[i] >>> shift) & 0xFF); + final int p = count[d]++; + dstK[p] = srcK[i]; + dstI[p] = srcI[i]; + } + final long[] tk = srcK; + srcK = dstK; + dstK = tk; + final int[] ti = srcI; + srcI = dstI; + dstI = ti; + } + // 8 passes: after the final swap the sorted pairs are back in the + // caller's keys/idx arrays. + } + + /** + * Parallel variant of {@link #sortPairs}: each pass builds per-chunk + * histograms concurrently, then computes scatter offsets digit-major / + * chunk-minor (chunk t's elements precede chunk t+1's within a digit), + * then scatters per chunk concurrently. That offset order preserves + * LSD stability exactly, so the result is IDENTICAL to the serial + * sort for any chunk count — the work partitioning is invisible to + * the output. Tasks are recorded on the thread-activity timeline like + * the rest of the sort machinery. + * + * @param histScratch scratch for per-chunk histograms and scatter + * offsets, length ≥ {@code threads * 512} + * @param threads chunk count (1 = fall back to the serial sort) + */ + static void sortPairsParallel(final long[] keys, final int[] idx, final int n, + final long[] keyTmp, final int[] idxTmp, + final int[] histScratch, + final java.util.concurrent.ExecutorService executor, + final int threads) { + if (threads <= 1 || executor == null) { + sortPairs(keys, idx, n, keyTmp, idxTmp); + return; + } + long[] srcK = keys; + long[] dstK = keyTmp; + int[] srcI = idx; + int[] dstI = idxTmp; + final int chunk = (n + threads - 1) / threads; + for (int shift = 0; shift < 64; shift += 8) { + final int s = shift; + final long[] sk = srcK; + final long[] dk = dstK; + final int[] si = srcI; + final int[] di = dstI; + + // Phase A: per-chunk histograms (concurrent) + runChunks(executor, n, chunk, (t, from, to) -> { + java.util.Arrays.fill(histScratch, t * 256, t * 256 + 256, 0); + for (int i = from; i < to; i++) + histScratch[t * 256 + (int) ((sk[i] >>> s) & 0xFF)]++; + }); + + // Serial combine: digit-major, chunk-minor offsets — this is + // what keeps the parallel sort stable and bit-identical to + // the serial one. + int pos = 0; + final int offsetsBase = threads * 256; + for (int d = 0; d < 256; d++) + for (int t = 0; t < threads; t++) { + final int c = histScratch[t * 256 + d]; + histScratch[offsetsBase + t * 256 + d] = pos; + pos += c; + } + + // Phase B: per-chunk scatter (concurrent; each chunk owns its + // private offset row) + runChunks(executor, n, chunk, (t, from, to) -> { + final int base = offsetsBase + t * 256; + for (int i = from; i < to; i++) { + final int d = (int) ((sk[i] >>> s) & 0xFF); + final int p = histScratch[base + d]++; + dk[p] = sk[i]; + di[p] = si[i]; + } + }); + + final long[] tk = srcK; + srcK = dstK; + dstK = tk; + final int[] ti = srcI; + srcI = dstI; + dstI = ti; + } + } + + /** One unit of chunk work: chunk index t and its [from, to) range. */ + interface ChunkWork { + void run(int t, int from, int to); + } + + /** + * Runs {@code work} for every non-empty chunk concurrently on + * {@code executor} and awaits completion; each task is recorded as + * KIND_SORT on the thread-activity timeline. Package-visible: the + * aggregator reuses it for the parallel key-build and permute. + */ + static void runChunks(final java.util.concurrent.ExecutorService executor, + final int n, final int chunk, + final ChunkWork work) { + final java.util.List> futures = + new java.util.ArrayList<>(); + for (int t = 0, from = 0; from < n; t++, from += chunk) { + final int ti = t; + final int f = from; + final int to = Math.min(n, from + chunk); + futures.add(executor.submit(() -> { + final boolean trace = + eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.isEnabled(); + final long t0 = trace ? System.nanoTime() : 0; + try { + work.run(ti, f, to); + } finally { + if (trace) + eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record( + eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.KIND_SORT, + t0, System.nanoTime()); + } + })); + } + try { + for (final java.util.concurrent.Future future : futures) + future.get(); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted during parallel radix sort", e); + } catch (final java.util.concurrent.ExecutionException e) { + throw new RuntimeException("Task failed during parallel radix sort", + e.getCause()); + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderAggregator.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderAggregator.java new file mode 100644 index 0000000..1e9ab17 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderAggregator.java @@ -0,0 +1,803 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster; + +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +/** + * Collects transformed shapes during a render frame and paints them in depth-sorted order. + * + *

Shapes are sorted from back to front (highest Z-depth first). Under the + * (unconditional) z-buffer, occlusion correctness comes from the per-pixel + * depth test; the queue order is a performance and coherence heuristic: + * pass 1 paints opaque shapes front-to-back (queue reversed) so the depth + * test rejects hidden fragments before the texture fetch, pass 2 paints + * alpha shapes back-to-front without depth writes so translucent overlap + * stays painter-coherent.

+ * + *

When two shapes have the same Z-depth, their unique {@link AbstractCoordinateShape#shapeId} + * is used as a tiebreaker to guarantee deterministic rendering order.

+ * + *

This class is used internally by {@link ShapeCollection} during the render pipeline. + * You typically do not need to interact with it directly.

+ * + * @see ShapeCollection#paintShapes(RenderingContext) + * @see AbstractCoordinateShape#getZ(int) + */ +public class RenderAggregator { + + /** + * Creates a new render aggregator. + */ + public RenderAggregator() { + this(0); + } + + /** + * Creates an aggregator bound to a projection buffer slot. Sorting and + * tile binning read the shapes' screen state for this slot, so the + * triple-buffered pipeline can fill one slot's aggregator while the + * other slots' aggregators are still being painted. + * + * @param slot the buffer slot (0..2) this aggregator serves + */ + public RenderAggregator(final int slot) { + this.slot = slot; + } + + /** Buffer slot whose screen state this aggregator sorts and bins by. */ + private final int slot; + + private final ArrayList shapes = new ArrayList<>(); + private final ShapesZIndexComparator comparator = new ShapesZIndexComparator(); + private boolean sorted = false; + + /** + * The sorted queue as a flat array, produced by {@link #sort()}. + * Paint and binning iterate this instead of the list: sorting works + * on the array in place, so the queue is copied exactly once. + * The backing array is REUSED across frames (grow-only); only the + * first {@link #sortedCount} entries are valid. + */ + private AbstractCoordinateShape[] sortedArray; + /** Valid entries of {@link #sortedArray} (it is oversized by reuse). */ + private int sortedCount; + + /** + * Reusable flat queue storage (grow-only). {@link #sort()} fills it + * either from the shape list or from {@link #mergeAllParallel}. + */ + private AbstractCoordinateShape[] queueArray; + /** Valid entries of {@link #queueArray} after a merge. */ + private int pendingMergeCount; + + /** Reusable permutation scratch (grow-only), see tryRadixSort. */ + private AbstractCoordinateShape[] sortScratch; + + /** Radix-sort scratch (grow-only): keys, indices, and their swap + * buffers, plus the tie-run pack array. See tryRadixSort. */ + private long[] radixKeys; + private long[] radixKeysTmp; + private long[] tiePack; + private int[] radixIdx; + private int[] radixIdxTmp; + /** Per-chunk histogram/offset scratch for the parallel radix passes. */ + private int[] radixHist; + + private static long[] ensureCapacity(final long[] array, final int capacity) { + return (array != null && array.length >= capacity) + ? array : new long[capacity]; + } + + private static int[] ensureCapacity(final int[] array, final int capacity) { + return (array != null && array.length >= capacity) + ? array : new int[capacity]; + } + + /** Grow-only capacity helper: returns {@code array} or a bigger one. */ + private static AbstractCoordinateShape[] ensureCapacity( + final AbstractCoordinateShape[] array, final int capacity) { + return array != null && array.length >= capacity + ? array : new AbstractCoordinateShape[capacity]; + } + + /** + * Sorts all queued shapes by Z-depth (back to front) and paints them. + * + * @param renderBuffer the rendering context to paint shapes into + */ + public void paint(final RenderingContext renderBuffer) { + ensureSorted(); + paintSorted(renderBuffer); + } + + /** + * Above this many queued shapes, {@link #sort()} uses the radix path + * (parallel when an executor is available) instead of a comparator sort. + */ + private static final int PARALLEL_SORT_THRESHOLD = 8192; + + /** + * Sorts all queued shapes by Z-depth (back to front). + * Must be called after all shapes are queued and before paintSorted. + * Uses a parallel sort for large queues. + */ + public void sort() { + sort(null); + } + + /** + * Sorts the queue by (Z, shapeId). Large queues go through the radix + * path (see {@link #tryRadixSort}), which is parallel when an executor + * is given; small queues use a plain comparator sort. Deterministic: + * (Z, shapeId) is a total order, so every path yields the same result. + * + * @param executor executor for parallel sorting, or null for serial + */ + public void sort(final ExecutorService executor) { + if (!sorted) { + comparator.sortSlot = slot; + if (pendingMergeCount > 0) { + // Merge already produced a flat array: sort it in place, + // no list copy at all + sortedArray = queueArray; + sortedCount = pendingMergeCount; + pendingMergeCount = 0; + } else { + // toArray(target) reuses the target when it fits: + // zero-allocation queue copy at steady state + sortedCount = shapes.size(); + sortedArray = shapes.toArray( + ensureCapacity(queueArray, sortedCount)); + queueArray = sortedArray; + } + if (sortedCount >= PARALLEL_SORT_THRESHOLD) { + tryRadixSort(sortedArray, sortedCount, executor); + } else { + Arrays.sort(sortedArray, 0, sortedCount, comparator); + } + sorted = true; + } + } + + /** + * Fast sort: maps Z-depth to unsigned-ordered long keys, stable-sorts + * (key, queueIndex) pairs with an LSD radix sort, then fixes equal-key + * runs to ascending shapeId — reproducing the comparator's total order + * (Z descending, shapeId ascending) exactly, without a single + * comparator call. Sequential memory throughout: key build and the + * final permute stream the queue array, the radix passes stream + * long/int arrays. With an executor the key build, radix passes and + * permute run chunked in parallel (bit-identical to serial: stability + * makes the partitioning invisible). + */ + private void tryRadixSort(final AbstractCoordinateShape[] array, + final int length, + final ExecutorService executor) { + radixKeys = ensureCapacity(radixKeys, length); + radixKeysTmp = ensureCapacity(radixKeysTmp, length); + radixIdx = ensureCapacity(radixIdx, length); + radixIdxTmp = ensureCapacity(radixIdxTmp, length); + + // Sweet spot measured on a 24-core desktop (450k pairs, SortSweep + // harness): ~16-24 chunks; below ~4 chunks bandwidth stays + // underutilized, and every chunk costs 2 task submissions per pass. + final int threads = executor == null ? 1 + : Math.max(1, Math.min( + Runtime.getRuntime().availableProcessors(), + length / 16384)); + + final long[] keys = radixKeys; + final int[] idx = radixIdx; + if (threads > 1) { + RadixLongSort.runChunks(executor, length, (length + threads - 1) / threads, + (t, from, to) -> { + for (int i = from; i < to; i++) { + keys[i] = RadixLongSort.zSortKey(array[i].getZ(slot)); + idx[i] = i; + } + }); + } else { + for (int i = 0; i < length; i++) { + radixKeys[i] = RadixLongSort.zSortKey(array[i].getZ(slot)); + radixIdx[i] = i; + } + } + + if (threads > 1) { + radixHist = ensureCapacity(radixHist, threads * 512); + RadixLongSort.sortPairsParallel(radixKeys, radixIdx, length, + radixKeysTmp, radixIdxTmp, radixHist, executor, threads); + } else { + RadixLongSort.sortPairs(radixKeys, radixIdx, length, + radixKeysTmp, radixIdxTmp); + } + + // Equal-key runs must resolve by ascending shapeId (the + // comparator's tie-break; queue order is NOT construction order). + // Runs are almost always singletons — the pack array only + // materializes for actual ties. + int runStart = 0; + while (runStart < length) { + int runEnd = runStart + 1; + final long key = radixKeys[runStart]; + while (runEnd < length && radixKeys[runEnd] == key) + runEnd++; + if (runEnd - runStart > 1) { + final int runLength = runEnd - runStart; + tiePack = ensureCapacity(tiePack, runLength); + for (int i = 0; i < runLength; i++) + tiePack[i] = + ((array[radixIdx[runStart + i]].shapeId + & 0xffffffffL) << 32) + | (radixIdx[runStart + i] & 0xffffffffL); + java.util.Arrays.sort(tiePack, 0, runLength); + for (int i = 0; i < runLength; i++) + radixIdx[runStart + i] = (int) tiePack[i]; + } + runStart = runEnd; + } + sortScratch = ensureCapacity(sortScratch, length); + final AbstractCoordinateShape[] scratch = sortScratch; + if (threads > 1) { + RadixLongSort.runChunks(executor, length, (length + threads - 1) / threads, + (t, from, to) -> { + for (int i = from; i < to; i++) + scratch[i] = array[radixIdx[i]]; + }); + } else { + for (int i = 0; i < length; i++) + sortScratch[i] = array[radixIdx[i]]; + } + System.arraycopy(sortScratch, 0, array, 0, length); + } + + private static void awaitAll(final java.util.List> futures, final String what) { + try { + for (final Future future : futures) + future.get(); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted during " + what, e); + } catch (final ExecutionException e) { + throw new RuntimeException("Task failed during " + what, e.getCause()); + } + } + + private void ensureSorted() { + sort(); + } + + /** + * Paints all shapes that have already been sorted. + * This method can be called multiple times with different segment contexts + * for multi-threaded rendering. + * + *

If {@link #binForTiles} was called and the given context's bounds + * exactly match one tile, only that tile's bin is iterated — shapes + * that cannot touch the tile are skipped entirely. Otherwise the full + * sorted queue is iterated (every shape clips itself to the context + * bounds, so the painted result is identical).

+ * + * @param renderBuffer the rendering context to paint shapes into + */ + public void paintSorted(final RenderingContext renderBuffer) { + // Two passes. Opaque class first, front-to-back (the queue is + // back-to-front, so iterate it in reverse) with depth test + + // write — early-z rejects hidden surfaces before texturing. + // Alpha class second, in queue order (back-to-front): + // depth-tested against the opaque result but never + // depth-written, so cutout foliage keeps painter-coherent + // overlap among itself. + renderBuffer.depthPass = 1; + paintRange(renderBuffer, true); + renderBuffer.depthPass = 2; + paintRange(renderBuffer, false); + renderBuffer.depthPass = 0; + } + + /** Iterates the matching bin (or the full sorted queue), optionally + * in reverse. */ + private void paintRange(final RenderingContext renderBuffer, + final boolean reverse) { + final int bin = matchingBinIndex(renderBuffer); + if (bin >= 0) { + final int start = binStart[bin]; + final int end = start + binCount[bin]; + if (reverse) { + for (int i = end - 1; i >= start; i--) + binEntries[i].paint(renderBuffer); + } else { + for (int i = start; i < end; i++) + binEntries[i].paint(renderBuffer); + } + return; + } + if (sortedArray != null) { + if (reverse) { + for (int i = sortedCount - 1; i >= 0; i--) + sortedArray[i].paint(renderBuffer); + } else { + for (int i = 0; i < sortedCount; i++) + sortedArray[i].paint(renderBuffer); + } + } else { + for (int i = 0; i < shapes.size(); i++) + shapes.get(i).paint(renderBuffer); + } + } + + /** + * Tile bins in CSR (compressed-sparse-row) form: one flat, + * grow-only entry array plus per-tile start/count. Built fresh + * each frame into REUSED arrays — the previous design allocated a + * fresh ArrayList per tile per chunk per frame (thousands of list + * objects plus their backing arrays at FO4 queue sizes). + */ + private AbstractCoordinateShape[] binEntries; + private int[] binStart; + private int[] binCount; + /** Per-(chunk × tile) scratch: counts in phase 1, write offsets in phase 3. */ + private int[] binScratch; + private int binTilesX; + private int binTilesY; + private int binOriginX; + private int binTileW; + private int binTileH; + private int binWidth; + private int binHeight; + private boolean binsActive; + + /** + * Returns the bin matching the given context's X/Y bounds, or -1 + * when the context does not correspond to exactly one binned tile. + * + * @param renderBuffer the rendering context to match + * @return the bin index, or -1 for full-queue iteration + */ + private int matchingBinIndex(final RenderingContext renderBuffer) { + if (!binsActive) + return -1; + final int tx = matchAxis(renderBuffer.renderMinX, renderBuffer.renderMaxX, + binOriginX, binTileW, binTilesX, binWidth); + if (tx < 0) + return -1; + final int ty = matchAxis(renderBuffer.renderMinY, renderBuffer.renderMaxY, + 0, binTileH, binTilesY, binHeight); + if (ty < 0) + return -1; + return ty * binTilesX + tx; + } + + /** + * Matches one axis of a context against the tile grid. + * + * @param min context minimum coordinate on this axis + * @param max context maximum coordinate (exclusive) on this axis + * @param origin grid origin on this axis + * @param tileSize tile size on this axis + * @param count tile count on this axis + * @param total total grid extent on this axis (last tile reaches it) + * @return the tile index on this axis, or -1 when no exact match + */ + private static int matchAxis(final int min, final int max, final int origin, + final int tileSize, final int count, final int total) { + final int rel = min - origin; + if (rel < 0 || rel % tileSize != 0) + return -1; + final int index = rel / tileSize; + if (index >= count) + return -1; + final int expectedMax = (index == count - 1) + ? origin + total : origin + (index + 1) * tileSize; + return max == expectedMax ? index : -1; + } + + /** + * Below this many queued shapes the bin build runs serially; the + * fork/join overhead would dominate. + */ + private static final int PARALLEL_BIN_MIN_SHAPES = 8192; + + /** + * Bins the sorted queue per rectangular paint tile by screen-space + * overlap. Must be called after {@link #sort()} and before tile + * painting. Built once per frame (per eye, in stereo) on the render + * thread; the bins are read-only during parallel painting. + * + *

Each bin preserves the global (Z, shapeId) sort order, so painting + * a bin produces exactly the same pixels as painting the full queue + * into that tile. Shapes are assigned with their + * {@link AbstractCoordinateShape#onScreenMinY} / {@code onScreenMaxY} / + * {@code onScreenMinX} / {@code onScreenMaxX} bounds, which include a + * per-shape margin for paint output extending past the vertices + * (thick lines, billboards, text glyphs).

+ * + *

Mouse hit detection is unaffected: a hit requires the cursor to be + * inside the shape, so the shape always overlaps the tile containing + * the cursor.

+ * + *

When an executor is given and the queue is large, the sorted list + * is scanned in per-core chunks concurrently and the per-chunk bins are + * concatenated in chunk order, preserving the global sort order.

+ * + * @param tilesX tile columns across the viewport + * @param tilesY tile rows down the viewport + * @param originX X origin of the tiled viewport (eye offset in stereo) + * @param width tiled viewport width in pixels + * @param height full render height in pixels + * @param executor executor for parallel binning, or null for serial + */ + public void binForTiles(final int tilesX, final int tilesY, + final int originX, final int width, final int height, + final ExecutorService executor) { + ensureSorted(); + + binsActive = false; + final int tileW = width / tilesX; + final int tileH = height / tilesY; + if (tilesX < 1 || tilesY < 1 || tileW <= 0 || tileH <= 0 + || sortedCount == 0) + return; + + buildBins(tilesX, tilesY, originX, tileW, tileH, + tilesX * tilesY, executor); + + binsActive = true; + binTilesX = tilesX; + binTilesY = tilesY; + binOriginX = originX; + binTileW = tileW; + binTileH = tileH; + binWidth = width; + binHeight = height; + } + + /** + * Floor of {@code value} as an int. The {@code (int)} cast truncates + * toward zero; decrement when the truncated value overshoots to get + * floor semantics for negatives. + */ + private static int floorInt(final double value) { + final int result = (int) value; + return result > value ? result - 1 : result; + } + + /** + * Builds the CSR bins in three phases over grow-only reused arrays: + * per-chunk counting (parallel), a tiny serial prefix pass, then + * per-chunk fill (parallel). Per-bin order is preserved exactly as + * with the old per-chunk-ArrayList concatenation: chunks cut the + * sorted queue contiguously and each bin's slices are laid down in + * chunk order. + */ + private void buildBins(final int tilesX, final int tilesY, + final int originX, final int tileW, final int tileH, + final int tileCount, + final ExecutorService executor) { + final AbstractCoordinateShape[] queue = sortedArray; + final int size = sortedCount; + final int targetTasks = Runtime.getRuntime().availableProcessors() * 4; + final int chunkSize = Math.max(1024, (size + targetTasks - 1) / targetTasks); + final int chunkCount = (size + chunkSize - 1) / chunkSize; + final double invTileW = 1.0 / tileW; + final double invTileH = 1.0 / tileH; + + if (binStart == null || binStart.length < tileCount) { + binStart = new int[tileCount]; + binCount = new int[tileCount]; + } + if (binScratch == null || binScratch.length < chunkCount * tileCount) { + binScratch = new int[chunkCount * tileCount]; + } else { + java.util.Arrays.fill(binScratch, 0, chunkCount * tileCount, 0); + } + + final boolean parallel = executor != null + && size >= PARALLEL_BIN_MIN_SHAPES && chunkCount > 1; + + // Phase 1: count shapes per (chunk, tile) + binPhase(queue, tilesX, tilesY, originX, invTileW, invTileH, + chunkCount, chunkSize, tileCount, true, parallel, executor); + + // Phase 2 (serial, tiny): per-tile prefix over chunks -> each + // chunk's write offset within the tile; per-tile totals -> + // binStart/binCount; overall capacity for the flat entries array + int total = 0; + for (int t = 0; t < tileCount; t++) { + int tileTotal = 0; + for (int c = 0; c < chunkCount; c++) { + final int idx = c * tileCount + t; + final int n = binScratch[idx]; + binScratch[idx] = tileTotal; + tileTotal += n; + } + binStart[t] = total; + binCount[t] = tileTotal; + total += tileTotal; + } + binEntries = ensureCapacity(binEntries, total); + + // Phase 3: fill; each chunk bumps only its own scratch row + binPhase(queue, tilesX, tilesY, originX, invTileW, invTileH, + chunkCount, chunkSize, tileCount, false, parallel, executor); + } + + /** Runs one bin phase over all chunks, inline or on the executor. */ + private void binPhase(final AbstractCoordinateShape[] queue, + final int tilesX, final int tilesY, + final int originX, + final double invTileW, final double invTileH, + final int chunkCount, final int chunkSize, + final int tileCount, + final boolean countMode, + final boolean parallel, + final ExecutorService executor) { + final int size = sortedCount; + final boolean trace = eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.isEnabled(); + final int traceKind = eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.KIND_BIN + + eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.frameParity(); + final java.util.List> futures = + parallel ? new java.util.ArrayList<>(chunkCount) : null; + for (int c = 0; c < chunkCount; c++) { + final int chunk = c; + final int from = c * chunkSize; + final int to = Math.min(size, from + chunkSize); + final Runnable body = () -> { + final long t0 = trace ? System.nanoTime() : 0; + try { + binRangeCsr(queue, tilesX, tilesY, originX, + invTileW, invTileH, from, to, + chunk * tileCount, countMode); + } finally { + if (trace) + eu.svjatoslav.aukio.e3d.diag.ThreadActivityRecorder.record( + traceKind, t0, System.nanoTime()); + } + }; + if (parallel) + futures.add(executor.submit(body)); + else + body.run(); + } + if (parallel) + awaitAll(futures, "parallel binning"); + } + + /** + * Assigns shapes[from..to) to tile bins by X/Y overlap. In count + * mode, increments binScratch per (chunk, tile); in fill mode, + * writes entries at binStart[tile] + running chunk offset. Both + * modes iterate in queue order, preserving (Z, shapeId) within bins. + */ + private void binRangeCsr(final AbstractCoordinateShape[] queue, + final int tilesX, final int tilesY, + final int originX, + final double invTileW, final double invTileH, + final int from, final int to, + final int scratchBase, + final boolean countMode) { + for (int i = from; i < to; i++) { + final AbstractCoordinateShape shape = queue[i]; + + int firstY = floorInt(shape.onScreenMinY(slot) * invTileH); + int lastY = floorInt(shape.onScreenMaxY(slot) * invTileH); + if (lastY < 0 || firstY >= tilesY) + continue; + if (firstY < 0) + firstY = 0; + if (lastY >= tilesY) + lastY = tilesY - 1; + + int firstX = floorInt((shape.onScreenMinX(slot) - originX) * invTileW); + int lastX = floorInt((shape.onScreenMaxX(slot) - originX) * invTileW); + if (lastX < 0 || firstX >= tilesX) + continue; + if (firstX < 0) + firstX = 0; + if (lastX >= tilesX) + lastX = tilesX - 1; + + for (int ty = firstY; ty <= lastY; ty++) { + final int rowBase = ty * tilesX; + for (int tx = firstX; tx <= lastX; tx++) { + final int tile = rowBase + tx; + if (countMode) { + binScratch[scratchBase + tile]++; + } else { + binEntries[binStart[tile] + + binScratch[scratchBase + tile]++] = shape; + } + } + } + } + } + + /** + * Returns the number of shapes currently queued. + * + * @return the shape count + */ + public int size() { + if (sortedArray != null) + return sortedCount; + if (pendingMergeCount > 0) + return pendingMergeCount; + return shapes.size(); + } + + /** + * Queues a shape for rendering. Called during the transform phase. + * + * @param shape the shape to queue + */ + public void queueShapeForRendering(final AbstractCoordinateShape shape) { + shapes.add(shape); + binsActive = false; + } + + /** + * Merges all shapes queued in another aggregator into this one. + * Used to combine the per-task queues produced by the parallel + * transform phase. Merge order does not affect the final render order: + * {@link #sort()} is deterministic on (Z, shapeId). + * + * @param other the aggregator whose queued shapes are moved into this one + */ + public void mergeFrom(final RenderAggregator other) { + shapes.addAll(other.shapes); + sorted = false; + binsActive = false; + } + + /** + * Merges many chunk aggregators into this one, producing a flat + * array that {@link #sort()} consumes directly. The per-chunk lists + * are copied into the merged array by parallel copy tasks on the + * given executor — the old per-chunk {@code addAll} chain + * (reallocating the target list serially) is gone. Merge order is + * irrelevant: the following sort re-establishes deterministic + * (Z, shapeId) order. + * + * @param parts chunk aggregators to merge + * @param executor executor for the parallel copy, or null for serial + */ + public void mergeAllParallel(final List parts, + final ExecutorService executor) { + final int ownSize = shapes.size(); + int total = ownSize; + for (final RenderAggregator part : parts) + total += part.shapes.size(); + + // Reused flat storage (grow-only); lists are copied out with + // plain indexed loops — no per-part toArray copies + queueArray = ensureCapacity(queueArray, total); + final AbstractCoordinateShape[] merged = queueArray; + final int partCount = parts.size(); + final int[] offsets = new int[partCount + 1]; + offsets[0] = ownSize; + for (int i = 0; i < partCount; i++) + offsets[i + 1] = offsets[i] + parts.get(i).shapes.size(); + + for (int j = 0; j < ownSize; j++) + merged[j] = shapes.get(j); + shapes.clear(); + + if (executor == null || partCount < 2 || total < 8192) { + for (int i = 0; i < partCount; i++) { + final ArrayList partShapes = parts.get(i).shapes; + for (int j = 0; j < partShapes.size(); j++) + merged[offsets[i] + j] = partShapes.get(j); + } + } else { + final int copyTasks = Math.min(partCount, + Runtime.getRuntime().availableProcessors()); + final int perTask = (partCount + copyTasks - 1) / copyTasks; + final List> futures = new ArrayList<>(copyTasks); + for (int t = 0; t < copyTasks; t++) { + final int from = t * perTask; + final int to = Math.min(partCount, from + perTask); + if (from >= to) + break; + futures.add(executor.submit(() -> { + for (int i = from; i < to; i++) { + final ArrayList partShapes = + parts.get(i).shapes; + final int partSize = partShapes.size(); + for (int j = 0; j < partSize; j++) + merged[offsets[i] + j] = partShapes.get(j); + } + })); + } + try { + for (final Future future : futures) + future.get(); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted during parallel merge", e); + } catch (final ExecutionException e) { + throw new RuntimeException("Parallel merge task failed", e.getCause()); + } + } + + pendingMergeCount = total; + sorted = false; + binsActive = false; + } + + /** + * Returns the live list of queued shapes, in current queue order. + * Package-private: exposed for pipeline verification tests. + * + * @return the queued shapes + */ + List getQueuedShapes() { + if (sortedArray != null) + return java.util.Collections.unmodifiableList( + Arrays.asList(sortedArray).subList(0, sortedCount)); + if (pendingMergeCount > 0) + return java.util.Collections.unmodifiableList( + Arrays.asList(queueArray).subList(0, pendingMergeCount)); + return shapes; + } + + /** + * Returns the number of shapes in each segment bin, or null when no + * binning is active. Package-private: exposed for pipeline + * verification tests. + * + * @return per-segment bin sizes, or null + */ + int[] getBinSizes() { + if (!binsActive) + return null; + return Arrays.copyOf(binCount, binTilesX * binTilesY); + } + + /** + * Clears all queued shapes, preparing for a new render frame. The + * reusable backing arrays (queue, sort scratch, bin storage) are + * kept, so steady-state frames do not reallocate them. + */ + public void reset() { + shapes.clear(); + sortedArray = null; + sortedCount = 0; + pendingMergeCount = 0; + sorted = false; + binsActive = false; + } + + /** + * Comparator that sorts shapes by Z-depth in descending order (farthest first) + * for the render queue. Uses shape ID as a tiebreaker. + */ + static class ShapesZIndexComparator implements Comparator, Serializable { + + /** Buffer slot the in-progress sort reads Z-depths from. */ + int sortSlot; + + @Override + public int compare(final AbstractCoordinateShape o1, final AbstractCoordinateShape o2) { + final double z1 = o1.getZ(sortSlot); + final double z2 = o2.getZ(sortSlot); + if (z1 < z2) + return 1; + else if (z1 > z2) + return -1; + + return Integer.compare(o1.shapeId, o2.shapeId); + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.java new file mode 100644 index 0000000..4ecf4d4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/RenderingContext.java @@ -0,0 +1,714 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster; +import eu.svjatoslav.aukio.e3d.diag.DebugLogBuffer; +import eu.svjatoslav.aukio.e3d.gui.DeveloperTools; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.gui.humaninput.KeyboardFocusStack; +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent; +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController; +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.awt.image.WritableRaster; +import java.util.concurrent.ExecutorService; +import eu.svjatoslav.aukio.e3d.renderer.raster.ParallelTransformCoordinator; +import java.util.function.Consumer; + +/** + * Contains all state needed to render a single frame: the pixel buffer, graphics context, + * screen dimensions, and mouse event tracking. + * + *

A new {@code RenderingContext} is created whenever the view panel is resized. + * During rendering, shapes use this context to:

+ *
    + *
  • Access the raw pixel array ({@link #pixels}) for direct pixel manipulation
  • + *
  • Access the {@link Graphics2D} context ({@link #graphics}) for Java2D drawing
  • + *
  • Read screen dimensions ({@link #width}, {@link #height}) and the + * {@link #centerCoordinate} for coordinate projection
  • + *
  • Use the {@link #projectionScale} factor for perspective projection
  • + *
+ * + *

The context also manages mouse interaction detection: as shapes are painted + * back-to-front, each shape can report itself as the object under the mouse cursor. + * After painting completes, the topmost shape receives the mouse event.

+ * + * @see eu.svjatoslav.aukio.e3d.gui.ViewPanel the panel that creates and manages this context + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape#paint(RenderingContext) + */ +public class RenderingContext { + + /** + * The {@link BufferedImage} pixel format used for the rendering buffer. + * TYPE_INT_RGB provides optimal performance for Java2D blitting. + */ + public static final int bufferedImageType = BufferedImage.TYPE_INT_RGB; + + /** + * Number of horizontal segments (bands) for parallel rendering. + * Bands are finer than the paint thread count: paint threads steal + * bands off a shared ticket until all bands are done, so a thread + * that finishes a cheap band immediately picks up more work. + * Derived from the render thread count via + * {@link eu.svjatoslav.aukio.e3d.gui.ViewPanel#setNumRenderThreads(int)}. + * + *

Equals {@code tilesX * tilesY * viewportCount}: the tile grid + * covers one viewport, and in stereo mode a second grid covers the + * other eye (segment indices for the right eye start at + * {@code tilesX * tilesY}).

+ */ + public final int numRenderSegments; + + /** Tile columns per viewport (1 = horizontal bands only). */ + public final int tilesX; + + /** Tile rows per viewport. */ + public final int tilesY; + + /** Number of side-by-side viewports (2 in stereo mode, else 1). */ + public final int viewportCount; + + /** + * 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}. + */ + public final int[] pixels; + + /** + * Per-pixel depth (biased 1/z, larger = nearer), always allocated — + * the painter path was deleted 2026-09-17 and the z-buffer is the + * only visibility mechanism. Shared with segment/pass copies like + * {@link #pixels}. Cleared per tile by the paint workers. + */ + public float[] depth; + + /** + * Active paint pass, set internally by + * {@code RenderAggregator.paintSorted}: 0 = not painting, 1 = + * opaque pass (opaque-class triangles only, depth test + write), + * 2 = alpha pass (alpha-carrying + * triangles only, depth test, no depth write). Shapes read it to + * decide whether they belong to the current pass. + */ + public int depthPass; + + /** + * Depth tolerance in world units for the z-buffer test, in the form + * {@code zw > stored - DEPTH_MARGIN_DZ * zw * zw} (tolerance behind + * stored, per-pixel at fragment depth). Default 0 = strict depth: any + * nonzero window exports per-triangle painter-sort errors into + * per-pixel occlusion errors (dirt whose triangles sort late beats + * road pavement that strictly wins at margin 0 — user bugreport + * 2026-09-16, road pose). Tunable via -Daukio.zbuffer.margin. + */ + public static final double DEPTH_MARGIN_DZ = + Double.parseDouble(System.getProperty("aukio.zbuffer.margin", "0")); + + /** + * Width of the rendering area in pixels. + */ + public final int width; + + /** + * Height of the rendering area in pixels. + */ + public final int height; + + /** + * Center of the screen in screen space (pixels). + * This is the point where (0,0) coordinate of the world space is rendered. + */ + public final Point2D centerCoordinate; + + /** + * Scale factor for perspective projection, derived from screen width. + * Used to convert normalized device coordinates to screen pixels. + * This is mutable to support stereo rendering where each eye has a different viewport width. + */ + public double projectionScale; + + /** + * Minimum Y coordinate (inclusive) to render. Used for multi-threaded rendering + * where each thread renders a horizontal segment. + */ + public final int renderMinY; + + /** + * Maximum Y coordinate (exclusive) to render. Used for multi-threaded rendering + * where each thread renders a horizontal segment. + */ + public final int renderMaxY; + + /** The backing image (public: the AWT shell in {@code gui} blits it directly). */ + public final BufferedImage bufferedImage; + /** + * Unique id of the current transform cycle, assigned by + * {@code ShapeCollection.transformShapes()} from a global counter. + * Unlike {@link #frameNumber} (per-context, can repeat across context + * instances), this never collides, so per-cycle memoization such as + * composite subtree weights can safely key on it. + */ + public long transformCycleId; + + /** + * Which projection buffer slot this context writes/reads: 0, 1 or 2. + * Cycles per render pass (per eye in stereo) when the + * triple-buffered pipeline is active, so the transform phase of a + * pass never overwrites the vertex state either of the two previous + * passes' paints may still be reading. Always 0 when the pipeline is + * off (tests, single-pass rendering). + */ + public int vertexSlot = 0; + + /** + * Near-plane distance in camera-space Z units. Polygons whose vertices + * straddle this plane are clipped against it (new intersection vertices + * are generated with interpolated UVs); polygons fully behind it are + * culled. Must be > 0 so the perspective divide stays safe. + */ + public double nearPlaneDistance = 1.0; + + /** + * Number of frame that is currently being rendered. + * Every frame has its own number. + */ + public int frameNumber = 0; + + /** + * Projected-size cull threshold in screen pixels: shapes whose screen + * bounds span less than this in both axes are not queued for + * rendering. 0 (the default) disables the cull. Set globally with + * {@code -Daukio.cull.subpixel=}. + */ + public double subpixelCullingThreshold = Double.parseDouble( + System.getProperty("aukio.cull.subpixel", "0")); + + /** + * Epoch of the subpixel-culling verdict cache, stamped per frame by + * {@code ShapeCollection.transformShapesBegin}: the epoch advances + * when the camera moves significantly (or after a bounded number of + * frames), which invalidates all cached skip verdicts and forces one + * re-evaluation pass. Meaningless when the cull is off. + */ + public int subpixelCullingEpoch; + + /** + * UI component that mouse is currently hovering over. + */ + private MouseInteractionController objectPreviouslyUnderMouseCursor; + /** + * Mouse click event that needs to be processed. + * This event is processed only once per frame. + * 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. + */ + private MouseEvent mouseEvent; + /** + * UI component that mouse is currently hovering over. + */ + private MouseInteractionController currentObjectUnderMouseCursor; + /** + * Texture coordinates of the mouse cursor on the hit shape (primary + * texture pixels), or NaN when the hit shape has no texture. + */ + private double currentMouseTextureU = Double.NaN; + private double currentMouseTextureV = Double.NaN; + /** + * Developer tools for this rendering context. + * Controls diagnostic features like logging and visualization. + */ + 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; + + /** + * Which eye is being rendered in stereo mode. NONE for normal single-view rendering. + */ + public StereoEye stereoEye = StereoEye.NONE; + + /** + * Width of the viewport for the current eye in stereo mode. + * Equals {@link #width} when not in stereo mode. + */ + public int stereoViewportWidth; + + /** + * X offset of the current eye's viewport within the full buffer. + * 0 for left eye, width/2 for right eye, 0 in normal mode. + */ + public int stereoViewportOffsetX; + + /** + * Minimum X coordinate (inclusive) for rendering. + * In stereo mode, this is {@link #stereoViewportOffsetX}. + * In normal mode, this is 0. + */ + public int renderMinX; + + /** + * Maximum X coordinate (exclusive) for rendering. + * In stereo mode, this is {@link #stereoViewportOffsetX} + {@link #stereoViewportWidth}. + * In normal mode, this is {@link #width}. + */ + public int renderMaxX; + + /** + * View frustum for frustum culling. + * Updated each frame from camera state and screen dimensions. + * Shapes can test their bounding boxes against this frustum to determine + * if they are potentially visible before expensive vertex transformations. + */ + public Frustum frustum; + + /** + * World-space position of the viewer for this pass, copied from the + * camera in {@link eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection#transformShapesBegin}. + * Used by BSP painter ordering (viewpoint for tree traversal). + * Fresh instance per context, including per-pass copies, so overlapping + * pipeline passes each see their own viewpoint. + */ + public final eu.svjatoslav.aukio.e3d.geometry.Point3D viewerPosition = + new eu.svjatoslav.aukio.e3d.geometry.Point3D(); + + /** + * Statistics for frustum culling performance tracking. + * Updated each frame: total shapes counted at start, visible shapes + * incremented during rendering, culled composites tracked during transform. + */ + public CullingStatistics cullingStatistics; + + /** + * Hi-Z occlusion pyramid, rebuilt from the depth buffer after every + * painted frame (live {@code ViewPanel} path only — headless + * {@code Snapshot} renders leave it empty so golden renders never + * cull). Read during the next frame's transform by + * {@code TriangleMeshBlock} to skip fully occluded blocks. Shared + * with pass/segment copies like {@link #depth}. + */ + public HiZPyramid occlusionPyramid; + + /** + * Executor for the parallel transform phase. When non-null, composites + * with enough children split their render lists into chunks transformed + * concurrently. When null, the transform phase runs serially on the + * render thread. + */ + public ExecutorService transformExecutor; + + /** + * Per-frame coordinator for the non-blocking parallel transform fork. + * Set by {@code ShapeCollection.transformShapes()} for the duration of + * the root transform when {@link #transformExecutor} is available; + * composites at any nesting level submit chunk tasks to it. Null outside + * the transform phase and when transforming serially. + */ + public ParallelTransformCoordinator transformCoordinator; + + /** + * Present gate for this framebuffer: fires when the frame currently + * held in this buffer has been presented to the display (or dropped + * from the presentation mailbox). The render thread installs a fresh + * gate at the start of each frame that reuses the buffer, and the + * frame's paint continuation awaits the PREVIOUS gate before writing + * pixels — without it, painting frame F+3 would overwrite the buffer + * while the present thread is still blitting frame F from it. + */ + public volatile java.util.concurrent.CountDownLatch presentGate = new java.util.concurrent.CountDownLatch(0); + + /** + * Chunk tasks submitted during the last transform phase, across all + * nesting levels. Diagnostics: proves nested composites forked. + */ + public int lastTransformTaskCount; + + /** + * Creates a new rendering context for full-screen rendering. + * + *

Equivalent to {@code RenderingContext(width, height, 0, height, numRenderSegments)}.

+ * + * @param width the rendering area width in pixels + * @param height the rendering area height in pixels + * @param numRenderSegments number of parallel render segments (threads) + */ + public RenderingContext(final int width, final int height, final int numRenderSegments) { + this(width, height, 0, height, 1, numRenderSegments, 1); + } + + /** + * Creates a new rendering context with a rectangular tile grid. + * + *

Equivalent to the band-only constructors when {@code tilesX == 1}. + * In stereo mode ({@code viewportCount == 2}) each viewport gets its own + * tile grid; segment indices for viewport v start at + * {@code v * tilesX * tilesY}.

+ * + * @param width the rendering area width in pixels + * @param height the rendering area height in pixels + * @param tilesX tile columns per viewport (1 = bands only) + * @param tilesY tile rows per viewport + * @param viewportCount number of side-by-side viewports (2 = stereo) + */ + public RenderingContext(final int width, final int height, + final int tilesX, final int tilesY, + final int viewportCount) { + this(width, height, 0, height, tilesX, tilesY, viewportCount); + } + + private RenderingContext(final int width, final int height, + final int renderMinY, final int renderMaxY, + final int tilesX, final int tilesY, + final int viewportCount) { + this.width = width; + this.height = height; + this.renderMinY = renderMinY; + this.renderMaxY = renderMaxY; + this.tilesX = tilesX; + this.tilesY = tilesY; + this.viewportCount = viewportCount; + this.numRenderSegments = tilesX * tilesY * viewportCount; + this.centerCoordinate = new Point2D(width / 2d, height / 2d); + this.projectionScale = width / 3d; + this.stereoViewportWidth = width; + this.stereoViewportOffsetX = 0; + this.renderMinX = 0; + this.renderMaxX = width; + + // Eagerly allocated so the developer-tools panel always finds it: + // transformPass() hands the pipeline a per-pass COPY of this context, + // and the copy constructor shares this reference. Lazy creation in + // ShapeCollection.transformShapesBegin() would only ever populate the + // throwaway pass copy, leaving this frame context null forever + // (the culling display then showed "-" permanently). + this.cullingStatistics = new CullingStatistics(); + this.occlusionPyramid = new HiZPyramid(); + + bufferedImage = new BufferedImage(width, height, bufferedImageType); + + final WritableRaster raster = bufferedImage.getRaster(); + final DataBufferInt dbi = (DataBufferInt) raster.getDataBuffer(); + pixels = dbi.getData(); + + // Z-buffer: one w-depth (biased 1/z) value per pixel, cleared per + // tile in the paint workers. Depth turns the queue order into a + // performance heuristic only; correctness comes from the per-pixel + // test. (The queue itself stays painter back-to-front — Z + // descending, see RenderAggregator.) Always allocated: the + // z-buffer path is the only renderer. + depth = new float[width * height]; + + 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(); + } + + /** + * Protected constructor for creating segment views. + * Shares the pixel buffer and graphics context with the parent. + * + * @param parent the parent rendering context + * @param renderMinY minimum Y coordinate (inclusive) for this segment + * @param renderMaxY maximum Y coordinate (exclusive) for this segment + */ + protected RenderingContext(final RenderingContext parent, + final int renderMinY, final int renderMaxY) { + this.width = parent.width; + this.height = parent.height; + this.renderMinY = renderMinY; + this.renderMaxY = renderMaxY; + this.tilesX = parent.tilesX; + this.tilesY = parent.tilesY; + this.viewportCount = parent.viewportCount; + this.numRenderSegments = parent.numRenderSegments; + this.centerCoordinate = parent.centerCoordinate; + this.projectionScale = parent.projectionScale; + this.stereoViewportWidth = parent.stereoViewportWidth; + this.stereoViewportOffsetX = parent.stereoViewportOffsetX; + this.stereoEye = parent.stereoEye; + this.renderMinX = parent.renderMinX; + this.renderMaxX = parent.renderMaxX; + this.bufferedImage = parent.bufferedImage; + this.pixels = parent.pixels; + this.depth = parent.depth; + this.graphics = parent.graphics; + this.vertexSlot = parent.vertexSlot; + this.nearPlaneDistance = parent.nearPlaneDistance; + this.developerTools = parent.developerTools; + this.debugLogBuffer = parent.debugLogBuffer; + this.lightingManager = parent.lightingManager; + this.occlusionPyramid = parent.occlusionPyramid; + this.segmentGraphics = null; + } + + /** + * Creates an independent pass context for one pipeline pass (one eye + * in stereo): shares the frame's pixel buffer, graphics and services, + * but owns the per-pass projection fields (center, scale, stereo + * viewport, slot, frame/cycle stamps). The next pass's setup writes + * to its own copy, so it cannot disturb this pass's in-flight + * transform chunks or its asynchronous sort/bin/paint continuation. + * + * @param parent the frame rendering context to copy from + */ + public RenderingContext(final RenderingContext parent) { + this.width = parent.width; + this.height = parent.height; + this.renderMinY = parent.renderMinY; + this.renderMaxY = parent.renderMaxY; + this.tilesX = parent.tilesX; + this.tilesY = parent.tilesY; + this.viewportCount = parent.viewportCount; + this.numRenderSegments = parent.numRenderSegments; + this.centerCoordinate = new Point2D(parent.centerCoordinate.x, parent.centerCoordinate.y); + this.projectionScale = parent.projectionScale; + this.stereoViewportWidth = parent.stereoViewportWidth; + this.stereoViewportOffsetX = parent.stereoViewportOffsetX; + this.stereoEye = parent.stereoEye; + this.renderMinX = parent.renderMinX; + this.renderMaxX = parent.renderMaxX; + this.bufferedImage = parent.bufferedImage; + this.pixels = parent.pixels; + this.depth = parent.depth; + this.graphics = parent.graphics; + this.vertexSlot = parent.vertexSlot; + this.nearPlaneDistance = parent.nearPlaneDistance; + this.frameNumber = parent.frameNumber; + this.transformCycleId = parent.transformCycleId; + this.transformExecutor = parent.transformExecutor; + this.developerTools = parent.developerTools; + this.debugLogBuffer = parent.debugLogBuffer; + this.lightingManager = parent.lightingManager; + this.cullingStatistics = parent.cullingStatistics; + this.occlusionPyramid = parent.occlusionPyramid; + this.subpixelCullingThreshold = parent.subpixelCullingThreshold; + this.subpixelCullingEpoch = parent.subpixelCullingEpoch; + this.setMouseEvent(parent.getMouseEvent()); + // Share the pre-clipped per-tile graphics: glyph rendering + // (user-facing text) draws through them by segment index. Null + // here made every glyph paint die with an NPE mid-tile (broken + // tiles whenever text faced the reader). + this.segmentGraphics = parent.segmentGraphics; + // frustum stays null: created fresh per pass in transformShapesBegin + } + + /** + * Resets per-frame state in preparation for rendering a new frame. + * Increments the frame number and clears the mouse event state. + */ + public void prepareForNewFrameRendering() { + frameNumber++; + mouseEvent = null; + currentObjectUnderMouseCursor = null; + } + + /** + * Creates Graphics2D contexts for each render segment, pre-clipped to + * its tile rectangle. Segment index layout: viewport v, tile row ty, + * tile column tx -> v * tilesX * tilesY + ty * tilesX + tx. + * + * @return array of Graphics2D objects, one per segment + */ + private Graphics2D[] createSegmentGraphics() { + final Graphics2D[] contexts = new Graphics2D[numRenderSegments]; + final int viewportWidth = width / viewportCount; + final int tileW = viewportWidth / tilesX; + final int tileH = height / tilesY; + + for (int v = 0; v < viewportCount; v++) { + final int viewportX = v * viewportWidth; + for (int ty = 0; ty < tilesY; ty++) { + final int minY = ty * tileH; + final int maxY = (ty == tilesY - 1) ? height : (ty + 1) * tileH; + for (int tx = 0; tx < tilesX; tx++) { + final int minX = viewportX + tx * tileW; + final int maxX = (tx == tilesX - 1) + ? viewportX + viewportWidth : minX + tileW; + + final Graphics2D g = bufferedImage.createGraphics(); + g.setClip(minX, minY, maxX - minX, maxY - minY); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + contexts[v * tilesX * tilesY + ty * tilesX + tx] = g; + } + } + } + + return contexts; + } + + /** + * Returns the backing image whose pixel buffer the rasterizer paints into. + * + *

Exposed for headless rendering: after a transform/sort/paint pass the + * image holds the finished frame and can be saved or compared directly.

+ * + * @return the backing buffered image + */ + public BufferedImage getImage() { + return bufferedImage; + } + + /** + * 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 numRenderSegments-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.) + * during multi-threaded rendering. + * + * @param operation the graphics operation to execute + */ + public void executeWithGraphics(final Consumer operation) { + synchronized (graphics) { + operation.accept(graphics); + } + } + + /** + * Returns the pending mouse event for this frame, or {@code null} if none. + * + * @return the mouse event to process, or {@code null} + */ + public MouseEvent getMouseEvent() { + return mouseEvent; + } + + /** + * Sets the mouse event to be processed during this frame's rendering. + * + * @param mouseEvent the mouse event with position and button information + */ + public void setMouseEvent(MouseEvent mouseEvent) { + this.mouseEvent = mouseEvent; + } + + /** + * Called when given object was detected under mouse cursor, while processing {@link #mouseEvent}. + * Because objects are rendered back to front. The last method caller will set the top-most object, if + * there are multiple objects under mouse cursor. + * + * @param currentObjectUnderMouseCursor the object that is currently under the mouse cursor + */ + public synchronized void setCurrentObjectUnderMouseCursor(MouseInteractionController currentObjectUnderMouseCursor) { + setCurrentObjectUnderMouseCursor(currentObjectUnderMouseCursor, + Double.NaN, Double.NaN); + } + + /** + * Called when given object was detected under mouse cursor, with the + * texture coordinates of the hit point (for textured shapes). + * + * @param currentObjectUnderMouseCursor the object under the mouse cursor + * @param textureU texture-space X of the hit point in primary-texture pixels + * @param textureV texture-space Y of the hit point in primary-texture pixels + */ + public synchronized void setCurrentObjectUnderMouseCursor( + final MouseInteractionController currentObjectUnderMouseCursor, + final double textureU, final double textureV) { + this.currentObjectUnderMouseCursor = currentObjectUnderMouseCursor; + this.currentMouseTextureU = textureU; + this.currentMouseTextureV = textureV; + } + + /** + * Returns the current object under the mouse cursor. + * Used by segment rendering to collect mouse results. + * + * @return the current object under mouse cursor, or null + */ + public synchronized MouseInteractionController getCurrentObjectUnderMouseCursor() { + return currentObjectUnderMouseCursor; + } + + /** + * Handles mouse events for components and returns whether a view repaint is needed. + * + * @param focusStack keyboard focus stack of the dispatching view, handed + * to clicked components so they can acquire focus + * without holding a ViewPanel reference + * @return {@code true} if view update is needed as a consequence of this mouse event + */ + public boolean handlePossibleComponentMouseEvent(final KeyboardFocusStack focusStack) { + if (mouseEvent == null) return false; + + boolean viewRepaintNeeded = false; + + if (objectPreviouslyUnderMouseCursor != currentObjectUnderMouseCursor) { + // Mouse cursor has just entered or left component. + viewRepaintNeeded = objectPreviouslyUnderMouseCursor != null && objectPreviouslyUnderMouseCursor.mouseExited(); + viewRepaintNeeded |= currentObjectUnderMouseCursor != null && currentObjectUnderMouseCursor.mouseEntered(); + objectPreviouslyUnderMouseCursor = currentObjectUnderMouseCursor; + } + + if (mouseEvent.button != 0 && currentObjectUnderMouseCursor != null) { + // Mouse button was clicked on some component. + viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseClicked( + mouseEvent.button, currentMouseTextureU, currentMouseTextureV, + focusStack); + } else if (currentObjectUnderMouseCursor != null) + // hover: let the component track the pointer position + viewRepaintNeeded |= currentObjectUnderMouseCursor.mouseHover( + currentMouseTextureU, currentMouseTextureV); + + return viewRepaintNeeded; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentRenderingContext.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentRenderingContext.java new file mode 100644 index 0000000..567bdcb --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentRenderingContext.java @@ -0,0 +1,105 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster; + +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseEvent; +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController; + +import java.awt.*; +import java.util.function.Consumer; + +/** + * A view of a RenderingContext for rendering a horizontal screen segment. + * + *

This class wraps a parent RenderingContext and provides its own Y-bounds + * for multi-threaded rendering. All operations delegate to the parent context, + * but with segment-specific Y bounds for pixel operations.

+ * + *

Mouse tracking is local to each segment and must be combined after all + * segments complete rendering.

+ * + * @see RenderingContext + */ +public class SegmentRenderingContext extends RenderingContext { + + private final RenderingContext parent; + private final int segmentIndex; + private MouseInteractionController segmentMouseHit; + private double segmentMouseHitU = Double.NaN; + private double segmentMouseHitV = Double.NaN; + + /** + * 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 segmentIndex the index of this segment (0 to numRenderSegments-1) + */ + public SegmentRenderingContext(final RenderingContext parent, + 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) { + operation.accept(parent.getSegmentGraphics(segmentIndex)); + } + + @Override + public MouseEvent getMouseEvent() { + return parent.getMouseEvent(); + } + + @Override + public void setMouseEvent(final MouseEvent mouseEvent) { + parent.setMouseEvent(mouseEvent); + } + + @Override + public synchronized void setCurrentObjectUnderMouseCursor(final MouseInteractionController controller) { + setCurrentObjectUnderMouseCursor(controller, Double.NaN, Double.NaN); + } + + @Override + public synchronized void setCurrentObjectUnderMouseCursor( + final MouseInteractionController controller, + final double textureU, final double textureV) { + this.segmentMouseHit = controller; + this.segmentMouseHitU = textureU; + this.segmentMouseHitV = textureV; + } + + /** + * Returns the mouse hit detected in this segment. + * + * @return the MouseInteractionController that was under the mouse in this segment, or null + */ + public MouseInteractionController getSegmentMouseHit() { + return segmentMouseHit; + } + + /** + * Texture-space X of the hit point (primary texture pixels), NaN if none. + */ + public double getSegmentMouseHitU() { + return segmentMouseHitU; + } + + /** + * Texture-space Y of the hit point (primary texture pixels), NaN if none. + */ + public double getSegmentMouseHitV() { + return segmentMouseHitV; + } + + @Override + public synchronized MouseInteractionController getCurrentObjectUnderMouseCursor() { + return segmentMouseHit; + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.java new file mode 100755 index 0000000..da63431 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/ShapeCollection.java @@ -0,0 +1,521 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.geometry.Camera; +import eu.svjatoslav.aukio.e3d.math.Quaternion; +import eu.svjatoslav.aukio.e3d.math.Transform; +import eu.svjatoslav.aukio.e3d.math.TransformStack; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.SubShape; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ExecutorService; + +/** + * Root container that holds all 3D shapes in a scene and orchestrates their rendering. + * + *

{@code ShapeCollection} is the top-level scene graph. You add shapes to it, and during + * each render frame it transforms all shapes from world space to screen space (relative to the + * camera), sorts them by depth, and paints them back-to-front.

+ * + *

Architecture:

+ *

The collection contains a single {@link AbstractCompositeShape} as its root container. + * This root composite:

+ *
    + *
  • Stores all scene shapes in its sub-shapes registry
  • + *
  • Triangulates N-vertex polygons (quads, etc.) into triangles during rendering
  • + *
  • Provides group-based visibility management (show/hide groups)
  • + *
  • Applies camera transform (position and rotation) to all shapes
  • + *
+ * + *

Usage example:

+ *
{@code
+ * // Get the root shape collection from the view panel
+ * ShapeCollection scene = viewPanel.getRootShapeCollection();
+ *
+ * // Add shapes to the scene
+ * scene.addShape(new Line(
+ *     new Point3D(0, 0, 100),
+ *     new Point3D(100, 0, 100),
+ *     Color.RED, 2.0
+ * ));
+ *
+ * // Add shapes with group identifier for visibility control
+ * scene.addShape(debugShape, "debug");
+ * scene.hideGroup("debug");  // hide all debug shapes
+ * scene.showGroup("debug");  // show them again
+ *
+ * // Add N-vertex polygons (quads, etc.) - automatically triangulated
+ * scene.addShape(SolidPolygon.quad(p1, p2, p3, p4, color));
+ * }
+ * + *

The {@link #addShape} method is synchronized, making it safe to add shapes from + * any thread while the rendering loop is active.

+ * + * @see eu.svjatoslav.aukio.e3d.gui.ViewPanel#getRootShapeCollection() + * @see AbstractShape the base class for all shapes + * @see AbstractCompositeShape the root composite that stores and processes all shapes + * @see RenderAggregator handles depth sorting and painting + */ +public class ShapeCollection { + + /** + * Render aggregators that collect transformed shapes, sort by depth, and + * paint — one per projection buffer slot. The double-buffered pipeline + * fills one slot's aggregator during transform while the other slot's + * aggregator is still being painted. Slot 0 serves all single-buffered + * (serial) rendering. + */ + private final RenderAggregator[] aggregators = { new RenderAggregator(0), new RenderAggregator(1), + new RenderAggregator(2) }; + + /** + * The transform stack used during the rendering pipeline. + */ + private final TransformStack transformStack = new TransformStack(); + + /** + * Global transform-cycle counter; each transform pass gets a unique id + * for per-cycle memoization (composite subtree weights). + */ + private long transformCycleCounter; + + // Subpixel-culling verdict cache state (only advanced when the cull + // is enabled on the pass context): the epoch bumps on significant + // camera change, and unconditionally every CULL_MAX_FRAMES frames so + // shape-side transform changes cannot hide behind a stale verdict. + private static final double CULL_TRANSLATE_DELTA = Double.parseDouble( + System.getProperty("aukio.cull.subpixel.translate", "25")); + private static final double CULL_ROTATE_DELTA = Double.parseDouble( + System.getProperty("aukio.cull.subpixel.rotate", "0.01")); + private static final int CULL_MAX_FRAMES = Integer.parseInt( + System.getProperty("aukio.cull.subpixel.frames", "30")); + private int subpixelCullingEpoch; + private int cullEpochAge; + private double cullCamX = Double.NaN; + private double cullCamY; + private double cullCamZ; + private double cullCamQw; + private double cullCamQx; + private double cullCamQy; + private double cullCamQz; + + + // Camera rotation. We reuse this object for every frame render to avoid garbage collections. + private final Transform cameraRotationTransform = new Transform(); + + // Camera rotation. We reuse this object for every frame render to avoid garbage collections. + private final Transform cameraTranslationTransform = new Transform(); + + /** + * Root composite shape containing all scene shapes. + * + *

Handles:

+ *
    + *
  • N-gon triangulation (quads → triangles)
  • + *
  • Group-based visibility management
  • + *
  • Camera transform application
  • + *
  • LOD slicing for nested composites
  • + *
+ * + *

The transform is updated each frame to match the camera position and rotation.

+ */ + private final AbstractCompositeShape rootComposite; + + /** + * Creates a new empty shape collection with a root composite. + */ + public ShapeCollection() { + rootComposite = new AbstractCompositeShape(); + rootComposite.setRootComposite(true); + } + + /** + * Adds a shape to this collection without a group identifier. This method is thread-safe. + * + * @param shape the shape to add to the scene + */ + public synchronized void addShape(final AbstractShape shape) { + rootComposite.addShape(shape); + } + + /** + * Collects the triangles currently rendered for this collection (render + * lists of all composites, recursively). Used by derived structures like + * the global-illumination scene snapshot. Render lists build lazily + * during transform, so the result may be empty until the first frame. + * + * @param out list receiving the polygons + */ + public void collectRenderTriangles(final List out) { + rootComposite.collectRenderTriangles(out); + } + + /** + * Adds a shape to this collection with a group identifier for visibility control. This method is thread-safe. + * + *

Grouped shapes can be shown, hidden, or removed together using + * {@link #showGroup}, {@link #hideGroup}, and {@link #removeGroup}.

+ * + * @param shape the shape to add + * @param groupId the group identifier, or {@code null} for ungrouped shapes + */ + public synchronized void addShape(final AbstractShape shape, final String groupId) { + rootComposite.addShape(shape, groupId); + } + + /** + * Returns all shapes currently in this collection (including hidden ones). + * + *

This returns the sub-shapes from the registry, unwrapped from their {@link SubShape} + * containers. For access to group and visibility metadata, use {@link #getSubShapesRegistry()}.

+ * + * @return a collection of all shapes in the scene + */ + public Collection getShapes() { + final List result = new ArrayList<>(); + for (final SubShape subShape : rootComposite.getSubShapesRegistry()) { + result.add(subShape.getShape()); + } + return result; + } + + /** + * Returns the sub-shapes registry with group and visibility metadata. + * + *

This provides direct access to the registry for advanced operations + * like inspecting group assignments or visibility states.

+ * + * @return the list of sub-shapes with their metadata + */ + public List getSubShapesRegistry() { + return rootComposite.getSubShapesRegistry(); + } + + /** + * Removes all shapes from this collection. This method is thread-safe. + */ + public synchronized void clear() { + rootComposite.getSubShapesRegistry().clear(); + rootComposite.setCacheNeedsRebuild(true); + } + + /** + * Shows all shapes belonging to the specified group. + * + * @param groupId the group identifier to show + */ + public void showGroup(final String groupId) { + rootComposite.showGroup(groupId); + } + + /** + * Hides all shapes belonging to the specified group. + * Hidden shapes are not rendered but remain in the collection. + * + * @param groupId the group identifier to hide + */ + public void hideGroup(final String groupId) { + rootComposite.hideGroup(groupId); + } + + /** + * Permanently removes all shapes belonging to the specified group. + * + * @param groupId the group identifier to remove + */ + public void removeGroup(final String groupId) { + rootComposite.removeGroup(groupId); + } + + /** + * Returns all sub-shapes belonging to the specified group. + * + * @param groupId the group identifier to match + * @return list of matching sub-shapes + */ + public List getGroup(final String groupId) { + return rootComposite.getGroup(groupId); + } + + /** + * Transforms all shapes to screen space and queues them for rendering. + * This is phase 1 of the multi-threaded render pipeline. + * + *

Updates the root composite's transform to match the camera position and rotation, + * then delegates to the root composite's transform method which handles all shapes.

+ * + *

Frustum culling: The view frustum is computed from camera state and + * screen dimensions before transforming shapes. Composite shapes can test their + * bounding boxes against this frustum to skip invisible objects.

+ * + *

Culling statistics: Statistics are reset and total shape count computed + * at the start of each frame. Visible shapes are counted as they are queued.

+ * + * @param viewPanel the view panel providing the camera state + * @param renderingContext the rendering context with frame metadata + */ + /** + * Camera-based variant of {@link #transformShapes(Camera, RenderingContext)} + * for headless rendering without a {@link eu.svjatoslav.aukio.e3d.gui.ViewPanel} (off-screen snapshots, + * golden-image tests, GI scene setup). + * + * @param camera the camera providing position and orientation + * @param renderingContext the rendering context with frame metadata + */ + public synchronized void transformShapes(final Camera camera, + final RenderingContext renderingContext) { + transformShapesBegin(camera, renderingContext); + drainTransformShapes(renderingContext); + } + + /** + * Camera-based variant of {@link #transformShapesBegin(Camera, RenderingContext)} + * for headless rendering without a {@link eu.svjatoslav.aukio.e3d.gui.ViewPanel}. + * + * @param camera the camera providing position and orientation + * @param renderingContext the pass rendering context + */ + public synchronized void transformShapesBegin(final Camera camera, + final RenderingContext renderingContext) { + + final RenderAggregator aggregator = aggregators[renderingContext.vertexSlot]; + aggregator.reset(); + transformStack.clear(); + renderingContext.transformCycleId = ++transformCycleCounter; + + // Update frustum for this frame (used for frustum culling) + if (renderingContext.frustum == null) { + renderingContext.frustum = new Frustum(); + } + renderingContext.frustum.update(camera, renderingContext.stereoViewportWidth, renderingContext.height); + + // Initialize culling statistics for this frame + if (renderingContext.cullingStatistics == null) { + renderingContext.cullingStatistics = new CullingStatistics(); + } + renderingContext.cullingStatistics.reset(); + // Note: totalShapes will be counted during rendering as shapes are queued + // This ensures we count actual rendered primitives (after triangulation/slicing) + + // final Transform rootTransform = rootComposite.getTransform(); + // TODO: Investigate if this transform can be reused instead of solution below + + cameraRotationTransform.getRotation().set(camera.getTransform().getRotation()); + cameraRotationTransform.invalidateCache(); + transformStack.addTransform(cameraRotationTransform); + + final Point3D cameraLocation = camera.getTransform().getTranslation(); + renderingContext.viewerPosition.x = cameraLocation.x; + renderingContext.viewerPosition.y = cameraLocation.y; + renderingContext.viewerPosition.z = cameraLocation.z; + + // Advance the subpixel-culling verdict epoch when the camera has + // moved significantly since the last bump (translation in world + // units; rotation compared on quaternion components, 0.01 ~ 1.1 + // degrees), or periodically. While the epoch holds, culled shapes + // skip their entire transform setup. + if (renderingContext.subpixelCullingThreshold > 0) { + final Quaternion camRot = camera.getTransform().getRotation(); + final boolean moved = Double.isNaN(cullCamX) + || Math.abs(cameraLocation.x - cullCamX) > CULL_TRANSLATE_DELTA + || Math.abs(cameraLocation.y - cullCamY) > CULL_TRANSLATE_DELTA + || Math.abs(cameraLocation.z - cullCamZ) > CULL_TRANSLATE_DELTA + || Math.abs(camRot.w - cullCamQw) > CULL_ROTATE_DELTA + || Math.abs(camRot.x - cullCamQx) > CULL_ROTATE_DELTA + || Math.abs(camRot.y - cullCamQy) > CULL_ROTATE_DELTA + || Math.abs(camRot.z - cullCamQz) > CULL_ROTATE_DELTA; + if (moved || ++cullEpochAge >= CULL_MAX_FRAMES) { + subpixelCullingEpoch++; + cullEpochAge = 0; + cullCamX = cameraLocation.x; + cullCamY = cameraLocation.y; + cullCamZ = cameraLocation.z; + cullCamQw = camRot.w; + cullCamQx = camRot.x; + cullCamQy = camRot.y; + cullCamQz = camRot.z; + } + renderingContext.subpixelCullingEpoch = subpixelCullingEpoch; + } + cameraTranslationTransform.getTranslation().x = -cameraLocation.x; + cameraTranslationTransform.getTranslation().y = -cameraLocation.y; + cameraTranslationTransform.getTranslation().z = -cameraLocation.z; + transformStack.addTransform(cameraTranslationTransform); + + // Non-blocking parallel fork: composites with enough children (at + // any nesting level) submit chunk tasks to the coordinator instead + // of transforming serially. The orchestrating thread (this one) is + // the only one allowed to block on task completion. + if (renderingContext.transformExecutor != null) { + renderingContext.transformCoordinator = + new ParallelTransformCoordinator(renderingContext.transformExecutor); + } + try { + rootComposite.transform(transformStack, aggregator, renderingContext); + } catch (final RuntimeException e) { + drainTransformShapes(renderingContext); + throw e; + } + } + + /** + * Second half of {@link #transformShapes}: waits for all transform + * chunk tasks and merges their aggregators into the frame's root + * aggregator. Safe to call from a worker thread while the render + * thread already walks the NEXT pass: chunk tasks capture their own + * transform-stack snapshots, and the aggregator is addressed by the + * pass's projection slot, so passes never touch the same state. + * + * @param renderingContext the pass rendering context + */ + public void drainTransformShapes(final RenderingContext renderingContext) { + if (renderingContext.transformCoordinator != null) { + renderingContext.transformCoordinator.drainAndMergeInto( + aggregators[renderingContext.vertexSlot]); + renderingContext.lastTransformTaskCount = + renderingContext.transformCoordinator.getSubmittedTaskCount(); + renderingContext.transformCoordinator = null; + } + } + + /** + * Sorts all queued shapes by Z-depth (back to front). + * This is phase 2 of the multi-threaded render pipeline. + */ + public void sortShapes() { + aggregators[0].sort(); + } + + /** + * Sorts the given buffer slot's queued shapes by Z-depth. + * + * @param slot buffer slot to sort (0..2) + */ + public void sortShapes(final int slot) { + aggregators[slot].sort(); + } + + /** + * Sorts the given buffer slot's queued shapes by Z-depth, parallelized + * over the given executor (instrumented parallel merge sort). + * + * @param slot buffer slot to sort + * @param executor executor for parallel sorting + */ + public void sortShapes(final int slot, final java.util.concurrent.ExecutorService executor) { + aggregators[slot].sort(executor); + } + + /** + * Bins the sorted render queue per rectangular paint tile by + * screen-space overlap, so each paint thread iterates only the + * shapes that can touch its tile instead of the whole queue. + * Call after {@link #sortShapes()}, before tile painting. + * + * @param tilesX tile columns across the viewport + * @param tilesY tile rows down the viewport + * @param originX X origin of the tiled viewport (eye offset in stereo) + * @param width tiled viewport width in pixels + * @param height full render height in pixels + * @param executor executor for parallel binning, or null for serial + */ + public void binShapesForTiles(final int tilesX, final int tilesY, + final int originX, final int width, final int height, + final ExecutorService executor) { + aggregators[0].binForTiles(tilesX, tilesY, originX, width, height, executor); + } + + /** + * Slot-selecting variant of + * {@link #binShapesForTiles(int, int, int, int, int, ExecutorService)} + * for the triple-buffered pipeline. + * + * @param slot buffer slot whose queue gets binned (0..2) + * @param tilesX tile columns across the viewport + * @param tilesY tile rows down the viewport + * @param originX X origin of the tiled viewport (eye offset in stereo) + * @param width tiled viewport width in pixels + * @param height full render height in pixels + * @param executor executor for parallel binning, or null for serial + */ + public void binShapesForTiles(final int slot, final int tilesX, final int tilesY, + final int originX, final int width, final int height, + final ExecutorService executor) { + aggregators[slot].binForTiles(tilesX, tilesY, originX, width, height, executor); + } + + /** + * Paints all already-sorted shapes to the rendering context. + * This is phase 3 of the multi-threaded render pipeline. + * Can be called multiple times with different segment contexts. + * + * @param renderingContext the rendering context to paint into + */ + public void paintShapes(final RenderingContext renderingContext) { + aggregators[renderingContext.vertexSlot].paintSorted(renderingContext); + } + + /** + * Returns the number of shapes queued for rendering. + * + * @return the shape count + */ + public int getQueuedShapeCount() { + return aggregators[0].size(); + } + + /** + * Returns the live list of shapes queued in the aggregator. + * Package-private: exposed for pipeline verification tests. + * + * @return the queued shapes + */ + List getQueuedShapes() { + return aggregators[0].getQueuedShapes(); + } + + /** + * Returns the number of shapes in each paint-segment bin, or null when + * no binning is active. Package-private: exposed for pipeline + * verification tests. + * + * @return per-segment bin sizes, or null + */ + int[] getBinSizes() { + return aggregators[0].getBinSizes(); + } + + /** + * Returns the root composite shape containing all scene shapes. + * + *

Useful for headless setups that must transform the scene without + * going through the full collection pipeline (e.g. pre-building render + * lists before a GI snapshot reads them via + * {@link #collectRenderTriangles}).

+ * + * @return the root composite (never null) + */ + public AbstractCompositeShape getRootComposite() { + return rootComposite; + } + + /** + * Sets the cache rebuild flag on the root composite. + * + *

Used internally to force a render-list rebuild. Public for advanced use cases.

+ * + * @param needsRebuild {@code true} to force cache rebuild + */ + public void setCacheNeedsRebuild(final boolean needsRebuild) { + rootComposite.setCacheNeedsRebuild(needsRebuild); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/StereoEye.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/StereoEye.java new file mode 100644 index 0000000..39ceafb --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/StereoEye.java @@ -0,0 +1,19 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster; + +/** + * Identifies which eye is being rendered in stereoscopic mode. + * + * @see RenderingContext#stereoEye + */ +public enum StereoEye { + /** Normal single-view rendering (no stereo). */ + NONE, + /** Left eye view in side-by-side stereo mode. */ + LEFT, + /** Right eye view in side-by-side stereo mode. */ + RIGHT +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.java new file mode 100644 index 0000000..9ee819d --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/Vertex.java @@ -0,0 +1,293 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster; +import eu.svjatoslav.aukio.e3d.math.TransformStack; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; + +/** + * A vertex in 3D space with transformation and screen projection support. + * + *

A vertex represents a corner point of a polygon or polyhedron. In addition to + * the 3D coordinate, it stores the transformed position (relative to viewer) and + * the projected screen coordinates for rendering.

+ * + *

Coordinate spaces:

+ *
    + *
  • {@link #coordinate} - Original position in local/model space
  • + *
  • {@link #transformedCoordinate} - Position relative to viewer (camera space)
  • + *
  • {@link #onScreenCoordinate} - 2D screen position after perspective projection
  • + *
+ * + *

Example:

+ *
{@code
+ * Vertex v = new Vertex(new Point3D(10, 20, 30));
+ * v.calculateLocationRelativeToViewer(transformStack, renderContext);
+ * if (v.transformedCoordinate.z > 0) {
+ *     // Vertex is in front of the camera
+ * }
+ * }
+ * + * @see Point3D + * @see TransformStack + */ +public class Vertex { + + /** + * Vertex coordinate in local/model 3D space. + */ + public Point3D coordinate; + + /** + * Vertex coordinate relative to the viewer after transformation (camera + * space), per buffer slot. Slot parity lets the transform phase of the + * NEXT frame write slot B while the paint phase of the current frame + * still reads slot A (double-buffered pipeline). Never access directly: + * use {@link #transformedCoordinate(RenderingContext)}. + */ + private final Point3D transformedCoordinate0 = new Point3D(); + private final Point3D transformedCoordinate1 = new Point3D(); + private final Point3D transformedCoordinate2 = new Point3D(); + + /** + * Vertex position on screen in pixels, per buffer slot. + * Use {@link #onScreenCoordinate(RenderingContext)}. + */ + private final Point2D onScreenCoordinate0 = new Point2D(); + private final Point2D onScreenCoordinate1 = new Point2D(); + private final Point2D onScreenCoordinate2 = new Point2D(); + + /** + * Texture coordinate for UV mapping (optional). + */ + public Point2D textureCoordinate; + + /** + * Normal vector for this vertex (optional). + * Used by CSG operations for smooth interpolation during polygon splitting. + * Null for non-CSG usage; existing rendering code ignores this field. + */ + public Point3D normal; + + + /** + * The transform cycle when each slot was last transformed (for + * caching). Keyed by {@link RenderingContext#transformCycleId}, NOT + * by frameNumber: in stereo mode both eye passes share the same + * parity context and therefore the same frameNumber, while using + * different slots — a frameNumber-based key lets one eye's pass + * falsely hit the cache entry the other eye wrote for the same slot + * an odd number of passes earlier, leaving opposite-eye (wrong + * viewport-offset) screen coordinates in the slot: the affected eye + * paints fully off-viewport, i.e. a black half-frame (observed as + * violent left/right flashing, 2026-09-09). transformCycleId is + * unique per pass, so a hit always means "already transformed within + * THIS pass" — the only correct dedupe semantics. + */ + private long lastTransformCycle0 = -1; + private long lastTransformCycle1 = -1; + private long lastTransformCycle2 = -1; + + /** + * Creates a vertex at the origin (0, 0, 0) with no texture coordinate. + */ + public Vertex() { + this(new Point3D()); + } + + /** + * Creates a vertex at the specified position with no texture coordinate. + * + * @param coordinate the 3D position of this vertex + */ + public Vertex(final Point3D coordinate) { + this(coordinate, null); + } + + /** + * Creates a vertex at the specified position with an optional texture coordinate. + * + * @param coordinate the 3D position of this vertex + * @param textureCoordinate the UV texture coordinate, or {@code null} for none + */ + public Vertex(final Point3D coordinate, final Point2D textureCoordinate) { + this.coordinate = coordinate; + this.textureCoordinate = textureCoordinate; + } + + /** + * Returns the camera-space coordinate for the rendering context's buffer + * slot. Valid only after this vertex was transformed for that slot's + * current frame. + * + * @param renderContext the rendering context (selects the buffer slot) + * @return the transformed coordinate (camera space) for the active slot + */ + public Point3D transformedCoordinate(final RenderingContext renderContext) { + // Dual fields, not a slot array: measured 2026-09-05 (400-sphere + // scene) that array indexing adds a second dependent load per access + // and cost ~60% of transform phase time; a perfectly-predicted + // branch + direct field load is free. + final int slot = renderContext.vertexSlot; + return slot == 0 ? transformedCoordinate0 + : slot == 1 ? transformedCoordinate1 : transformedCoordinate2; + } + + /** + * Returns the screen-space position for the rendering context's buffer + * slot. + * + * @param renderContext the rendering context (selects the buffer slot) + * @return the on-screen coordinate (pixels) for the active slot + */ + public Point2D onScreenCoordinate(final RenderingContext renderContext) { + final int slot = renderContext.vertexSlot; + return slot == 0 ? onScreenCoordinate0 + : slot == 1 ? onScreenCoordinate1 : onScreenCoordinate2; + } + + + /** + * Transforms this vertex from model space to screen space. + * + *

This method applies the transform stack to compute the vertex position + * relative to the viewer, then projects it to 2D screen coordinates. + * Results are cached per-frame per-slot to avoid redundant calculations.

+ * + * @param transforms the transform stack to apply (world-to-camera transforms) + * @param renderContext the rendering context providing projection parameters + */ + public void calculateLocationRelativeToViewer(final TransformStack transforms, + final RenderingContext renderContext) { + + final Point3D transformedCoordinate; + final Point2D onScreenCoordinate; + switch (renderContext.vertexSlot) { + case 0: + if (lastTransformCycle0 == renderContext.transformCycleId) + return; + lastTransformCycle0 = renderContext.transformCycleId; + transformedCoordinate = transformedCoordinate0; + onScreenCoordinate = onScreenCoordinate0; + break; + case 1: + if (lastTransformCycle1 == renderContext.transformCycleId) + return; + lastTransformCycle1 = renderContext.transformCycleId; + transformedCoordinate = transformedCoordinate1; + onScreenCoordinate = onScreenCoordinate1; + break; + default: + if (lastTransformCycle2 == renderContext.transformCycleId) + return; + lastTransformCycle2 = renderContext.transformCycleId; + transformedCoordinate = transformedCoordinate2; + onScreenCoordinate = onScreenCoordinate2; + break; + } + transforms.transform(coordinate, transformedCoordinate); + onScreenCoordinate.x = ((transformedCoordinate.x / transformedCoordinate.z) * renderContext.projectionScale); + onScreenCoordinate.y = ((transformedCoordinate.y / transformedCoordinate.z) * renderContext.projectionScale); + onScreenCoordinate.add(renderContext.centerCoordinate); + onScreenCoordinate.x += renderContext.stereoViewportOffsetX; + } + + /** + * Writes a camera-space position directly into this vertex's slot state + * and projects it to screen coordinates, bypassing the transform stack. + * + *

Used by near-plane clipping ({@code AbstractCoordinateShape}), which + * creates intersection vertices that exist ONLY in camera space — there + * is no model-space coordinate to transform. The given z must be > 0 + * (clip against the near plane guarantees z == nearPlaneDistance).

+ * + * @param x camera-space X + * @param y camera-space Y + * @param z camera-space Z (depth in front of viewer, > 0) + * @param renderContext the rendering context (selects the buffer slot and + * provides projection parameters) + */ + public void setCameraSpaceCoordinate(final double x, final double y, final double z, + final RenderingContext renderContext) { + final Point3D transformedCoordinate; + final Point2D onScreenCoordinate; + switch (renderContext.vertexSlot) { + case 0: + transformedCoordinate = transformedCoordinate0; + onScreenCoordinate = onScreenCoordinate0; + break; + case 1: + transformedCoordinate = transformedCoordinate1; + onScreenCoordinate = onScreenCoordinate1; + break; + default: + transformedCoordinate = transformedCoordinate2; + onScreenCoordinate = onScreenCoordinate2; + break; + } + transformedCoordinate.x = x; + transformedCoordinate.y = y; + transformedCoordinate.z = z; + onScreenCoordinate.x = ((x / z) * renderContext.projectionScale); + onScreenCoordinate.y = ((y / z) * renderContext.projectionScale); + onScreenCoordinate.add(renderContext.centerCoordinate); + onScreenCoordinate.x += renderContext.stereoViewportOffsetX; + } + + // ========== CSG support methods ========== + + /** + * Creates a deep copy of this vertex. + * Clones the coordinate, normal (if present), and texture coordinate (if present). + * The transformedCoordinate and onScreenCoordinate are not cloned (they are computed per-frame). + * + * @return a new Vertex with cloned data + */ + public Vertex clone() { + final Vertex result = new Vertex(new Point3D(coordinate), + textureCoordinate != null ? new Point2D(textureCoordinate) : null); + if (normal != null) { + result.normal = new Point3D(normal); + } + return result; + } + + /** + * Flips the orientation of this vertex by negating the normal vector. + * Called when the orientation of a polygon is flipped during CSG operations. + * If normal is null, this method does nothing. + */ + public void flip() { + if (normal != null) { + normal = normal.withNegated(); + } + } + + /** + * Creates a new vertex between this vertex and another by linearly interpolating + * all properties using parameter t. + * + *

Interpolates: position, normal (if present), and texture coordinate (if present).

+ * + * @param other the other vertex to interpolate towards + * @param t the interpolation parameter (0 = this vertex, 1 = other vertex) + * @return a new Vertex representing the interpolated position + */ + public Vertex interpolate(final Vertex other, final double t) { + final Vertex result = new Vertex( + coordinate.interpolate(other.coordinate, t), + (textureCoordinate != null && other.textureCoordinate != null) + ? new Point2D( + textureCoordinate.x + (other.textureCoordinate.x - textureCoordinate.x) * t, + textureCoordinate.y + (other.textureCoordinate.y - textureCoordinate.y) * t) + : null + ); + if (normal != null && other.normal != null) { + result.normal = normal.interpolate(other.normal, t); + } + return result; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/GlobalIllumination.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/GlobalIllumination.java new file mode 100644 index 0000000..d5fa783 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/GlobalIllumination.java @@ -0,0 +1,905 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.gi; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection; +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.GiLightProvider; +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource; +import eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Progressive CPU global illumination, running on dedicated low-priority + * threads (never on the render ForkJoinPool). + * + *

Two sampling resolutions:

+ *
    + *
  • Lightmapped triangles ({@link LightmappedShape}, e.g. the + * wrapped polygons of a lightmapping-enabled + * {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.LightmappedCompositeShape}): + * per-texel sampling. Shadows and gradients live INSIDE the polygon + * surface; the painted texture is the premultiplied composite + * (baseColor x ambient+direct+indirect), regenerated on GI threads + * and swapped in double-buffered — painters never see a half-updated + * texture.
  • + *
  • Plain solid polygons: per-polygon sampling; the result feeds + * the flat-shading path through {@link GiLightProvider} (shadow tests + * + indirect add). Polygons stay single-colored.
  • + *
+ * + *

Estimator: each sample casts one cosine-weighted hemisphere ray + * from the surface. At the hit it evaluates direct light with cached shadow + * tests (next-event estimation) plus the hit surface's current indirect + * estimate, so bounce light propagates deeper over sweeps without an + * explicit recursion limit. Two nested exponential moving averages shape + * what the user sees: the inner per-sample EMA smooths Monte Carlo noise in + * the indirect term, and the outer per-composite-update EMA wraps the + * complete sum ambient+direct+indirect with a fixed alpha — lightmaps start + * at a uniform medium irradiance and glide to the traced solution (lit + * areas brighten, unlit areas sink to darkness), so no black-to-lit flash + * or shadow pop is possible. Scene or light changes rebuild the + * snapshot and restart convergence. When converged, workers idle at a low + * cadence instead of burning CPU.

+ * + *

Render-side cost: zero ray casting. Lightmapped triangles paint + * from their current composite texture; the flat-shading path only reads + * cached per-polygon values. Frame rate is unaffected by GI quality.

+ * + *

Limitations: diffuse light only; polygon vertices are used in + * composite-local space, so scenes combining composites with non-identity + * transforms are traced incorrectly.

+ * + *

Usage:

+ *
{@code
+ * GlobalIllumination gi = viewPanel.enableGlobalIllumination(); // 2 threads
+ * }
+ */ +public class GlobalIllumination implements GiLightProvider { + + /** + * Diffuse bounce gain: outgoing radiance is albedo/pi times irradiance + * (the pi comes from cosine-weighted hemisphere integration). Without it + * the indirect feedback loop converges to several times the direct + * energy and the scene saturates. + */ + private static final double BOUNCE_GAIN = 1.0 / Math.PI; + + /** Origin offset along the surface normal to avoid self-intersection. */ + private static final double ORIGIN_EPSILON = 0.5; + + /** Shadow ray segments end this far before the light to avoid grazing hits. */ + private static final double LIGHT_EPSILON = 1.0; + + /** Minimum sleep between sweeps once the solution has converged. */ + private static final long IDLE_SLEEP_MS = 250; + + /** Maximum converged-state sleep: edits restart tracing at this latency. */ + private static final long IDLE_SLEEP_MAX_MS = 2000; + + /** Minimum interval between composite texture updates. */ + private static final long COMPOSITE_INTERVAL_MS = 500; + + /** Per-light visibility is tracked for at most this many lights. */ + private static final int MAX_TRACKED_LIGHTS = 16; + + /** Debug statistics with {@code -De3d.gi.debug}. */ + private static final boolean DEBUG = Boolean.getBoolean("e3d.gi.debug"); + + /** Albedo for snapshot entries that carry no flat color (textured triangles). */ + private static final Color FALLBACK_ALBEDO = new Color(128, 128, 128); + + /** + * EMA policy for the inner per-sample indirect blend: "fixed" (default, + * 0.15) keeps every ray hit equally intensive forever — fading toward + * darkness stays as alive as brightening. "adaptive" decays alpha with + * sample count (lower final noise, but late-time adaptation nearly + * stops). {@code -De3d.gi.alphaMode}. + */ + private static final String ALPHA_MODE = System.getProperty("e3d.gi.alphaMode", "fixed"); + /** Adaptive alpha floor: keeps post-change fade alive. */ + private static final double ALPHA_FLOOR = Double.parseDouble(System.getProperty("e3d.gi.alphaFloor", "0.08")); + /** Spatial despeckle of indirect at composite time. */ + private static final boolean DESPECKLE = Boolean.parseBoolean(System.getProperty("e3d.gi.despeckle", "true")); + + /** + * Outer EMA: fraction of the freshly computed total irradiance blended + * into the on-screen composite estimate per update. Lower = slower, + * calmer fade (and less visible Monte Carlo noise); higher = faster + * reaction. At the default 0.2 and 500 ms update cadence the scene + * reaches its true lighting in roughly 5 seconds. + * {@code -De3d.gi.compositeAlpha}. + */ + private static final double COMPOSITE_ALPHA = + Double.parseDouble(System.getProperty("e3d.gi.compositeAlpha", "0.2")); + + /** + * Convergence: declared after five consecutive composite updates whose + * average per-texel estimate movement falls below this many light + * units. With a constant alpha the estimate never freezes completely + * (Monte Carlo jitter), so this judges the VISIBLE movement, not the + * per-sample deltas. {@code -De3d.gi.calmThreshold}. + */ + private static final double CALM_THRESHOLD = + Double.parseDouble(System.getProperty("e3d.gi.calmThreshold", "1.0")); + + private final ShapeCollection shapes; + private final LightingManager lightingManager; + private final int threadCount; + + private final List threads = new ArrayList<>(); + private volatile boolean running; + + // --- Snapshot (rebuilt on scene/light change; published via volatile) --- + + private volatile Snapshot snapshot; + + /** Per-polygon state for PLAIN solid polygons, read by render threads. */ + private final ConcurrentHashMap states = new ConcurrentHashMap<>(); + + private int lastSeenRenderListVersion = -1; + private double lastLightSignature = Double.NaN; + private final AtomicInteger workIndex = new AtomicInteger(); + private volatile int calmSweeps; + private volatile long lastCompositeUpdate; + private final java.util.concurrent.atomic.AtomicBoolean compositeUpdateInFlight = + new java.util.concurrent.atomic.AtomicBoolean(); + + private static class Snapshot { + List entries; + TriangleBvh bvh; + List lights; + IdentityHashMap lightIndex; + double ambientR, ambientG, ambientB; + /** Flattened work list: one item per lightmap texel / plain polygon. */ + WorkItem[] workItems; + /** All lightmaps in the snapshot (for composite updates). */ + List lightmaps; + } + + /** One unit of GI work: a lightmap texel, or a whole plain polygon. */ + private static class WorkItem { + TriangleBvh.Entry entry; + int texel; // -1 = plain polygon + } + + /** Progressive per-polygon GI state for plain solid polygons. */ + private static class GiState { + volatile float indirectR, indirectG, indirectB; // irradiance, light units + volatile long visibleBits; // per-light: shadow ray says visible + volatile long knownBits; // per-light: visibility computed at least once + int nextLight; // round-robin cursor (GI threads only) + int samples; // adaptive EMA counter (GI threads only) + } + + /** + * Creates the GI system. Call {@link #start()} to begin tracing. + * + * @param shapes the scene to trace + * @param lightingManager the lights to sample + * @param threadCount dedicated worker threads (2 is a good default) + */ + public GlobalIllumination(final ShapeCollection shapes, + final LightingManager lightingManager, + final int threadCount) { + this.shapes = shapes; + this.lightingManager = lightingManager; + this.threadCount = Math.max(1, threadCount); + } + + /** Registers the GI provider and starts the worker threads. */ + public void start() { + if (running) + return; + running = true; + lightingManager.setGiProvider(this); + for (int i = 0; i < threadCount; i++) { + final Thread thread = new Thread(this::workLoop, "e3d-gi-" + i); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + threads.add(thread); + thread.start(); + } + } + + /** Stops the worker threads and unregisters the provider. */ + public void stop() { + running = false; + lightingManager.setGiProvider(null); + for (final Thread thread : threads) + thread.interrupt(); + threads.clear(); + } + + /** + * Returns whether the GI worker threads are running. + * + * @return {@code true} after {@link #start()} and before {@link #stop()} + */ + public boolean isRunning() { + return running; + } + + /** + * Returns whether the solution has converged (workers idling at a low + * duty cycle). Convergence is declared after five consecutive composite + * updates whose average per-texel estimate movement is below + * {@code e3d.gi.calmThreshold} (default 1.0 light unit). + * + * @return {@code true} when converged + */ + public boolean isConverged() { + return calmSweeps >= 5; + } + + /** + * Returns the number of work items in the current scene snapshot + * (one per lightmap texel plus one per plain polygon), or 0 when no + * snapshot has been built yet. + * + * @return the work item count + */ + public int getWorkItemCount() { + final Snapshot snap = snapshot; + return snap == null || snap.workItems == null ? 0 : snap.workItems.length; + } + + // ------------------------------------------------------------------ + // GiLightProvider — plain-polygon path, called from parallel + // render-pool threads. Must be fast, thread-safe, allocation-free. + // ------------------------------------------------------------------ + + @Override + public boolean isLightVisible(final SolidPolygon polygon, final LightSource light) { + final Snapshot snap = snapshot; + if (snap == null) + return true; + final Integer index = snap.lightIndex.get(light); + if (index == null || index >= 64) + return true; + final GiState state = states.get(polygon); + if (state == null) + return true; // not computed yet: shadows fade in, never pop out + final long bit = 1L << index; + if ((state.knownBits & bit) == 0) + return true; + return (state.visibleBits & bit) != 0; + } + + @Override + public void addIndirectLight(final SolidPolygon polygon, final Color baseColor, final Color result) { + final GiState state = states.get(polygon); + if (state == null) + return; + final int r = result.r + (int) (state.indirectR * baseColor.r / 255f); + final int g = result.g + (int) (state.indirectG * baseColor.g / 255f); + final int b = result.b + (int) (state.indirectB * baseColor.b / 255f); + result.set(Math.min(255, r), Math.min(255, g), Math.min(255, b), result.a); + } + + // ------------------------------------------------------------------ + // Worker threads + // ------------------------------------------------------------------ + + private void workLoop() { + final TriangleBvh.Hit hit = new TriangleBvh.Hit(); + final double[] pos = new double[3]; + while (running) { + try { + maybeRebuildSnapshot(); + final Snapshot snap = snapshot; + if (snap == null || snap.workItems.length == 0) { + Thread.sleep(100); + continue; + } + + // One sweep over all work items. Convergence is judged by + // composite-estimate movement inside updateComposites() + // (per-sample deltas are Monte Carlo noise and, with the + // fixed alpha, never settle). + final long sweepStart = System.currentTimeMillis(); + final int size = snap.workItems.length; + double deltaSum = 0; + for (int i = 0; i < size && running; i++) { + final int index = Math.floorMod(workIndex.getAndIncrement(), size); + deltaSum += sample(snap, snap.workItems[index], hit, pos); + } + final double avgDelta = deltaSum / size; + + // Regenerate composite textures at most every + // COMPOSITE_INTERVAL_MS; a paint pass is one texture swap + // per triangle, invisible to the render threads. Also + // advances the convergence counter. + final long now = System.currentTimeMillis(); + if (now - lastCompositeUpdate >= COMPOSITE_INTERVAL_MS) { + updateComposites(snap); + lastCompositeUpdate = now; + } + + if (DEBUG) + System.out.println("[GI] sweep done, avgDelta=" + String.format("%.2f", avgDelta) + + ", calmSweeps=" + calmSweeps); + + if (isConverged()) { + // Converged: cap duty cycle at ~50% of sweep time + // (a big scene's sweep takes seconds; a flat 250ms + // sleep would barely throttle it). Hard cap keeps + // post-edit re-convergence prompt. + final long sweepMillis = System.currentTimeMillis() - sweepStart; + Thread.sleep(Math.min(IDLE_SLEEP_MAX_MS, + Math.max(IDLE_SLEEP_MS, sweepMillis))); + } + } catch (final InterruptedException e) { + return; + } catch (final Exception e) { + e.printStackTrace(); + try { + Thread.sleep(500); + } catch (final InterruptedException ie) { + return; + } + } + } + } + + /** One progressive sample: one shadow ray + one bounce ray. */ + private double sample(final Snapshot snap, final WorkItem item, + final TriangleBvh.Hit hit, final double[] pos) { + final TriangleBvh.Entry entry = item.entry; + final Lightmap lightmap = entry.lightmap; + + final double ox, oy, oz, nx, ny, nz; + if (lightmap != null) { + lightmap.texelWorldPosition(item.texel, pos); + nx = lightmap.normalX; + ny = lightmap.normalY; + nz = lightmap.normalZ; + ox = pos[0] + nx * ORIGIN_EPSILON; + oy = pos[1] + ny * ORIGIN_EPSILON; + oz = pos[2] + nz * ORIGIN_EPSILON; + } else { + nx = entry.normal[0]; + ny = entry.normal[1]; + nz = entry.normal[2]; + ox = entry.centroidX + nx * ORIGIN_EPSILON; + oy = entry.centroidY + ny * ORIGIN_EPSILON; + oz = entry.centroidZ + nz * ORIGIN_EPSILON; + } + + // 1. Shadow rays. First visit per texel: test ALL lights, so direct + // light + hard shadows appear after one sweep instead of trickling + // in over lightCount sweeps. Afterwards: one light, round-robin. + final int lightCount = snap.lights.size(); + if (lightCount > 0) { + if (lightmap != null) { + lightmap.ensureLightCapacity(lightCount); + final boolean firstVisit = lightmap.sampleCounts[item.texel] == 0; + if (firstVisit) { + for (int i = 0; i < lightCount && i < MAX_TRACKED_LIGHTS; i++) + lightmap.lightVisibility[item.texel * lightCount + i] = + shadowTest(snap, ox, oy, oz, nx, ny, nz, snap.lights.get(i)) + ? Lightmap.VISIBILITY_VISIBLE : Lightmap.VISIBILITY_OCCLUDED; + } else { + final int lightIdx = lightmap.nextLight++ % lightCount; + lightmap.lightVisibility[item.texel * lightCount + lightIdx] = + shadowTest(snap, ox, oy, oz, nx, ny, nz, snap.lights.get(lightIdx)) + ? Lightmap.VISIBILITY_VISIBLE : Lightmap.VISIBILITY_OCCLUDED; + } + } else { + final GiState state = states.computeIfAbsent(entry.polygon, p -> new GiState()); + final int lightIdx = state.nextLight++ % lightCount; + final boolean visible = shadowTest(snap, ox, oy, oz, nx, ny, nz, snap.lights.get(lightIdx)); + final long bit = 1L << lightIdx; + synchronized (state) { + state.visibleBits = visible ? (state.visibleBits | bit) : (state.visibleBits & ~bit); + state.knownBits |= bit; + } + } + } + + // 2. Bounce ray: cosine-weighted hemisphere around the normal. + final double[] dir = cosineHemisphere(nx, ny, nz, ThreadLocalRandom.current()); + + double targetR = 0, targetG = 0, targetB = 0; + if (snap.bvh.nearest(ox, oy, oz, dir[0], dir[1], dir[2], hit)) { + // Direct irradiance at the hit point (clamped to display range) + // plus the hit surface's current indirect estimate. + final double[] irr = directIrradiance(snap, hit); + final Color hitColor = colorOf(hit.entry); + final float hiR, hiG, hiB; + if (hit.entry.lightmap != null) { + final int hitTexel = hit.entry.lightmap.texelAt(hit.pointX, hit.pointY, hit.pointZ); + hiR = hit.entry.lightmap.indirectR[hitTexel]; + hiG = hit.entry.lightmap.indirectG[hitTexel]; + hiB = hit.entry.lightmap.indirectB[hitTexel]; + } else { + final GiState hitState = states.get(hit.entry.polygon); + hiR = hitState == null ? 0 : hitState.indirectR; + hiG = hitState == null ? 0 : hitState.indirectG; + hiB = hitState == null ? 0 : hitState.indirectB; + } + targetR = BOUNCE_GAIN * hitColor.r * (irr[0] + hiR) / 255.0; + targetG = BOUNCE_GAIN * hitColor.g * (irr[1] + hiG) / 255.0; + targetB = BOUNCE_GAIN * hitColor.b * (irr[2] + hiB) / 255.0; + } + + // Inner EMA update. "fixed" mode (default): every ray hit lands + // with the same weight forever, so unlit areas keep fading to + // darkness at the same rate lit areas brighten. "adaptive" mode: + // alpha starts at ~1 and decays with sample count (floored). + if (lightmap != null) { + final int count = Math.min(32000, ++lightmap.sampleCounts[item.texel]); + final float alpha = "fixed".equals(ALPHA_MODE) ? 0.15f + : (float) Math.max(ALPHA_FLOOR, 2f / (2f + count)); + final float dR = (float) (targetR - lightmap.indirectR[item.texel]); + final float dG = (float) (targetG - lightmap.indirectG[item.texel]); + final float dB = (float) (targetB - lightmap.indirectB[item.texel]); + lightmap.indirectR[item.texel] += alpha * dR; + lightmap.indirectG[item.texel] += alpha * dG; + lightmap.indirectB[item.texel] += alpha * dB; + return Math.max(Math.abs(dR), Math.max(Math.abs(dG), Math.abs(dB))) * alpha; + } else { + final GiState state = states.computeIfAbsent(entry.polygon, p -> new GiState()); + synchronized (state) { + final int count = Math.min(32000, ++state.samples); + final float alpha = "fixed".equals(ALPHA_MODE) ? 0.15f + : (float) Math.max(ALPHA_FLOOR, 2f / (2f + count)); + final float dR = (float) (targetR - state.indirectR); + final float dG = (float) (targetG - state.indirectG); + final float dB = (float) (targetB - state.indirectB); + state.indirectR += alpha * dR; + state.indirectG += alpha * dG; + state.indirectB += alpha * dB; + return Math.max(Math.abs(dR), Math.max(Math.abs(dG), Math.abs(dB))) * alpha; + } + } + } + + /** Shadow ray from a surface point toward a light. */ + private boolean shadowTest(final Snapshot snap, + final double ox, final double oy, final double oz, + final double nx, final double ny, final double nz, + final LightSource light) { + final Point3D lightPos = light.getPosition(); + final double dx = lightPos.x - ox; + final double dy = lightPos.y - oy; + final double dz = lightPos.z - oz; + final double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist < 1.0) + return true; + + // Light behind the surface never illuminates it. + if ((dx * nx + dy * ny + dz * nz) / dist <= 0) + return false; + + final double maxT = dist - LIGHT_EPSILON; + if (maxT <= 0) + return true; + return !snap.bvh.occluded(ox, oy, oz, dx / dist, dy / dist, dz / dist, maxT); + } + + /** + * Direct irradiance at a ray hit point, same scale as LightingManager, + * clamped to display range: light units near a lamp can sum far beyond + * 255 and feeding unbounded energy into the bounce loop saturates the + * scene. Uses the hit surface's CACHED visibility (no new shadow rays). + */ + private double[] directIrradiance(final Snapshot snap, final TriangleBvh.Hit hit) { + final TriangleBvh.Entry entry = hit.entry; + final Lightmap lightmap = entry.lightmap; + + double r = snap.ambientR, g = snap.ambientG, b = snap.ambientB; + final int lightCount = snap.lights.size(); + final int hitTexel = lightmap != null + ? lightmap.texelAt(hit.pointX, hit.pointY, hit.pointZ) : -1; + final GiState hitState = lightmap == null ? states.get(entry.polygon) : null; + + for (int i = 0; i < lightCount && i < MAX_TRACKED_LIGHTS; i++) { + final LightSource light = snap.lights.get(i); + + if (lightmap != null) { + if (lightmap.lightVisibility != null + && lightmap.lightVisibility[hitTexel * lightCount + i] == Lightmap.VISIBILITY_OCCLUDED) + continue; + } else if (hitState != null) { + final long bit = 1L << i; + if ((hitState.knownBits & bit) != 0 && (hitState.visibleBits & bit) == 0) + continue; + } + + final Point3D lightPos = light.getPosition(); + final double dx = lightPos.x - hit.pointX; + final double dy = lightPos.y - hit.pointY; + final double dz = lightPos.z - hit.pointZ; + final double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist < 0.0001) + continue; + final double dot = (entry.normal[0] * dx + entry.normal[1] * dy + entry.normal[2] * dz) / dist; + if (dot <= 0) + continue; + final double attenuation = 1.0 / (1.0 + 0.0001 * dist * dist); + final double intensity = dot * attenuation * light.getIntensity(); + final Color lightColor = light.getColor(); + r += lightColor.r * intensity; + g += lightColor.g * intensity; + b += lightColor.b * intensity; + } + return new double[]{Math.min(255, r), Math.min(255, g), Math.min(255, b)}; + } + + /** Surface color of a snapshot entry (albedo source). */ + private static Color colorOf(final TriangleBvh.Entry entry) { + if (entry.lightmap != null) + return entry.lightmap.baseColor; + if (entry.polygon instanceof SolidPolygon) + return ((SolidPolygon) entry.polygon).getColor(); + // Snapshot triangles are not all SolidPolygons (e.g. textured + // triangles from a TextCanvas in a GI scene carry no flat color) — + // a neutral gray albedo keeps bounce light plausible instead of + // throwing ClassCastException into the worker loop. + return FALLBACK_ALBEDO; + } + + // ------------------------------------------------------------------ + // Composite textures: baseColor x (ambient + direct with shadows + + // indirect), regenerated into the back buffer and swapped in. + // ------------------------------------------------------------------ + + private void updateComposites(final Snapshot snap) { + // Single flight: both workers finish sweeps concurrently and must + // not write the same back buffers simultaneously. + if (!compositeUpdateInFlight.compareAndSet(false, true)) + return; + try { + final int lightCount = snap.lights.size(); + final double[] pos = new double[3]; + double movementSum = 0; + long texelTotal = 0; + for (final Lightmap lightmap : snap.lightmaps) { + lightmap.ensureLightCapacity(lightCount); + final int width = lightmap.width; + final int height = lightmap.height; + final int texelCount = width * height; + + // 1. Total irradiance per valid texel (float, no clamping yet). + final float[] irrR = new float[texelCount]; + final float[] irrG = new float[texelCount]; + final float[] irrB = new float[texelCount]; + for (final int texel : lightmap.validTexels) { + lightmap.texelWorldPosition(texel, pos); + double r = snap.ambientR, g = snap.ambientG, b = snap.ambientB; + for (int i = 0; i < lightCount && i < MAX_TRACKED_LIGHTS; i++) { + if (lightmap.lightVisibility[texel * lightCount + i] == Lightmap.VISIBILITY_OCCLUDED) + continue; + final LightSource light = snap.lights.get(i); + final Point3D lightPos = light.getPosition(); + final double dx = lightPos.x - pos[0]; + final double dy = lightPos.y - pos[1]; + final double dz = lightPos.z - pos[2]; + final double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist < 0.0001) + continue; + final double dot = (lightmap.normalX * dx + lightmap.normalY * dy + + lightmap.normalZ * dz) / dist; + if (dot <= 0) + continue; + final double attenuation = 1.0 / (1.0 + 0.0001 * dist * dist); + final double intensity = dot * attenuation * light.getIntensity(); + final Color lightColor = light.getColor(); + r += lightColor.r * intensity; + g += lightColor.g * intensity; + b += lightColor.b * intensity; + } + // Indirect, lightly blended with valid 4-neighbors: + // single-texel Monte Carlo spikes are smoothed without + // blurring real gradients (texels are sub-pixel at 4K). + final float smoothedR = DESPECKLE ? smoothedIndirect(lightmap.indirectR, lightmap, texel) : lightmap.indirectR[texel]; + final float smoothedG = DESPECKLE ? smoothedIndirect(lightmap.indirectG, lightmap, texel) : lightmap.indirectG[texel]; + final float smoothedB = DESPECKLE ? smoothedIndirect(lightmap.indirectB, lightmap, texel) : lightmap.indirectB[texel]; + irrR[texel] = (float) Math.min(255, r) + smoothedR; + irrG[texel] = (float) Math.min(255, g) + smoothedG; + irrB[texel] = (float) Math.min(255, b) + smoothedB; + } + + // 2. Fill the invalid half (u+v > 1) from nearest valid + // neighbors, so bilinear upsampling never reads garbage. + final boolean[] filled = new boolean[texelCount]; + for (final int texel : lightmap.validTexels) + filled[texel] = true; + boolean progressed = true; + while (progressed) { + progressed = false; + for (int t = 0; t < texelCount; t++) { + if (filled[t]) + continue; + final int i = t % width; + final int j = t / width; + final int left = i > 0 ? t - 1 : -1; + final int right = i < width - 1 ? t + 1 : -1; + final int up = j > 0 ? t - width : -1; + final int down = j < height - 1 ? t + width : -1; + final int source = left >= 0 && filled[left] ? left + : right >= 0 && filled[right] ? right + : up >= 0 && filled[up] ? up + : down >= 0 && filled[down] ? down : -1; + if (source >= 0) { + irrR[t] = irrR[source]; + irrG[t] = irrG[source]; + irrB[t] = irrB[source]; + filled[t] = true; + progressed = true; + } + } + } + + // 3. Blend the computed irradiance into the persistent + // per-texel estimate (the outer EMA), then write the + // composite texture 1:1 from the ESTIMATE — the texture + // can only move COMPOSITE_ALPHA of the remaining + // distance per update, so direct light, shadows and + // indirect all fade in/out gradually. + final Texture back = lightmap.backTexture(); + final int[] pixels = back.primaryBitmap.pixels; + for (int j = 0; j < height; j++) + for (int i = 0; i < width; i++) { + final int t = j * width + i; + final float dR = (float) (COMPOSITE_ALPHA * (irrR[t] - lightmap.estimateR[t])); + final float dG = (float) (COMPOSITE_ALPHA * (irrG[t] - lightmap.estimateG[t])); + final float dB = (float) (COMPOSITE_ALPHA * (irrB[t] - lightmap.estimateB[t])); + lightmap.estimateR[t] += dR; + lightmap.estimateG[t] += dG; + lightmap.estimateB[t] += dB; + movementSum += Math.max(Math.abs(dR), Math.max(Math.abs(dG), Math.abs(dB))); + texelTotal++; + pixels[t] = compositePixel(lightmap, + lightmap.estimateR[t], lightmap.estimateG[t], lightmap.estimateB[t]); + } + + back.resetResampledBitmapCache(); + if (lightmap.owner != null) { + lightmap.owner.setTexture(back); + lightmap.swapBuffers(); + } + + // Debug: -De3d.gi.dumpLightmaps=/tmp/lm dumps composites as PNGs. + if (DUMP_DIR != null) + dumpLightmap(lightmap, pixels); + } + + // Convergence: average per-texel movement of the on-screen + // estimate. With a constant alpha the estimate never fully + // freezes (Monte Carlo jitter), so CALM_THRESHOLD judges the + // VISIBLE movement; five calm updates in a row -> idle. + final double avgMovement = texelTotal > 0 ? movementSum / texelTotal : 0; + if (avgMovement < CALM_THRESHOLD) + calmSweeps++; + else + calmSweeps = 0; + if (DEBUG) + System.out.println("[GI] composite update, avgMovement=" + + String.format("%.2f", avgMovement)); + } finally { + compositeUpdateInFlight.set(false); + } + } + + private static final String DUMP_DIR = System.getProperty("e3d.gi.dumpLightmaps"); + private static int dumpCounter; + + private static void dumpLightmap(final Lightmap lightmap, final int[] pixels) { + if (dumpCounter++ % 173 != 0) // spread dumps across lightmaps + return; + try { + final int scale = 8; + final int w = lightmap.width; + final int h = lightmap.height; + final java.awt.image.BufferedImage image = new java.awt.image.BufferedImage( + w * scale, h * scale, java.awt.image.BufferedImage.TYPE_INT_RGB); + for (int j = 0; j < h * scale; j++) + for (int i = 0; i < w * scale; i++) + image.setRGB(i, j, pixels[(j / scale) * w + (i / scale)]); + final java.io.File dir = new java.io.File(DUMP_DIR); + dir.mkdirs(); + final String name = String.format("%s/lm-%03d-%dx%d-(%.0f,%.0f,%.0f).png", DUMP_DIR, + dumpCounter, w, h, + lightmap.originX, lightmap.originY, lightmap.originZ); + javax.imageio.ImageIO.write(image, "png", new java.io.File(name)); + } catch (final Exception e) { + e.printStackTrace(); + } + } + + /** + * Indirect value blended 50/50 with the mean of valid 4-neighbors. + * Kills single-texel Monte Carlo spikes (bright speckles in shadows). + */ + private static float smoothedIndirect(final float[] indirect, final Lightmap lightmap, final int texel) { + final int width = lightmap.width; + final int height = lightmap.height; + final int i = texel % width; + final int j = texel / width; + float sum = 0; + int count = 0; + if (i > 0 && isValid(lightmap, texel - 1)) { sum += indirect[texel - 1]; count++; } + if (i < width - 1 && isValid(lightmap, texel + 1)) { sum += indirect[texel + 1]; count++; } + if (j > 0 && isValid(lightmap, texel - width)) { sum += indirect[texel - width]; count++; } + if (j < height - 1 && isValid(lightmap, texel + width)) { sum += indirect[texel + width]; count++; } + if (count == 0) + return indirect[texel]; + return 0.5f * indirect[texel] + 0.5f * sum / count; + } + + private static boolean isValid(final Lightmap lightmap, final int texel) { + final double u = ((texel % lightmap.width) + 0.5) / lightmap.width; + final double v = ((texel / lightmap.width) + 0.5) / lightmap.height; + return u + v <= 1.0; + } + + /** Composite texel: baseColor scaled by total irradiance, clamped. */ + private static int compositePixel(final Lightmap lightmap, + final double irrR, final double irrG, final double irrB) { + final int r = Math.min(255, (int) (irrR * lightmap.baseColor.r / 255)); + final int g = Math.min(255, (int) (irrG * lightmap.baseColor.g / 255)); + final int b = Math.min(255, (int) (irrB * lightmap.baseColor.b / 255)); + return 0xFF000000 | (r << 16) | (g << 8) | b; + } + + // ------------------------------------------------------------------ + // Snapshot management + // ------------------------------------------------------------------ + + private void maybeRebuildSnapshot() { + final int version = AbstractCompositeShape.getGlobalRenderListVersion(); + final double lightSignature = lightSignature(); + if (version == lastSeenRenderListVersion && lightSignature == lastLightSignature) + return; + + final List triangles = new ArrayList<>(); + shapes.collectRenderTriangles(triangles); + + final Snapshot snap = new Snapshot(); + snap.entries = new ArrayList<>(triangles.size()); + snap.lightmaps = new ArrayList<>(); + for (final AbstractCoordinateShape triangle : triangles) { + if (triangle.vertices.size() < 3) + continue; + final TriangleBvh.Entry entry = buildEntry(triangle); + snap.entries.add(entry); + if (entry.lightmap != null) + snap.lightmaps.add(entry.lightmap); + } + if (!snap.entries.isEmpty()) + snap.bvh = new TriangleBvh(snap.entries); + snap.lights = new ArrayList<>(lightingManager.getLights()); + snap.lightIndex = new IdentityHashMap<>(); + for (int i = 0; i < snap.lights.size(); i++) + snap.lightIndex.put(snap.lights.get(i), i); + final Color ambient = lightingManager.getAmbientLight(); + snap.ambientR = ambient.r; + snap.ambientG = ambient.g; + snap.ambientB = ambient.b; + + // Flattened work list: one item per valid lightmap texel, + // one per plain polygon. + final List workItems = new ArrayList<>(); + for (final TriangleBvh.Entry entry : snap.entries) { + if (entry.lightmap != null) { + for (final int texel : entry.lightmap.validTexels) { + final WorkItem item = new WorkItem(); + item.entry = entry; + item.texel = texel; + workItems.add(item); + } + } else { + final WorkItem item = new WorkItem(); + item.entry = entry; + item.texel = -1; + workItems.add(item); + } + } + snap.workItems = workItems.toArray(new WorkItem[0]); + + snapshot = snap; + states.clear(); + lastSeenRenderListVersion = version; + lastLightSignature = lightSignature; + calmSweeps = 0; // scene changed: back to full-speed tracing + + if (DEBUG) + System.out.println("[GI] snapshot: " + snap.entries.size() + " triangles, " + + snap.workItems.length + " work items, " + + snap.lightmaps.size() + " lightmaps, " + + snap.lights.size() + " lights"); + } + + private double lightSignature() { + double signature = 0; + for (final LightSource light : lightingManager.getLights()) { + final Point3D p = light.getPosition(); + final Color c = light.getColor(); + signature += p.x + p.y + p.z + c.r + c.g + c.b + light.getIntensity() * 31.0; + } + return signature; + } + + private TriangleBvh.Entry buildEntry(final AbstractCoordinateShape triangle) { + final TriangleBvh.Entry entry = new TriangleBvh.Entry(triangle); + if (triangle instanceof LightmappedShape) + entry.lightmap = ((LightmappedShape) triangle).getLightmap(); + + final Point3D a = triangle.vertices.get(0).coordinate; + final Point3D b = triangle.vertices.get(1).coordinate; + final Point3D c = triangle.vertices.get(2).coordinate; + entry.v[0] = (float) a.x; entry.v[1] = (float) a.y; entry.v[2] = (float) a.z; + entry.v[3] = (float) b.x; entry.v[4] = (float) b.y; entry.v[5] = (float) b.z; + entry.v[6] = (float) c.x; entry.v[7] = (float) c.y; entry.v[8] = (float) c.z; + entry.centroidX = (float) ((a.x + b.x + c.x) / 3); + entry.centroidY = (float) ((a.y + b.y + c.y) / 3); + entry.centroidZ = (float) ((a.z + b.z + c.z) / 3); + entry.minX = Math.min(entry.v[0], Math.min(entry.v[3], entry.v[6])); + entry.maxX = Math.max(entry.v[0], Math.max(entry.v[3], entry.v[6])); + entry.minY = Math.min(entry.v[1], Math.min(entry.v[4], entry.v[7])); + entry.maxY = Math.max(entry.v[1], Math.max(entry.v[4], entry.v[7])); + entry.minZ = Math.min(entry.v[2], Math.min(entry.v[5], entry.v[8])); + entry.maxZ = Math.max(entry.v[2], Math.max(entry.v[5], entry.v[8])); + // Normal: right-handed cross(b-a, c-a), matching Plane.computeNormal. + final double e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z; + final double e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z; + double nx = e1y * e2z - e1z * e2y; + double ny = e1z * e2x - e1x * e2z; + double nz = e1x * e2y - e1y * e2x; + final double len = Math.sqrt(nx * nx + ny * ny + nz * nz); + if (len > 0) { + nx /= len; + ny /= len; + nz /= len; + } + entry.normal = new float[]{(float) nx, (float) ny, (float) nz}; + return entry; + } + + /** Cosine-weighted hemisphere direction around a normal. */ + private double[] cosineHemisphere(final double nx, final double ny, final double nz, + final ThreadLocalRandom random) { + final double u = random.nextDouble(); + final double v = random.nextDouble(); + final double r = Math.sqrt(u); + final double theta = 2 * Math.PI * v; + final double x = r * Math.cos(theta); + final double y = r * Math.sin(theta); + final double z = Math.sqrt(Math.max(0, 1 - u)); + + // Orthonormal basis around the normal. + final double upX = Math.abs(ny) < 0.9 ? 0 : 1; + final double upY = Math.abs(ny) < 0.9 ? 1 : 0; + double tx = upY * nz; + double ty = -upX * nz; + double tz = upX * ny - upY * nx; + final double tLen = Math.sqrt(tx * tx + ty * ty + tz * tz); + tx /= tLen; + ty /= tLen; + tz /= tLen; + final double bx = ny * tz - nz * ty; + final double by = nz * tx - nx * tz; + final double bz = nx * ty - ny * tx; + + return new double[]{ + tx * x + bx * y + nx * z, + ty * x + by * y + ny * z, + tz * x + bz * y + nz * z + }; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/Lightmap.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/Lightmap.java new file mode 100644 index 0000000..4acf19e --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/Lightmap.java @@ -0,0 +1,279 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.gi; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture; + +/** + * Per-triangle lightmap: a small generated texture whose texels map onto the + * triangle surface. The triangle's UVs (in texture pixel units) are + * (0,0), (width,0), (0,height), so the valid texel region is the half + * where u+v <= 1 in normalized coordinates; the other half is filled + * by mirroring to keep mipmaps and edge sampling clean. + * + *

What you trace is what you see: the composite texture the painter + * samples IS the lightmap, at native texel resolution. Shadow-edge + * smoothness comes from tracing at finer resolution (smaller + * unitsPerTexel), never from upsampling.

+ * + *

The GI system stores per-texel indirect irradiance and per-texel + * per-light visibility here, and periodically regenerates the premultiplied + * composite texture (baseColor x lighting) into the back buffer, then swaps + * it onto the rendered triangle — painters never see a half-updated + * texture.

+ * + *

Gradual convergence: what reaches the texture is never the raw + * computed irradiance but a persistent per-texel exponential moving average + * ({@link #estimateR}/{@link #estimateG}/{@link #estimateB}) over the + * complete sum ambient+direct+indirect. The estimate starts at a uniform + * medium value ({@link #INITIAL_IRRADIANCE}), so the world is visible from + * frame one; lit areas then brighten and unlit areas sink to darkness + * gradually — no black-to-lit flash is possible, since the texture can only + * move a fixed alpha fraction per composite update.

+ * + *

Threading: texel state arrays are written by GI worker threads and read + * by whoever holds the snapshot; element-wise racy access is benign for + * progressive refinement. Texture buffer swaps are volatile/atomic via + * {@link LightmappedTriangle#setTexture}.

+ */ +public class Lightmap { + + /** Visibility value: not yet computed. */ + public static final byte VISIBILITY_UNKNOWN = 0; + /** Visibility value: shadow ray reached the light. */ + public static final byte VISIBILITY_VISIBLE = 1; + /** Visibility value: shadow ray was blocked. */ + public static final byte VISIBILITY_OCCLUDED = 2; + + /** Texture size limits, power of two. */ + private static final int MIN_SIZE = 4; + private static final int MAX_SIZE = 128; + + /** + * Uniform irradiance the composite estimate starts at (light units, + * display scale 0..255): the world begins medium-lit and visible, then + * fades toward the traced solution. {@code -De3d.gi.initialIrradiance}. + */ + public static final double INITIAL_IRRADIANCE = + Double.parseDouble(System.getProperty("e3d.gi.initialIrradiance", "128")); + + public final int width; + public final int height; + + /** Unlit surface color of the triangle. */ + public final Color baseColor; + + // World mapping: p(u,v) = origin + e1*u + e2*v. + public final double originX, originY, originZ; + public final double edge1X, edge1Y, edge1Z; + public final double edge2X, edge2Y, edge2Z; + /** Unit surface normal. */ + public final double normalX, normalY, normalZ; + + /** Valid (u+v <= 1) texel indices, for round-robin sampling. */ + public final int[] validTexels; + + /** Per-texel indirect irradiance (light units, pre-albedo). */ + public final float[] indirectR; + public final float[] indirectG; + public final float[] indirectB; + + /** + * Per-texel EMA estimate of TOTAL irradiance (ambient + direct + + * indirect), the only value ever written to the composite texture. + * Initialized to {@link #INITIAL_IRRADIANCE} (uniform medium start); + * each composite update blends the freshly computed irradiance in with + * a fixed alpha, so both brightening and fading to darkness stay alive + * forever and no single-frame jump can occur. + */ + public final float[] estimateR; + public final float[] estimateG; + public final float[] estimateB; + + /** + * Per-texel sample counters. In "adaptive" alpha mode they drive the + * decaying EMA weight; in the default "fixed" mode they only mark + * first-visit texels (all-lights shadow test on the first sweep). + */ + public final short[] sampleCounts; + + /** Per-texel per-light visibility: texelCount * lightCount bytes. */ + public byte[] lightVisibility; + public int lightCount; + + /** Double-buffered composite textures; the triangle shows one, GI fills the other. */ + private final Texture[] buffers = new Texture[2]; + private int shownBuffer; + + /** The triangle currently displaying this lightmap (for texture swaps). */ + public volatile LightmappedTriangle owner; + + /** Round-robin light cursor for shadow sampling (GI threads only). */ + public int nextLight; + + /** + * Creates a lightmap for a triangle. + * + * @param a first vertex (UV 0,0) + * @param b second vertex (UV 1,0) + * @param c third vertex (UV 0,1) + * @param baseColor unlit surface color + * @param unitsPerTexel world units per lightmap texel (resolution knob) + * @param normalX unit normal x + * @param normalY unit normal y + * @param normalZ unit normal z + */ + public Lightmap(final Point3D a, final Point3D b, final Point3D c, + final Color baseColor, final double unitsPerTexel, + final double normalX, final double normalY, final double normalZ) { + this.baseColor = baseColor; + originX = a.x; + originY = a.y; + originZ = a.z; + edge1X = b.x - a.x; + edge1Y = b.y - a.y; + edge1Z = b.z - a.z; + edge2X = c.x - a.x; + edge2Y = c.y - a.y; + edge2Z = c.z - a.z; + this.normalX = normalX; + this.normalY = normalY; + this.normalZ = normalZ; + + final double len1 = Math.sqrt(edge1X * edge1X + edge1Y * edge1Y + edge1Z * edge1Z); + final double len2 = Math.sqrt(edge2X * edge2X + edge2Y * edge2Y + edge2Z * edge2Z); + width = powerOfTwo(len1 / unitsPerTexel); + height = powerOfTwo(len2 / unitsPerTexel); + + indirectR = new float[width * height]; + indirectG = new float[width * height]; + indirectB = new float[width * height]; + estimateR = new float[width * height]; + estimateG = new float[width * height]; + estimateB = new float[width * height]; + java.util.Arrays.fill(estimateR, (float) INITIAL_IRRADIANCE); + java.util.Arrays.fill(estimateG, (float) INITIAL_IRRADIANCE); + java.util.Arrays.fill(estimateB, (float) INITIAL_IRRADIANCE); + sampleCounts = new short[width * height]; + + final int[] valid = new int[width * height]; + int count = 0; + for (int j = 0; j < height; j++) + for (int i = 0; i < width; i++) { + final double u = (i + 0.5) / width; + final double v = (j + 0.5) / height; + if (u + v <= 1.0) + valid[count++] = j * width + i; + } + validTexels = new int[count]; + System.arraycopy(valid, 0, validTexels, 0, count); + + // Both buffers start at the uniform medium INITIAL_IRRADIANCE: + // the world is visible from frame one and fades toward the traced + // solution (lit areas brighten, unlit areas sink to darkness). + buffers[0] = createTexture(); + buffers[1] = createTexture(); + } + + private static int powerOfTwo(final double size) { + int result = MIN_SIZE; + while (result < size && result < MAX_SIZE) + result <<= 1; + return result; + } + + private Texture createTexture() { + final Texture texture = new Texture(width, height, 0); + final int r = Math.min(255, (int) (INITIAL_IRRADIANCE * baseColor.r / 255)); + final int g = Math.min(255, (int) (INITIAL_IRRADIANCE * baseColor.g / 255)); + final int b = Math.min(255, (int) (INITIAL_IRRADIANCE * baseColor.b / 255)); + final int pixel = 0xFF000000 | (r << 16) | (g << 8) | b; + java.util.Arrays.fill(texture.primaryBitmap.pixels, pixel); + return texture; + } + + /** + * World position of a texel center. + * + * @param texel texel index (j * width + i) + * @param out receives x, y, z + */ + public void texelWorldPosition(final int texel, final double[] out) { + final double u = ((texel % width) + 0.5) / width; + final double v = ((texel / width) + 0.5) / height; + out[0] = originX + edge1X * u + edge2X * v; + out[1] = originY + edge1Y * u + edge2Y * v; + out[2] = originZ + edge1Z * u + edge2Z * v; + } + + /** + * Texel index nearest to a world point on the triangle plane. + * + * @param px world x + * @param py world y + * @param pz world z + * @return texel index, clamped into the texture + */ + public int texelAt(final double px, final double py, final double pz) { + final double dx = px - originX; + final double dy = py - originY; + final double dz = pz - originZ; + final double d11 = edge1X * edge1X + edge1Y * edge1Y + edge1Z * edge1Z; + final double d22 = edge2X * edge2X + edge2Y * edge2Y + edge2Z * edge2Z; + final double d12 = edge1X * edge2X + edge1Y * edge2Y + edge1Z * edge2Z; + final double dp1 = dx * edge1X + dy * edge1Y + dz * edge1Z; + final double dp2 = dx * edge2X + dy * edge2Y + dz * edge2Z; + final double denom = d11 * d22 - d12 * d12; + if (denom < 1e-12) + return 0; + final double u = (dp1 * d22 - dp2 * d12) / denom; + final double v = (dp2 * d11 - dp1 * d12) / denom; + int i = (int) (u * width); + int j = (int) (v * height); + if (i < 0) i = 0; + if (i >= width) i = width - 1; + if (j < 0) j = 0; + if (j >= height) j = height - 1; + return j * width + i; + } + + /** + * Ensures the per-texel visibility array matches the light count. + * Called from GI threads during sampling. + * + * @param lights number of lights in the snapshot + */ + public void ensureLightCapacity(final int lights) { + if (lightVisibility == null || lightCount != lights) { + lightVisibility = new byte[width * height * lights]; + lightCount = lights; + } + } + + /** + * The texture the triangle should show right now. + * + * @return the front composite texture + */ + public Texture shownTexture() { + return buffers[shownBuffer]; + } + + /** + * The texture GI should write the next composite into. + * + * @return the back composite texture + */ + public Texture backTexture() { + return buffers[1 - shownBuffer]; + } + + /** Flips the buffers after the back texture has been regenerated. */ + public void swapBuffers() { + shownBuffer = 1 - shownBuffer; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedShape.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedShape.java new file mode 100644 index 0000000..bdf50bd --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedShape.java @@ -0,0 +1,20 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.gi; + +/** + * A shape carrying a {@link Lightmap}. The global illumination system + * detects this interface in its scene snapshot and samples GI per lightmap + * texel instead of per polygon. + */ +public interface LightmappedShape { + + /** + * Returns the lightmap for this shape. + * + * @return the lightmap + */ + Lightmap getLightmap(); +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.java new file mode 100644 index 0000000..0df0d60 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/LightmappedTriangle.java @@ -0,0 +1,65 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.gi; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle; + +/** + * A textured triangle whose texture is a GI-generated lightmap composite + * (baseColor x lighting). UVs are fixed at (0,0), (width,0), (0,height) + * (engine UVs are in texture pixel units): the whole triangle is covered by + * its own lightmap, valid region u+v <= 1 in normalized coordinates. + * + *

Shading via {@code LightingManager} does not apply — all light (ambient, + * direct with shadows, indirect) lives in the composite texture, which the + * GI system regenerates and swaps in every few sweeps.

+ */ +public class LightmappedTriangle extends TexturedTriangle implements LightmappedShape { + + private final Lightmap lightmap; + + /** + * Creates a lightmapped triangle. + * + * @param a first vertex (shared coordinate reference is fine) + * @param b second vertex + * @param c third vertex + * @param baseColor unlit surface color + * @param unitsPerTexel world units per lightmap texel + * @param normalX unit normal x + * @param normalY unit normal y + * @param normalZ unit normal z + */ + public LightmappedTriangle(final Point3D a, final Point3D b, final Point3D c, + final Color baseColor, final double unitsPerTexel, + final double normalX, final double normalY, final double normalZ) { + super(new Vertex(a, new Point2D(0, 0)), + new Vertex(b, new Point2D(1, 0)), + new Vertex(c, new Point2D(0, 1)), + null); + lightmap = new Lightmap(a, b, c, baseColor, unitsPerTexel, + normalX, normalY, normalZ); + lightmap.owner = this; + + // UVs are in PRIMARY TEXTURE PIXELS in this engine (multiplication + // factor scales them into the selected mip level), so the triangle + // corners map to the lightmap corners directly. + vertices.get(0).textureCoordinate = new Point2D(0, 0); + vertices.get(1).textureCoordinate = new Point2D(lightmap.width, 0); + vertices.get(2).textureCoordinate = new Point2D(0, lightmap.height); + refreshTextureDistance(); + + setTexture(lightmap.shownTexture()); + } + + @Override + public Lightmap getLightmap() { + return lightmap; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/TriangleBvh.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/TriangleBvh.java new file mode 100644 index 0000000..5e099ae --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/TriangleBvh.java @@ -0,0 +1,231 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.gi; + +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape; + +import java.util.List; + +/** + * Bounding volume hierarchy over world-space triangles for fast ray queries. + * Used by the global illumination system; deliberately separate from the + * voxel octree (which serves voxel tracing and stays untouched). + * + *

Build: top-down median split along the longest AABB axis. + * Queries: nearest-hit for bounce rays, any-hit with early out for shadow + * rays. Intersection is Möller–Trumbore, two-sided (walls are single quads + * that must occlude from both sides). Not thread-safe to build, safe to + * query concurrently once published via a volatile/atomic reference.

+ */ +public class TriangleBvh { + + /** One ray-traced triangle with cached data. */ + public static class Entry { + public final AbstractCoordinateShape polygon; + /** Lightmap when the polygon is a {@link LightmappedShape}, else null. */ + public Lightmap lightmap; + /** Triangle vertices, world space: x0,y0,z0, x1,y1,z1, x2,y2,z2. */ + public final float[] v = new float[9]; + public float centroidX, centroidY, centroidZ; + public float minX, minY, minZ, maxX, maxY, maxZ; + /** Unit surface normal, world space. */ + public volatile float[] normal; + + public Entry(final AbstractCoordinateShape polygon) { + this.polygon = polygon; + } + } + + /** Nearest-hit query result. */ + public static class Hit { + public Entry entry; + public double t; + public double pointX, pointY, pointZ; + } + + private static class Node { + double minX, minY, minZ, maxX, maxY, maxZ; + Node left, right; + Entry[] entries; // leaf only + } + + private static final int LEAF_SIZE = 4; + private static final double EPSILON = 0.01; + + private final Node root; + + /** + * Builds the tree over the given triangle entries. + * + * @param entries triangles to index (must not be empty) + */ + public TriangleBvh(final List entries) { + root = build(entries.toArray(new Entry[0]), 0, entries.size()); + } + + private Node build(final Entry[] entries, final int from, final int to) { + final Node node = new Node(); + double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE, minZ = Double.MAX_VALUE; + double maxX = -Double.MAX_VALUE, maxY = -Double.MAX_VALUE, maxZ = -Double.MAX_VALUE; + for (int i = from; i < to; i++) { + final Entry e = entries[i]; + minX = Math.min(minX, e.minX); maxX = Math.max(maxX, e.maxX); + minY = Math.min(minY, e.minY); maxY = Math.max(maxY, e.maxY); + minZ = Math.min(minZ, e.minZ); maxZ = Math.max(maxZ, e.maxZ); + } + node.minX = minX; node.minY = minY; node.minZ = minZ; + node.maxX = maxX; node.maxY = maxY; node.maxZ = maxZ; + + final int count = to - from; + if (count <= LEAF_SIZE) { + node.entries = new Entry[count]; + System.arraycopy(entries, from, node.entries, 0, count); + return node; + } + + // Split along the longest axis at the median centroid. + final double dx = maxX - minX, dy = maxY - minY, dz = maxZ - minZ; + final int axis = (dx >= dy && dx >= dz) ? 0 : (dy >= dz ? 1 : 2); + java.util.Arrays.sort(entries, from, to, (a, b) -> { + final double ca = axis == 0 ? a.centroidX : axis == 1 ? a.centroidY : a.centroidZ; + final double cb = axis == 0 ? b.centroidX : axis == 1 ? b.centroidY : b.centroidZ; + return Double.compare(ca, cb); + }); + final int mid = from + count / 2; + node.left = build(entries, from, mid); + node.right = build(entries, mid, to); + return node; + } + + /** + * Finds the nearest triangle hit along the ray, or null. + * + * @param hit reusable result object, filled on hit + * @return true on hit + */ + public boolean nearest(final double ox, final double oy, final double oz, + final double dx, final double dy, final double dz, + final Hit hit) { + hit.t = Double.MAX_VALUE; + hit.entry = null; + nearestNode(root, ox, oy, oz, dx, dy, dz, hit); + if (hit.entry == null) + return false; + hit.pointX = ox + dx * hit.t; + hit.pointY = oy + dy * hit.t; + hit.pointZ = oz + dz * hit.t; + return true; + } + + private void nearestNode(final Node node, final double ox, final double oy, final double oz, + final double dx, final double dy, final double dz, final Hit hit) { + if (!rayBox(node, ox, oy, oz, dx, dy, dz, hit.t)) + return; + + if (node.entries != null) { + for (final Entry e : node.entries) { + final double t = rayTriangle(ox, oy, oz, dx, dy, dz, e.v); + if (t > EPSILON && t < hit.t) { + hit.t = t; + hit.entry = e; + } + } + return; + } + nearestNode(node.left, ox, oy, oz, dx, dy, dz, hit); + nearestNode(node.right, ox, oy, oz, dx, dy, dz, hit); + } + + /** + * Any-hit shadow query: is the segment from the origin to + * {@code maxT} along the direction blocked? + * + * @return true if any triangle intersects the segment + */ + public boolean occluded(final double ox, final double oy, final double oz, + final double dx, final double dy, final double dz, + final double maxT) { + return occludedNode(root, ox, oy, oz, dx, dy, dz, maxT); + } + + private boolean occludedNode(final Node node, final double ox, final double oy, final double oz, + final double dx, final double dy, final double dz, final double maxT) { + if (!rayBox(node, ox, oy, oz, dx, dy, dz, maxT)) + return false; + + if (node.entries != null) { + for (final Entry e : node.entries) { + final double t = rayTriangle(ox, oy, oz, dx, dy, dz, e.v); + if (t > EPSILON && t < maxT) + return true; + } + return false; + } + return occludedNode(node.left, ox, oy, oz, dx, dy, dz, maxT) + || occludedNode(node.right, ox, oy, oz, dx, dy, dz, maxT); + } + + /** Slab test: does the ray hit the node box before limitT? */ + private boolean rayBox(final Node node, final double ox, final double oy, final double oz, + final double dx, final double dy, final double dz, final double limitT) { + double tMin = 0, tMax = limitT; + + double t1 = (node.minX - ox) / dx; + double t2 = (node.maxX - ox) / dx; + if (Double.isNaN(t1)) t1 = Double.NEGATIVE_INFINITY; + if (Double.isNaN(t2)) t2 = Double.POSITIVE_INFINITY; + tMin = Math.max(tMin, Math.min(t1, t2)); + tMax = Math.min(tMax, Math.max(t1, t2)); + + t1 = (node.minY - oy) / dy; + t2 = (node.maxY - oy) / dy; + if (Double.isNaN(t1)) t1 = Double.NEGATIVE_INFINITY; + if (Double.isNaN(t2)) t2 = Double.POSITIVE_INFINITY; + tMin = Math.max(tMin, Math.min(t1, t2)); + tMax = Math.min(tMax, Math.max(t1, t2)); + + t1 = (node.minZ - oz) / dz; + t2 = (node.maxZ - oz) / dz; + if (Double.isNaN(t1)) t1 = Double.NEGATIVE_INFINITY; + if (Double.isNaN(t2)) t2 = Double.POSITIVE_INFINITY; + tMin = Math.max(tMin, Math.min(t1, t2)); + tMax = Math.min(tMax, Math.max(t1, t2)); + + return tMax >= tMin && tMax > EPSILON; + } + + /** Möller–Trumbore, two-sided. Returns t or -1. */ + private double rayTriangle(final double ox, final double oy, final double oz, + final double dx, final double dy, final double dz, + final float[] v) { + final double e1x = v[3] - v[0], e1y = v[4] - v[1], e1z = v[5] - v[2]; + final double e2x = v[6] - v[0], e2y = v[7] - v[1], e2z = v[8] - v[2]; + + final double px = dy * e2z - dz * e2y; + final double py = dz * e2x - dx * e2z; + final double pz = dx * e2y - dy * e2x; + + final double det = e1x * px + e1y * py + e1z * pz; + if (det > -1e-12 && det < 1e-12) + return -1; + + final double invDet = 1.0 / det; + final double tx = ox - v[0], ty = oy - v[1], tz = oz - v[2]; + final double u = (tx * px + ty * py + tz * pz) * invDet; + if (u < 0 || u > 1) + return -1; + + final double qx = ty * e1z - tz * e1y; + final double qy = tz * e1x - tx * e1z; + final double qz = tx * e1y - ty * e1x; + final double vv = (dx * qx + dy * qy + dz * qz) * invDet; + if (vv < 0 || u + vv > 1) + return -1; + + final double t = (e2x * qx + e2y * qy + e2z * qz) * invDet; + return t > 0 ? t : -1; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/package-info.java new file mode 100644 index 0000000..daa9df9 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/gi/package-info.java @@ -0,0 +1,22 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Progressive CPU global illumination. + * + *

{@link eu.svjatoslav.aukio.e3d.renderer.raster.gi.GlobalIllumination} + * runs Monte Carlo ray casting on dedicated low-priority threads against a + * {@link eu.svjatoslav.aukio.e3d.renderer.raster.gi.TriangleBvh} built over + * the rendered polygons, and feeds per-polygon direct-light visibility + * (shadows) and indirect irradiance (bounced light) into the shading path + * through + * {@link eu.svjatoslav.aukio.e3d.renderer.raster.lighting.GiLightProvider}. + * Rendering itself never casts rays; illumination converges progressively + * and adapts to scene changes over time.

+ * + *

GI is strictly opt-in: enable it with + * {@code viewPanel.enableGlobalIllumination()}.

+ */ +package eu.svjatoslav.aukio.e3d.renderer.raster.gi; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.java new file mode 100644 index 0000000..f30cf3f --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/GiLightProvider.java @@ -0,0 +1,50 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.lighting; + +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; + +/** + * Optional provider of global-illumination data for + * {@link LightingManager}. When a provider is installed, the per-polygon + * lighting computation additionally: + * + *
    + *
  • asks the provider whether each light source is occluded from the + * polygon (direct-light shadows), and
  • + *
  • adds the provider's indirect (bounced light) contribution.
  • + *
+ * + *

Implementations are called from parallel render-pool threads: all + * methods must be thread-safe, fast and allocation-free. Providers are + * expected to answer from progressively updated caches.

+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.gi.GlobalIllumination the progressive CPU global illumination system + */ +public interface GiLightProvider { + + /** + * Tells whether a light source currently has an unobstructed path to + * the polygon. Until the provider has computed the answer, it should + * return {@code true} (light visible): shadows then fade in gradually + * instead of popping out. + * + * @param polygon the shaded polygon + * @param light the light source being evaluated + * @return true if the light reaches the polygon + */ + boolean isLightVisible(SolidPolygon polygon, LightSource light); + + /** + * Adds the polygon's indirect (bounced light) contribution to an + * already computed direct-lighting color, in place. + * + * @param polygon the shaded polygon + * @param baseColor the polygon's unlit color (for albedo scaling) + * @param result the direct-lighted color to augment (modified in place) + */ + void addIndirectLight(SolidPolygon polygon, Color baseColor, Color result); +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.java new file mode 100644 index 0000000..2bde5e6 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightSource.java @@ -0,0 +1,136 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.lighting; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; + +/** + * Represents a light source in the 3D scene with position, color, and intensity. + * + *

Light sources emit colored light that illuminates polygons based on their + * orientation relative to the light. The intensity of illumination follows the + * Lambert cosine law - surfaces facing the light receive full intensity, while + * surfaces at an angle receive proportionally less light.

+ * + *

Usage example:

+ *
{@code
+ * // Create a yellow light source at position (100, -50, 200)
+ * LightSource light = new LightSource(
+ *     new Point3D(100, -50, 200),
+ *     Color.YELLOW,
+ *     1.5
+ * );
+ *
+ * // Move the light source
+ * light.setPosition(new Point3D(0, 0, 300));
+ *
+ * // Change the light color
+ * light.setColor(new Color(255, 100, 50));
+ *
+ * // Adjust intensity
+ * light.setIntensity(2.0);
+ * }
+ * + * @see LightingManager manages multiple light sources and calculates shading + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon + */ +public class LightSource { + + /** + * Position of the light source in 3D world space. + */ + private Point3D position; + + /** + * Color of the light emitted by this source. + */ + private Color color; + + /** + * Intensity multiplier for this light source. + * Values greater than 1.0 make the light brighter, values less than 1.0 make it dimmer. + * High intensity values can cause surfaces to appear white (clamped at 255). + */ + private double intensity; + + /** + * Creates a new light source at the specified position with the given color and intensity. + * + * @param position the position of the light in world space + * @param color the color of the light + * @param intensity the intensity multiplier (1.0 = normal brightness) + */ + public LightSource(final Point3D position, final Color color, final double intensity) { + this.position = position; + this.color = color; + this.intensity = intensity; + } + + /** + * Creates a new light source at the specified position with the given color. + * Default intensity is 1.0. + * + * @param position the position of the light in world space + * @param color the color of the light + */ + public LightSource(final Point3D position, final Color color) { + this(position, color, 1.0); + } + + /** + * Returns the color of this light source. + * + * @return the light color + */ + public Color getColor() { + return color; + } + + /** + * Returns the intensity multiplier of this light source. + * + * @return the intensity multiplier + */ + public double getIntensity() { + return intensity; + } + + /** + * Returns the position of this light source. + * + * @return the position in world space + */ + public Point3D getPosition() { + return position; + } + + /** + * Sets the color of this light source. + * + * @param color the new light color + */ + public void setColor(final Color color) { + this.color = color; + } + + /** + * Sets the intensity multiplier of this light source. + * + * @param intensity the new intensity multiplier (1.0 = normal brightness) + */ + public void setIntensity(final double intensity) { + this.intensity = intensity; + } + + /** + * Sets the position of this light source. + * + * @param position the new position in world space + */ + public void setPosition(final Point3D position) { + this.position = position; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.java new file mode 100644 index 0000000..6ec3972 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/LightingManager.java @@ -0,0 +1,278 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.lighting; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; + +import java.util.ArrayList; +import java.util.List; + +/** + * Manages light sources in the scene and calculates lighting for polygons. + * + *

This class implements flat shading using the Lambert cosine law. For each + * polygon face, it calculates the surface normal and determines how much light + * each source contributes based on the angle between the normal and the light + * direction.

+ * + *

The lighting calculation considers:

+ *
    + *
  • Distance from polygon center to each light source
  • + *
  • Angle between surface normal and light direction
  • + *
  • Color and intensity of each light source
  • + *
+ * + *

Usage example:

+ *
{@code
+ * LightingManager lighting = new LightingManager();
+ *
+ * // Add light sources
+ * lighting.addLight(new LightSource(new Point3D(100, -50, 200), Color.YELLOW));
+ * lighting.addLight(new LightSource(new Point3D(-100, 50, 200), Color.BLUE));
+ *
+ * // Set ambient light (base illumination)
+ * lighting.setAmbientLight(new Color(30, 30, 30));
+ *
+ * // Calculate shaded color for a polygon (reusing result Color to avoid allocation)
+ * Color result = new Color();
+ * lighting.computeLighting(polygonCenter, surfaceNormal, baseColor, result);
+ * }
+ * + * @see LightSource represents a single light source + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon + */ +public class LightingManager { + + private final List lights = new ArrayList<>(); + private Color ambientLight = new Color(10, 10, 10); + + /** + * Optional global-illumination provider. When set, the polygon-aware + * {@link #computeLighting(SolidPolygon, Point3D, Point3D, Color, Color)} + * overload adds shadow tests and indirect light. Null by default: + * lighting behaves exactly as before (no occlusion, no indirect). + */ + private volatile GiLightProvider giProvider; + + /** + * Creates a new lighting manager with no light sources. + */ + public LightingManager() { + } + + /** + * Adds a light source to the scene. + * + * @param light the light source to add + */ + public void addLight(final LightSource light) { + lights.add(light); + } + + /** + * Computes lighting for a polygon and stores the result in an existing Color. + * + *

This method avoids allocation by reusing an existing Color instance. + * Safe to call from multiple threads on the same result Color - the computation + * is deterministic (same polygon, same lights = same result).

+ * + * @param polygonCenter the center point of the polygon in world space + * @param normal the surface normal vector (should be normalized) + * @param baseColor the original color of the polygon + * @param result the Color to receive the shaded result (modified in place) + */ + public void computeLighting(final Point3D polygonCenter, + final Point3D normal, + final Color baseColor, + final Color result) { + // Start with ambient light contribution + int totalR = ambientLight.r; + int totalG = ambientLight.g; + int totalB = ambientLight.b; + + // Calculate contribution from each light source + for (final LightSource light : lights) { + final Point3D lightPos = light.getPosition(); + final Color lightColor = light.getColor(); + final double lightIntensity = light.getIntensity(); + + // Calculate vector from polygon to light + final double lightDirX = lightPos.x - polygonCenter.x; + final double lightDirY = lightPos.y - polygonCenter.y; + final double lightDirZ = lightPos.z - polygonCenter.z; + + // Normalize the light direction + final double lightDist = Math.sqrt( + lightDirX * lightDirX + + lightDirY * lightDirY + + lightDirZ * lightDirZ + ); + + if (lightDist < 0.0001) + continue; + + final double invLightDist = 1.0 / lightDist; + final double normLightDirX = lightDirX * invLightDist; + final double normLightDirY = lightDirY * invLightDist; + final double normLightDirZ = lightDirZ * invLightDist; + + // Calculate dot product (Lambert cosine law) + final double dotProduct = normal.x * normLightDirX + + normal.y * normLightDirY + + normal.z * normLightDirZ; + + // Only add light if surface faces the light + if (dotProduct > 0) { + // Apply distance attenuation (inverse square law, simplified) + final double attenuation = 1.0 / (1.0 + 0.0001 * lightDist * lightDist); + final double intensity = dotProduct * attenuation * lightIntensity; + + // Add light color contribution + totalR += (int) (lightColor.r * intensity); + totalG += (int) (lightColor.g * intensity); + totalB += (int) (lightColor.b * intensity); + } + } + + // Clamp values to valid range and apply to base color + final int r = Math.min(255, (totalR * baseColor.r) / 255); + final int g = Math.min(255, (totalG * baseColor.g) / 255); + final int b = Math.min(255, (totalB * baseColor.b) / 255); + + result.set(r, g, b, baseColor.a); + } + + /** + * GI-aware lighting computation: identical to + * {@link #computeLighting(Point3D, Point3D, Color, Color)} when no + * {@link GiLightProvider} is installed. With a provider, lights occluded + * from the polygon are skipped (direct shadows) and the provider's + * indirect contribution is added on top. + * + * @param polygon the polygon being shaded (provider key) + * @param polygonCenter the center point of the polygon in world space + * @param normal the surface normal vector (should be normalized) + * @param baseColor the original color of the polygon + * @param result the Color to receive the shaded result (modified in place) + */ + public void computeLighting(final SolidPolygon polygon, + final Point3D polygonCenter, + final Point3D normal, + final Color baseColor, + final Color result) { + final GiLightProvider gi = giProvider; + if (gi == null) { + computeLighting(polygonCenter, normal, baseColor, result); + return; + } + + int totalR = ambientLight.r; + int totalG = ambientLight.g; + int totalB = ambientLight.b; + + for (final LightSource light : lights) { + if (!gi.isLightVisible(polygon, light)) + continue; + + final Point3D lightPos = light.getPosition(); + final Color lightColor = light.getColor(); + final double lightIntensity = light.getIntensity(); + + final double lightDirX = lightPos.x - polygonCenter.x; + final double lightDirY = lightPos.y - polygonCenter.y; + final double lightDirZ = lightPos.z - polygonCenter.z; + + final double lightDist = Math.sqrt( + lightDirX * lightDirX + + lightDirY * lightDirY + + lightDirZ * lightDirZ + ); + + if (lightDist < 0.0001) + continue; + + final double invLightDist = 1.0 / lightDist; + final double dotProduct = normal.x * lightDirX * invLightDist + + normal.y * lightDirY * invLightDist + + normal.z * lightDirZ * invLightDist; + + if (dotProduct > 0) { + final double attenuation = 1.0 / (1.0 + 0.0001 * lightDist * lightDist); + final double intensity = dotProduct * attenuation * lightIntensity; + + totalR += (int) (lightColor.r * intensity); + totalG += (int) (lightColor.g * intensity); + totalB += (int) (lightColor.b * intensity); + } + } + + final int r = Math.min(255, (totalR * baseColor.r) / 255); + final int g = Math.min(255, (totalG * baseColor.g) / 255); + final int b = Math.min(255, (totalB * baseColor.b) / 255); + + result.set(r, g, b, baseColor.a); + gi.addIndirectLight(polygon, baseColor, result); + } + + /** + * Installs (or clears, with null) the global-illumination provider used + * by the polygon-aware computeLighting overload. + * + * @param giProvider the provider, or null to disable GI + */ + public void setGiProvider(final GiLightProvider giProvider) { + this.giProvider = giProvider; + } + + /** + * Returns the installed global-illumination provider, or null. + * + * @return the GI provider or null + */ + public GiLightProvider getGiProvider() { + return giProvider; + } + + /** + * Returns the ambient light color. + * + * @return the ambient light color + */ + public Color getAmbientLight() { + return ambientLight; + } + + /** + * Sets the ambient light color for the scene. + * + *

Ambient light provides base illumination that affects all surfaces + * equally, regardless of their orientation.

+ * + * @param ambientLight the ambient light color + */ + public void setAmbientLight(final Color ambientLight) { + this.ambientLight = ambientLight; + } + + /** + * Returns all light sources in the scene. + * + * @return list of light sources + */ + public List getLights() { + return lights; + } + + /** + * Removes a light source from the scene. + * + * @param light the light source to remove + */ + public void removeLight(final LightSource light) { + lights.remove(light); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/package-info.java new file mode 100644 index 0000000..ce0963f --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/lighting/package-info.java @@ -0,0 +1,21 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Lighting system for flat-shaded polygon rendering. + * + *

This package implements a simple Lambertian lighting model for shading + * solid polygons based on their surface normals relative to light sources.

+ * + *

Key classes:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager} - Manages lights and calculates shading
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightSource} - Represents a point light source
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.lighting.LightingManager + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster.lighting; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/package-info.java new file mode 100755 index 0000000..0d6a073 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/package-info.java @@ -0,0 +1,26 @@ +/** + * Rasterization-based real-time software renderer for the Aukio 3D engine. + * + *

This package provides a complete rasterization pipeline that renders 3D scenes + * to a 2D pixel buffer using traditional approaches:

+ *
    + *
  • Wireframe rendering - lines and wireframe shapes
  • + *
  • Solid polygon rendering - filled polygons with flat shading
  • + *
  • Textured polygon rendering - polygons with texture mapping and mipmap support
  • + *
  • Depth sorting - back-to-front Z-index ordering feeding the two-pass z-buffer paint
  • + *
+ * + *

Key classes in this package:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection} - root container for all 3D shapes in a scene
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator} - collects and depth-sorts shapes for rendering
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.Color} - RGBA color representation with predefined constants
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic basic shape primitives (lines, polygons) + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite composite shapes (boxes, grids, text) + * @see eu.svjatoslav.aukio.e3d.renderer.raster.texture texture and mipmap support + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster; + diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.java new file mode 100644 index 0000000..5d18d0a --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractCoordinateShape.java @@ -0,0 +1,643 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes; + +import eu.svjatoslav.aukio.e3d.geometry.Box; +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.math.TransformStack; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Base class for shapes defined by a list of vertex coordinates. + * + *

This is the foundation for all primitive renderable shapes such as lines, + * solid polygons, and textured polygons. Each shape has a list of vertices + * ({@link Vertex} objects) that define its geometry in 3D space.

+ * + *

During each render frame, the {@link #transform} method projects all vertices + * from world space to screen space. If all vertices are visible (in front of the camera), + * the shape is queued in the {@link RenderAggregator} for depth-sorted painting via + * the {@link #paint} method.

+ * + *

Creating a custom coordinate shape:

+ *
{@code
+ * public class Triangle extends AbstractCoordinateShape {
+ *     private final Color color;
+ *
+ *     public Triangle(Point3D p1, Point3D p2, Point3D p3, Color color) {
+ *         super(new Vertex(p1), new Vertex(p2), new Vertex(p3));
+ *         this.color = color;
+ *     }
+ *
+ *     public void paint(RenderingContext ctx) {
+ *         // Custom painting logic using ctx.graphics and
+ *         // vertices.get(i).transformedCoordinate for screen positions
+ *     }
+ * }
+ * }
+ * + * @see AbstractShape the parent class for all shapes + * @see Vertex wraps a 3D coordinate with its transformed (screen-space) position + * @see RenderAggregator collects and depth-sorts shapes before painting + */ +public abstract class AbstractCoordinateShape extends AbstractShape { + + /** + * Global counter used to assign unique IDs to shapes, ensuring deterministic + * rendering order for shapes at the same depth. + */ + private static final AtomicInteger lastShapeId = new AtomicInteger(); + + /** + * Unique identifier for this shape instance, used as a tiebreaker when + * sorting shapes with identical Z-depth values. + */ + public final int shapeId; + + /** + * The vertex coordinates that define this shape's geometry. + * Each vertex contains both the original world-space coordinate and + * a transformed screen-space coordinate computed during {@link #transform}. + * + *

Stored as a mutable list to support CSG operations that modify + * polygon vertices in place (splitting, flipping).

+ */ + public final List vertices; + + /** + * Average Z-depth of this shape in screen space after transformation, + * per buffer slot. Used by the {@link RenderAggregator} to sort shapes + * back-to-front (the queue order the two-pass z-buffer paint consumes). + * Access via {@link #getZ(RenderingContext)} / {@link #getZ(int)}. + */ + private double onScreenZ0; + private double onScreenZ1; + private double onScreenZ2; + + /** + * Screen-space Y bounds of this shape after transformation, per buffer + * slot, expanded by {@link #getScreenYMargin(RenderingContext)} so they + * cover every pixel {@link #paint} can touch. Valid only in frames where + * this shape was queued for rendering. Used by {@link RenderAggregator} + * to bin shapes per paint tile by overlap. + */ + private double onScreenMinY0; + private double onScreenMaxY0; + private double onScreenMinY1; + private double onScreenMaxY1; + private double onScreenMinY2; + private double onScreenMaxY2; + + /** + * Screen-space X bounds of this shape after transformation, per buffer + * slot, expanded by {@link #getScreenXMargin(RenderingContext)}. + */ + private double onScreenMinX0; + private double onScreenMaxX0; + private double onScreenMinX1; + private double onScreenMaxX1; + private double onScreenMinX2; + private double onScreenMaxX2; + + /** + * Writes this shape's screen state for one buffer slot in one call. + * Used by bulk transform paths ({@code TriangleMeshBlock}) whose + * triangles carry no per-vertex objects: the handle exposes the same + * per-slot values an object-backed transform would have written, so + * the Z comparator and tile binning keep reading plain fields. + * + * @param slot buffer slot (0, 1 or 2) + * @param z average camera-space Z + * @param minY screen-space minimum Y (with paint margins) + * @param maxY screen-space maximum Y + * @param minX screen-space minimum X (with paint margins) + * @param maxX screen-space maximum X + */ + protected final void setSlotScreenState(final int slot, final double z, + final double minY, final double maxY, + final double minX, final double maxX) { + if (slot == 0) { + onScreenZ0 = z; + onScreenMinY0 = minY; + onScreenMaxY0 = maxY; + onScreenMinX0 = minX; + onScreenMaxX0 = maxX; + } else if (slot == 1) { + onScreenZ1 = z; + onScreenMinY1 = minY; + onScreenMaxY1 = maxY; + onScreenMinX1 = minX; + onScreenMaxX1 = maxX; + } else { + onScreenZ2 = z; + onScreenMinY2 = minY; + onScreenMaxY2 = maxY; + onScreenMinX2 = minX; + onScreenMaxX2 = maxX; + } + } + + /** + * Near-plane-clipped vertex loop for this shape, per buffer slot. + * Null when the shape was NOT clipped this frame (all original vertices + * in front of the near plane) or when the shape was culled. When set, + * {@link #paint} must iterate THIS list instead of {@link #vertices}: + * the original vertices contain behind-camera positions whose projected + * screen coordinates are garbage (divide by z <= 0 flips signs). + * + *

The list holds a mix of original {@link Vertex} objects (their + * per-slot state was filled by the normal transform) and freshly + * created intersection vertices (filled via + * {@link Vertex#setCameraSpaceCoordinate}). Stored per slot so the + * double-buffered pipeline can transform frame N+1 into slot B while + * frame N is still painting from slot A.

+ */ + private List clippedVertices0; + + /** + * Cached subpixel-cull verdict (see the size check in + * {@link #transform}): while {@link #subpixelCulledEpoch} matches the + * context's epoch, transform returns immediately without any setup. + */ + private boolean subpixelCulled; + private int subpixelCulledEpoch = -1; + private List clippedVertices1; + private List clippedVertices2; + + /** + * Creates a shape with the specified number of vertices, each initialized + * to the origin (0, 0, 0). + * + * @param vertexCount the number of vertices in this shape + */ + public AbstractCoordinateShape(final int vertexCount) { + vertices = new ArrayList<>(vertexCount); + for (int i = 0; i < vertexCount; i++) { + vertices.add(new Vertex()); + } + shapeId = lastShapeId.getAndIncrement(); + } + + /** + * Creates a shape from the given vertices. + * + * @param vertices the vertices defining this shape's geometry + */ + public AbstractCoordinateShape(final Vertex... vertices) { + this.vertices = new ArrayList<>(Arrays.asList(vertices)); + shapeId = lastShapeId.getAndIncrement(); + } + + /** + * Creates a shape from a list of vertices. + * + * @param vertices the list of vertices defining this shape's geometry + */ + public AbstractCoordinateShape(final List vertices) { + this.vertices = vertices; + shapeId = lastShapeId.getAndIncrement(); + } + + /** + * Returns the average Z-depth of this shape in screen space for the + * context's buffer slot. + * + * @param renderingContext the rendering context (selects the buffer slot) + * @return the average Z-depth value, used for depth sorting + */ + public double getZ(final RenderingContext renderingContext) { + final int slot = renderingContext.vertexSlot; + return slot == 0 ? onScreenZ0 : slot == 1 ? onScreenZ1 : onScreenZ2; + } + + /** + * Returns the average Z-depth of this shape for an explicit buffer slot. + * + * @param slot buffer slot (0 or 1) + * @return the average Z-depth value, used for depth sorting + */ + public double getZ(final int slot) { + return slot == 0 ? onScreenZ0 : slot == 1 ? onScreenZ1 : onScreenZ2; + } + + /** + * Screen-space minimum Y bound (pixels) for the given buffer slot. + * + * @param slot buffer slot (0 or 1) + * @return minimum Y this shape's paint can touch + */ + public double onScreenMinY(final int slot) { + return slot == 0 ? onScreenMinY0 : slot == 1 ? onScreenMinY1 : onScreenMinY2; + } + + /** + * Screen-space maximum Y bound (pixels) for the given buffer slot. + * + * @param slot buffer slot (0 or 1) + * @return maximum Y this shape's paint can touch + */ + public double onScreenMaxY(final int slot) { + return slot == 0 ? onScreenMaxY0 : slot == 1 ? onScreenMaxY1 : onScreenMaxY2; + } + + /** + * Screen-space minimum X bound (pixels) for the given buffer slot. + * + * @param slot buffer slot (0 or 1) + * @return minimum X this shape's paint can touch + */ + public double onScreenMinX(final int slot) { + return slot == 0 ? onScreenMinX0 : slot == 1 ? onScreenMinX1 : onScreenMinX2; + } + + /** + * Screen-space maximum X bound (pixels) for the given buffer slot. + * + * @param slot buffer slot (0 or 1) + * @return maximum X this shape's paint can touch + */ + public double onScreenMaxX(final int slot) { + return slot == 0 ? onScreenMaxX0 : slot == 1 ? onScreenMaxX1 : onScreenMaxX2; + } + + /** + * Sets the average Z-depth for the given buffer slot. + * + * @param slot buffer slot (0 or 1) + * @param z average Z-depth value + */ + public void setZ(final int slot, final double z) { + if (slot == 0) + onScreenZ0 = z; + else if (slot == 1) + onScreenZ1 = z; + else + onScreenZ2 = z; + } + + /** + * Returns the axis-aligned bounding box computed from vertex coordinates. + * + *

The bounding box encompasses all vertices in this shape, computed + * by finding the minimum and maximum coordinates along each axis.

+ * + *

Caching: The bounding box is cached after first computation. + * If vertices change, call {@link #invalidateBounds()} before calling + * this method to trigger recomputation.

+ * + * @return the axis-aligned bounding box in local coordinates + */ + @Override + public Box getBoundingBox() { + if (cachedBoundingBox == null && !vertices.isEmpty()) { + // Compute bounds from vertex coordinates + double minX = Double.MAX_VALUE; + double maxX = -Double.MAX_VALUE; + double minY = Double.MAX_VALUE; + double maxY = -Double.MAX_VALUE; + double minZ = Double.MAX_VALUE; + double maxZ = -Double.MAX_VALUE; + + for (final Vertex vertex : vertices) { + final Point3D coord = vertex.coordinate; + minX = Math.min(minX, coord.x); + maxX = Math.max(maxX, coord.x); + minY = Math.min(minY, coord.y); + maxY = Math.max(maxY, coord.y); + minZ = Math.min(minZ, coord.z); + maxZ = Math.max(maxZ, coord.z); + } + + cachedBoundingBox = new Box( + new Point3D(minX, minY, minZ), + new Point3D(maxX, maxY, maxZ) + ); + } + return cachedBoundingBox != null ? cachedBoundingBox : super.getBoundingBox(); + } + + /** + * Translates all vertices by the specified offsets. + * + *

This method moves the entire shape by modifying each vertex's + * world-space coordinate. It also invalidates the cached bounding box + * so that frustum culling uses the correct bounds after movement.

+ * + *

Usage example:

+ *
{@code
+     * // Move shape 10 units up (Y decreases in Aukio 3D's coordinate system)
+     * shape.translate(0, -10, 0);
+     *
+     * // Move shape diagonally
+     * shape.translate(5, 0, 5);
+     * }
+ * + * @param dx offset along the X axis (positive = right) + * @param dy offset along the Y axis (positive = down, negative = up) + * @param dz offset along the Z axis (positive = away from camera) + */ + public void translate(final double dx, final double dy, final double dz) { + for (final Vertex vertex : vertices) { + vertex.coordinate.x += dx; + vertex.coordinate.y += dy; + vertex.coordinate.z += dz; + } + invalidateBounds(); + } + + /** + * Paints this shape onto the rendering context's pixel buffer. + * + *

This method is called after all shapes have been transformed and sorted + * by depth. Implementations should use the transformed screen-space coordinates + * from {@link Vertex#transformedCoordinate} to draw pixels.

+ * + * @param renderBuffer the rendering context containing the pixel buffer and graphics context + */ + public abstract void paint(RenderingContext renderBuffer); + + /** + * Extra screen-space Y distance beyond the vertex bounds that + * {@link #paint} can touch. Shapes whose paint output extends past the + * vertex positions (thick lines, billboards, text glyphs) must override + * this so tile binning does not drop them from tiles they + * partially overlap. + * + * @param renderingContext the rendering context (provides projection + * parameters for size computation) + * @return the Y margin in screen pixels (0 = vertex bounds are exact) + */ + protected double getScreenYMargin(final RenderingContext renderingContext) { + return 0; + } + + /** + * Extra screen-space X distance beyond the vertex bounds that + * {@link #paint} can touch. Same contract as + * {@link #getScreenYMargin(RenderingContext)}, for the X axis. + * + * @param renderingContext the rendering context (provides projection + * parameters for size computation) + * @return the X margin in screen pixels (0 = vertex bounds are exact) + */ + protected double getScreenXMargin(final RenderingContext renderingContext) { + return 0; + } + + /** + * {@inheritDoc} + * + *

Transforms all vertices to screen space by applying the current transform stack. + * If ALL vertices are behind the near plane the shape is culled; if SOME are, + * the vertex loop is clipped against the near plane (see + * {@link #clipToNearPlane(RenderingContext)}) and the clipped loop — not the + * original vertices — is queued for rendering. Computes the average Z-depth + * and screen bounds from the active (possibly clipped) vertices and queues + * this shape for rendering.

+ */ + @Override + public void transform(final TransformStack transforms, + final RenderAggregator aggregator, + final RenderingContext renderingContext) { + + // Cached subpixel-cull verdict: skip the entire setup (vertex + // transforms, clipping, bounds) while the verdict is fresh. The + // epoch advances on significant camera movement, so a culled + // shape is re-evaluated whenever it could have grown on screen. + if (subpixelCulled + && subpixelCulledEpoch == renderingContext.subpixelCullingEpoch) + return; + + final int slot = renderingContext.vertexSlot; + final double near = renderingContext.nearPlaneDistance; + boolean anyBehind = false; + boolean allBehind = true; + + // Indexed loops, not enhanced-for: an iterator per shape per + // frame was a measurable share of render-time allocation + // (~26 MB sampled over 8 frames at the FO4 spawn view). + for (int vi = 0; vi < vertices.size(); vi++) { + final Vertex geometryPoint = vertices.get(vi); + geometryPoint.calculateLocationRelativeToViewer(transforms, renderingContext); + if (geometryPoint.transformedCoordinate(renderingContext).z > near) + allBehind = false; + else + anyBehind = true; + } + + if (allBehind) { + setClippedVertices(slot, null); + return; + } + + final List active; + if (anyBehind) { + active = clipToNearPlane(renderingContext); + // Degenerate sliver (fewer points than a renderable primitive): + // a line needs 2, a polygon needs 3. + if (active.size() < Math.min(vertices.size(), 3)) { + setClippedVertices(slot, null); + return; + } + setClippedVertices(slot, active); + } else { + setClippedVertices(slot, null); + active = vertices; + } + + double accumulatedZ = 0; + double minY = Double.POSITIVE_INFINITY; + double maxY = Double.NEGATIVE_INFINITY; + double minX = Double.POSITIVE_INFINITY; + double maxX = Double.NEGATIVE_INFINITY; + + for (int vi = 0; vi < active.size(); vi++) { + final Vertex geometryPoint = active.get(vi); + + final Point3D transformed = geometryPoint.transformedCoordinate(renderingContext); + final Point2D onScreen = geometryPoint.onScreenCoordinate(renderingContext); + + accumulatedZ += transformed.z; + + if (onScreen.y < minY) + minY = onScreen.y; + if (onScreen.y > maxY) + maxY = onScreen.y; + if (onScreen.x < minX) + minX = onScreen.x; + if (onScreen.x > maxX) + maxX = onScreen.x; + } + + // Subpixel culling: raw projected span (before paint margins) + // below the threshold on both axes means the shape cannot cover + // even a fraction of one pixel. Cache the verdict so frames with + // a barely-moved camera skip this shape's whole transform setup. + final double cullThreshold = renderingContext.subpixelCullingThreshold; + if (cullThreshold > 0 + && maxX - minX < cullThreshold + && maxY - minY < cullThreshold) { + subpixelCulled = true; + subpixelCulledEpoch = renderingContext.subpixelCullingEpoch; + return; + } + subpixelCulled = false; + + final double z = accumulatedZ / active.size(); + final double marginY = getScreenYMargin(renderingContext); + final double loY = minY - marginY; + final double hiY = maxY + marginY; + final double marginX = getScreenXMargin(renderingContext); + final double loX = minX - marginX; + final double hiX = maxX + marginX; + if (slot == 0) { + onScreenZ0 = z; + onScreenMinY0 = loY; + onScreenMaxY0 = hiY; + onScreenMinX0 = loX; + onScreenMaxX0 = hiX; + } else if (slot == 1) { + onScreenZ1 = z; + onScreenMinY1 = loY; + onScreenMaxY1 = hiY; + onScreenMinX1 = loX; + onScreenMaxX1 = hiX; + } else { + onScreenZ2 = z; + onScreenMinY2 = loY; + onScreenMaxY2 = hiY; + onScreenMinX2 = loX; + onScreenMaxX2 = hiX; + } + // Screen-space culling: composite frustum culling skips whole + // invisible cells, but a visible cell still queues every one of + // its triangles — including those behind the camera or past the + // viewport edge. Bounds (with paint margins) are already computed + // above, so drop shapes that cannot touch the viewport: on dense + // scenes this shrinks the sort/paint queues several-fold. The + // test mirrors RenderAggregator tile binning, which uses the same + // margined bounds, so nothing paintable is ever dropped. + if (hiX < renderingContext.renderMinX + || loX >= renderingContext.renderMaxX + || hiY < renderingContext.renderMinY + || loY >= renderingContext.renderMaxY) { + return; + } + aggregator.queueShapeForRendering(this); + } + + /** + * Returns the near-plane-clipped vertex loop for the context's buffer + * slot, or null if this shape was not clipped (or was culled) this + * frame. Paint implementations must use this list when non-null. + * + * @param renderingContext the rendering context (selects the buffer slot) + * @return the clipped vertex loop, or null + */ + public List clippedVertices(final RenderingContext renderingContext) { + final int slot = renderingContext.vertexSlot; + return slot == 0 ? clippedVertices0 + : slot == 1 ? clippedVertices1 : clippedVertices2; + } + + /** + * Stores the clipped vertex loop for the given buffer slot. + * + * @param slot buffer slot (0, 1 or 2) + * @param clipped the clipped loop, or null to clear + */ + private void setClippedVertices(final int slot, final List clipped) { + if (slot == 0) + clippedVertices0 = clipped; + else if (slot == 1) + clippedVertices1 = clipped; + else + clippedVertices2 = clipped; + } + + /** + * Clips this shape's vertex loop against the near plane + * (camera-space z == {@code renderingContext.nearPlaneDistance}), + * Sutherland-Hodgman style. Assumes at least one vertex is in front and + * at least one is behind (checked by {@link #transform}). + * + *

For every edge crossing the plane a new intersection vertex is + * created with linearly interpolated position, UV and normal, and its + * per-slot screen position is computed immediately via + * {@link Vertex#setCameraSpaceCoordinate}. In-front original vertices + * pass through by reference.

+ * + *

A convex N-gon crossing the plane clips to a single contiguous + * loop of 3..N+1 vertices (a triangle can become a quad). Two-vertex + * shapes (lines) are clipped as a single open edge, yielding the + * in-front endpoint plus the intersection point.

+ * + * @param renderingContext the rendering context (provides the near + * distance and projection parameters) + * @return the clipped vertex loop in original winding order + */ + private List clipToNearPlane(final RenderingContext renderingContext) { + final double near = renderingContext.nearPlaneDistance; + final int n = vertices.size(); + // Lines (2 vertices) form one open edge, not a closed loop: + // iterating n edges would emit the intersection point twice. + final int edgeCount = n == 2 ? 1 : n; + final List result = new ArrayList<>(n + 1); + + for (int i = 0; i < edgeCount; i++) { + final Vertex current = vertices.get(i); + final Vertex next = vertices.get((i + 1) % n); + final Point3D c = current.transformedCoordinate(renderingContext); + final Point3D p = next.transformedCoordinate(renderingContext); + final boolean currentIn = c.z > near; + final boolean nextIn = p.z > near; + + if (currentIn) + result.add(current); + + if (currentIn != nextIn) { + final double t = (near - c.z) / (p.z - c.z); + result.add(interpolateAtPlane(current, next, c, p, t, renderingContext)); + } + } + return result; + } + + /** + * Creates the vertex where edge a->b crosses the near plane. + * Position, texture coordinate and normal are interpolated with the + * same parameter t (linear in 3D, which is exactly what + * perspective-correct texturing expects of a point on the edge). + */ + private static Vertex interpolateAtPlane(final Vertex a, final Vertex b, + final Point3D ca, final Point3D cb, + final double t, + final RenderingContext renderingContext) { + final double x = ca.x + (cb.x - ca.x) * t; + final double y = ca.y + (cb.y - ca.y) * t; + final double z = ca.z + (cb.z - ca.z) * t; + + final Point2D uv = (a.textureCoordinate != null && b.textureCoordinate != null) + ? new Point2D( + a.textureCoordinate.x + (b.textureCoordinate.x - a.textureCoordinate.x) * t, + a.textureCoordinate.y + (b.textureCoordinate.y - a.textureCoordinate.y) * t) + : null; + + final Vertex clipped = new Vertex(new Point3D(x, y, z), uv); + if (a.normal != null && b.normal != null) + clipped.normal = a.normal.interpolate(b.normal, t); + clipped.setCameraSpaceCoordinate(x, y, z, renderingContext); + return clipped; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.java new file mode 100644 index 0000000..633a3bd --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/AbstractShape.java @@ -0,0 +1,157 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes; + +import eu.svjatoslav.aukio.e3d.geometry.Box; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController; +import eu.svjatoslav.aukio.e3d.math.TransformStack; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator; + +/** + * Base class for all renderable shapes in the Aukio 3D engine. + * + *

Every shape that can be rendered must extend this class and implement the + * {@link #transform(TransformStack, RenderAggregator, RenderingContext)} method, + * which projects the shape from world space into screen space during each render frame.

+ * + *

Shapes can optionally have a {@link MouseInteractionController} attached to receive + * mouse click and hover events when the user interacts with the shape in the 3D view.

+ * + *

Shape hierarchy overview:

+ *
+ * AbstractShape
+ *   +-- AbstractCoordinateShape   (shapes with vertex coordinates: lines, polygons)
+ *   +-- AbstractCompositeShape    (groups of sub-shapes: boxes, grids, text canvases)
+ * 
+ * + * @see AbstractCoordinateShape for shapes defined by vertex coordinates + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape for compound shapes + * @see MouseInteractionController for handling mouse events on shapes + */ +public abstract class AbstractShape { + + /** + * Default constructor for abstract shape. + */ + public AbstractShape() { + } + + /** + * Optional controller that receives mouse interaction events (click, enter, exit) + * when the user interacts with this shape in the 3D view. + * Set to {@code null} if mouse interaction is not needed. + */ + public MouseInteractionController mouseInteractionController; + + /** + * Cached bounding box in local coordinates. + * Lazily computed on first call to {@link #getBoundingBox()}. + * Subclasses should set this to null when geometry changes to trigger recomputation. + */ + protected Box cachedBoundingBox = null; + + /** + * Returns the axis-aligned bounding box for this shape in local coordinates. + * + *

The bounding box is used for frustum culling to determine if the shape + * is potentially visible before expensive vertex transformations.

+ * + *

Conservative default: Returns a very large box that ensures + * the shape is always considered visible. Subclasses should override to + * provide tight bounds computed from their geometry.

+ * + *

Caching: The bounding box is cached after first computation. + * If geometry changes, call {@link #invalidateBounds()} to trigger + * recomputation on next call.

+ * + * @return the axis-aligned bounding box in local coordinates + */ + public Box getBoundingBox() { + if (cachedBoundingBox == null) { + // Conservative default: very large box (shape always visible) + cachedBoundingBox = new Box( + new Point3D(-1e10, -1e10, -1e10), + new Point3D(1e10, 1e10, 1e10) + ); + } + return cachedBoundingBox; + } + + /** + * Invalidates the cached bounding box, forcing recomputation on next call + * to {@link #getBoundingBox()}. + * + *

Call this method whenever the shape's geometry changes to ensure + * frustum culling uses up-to-date bounds. This is critical for shapes + * that move or deform after creation.

+ * + *

Usage example:

+ *
{@code
+     * // After modifying vertex coordinates directly:
+     * vertex.coordinate.translate(0, 10, 0);
+     * shape.invalidateBounds();
+     *
+     * // Or use translate() on AbstractCoordinateShape which handles this automatically
+     * }
+ */ + public void invalidateBounds() { + cachedBoundingBox = null; + } + + /** + * Assigns a mouse interaction controller to this shape. + * + *

Example usage:

+ *
{@code
+     * shape.setMouseInteractionController(new MouseInteractionController() {
+     *     public boolean mouseClicked(int button) {
+     *         System.out.println("Shape clicked!");
+     *         return true;
+     *     }
+     *     public boolean mouseEntered() { return false; }
+     *     public boolean mouseExited() { return false; }
+     * });
+     * }
+ * + * @param mouseInteractionController the controller to handle mouse events, + * or {@code null} to disable mouse interaction + */ + public void setMouseInteractionController( + final MouseInteractionController mouseInteractionController) { + this.mouseInteractionController = mouseInteractionController; + } + + /** + * Transforms this shape from world space to screen space and queues it for rendering. + * + *

This method is called once per frame for each shape in the scene. Implementations + * should apply the current transform stack to their vertices, compute screen-space + * coordinates, and if the shape is visible, add it to the {@link RenderAggregator} + * for depth-sorted painting.

+ * + * @param transforms the current stack of transforms (world-to-camera transformations) + * @param aggregator collects transformed shapes for depth-sorted rendering + * @param renderingContext provides frame dimensions, graphics context, and frame metadata + */ + public abstract void transform(final TransformStack transforms, + final RenderAggregator aggregator, + final RenderingContext renderingContext); + + /** + * Estimated cost of transforming this shape, in arbitrary units where + * a leaf primitive counts 1. Composites override this with their total + * subtree weight. Used by the parallel transform fork to decide where + * splitting pays off. + * + * @param renderingContext the rendering context (frame identity for caching) + * @return transform weight, always at least 1 + */ + public int getTransformWeight(final RenderingContext renderingContext) { + return 1; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java new file mode 100644 index 0000000..fa40bdc --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/Billboard.java @@ -0,0 +1,261 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap; + +/** + * A billboard: a texture that always faces the viewer. + * + *

This class implements the "billboard" rendering technique where the texture + * remains oriented towards the camera regardless of 3D position. The visible size + * is calculated based on distance from viewer (z-coordinate) and scale factor.

+ * + *

Texture mapping algorithm:

+ *
    + *
  1. Calculates screen coverage based on perspective
  2. + *
  3. Clips to viewport boundaries
  4. + *
  5. Maps texture pixels to screen pixels using proportional scaling
  6. + *
+ * + * @see GlowingPoint a billboard with a circular gradient texture + * @see Texture + */ +public class Billboard extends AbstractCoordinateShape { + + private static final double SCALE_MULTIPLIER = 0.005; + + /** + * The texture to display on this billboard. + */ + public final Texture texture; + + /** + * Scale factor for the billboard's visible size. + *
    + *
  • 0 means infinitely small
  • + *
  • 1 is recommended to maintain texture sharpness
  • + *
+ */ + private double scale; + + /** + * Creates a billboard at the specified position with the given scale and texture. + * + * @param point the 3D position of the billboard center + * @param scale the scale factor (1.0 is recommended for sharpness) + * @param texture the texture to display + */ + public Billboard(final Point3D point, final double scale, + final Texture texture) { + super(new Vertex(point)); + this.texture = texture; + setScale(scale); + } + + /** + * The screen-aligned quad extends this far above and below the center + * vertex, well beyond the (single-vertex) Y range. + */ + @Override + protected double getScreenYMargin(final RenderingContext renderingContext) { + return (renderingContext.width * scale * texture.primaryBitmap.height) + / vertices.get(0).transformedCoordinate(renderingContext).z; + } + + /** + * Same story on the X axis: the quad extends half its screen width + * to either side of the center vertex. + */ + @Override + protected double getScreenXMargin(final RenderingContext renderingContext) { + return (renderingContext.width * scale * texture.primaryBitmap.width) + / vertices.get(0).transformedCoordinate(renderingContext).z; + } + + /** + * Renders this billboard to the screen. + * + *

The billboard is rendered as a screen-aligned quad centered on the projected + * position. The size is computed based on distance and scale factor.

+ * + *

Performance optimization: Uses fixed-point incremental stepping to avoid + * per-pixel division, and inlines alpha blending to avoid method call overhead. + * This provides 50-70% better performance than the previous division-based approach.

+ * + * @param targetRenderingArea the rendering context containing the pixel buffer + */ + @Override + public void paint(final RenderingContext targetRenderingArea) { + // Sprites paint only in the alpha pass: they blend, depth-TEST + // against the opaque z-buffer (solid geometry occludes them), + // and never depth-WRITE. The sprite is screen-aligned, so 1/z + // is constant across the quad — one depth value for all pixels. + if (targetRenderingArea.depthPass == 1) + return; + + // distance from camera/viewer to center of the texture + final double z = vertices.get(0).transformedCoordinate(targetRenderingArea).z; + final double zw = 1d / z; + + // compute forward oriented texture visible distance from center + final double visibleHorizontalDistanceFromCenter = (targetRenderingArea.width + * scale * texture.primaryBitmap.width) / z; + + final double visibleVerticalDistanceFromCenter = (targetRenderingArea.width + * scale * texture.primaryBitmap.height) / z; + + // compute visible pixel density, and get appropriate bitmap + final double scale = (visibleHorizontalDistanceFromCenter * 2) + / texture.primaryBitmap.width; + + final TextureBitmap textureBitmap = texture.getMipmapForScale(scale); + + final Point2D onScreenCoordinate = vertices.get(0).onScreenCoordinate(targetRenderingArea); + + // compute Y + final int onScreenUncappedYStart = (int) (onScreenCoordinate.y - visibleVerticalDistanceFromCenter); + final int onScreenUncappedYEnd = (int) (onScreenCoordinate.y + visibleVerticalDistanceFromCenter); + final int onScreenUncappedHeight = onScreenUncappedYEnd - onScreenUncappedYStart; + + int onScreenCappedYStart = onScreenUncappedYStart; + int onScreenCappedYEnd = onScreenUncappedYEnd; + + // cap Y to upper screen border + if (onScreenCappedYStart < 0) + onScreenCappedYStart = 0; + + // cap Y to lower screen border + if (onScreenCappedYEnd > targetRenderingArea.height) + onScreenCappedYEnd = targetRenderingArea.height; + + // clamp to render Y bounds + onScreenCappedYStart = Math.max(onScreenCappedYStart, targetRenderingArea.renderMinY); + onScreenCappedYEnd = Math.min(onScreenCappedYEnd, targetRenderingArea.renderMaxY); + if (onScreenCappedYStart >= onScreenCappedYEnd) + return; + + // compute X + final int onScreenUncappedXStart = (int) (onScreenCoordinate.x - visibleHorizontalDistanceFromCenter); + final int onScreenUncappedXEnd = (int) (onScreenCoordinate.x + visibleHorizontalDistanceFromCenter); + final int onScreenUncappedWidth = onScreenUncappedXEnd - onScreenUncappedXStart; + + // cap X to left viewport border (supports stereo per-eye clipping) + int onScreenCappedXStart = onScreenUncappedXStart; + if (onScreenCappedXStart < targetRenderingArea.renderMinX) + onScreenCappedXStart = targetRenderingArea.renderMinX; + + // cap X to right viewport border (supports stereo per-eye clipping) + int onScreenCappedXEnd = onScreenUncappedXEnd; + if (onScreenCappedXEnd > targetRenderingArea.renderMaxX) + onScreenCappedXEnd = targetRenderingArea.renderMaxX; + + if (onScreenCappedXStart >= onScreenCappedXEnd) + return; + + final int[] targetPixels = targetRenderingArea.pixels; + final float[] targetDepth = targetRenderingArea.depth; + final int[] sourcePixels = textureBitmap.pixels; + final int textureWidth = textureBitmap.width; + final int textureHeight = textureBitmap.height; + final double depthMargin = RenderingContext.DEPTH_MARGIN_DZ * zw * zw; + final int targetWidth = targetRenderingArea.width; + + // Fixed-point (16.16) texture stepping values - eliminates per-pixel division + // Source X advances by textureWidth / onScreenUncappedWidth per screen pixel + final int sourceXStep = (textureWidth << 16) / onScreenUncappedWidth; + // Source Y advances by textureHeight / onScreenUncappedHeight per screen scanline + final int sourceYStep = (textureHeight << 16) / onScreenUncappedHeight; + + // Initialize source Y position (fixed-point) at the first capped scanline + int sourceY = ((onScreenCappedYStart - onScreenUncappedYStart) * sourceYStep); + + for (int y = onScreenCappedYStart; y < onScreenCappedYEnd; y++) { + + // Convert fixed-point Y to integer scanline base address + final int sourceYInt = sourceY >> 16; + final int scanlineBase = sourceYInt * textureWidth; + + // Initialize source X position (fixed-point) at the first capped pixel + int sourceX = ((onScreenCappedXStart - onScreenUncappedXStart) * sourceXStep); + + int targetOffset = (y * targetWidth) + onScreenCappedXStart; + + for (int x = onScreenCappedXStart; x < onScreenCappedXEnd; x++) { + + // depth-test (never write): solids occlude sprites + if (zw <= targetDepth[targetOffset] - depthMargin) { + sourceX += sourceXStep; + targetOffset++; + continue; + } + + // Convert fixed-point X to integer and compute source address + final int sourceAddress = scanlineBase + (sourceX >> 16); + + // Inline alpha blending from TextureBitmap.drawPixel() + final int sourcePixel = sourcePixels[sourceAddress]; + final int srcAlpha = (sourcePixel >> 24) & 0xff; + + if (srcAlpha != 0) { + if (srcAlpha == 255) { + // Fully opaque - direct copy + targetPixels[targetOffset] = sourcePixel; + } else { + // Semi-transparent - alpha blend + final int backgroundAlpha = 255 - srcAlpha; + + final int srcR = ((sourcePixel >> 16) & 0xff) * srcAlpha; + final int srcG = ((sourcePixel >> 8) & 0xff) * srcAlpha; + final int srcB = (sourcePixel & 0xff) * srcAlpha; + + final int destPixel = targetPixels[targetOffset]; + final int destR = (destPixel >> 16) & 0xff; + final int destG = (destPixel >> 8) & 0xff; + final int destB = destPixel & 0xff; + + final int r = ((destR * backgroundAlpha) + srcR) >> 8; + final int g = ((destG * backgroundAlpha) + srcG) >> 8; + final int b = ((destB * backgroundAlpha) + srcB) >> 8; + + targetPixels[targetOffset] = (r << 16) | (g << 8) | b; + } + } + + // Advance source X using fixed-point addition (no division!) + sourceX += sourceXStep; + targetOffset++; + } + + // Advance source Y using fixed-point addition (no division!) + sourceY += sourceYStep; + } + } + + /** + * Sets the scale factor for this billboard. + * + * @param scale the scale factor (1.0 is recommended for sharpness) + */ + public void setScale(final double scale) { + this.scale = scale * SCALE_MULTIPLIER; + } + + /** + * Returns the 3D position of this billboard. + * + * @return the center position in world coordinates + */ + public Point3D getLocation() { + return vertices.get(0).coordinate; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/GlowingPoint.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/GlowingPoint.java new file mode 100644 index 0000000..3b30912 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/GlowingPoint.java @@ -0,0 +1,114 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +import static java.lang.Math.pow; +import static java.lang.Math.sqrt; + +/** + * A glowing 3D point rendered with a circular gradient texture. + * + *

This class creates and reuses textures for glowing points of the same color. + * The texture is a circle with an alpha gradient from center to edge, ensuring + * a consistent visual appearance regardless of viewing angle.

+ * + *

Texture sharing: Glowing points of the same color share textures + * to reduce memory usage. Textures are garbage collected via WeakHashMap when + * no longer referenced.

+ * + * @see Billboard the parent class + * @see Color + */ +public class GlowingPoint extends Billboard { + + private static final int TEXTURE_RESOLUTION_PIXELS = 100; + + /** + * Set of all existing glowing points, used for texture sharing. + */ + private static final Set glowingPoints = Collections.newSetFromMap(new WeakHashMap<>()); + private final Color color; + + /** + * Creates a glowing point at the specified position with the given size and color. + * + * @param point the 3D position of the point + * @param pointSize the visible size of the point + * @param color the color of the glow + */ + public GlowingPoint(final Point3D point, final double pointSize, + final Color color) { + super(point, computeScale(pointSize), getTexture(color)); + this.color = color; + + synchronized (glowingPoints) { + glowingPoints.add(this); + } + } + + + /** + * Computes the scale factor from point size. + * + * @param pointSize the desired visible size + * @return the scale factor for the billboard + */ + private static double computeScale(double pointSize) { + return pointSize / ((double) (TEXTURE_RESOLUTION_PIXELS / 50f)); + } + + /** + * Returns a texture for a glowing point of the given color. + * + *

Attempts to reuse an existing texture from another glowing point of the + * same color. If none exists, creates a new texture.

+ * + * @param color the color of the glow + * @return a texture with a circular alpha gradient + */ + private static Texture getTexture(final Color color) { + // attempt to reuse texture from existing glowing point of the same color + synchronized (glowingPoints) { + for (GlowingPoint glowingPoint : glowingPoints) + if (color.equals(glowingPoint.color)) + return glowingPoint.texture; + } + + // existing texture not found, creating new one + return createTexture(color); + } + + /** + * Creates a texture for a glowing point of the given color. + * The texture is a circle with a gradient from transparent to the given color. + */ + private static Texture createTexture(final Color color) { + final Texture texture = new Texture(TEXTURE_RESOLUTION_PIXELS, TEXTURE_RESOLUTION_PIXELS, 1); + int halfResolution = TEXTURE_RESOLUTION_PIXELS / 2; + + for (int x = 0; x < TEXTURE_RESOLUTION_PIXELS; x++) + for (int y = 0; y < TEXTURE_RESOLUTION_PIXELS; y++) { + final int distanceFromCenter = (int) sqrt(pow(halfResolution - x, 2) + pow(halfResolution - y, 2)); + + int alpha = 255 - ((270 * distanceFromCenter) / halfResolution); + if (alpha < 0) + alpha = 0; + + texture.primaryBitmap.pixels[texture.primaryBitmap.getAddress(x, y)] = + (alpha << 24) | (color.r << 16) | (color.g << 8) | color.b; + } + + return texture; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java new file mode 100644 index 0000000..a0478d6 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/Line.java @@ -0,0 +1,537 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape; + +import java.util.List; + + +/** + * A 3D line segment with perspective-correct width and alpha blending. + *

+ * This class represents a line between two 3D points, rendered with a specified + * width that adjusts based on perspective (distance from the viewer). + * The line is drawn using interpolators to handle edge cases and alpha blending for + * transparency effects. + *

+ * The rendering algorithm: + * 1. For thin lines (below a threshold), draws single-pixel lines with alpha + * adjustment based on perspective. + * 2. For thicker lines, creates four interpolators to define the line's + * rectangular area and fills it scanline by scanline. + *

+ * Note: The width is scaled by the LINE_WIDTH_MULTIPLIER and adjusted based on + * the distance from the viewer (z-coordinate) to maintain a consistent visual size. + */ +public class Line extends AbstractCoordinateShape { + + private static final double MINIMUM_WIDTH_THRESHOLD = 1; + + private static final double LINE_WIDTH_MULTIPLIER = 0.2d; + + /** + * Thread-local interpolators for line rendering. + * Each rendering thread gets its own array to avoid race conditions. + */ + private static final ThreadLocal LINE_INTERPOLATORS = + ThreadLocal.withInitial(() -> { + final LineInterpolator[] arr = new LineInterpolator[4]; + for (int i = 0; i < arr.length; i++) { + arr[i] = new LineInterpolator(); + } + return arr; + }); + + /** + * width of the line. + */ + public final double width; + + /** + * Color of the line. + */ + public Color color; + + /** + * Creates a copy of an existing line with cloned coordinates and color. + * + * @param parentLine the line to copy + */ + public Line(final Line parentLine) { + this(parentLine.vertices.get(0).coordinate.clone(), + parentLine.vertices.get(1).coordinate.clone(), + new Color(parentLine.color), parentLine.width); + } + + /** + * Creates a line between two points with the specified color and width. + * + * @param point1 the starting point of the line + * @param point2 the ending point of the line + * @param color the color of the line + * @param width the width of the line in world units + */ + public Line(final Point3D point1, final Point3D point2, final Color color, + final double width) { + + super( + new Vertex(point1), + new Vertex(point2) + ); + + this.color = color; + this.width = width; + } + + /** + * Draws a thick line as a series of horizontal spans. + * + *

Each pixel is depth-tested against the z-buffer (lines + * participate in occlusion: solid geometry hides the parts of a + * line that lie behind it), but depth is never written — a line + * must not occlude geometry painted after it.

+ * + * @param line1 the left edge interpolator + * @param line2 the right edge interpolator + * @param y the Y coordinate of the scanline + * @param renderBuffer the rendering context to draw into + * @param p1x X of the line's first projected endpoint + * @param p1y Y of the line's first projected endpoint + * @param dtDx d(t)/dx of the 2D line parameter (xp / len²) + * @param dtDy d(t)/dy of the 2D line parameter (yp / len²) + * @param zwP1 1/z at the first endpoint + * @param zwDelta (1/z at second endpoint) - zwP1 + */ + private void drawHorizontalLine(final LineInterpolator line1, + final LineInterpolator line2, final int y, + final RenderingContext renderBuffer, + final double p1x, final double p1y, + final double dtDx, final double dtDy, + final double zwP1, final double zwDelta) { + + int x1 = line1.getX(y); + int x2 = line2.getX(y); + + double d1 = line1.getD(); + double d2 = line2.getD(); + + if (x1 > x2) { + final int tmp = x1; + x1 = x2; + x2 = tmp; + + final double tmp2 = d1; + d1 = d2; + d2 = tmp2; + } + + final int unclippedWidth = x2 - x1; + final double dinc = (d2 - d1) / unclippedWidth; + + // 1/z at the first (leftmost) span pixel: project the pixel onto + // the 2D line for its parameter t, then interpolate 1/z linearly + // (projectively correct along a projected 3D line). Computed + // after the endpoint swap so x1 is genuinely the smaller X. + double zw = zwP1 + ((((x1 - p1x) * dtDx) + ((y - p1y) * dtDy)) * zwDelta); + final double zwInc = dtDx * zwDelta; + + if (x1 < renderBuffer.renderMinX) { + d1 += (dinc * (renderBuffer.renderMinX - x1)); + zw += zwInc * (renderBuffer.renderMinX - x1); + x1 = renderBuffer.renderMinX; + } + + // x2 is exclusive (loop paints [x1, x2)): clamp to renderMaxX, + // not renderMaxX-1, or the rightmost tile column stays unpainted + if (x2 >= renderBuffer.renderMaxX) + x2 = renderBuffer.renderMaxX; + + final int drawnWidth = x2 - x1; + + int offset = (y * renderBuffer.width) + x1; + final int[] pixels = renderBuffer.pixels; + final float[] depth = renderBuffer.depth; + + final int lineAlpha = color.a; + + final int colorR = color.r; + final int colorG = color.g; + final int colorB = color.b; + + for (int i = 0; i < drawnWidth; i++) { + + final double alphaMultiplier = 1d - Math.abs(d1); + + final int realLineAlpha = (int) (lineAlpha * alphaMultiplier); + final int backgroundAlpha = 255 - realLineAlpha; + + // Depth-test like pass-2 translucent geometry: never write. + if (zw > depth[offset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) { + final int dest = pixels[offset]; + final int destR = (dest >> 16) & 0xff; + final int destG = (dest >> 8) & 0xff; + final int destB = dest & 0xff; + + final int newR = ((destR * backgroundAlpha) + (colorR * realLineAlpha)) >> 8; + final int newG = ((destG * backgroundAlpha) + (colorG * realLineAlpha)) >> 8; + final int newB = ((destB * backgroundAlpha) + (colorB * realLineAlpha)) >> 8; + + pixels[offset] = (newR << 16) | (newG << 8) | newB; + } + + offset++; + d1 += dinc; + zw += zwInc; + } + + } + + /** + * Draws a thin line as single pixels with alpha-adjusted color. + * Used for lines that appear thin on screen (below minimum width threshold). + * + * @param buffer the rendering context to draw into + * @param alpha the alpha value for the entire line + */ + private void drawSinglePixelHorizontalLine(final RenderingContext buffer, + final int alpha, + final Point2D onScreenPoint1, + final Point2D onScreenPoint2, + final double zwP1, + final double zwP2) { + int xStart = (int) onScreenPoint1.x; + int xEnd = (int) onScreenPoint2.x; + + int lineHeight; + int yBase; + final double zwA; + final double zwB; + + if (xStart > xEnd) { + final int tmp = xStart; + xStart = xEnd; + xEnd = tmp; + lineHeight = (int) (onScreenPoint1.y - onScreenPoint2.y); + yBase = (int) onScreenPoint2.y; + // walk runs from endpoint 2 to endpoint 1 + zwA = zwP2; + zwB = zwP1; + } else { + yBase = (int) onScreenPoint1.y; + lineHeight = (int) (onScreenPoint2.y - onScreenPoint1.y); + zwA = zwP1; + zwB = zwP2; + } + + final int lineWidth = xEnd - xStart; + if (lineWidth == 0) + return; + + final int[] pixels = buffer.pixels; + final float[] depth = buffer.depth; + final int backgroundAlpha = 255 - alpha; + + final int redWithAlpha = color.r * alpha; + final int greenWithAlpha = color.g * alpha; + final int blueWithAlpha = color.b * alpha; + + for (int relativeX = 0; relativeX <= lineWidth; relativeX++) { + final int x = xStart + relativeX; + + if ((x >= buffer.renderMinX) && (x < buffer.renderMaxX)) { + + final int y = yBase + ((relativeX * lineHeight) / lineWidth); + if ((y >= buffer.renderMinY) && (y < buffer.renderMaxY)) { + if ((y >= 0) && (y < buffer.height)) { + int offset = (y * buffer.width) + x; + + // depth-test (never write): solids occlude lines + final double zw = zwA + + (((double) relativeX / lineWidth) * (zwB - zwA)); + if (zw > depth[offset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) { + final int dest = pixels[offset]; + final int destR = (dest >> 16) & 0xff; + final int destG = (dest >> 8) & 0xff; + final int destB = dest & 0xff; + + final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8; + final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8; + final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8; + + pixels[offset] = (newR << 16) | (newG << 8) | newB; + } + } + } + } + } + + } + + /** + * Draws a thin vertical line as single pixels with alpha-adjusted color. + * Used for lines that appear thin on screen and are more vertical than horizontal. + * + * @param buffer the rendering context to draw into + * @param alpha the alpha value for the entire line + */ + private void drawSinglePixelVerticalLine(final RenderingContext buffer, + final int alpha, + final Point2D onScreenPoint1, + final Point2D onScreenPoint2, + final double zwP1, + final double zwP2) { + int yStart = (int) onScreenPoint1.y; + int yEnd = (int) onScreenPoint2.y; + + int lineWidth; + int xBase; + final double zwA; + final double zwB; + + if (yStart > yEnd) { + final int tmp = yStart; + yStart = yEnd; + yEnd = tmp; + lineWidth = (int) (onScreenPoint1.x - onScreenPoint2.x); + xBase = (int) onScreenPoint2.x; + // walk runs from endpoint 2 to endpoint 1 + zwA = zwP2; + zwB = zwP1; + } else { + xBase = (int) onScreenPoint1.x; + lineWidth = (int) (onScreenPoint2.x - onScreenPoint1.x); + zwA = zwP1; + zwB = zwP2; + } + + final int lineHeight = yEnd - yStart; + if (lineHeight == 0) + return; + + final int[] pixels = buffer.pixels; + final float[] depth = buffer.depth; + final int backgroundAlpha = 255 - alpha; + + final int redWithAlpha = color.r * alpha; + final int greenWithAlpha = color.g * alpha; + final int blueWithAlpha = color.b * alpha; + + for (int relativeY = 0; relativeY <= lineHeight; relativeY++) { + final int y = yStart + relativeY; + + if ((y >= buffer.renderMinY) && (y < buffer.renderMaxY)) { + if ((y >= 0) && (y < buffer.height)) { + + final int x = xBase + ((relativeY * lineWidth) / lineHeight); + if ((x >= buffer.renderMinX) && (x < buffer.renderMaxX)) { + int offset = (y * buffer.width) + x; + + // depth-test (never write): solids occlude lines + final double zw = zwA + + (((double) relativeY / lineHeight) * (zwB - zwA)); + if (zw > depth[offset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) { + final int dest = pixels[offset]; + final int destR = (dest >> 16) & 0xff; + final int destG = (dest >> 8) & 0xff; + final int destB = dest & 0xff; + + final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8; + final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8; + final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8; + + pixels[offset] = (newR << 16) | (newG << 8) | newB; + } + } + } + } + } + } + + /** + * Finds the index of the first interpolator (starting from startPointer) that contains the given Y coordinate. + * + * @param lineInterpolators the interpolators array + * @param startPointer the index to start searching from + * @param y the Y coordinate to search for + * @return the index of the interpolator, or -1 if not found + */ + private int getLineInterpolator(final LineInterpolator[] lineInterpolators, + final int startPointer, final int y) { + + for (int i = startPointer; i < lineInterpolators.length; i++) + if (lineInterpolators[i].containsY(y)) + return i; + return -1; + } + + /** + * The thick-line path widens the line perpendicular to its direction by + * the projected endpoint radii, reaching beyond the vertex Y range. + * The thin paths stay within the vertex Y range, but there the radius + * is below 1 pixel anyway. + */ + @Override + protected double getScreenYMargin(final RenderingContext renderingContext) { + final List clipped = clippedVertices(renderingContext); + final Vertex p1 = clipped != null ? clipped.get(0) : vertices.get(0); + final Vertex p2 = clipped != null ? clipped.get(1) : vertices.get(1); + final double point1radius = (renderingContext.width * LINE_WIDTH_MULTIPLIER * width) + / p1.transformedCoordinate(renderingContext).z; + final double point2radius = (renderingContext.width * LINE_WIDTH_MULTIPLIER * width) + / p2.transformedCoordinate(renderingContext).z; + return Math.max(point1radius, point2radius); + } + + /** + * The thick-line widening is perpendicular to the line direction, so it + * reaches past the vertex range on the X axis exactly as much as on Y. + */ + @Override + protected double getScreenXMargin(final RenderingContext renderingContext) { + return getScreenYMargin(renderingContext); + } + + /** + * Renders this line to the screen using perspective-correct width and alpha blending. + * + *

This method handles two rendering modes:

+ *
    + *
  • Thin lines: When the projected width is below threshold, draws single-pixel + * lines with alpha adjusted for sub-pixel appearance.
  • + *
  • Thick lines: Creates four edge interpolators and fills the rectangular area + * scanline by scanline with perspective-correct alpha fading at edges.
  • + *
+ * + * @param buffer the rendering context containing the pixel buffer + */ + @Override + public void paint(final RenderingContext buffer) { + // Lines paint only in the alpha pass: they blend, depth-TEST + // against the opaque z-buffer (solid geometry occludes them), + // and never depth-WRITE (a line must not occlude later geometry). + if (buffer.depthPass == 1) + return; + + // Near-plane clip output takes precedence: a straddling line is + // shortened to its in-front endpoint plus the intersection point. + final List clipped = clippedVertices(buffer); + final Vertex endpoint1 = clipped != null ? clipped.get(0) : vertices.get(0); + final Vertex endpoint2 = clipped != null ? clipped.get(1) : vertices.get(1); + + final Point2D onScreenPoint1 = endpoint1.onScreenCoordinate(buffer); + final Point2D onScreenPoint2 = endpoint2.onScreenCoordinate(buffer); + + final double xp = onScreenPoint2.x - onScreenPoint1.x; + final double yp = onScreenPoint2.y - onScreenPoint1.y; + + final double z1 = endpoint1.transformedCoordinate(buffer).z; + final double z2 = endpoint2.transformedCoordinate(buffer).z; + + final double point1radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width) / z1; + final double point2radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width) / z2; + + // 1/z at the endpoints: interpolates linearly in screen space + // (projectively correct) — the basis for per-pixel depth tests. + final double zwP1 = 1d / z1; + final double zwP2 = 1d / z2; + + if ((point1radius < MINIMUM_WIDTH_THRESHOLD) + || (point2radius < MINIMUM_WIDTH_THRESHOLD)) { + + double averageRadius = (point1radius + point2radius) / 2; + + if (averageRadius > 1) + averageRadius = 1; + + final int alpha = (int) (color.a * averageRadius); + if (alpha < 2) + return; + + if (Math.abs(xp) > Math.abs(yp)) + drawSinglePixelHorizontalLine(buffer, alpha, onScreenPoint1, onScreenPoint2, zwP1, zwP2); + else + drawSinglePixelVerticalLine(buffer, alpha, onScreenPoint1, onScreenPoint2, zwP1, zwP2); + return; + } + + final double lineLength = Math.sqrt((xp * xp) + (yp * yp)); + + final double yinc1 = (point1radius * xp) / lineLength; + final double yinc2 = (point2radius * xp) / lineLength; + + final double xdec1 = (point1radius * yp) / lineLength; + final double xdec2 = (point2radius * yp) / lineLength; + + final double p1x1 = onScreenPoint1.x - xdec1; + final double p1y1 = onScreenPoint1.y + yinc1; + + final double p1x2 = onScreenPoint1.x + xdec1; + final double p1y2 = onScreenPoint1.y - yinc1; + + final double p2x1 = onScreenPoint2.x - xdec2; + final double p2y1 = onScreenPoint2.y + yinc2; + + final double p2x2 = onScreenPoint2.x + xdec2; + final double p2y2 = onScreenPoint2.y - yinc2; + + // Get thread-local interpolators + final LineInterpolator[] lineInterpolators = LINE_INTERPOLATORS.get(); + + lineInterpolators[0].setPoints(p1x1, p1y1, 1d, p2x1, p2y1, 1d); + lineInterpolators[1].setPoints(p1x2, p1y2, -1d, p2x2, p2y2, -1d); + + lineInterpolators[2].setPoints(p1x1, p1y1, 1d, p1x2, p1y2, -1d); + lineInterpolators[3].setPoints(p2x1, p2y1, 1d, p2x2, p2y2, -1d); + + double ymin = p1y1; + if (p1y2 < ymin) + ymin = p1y2; + if (p2y1 < ymin) + ymin = p2y1; + if (p2y2 < ymin) + ymin = p2y2; + if (ymin < 0) + ymin = 0; + + double ymax = p1y1; + if (p1y2 > ymax) + ymax = p1y2; + if (p2y1 > ymax) + ymax = p2y1; + if (p2y2 > ymax) + ymax = p2y2; + if (ymax >= buffer.height) + ymax = buffer.height - 1; + + // clamp to render Y bounds + ymin = Math.max(ymin, buffer.renderMinY); + ymax = Math.min(ymax, buffer.renderMaxY - 1); + if (ymin > ymax) + return; + + // 2D-line parameter gradients for per-pixel 1/z interpolation + final double len2 = (xp * xp) + (yp * yp); + final double dtDx = xp / len2; + final double dtDy = yp / len2; + + for (int y = (int) ymin; y <= ymax; y++) { + final int li1 = getLineInterpolator(lineInterpolators, 0, y); + if (li1 != -1) { + final int li2 = getLineInterpolator(lineInterpolators, li1 + 1, y); + if (li2 != -1) + drawHorizontalLine(lineInterpolators[li1], lineInterpolators[li2], y, + buffer, onScreenPoint1.x, onScreenPoint1.y, dtDx, dtDy, + zwP1, zwP2 - zwP1); + } + } + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineAppearance.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineAppearance.java new file mode 100644 index 0000000..e335b2f --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineAppearance.java @@ -0,0 +1,97 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; + + +/** + * Factory for creating Line objects with consistent appearance settings. + *

+ * This class encapsulates common line styling parameters (width and color) to + * avoid redundant configuration. It provides multiple constructors for + * flexibility and ensures default values are used when not specified. + * + *

Example usage:

+ *
{@code
+ * // Create a line appearance with default color and width 2.0
+ * LineAppearance appearance = new LineAppearance(2.0, Color.RED);
+ *
+ * // Create multiple lines with the same appearance
+ * Line line1 = appearance.getLine(new Point3D(0, 0, 100), new Point3D(10, 0, 100));
+ * Line line2 = appearance.getLine(new Point3D(0, 10, 100), new Point3D(10, 10, 100));
+ *
+ * // Override color for a specific line
+ * Line blueLine = appearance.getLine(p1, p2, Color.BLUE);
+ * }
+ */ +public class LineAppearance { + + private final double lineWidth; + + private Color color = new Color(100, 100, 255, 255); + + /** + * Creates a line appearance with default width (1.0) and default color (light blue). + */ + public LineAppearance() { + lineWidth = 1; + } + + /** + * Creates a line appearance with the specified width and default color (light blue). + * + * @param lineWidth the line width in world units + */ + public LineAppearance(final double lineWidth) { + this.lineWidth = lineWidth; + } + + /** + * Creates a line appearance with the specified width and color. + * + * @param lineWidth the line width in world units + * @param color the line color + */ + public LineAppearance(final double lineWidth, final Color color) { + this.lineWidth = lineWidth; + this.color = color; + } + + /** + * Creates a line between two points using this appearance's width and color. + * + * @param point1 the starting point of the line + * @param point2 the ending point of the line + * @return a new Line instance + */ + public Line getLine(final Point3D point1, final Point3D point2) { + return new Line(point1, point2, color, lineWidth); + } + + /** + * Creates a line between two points using this appearance's width and a custom color. + * + * @param point1 the starting point of the line + * @param point2 the ending point of the line + * @param color the color for this specific line (overrides the default) + * @return a new Line instance + */ + public Line getLine(final Point3D point1, final Point3D point2, + final Color color) { + return new Line(point1, point2, color, lineWidth); + } + + /** + * Returns the line width configured for this appearance. + * + * @return the line width in world units + */ + public double getLineWidth() { + return lineWidth; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineInterpolator.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineInterpolator.java new file mode 100644 index 0000000..83e8c6a --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/LineInterpolator.java @@ -0,0 +1,101 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line; + +/** + * Interpolates between two points along a line for scanline rendering. + *

+ * This class calculates screen coordinates and depth values (d) for a given Y + * position. It supports perspective-correct interpolation by tracking the + * distance between points and using it to compute step increments. + *

+ * The comparison logic prioritizes interpolators with greater vertical coverage + * to optimize scanline ordering. + */ +public class LineInterpolator { + + private double x1, y1, d1, x2, y2, d2; + + private double d; + private int height; + private int width; + private double dinc; + + /** + * Creates a new line interpolator with uninitialized endpoints. + */ + public LineInterpolator() { + } + + /** + * Checks if the given Y coordinate falls within the vertical span of this line. + * + * @param y the Y coordinate to test + * @return {@code true} if y is between y1 and y2 (inclusive) + */ + public boolean containsY(final int y) { + + if (y1 < y2) { + if (y >= y1) + return y <= y2; + } else if (y >= y2) + return y <= y1; + + return false; + } + + /** + * Returns the depth value (d) at the current Y position. + * + * @return the interpolated depth value + */ + public double getD() { + return d; + } + + /** + * Computes the X coordinate for the given Y position. + * + * @param y the Y coordinate + * @return the interpolated X coordinate + */ + public int getX(final int y) { + if (height == 0) + return (int) (x2 + x1) / 2; + + final int distanceFromY1 = y - (int) y1; + + d = d1 + ((dinc * distanceFromY1) / height); + + return (int) x1 + ((width * distanceFromY1) / height); + } + + /** + * Sets the endpoints and depth values for this line interpolator. + * + * @param x1 the X coordinate of the first point + * @param y1 the Y coordinate of the first point + * @param d1 the depth value at the first point + * @param x2 the X coordinate of the second point + * @param y2 the Y coordinate of the second point + * @param d2 the depth value at the second point + */ + public void setPoints(final double x1, final double y1, final double d1, + final double x2, final double y2, final double d2) { + + this.x1 = x1; + this.y1 = y1; + this.d1 = d1; + + this.x2 = x2; + this.y2 = y2; + this.d2 = d2; + + height = (int) y2 - (int) y1; + width = (int) x2 - (int) x1; + + dinc = d2 - d1; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/package-info.java new file mode 100644 index 0000000..1e3032d --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/line/package-info.java @@ -0,0 +1,22 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * 3D line segment rendering with perspective-correct width and alpha blending. + * + *

Lines are rendered with width that adjusts based on distance from the viewer. + * The rendering uses interpolators for smooth edges and proper alpha blending.

+ * + *

Key classes:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.Line} - The line shape
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance} - Color and width configuration
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineInterpolator} - Scanline edge interpolation
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.Line + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/package-info.java new file mode 100644 index 0000000..57d2f80 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/package-info.java @@ -0,0 +1,28 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Primitive shape implementations for the rasterization pipeline. + * + *

Basic shapes are the building blocks of 3D scenes. Each can be rendered + * independently and combined to create more complex objects.

+ * + *

Subpackages:

+ *
    + *
  • {@code line} - 3D line segments with perspective-correct width
  • + *
  • {@code solidpolygon} - Solid-color triangles with flat shading
  • + *
  • {@code texturedpolygon} - Triangles with UV-mapped textures
  • + *
+ * + *

Additional basic shapes:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.Billboard} - Textures that always face the camera
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.GlowingPoint} - Circular gradient billboards
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.Billboard + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java new file mode 100644 index 0000000..6fcef60 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java @@ -0,0 +1,151 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; + +import static java.lang.Math.round; + +/** + * Interpolates the x coordinate along a 2D line edge for scanline-based polygon rasterization. + * + *

{@code LineInterpolator} represents one edge of a polygon in screen space, defined by + * two {@link Point2D} endpoints. Given a scanline y coordinate, it computes the corresponding + * x coordinate via linear interpolation. This is a core building block for the solid polygon + * rasterizer, which fills triangles by sweeping horizontal scanlines and using two + * {@code LineInterpolator} instances to find the left and right x boundaries at each y level.

+ * + *

Subpixel precision: This class uses double-precision arithmetic throughout + * the interpolation pipeline to eliminate T-junction gaps. Vertices that should be at the + * same position but land at slightly different screen coordinates (e.g., 100.4 vs 100.6) + * will produce consistent interpolated results when rounded, ensuring adjacent polygons + * fill seamlessly without gaps.

+ * + *

Instances are {@link Comparable}, sorted by absolute height (tallest first) and then + * by width. This ordering is used during rasterization to select the primary (longest) edge + * of the triangle for the outer scanline loop.

+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon + * @see Point2D + */ +public class LineInterpolator { + + /** + * Small epsilon value for comparing near-zero heights to detect horizontal edges. + */ + private static final double EPSILON = 0.0001; + /** + * The first endpoint of this edge. + */ + Point2D p1; + /** + * The second endpoint of this edge. + */ + Point2D p2; + /** + * The vertical span (p2.y - p1.y) in double precision, which may be negative. + * + *

Stored as double to preserve subpixel precision during interpolation, + * eliminating rounding errors that cause T-junction gaps.

+ */ + private double height; + /** + * The horizontal span (p2.x - p1.x) in double precision, which may be negative. + * + *

Stored as double to preserve subpixel precision during interpolation.

+ */ + private double width; + + /** + * 1/z (the depth-buffer quantity) at the endpoints. Linear in screen + * space, so it rides the same edge interpolation as x; set only via + * {@link #setPointsZW} when the polygon participates in the z-buffer. + */ + private double zw1; + private double zw2; + private double zwSpan; + + /** + * Creates a new line interpolator with uninitialized endpoints. + */ + public LineInterpolator() { + } + + /** + * Tests whether the given y coordinate falls within the vertical span of this edge. + * + *

Uses double-precision comparison to handle subpixel vertex positions correctly.

+ * + * @param y the scanline y coordinate to test + * @return {@code true} if {@code y} is between the y coordinates of the two endpoints (inclusive) + */ + public boolean containsY(final int y) { + final double minY = Math.min(p1.y, p2.y); + final double maxY = Math.max(p1.y, p2.y); + return y >= minY && y <= maxY; + } + + /** + * Computes the interpolated x coordinate rounded to the nearest integer. + * + *

For horizontal edges (height near zero), returns the midpoint x value + * to avoid division by zero. This case should only occur when the edge + * spans exactly one scanline.

+ * + * @param y the scanline y coordinate + * @return the interpolated x coordinate rounded to the nearest integer + */ + public int getX(final int y) { + if (Math.abs(height) < EPSILON) { + return (int) round((p1.x + p2.x) / 2); + } + return (int) round(p1.x + (width * (y - p1.y)) / height); + } + + /** + * Sets the two endpoints of this edge and precomputes the width, height, and absolute height. + * + *

This method stores the endpoints directly and computes spans using double-precision + * arithmetic from the Point2D coordinates.

+ * + * @param p1 the first endpoint + * @param p2 the second endpoint + */ + public void setPoints(final Point2D p1, final Point2D p2) { + this.p1 = p1; + this.p2 = p2; + height = p2.y - p1.y; + width = p2.x - p1.x; + } + + /** + * Sets the depth-buffer endpoint values (1/z) for this edge. + * Companion to {@link #setPoints}; call after it. + * + * @param zw1 1/z at {@code p1} + * @param zw2 1/z at {@code p2} + */ + public void setPointsZW(final double zw1, final double zw2) { + this.zw1 = zw1; + this.zw2 = zw2; + zwSpan = zw2 - zw1; + } + + /** + * Computes the interpolated 1/z (depth value) at the given scanline. + * Uses the same interpolation parameter as {@link #getX}, so depth + * and x stay consistent along the edge. + * + * @param y the scanline y coordinate + * @return the interpolated 1/z + */ + public double getZW(final int y) { + if (Math.abs(height) < EPSILON) { + return (zw1 + zw2) / 2d; + } + return zw1 + (zwSpan * (y - p1.y)) / height; + } + +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java new file mode 100644 index 0000000..3a3ca1d --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java @@ -0,0 +1,823 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon; + +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.Plane; +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController; +import eu.svjatoslav.aukio.e3d.math.TransformStack; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static eu.svjatoslav.aukio.e3d.geometry.Polygon.pointWithinPolygon; + +/** + * A solid-color convex polygon renderer supporting N vertices (N >= 3). + * + *

This class serves as the unified polygon type for both rendering and CSG operations. + * It renders convex polygons by decomposing them into triangles using fan triangulation, + * and supports CSG operations directly without conversion to intermediate types.

+ * + *

Rendering:

+ *
    + *
  • Fan triangulation for N-vertex polygons (N-2 triangles)
  • + *
  • Scanline rasterization with per-pixel z-buffering and alpha blending
  • + *
  • Backface culling and flat shading support
  • + *
  • Mouse interaction via point-in-polygon testing
  • + *
+ * + *

CSG Support:

+ *
    + *
  • Lazy-computed plane for BSP operations
  • + *
  • {@link #flip()} for inverting polygon orientation
  • + *
  • {@link #deepClone()} for creating independent copies
  • + *
+ * + *

Usage examples:

+ *
{@code
+ * // Create a triangle
+ * SolidPolygon triangle = new SolidPolygon(
+ *     new Point3D(0, 0, 0),
+ *     new Point3D(50, 0, 0),
+ *     new Point3D(25, 50, 0),
+ *     Color.RED
+ * );
+ *
+ * // Create a quad
+ * SolidPolygon quad = SolidPolygon.quad(
+ *     new Point3D(-50, -50, 0),
+ *     new Point3D(50, -50, 0),
+ *     new Point3D(50, 50, 0),
+ *     new Point3D(-50, 50, 0),
+ *     Color.BLUE
+ * );
+ *
+ * // Use with CSG (via AbstractCompositeShape)
+ * SolidPolygonRectangularBox box = new SolidPolygonRectangularBox(...);
+ * box.subtract(sphere);
+ * }
+ * + * @see Plane for BSP plane operations + * @see LineInterpolator for scanline edge interpolation + */ +public class SolidPolygon extends AbstractCoordinateShape { + + /** + * Thread-local storage for line interpolators used during scanline rasterization. + * + *

Contains three interpolators representing the three edges of a triangle. + * ThreadLocal ensures thread safety when multiple threads render triangles + * concurrently, avoiding allocation during rendering by reusing these objects.

+ */ + private static final ThreadLocal INTERPOLATORS = + ThreadLocal.withInitial(() -> new LineInterpolator[]{ + new LineInterpolator(), new LineInterpolator(), new LineInterpolator() + }); + /** + * Thread-local storage for screen coordinates during rendering. + * Each rendering thread gets its own array to avoid race conditions. + */ + private static final ThreadLocal SCREEN_POINTS = new ThreadLocal<>(); + /** + * Reusable color for shading calculations. + * Computed once during transform phase, used during paint phase. + */ + private final Color shadedColor = new Color(); + /** + * Reusable point for polygon center calculation. + */ + private final Point3D cachedCenter = new Point3D(); + /** + * Reusable point for polygon normal calculation. + */ + private final Point3D cachedNormal = new Point3D(); + /** + * Cached plane containing this polygon, used for BSP operations. + * + *

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

+ */ + private Plane plane; + /** + * The fill color of this polygon. + */ + private Color color; + /** + * Whether flat shading is enabled for this polygon. + */ + private boolean shadingEnabled = false; + + /** + * Whether backface culling is enabled for this polygon. + */ + private boolean backfaceCulling = false; + + // ==================== CONSTRUCTORS ==================== + + /** + * Creates a solid polygon with the specified vertices and color. + * + * @param vertices the vertices defining the polygon (must have at least 3) + * @param color the fill color of the polygon + * @throws IllegalArgumentException if vertices is null or has fewer than 3 vertices + */ + public SolidPolygon(final Point3D[] vertices, final Color color) { + super(createVerticesFromPoints(vertices)); + if (vertices == null || vertices.length < 3) { + throw new IllegalArgumentException( + "Polygon must have at least 3 vertices, but got " + + (vertices == null ? "null" : vertices.length)); + } + this.color = color; + } + + /** + * Creates a solid polygon from a list of points and color. + * + * @param points the list of points defining the polygon (must have at least 3) + * @param color the fill color of the polygon + * @throws IllegalArgumentException if points is null or has fewer than 3 points + */ + public SolidPolygon(final List points, final Color color) { + super(createVerticesFromPoints(points)); + if (points == null || points.size() < 3) { + throw new IllegalArgumentException( + "Polygon must have at least 3 vertices, but got " + + (points == null ? "null" : points.size())); + } + this.color = color; + } + + /** + * Private constructor for creating a polygon from existing vertices. + * + *

Parameter order (color first) avoids erasure conflict with + * {@link #SolidPolygon(List, Color)} which takes List<Point3D>.

+ * + * @param color the fill color of the polygon + * @param vertices the list of Vertex objects (used directly, not copied) + */ + private SolidPolygon(final Color color, final List vertices) { + super(vertices); + this.color = color; + } + + /** + * Creates a solid triangle with the specified vertices and color. + * + * @param point1 the first vertex position + * @param point2 the second vertex position + * @param point3 the third vertex position + * @param color the fill color + */ + public SolidPolygon(final Point3D point1, final Point3D point2, + final Point3D point3, final Color color) { + super(new Vertex(point1), new Vertex(point2), new Vertex(point3)); + this.color = color; + } + + /** + * Creates a solid polygon from existing vertices. + * + *

Used for CSG operations and cloning where vertices already exist. + * The vertex list is used directly (not copied), so callers should not + * modify the list after passing it to this method.

+ * + * @param vertices the list of Vertex objects (used directly, not copied) + * @param color the fill color of the polygon + * @return a new SolidPolygon with the given vertices (shading disabled by default) + * @throws IllegalArgumentException if vertices is null or has fewer than 3 vertices + */ + public static SolidPolygon fromVertices(final List vertices, final Color color) { + return fromVertices(vertices, color, false); + } + + /** + * Creates a solid polygon from existing vertices with specified shading. + * + *

Used for CSG operations and cloning where vertices already exist. + * The vertex list is used directly (not copied), so callers should not + * modify the list after passing it to this method.

+ * + * @param vertices the list of Vertex objects (used directly, not copied) + * @param color the fill color of the polygon + * @param shadingEnabled whether shading is enabled for this polygon + * @return a new SolidPolygon with the given vertices and shading setting + * @throws IllegalArgumentException if vertices is null or has fewer than 3 vertices + */ + public static SolidPolygon fromVertices(final List vertices, final Color color, + final boolean shadingEnabled) { + if (vertices == null || vertices.size() < 3) { + throw new IllegalArgumentException( + "Polygon must have at least 3 vertices, but got " + + (vertices == null ? "null" : vertices.size())); + } + final SolidPolygon polygon = new SolidPolygon(color, vertices); + polygon.setShadingEnabled(shadingEnabled); + return polygon; + } + + // ==================== STATIC FACTORY METHODS ==================== + + /** + * Creates a triangle (3-vertex polygon). + * + * @param p1 the first vertex + * @param p2 the second vertex + * @param p3 the third vertex + * @param color the fill color + * @return a new SolidPolygon with 3 vertices + */ + public static SolidPolygon triangle(final Point3D p1, final Point3D p2, + final Point3D p3, final Color color) { + return new SolidPolygon(p1, p2, p3, color); + } + + /** + * Creates a quad (4-vertex polygon). + * + * @param p1 the first vertex + * @param p2 the second vertex + * @param p3 the third vertex + * @param p4 the fourth vertex + * @param color the fill color + * @return a new SolidPolygon with 4 vertices + */ + public static SolidPolygon quad(final Point3D p1, final Point3D p2, + final Point3D p3, final Point3D p4, final Color color) { + return new SolidPolygon(new Point3D[]{p1, p2, p3, p4}, color); + } + + // ==================== VERTEX HELPER METHODS ==================== + + /** + * Helper method to create Vertex list from Point3D array. + */ + private static List createVerticesFromPoints(final Point3D[] points) { + if (points == null || points.length < 3) { + return new ArrayList<>(); + } + final List verts = new ArrayList<>(points.length); + for (final Point3D point : points) { + verts.add(new Vertex(point)); + } + return verts; + } + + /** + * Helper method to create Vertex list from Point3D list. + */ + private static List createVerticesFromPoints(final List points) { + if (points == null || points.size() < 3) { + return new ArrayList<>(); + } + final List verts = new ArrayList<>(points.size()); + for (final Point3D point : points) { + verts.add(new Vertex(point)); + } + return verts; + } + + /** + * Draws a horizontal scanline between two edge interpolators with alpha + * blending and per-pixel depth testing. + * + *

The 1/z endpoint values ride the interpolators' zw channel; every + * pixel is depth-tested before writing. Opaque pixels write depth (pass + * 1), translucent pixels blend without a depth write (pass 2), so + * translucency never occludes.

+ * + * @param line1 the left edge interpolator + * @param line2 the right edge interpolator + * @param y the Y coordinate of the scanline + * @param renderBuffer the rendering context to draw into + * @param color the color to draw with + */ + private static void drawHorizontalLine(final LineInterpolator line1, + final LineInterpolator line2, final int y, + final RenderingContext renderBuffer, final Color color) { + + int x1 = line1.getX(y); + int x2 = line2.getX(y); + + double zw1 = line1.getZW(y); + double zw2 = line2.getZW(y); + + if (x1 > x2) { + final int tmp = x1; + x1 = x2; + x2 = tmp; + final double tmpZw = zw1; + zw1 = zw2; + zw2 = tmpZw; + } + + final double realX1 = x1; + final double realWidth = x2 - x1; + + if (x1 < renderBuffer.renderMinX) x1 = renderBuffer.renderMinX; + + // x2 is exclusive (loop paints [x1, x2)): clamp to renderMaxX, + // not renderMaxX-1, or the rightmost tile column stays unpainted + if (x2 >= renderBuffer.renderMaxX) x2 = renderBuffer.renderMaxX; + + final int width = x2 - x1; + if (width <= 0) + return; + + int offset = (y * renderBuffer.width) + x1; + final int[] pixels = renderBuffer.pixels; + final float[] depth = renderBuffer.depth; + // Alpha pass (depthPass 2): depth-test but never depth-write + final boolean writeDepth = renderBuffer.depthPass != 2; + + final double dzw = (zw2 - zw1) / realWidth; + double zw = zw1 + dzw * (x1 - realX1); + + final int polygonAlpha = color.a; + final int r = color.r; + final int g = color.g; + final int b = color.b; + + if (polygonAlpha == 255) { + final int pixel = (r << 16) | (g << 8) | b; + for (int i = 0; i < width; i++) { + if (zw > depth[offset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) { + pixels[offset] = pixel; + if (writeDepth) + depth[offset] = (float) zw; + } + offset++; + zw += dzw; + } + } else { + final int backgroundAlpha = 255 - polygonAlpha; + + final int redWithAlpha = r * polygonAlpha; + final int greenWithAlpha = g * polygonAlpha; + final int blueWithAlpha = b * polygonAlpha; + + // Blend form ((255-a)*dest + a*src) >> 8. Proven bit-identical + // to TexturedTriangle's lerp form dest + ((a*(src-dest) - dest) + // >> 8) for every (a,src,dest) — do NOT "fix" one to match the + // other cosmetically; both are the same formula. + for (int i = 0; i < width; i++) { + if (zw > depth[offset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) { + final int dest = pixels[offset]; + final int destR = (dest >> 16) & 0xff; + final int destG = (dest >> 8) & 0xff; + final int destB = dest & 0xff; + + final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8; + final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8; + final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8; + + pixels[offset] = (newR << 16) | (newG << 8) | newB; + } + offset++; + zw += dzw; + } + } + } + + /** + * Renders a triangle using scanline rasterization. + * + *

This static method handles:

+ *
    + *
  • Rounding vertices to integer screen coordinates
  • + *
  • Mouse hover detection via point-in-triangle test
  • + *
  • Viewport clipping
  • + *
  • Scanline rasterization with per-pixel z-buffering and alpha blending
  • + *
+ * + * @param context the rendering context + * @param onScreenPoint1 the first vertex in screen coordinates + * @param onScreenPoint2 the second vertex in screen coordinates + * @param onScreenPoint3 the third vertex in screen coordinates + * @param z1 camera-space z of the first vertex + * @param z2 camera-space z of the second vertex + * @param z3 camera-space z of the third vertex + * @param mouseInteractionController optional controller for mouse events, or null + * @param color the fill color + */ + public static void drawTriangle(final RenderingContext context, + final Point2D onScreenPoint1, final Point2D onScreenPoint2, + final Point2D onScreenPoint3, + final double z1, final double z2, final double z3, + final MouseInteractionController mouseInteractionController, + final Color color) { + + if (mouseInteractionController != null) + if (context.getMouseEvent() != null) + if (pointWithinPolygon(context.getMouseEvent().coordinate, onScreenPoint1, onScreenPoint2, onScreenPoint3)) + context.setCurrentObjectUnderMouseCursor(mouseInteractionController); + + if (color.isTransparent()) return; + + // Copy coordinates to local variables (don't modify original Point2D) + // Keep double precision to eliminate T-junction gaps from truncation errors + final double y1 = onScreenPoint1.y; + final double y2 = onScreenPoint2.y; + final double y3 = onScreenPoint3.y; + + // Find top-most point (use ceil to include all pixels triangle touches) + int yTop = (int) Math.ceil(Math.min(y1, Math.min(y2, y3))); + if (yTop < 0) yTop = 0; + + // Find bottom-most point (use floor to include all pixels triangle touches) + int yBottom = (int) Math.floor(Math.max(y1, Math.max(y2, y3))); + if (yBottom >= context.height) yBottom = context.height - 1; + + // Clamp to render Y bounds (use renderMaxY - 1 because loop is inclusive) + yTop = Math.max(yTop, context.renderMinY); + yBottom = Math.min(yBottom, context.renderMaxY - 1); + if (yTop > yBottom) return; + + // Paint using line interpolators + final LineInterpolator[] interp = INTERPOLATORS.get(); + final LineInterpolator li1 = interp[0]; + final LineInterpolator li2 = interp[1]; + final LineInterpolator li3 = interp[2]; + li1.setPoints(onScreenPoint1, onScreenPoint2); + li2.setPoints(onScreenPoint1, onScreenPoint3); + li3.setPoints(onScreenPoint2, onScreenPoint3); + + // 1/z rides the same edge interpolation; spans depth-test per pixel + li1.setPointsZW(1d / z1, 1d / z2); + li2.setPointsZW(1d / z1, 1d / z3); + li3.setPointsZW(1d / z2, 1d / z3); + + for (int y = yTop; y <= yBottom; y++) { + if (li1.containsY(y)) { + if (li2.containsY(y)) { + drawHorizontalLine(li1, li2, y, context, color); + } else if (li3.containsY(y)) { + drawHorizontalLine(li1, li3, y, context, color); + } + } else if (li2.containsY(y)) { + if (li3.containsY(y)) { + drawHorizontalLine(li2, li3, y, context, color); + } + } + } + } + + /** + * Returns the number of vertices in this polygon. + * + * @return the vertex count + */ + public int getVertexCount() { + return vertices.size(); + } + + /** + * Returns the fill color of this polygon. + * + * @return the polygon color + */ + public Color getColor() { + return color; + } + + /** + * Sets the fill color of this polygon. + * + * @param color the new color + */ + public void setColor(final Color color) { + this.color = color; + } + + /** + * Checks if shading is enabled for this polygon. + * + * @return true if shading is enabled, false otherwise + */ + public boolean isShadingEnabled() { + return shadingEnabled; + } + + /** + * Enables or disables shading for this polygon. + * + * @param shadingEnabled true to enable shading, false to disable + */ + public void setShadingEnabled(final boolean shadingEnabled) { + this.shadingEnabled = shadingEnabled; + } + + // ==================== CSG SUPPORT ==================== + + /** + * Checks if backface culling is enabled for this polygon. + * + * @return {@code true} if backface culling is enabled + */ + public boolean isBackfaceCullingEnabled() { + return backfaceCulling; + } + + /** + * Enables or disables backface culling for this polygon. + * + * @param backfaceCulling {@code true} to enable backface culling + */ + public void setBackfaceCulling(final boolean backfaceCulling) { + this.backfaceCulling = backfaceCulling; + } + + /** + * Returns the plane containing this polygon. + * + *

Computed from the first three vertices and cached for reuse. + * Used by BSP tree construction for spatial partitioning.

+ * + * @return the Plane containing this polygon + */ + public Plane getPlane() { + if (plane == null) { + plane = Plane.fromPoints( + vertices.get(0).coordinate, + vertices.get(1).coordinate, + vertices.get(2).coordinate + ); + } + return plane; + } + + // ==================== RENDERING ==================== + + /** + * Flips the orientation of this polygon. + * + *

Reverses the vertex order and negates vertex normals. + * Also flips the cached plane if computed. Used during CSG operations + * when inverting solids.

+ */ + public void flip() { + Collections.reverse(vertices); + for (final Vertex vertex : vertices) vertex.flip(); + if (plane != null) plane.flip(); + } + + /** + * Creates a deep clone of this polygon. + * + *

Clones all vertices and preserves the color, shading, and backface culling settings. + * Used by CSG operations to create independent copies before modification.

+ * + * @return a new SolidPolygon with cloned data and preserved settings + */ + public SolidPolygon deepClone() { + final List clonedVertices = new ArrayList<>(vertices.size()); + for (final Vertex v : vertices) { + clonedVertices.add(v.clone()); + } + final SolidPolygon clone = SolidPolygon.fromVertices(clonedVertices, color, shadingEnabled); + clone.backfaceCulling = this.backfaceCulling; + return clone; + } + + /** + * Calculates the centroid (geometric center) of this polygon. + * + * @param result the point to store the center in + */ + private void calculateCenter(final Point3D result) { + if (vertices.isEmpty()) { + result.x = result.y = result.z = 0; + return; + } + + double sumX = 0, sumY = 0, sumZ = 0; + for (final Vertex v : vertices) { + sumX += v.coordinate.x; + sumY += v.coordinate.y; + sumZ += v.coordinate.z; + } + + result.x = sumX / vertices.size(); + result.y = sumY / vertices.size(); + result.z = sumZ / vertices.size(); + } + + /** + * Calculates the signed area of this polygon in screen space. + * + * @param screenPoints the screen coordinates of this polygon's vertices + * @param vertexCount the number of vertices in the polygon + * @return the signed area (negative = front-facing in Y-down coordinate system) + */ + private double calculateSignedArea(final Point2D[] screenPoints, final int vertexCount) { + double area = 0; + final int n = vertexCount; + for (int i = 0; i < n; i++) { + final Point2D curr = screenPoints[i]; + final Point2D next = screenPoints[(i + 1) % n]; + area += curr.x * next.y - next.x * curr.y; + } + return area / 2.0; + } + + /** + * Tests whether a point lies inside this polygon using ray-casting. + * + * @param point the point to test + * @param screenPoints the screen coordinates of this polygon's vertices + * @param vertexCount the number of vertices in the polygon + * @return {@code true} if the point is inside the polygon + */ + private boolean isPointInsidePolygon(final Point2D point, final Point2D[] screenPoints, + final int vertexCount) { + int intersectionCount = 0; + final int n = vertexCount; + + for (int i = 0; i < n; i++) { + final Point2D p1 = screenPoints[i]; + final Point2D p2 = screenPoints[(i + 1) % n]; + + if (intersectsRay(point, p1, p2)) { + intersectionCount++; + } + } + + return (intersectionCount % 2) == 1; + } + + /** + * Tests if a horizontal ray from the point intersects the edge. + */ + private boolean intersectsRay(final Point2D point, Point2D edgeP1, Point2D edgeP2) { + if (edgeP1.y > edgeP2.y) { + final Point2D tmp = edgeP1; + edgeP1 = edgeP2; + edgeP2 = tmp; + } + + if (point.y < edgeP1.y || point.y > edgeP2.y) { + return false; + } + + final double dy = edgeP2.y - edgeP1.y; + if (Math.abs(dy) < 0.0001) { + return false; + } + + final double t = (point.y - edgeP1.y) / dy; + final double intersectX = edgeP1.x + t * (edgeP2.x - edgeP1.x); + + return point.x >= intersectX; + } + + /** + * Renders this polygon to the screen. + * + * @param renderBuffer the rendering context containing the pixel buffer + */ + @Override + public void paint(final RenderingContext renderBuffer) { + // Near-plane clip output takes precedence: a straddling triangle + // becomes a triangle or quad of in-front vertices; the original + // vertices hold behind-camera positions with garbage projections. + final List clipped = clippedVertices(renderBuffer); + final List active = clipped != null ? clipped : vertices; + + if (active.size() < 3 || color.isTransparent()) { + return; + } + + // Use pre-computed shaded color (computed during transform phase) + final Color paintColor = shadingEnabled ? shadedColor : color; + + // Z-buffer two-pass classification: opaque polygons paint in + // pass 1 (depth test + write), translucent ones in pass 2 + // (depth test, no write — translucency must not occlude). + // See RenderAggregator.paintSorted. + final boolean alphaClass = paintColor.a != 255; + if ((renderBuffer.depthPass == 1) == alphaClass) + return; + + // Get thread-local screen points array + final Point2D[] screenPoints = getScreenPoints(active.size()); + final double[] cameraZ = getCameraZ(active.size()); + + // Get screen coordinates and per-vertex depth + for (int i = 0; i < active.size(); i++) { + final Vertex vertex = active.get(i); + screenPoints[i] = vertex.onScreenCoordinate(renderBuffer); + cameraZ[i] = vertex.transformedCoordinate(renderBuffer).z; + } + + // Backface culling check + if (backfaceCulling) { + final double signedArea = calculateSignedArea(screenPoints, active.size()); + if (signedArea >= 0) { + return; + } + } + + // Mouse interaction + if (mouseInteractionController != null && renderBuffer.getMouseEvent() != null) { + if (isPointInsidePolygon(renderBuffer.getMouseEvent().coordinate, screenPoints, active.size())) { + renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController); + } + } + + // Only triangles can be rendered directly; N-vertex polygons must be triangulated + // by AbstractCompositeShape.rebuildRenderList() before rendering. The single + // exception: near-plane clipping can turn a renderable triangle into a quad + // (one corner cut off), painted here as a 2-triangle fan — the clip of a + // convex polygon stays convex, so fan triangulation is exact. + if (clipped == null && active.size() != 3) { + throw new IllegalStateException( + "SolidPolygon with " + active.size() + " vertices cannot be rendered directly. " + + "Only triangles (3 vertices) support direct rendering. " + + "For N-vertex polygons, use AbstractCompositeShape which triangulates when building its render list."); + } + + for (int i = 1; i + 1 < active.size(); i++) { + drawTriangle(renderBuffer, screenPoints[0], screenPoints[i], screenPoints[i + 1], + cameraZ[0], cameraZ[i], cameraZ[i + 1], + mouseInteractionController, paintColor); + } + } + + /** + * Thread-local storage for per-vertex camera-space z during rendering. + */ + private static final ThreadLocal CAMERA_Z = new ThreadLocal<>(); + + /** + * Gets a thread-local camera-z array sized for the given number of vertices. + * + * @param size the required array size + * @return a thread-local double array + */ + private double[] getCameraZ(final int size) { + double[] cameraZ = CAMERA_Z.get(); + if (cameraZ == null || cameraZ.length < size) { + cameraZ = new double[size]; + CAMERA_Z.set(cameraZ); + } + return cameraZ; + } + + /** + * Gets a thread-local screen points array sized for the given number of vertices. + * + * @param size the required array size + * @return a thread-local Point2D array + */ + private Point2D[] getScreenPoints(final int size) { + Point2D[] screenPoints = SCREEN_POINTS.get(); + if (screenPoints == null || screenPoints.length < size) { + screenPoints = new Point2D[size]; + SCREEN_POINTS.set(screenPoints); + } + return screenPoints; + } + + /** + * Transforms vertices to screen space and computes lighting once per frame. + * + *

Overrides parent to add lighting computation during the single-threaded + * transform phase. This ensures lighting is calculated only once per polygon + * per frame, rather than once per render thread.

+ * + * @param transforms the transform stack to apply + * @param aggregator the render aggregator to queue shapes into + * @param renderingContext the rendering context + */ + @Override + public void transform(final TransformStack transforms, + final RenderAggregator aggregator, + final RenderingContext renderingContext) { + // Transform vertices to screen space + super.transform(transforms, aggregator, renderingContext); + + // Compute lighting once during transform phase (single-threaded) + if (shadingEnabled && renderingContext.lightingManager != null) { + calculateCenter(cachedCenter); + // Compute normal from first 3 vertices + Plane.computeNormal( + vertices.get(0).coordinate, + vertices.get(1).coordinate, + vertices.get(2).coordinate, + cachedNormal + ); + renderingContext.lightingManager.computeLighting( + this, cachedCenter, cachedNormal, color, shadedColor); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java new file mode 100644 index 0000000..80d976e --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java @@ -0,0 +1,22 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Solid-color polygon rendering with scanline rasterization. + * + *

SolidPolygon is the unified polygon type for both rendering and CSG operations. + * It supports N vertices (N >= 3) and handles perspective-correct interpolation, + * alpha blending, viewport clipping, backface culling, and optional flat shading.

+ * + *

Key classes:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon} - Unified polygon for rendering and CSG
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.LineInterpolator} - Edge interpolation for scanlines
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/MeshTriangle.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/MeshTriangle.java new file mode 100644 index 0000000..391b17d --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/MeshTriangle.java @@ -0,0 +1,138 @@ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator; +import eu.svjatoslav.aukio.e3d.math.TransformStack; + +/** + * Paint/sort handle for one triangle of a {@link TriangleMeshBlock}. The + * triangle's geometry lives in the block's flat arrays (SoA layout); this + * object exists only so the existing sort/bin/paint pipeline — typed on + * individual shapes — can address one triangle. Handles are allocated once + * at block build time and reused every frame; {@link #transform} is a + * no-op because the block transforms all its triangles in one tight loop. + * + *

Per-slot screen data is read from the block's arrays through the + * overridden accessors, so the Z-comparator and tile binning see exactly + * the values an object-backed {@link TexturedTriangle} would expose.

+ */ +final class MeshTriangle extends TexturedTriangle { + + /** + * Scratch Point2D carriers for the flat paint call. Interpolators + * hold references to the screen/UV points they are given, so the + * objects must stay stable for the duration of one paint — but a + * paint never nests, so three screen + three UV points per thread + * suffice and nothing is allocated per triangle. + */ + private static final ThreadLocal SCREEN_SCRATCH = + ThreadLocal.withInitial(() -> new Point2D[]{ + new Point2D(), new Point2D(), new Point2D()}); + private static final ThreadLocal UV_SCRATCH = + ThreadLocal.withInitial(() -> new Point2D[]{ + new Point2D(), new Point2D(), new Point2D()}); + + private final TriangleMeshBlock block; + private final int index; + + MeshTriangle(final TriangleMeshBlock block, final int index, + final eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture texture) { + super(texture); + this.block = block; + this.index = index; + } + + /** + * No-op: the owning block transforms all its triangles (including this + * one) in a single flat-array loop. Queued handles are never + * transformed through the shape path. Sort Z and screen bounds live in + * the inherited per-slot fields, written by the block via + * {@code setSlotScreenState}. + */ + @Override + public void transform(final TransformStack transforms, + final RenderAggregator aggregator, + final RenderingContext renderingContext) { + // intentionally empty — see TriangleMeshBlock.transform + } + + /** + * Publishes this triangle's per-slot screen state (called by the + * owning block during its flat transform loop). Trampoline: the + * inherited setter is protected, so the call must go through the + * subclass. + */ + void publishSlotState(final int slot, final double z, + final double minY, final double maxY, + final double minX, final double maxX) { + setSlotScreenState(slot, z, minY, maxY, minX, maxX); + } + + @Override + public void paint(final RenderingContext renderBuffer) { + final int slot = renderBuffer.vertexSlot; + final Point2D[] screen = SCREEN_SCRATCH.get(); + final Point2D[] uvs = UV_SCRATCH.get(); + + final int clip = block.clipOffset(slot, index); + if (clip >= 0) { + // Near-plane straddler: the block clipped to a loop of 3-4 + // vertices, stored as (x, y, z, u, v, sx, sy) tuples. Paint + // as a fan, mirroring TexturedTriangle.paint's clipped path. + final int count = block.clipCount(slot, index); + final double[] store = block.clipStore(slot); + loadClipVertex(screen[0], uvs[0], store, clip); + for (int i = 1; i + 1 < count; i++) { + loadClipVertex(screen[1], uvs[1], store, clip + i * 7); + loadClipVertex(screen[2], uvs[2], store, clip + (i + 1) * 7); + // Fan sub-triangle screen perimeter — same expression as + // the object path (edge12 + edge13 + edge23); straddlers + // are rare, so it is computed here rather than stored. + final double dx01 = screen[0].x - screen[1].x; + final double dy01 = screen[0].y - screen[1].y; + final double dx02 = screen[0].x - screen[2].x; + final double dy02 = screen[0].y - screen[2].y; + final double dx12 = screen[1].x - screen[2].x; + final double dy12 = screen[1].y - screen[2].y; + final double visPerimeter = Math.sqrt(dx01 * dx01 + dy01 * dy01) + + Math.sqrt(dx02 * dx02 + dy02 * dy02) + + Math.sqrt(dx12 * dx12 + dy12 * dy12); + paintFlat(renderBuffer, block.texture(index), block.backfaceCull(), + screen[0], screen[1], screen[2], + uvs[0], uvs[1], uvs[2], + store[clip + 2], + store[clip + i * 7 + 2], + store[clip + (i + 1) * 7 + 2], + visPerimeter, + block.clipTtd(slot, clip)); + } + return; + } + + block.loadScreenVertex(screen[0], uvs[0], slot, index, 0, renderBuffer); + block.loadScreenVertex(screen[1], uvs[1], slot, index, 1, renderBuffer); + block.loadScreenVertex(screen[2], uvs[2], slot, index, 2, renderBuffer); + paintFlat(renderBuffer, block.texture(index), block.backfaceCull(), + screen[0], screen[1], screen[2], + uvs[0], uvs[1], uvs[2], + block.camZ(slot, index, 0), + block.camZ(slot, index, 1), + block.camZ(slot, index, 2), + block.screenPerim(slot, index), + block.uvPerimeter(index)); + } + + /** + * Fills the scratch screen/UV points from one clip-loop entry; screen + * coordinates were stored at clip time with the exact + * {@code Vertex.setCameraSpaceCoordinate} expression. + */ + private void loadClipVertex(final Point2D screen, final Point2D uv, + final double[] store, final int entry) { + screen.x = store[entry + 5]; + screen.y = store[entry + 6]; + uv.x = store[entry + 3]; + uv.y = store[entry + 4]; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PerspectiveBorderInterpolator.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PerspectiveBorderInterpolator.java new file mode 100644 index 0000000..79d39cb --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PerspectiveBorderInterpolator.java @@ -0,0 +1,164 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; + +import static java.lang.Math.round; + +/** + * Border interpolator carrying perspective-corrected texture gradients. + * + *

Quake-style perspective texturing: instead of interpolating texture + * coordinates (u, v) linearly in screen space (affine — wrong except for + * face-on triangles), interpolates (u/z, v/z, 1/z) which ARE linear in + * screen space. The scanline renderer recovers exact (u, v) with one + * reciprocal every N pixels and steps affinely in between, so the divide + * cost is amortized.

+ * + *

The u/v values handed to this interpolator must already be premultiplied + * by the mipmap's multiplication factor and divided by the vertex's + * camera-space z; w values are 1/z.

+ * + * @see PolygonBorderInterpolator + * @see TexturedTriangle + */ +public class PerspectiveBorderInterpolator { + + /** + * Small epsilon value for comparing near-zero heights to detect horizontal edges. + */ + private static final double EPSILON = 0.0001; + + /** The first endpoint of this edge in screen space. */ + private Point2D p1; + /** The second endpoint of this edge in screen space. */ + private Point2D p2; + + /** The vertical span (p2.y - p1.y) in double precision, may be negative. */ + private double height; + /** The horizontal span (p2.x - p1.x) in double precision, may be negative. */ + private double width; + + /** u/z at the endpoints (mipmap factor premultiplied). */ + private double su1, su2; + /** v/z at the endpoints (mipmap factor premultiplied). */ + private double sv1, sv2; + /** 1/z at the endpoints. */ + private double sw1, sw2; + + /** Spans along the edge. */ + private double suSpan, svSpan, swSpan; + + /** + * Biased 1/z (the depth-buffer quantity) at the endpoints. Linear in + * screen space exactly like sw, so it rides the same edge + * interpolation; set only in z-buffer mode via {@link #setPointsZW}. + */ + private double zw1, zw2; + private double zwSpan; + + /** The current Y coordinate being interpolated. */ + private int currentY; + + /** + * Sets the screen endpoints and perspective-corrected gradients for this edge. + * + * @param screenPoint1 the first screen-space endpoint + * @param screenPoint2 the second screen-space endpoint + * @param su1 u/z at the first endpoint (mipmap factor premultiplied) + * @param sv1 v/z at the first endpoint (mipmap factor premultiplied) + * @param sw1 1/z at the first endpoint + * @param su2 u/z at the second endpoint + * @param sv2 v/z at the second endpoint + * @param sw2 1/z at the second endpoint + */ + public void setPoints(final Point2D screenPoint1, final Point2D screenPoint2, + final double su1, final double sv1, final double sw1, + final double su2, final double sv2, final double sw2) { + this.p1 = screenPoint1; + this.p2 = screenPoint2; + this.su1 = su1; + this.sv1 = sv1; + this.sw1 = sw1; + this.su2 = su2; + this.sv2 = sv2; + this.sw2 = sw2; + + height = p2.y - p1.y; + width = p2.x - p1.x; + + suSpan = su2 - su1; + svSpan = sv2 - sv1; + swSpan = sw2 - sw1; + } + + /** + * Tests whether the given y coordinate falls within the vertical span of this edge. + */ + public boolean containsY(final int y) { + final double minY = Math.min(p1.y, p2.y); + final double maxY = Math.max(p1.y, p2.y); + return y >= minY && y <= maxY; + } + + private double interpolationT() { + return (currentY - p1.y) / height; + } + + /** Returns interpolated u/z at the current Y. */ + public double getSU() { + if (Math.abs(height) < EPSILON) + return (su1 + su2) / 2d; + return su1 + interpolationT() * suSpan; + } + + /** Returns interpolated v/z at the current Y. */ + public double getSV() { + if (Math.abs(height) < EPSILON) + return (sv1 + sv2) / 2d; + return sv1 + interpolationT() * svSpan; + } + + /** Returns interpolated 1/z at the current Y. */ + public double getSW() { + if (Math.abs(height) < EPSILON) + return (sw1 + sw2) / 2d; + return sw1 + interpolationT() * swSpan; + } + + /** + * Sets the depth-buffer endpoint values (biased 1/z) for this edge. + * Companion to {@link #setPoints}: kept separate so the classic + * painter path never pays for the extra channel. + */ + public void setPointsZW(final double zw1, final double zw2) { + this.zw1 = zw1; + this.zw2 = zw2; + zwSpan = zw2 - zw1; + } + + /** Returns interpolated biased 1/z (depth value) at the current Y. */ + public double getZW() { + if (Math.abs(height) < EPSILON) + return (zw1 + zw2) / 2d; + return zw1 + interpolationT() * zwSpan; + } + + /** + * Computes the interpolated x coordinate rounded to the nearest integer. + * Identical semantics to {@link PolygonBorderInterpolator#getX()}. + */ + public int getX() { + if (Math.abs(height) < EPSILON) + return (int) round((p1.x + p2.x) / 2); + return (int) round(p1.x + (width * (currentY - p1.y)) / height); + } + + /** Sets the current Y coordinate for interpolation. */ + public void setCurrentY(final int y) { + this.currentY = y; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java new file mode 100644 index 0000000..d3f0893 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java @@ -0,0 +1,196 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; + +import static java.lang.Math.round; + +/** + * Interpolator for textured polygon edges with perspective correction. + * + *

Maps screen coordinates to texture coordinates while maintaining + * perspective accuracy. Uses double-precision arithmetic to eliminate + * T-junction gaps from truncation errors, matching {@code LineInterpolator} + * behavior in the solid polygon renderer.

+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.LineInterpolator + */ +public class PolygonBorderInterpolator { + + /** + * Small epsilon value for comparing near-zero heights to detect horizontal edges. + */ + private static final double EPSILON = 0.0001; + + /** + * The first endpoint of this edge in screen space. + */ + Point2D p1; + /** + * The second endpoint of this edge in screen space. + */ + Point2D p2; + + /** + * The vertical span (p2.y - p1.y) in double precision, which may be negative. + * + *

Stored as double to preserve subpixel precision during interpolation, + * eliminating rounding errors that cause T-junction gaps.

+ */ + private double height; + /** + * The horizontal span (p2.x - p1.x) in double precision, which may be negative. + */ + private double width; + + /** + * The texture coordinate at the first endpoint. + */ + private Point2D texturePoint1; + /** + * The texture coordinate at the second endpoint. + */ + private Point2D texturePoint2; + /** + * The texture U span (texturePoint2.x - texturePoint1.x). + */ + private double textureWidth; + /** + * The texture V span (texturePoint2.y - texturePoint1.y). + */ + private double textureHeight; + + /** + * The current Y coordinate being interpolated, used for computing texture coordinates. + */ + private int currentY; + + /** + * Creates a new polygon border interpolator. + */ + public PolygonBorderInterpolator() { + } + + /** + * Tests whether the given y coordinate falls within the vertical span of this edge. + * + *

Uses double-precision comparison to handle subpixel vertex positions correctly.

+ * + * @param y the scanline y coordinate to test + * @return {@code true} if {@code y} is between the y coordinates of the two endpoints (inclusive) + */ + public boolean containsY(final int y) { + final double minY = Math.min(p1.y, p2.y); + final double maxY = Math.max(p1.y, p2.y); + return y >= minY && y <= maxY; + } + + /** + * Returns the interpolated texture X coordinate at the current Y position. + * + *

For horizontal edges (height near zero), returns the midpoint texture X.

+ * + * @return the texture X coordinate + */ + public double getTX() { + if (Math.abs(height) < EPSILON) { + return (texturePoint1.x + texturePoint2.x) / 2d; + } + final double t = (currentY - p1.y) / height; + return texturePoint1.x + t * textureWidth; + } + + /** + * Returns the interpolated texture Y coordinate at the current Y position. + * + *

For horizontal edges (height near zero), returns the midpoint texture Y.

+ * + * @return the texture Y coordinate + */ + public double getTY() { + if (Math.abs(height) < EPSILON) { + return (texturePoint1.y + texturePoint2.y) / 2d; + } + final double t = (currentY - p1.y) / height; + return texturePoint1.y + t * textureHeight; + } + + /** + * Computes the interpolated x coordinate rounded to the nearest integer. + * + *

For horizontal edges (height near zero), returns the midpoint x value + * to avoid division by zero.

+ * + * @return the interpolated x coordinate rounded to the nearest integer + */ + public int getX() { + if (Math.abs(height) < EPSILON) { + return (int) round((p1.x + p2.x) / 2); + } + return (int) round(p1.x + (width * (currentY - p1.y)) / height); + } + + /** + * Sets the current Y coordinate for interpolation. + * + * @param y the current Y coordinate + */ + public void setCurrentY(final int y) { + this.currentY = y; + } + + /** + * Sets the screen and texture coordinates for this edge. + * + *

Screen coordinates are stored directly as references. Callers should + * ensure coordinates are not modified during rendering for thread safety.

+ * + * @param screenPoint1 the first screen-space endpoint + * @param screenPoint2 the second screen-space endpoint + * @param texturePoint1 the texture coordinate for the first endpoint + * @param texturePoint2 the texture coordinate for the second endpoint + */ + public void setPoints(final Point2D screenPoint1, final Point2D screenPoint2, + final Point2D texturePoint1, final Point2D texturePoint2) { + + this.p1 = screenPoint1; + this.p2 = screenPoint2; + this.texturePoint1 = texturePoint1; + this.texturePoint2 = texturePoint2; + + height = p2.y - p1.y; + width = p2.x - p1.x; + + textureWidth = texturePoint2.x - texturePoint1.x; + textureHeight = texturePoint2.y - texturePoint1.y; + } + + /** + * Biased 1/z (the depth-buffer quantity) at the endpoints; set only + * in z-buffer mode via {@link #setPointsZW}. Interpolated linearly + * along the edge like the texture coordinates. + */ + private double zw1, zw2, zwSpan; + + /** + * Sets the depth-buffer endpoint values (biased 1/z) for this edge. + * Companion to {@link #setPoints}. + */ + public void setPointsZW(final double zw1, final double zw2) { + this.zw1 = zw1; + this.zw2 = zw2; + zwSpan = zw2 - zw1; + } + + /** Returns interpolated biased 1/z (depth value) at the current Y. */ + public double getZW() { + if (Math.abs(height) < EPSILON) + return (zw1 + zw2) / 2d; + final double t = (currentY - p1.y) / height; + return zw1 + t * zwSpan; + } + +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java new file mode 100644 index 0000000..643a930 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java @@ -0,0 +1,1481 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap; + +import java.awt.*; +import java.util.List; + +import static eu.svjatoslav.aukio.e3d.geometry.Polygon.pointWithinPolygon; + +/** + * A textured triangle renderer with perspective-correct texture mapping. + * + *

Quake-style subdivided perspective correction: (u/z, v/z, 1/z) are + * interpolated linearly in screen space and the exact texture coordinate + * is recovered with one reciprocal per subdivision interval, with affine + * stepping in between. This makes screen-size tessellation unnecessary — + * triangles of any size render with correct perspective.

+ * + * @see Texture + * @see Vertex#textureCoordinate + */ +public class TexturedTriangle extends AbstractCoordinateShape { + + private static final ThreadLocal INTERPOLATORS = + ThreadLocal.withInitial(() -> new PolygonBorderInterpolator[]{ + new PolygonBorderInterpolator(), new PolygonBorderInterpolator(), new PolygonBorderInterpolator() + }); + + private static final ThreadLocal PERSPECTIVE_INTERPOLATORS = + ThreadLocal.withInitial(() -> new PerspectiveBorderInterpolator[]{ + new PerspectiveBorderInterpolator(), new PerspectiveBorderInterpolator(), + new PerspectiveBorderInterpolator() + }); + + /** + * Quake-style perspective correction interval: the exact texture + * coordinate (one reciprocal) is computed every this many pixels; + * between correction points the scanline steps affinely. + */ + private static final int PERSPECTIVE_CORRECTION_INTERVAL = 16; + + /** + * Minimum camera-space z for the perspective path. Triangles with any + * vertex closer than this fall back to the affine path (they straddle + * the near plane, where 1/z interpolation is invalid). + */ + private static final double PERSPECTIVE_MIN_Z = 0.001; + + /** A/B tuning knobs for the SDF path, see paintSdf. */ + private static final double SDF_GAMMA = + Double.parseDouble(System.getProperty("e3d.sdf.gamma", "0")); + private static final double SDF_SHARPEN = + Double.parseDouble(System.getProperty("e3d.sdf.sharpen", "2")); + /** Debug: print SDF path decisions when they change. */ + private static final boolean SDF_DEBUG = Boolean.getBoolean("e3d.sdf.debug"); + private static boolean sdfDebugLastPerspective; + private static double sdfDebugLastFootY = -1; + + /** + * When true (default), textured triangles render with Quake-style + * perspective-correct texture mapping. + * Volatile: consulted from parallel paint workers. + */ + private static volatile boolean perspectiveCorrectionEnabled = true; + + /** + * Enables or disables perspective-correct texture mapping. + * When disabled, rendering falls back to plain affine mapping, which + * visibly warps textures on large on-screen triangles at steep angles. + * + * @param enabled {@code true} for perspective-correct mapping + */ + public static void setPerspectiveCorrectionEnabled(final boolean enabled) { + perspectiveCorrectionEnabled = enabled; + } + + /** + * Returns whether perspective-correct texture mapping is enabled. + * + * @return {@code true} when perspective correction is active + */ + public static boolean isPerspectiveCorrectionEnabled() { + return perspectiveCorrectionEnabled; + } + + /** + * The texture to apply to this triangle. + * Volatile: the global illumination system swaps the premultiplied + * lightmap composite on GI threads while paint workers read it. + * Read once per paint call so a triangle never shows a half-updated + * texture. + */ + private volatile Texture texture; + + /** + * Returns the current texture. + * + * @return the texture + */ + public Texture getTexture() { + return texture; + } + + /** + * Atomically swaps the texture. Painters pick up the new texture at the + * next paint call; a triangle in flight finishes with the old one. + * + * @param texture the new texture + */ + public void setTexture(final Texture texture) { + this.texture = texture; + } + + private boolean backfaceCulling = Boolean.getBoolean("e3d.backface"); + + // --- ad-hoc frame profiling (-De3d.prof=true; dead code when off) + /** Master switch, constant-folded when false. */ + private static final boolean PROF = + Boolean.getBoolean("e3d.prof"); + /** paintTriangle invocations. */ + public static final java.util.concurrent.atomic.AtomicLong + PROF_TRIS = new java.util.concurrent.atomic.AtomicLong(); + /** Triangles facing away (engine winding convention). */ + public static final java.util.concurrent.atomic.AtomicLong + PROF_BACKFACE = new java.util.concurrent.atomic.AtomicLong(); + /** Triangles discarded by vertical render-bounds clamp. */ + public static final java.util.concurrent.atomic.AtomicLong + PROF_OFFY = new java.util.concurrent.atomic.AtomicLong(); + /** Triangles with screen bounding box below 2x2 pixels. */ + public static final java.util.concurrent.atomic.AtomicLong + PROF_TINY = new java.util.concurrent.atomic.AtomicLong(); + /** Scanline spans drawn. */ + public static final java.util.concurrent.atomic.AtomicLong + PROF_SPANS = new java.util.concurrent.atomic.AtomicLong(); + /** Pixel loop iterations across all spans. */ + public static final java.util.concurrent.atomic.AtomicLong + PROF_PIXELS = new java.util.concurrent.atomic.AtomicLong(); + + /** Resets all profiling counters. */ + public static void profReset() { + PROF_TRIS.set(0); + PROF_BACKFACE.set(0); + PROF_OFFY.set(0); + PROF_TINY.set(0); + PROF_SPANS.set(0); + PROF_PIXELS.set(0); + } + + /** + * Total UV distance between all texture coordinate pairs. + * Computed at construction time to determine appropriate mipmap level. + */ + private double totalTextureDistance; + + /** + * Creates a textured triangle with the specified vertices and texture. + * + * @param p1 the first vertex (must have textureCoordinate set) + * @param p2 the second vertex (must have textureCoordinate set) + * @param p3 the third vertex (must have textureCoordinate set) + * @param texture the texture to apply + */ + public TexturedTriangle(Vertex p1, Vertex p2, Vertex p3, final Texture texture) { + + super(p1, p2, p3); + this.texture = texture; + computeTotalTextureDistance(); + } + + /** + * Constructor for flat-array-backed subclasses ({@code MeshTriangle}) + * that carry no {@link Vertex} objects and paint exclusively through + * {@link #paintFlat}, which computes the mipmap metric inline. + * {@code totalTextureDistance} is set to a neutral 1 — never read on + * that path. + * + * @param texture the texture the subclass paints with + */ + protected TexturedTriangle(final Texture texture) { + super(0); + this.texture = texture; + this.totalTextureDistance = 1; + } + + /** + * Computes the total UV distance between all texture coordinate pairs. + * Used to determine appropriate mipmap level. + */ + private void computeTotalTextureDistance() { + totalTextureDistance = vertices.get(0).textureCoordinate.getDistanceTo(vertices.get(1).textureCoordinate); + totalTextureDistance += vertices.get(0).textureCoordinate.getDistanceTo(vertices.get(2).textureCoordinate); + totalTextureDistance += vertices.get(1).textureCoordinate.getDistanceTo(vertices.get(2).textureCoordinate); + } + + /** + * Recomputes the mipmap-selection metric after texture coordinates are + * replaced post-construction (used by generated-UV subclasses like + * lightmapped triangles). + */ + protected final void refreshTextureDistance() { + computeTotalTextureDistance(); + } + + /** + * Z-buffer span writer: the biased 1/z endpoint values ride the + * interpolators' zw channel, and every pixel is depth-tested BEFORE + * the texture fetch — rejected pixels cost one float compare instead + * of a texel read. Opaque texels (alpha 255) write depth; blended + * texels write color only, so translucency never occludes. + * {@code renderBuffer.depth} is always allocated (the z-buffer path + * is the only renderer); requires {@code setPointsZW} called on both + * interpolators. + */ + private void drawHorizontalLinePerspectiveZ( + final PerspectiveBorderInterpolator line1, + final PerspectiveBorderInterpolator line2, + final int y, + final RenderingContext renderBuffer, + final TextureBitmap textureBitmap) { + + line1.setCurrentY(y); + line2.setCurrentY(y); + + int x1 = line1.getX(); + int x2 = line2.getX(); + + final double su1, sv1, sw1, zw1; + final double su2, sv2, sw2, zw2; + + if (x1 <= x2) { + su1 = line1.getSU(); + sv1 = line1.getSV(); + sw1 = line1.getSW(); + zw1 = line1.getZW(); + su2 = line2.getSU(); + sv2 = line2.getSV(); + sw2 = line2.getSW(); + zw2 = line2.getZW(); + } else { + final int tmp = x1; + x1 = x2; + x2 = tmp; + su1 = line2.getSU(); + sv1 = line2.getSV(); + sw1 = line2.getSW(); + zw1 = line2.getZW(); + su2 = line1.getSU(); + sv2 = line1.getSV(); + sw2 = line1.getSW(); + zw2 = line1.getZW(); + } + + final double realWidth = x2 - x1; + final double realX1 = x1; + + if (x1 < renderBuffer.renderMinX) + x1 = renderBuffer.renderMinX; + if (x2 >= renderBuffer.renderMaxX) + x2 = renderBuffer.renderMaxX; + + final int span = x2 - x1; + if (span <= 0) + return; + + if (PROF) { + PROF_SPANS.incrementAndGet(); + PROF_PIXELS.addAndGet(span); + } + + int renderBufferOffset = (y * renderBuffer.width) + x1; + + final double dsu = (su2 - su1) / realWidth; + final double dsv = (sv2 - sv1) / realWidth; + final double dsw = (sw2 - sw1) / realWidth; + final double dzw = (zw2 - zw1) / realWidth; + + // Depth margin (polygon offset): fragments within dzMargin world + // units of the stored depth resolve coherently instead of + // z-fighting per pixel — near-coplanar surface pairs (kit-bashed + // wall pieces, draped decals, LOD shells). The queue is + // back-to-front (painter, Z descending), so WITHIN the window + // the LATER (nearer) writer must win: the test therefore rejects + // only fragments that are BEHIND the stored depth by more than + // the margin. (The previous "+margin" form made the FIRST — + // i.e. FARTHER — writer win the window, so dirt within margin + // below the road beat the pavement; combined with a per-span + // margin constant that inflates by (z_pixel/z_near)^2 down + // grazing spans, ground leaked through the road at near-horizon + // pitches. Fixed camera, view-dependent holes = impossible for + // a correct z-buffer.) + // The w-space margin is dz*w^2 evaluated PER PIXEL at the + // fragment's own depth. + + double su = su1 + dsu * (x1 - realX1); + double sv = sv1 + dsv * (x1 - realX1); + double sw = sw1 + dsw * (x1 - realX1); + double zw = zw1 + dzw * (x1 - realX1); + + final int[] texPixels = textureBitmap.pixels; + final int texW = textureBitmap.width; + final int texH = textureBitmap.height; + final int texWMinus1 = texW - 1; + final int texHMinus1 = texH - 1; + final int[] renderBufferPixels = renderBuffer.pixels; + final float[] depth = renderBuffer.depth; + // Alpha pass (depthPass 2): depth-test but never depth-write, + // so cutout foliage cannot occlude later fragments + final boolean writeDepth = renderBuffer.depthPass != 2; + // null texture (unit tests) = clamp + final boolean wrap = texture != null && texture.wrap; + + // Adaptive-subdivision perspective ladder (Quake-style stepping) + final double ue1 = su1 / sw1; + final double ue2 = su2 / sw2; + final double ve1 = sv1 / sw1; + final double ve2 = sv2 / sw2; + final double wRatio = Math.max(sw1, sw2) / Math.min(sw1, sw2); + final double texelRate = Math.max(Math.abs(ue2 - ue1), Math.abs(ve2 - ve1)) + / realWidth * wRatio; + final double k = Math.abs(dsw) / Math.min(sw1, sw2); + final double curvature = texelRate * k; + final int interval = curvature < 0.5 / (16 * 16) ? PERSPECTIVE_CORRECTION_INTERVAL + : curvature < 0.5 / (8 * 8) ? 8 + : curvature < 0.5 / (4 * 4) ? 4 + : curvature < 0.5 / (2 * 2) ? 2 : 1; + final double invInterval = 1d / interval; + + int done = 0; + double invW = 1d / sw; + double tx = su * invW; + double ty = sv * invW; + while (done < span) { + final int block = Math.min(interval, span - done); + + su += dsu * block; + sv += dsv * block; + sw += dsw * block; + final double invWNext = 1d / sw; + final double txNext = su * invWNext; + final double tyNext = sv * invWNext; + + final double invBlock = block == interval ? invInterval : 1d / block; + final double txStep = (txNext - tx) * invBlock; + final double tyStep = (tyNext - ty) * invBlock; + + for (int i = 0; i < block; i++) { + if (zw > depth[renderBufferOffset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) { + int itx = (int) tx; + int ity = (int) ty; + + if (wrap) { + itx = Math.floorMod(itx, texW); + ity = Math.floorMod(ity, texH); + } else { + if (itx < 0) itx = 0; + else if (itx > texWMinus1) itx = texWMinus1; + + if (ity < 0) ity = 0; + else if (ity > texHMinus1) ity = texHMinus1; + } + + final int srcPixel = texPixels[ity * texW + itx]; + final int srcAlpha = (srcPixel >> 24) & 0xff; + + if (srcAlpha == 255) { + renderBufferPixels[renderBufferOffset] = srcPixel; + if (writeDepth) + depth[renderBufferOffset] = (float) zw; + } else if (srcAlpha != 0) { + // Translucent: blend, but do NOT write depth — + // translucency must not occlude later fragments. + // Lerp form dest + ((a*(src-dest) - dest) >> 8): + // algebraically ((255-a)*dest + a*src) >> 8 — proven + // bit-identical to SolidPolygon's form for all + // inputs, so the two span writers blend the same. + final int destPixel = renderBufferPixels[renderBufferOffset]; + final int destR = (destPixel >> 16) & 0xff; + final int destG = (destPixel >> 8) & 0xff; + final int destB = destPixel & 0xff; + + final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8); + final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8); + final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8); + + renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b; + } + } + + tx += txStep; + ty += tyStep; + zw += dzw; + renderBufferOffset++; + } + + tx = txNext; + ty = tyNext; + + done += block; + } + } + + @Override + public void paint(final RenderingContext renderBuffer) { + // Near-plane clip output takes precedence: a straddling triangle + // clips to a triangle or a quad (one corner cut off). The quad is + // painted as a 2-triangle fan — the clip of a convex polygon stays + // convex, so fan triangulation is exact. Clipped vertices carry + // UVs interpolated in 3D at the cut, which is exactly what the + // perspective-correct path expects of a point on the edge. + final List clipped = clippedVertices(renderBuffer); + if (clipped == null) { + paintTriangle(renderBuffer, vertices.get(0), vertices.get(1), vertices.get(2)); + return; + } + for (int i = 1; i + 1 < clipped.size(); i++) { + paintTriangle(renderBuffer, clipped.get(0), clipped.get(i), clipped.get(i + 1)); + } + } + + /** + * Renders one textured triangle defined by the given vertices (either + * the shape's own three, or a fan triple from the near-plane-clipped + * loop). + * + *

This method performs:

+ *
    + *
  • Backface culling check (if enabled)
  • + *
  • Mouse interaction detection
  • + *
  • Mipmap level selection based on screen coverage
  • + *
  • Scanline rasterization with texture sampling
  • + *
+ * + * @param renderBuffer the rendering context containing the pixel buffer + * @param v1 the first triangle vertex + * @param v2 the second triangle vertex + * @param v3 the third triangle vertex + */ + private void paintTriangle(final RenderingContext renderBuffer, + final Vertex v1, final Vertex v2, final Vertex v3) { + + final Point2D projectedPoint1 = v1.onScreenCoordinate(renderBuffer); + final Point2D projectedPoint2 = v2.onScreenCoordinate(renderBuffer); + final Point2D projectedPoint3 = v3.onScreenCoordinate(renderBuffer); + + if (mouseInteractionController != null) + if (renderBuffer.getMouseEvent() != null) + if (pointWithinPolygon( + renderBuffer.getMouseEvent().coordinate, projectedPoint1, projectedPoint2, projectedPoint3)) { + final double[] uv = textureCoordinateAt( + renderBuffer.getMouseEvent().coordinate, + projectedPoint1, projectedPoint2, projectedPoint3, + v1, v2, v3, renderBuffer); + renderBuffer.setCurrentObjectUnderMouseCursor( + mouseInteractionController, uv[0], uv[1]); + } + + // Show polygon boundaries (for debugging) + if (renderBuffer.developerTools != null && renderBuffer.developerTools.showPolygonBorders) + showBorders(renderBuffer); + + // Keep double precision to eliminate T-junction gaps from truncation errors + final double y1 = projectedPoint1.y; + final double y2 = projectedPoint2.y; + final double y3 = projectedPoint3.y; + + // Find top-most point (use ceil to include all pixels triangle touches) + int yTop = (int) Math.ceil(Math.min(y1, Math.min(y2, y3))); + if (yTop < 0) yTop = 0; + + // Find bottom-most point (use floor to include all pixels triangle touches) + int yBottom = (int) Math.floor(Math.max(y1, Math.max(y2, y3))); + if (yBottom >= renderBuffer.height) yBottom = renderBuffer.height - 1; + + // Clamp to render Y bounds (use renderMaxY - 1 because loop is inclusive) + yTop = Math.max(yTop, renderBuffer.renderMinY); + yBottom = Math.min(yBottom, renderBuffer.renderMaxY - 1); + if (yTop > yBottom) { + if (PROF) + PROF_OFFY.incrementAndGet(); + return; + } + + // Snapshot the texture reference for the whole paint: the GI system + // may swap the composite lightmap on another thread mid-frame. + final Texture texture = this.texture; + + final double edge12 = projectedPoint1.getDistanceTo(projectedPoint2); + final double edge13 = projectedPoint1.getDistanceTo(projectedPoint3); + final double edge23 = projectedPoint2.getDistanceTo(projectedPoint3); + final double totalVisibleDistance = edge12 + edge13 + edge23; + + final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d; + + // SDF text/vector-art path: coverage comes from the distance + // field, not from stored coverage, so the mipmap chain (which + // trades sharpness for alias-freedom) is bypassed entirely. + if (texture.isSdf()) { + paintSdf(yTop, yBottom, renderBuffer, + projectedPoint1, projectedPoint2, projectedPoint3, scaleFactor, + v1, v2, v3); + return; + } + + paintFlat(renderBuffer, texture, backfaceCulling, + projectedPoint1, projectedPoint2, projectedPoint3, + v1.textureCoordinate, v2.textureCoordinate, v3.textureCoordinate, + v1.transformedCoordinate(renderBuffer).z, + v2.transformedCoordinate(renderBuffer).z, + v3.transformedCoordinate(renderBuffer).z, + totalVisibleDistance, + totalTextureDistance); + } + + /** + * Shared rasterization core for one textured triangle whose vertices + * are already in screen space. Called both by the object-backed path + * ({@link #paintTriangle}) and by {@code TriangleMeshBlock} handles, + * whose vertex data lives in flat per-block arrays instead of + * {@link Vertex} objects — the math here is identical either way, + * keeping both paths bit-exact. + * + *

Mouse interaction and debug borders stay in the object path + * (mesh blocks do not support picking). SDF textures are rejected: + * mesh blocks never carry them (enforced at block build time).

+ * + * @param renderBuffer the rendering context containing the pixel buffer + * @param texture the texture to sample + * @param backfaceCulling whether to cull counter-clockwise triangles + * @param projectedPoint1 screen-space vertex 1 + * @param projectedPoint2 screen-space vertex 2 + * @param projectedPoint3 screen-space vertex 3 + * @param texturePoint1 UV (primary-texture pixels) of vertex 1 + * @param texturePoint2 UV of vertex 2 + * @param texturePoint3 UV of vertex 3 + * @param z1 camera-space depth of vertex 1 + * @param z2 camera-space depth of vertex 2 + * @param z3 camera-space depth of vertex 3 + * @param totalVisibleDistance screen-edge perimeter for mipmap + * selection, computed once per paint call + * (or once per slot for mesh blocks) — + * callers share it instead of the core + * recomputing per tile + * @param totalTextureDistance UV perimeter for mipmap selection. For a + * near-plane-clipped fan this is the + * ORIGINAL triangle's perimeter (the clip + * does not change the texture's texel + * density), matching the object path. + */ + void paintFlat(final RenderingContext renderBuffer, + final Texture texture, + final boolean backfaceCulling, + final Point2D projectedPoint1, + final Point2D projectedPoint2, + final Point2D projectedPoint3, + final Point2D texturePoint1, + final Point2D texturePoint2, + final Point2D texturePoint3, + final double z1, final double z2, final double z3, + final double totalVisibleDistance, + final double totalTextureDistance) { + + // Z-buffer two-pass classification: opaque-class triangles + // paint in pass 1 (depth test + write), alpha-class in pass 2 + // (depth test, no write) — see RenderAggregator.paintSorted. + final boolean alphaClass = texture.isSdf() || texture.hasAlpha; + if ((renderBuffer.depthPass == 1) == alphaClass) + return; + + if (PROF) { + PROF_TRIS.incrementAndGet(); + if ((projectedPoint2.x - projectedPoint1.x) + * (projectedPoint3.y - projectedPoint1.y) + - (projectedPoint3.x - projectedPoint1.x) + * (projectedPoint2.y - projectedPoint1.y) >= 0) + PROF_BACKFACE.incrementAndGet(); + final double bw = Math.max(projectedPoint1.x, Math.max( + projectedPoint2.x, projectedPoint3.x)) + - Math.min(projectedPoint1.x, Math.min( + projectedPoint2.x, projectedPoint3.x)); + final double bh = Math.max(projectedPoint1.y, Math.max( + projectedPoint2.y, projectedPoint3.y)) + - Math.min(projectedPoint1.y, Math.min( + projectedPoint2.y, projectedPoint3.y)); + if (bw * bh < 4) + PROF_TINY.incrementAndGet(); + } + + if (backfaceCulling) { + final double signedArea = (projectedPoint2.x - projectedPoint1.x) + * (projectedPoint3.y - projectedPoint1.y) + - (projectedPoint3.x - projectedPoint1.x) + * (projectedPoint2.y - projectedPoint1.y); + if (signedArea >= 0) + return; + } + + // Keep double precision to eliminate T-junction gaps from truncation errors + final double y1 = projectedPoint1.y; + final double y2 = projectedPoint2.y; + final double y3 = projectedPoint3.y; + + // Find top-most point (use ceil to include all pixels triangle touches) + int yTop = (int) Math.ceil(Math.min(y1, Math.min(y2, y3))); + if (yTop < 0) yTop = 0; + + // Find bottom-most point (use floor to include all pixels triangle touches) + int yBottom = (int) Math.floor(Math.max(y1, Math.max(y2, y3))); + if (yBottom >= renderBuffer.height) yBottom = renderBuffer.height - 1; + + // Clamp to render Y bounds (use renderMaxY - 1 because loop is inclusive) + yTop = Math.max(yTop, renderBuffer.renderMinY); + yBottom = Math.min(yBottom, renderBuffer.renderMaxY - 1); + if (yTop > yBottom) { + if (PROF) + PROF_OFFY.incrementAndGet(); + return; + } + + if (texture.isSdf()) + throw new IllegalStateException( + "SDF textures are not supported in mesh blocks"); + + final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d; + + final TextureBitmap mipmap = texture.getMipmapForScale(scaleFactor); + + if (perspectiveCorrectionEnabled) { + if (z1 > PERSPECTIVE_MIN_Z && z2 > PERSPECTIVE_MIN_Z && z3 > PERSPECTIVE_MIN_Z) { + // Affine mapping is within half a texel of exact + // perspective for small or nearly-flat triangles, making + // the perspective setup pointless for them: the midpoint + // error of affine vs exact is ~= texelSpan*(zRatio-1)/4 + // where texelSpan is the texture range (in selected-mip + // texels) the triangle covers — NOT its pixel size (a + // triangle can map many texels into few pixels; measured + // 2026-09-06: a 4px span with a 56-texel range deviated 3 + // texels under the pixel-size rule). Distant clusters of + // small triangles render affine. + // Verified by TexturedTrianglePerspectiveTest#affineWithinHalfTexelBound. + final double mf0 = mipmap.multiplicationFactor; + final double tu1 = texturePoint1.x * mf0; + final double tv1 = texturePoint1.y * mf0; + final double tu2 = texturePoint2.x * mf0; + final double tv2 = texturePoint2.y * mf0; + final double tu3 = texturePoint3.x * mf0; + final double tv3 = texturePoint3.y * mf0; + final double texelSpan = Math.max( + Math.max(Math.abs(tu2 - tu1), Math.abs(tv2 - tv1)), + Math.max( + Math.max(Math.abs(tu3 - tu1), Math.abs(tv3 - tv1)), + Math.max(Math.abs(tu3 - tu2), Math.abs(tv3 - tv2)))); + final double zMin = Math.min(z1, Math.min(z2, z3)); + final double zMax = Math.max(z1, Math.max(z2, z3)); + if (texelSpan * (zMax / zMin - 1d) < 2d) { + paintAffine(yTop, yBottom, mipmap, renderBuffer, + projectedPoint1, projectedPoint2, projectedPoint3, + texturePoint1, texturePoint2, texturePoint3, + z1, z2, z3); + return; + } + + // Quake-style perspective-correct mapping: interpolate + // (u/z, v/z, 1/z), which are linear in screen space, and + // recover exact (u, v) every PERSPECTIVE_CORRECTION_INTERVAL + // pixels in the scanline. The mipmap multiplication factor + // is folded into the gradients here, so the scanline works + // directly in texture pixel units. + final double mf = mipmap.multiplicationFactor; + + final double sw1 = 1d / z1; + final double sw2 = 1d / z2; + final double sw3 = 1d / z3; + + final double su1 = texturePoint1.x * mf * sw1; + final double sv1 = texturePoint1.y * mf * sw1; + final double su2 = texturePoint2.x * mf * sw2; + final double sv2 = texturePoint2.y * mf * sw2; + final double su3 = texturePoint3.x * mf * sw3; + final double sv3 = texturePoint3.y * mf * sw3; + + final PerspectiveBorderInterpolator[] pi = PERSPECTIVE_INTERPOLATORS.get(); + pi[0].setPoints(projectedPoint1, projectedPoint2, su1, sv1, sw1, su2, sv2, sw2); + pi[1].setPoints(projectedPoint1, projectedPoint3, su1, sv1, sw1, su3, sv3, sw3); + pi[2].setPoints(projectedPoint2, projectedPoint3, su2, sv2, sw2, su3, sv3, sw3); + + { + // 1/z rides the same edge interpolation; spans + // depth-test before texturing. + final double zw1 = 1d / z1; + final double zw2 = 1d / z2; + final double zw3 = 1d / z3; + pi[0].setPointsZW(zw1, zw2); + pi[1].setPointsZW(zw1, zw3); + pi[2].setPointsZW(zw2, zw3); + for (int y = yTop; y <= yBottom; y++) { + if (pi[0].containsY(y)) { + if (pi[1].containsY(y)) + drawHorizontalLinePerspectiveZ(pi[0], pi[1], y, renderBuffer, mipmap); + else if (pi[2].containsY(y)) + drawHorizontalLinePerspectiveZ(pi[0], pi[2], y, renderBuffer, mipmap); + } else if (pi[1].containsY(y)) { + if (pi[2].containsY(y)) + drawHorizontalLinePerspectiveZ(pi[1], pi[2], y, renderBuffer, mipmap); + } + } + return; + } + } + } + + paintAffine(yTop, yBottom, mipmap, renderBuffer, + projectedPoint1, projectedPoint2, projectedPoint3, + texturePoint1, texturePoint2, texturePoint3, + z1, z2, z3); + } + + /** + * Computes the perspective-correct texture coordinate at a screen-space + * point known to lie inside the triangle. + * + *

Screen-space barycentric weights are divided by the camera-space z + * of each vertex and renormalized — the same (u/z, v/z, 1/z) math the + * perspective-correct scanline path uses — so the returned coordinate + * matches the texel that was actually painted at that pixel, even at + * steep viewing angles. Texture coordinates are in primary-texture + * pixels (no mipmap factor applied).

+ * + * @return double[2] with {u, v} in primary-texture pixels + */ + private static double[] textureCoordinateAt(final Point2D point, + final Point2D p1, final Point2D p2, final Point2D p3, + final Vertex v1, final Vertex v2, final Vertex v3, + final RenderingContext renderBuffer) { + final double denom = (p2.y - p3.y) * (p1.x - p3.x) + + (p3.x - p2.x) * (p1.y - p3.y); + if (Math.abs(denom) < 1e-9) + // degenerate on screen; the hit pixel is effectively a vertex + return new double[]{v1.textureCoordinate.x, v1.textureCoordinate.y}; + + double w1 = ((p2.y - p3.y) * (point.x - p3.x) + + (p3.x - p2.x) * (point.y - p3.y)) / denom; + double w2 = ((p3.y - p1.y) * (point.x - p3.x) + + (p1.x - p3.x) * (point.y - p3.y)) / denom; + double w3 = 1d - w1 - w2; + + final double z1 = v1.transformedCoordinate(renderBuffer).z; + final double z2 = v2.transformedCoordinate(renderBuffer).z; + final double z3 = v3.transformedCoordinate(renderBuffer).z; + + if (z1 > PERSPECTIVE_MIN_Z && z2 > PERSPECTIVE_MIN_Z + && z3 > PERSPECTIVE_MIN_Z) { + w1 /= z1; + w2 /= z2; + w3 /= z3; + final double sum = w1 + w2 + w3; + w1 /= sum; + w2 /= sum; + w3 /= sum; + } + // near-plane straddlers: plain screen-space barycentric (affine), + // matching the affine fallback path used for painting them + + return new double[]{ + w1 * v1.textureCoordinate.x + w2 * v2.textureCoordinate.x + + w3 * v3.textureCoordinate.x, + w1 * v1.textureCoordinate.y + w2 * v2.textureCoordinate.y + + w3 * v3.textureCoordinate.y}; + } + + /** + * SDF (signed distance field) rendering path. Coverage is not stored + * in the texture; it is re-derived per pixel from a smooth distance + * mask, so edges stay sharp at any magnification and fade to clean + * gray under minification. Layers: {@code texture.primaryBitmap} is + * the background color layer, {@code texture.sdfForeground} the ink + * color layer (both sampled nearest — they are flat per region), + * {@code texture.sdfMask} the distance field (sampled bilinear). + * + *

Minification is handled analytically: the coverage window is + * widened by the screen-space pixel footprint, which gives correct + * area coverage without a mipmap chain.

+ * + * @param scaleFactor the same screen-pixels-per-texel estimate the + * mipmap selection uses (times 1.2) + */ + private void paintSdf(final int yTop, final int yBottom, + final RenderingContext renderBuffer, + final Point2D projectedPoint1, final Point2D projectedPoint2, + final Point2D projectedPoint3, final double scaleFactor, + final Vertex v1, final Vertex v2, final Vertex v3) { + // SDF (text/decal) is alpha-class — it paints in the + // back-to-front alpha pass only, without depth interaction. + if (renderBuffer.depthPass == 1) + return; + // Per-axis screen-space UV gradients (affine estimate — adequate + // for a footprint). Text on an angled plane is minified mostly + // along ONE axis; an isotropic average would blur the axis that + // still has resolution to spare. + double footX; + double footY; + final double ex = projectedPoint2.x - projectedPoint1.x; + final double ey = projectedPoint2.y - projectedPoint1.y; + final double fx3 = projectedPoint3.x - projectedPoint1.x; + final double fy3 = projectedPoint3.y - projectedPoint1.y; + final double denom = ex * fy3 - fx3 * ey; + if (Math.abs(denom) > 1e-9) { + final double u1 = v1.textureCoordinate.x; + final double vv1 = v1.textureCoordinate.y; + final double du21 = v2.textureCoordinate.x - u1; + final double dv21 = v2.textureCoordinate.y - vv1; + final double du31 = v3.textureCoordinate.x - u1; + final double dv31 = v3.textureCoordinate.y - vv1; + final double dudx = (du21 * fy3 - du31 * ey) / denom; + final double dudy = (du31 * ex - du21 * fx3) / denom; + final double dvdx = (dv21 * fy3 - dv31 * ey) / denom; + final double dvdy = (dv31 * ex - dv21 * fx3) / denom; + footX = Math.hypot(dudx, dvdx); + footY = Math.hypot(dudy, dvdy); + } else { + footX = footY = 1.2d / scaleFactor; + } + + // The coverage window follows the SHARPEST axis: one screen pixel + // spans texelsPerPixel texels along it, i.e. + // texelsPerPixel/(2*spread) of the normalized mask range; aaK + // converts a mask sample (0..255, edge at 127.5) into fixed-point + // coverage in [0, 256]: cov = (127.5 - d)*aaK + 128. + final double maxFootprint = Math.max(footX, footY); + double texelsPerPixel = Math.max(Math.min(footX, footY), 0.01d); + if (maxFootprint > 1d) { + texelsPerPixel /= SDF_SHARPEN; + } + final double aaK = (2d * texture.sdfSpreadTexels) / texelsPerPixel / 255d * 256d; + + // No mip chain for SDF layers. A distance field's edge gradient + // spans just 2 texels, so a half/quarter-res mask visibly melts + // glyph edges — and because the two triangles of a rectangle get + // slightly different perspective footprints, they crossed mip + // thresholds at different distances, producing a hard diagonal + // quality split plus sudden blur steps while dollying (observed + // 2026-09-06). Sampling the primary field costs some bandwidth + // under minification, but text surfaces are small and the + // per-pixel sample count is what matters. Quality then degrades + // smoothly with distance instead of in steps. + final TextureBitmap mask = texture.sdfMask; + final TextureBitmap fg = texture.sdfForeground; + final TextureBitmap bg = texture.primaryBitmap; + final double mf = mask.multiplicationFactor; + + // Under minification, area-correct coverage reads as low-contrast + // gray haze. Two perceptual corrections (A/B-tuned 2026-09-06 on + // far+angled text): SDF_SHARPEN narrows the coverage window below + // one pixel (kills the haze halo, keeps edges crisp at the cost + // of a little shimmer), and a mild coverage gamma < 1 (stem + // darkening, the small-ppm font rasterizer trick) keeps thin + // strokes present. + // Knobs: -De3d.sdf.gamma=1.4 forces a fixed gamma (0 = auto), + // -De3d.sdf.sharpen=1 restores the pixel-exact window. + final int[] covLut; + final double gamma = SDF_GAMMA != 0 ? SDF_GAMMA + : Math.max(0.75d, 1d - 0.08d * (Math.log(maxFootprint) / Math.log(2d))); + if (gamma != 1d && maxFootprint > 1d) { + covLut = new int[257]; + for (int i = 0; i <= 256; i++) { + covLut[i] = Math.min(256, (int) (256d * Math.pow(i / 256d, gamma))); + } + } else { + covLut = null; + } + + boolean usePerspective = false; + double su1 = 0, sv1 = 0, sw1 = 0; + double su2 = 0, sv2 = 0, sw2 = 0; + double su3 = 0, sv3 = 0, sw3 = 0; + if (perspectiveCorrectionEnabled) { + final double z1 = v1.transformedCoordinate(renderBuffer).z; + final double z2 = v2.transformedCoordinate(renderBuffer).z; + final double z3 = v3.transformedCoordinate(renderBuffer).z; + if (z1 > PERSPECTIVE_MIN_Z && z2 > PERSPECTIVE_MIN_Z && z3 > PERSPECTIVE_MIN_Z) { + // Same affine-sufficiency test as the coverage path + // (mask/fg/bg are all primary resolution, mf = 1). + final double tu1 = v1.textureCoordinate.x; + final double tv1 = v1.textureCoordinate.y; + final double tu2 = v2.textureCoordinate.x; + final double tv2 = v2.textureCoordinate.y; + final double tu3 = v3.textureCoordinate.x; + final double tv3 = v3.textureCoordinate.y; + final double texelSpan = Math.max( + Math.max(Math.abs(tu2 - tu1), Math.abs(tv2 - tv1)), + Math.max( + Math.max(Math.abs(tu3 - tu1), Math.abs(tv3 - tv1)), + Math.max(Math.abs(tu3 - tu2), Math.abs(tv3 - tv2)))); + final double zMin = Math.min(z1, Math.min(z2, z3)); + final double zMax = Math.max(z1, Math.max(z2, z3)); + usePerspective = texelSpan * (zMax / zMin - 1d) >= 2d; + if (usePerspective) { + sw1 = 1d / z1; + sw2 = 1d / z2; + sw3 = 1d / z3; + su1 = tu1 * mf * sw1; + sv1 = tv1 * mf * sw1; + su2 = tu2 * mf * sw2; + sv2 = tv2 * mf * sw2; + su3 = tu3 * mf * sw3; + sv3 = tv3 * mf * sw3; + } + } + } + + if (SDF_DEBUG && (usePerspective != sdfDebugLastPerspective + || Math.abs(footY - sdfDebugLastFootY) > 0.5)) { + sdfDebugLastPerspective = usePerspective; + sdfDebugLastFootY = footY; + System.err.printf("[SDF] perspective=%b footX=%.2f footY=%.2f aaK=%.3f%n", + usePerspective, footX, footY, aaK); + } + + if (usePerspective) { + final PerspectiveBorderInterpolator[] pi = PERSPECTIVE_INTERPOLATORS.get(); + pi[0].setPoints(projectedPoint1, projectedPoint2, su1, sv1, sw1, su2, sv2, sw2); + pi[1].setPoints(projectedPoint1, projectedPoint3, su1, sv1, sw1, su3, sv3, sw3); + pi[2].setPoints(projectedPoint2, projectedPoint3, su2, sv2, sw2, su3, sv3, sw3); + + for (int y = yTop; y <= yBottom; y++) { + if (pi[0].containsY(y)) { + if (pi[1].containsY(y)) + drawHorizontalLinePerspectiveSdf(pi[0], pi[1], y, renderBuffer, mask, fg, bg, aaK, covLut); + else if (pi[2].containsY(y)) + drawHorizontalLinePerspectiveSdf(pi[0], pi[2], y, renderBuffer, mask, fg, bg, aaK, covLut); + } else if (pi[1].containsY(y)) { + if (pi[2].containsY(y)) + drawHorizontalLinePerspectiveSdf(pi[1], pi[2], y, renderBuffer, mask, fg, bg, aaK, covLut); + } + } + return; + } + + final PolygonBorderInterpolator[] interpolators = INTERPOLATORS.get(); + final PolygonBorderInterpolator pbi1 = interpolators[0]; + final PolygonBorderInterpolator pbi2 = interpolators[1]; + final PolygonBorderInterpolator pbi3 = interpolators[2]; + + pbi1.setPoints(projectedPoint1, projectedPoint2, v1.textureCoordinate, v2.textureCoordinate); + pbi2.setPoints(projectedPoint1, projectedPoint3, v1.textureCoordinate, v3.textureCoordinate); + pbi3.setPoints(projectedPoint2, projectedPoint3, v2.textureCoordinate, v3.textureCoordinate); + + for (int y = yTop; y <= yBottom; y++) { + if (pbi1.containsY(y)) { + if (pbi2.containsY(y)) + drawHorizontalLineSdf(pbi1, pbi2, y, renderBuffer, mask, fg, bg, aaK, mf, covLut); + else if (pbi3.containsY(y)) + drawHorizontalLineSdf(pbi1, pbi3, y, renderBuffer, mask, fg, bg, aaK, mf, covLut); + } else if (pbi2.containsY(y)) { + if (pbi3.containsY(y)) + drawHorizontalLineSdf(pbi2, pbi3, y, renderBuffer, mask, fg, bg, aaK, mf, covLut); + } + } + } + + /** + * SDF scanline, affine mapping. Texture coordinates are scaled by the + * selected mip's multiplication factor (all layers share one mip + * level, so one factor covers mask and both color layers). + */ + private void drawHorizontalLineSdf(final PolygonBorderInterpolator line1, + final PolygonBorderInterpolator line2, final int y, + final RenderingContext renderBuffer, + final TextureBitmap mask, final TextureBitmap fg, + final TextureBitmap bg, final double aaK, + final double mf, final int[] covLut) { + line1.setCurrentY(y); + line2.setCurrentY(y); + + int x1 = line1.getX(); + int x2 = line2.getX(); + + final double tx1, ty1, tx2, ty2; + if (x1 <= x2) { + tx1 = line1.getTX() * mf; + ty1 = line1.getTY() * mf; + tx2 = line2.getTX() * mf; + ty2 = line2.getTY() * mf; + } else { + final int tmp = x1; + x1 = x2; + x2 = tmp; + tx1 = line2.getTX() * mf; + ty1 = line2.getTY() * mf; + tx2 = line1.getTX() * mf; + ty2 = line1.getTY() * mf; + } + + final double realWidth = x2 - x1; + final double realX1 = x1; + + if (x1 < renderBuffer.renderMinX) + x1 = renderBuffer.renderMinX; + if (x2 >= renderBuffer.renderMaxX) + x2 = renderBuffer.renderMaxX; + + int renderBufferOffset = (y * renderBuffer.width) + x1; + final int[] renderBufferPixels = renderBuffer.pixels; + + final double txStep = (tx2 - tx1) / realWidth; + final double tyStep = (ty2 - ty1) / realWidth; + + double tx = tx1 + txStep * (x1 - realX1); + double ty = ty1 + tyStep * (x1 - realX1); + + final int[] maskPixels = mask.pixels; + final int[] fgPixels = fg.pixels; + final int[] bgPixels = bg.pixels; + final int mw = mask.width; + final int mh = mask.height; + final double bilinearCapX = mw - 1.0001d; + final double bilinearCapY = mh - 1.0001d; + final int mw1 = mw - 1; + final int mh1 = mh - 1; + + for (int x = x1; x < x2; x++) { + // Fixed-point bilinear distance fetch (8.8 fractions) + final double ctx = tx < 0 ? 0 : Math.min(tx, bilinearCapX); + final double cty = ty < 0 ? 0 : Math.min(ty, bilinearCapY); + final int x0 = (int) ctx; + final int y0 = (int) cty; + final int fx = (int) ((ctx - x0) * 256); + final int fy = (int) ((cty - y0) * 256); + final int row0 = y0 * mw + x0; + final int row1 = row0 + mw; + final int m00 = (maskPixels[row0] >> 16) & 0xff; + final int m10 = (maskPixels[row0 + 1] >> 16) & 0xff; + final int m01 = (maskPixels[row1] >> 16) & 0xff; + final int m11 = (maskPixels[row1 + 1] >> 16) & 0xff; + final int d = (m00 * (256 - fx) * (256 - fy) + m10 * fx * (256 - fy) + + m01 * (256 - fx) * fy + m11 * fx * fy) >> 16; + + int cov = (int) ((127.5d - d) * aaK + 128d); + if (cov < 0) cov = 0; + else if (cov > 256) cov = 256; + if (covLut != null) cov = covLut[cov]; + + int itx = (int) tx; + int ity = (int) ty; + if (itx < 0) itx = 0; + else if (itx > mw1) itx = mw1; + if (ity < 0) ity = 0; + else if (ity > mh1) ity = mh1; + final int addr = ity * mw + itx; + + final int srcPixel; + if (cov <= 0) { + srcPixel = bgPixels[addr]; + } else if (cov >= 256) { + srcPixel = fgPixels[addr]; + } else { + final int bgP = bgPixels[addr]; + final int fgP = fgPixels[addr]; + final int a = (bgP >>> 24) + ((((int) (fgP >>> 24) - (bgP >>> 24)) * cov) >> 8); + final int r = ((bgP >> 16) & 0xff) + (((((fgP >> 16) & 0xff) - ((bgP >> 16) & 0xff)) * cov) >> 8); + final int g = ((bgP >> 8) & 0xff) + (((((fgP >> 8) & 0xff) - ((bgP >> 8) & 0xff)) * cov) >> 8); + final int b = (bgP & 0xff) + ((((fgP & 0xff) - (bgP & 0xff)) * cov) >> 8); + srcPixel = (a << 24) | (r << 16) | (g << 8) | b; + } + + final int srcAlpha = (srcPixel >> 24) & 0xff; + if (srcAlpha == 255) { + renderBufferPixels[renderBufferOffset] = srcPixel; + } else if (srcAlpha != 0) { + final int destPixel = renderBufferPixels[renderBufferOffset]; + final int destR = (destPixel >> 16) & 0xff; + final int destG = (destPixel >> 8) & 0xff; + final int destB = destPixel & 0xff; + final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8); + final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8); + final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8); + renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b; + } + + tx += txStep; + ty += tyStep; + renderBufferOffset++; + } + } + + /** + * SDF scanline with Quake-style subdivided perspective correction — + * same stepping structure as {@link #drawHorizontalLinePerspectiveZ}, + * with the coverage fetch replaced by the distance-field evaluation. + */ + private void drawHorizontalLinePerspectiveSdf( + final PerspectiveBorderInterpolator line1, + final PerspectiveBorderInterpolator line2, + final int y, + final RenderingContext renderBuffer, + final TextureBitmap mask, final TextureBitmap fg, + final TextureBitmap bg, final double aaK, final int[] covLut) { + + line1.setCurrentY(y); + line2.setCurrentY(y); + + int x1 = line1.getX(); + int x2 = line2.getX(); + + final double su1, sv1, sw1; + final double su2, sv2, sw2; + + if (x1 <= x2) { + su1 = line1.getSU(); + sv1 = line1.getSV(); + sw1 = line1.getSW(); + su2 = line2.getSU(); + sv2 = line2.getSV(); + sw2 = line2.getSW(); + } else { + final int tmp = x1; + x1 = x2; + x2 = tmp; + su1 = line2.getSU(); + sv1 = line2.getSV(); + sw1 = line2.getSW(); + su2 = line1.getSU(); + sv2 = line1.getSV(); + sw2 = line1.getSW(); + } + + final double realWidth = x2 - x1; + final double realX1 = x1; + + if (x1 < renderBuffer.renderMinX) + x1 = renderBuffer.renderMinX; + if (x2 >= renderBuffer.renderMaxX) + x2 = renderBuffer.renderMaxX; + + final int span = x2 - x1; + if (span <= 0) + return; + + int renderBufferOffset = (y * renderBuffer.width) + x1; + + final double dsu = (su2 - su1) / realWidth; + final double dsv = (sv2 - sv1) / realWidth; + final double dsw = (sw2 - sw1) / realWidth; + + double su = su1 + dsu * (x1 - realX1); + double sv = sv1 + dsv * (x1 - realX1); + double sw = sw1 + dsw * (x1 - realX1); + + final int[] renderBufferPixels = renderBuffer.pixels; + + final int[] maskPixels = mask.pixels; + final int[] fgPixels = fg.pixels; + final int[] bgPixels = bg.pixels; + final int mw = mask.width; + final int mh = mask.height; + final double bilinearCapX = mw - 1.0001d; + final double bilinearCapY = mh - 1.0001d; + final int mw1 = mw - 1; + final int mh1 = mh - 1; + + // Same adaptive-interval ladder as the coverage path. + final double ue1 = su1 / sw1; + final double ue2 = su2 / sw2; + final double ve1 = sv1 / sw1; + final double ve2 = sv2 / sw2; + final double wRatio = Math.max(sw1, sw2) / Math.min(sw1, sw2); + final double texelRate = Math.max(Math.abs(ue2 - ue1), Math.abs(ve2 - ve1)) + / realWidth * wRatio; + final double k = Math.abs(dsw) / Math.min(sw1, sw2); + final double curvature = texelRate * k; + final int interval = curvature < 0.5 / (16 * 16) ? PERSPECTIVE_CORRECTION_INTERVAL + : curvature < 0.5 / (8 * 8) ? 8 + : curvature < 0.5 / (4 * 4) ? 4 + : curvature < 0.5 / (2 * 2) ? 2 : 1; + final double invInterval = 1d / interval; + + int done = 0; + double invW = 1d / sw; + double tx = su * invW; + double ty = sv * invW; + while (done < span) { + final int block = Math.min(interval, span - done); + + su += dsu * block; + sv += dsv * block; + sw += dsw * block; + final double invWNext = 1d / sw; + final double txNext = su * invWNext; + final double tyNext = sv * invWNext; + + final double invBlock = block == interval ? invInterval : 1d / block; + final double txStep = (txNext - tx) * invBlock; + final double tyStep = (tyNext - ty) * invBlock; + + for (int i = 0; i < block; i++) { + // Fixed-point bilinear distance fetch (8.8 fractions) + final double ctx = tx < 0 ? 0 : Math.min(tx, bilinearCapX); + final double cty = ty < 0 ? 0 : Math.min(ty, bilinearCapY); + final int x0 = (int) ctx; + final int y0 = (int) cty; + final int fx = (int) ((ctx - x0) * 256); + final int fy = (int) ((cty - y0) * 256); + final int row0 = y0 * mw + x0; + final int row1 = row0 + mw; + final int m00 = (maskPixels[row0] >> 16) & 0xff; + final int m10 = (maskPixels[row0 + 1] >> 16) & 0xff; + final int m01 = (maskPixels[row1] >> 16) & 0xff; + final int m11 = (maskPixels[row1 + 1] >> 16) & 0xff; + final int d = (m00 * (256 - fx) * (256 - fy) + m10 * fx * (256 - fy) + + m01 * (256 - fx) * fy + m11 * fx * fy) >> 16; + + int cov = (int) ((127.5d - d) * aaK + 128d); + if (cov < 0) cov = 0; + else if (cov > 256) cov = 256; + if (covLut != null) cov = covLut[cov]; + + int itx = (int) tx; + int ity = (int) ty; + if (itx < 0) itx = 0; + else if (itx > mw1) itx = mw1; + if (ity < 0) ity = 0; + else if (ity > mh1) ity = mh1; + final int addr = ity * mw + itx; + + final int srcPixel; + if (cov <= 0) { + srcPixel = bgPixels[addr]; + } else if (cov >= 256) { + srcPixel = fgPixels[addr]; + } else { + final int bgP = bgPixels[addr]; + final int fgP = fgPixels[addr]; + final int a = (bgP >>> 24) + ((((int) (fgP >>> 24) - (bgP >>> 24)) * cov) >> 8); + final int r = ((bgP >> 16) & 0xff) + (((((fgP >> 16) & 0xff) - ((bgP >> 16) & 0xff)) * cov) >> 8); + final int g = ((bgP >> 8) & 0xff) + (((((fgP >> 8) & 0xff) - ((bgP >> 8) & 0xff)) * cov) >> 8); + final int b = (bgP & 0xff) + ((((fgP & 0xff) - (bgP & 0xff)) * cov) >> 8); + srcPixel = (a << 24) | (r << 16) | (g << 8) | b; + } + + final int srcAlpha = (srcPixel >> 24) & 0xff; + if (srcAlpha == 255) { + renderBufferPixels[renderBufferOffset] = srcPixel; + } else if (srcAlpha != 0) { + final int destPixel = renderBufferPixels[renderBufferOffset]; + final int destR = (destPixel >> 16) & 0xff; + final int destG = (destPixel >> 8) & 0xff; + final int destB = destPixel & 0xff; + final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8); + final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8); + final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8); + renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b; + } + + tx += txStep; + ty += tyStep; + renderBufferOffset++; + } + + done += block; + } + } + + /** + * Affine texture mapping (u, v linear in screen space). Used for + * near-plane straddlers and for triangles small/flat enough that + * affine is within half a texel of exact perspective mapping. + */ + private void paintAffine(final int yTop, final int yBottom, + final TextureBitmap mipmap, + final RenderingContext renderBuffer, + final Point2D projectedPoint1, final Point2D projectedPoint2, + final Point2D projectedPoint3, + final Point2D texturePoint1, final Point2D texturePoint2, + final Point2D texturePoint3, + final double z1, final double z2, final double z3) { + final PolygonBorderInterpolator[] interpolators = INTERPOLATORS.get(); + final PolygonBorderInterpolator pbi1 = interpolators[0]; + final PolygonBorderInterpolator pbi2 = interpolators[1]; + final PolygonBorderInterpolator pbi3 = interpolators[2]; + + pbi1.setPoints(projectedPoint1, projectedPoint2, texturePoint1, texturePoint2); + pbi2.setPoints(projectedPoint1, projectedPoint3, texturePoint1, texturePoint3); + pbi3.setPoints(projectedPoint2, projectedPoint3, texturePoint2, texturePoint3); + + final double zw1 = 1d / z1; + final double zw2 = 1d / z2; + final double zw3 = 1d / z3; + pbi1.setPointsZW(zw1, zw2); + pbi2.setPointsZW(zw1, zw3); + pbi3.setPointsZW(zw2, zw3); + for (int y = yTop; y <= yBottom; y++) { + if (pbi1.containsY(y)) { + if (pbi2.containsY(y)) + drawHorizontalLineZ(pbi1, pbi2, y, renderBuffer, mipmap); + else if (pbi3.containsY(y)) + drawHorizontalLineZ(pbi1, pbi3, y, renderBuffer, mipmap); + } else if (pbi2.containsY(y)) { + if (pbi3.containsY(y)) + drawHorizontalLineZ(pbi2, pbi3, y, renderBuffer, mipmap); + } + } + + } + + /** + * Z-buffer span writer: per-pixel depth test (biased 1/z, linear + * along the span) BEFORE the texture fetch. Opaque texels write + * depth; blended texels write color only. + */ + private void drawHorizontalLineZ(final PolygonBorderInterpolator line1, + final PolygonBorderInterpolator line2, + final int y, + final RenderingContext renderBuffer, + final TextureBitmap textureBitmap) { + + line1.setCurrentY(y); + line2.setCurrentY(y); + + int x1 = line1.getX(); + int x2 = line2.getX(); + + final double tx1, ty1, zw1; + final double tx2, ty2, zw2; + + if (x1 <= x2) { + tx1 = line1.getTX() * textureBitmap.multiplicationFactor; + ty1 = line1.getTY() * textureBitmap.multiplicationFactor; + zw1 = line1.getZW(); + tx2 = line2.getTX() * textureBitmap.multiplicationFactor; + ty2 = line2.getTY() * textureBitmap.multiplicationFactor; + zw2 = line2.getZW(); + } else { + final int tmp = x1; + x1 = x2; + x2 = tmp; + + tx1 = line2.getTX() * textureBitmap.multiplicationFactor; + ty1 = line2.getTY() * textureBitmap.multiplicationFactor; + zw1 = line2.getZW(); + + tx2 = line1.getTX() * textureBitmap.multiplicationFactor; + ty2 = line1.getTY() * textureBitmap.multiplicationFactor; + zw2 = line1.getZW(); + } + + final double realWidth = x2 - x1; + final double realX1 = x1; + + if (x1 < renderBuffer.renderMinX) + x1 = renderBuffer.renderMinX; + + // x2 is exclusive: clamp to renderMaxX (see drawHorizontalLine) + if (x2 >= renderBuffer.renderMaxX) + x2 = renderBuffer.renderMaxX; + + if (PROF) { + PROF_SPANS.incrementAndGet(); + PROF_PIXELS.addAndGet(Math.max(0, x2 - x1)); + } + + int renderBufferOffset = (y * renderBuffer.width) + x1; + final int[] renderBufferPixels = renderBuffer.pixels; + final float[] depth = renderBuffer.depth; + // Alpha pass (depthPass 2): depth-test but never depth-write + final boolean writeDepth = renderBuffer.depthPass != 2; + + final double txStep = (tx2 - tx1) / realWidth; + final double tyStep = (ty2 - ty1) / realWidth; + final double dzw = (zw2 - zw1) / realWidth; + double tx = tx1 + txStep * (x1 - realX1); + double ty = ty1 + tyStep * (x1 - realX1); + double zw = zw1 + dzw * (x1 - realX1); + + final int[] texPixels = textureBitmap.pixels; + final int texW = textureBitmap.width; + final int texH = textureBitmap.height; + final int texWMinus1 = texW - 1; + final int texHMinus1 = texH - 1; + // texture is null in unit tests: clamp (see drawHorizontalLine) + final boolean wrap = texture != null && texture.wrap; + + for (int x = x1; x < x2; x++) { + + if (zw > depth[renderBufferOffset] - RenderingContext.DEPTH_MARGIN_DZ * zw * zw) { + int itx = (int) tx; + int ity = (int) ty; + + if (wrap) { + itx = Math.floorMod(itx, texW); + ity = Math.floorMod(ity, texH); + } else { + if (itx < 0) itx = 0; + else if (itx > texWMinus1) itx = texWMinus1; + + if (ity < 0) ity = 0; + else if (ity > texHMinus1) ity = texHMinus1; + } + + final int srcPixel = texPixels[ity * texW + itx]; + final int srcAlpha = (srcPixel >> 24) & 0xff; + + if (srcAlpha == 255) { + renderBufferPixels[renderBufferOffset] = srcPixel; + if (writeDepth) + depth[renderBufferOffset] = (float) zw; + } else if (srcAlpha != 0) { + // Translucent: blend without writing depth + final int destPixel = renderBufferPixels[renderBufferOffset]; + final int destR = (destPixel >> 16) & 0xff; + final int destG = (destPixel >> 8) & 0xff; + final int destB = destPixel & 0xff; + + final int r = destR + ((srcAlpha * (((srcPixel >> 16) & 0xff) - destR) - destR) >> 8); + final int g = destG + ((srcAlpha * (((srcPixel >> 8) & 0xff) - destG) - destG) >> 8); + final int b = destB + ((srcAlpha * ((srcPixel & 0xff) - destB) - destB) >> 8); + + renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b; + } + } + + tx += txStep; + ty += tyStep; + zw += dzw; + renderBufferOffset++; + } + + } + + /** + * Checks if backface culling is enabled for this triangle. + * + * @return {@code true} if backface culling is enabled + */ + public boolean isBackfaceCullingEnabled() { + return backfaceCulling; + } + + /** + * Enables or disables backface culling for this triangle. + * + * @param backfaceCulling {@code true} to enable backface culling + */ + public void setBackfaceCulling(final boolean backfaceCulling) { + this.backfaceCulling = backfaceCulling; + } + + /** + * Draws the triangle border edges in yellow (for debugging). + * + * @param renderBuffer the rendering context + */ + private void showBorders(final RenderingContext renderBuffer) { + + final Point2D projectedPoint1 = vertices.get(0).onScreenCoordinate(renderBuffer); + final Point2D projectedPoint2 = vertices.get(1).onScreenCoordinate(renderBuffer); + final Point2D projectedPoint3 = vertices.get(2).onScreenCoordinate(renderBuffer); + + final int x1 = (int) projectedPoint1.x; + final int y1 = (int) projectedPoint1.y; + final int x2 = (int) projectedPoint2.x; + final int y2 = (int) projectedPoint2.y; + final int x3 = (int) projectedPoint3.x; + final int y3 = (int) projectedPoint3.y; + + renderBuffer.executeWithGraphics(g -> { + g.setColor(Color.YELLOW); + g.drawLine(x1, y1, x2, y2); + g.drawLine(x3, y3, x2, y2); + g.drawLine(x1, y1, x3, y3); + }); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TriangleMeshBlock.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TriangleMeshBlock.java new file mode 100644 index 0000000..5220870 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TriangleMeshBlock.java @@ -0,0 +1,502 @@ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon; + +import eu.svjatoslav.aukio.e3d.geometry.Box; +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.HiZPyramid; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.renderer.raster.StereoEye; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture; +import eu.svjatoslav.aukio.e3d.math.TransformStack; + +/** + * A block of textured triangles stored as flat primitive arrays + * (struct-of-arrays) instead of one object graph per triangle. Built once + * (off the render thread), then every frame a single tight loop applies + * the composed camera transform to all vertices — sequential memory + * access instead of pointer chasing through {@code Vertex}/{@code Point3D} + * soup, which is what made the transform phase memory-latency-bound. + * + *

Interaction with the rest of the pipeline is unchanged: each visible + * triangle is queued as a thin preallocated {@link MeshTriangle} handle + * into the same {@link RenderAggregator}, sorted by the same comparator, + * binned into the same tile grid, and painted by the same + * {@link TexturedTriangle#paintFlat} core — bit-exact with the + * object-backed path.

+ * + *

Subpixel-cull verdicts are cached per triangle ({@link #cullEpoch}): + * while the verdict epoch holds, a culled triangle costs one integer + * compare per frame — no vertex math at all.

+ * + *

Limitations vs object-backed triangles: no mouse picking, no SDF + * textures (rejected at build), no GI/lightmap integration.

+ */ +public final class TriangleMeshBlock extends AbstractShape { + + // Build-time geometry: 9 world doubles and 6 UV doubles per triangle. + private final double[] world; + private final double[] uv; + private final Texture[] textures; + private final boolean backfaceCull; + private final int triCount; + private final MeshTriangle[] handles; + private final Box boundingBox; + + // Per-slot projected state: 3 screen/camera doubles per triangle per + // slot. (Sort Z and binning bounds are published into the handles' + // per-slot fields at transform time, not kept here.) + private final double[][] projX = new double[3][]; + private final double[][] projY = new double[3][]; + private final double[][] camZ = new double[3][]; + + // Subpixel-cull verdicts: epoch in which the triangle was found tiny. + private final int[] cullEpoch; + + // Near-plane clip output per slot: packed (offset << 3) | count into + // clipStore, -1 = not clipped. Clip data is 7 doubles per loop vertex + // (camera x, y, z, u, v + screen x, y), at most 4 vertices per + // straddler. Sort Z and binning bounds for straddlers are derived + // from the store, exactly like the object path derives them from the + // clipped loop. clipTtd keeps the ORIGINAL triangle's UV perimeter + // per straddler (index = clip offset / 28): mipmap selection must + // use the unclipped texel density, exactly like the object path. + private static final int CLIP_STRIDE = 7; + private static final int CLIP_ENTRY = 4 * CLIP_STRIDE; + private final int[][] clipRef = new int[3][]; + private final double[][] clipStore = new double[3][]; + private final double[][] clipTtd = new double[3][]; + + /** Per-triangle UV perimeter (mipmap metric), computed once at build: + * the uv array never changes afterwards. Same expression and + * accumulation order as {@code TexturedTriangle}'s + * computeTotalTextureDistance: d(0,1) + d(0,2) + d(1,2). */ + private final double[] uvPerimeter; + + /** Per-triangle screen-edge perimeter (mipmap metric's visible side), + * computed once per slot in the transform loop — identical expression + * to what the paint core used to recompute per tile. */ + private final double[][] screenPerim = new double[3][]; + private final int[] clipUsed = new int[3]; + + // Scratch for the composed top transform of the current transform call. + private final double[] top = new double[12]; + + /** + * Builds a block from baked world-space triangle soup. + * + * @param world 9 doubles per triangle (x0,y0,z0,x1,...), world + * space; the array is adopted, not copied + * @param uv 6 doubles per triangle (u0,v0,...) in primary + * texture pixels; adopted + * @param textures one texture per triangle + * @param backfaceCull cull clockwise triangles on screen + */ + public TriangleMeshBlock(final double[] world, final double[] uv, + final Texture[] textures, + final boolean backfaceCull) { + this.triCount = textures.length; + if (world.length != triCount * 9 || uv.length != triCount * 6) + throw new IllegalArgumentException("array length mismatch"); + this.world = world; + this.uv = uv; + this.textures = textures; + this.backfaceCull = backfaceCull; + for (final Texture texture : textures) + if (texture != null && texture.isSdf()) + throw new IllegalArgumentException( + "SDF textures are not supported in mesh blocks"); + + this.handles = new MeshTriangle[triCount]; + for (int t = 0; t < triCount; t++) + handles[t] = new MeshTriangle(this, t, textures[t]); + + this.cullEpoch = new int[triCount]; + java.util.Arrays.fill(cullEpoch, -1); + + for (int s = 0; s < 3; s++) { + projX[s] = new double[triCount * 3]; + projY[s] = new double[triCount * 3]; + camZ[s] = new double[triCount * 3]; + clipRef[s] = new int[triCount]; + java.util.Arrays.fill(clipRef[s], -1); + clipStore[s] = new double[256]; + clipTtd[s] = new double[16]; + screenPerim[s] = new double[triCount]; + } + + uvPerimeter = new double[triCount]; + for (int t = 0; t < triCount; t++) { + final int u = t * 6; + final double d1 = Math.sqrt( + ((uv[u] - uv[u + 2]) * (uv[u] - uv[u + 2])) + + ((uv[u + 1] - uv[u + 3]) * (uv[u + 1] - uv[u + 3]))); + final double d2 = Math.sqrt( + ((uv[u] - uv[u + 4]) * (uv[u] - uv[u + 4])) + + ((uv[u + 1] - uv[u + 5]) * (uv[u + 1] - uv[u + 5]))); + final double d3 = Math.sqrt( + ((uv[u + 2] - uv[u + 4]) * (uv[u + 2] - uv[u + 4])) + + ((uv[u + 3] - uv[u + 5]) * (uv[u + 3] - uv[u + 5]))); + uvPerimeter[t] = d1 + d2 + d3; + } + + double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE, + minZ = Double.MAX_VALUE; + double maxX = -Double.MAX_VALUE, maxY = -Double.MAX_VALUE, + maxZ = -Double.MAX_VALUE; + for (int i = 0; i < world.length; i += 3) { + if (world[i] < minX) minX = world[i]; + if (world[i] > maxX) maxX = world[i]; + if (world[i + 1] < minY) minY = world[i + 1]; + if (world[i + 1] > maxY) maxY = world[i + 1]; + if (world[i + 2] < minZ) minZ = world[i + 2]; + if (world[i + 2] > maxZ) maxZ = world[i + 2]; + } + boundingBox = new Box(new Point3D(minX, minY, minZ), + new Point3D(maxX, maxY, maxZ)); + } + + public int triCount() { + return triCount; + } + + @Override + public Box getBoundingBox() { + return boundingBox; + } + + @Override + public int getTransformWeight(final RenderingContext renderingContext) { + return Math.max(1, triCount); + } + + /** + * Transforms every triangle of the block with the composed top + * transform of the stack (hoisted out of the loop), culls (near + * plane, subpixel with verdict cache, viewport) and queues a thin + * handle per surviving triangle. All expressions replicate + * {@code TransformStack.transform} / + * {@code Vertex.calculateLocationRelativeToViewer} exactly, so output + * is bit-identical with the object-backed path. + */ + @Override + public void transform(final TransformStack transforms, + final RenderAggregator aggregator, + final RenderingContext renderingContext) { + final int slot = renderingContext.vertexSlot; + final double[] px = projX[slot]; + final double[] py = projY[slot]; + final double[] cz = camZ[slot]; + final int[] cref = clipRef[slot]; + clipUsed[slot] = 0; + + transforms.getTopTransform(top); + final double r0 = top[0], r1 = top[1], r2 = top[2]; + final double r3 = top[3], r4 = top[4], r5 = top[5]; + final double r6 = top[6], r7 = top[7], r8 = top[8]; + final double t0 = top[9], t1 = top[10], t2 = top[11]; + + final double near = renderingContext.nearPlaneDistance; + final double scale = renderingContext.projectionScale; + final double centerX = renderingContext.centerCoordinate.x; + final double centerY = renderingContext.centerCoordinate.y; + final double stereo = renderingContext.stereoViewportOffsetX; + + // Hi-Z whole-block occlusion: test the world AABB against last + // frame's depth pyramid before touching a single triangle. + // Skipped in stereo (the pyramid is mono-view) and whenever a + // corner crosses the near plane (its projection is unreliable). + final HiZPyramid hiz = renderingContext.occlusionPyramid; + if (hiz != null + && renderingContext.stereoEye == StereoEye.NONE) { + hiz.blocksTested.incrementAndGet(); + final Point3D lo = boundingBox.p1, hi = boundingBox.p2; + double ax1 = Double.MAX_VALUE, ay1 = Double.MAX_VALUE; + double ax2 = -Double.MAX_VALUE, ay2 = -Double.MAX_VALUE; + double nearestW = -Double.MAX_VALUE; + boolean usable = true; + for (int c = 0; c < 8; c++) { + final double wx = (c & 1) != 0 ? hi.x : lo.x; + final double wy = (c & 2) != 0 ? hi.y : lo.y; + final double wz = (c & 4) != 0 ? hi.z : lo.z; + final double ccz = r6 * wx + r7 * wy + r8 * wz + t2; + if (ccz <= near) { + usable = false; + break; + } + final double ccx = r0 * wx + r1 * wy + r2 * wz + t0; + final double ccy = r3 * wx + r4 * wy + r5 * wz + t1; + final double sx = ((ccx / ccz) * scale) + centerX + stereo; + final double sy = ((ccy / ccz) * scale) + centerY; + if (sx < ax1) ax1 = sx; + if (sx > ax2) ax2 = sx; + if (sy < ay1) ay1 = sy; + if (sy > ay2) ay2 = sy; + final double w = 1d / ccz; + if (w > nearestW) nearestW = w; + } + if (usable && ax1 <= ax2 && ay1 <= ay2 + && hiz.occluded(ax1, ay1, ax2, ay2, nearestW)) { + hiz.blocksCulled.incrementAndGet(); + return; + } + } + final double cullThreshold = renderingContext.subpixelCullingThreshold; + final int epoch = renderingContext.subpixelCullingEpoch; + final double rMinX = renderingContext.renderMinX; + final double rMaxX = renderingContext.renderMaxX; + final double rMinY = renderingContext.renderMinY; + final double rMaxY = renderingContext.renderMaxY; + + for (int t = 0; t < triCount; t++) { + if (cullThreshold > 0 && cullEpoch[t] == epoch) + continue; + + final int w = t * 9; + // Same expression order as TransformStack.transform. + final double x0 = world[w], y0 = world[w + 1], z0 = world[w + 2]; + final double cx0 = r0 * x0 + r1 * y0 + r2 * z0 + t0; + final double cy0 = r3 * x0 + r4 * y0 + r5 * z0 + t1; + final double cz0 = r6 * x0 + r7 * y0 + r8 * z0 + t2; + final double x1 = world[w + 3], y1 = world[w + 4], z1 = world[w + 5]; + final double cx1 = r0 * x1 + r1 * y1 + r2 * z1 + t0; + final double cy1 = r3 * x1 + r4 * y1 + r5 * z1 + t1; + final double cz1 = r6 * x1 + r7 * y1 + r8 * z1 + t2; + final double x2 = world[w + 6], y2 = world[w + 7], z2 = world[w + 8]; + final double cx2 = r0 * x2 + r1 * y2 + r2 * z2 + t0; + final double cy2 = r3 * x2 + r4 * y2 + r5 * z2 + t1; + final double cz2 = r6 * x2 + r7 * y2 + r8 * z2 + t2; + + final boolean in0 = cz0 > near; + final boolean in1 = cz1 > near; + final boolean in2 = cz2 > near; + + if (!in0 && !in1 && !in2) { + cref[t] = -1; + continue; + } + + final int v = t * 3; + if (!(in0 && in1 && in2)) { + clipAndQueue(t, v, slot, cx0, cy0, cz0, cx1, cy1, cz1, + cx2, cy2, cz2, in0, in1, in2, near, cref, + aggregator, renderingContext); + continue; + } + + cref[t] = -1; + cz[v] = cz0; + cz[v + 1] = cz1; + cz[v + 2] = cz2; + // Same expression order as + // Vertex.calculateLocationRelativeToViewer (divide, scale, + // add center, add stereo offset). + final double sx0 = ((cx0 / cz0) * scale) + centerX + stereo; + final double sy0 = ((cy0 / cz0) * scale) + centerY; + final double sx1 = ((cx1 / cz1) * scale) + centerX + stereo; + final double sy1 = ((cy1 / cz1) * scale) + centerY; + final double sx2 = ((cx2 / cz2) * scale) + centerX + stereo; + final double sy2 = ((cy2 / cz2) * scale) + centerY; + px[v] = sx0; + py[v] = sy0; + px[v + 1] = sx1; + py[v + 1] = sy1; + px[v + 2] = sx2; + py[v + 2] = sy2; + final double triZ = (cz0 + cz1 + cz2) / 3; + + final double minX = Math.min(sx0, Math.min(sx1, sx2)); + final double maxX = Math.max(sx0, Math.max(sx1, sx2)); + final double minY = Math.min(sy0, Math.min(sy1, sy2)); + final double maxY = Math.max(sy0, Math.max(sy1, sy2)); + + // Publish the same per-slot state an object-backed triangle + // would have written (paint margins are 0 for mesh tris): + // comparator and tile binning then read plain fields. + handles[t].publishSlotState(slot, triZ, minY, maxY, minX, maxX); + + // Subpixel verdict with per-triangle cache (same raw-span + // semantics as AbstractCoordinateShape). + if (cullThreshold > 0 + && maxX - minX < cullThreshold + && maxY - minY < cullThreshold) { + cullEpoch[t] = epoch; + continue; + } + + // Viewport cull (paint margins are 0 for mesh triangles). + if (maxX < rMinX || minX >= rMaxX || maxY < rMinY || minY >= rMaxY) + continue; + + // Mipmap metric's visible side, once per slot instead of per + // tile: edge12 + edge13 + edge23, the same expression the + // paint core ran per tile (getDistanceTo sequence). + final double dx01 = sx0 - sx1, dy01 = sy0 - sy1; + final double dx02 = sx0 - sx2, dy02 = sy0 - sy2; + final double dx12 = sx1 - sx2, dy12 = sy1 - sy2; + screenPerim[slot][t] = Math.sqrt(dx01 * dx01 + dy01 * dy01) + + Math.sqrt(dx02 * dx02 + dy02 * dy02) + + Math.sqrt(dx12 * dx12 + dy12 * dy12); + + aggregator.queueShapeForRendering(handles[t]); + } + } + + /** + * Near-plane clip for one straddling triangle, Sutherland-Hodgman + * over the three edges with the exact interpolation expressions of + * {@code AbstractCoordinateShape.interpolateAtPlane}. Output goes to + * the slot's grow-only clip store; the handle is queued with a packed + * reference. + */ + private void clipAndQueue(final int t, final int v, final int slot, + final double cx0, final double cy0, final double cz0, + final double cx1, final double cy1, final double cz1, + final double cx2, final double cy2, final double cz2, + final boolean in0, final boolean in1, final boolean in2, + final double near, final int[] cref, + final RenderAggregator aggregator, + final RenderingContext renderingContext) { + double[] store = clipStore[slot]; + int used = clipUsed[slot]; + if (used + CLIP_ENTRY > store.length) { + final double[] grown = new double[store.length * 2]; + System.arraycopy(store, 0, grown, 0, used); + store = grown; + clipStore[slot] = grown; + final double[] grownTtd = new double[grown.length / CLIP_ENTRY]; + System.arraycopy(clipTtd[slot], 0, grownTtd, 0, clipTtd[slot].length); + clipTtd[slot] = grownTtd; + } + final int base = used; + clipTtd[slot][base / CLIP_ENTRY] = uvPerimeter[t]; + + final double scale = renderingContext.projectionScale; + final double centerX = renderingContext.centerCoordinate.x; + final double centerY = renderingContext.centerCoordinate.y; + final double stereo = renderingContext.stereoViewportOffsetX; + + final double[] cx = {cx0, cx1, cx2}; + final double[] cy = {cy0, cy1, cy2}; + final double[] czz = {cz0, cz1, cz2}; + final boolean[] in = {in0, in1, in2}; + final int uvi = t * 6; + + int n = 0; + double sumZ = 0; + for (int i = 0; i < 3; i++) { + final int j = (i + 1) % 3; + final boolean currentIn = in[i]; + final boolean nextIn = in[j]; + if (currentIn) { + store[used++] = cx[i]; + store[used++] = cy[i]; + store[used++] = czz[i]; + store[used++] = uv[uvi + i * 2]; + store[used++] = uv[uvi + i * 2 + 1]; + // setCameraSpaceCoordinate expression, same order + store[used++] = ((cx[i] / czz[i]) * scale) + centerX + stereo; + store[used++] = ((cy[i] / czz[i]) * scale) + centerY; + n++; + sumZ += czz[i]; + } + if (currentIn != nextIn) { + final double tt = (near - czz[i]) / (czz[j] - czz[i]); + final double ix = cx[i] + (cx[j] - cx[i]) * tt; + final double iy = cy[i] + (cy[j] - cy[i]) * tt; + final double iz = czz[i] + (czz[j] - czz[i]) * tt; + store[used++] = ix; + store[used++] = iy; + store[used++] = iz; + store[used++] = uv[uvi + i * 2] + + (uv[uvi + j * 2] - uv[uvi + i * 2]) * tt; + store[used++] = uv[uvi + i * 2 + 1] + + (uv[uvi + j * 2 + 1] - uv[uvi + i * 2 + 1]) * tt; + store[used++] = ((ix / iz) * scale) + centerX + stereo; + store[used++] = ((iy / iz) * scale) + centerY; + n++; + sumZ += iz; + } + } + + // Degenerate sliver: fewer loop points than a renderable triangle. + if (n < 3) { + cref[t] = -1; + return; + } + clipUsed[slot] = used; + cref[t] = (base << 3) | n; + // Object path averages Z and derives bounds over the clipped loop + double cMinX = Double.MAX_VALUE, cMaxX = -Double.MAX_VALUE; + double cMinY = Double.MAX_VALUE, cMaxY = -Double.MAX_VALUE; + for (int i = 0; i < n; i++) { + final double sx = store[base + i * CLIP_STRIDE + 5]; + final double sy = store[base + i * CLIP_STRIDE + 6]; + if (sx < cMinX) cMinX = sx; + if (sx > cMaxX) cMaxX = sx; + if (sy < cMinY) cMinY = sy; + if (sy > cMaxY) cMaxY = sy; + } + handles[t].publishSlotState(slot, sumZ / n, cMinY, cMaxY, cMinX, cMaxX); + aggregator.queueShapeForRendering(handles[t]); + } + + // ---- handle-facing accessors (package-private) ---- + + double camZ(final int slot, final int tri, final int vertex) { + return camZ[slot][tri * 3 + vertex]; + } + + Texture texture(final int tri) { + return textures[tri]; + } + + boolean backfaceCull() { + return backfaceCull; + } + + int clipOffset(final int slot, final int tri) { + final int ref = clipRef[slot][tri]; + return ref < 0 ? -1 : ref >> 3; + } + + int clipCount(final int slot, final int tri) { + return clipRef[slot][tri] & 7; + } + + double[] clipStore(final int slot) { + return clipStore[slot]; + } + + double clipTtd(final int slot, final int clipOffset) { + return clipTtd[slot][clipOffset / CLIP_ENTRY]; + } + + /** The triangle's UV perimeter, precomputed at build (see field). */ + double uvPerimeter(final int tri) { + return uvPerimeter[tri]; + } + + /** + * The triangle's screen-edge perimeter for this slot, precomputed in + * the transform loop (only valid for triangles queued unclipped). + */ + double screenPerim(final int slot, final int tri) { + return screenPerim[slot][tri]; + } + + /** + * Loads one unclipped triangle vertex (screen + UV) into the scratch + * carriers, values exactly as computed at transform time. + */ + void loadScreenVertex(final Point2D screen, final Point2D uvOut, + final int slot, final int tri, final int vertex, + final RenderingContext ctx) { + final int v = tri * 3 + vertex; + screen.x = projX[slot][v]; + screen.y = projY[slot][v]; + uvOut.x = uv[tri * 6 + vertex * 2]; + uvOut.y = uv[tri * 6 + vertex * 2 + 1]; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java new file mode 100644 index 0000000..3b138bb --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java @@ -0,0 +1,28 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Textured triangle rendering with perspective-correct UV mapping. + * + *

Textured triangles apply 2D textures to 3D triangles using UV coordinates. + * Quake-style subdivided perspective correction (per-scanline recovery of exact + * u/v from linearly interpolated u/z, v/z, 1/z) keeps textures correct at any + * triangle size, so no tessellation is needed.

+ * + *

Key classes:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle} - + * The base textured triangle with perspective-correct scanline rendering
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.PerspectiveBorderInterpolator} - + * Edge interpolation of u/z, v/z, 1/z gradients
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.PolygonBorderInterpolator} - + * Affine edge interpolation (fallback path)
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle + * @see eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon; diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java new file mode 100644 index 0000000..a0d8468 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java @@ -0,0 +1,108 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.Billboard; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture; + +import java.awt.Font; + +/** + * A text label rendered as a billboard texture that always faces the camera. + * + *

This shape renders a single line of text onto a {@link Texture} using the cell metrics + * defined in {@link TextCanvas} ({@link TextCanvas#FONT_CHAR_WIDTH_TEXTURE_PIXELS}, + * {@link TextCanvas#FONT_CHAR_HEIGHT_TEXTURE_PIXELS}), then displays the texture as a + * forward-oriented billboard via its {@link Billboard} superclass. The result + * is a text label that remains readable from any viewing angle.

+ * + *

Usage example:

+ *
{@code
+ * // Create a red text label at position (0, -50, 300)
+ * ForwardOrientedTextBlock label = new ForwardOrientedTextBlock(
+ *     new Point3D(0, -50, 300),
+ *     1.0,
+ *     2,
+ *     "Hello, World!",
+ *     Color.RED
+ * );
+ * shapeCollection.addShape(label);
+ * }
+ * + * @see Billboard + * @see TextCanvas + * @see Texture + */ +public class ForwardOrientedTextBlock extends Billboard { + + /** + * The font used to render the label text. Matches the SDF text + * pipeline's family (Liberation Mono Bold, Courier-metric-compatible) + * at the cell size used by {@link TextCanvas}. + */ + private static final Font FONT = createFont(); + + private static Font createFont() { + final Font font = new Font("Liberation Mono", Font.BOLD, 30); + if (!font.getFamily().toLowerCase().contains("liberation")) { + return new Font("Monospaced", Font.BOLD, 30); + } + return font; + } + + /** + * Creates a new forward-oriented text block at the given 3D position. + * + * @param point the 3D position where the text label is placed + * @param scale the scale factor controlling the rendered size of the text + * @param maxUpscaleFactor the maximum mipmap upscale factor for the backing texture + * @param text the text string to render + * @param textColor the color of the rendered text + */ + public ForwardOrientedTextBlock(final Point3D point, final double scale, + final int maxUpscaleFactor, final String text, + final eu.svjatoslav.aukio.e3d.renderer.raster.Color textColor) { + super(point, scale, getTexture(text, maxUpscaleFactor, textColor)); + + } + + /** + * Creates a {@link Texture} containing the rendered text string. + * + *

The texture dimensions are calculated from the text length and the cell metrics + * defined in {@link TextCanvas}. Each character is drawn individually at the appropriate + * horizontal offset.

+ * + * @param text the text string to render into the texture + * @param maxUpscaleFactor the maximum mipmap upscale factor for the texture + * @param textColor the color of the rendered text + * @return a new {@link Texture} containing the rendered text + */ + public static Texture getTexture(final String text, + final int maxUpscaleFactor, + final eu.svjatoslav.aukio.e3d.renderer.raster.Color textColor) { + + final Texture texture = new Texture(text.length() + * TextCanvas.FONT_CHAR_WIDTH_TEXTURE_PIXELS, TextCanvas.FONT_CHAR_HEIGHT_TEXTURE_PIXELS, + maxUpscaleFactor); + + // Put blue background to test if texture has correct size + // texture.graphics.setColor(Color.BLUE); + // texture.graphics.fillRect(0, 0, texture.primaryBitmap.width, + // texture.primaryBitmap.width); + + texture.graphics.setFont(FONT); + texture.graphics.setColor(textColor.toAwtColor()); + + for (int c = 0; c < text.length(); c++) + texture.graphics.drawChars(new char[]{text.charAt(c),}, 0, 1, + (c * TextCanvas.FONT_CHAR_WIDTH_TEXTURE_PIXELS), + (int) (TextCanvas.FONT_CHAR_HEIGHT_TEXTURE_PIXELS / 1.45)); + + return texture; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/Graph.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/Graph.java new file mode 100644 index 0000000..a54809b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/Graph.java @@ -0,0 +1,180 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.math.Transform; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.Line; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas; + +import java.util.List; + +/** + * A 2D graph visualization rendered in 3D space. + * + *

Plots a series of {@link Point2D} data points as a connected line graph, overlaid on a + * grid with horizontal and vertical grid lines, axis labels, and a title. The graph is + * rendered in the XY plane at the specified 3D location, with all dimensions scaled by + * a configurable scale factor.

+ * + *

The graph uses the following default configuration:

+ *
    + *
  • X-axis range: {@code 0} to {@code 20} (world units before scaling)
  • + *
  • Y-axis range: {@code -2} to {@code 2}
  • + *
  • Grid spacing: {@code 0.5} in both horizontal and vertical directions
  • + *
  • Grid color: semi-transparent blue ({@code rgba(100, 100, 250, 100)})
  • + *
  • Plot color: semi-transparent red ({@code rgba(255, 0, 0, 100)})
  • + *
+ * + *

Usage example:

+ *
{@code
+ * // Prepare data points
+ * List data = new ArrayList<>();
+ * for (double x = 0; x <= 20; x += 0.1) {
+ *     data.add(new Point2D(x, Math.sin(x)));
+ * }
+ *
+ * // Create a graph at position (0, 0, 500) with scale factor 10
+ * Graph graph = new Graph(10.0, data, "sin(x)", new Point3D(0, 0, 500));
+ *
+ * // Add to the scene
+ * shapeCollection.addShape(graph);
+ * }
+ * + * @see Line + * @see TextCanvas + * @see AbstractCompositeShape + */ +public class Graph extends AbstractCompositeShape { + + /** The width of the graph in unscaled world units. */ + private final double width; + /** The minimum Y-axis value. */ + private final double yMin; + /** The maximum Y-axis value. */ + private final double yMax; + /** The spacing between vertical grid lines along the X-axis. */ + private final double horizontalStep; + /** The spacing between horizontal grid lines along the Y-axis. */ + private final double verticalStep; + /** The color used for grid lines. */ + private final Color gridColor; + /** The width of grid lines in world units (after scaling). */ + private final double lineWidth; + /** The color used for the data plot line. */ + private final Color plotColor; + + /** + * Creates a new graph visualization at the specified 3D location. + * + *

The graph is constructed with grid lines, axis labels, plotted data, and a title + * label. All spatial dimensions are multiplied by the given scale factor.

+ * + * @param scale the scale factor applied to all spatial dimensions of the graph + * @param data the list of 2D data points to plot; consecutive points are connected by lines + * @param label the title text displayed above the graph + * @param location the 3D position of the graph's origin in the scene + */ + public Graph(final double scale, final List data, + final String label, final Point3D location) { + super(location); + + width = 20; + + yMin = -2; + yMax = 2; + + horizontalStep = 0.5; + verticalStep = 0.5; + + gridColor = new Color(100, 100, 250, 100); + + lineWidth = 0.1 * scale; + plotColor = new Color(255, 0, 0, 100); + + addVerticalLines(scale); + addXLabels(scale); + addHorizontalLinesAndLabels(scale); + plotData(scale, data); + + final Point3D labelLocation = new Point3D(width / 2, yMax + 0.5, 0) + .multiply(scale); + + final TextCanvas labelCanvas = new TextCanvas(new Transform( + labelLocation), label, Color.WHITE, Color.TRANSPARENT); + + addShape(labelCanvas); + } + + private void addHorizontalLinesAndLabels(final double scale) { + for (double y = yMin; y <= yMax; y += verticalStep) { + + final Point3D p1 = new Point3D(0, y, 0).multiply(scale); + + final Point3D p2 = new Point3D(width, y, 0).multiply(scale); + + final Line line = new Line(p1, p2, gridColor, lineWidth); + + addShape(line); + + final Point3D labelLocation = new Point3D(-0.5, y, 0) + .multiply(scale); + + final TextCanvas label = new TextCanvas( + new Transform(labelLocation), String.valueOf(y), + Color.WHITE, Color.TRANSPARENT); + + addShape(label); + + } + } + + private void addVerticalLines(final double scale) { + for (double x = 0; x <= width; x += horizontalStep) { + + final Point3D p1 = new Point3D(x, yMin, 0).multiply(scale); + final Point3D p2 = new Point3D(x, yMax, 0).multiply(scale); + + final Line line = new Line(p1, p2, gridColor, lineWidth); + + addShape(line); + + } + } + + private void addXLabels(final double scale) { + for (double x = 0; x <= width; x += horizontalStep * 2) { + final Point3D labelLocation = new Point3D(x, yMin - 0.4, 0) + .multiply(scale); + + final TextCanvas label = new TextCanvas( + new Transform(labelLocation), String.valueOf(x), + Color.WHITE, Color.TRANSPARENT); + + addShape(label); + } + } + + private void plotData(final double scale, final List data) { + Point3D previousPoint = null; + for (final Point2D point : data) { + + final Point3D p3d = new Point3D(point.x, point.y, 0).multiply(scale); + + if (previousPoint != null) { + + final Line line = new Line(previousPoint, p3d, plotColor, + 0.4 * scale); + + addShape(line); + } + + previousPoint = p3d; + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightSourceMarker.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightSourceMarker.java new file mode 100755 index 0000000..0ef6094 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightSourceMarker.java @@ -0,0 +1,43 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.GlowingPoint; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A visual marker that indicates a light source position in the 3D scene. + * + *

Rendered as a glowing point that provides a clear, lightweight visual + * indicator useful for debugging light placement in the scene.

+ * + *

Usage example:

+ *
{@code
+ * // Place a yellow light source marker at position (100, -50, 200)
+ * LightSourceMarker marker = new LightSourceMarker(
+ *     new Point3D(100, -50, 200),
+ *     Color.YELLOW
+ * );
+ * shapeCollection.addShape(marker);
+ * }
+ * + * @see GlowingPoint + * @see AbstractCompositeShape + */ +public class LightSourceMarker extends AbstractCompositeShape { + + /** + * Creates a new light source marker at the specified location. + * + * @param location the 3D position of the marker in the scene + * @param color the color of the glowing point + */ + public LightSourceMarker(final Point3D location, final Color color) { + super(location); + addShape(new GlowingPoint(new Point3D(0, 0, 0), 15, color)); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightmappedCompositeShape.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightmappedCompositeShape.java new file mode 100644 index 0000000..9aba6d4 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/LightmappedCompositeShape.java @@ -0,0 +1,176 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.gi.LightmappedTriangle; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +import java.util.ArrayList; +import java.util.List; + +/** + * Composite shape whose solid polygons can render as lightmapped + * triangles for the global illumination system. + * + *

Lightmapping: when enabled, every {@link SolidPolygon} + * (including those inside nested composites, which are flattened) is + * wrapped as a {@link LightmappedTriangle} with its own lightmap texture, + * and the GI system paints light across its surface instead of leaving + * it a single flat color. Rebuild is automatic via the normal + * cache-invalidation path.

+ * + *

Ordering: none required — the engine's z-buffer resolves + * visibility per pixel, so this class needs no spatial ordering + * structure of its own. Nested composites are flattened assuming + * identity transforms (vertices are used in the composite's local + * space); animate transforms above this composite, not inside it.

+ * + * @see LightmappedTriangle the per-fragment lightmap carrier + * @see AbstractCompositeShape the base composite (fan-triangulates + * N-vertex polygons when building the render list) + */ +public class LightmappedCompositeShape extends AbstractCompositeShape { + + /** + * When true, the composite's polygons render as + * {@link LightmappedTriangle}s: each polygon gets its own lightmap + * texture and the GI system paints light across its surface. + */ + private boolean lightmappingEnabled; + + /** World units per lightmap texel (smaller = finer GI detail). */ + private double lightmapUnitsPerTexel = 12.0; + + /** + * Replaces solid polygons with lightmapped wrappers when lightmapping + * is enabled; otherwise returns the list unchanged. + * + * @param renderList the render list built from the shape registry + * @return lightmapped wrappers + non-polygon passthrough shapes + */ + @Override + protected List postprocessRenderList(final List renderList) { + if (!lightmappingEnabled) + return renderList; + + final List result = new ArrayList<>(renderList.size()); + for (final AbstractShape shape : renderList) { + if (shape instanceof SolidPolygon) { + wrapLightmapped((SolidPolygon) shape, result); + } else if (shape instanceof AbstractCompositeShape) { + // Flatten nested composites. Assumes identity transforms + // on the nested composite chain. + for (final SolidPolygon polygon + : ((AbstractCompositeShape) shape).extractSolidPolygons()) + wrapLightmapped(polygon, result); + } else { + result.add(shape); + } + } + return result; + } + + /** + * Enables or disables lightmapping. When enabled, the composite's + * polygons render as {@link LightmappedTriangle}s with per-polygon + * lightmap textures filled by the global illumination system. + * Rebuilds the render list on the next frame. + * + * @param enabled true to render polygons lightmapped + */ + public void setLightmappingEnabled(final boolean enabled) { + if (lightmappingEnabled != enabled) { + lightmappingEnabled = enabled; + setCacheNeedsRebuild(true); + } + } + + /** + * Returns whether lightmapping is enabled. + * + * @return true when polygons render as lightmapped triangles + */ + public boolean isLightmappingEnabled() { + return lightmappingEnabled; + } + + /** + * Sets the lightmap resolution. Default 12 world units per texel: + * a 100-unit wall cell gets an 8x8 lightmap. Halving the units + * quadruples the GI tracing work; finer texels are the ONLY way to + * smoother shadow edges (there is no upsampling). Takes effect on the + * next render list rebuild. + * + * @param unitsPerTexel world units per texel + */ + public void setLightmapUnitsPerTexel(final double unitsPerTexel) { + lightmapUnitsPerTexel = unitsPerTexel; + setCacheNeedsRebuild(true); + } + + /** + * Fan-triangulates a polygon (triangles pass through as-is) and wraps + * each triangle into a lightmapped textured triangle with the same + * geometry, color and culling. The initial texture is dim; the GI + * system converges it to full lighting within seconds. + * + * @param polygon the polygon to wrap (any vertex count >= 3) + * @param out the list receiving the lightmapped triangles + */ + private void wrapLightmapped(final SolidPolygon polygon, final List out) { + final int vertexCount = polygon.getVertexCount(); + if (vertexCount == 3) { + out.add(wrapTriangle(polygon)); + return; + } + // Fan: anchor vertex 0, then consecutive pairs + final Point3D anchor = polygon.vertices.get(0).coordinate; + for (int i = 1; i + 1 < vertexCount; i++) { + final SolidPolygon triangle = new SolidPolygon( + anchor, + polygon.vertices.get(i).coordinate, + polygon.vertices.get(i + 1).coordinate, + polygon.getColor()); + triangle.setShadingEnabled(polygon.isShadingEnabled()); + triangle.setBackfaceCulling(polygon.isBackfaceCullingEnabled()); + triangle.setMouseInteractionController(polygon.mouseInteractionController); + out.add(wrapTriangle(triangle)); + } + } + + /** + * Wraps one triangle into a lightmapped textured triangle with the + * same geometry, color and culling. + */ + private AbstractCoordinateShape wrapTriangle(final SolidPolygon polygon) { + final Point3D a = polygon.vertices.get(0).coordinate; + final Point3D b = polygon.vertices.get(1).coordinate; + final Point3D c = polygon.vertices.get(2).coordinate; + + // Normal: right-handed cross(b-a, c-a). + final double e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z; + final double e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z; + double nx = e1y * e2z - e1z * e2y; + double ny = e1z * e2x - e1x * e2z; + double nz = e1x * e2y - e1y * e2x; + final double len = Math.sqrt(nx * nx + ny * ny + nz * nz); + if (len > 0) { + nx /= len; + ny /= len; + nz /= len; + } + + final LightmappedTriangle triangle = new LightmappedTriangle( + a, b, c, polygon.getColor(), lightmapUnitsPerTexel, + nx, ny, nz); + triangle.setBackfaceCulling(polygon.isBackfaceCullingEnabled()); + triangle.setMouseInteractionController(polygon.mouseInteractionController); + return triangle; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/TexturedRectangle.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/TexturedRectangle.java new file mode 100644 index 0000000..a6b5db3 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/TexturedRectangle.java @@ -0,0 +1,180 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.math.Transform; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture; + +/** + * A rectangular shape with texture mapping, composed of two textured triangles. + * + *

This composite shape creates a textured rectangle in 3D space by splitting it into + * two {@link TexturedTriangle} triangles that share a common {@link Texture}. The rectangle + * is centered at the origin of its local coordinate system, with configurable world-space + * dimensions and independent texture resolution.

+ * + *

The contained {@link Texture} object is accessible via {@link #getTexture()}, allowing + * dynamic rendering to the texture surface (e.g., drawing text, images, or procedural content) + * after construction.

+ * + *

Usage example:

+ *
{@code
+ * // Create a 200x100 textured rectangle at position (0, 0, 300)
+ * Transform transform = new Transform(new Point3D(0, 0, 300));
+ * TexturedRectangle rect = new TexturedRectangle(transform, 200, 100, 2);
+ *
+ * // Draw onto the texture dynamically
+ * Texture tex = rect.getTexture();
+ * tex.graphics.setColor(java.awt.Color.RED);
+ * tex.graphics.fillRect(0, 0, 50, 50);
+ *
+ * // Add to the scene
+ * shapeCollection.addShape(rect);
+ * }
+ * + * @see TexturedTriangle + * @see Texture + * @see AbstractCompositeShape + */ +public class TexturedRectangle extends AbstractCompositeShape { + + /** Top-left corner position in local 3D coordinates. */ + public Point3D topLeft; + /** Top-right corner position in local 3D coordinates. */ + public Point3D topRight; + /** Bottom-right corner position in local 3D coordinates. */ + public Point3D bottomRight; + /** Bottom-left corner position in local 3D coordinates. */ + public Point3D bottomLeft; + /** Top-left corner mapping in texture coordinates (pixels). */ + public Point2D textureTopLeft; + /** Top-right corner mapping in texture coordinates (pixels). */ + public Point2D textureTopRight; + /** Bottom-right corner mapping in texture coordinates (pixels). */ + public Point2D textureBottomRight; + /** Bottom-left corner mapping in texture coordinates (pixels). */ + public Point2D textureBottomLeft; + private Texture texture; + + /** + * Creates a textured rectangle with only a transform, without initializing geometry. + * + *

After construction, call {@link #initialize(double, double, int, int, int)} to + * set up the rectangle's dimensions, texture, and triangle geometry.

+ * + * @param transform the position and orientation of this rectangle in the scene + */ + public TexturedRectangle(final Transform transform) { + super(transform); + } + + /** + * Creates a textured rectangle where the texture resolution matches the world-space size. + * + *

This is a convenience constructor equivalent to calling + * {@link #TexturedRectangle(Transform, int, int, int, int, int)} with + * {@code textureWidth = width} and {@code textureHeight = height}.

+ * + * @param transform the position and orientation of this rectangle in the scene + * @param width the width of the rectangle in world units (also used as texture width in pixels) + * @param height the height of the rectangle in world units (also used as texture height in pixels) + * @param maxTextureUpscale the maximum mipmap upscale factor for the texture + */ + public TexturedRectangle(final Transform transform, final int width, + final int height, final int maxTextureUpscale) { + this(transform, width, height, width, height, maxTextureUpscale); + } + + /** + * Creates a fully initialized textured rectangle with independent world-space size and texture resolution. + * + * @param transform the position and orientation of this rectangle in the scene + * @param width the width of the rectangle in world units + * @param height the height of the rectangle in world units + * @param textureWidth the width of the backing texture in pixels + * @param textureHeight the height of the backing texture in pixels + * @param maxTextureUpscale the maximum mipmap upscale factor for the texture + */ + public TexturedRectangle(final Transform transform, final int width, + final int height, final int textureWidth, final int textureHeight, + final int maxTextureUpscale) { + + super(transform); + + initialize(width, height, textureWidth, textureHeight, + maxTextureUpscale); + } + + /** + * Returns the backing texture for this rectangle. + * + *

The returned {@link Texture} can be used to draw dynamic content onto the + * rectangle's surface via its {@code graphics} field (a {@link java.awt.Graphics2D} instance).

+ * + * @return the texture mapped onto this rectangle + */ + public Texture getTexture() { + return texture; + } + + /** + * Initializes the rectangle geometry, texture, and the two constituent textured triangles. + * + *

The rectangle is centered at the local origin: corners span from + * {@code (-width/2, -height/2, 0)} to {@code (width/2, height/2, 0)}. + * Two {@link TexturedTriangle} triangles are created to cover the full rectangle, + * sharing a single {@link Texture} instance.

+ * + * @param width the width of the rectangle in world units + * @param height the height of the rectangle in world units + * @param textureWidth the width of the backing texture in pixels + * @param textureHeight the height of the backing texture in pixels + * @param maxTextureUpscale the maximum mipmap upscale factor for the texture + */ + public void initialize(final double width, final double height, + final int textureWidth, final int textureHeight, + final int maxTextureUpscale) { + + topLeft = new Point3D(-width / 2, -height / 2, 0); + topRight = new Point3D(width / 2, -height / 2, 0); + bottomRight = new Point3D(width / 2, height / 2, 0); + bottomLeft = new Point3D(-width / 2, height / 2, 0); + + texture = new Texture(textureWidth, textureHeight, maxTextureUpscale); + + textureTopRight = new Point2D(textureWidth, 0); + textureTopLeft = new Point2D(0, 0); + textureBottomRight = new Point2D(textureWidth, textureHeight); + textureBottomLeft = new Point2D(0, textureHeight); + + + + + final TexturedTriangle texturedPolygon1 = new TexturedTriangle( + new Vertex(topLeft, textureTopLeft), + new Vertex(topRight, textureTopRight), + new Vertex(bottomRight, textureBottomRight), texture); + + texturedPolygon1 + .setMouseInteractionController(mouseInteractionController); + + final TexturedTriangle texturedPolygon2 = new TexturedTriangle( + new Vertex(topLeft, textureTopLeft), + new Vertex(bottomLeft, textureBottomLeft), + new Vertex(bottomRight, textureBottomRight), texture); + + texturedPolygon2 + .setMouseInteractionController(mouseInteractionController); + + addShape(texturedPolygon1); + addShape(texturedPolygon2); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java new file mode 100644 index 0000000..776d21b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java @@ -0,0 +1,1203 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base; + +import eu.svjatoslav.aukio.e3d.geometry.Box; +import eu.svjatoslav.aukio.e3d.renderer.raster.Frustum; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.gui.ViewSpaceTracker; +import eu.svjatoslav.aukio.e3d.gui.humaninput.MouseInteractionController; +import eu.svjatoslav.aukio.e3d.math.Transform; +import eu.svjatoslav.aukio.e3d.math.TransformStack; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.ParallelTransformCoordinator; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderAggregator; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.Line; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A composite shape that groups multiple sub-shapes into a single logical unit. + * + *

Use {@code AbstractCompositeShape} to build complex 3D objects by combining + * primitive shapes (lines, polygons, textured polygons) into a group that can be + * positioned, rotated, and manipulated as one entity. Sub-shapes can be organized + * into named groups for selective visibility toggling.

+ * + *

Usage example - creating a custom composite shape:

+ *
{@code
+ * // Create a composite shape at position (0, 0, 200)
+ * AbstractCompositeShape myObject = new AbstractCompositeShape(
+ *     new Point3D(0, 0, 200)
+ * );
+ *
+ * // Add sub-shapes
+ * myObject.addShape(new Line(
+ *     new Point3D(-50, 0, 0), new Point3D(50, 0, 0),
+ *     Color.RED, 2.0
+ * ));
+ *
+ * // Add shapes to a named group for toggling visibility
+ * myObject.addShape(labelShape, "labels");
+ * myObject.hideGroup("labels");  // hide all shapes in "labels" group
+ * myObject.showGroup("labels");  // show them again
+ *
+ * // Add to scene
+ * viewPanel.getRootShapeCollection().addShape(myObject);
+ * }
+ * + *

Perspective-correct texturing:

+ *

Textured polygons are rendered with Quake-style perspective-correct scanline + * mapping ({@code TexturedTriangle}), so no screen-size tessellation is needed.

+ * + *

Extending this class:

+ *

Override {@link #beforeTransformHook} to customize shape appearance or behavior + * on each frame (e.g., animations, dynamic geometry updates).

+ * + * @see SubShape wrapper for individual sub-shapes with group and visibility support + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape the base shape class + */ +public class AbstractCompositeShape extends AbstractShape { + /** + * Source-of-truth registry of all sub-shapes added to this composite. + * + *

Each sub-shape is wrapped with its group identifier and visibility state. + * Shapes are stored in insertion order and remain in this collection even when + * hidden (visibility state toggles instead of removal).

+ * + *

Performance note: This list is NOT processed for every frame. + * Instead, it serves as the authoritative source from which {@link #cachedRenderList} + * is compiled whenever the cache becomes invalid (see {@link #cacheNeedsRebuild}). + * Only modifications to this registry (add/remove/show/hide) trigger cache rebuild.

+ * + * @see #cachedRenderList the frame-optimized cache derived from this registry + * @see #cacheNeedsRebuild the flag controlling when the cache is rebuilt + */ + private final List subShapesRegistry = new ArrayList<>(); + + /** + * Tracks the distance and angle between the camera and this shape. + * Used e.g. by TextCanvas for distance-based rendering mode selection. + */ + private final ViewSpaceTracker viewSpaceTracker; + + /** + * Frame-optimized cache of shapes ready for rendering, derived from {@link #subShapesRegistry}. + * + *

This list is processed during every frame in the {@link #transform} method. + * It contains:

+ *
    + *
  • Shapes passing through directly (Line, TexturedTriangle, ...)
  • + *
  • Solid polygons with more than 3 vertices - fan-triangulated
  • + *
+ * + *

Caching strategy: The list is rebuilt only when + * {@link #cacheNeedsRebuild} is true, avoiding per-frame reconstruction + * overhead.

+ * + * @see #subShapesRegistry the source registry this cache is derived from + * @see #cacheNeedsRebuild the flag that triggers cache regeneration + */ + private List cachedRenderList = new ArrayList<>(); + + /** + * Flag indicating whether {@link #cachedRenderList} needs to be rebuilt from {@link #subShapesRegistry}. + * + *

Set to {@code true} when:

+ *
    + *
  • A shape is added via {@link #addShape}
  • + *
  • A shape is removed via {@link #removeGroup}
  • + *
  • Group visibility changes via {@link #showGroup} or {@link #hideGroup}
  • + *
+ * + *

Set to {@code false} after {@link #rebuildRenderList} completes the cache rebuild.

+ * + *

This flag enables the performance optimization of avoiding per-frame list + * reconstruction - the registry is only re-processed when something actually changed.

+ * + * @see #subShapesRegistry the source data that may need reprocessing + * @see #cachedRenderList the cache that gets rebuilt when this flag is true + */ + private boolean cacheNeedsRebuild = true; + + /** + * Flag indicating this composite is the root scene container (ShapeCollection's root). + * + *

Set via {@link #setRootComposite(boolean)} by ShapeCollection.

+ */ + private boolean isRootComposite = false; + + /** + * The position and orientation transform for this composite shape. + * Applied to all sub-shapes during the rendering transform pass. + */ + private Transform transform; + + /** + * Creates a composite shape at the world origin with no rotation. + */ + public AbstractCompositeShape() { + this(new Transform()); + } + + /** + * Creates a composite shape at the specified location with no rotation. + * + * @param location the position in world space + */ + public AbstractCompositeShape(final Point3D location) { + this(new Transform(location)); + } + + /** + * Creates a composite shape with the specified transform (position and orientation). + * + * @param transform the initial transform defining position and rotation + */ + public AbstractCompositeShape(final Transform transform) { + this.transform = transform; + viewSpaceTracker = new ViewSpaceTracker(); + } + + /** + * Adds a sub-shape to this composite shape without a group identifier. + * + * @param shape the shape to add + */ + public void addShape(final AbstractShape shape) { + addShape(shape, null); + } + + /** + * Adds a sub-shape to this composite shape with an optional group identifier. + * + *

Grouped shapes can be shown, hidden, or removed together using + * {@link #showGroup}, {@link #hideGroup}, and {@link #removeGroup}.

+ * + * @param shape the shape to add + * @param groupId the group identifier, or {@code null} for ungrouped shapes + */ + public void addShape(final AbstractShape shape, final String groupId) { + subShapesRegistry.add(new SubShape(shape, groupId, true)); + cacheNeedsRebuild = true; + } + + /** + * This method should be overridden by anyone wanting to customize the shape + * before it is rendered. + * + * @param transformPipe the current transform stack + * @param context the rendering context for the current frame + */ + public void beforeTransformHook(final TransformStack transformPipe, + final RenderingContext context) { + } + + /** + * Returns the world-space position of this composite shape. + * + * @return the translation component of this shape's transform + */ + public Point3D getLocation() { + return transform.getTranslation(); + } + + /** + * Returns the axis-aligned bounding box encompassing all sub-shapes. + * + *

The bounding box is computed by aggregating the bounds of all visible + * sub-shapes, then transforming the result by this composite's own transform.

+ * + *

Caching: The bounding box is recomputed whenever + * {@link #cacheNeedsRebuild} is true (shapes added/removed/visibility changed). + * For nested composites, the bounds include their local transform offset.

+ * + * @return the axis-aligned bounding box in this composite's local coordinates + */ + @Override + public Box getBoundingBox() { + if (cachedBoundingBox == null || cacheNeedsRebuild) { + if (subShapesRegistry.isEmpty()) { + return super.getBoundingBox(); + } + + double minX = Double.MAX_VALUE; + double maxX = -Double.MAX_VALUE; + double minY = Double.MAX_VALUE; + double maxY = -Double.MAX_VALUE; + double minZ = Double.MAX_VALUE; + double maxZ = -Double.MAX_VALUE; + + for (final SubShape subShape : subShapesRegistry) { + if (!subShape.isVisible()) { + continue; + } + + final AbstractShape shape = subShape.getShape(); + final Box shapeBounds = shape.getBoundingBox(); + + // Get bounds and apply sub-shape's transform if it's a composite + Point3D shapeMin = new Point3D(shapeBounds.getMinX(), shapeBounds.getMinY(), shapeBounds.getMinZ()); + Point3D shapeMax = new Point3D(shapeBounds.getMaxX(), shapeBounds.getMaxY(), shapeBounds.getMaxZ()); + if (shape instanceof AbstractCompositeShape) { + final Transform subTransform = ((AbstractCompositeShape) shape).getTransform(); + final Point3D subTranslation = subTransform.getTranslation(); + shapeMin.add(subTranslation); + shapeMax.add(subTranslation); + } + + minX = Math.min(minX, shapeMin.x); + maxX = Math.max(maxX, shapeMax.x); + minY = Math.min(minY, shapeMin.y); + maxY = Math.max(maxY, shapeMax.y); + minZ = Math.min(minZ, shapeMin.z); + maxZ = Math.max(maxZ, shapeMax.z); + } + + if (minX == Double.MAX_VALUE) { + // No visible shapes + return super.getBoundingBox(); + } + + cachedBoundingBox = new Box( + new Point3D(minX, minY, minZ), + new Point3D(maxX, maxY, maxZ) + ); + } + return cachedBoundingBox; + } + + /** + * Returns the sub-shapes registry (source of truth for all sub-shapes). + * + *

This is the authoritative list of all sub-shapes including hidden ones. + * For per-frame rendering, use {@link #cachedRenderList} instead (accessed internally).

+ * + * @return the registry list of all sub-shapes with their group and visibility metadata + * @see #cachedRenderList the frame-optimized cache derived from this registry + */ + public List getSubShapesRegistry() { + return subShapesRegistry; + } + + /** + * Extracts all SolidPolygon instances from this composite shape. + * + *

Recursively traverses the shape hierarchy and collects all + * SolidPolygon instances. Used for CSG operations where polygons + * are needed directly without conversion.

+ * + * @return list of SolidPolygon instances from this shape hierarchy + */ + public List extractSolidPolygons() { + final List result = new ArrayList<>(); + for (final SubShape subShape : subShapesRegistry) { + final AbstractShape shape = subShape.getShape(); + if (shape instanceof SolidPolygon) { + result.add((SolidPolygon) shape); + } else if (shape instanceof AbstractCompositeShape) { + result.addAll(((AbstractCompositeShape) shape).extractSolidPolygons()); + } + } + return result; + } + + /** + * Returns the view-space tracker that monitors the distance + * and angle between the camera and this shape for level-of-detail adjustments. + * + * @return the view-space tracker for this shape + */ + public ViewSpaceTracker getViewSpaceTracker() { + return viewSpaceTracker; + } + + /** + * Hides all sub-shapes belonging to the specified group. + * Hidden shapes are not rendered but remain in the collection. + * + * @param groupIdentifier the group to hide + * @see #showGroup(String) + * @see #removeGroup(String) + */ + public void hideGroup(final String groupIdentifier) { + for (final SubShape subShape : subShapesRegistry) { + if (subShape.matchesGroup(groupIdentifier)) { + subShape.setVisible(false); + cacheNeedsRebuild = true; + } + } + } + + /** + * Permanently removes all sub-shapes belonging to the specified group. + * + * @param groupIdentifier the group to remove + * @see #hideGroup(String) + */ + public void removeGroup(final String groupIdentifier) { + final java.util.Iterator iterator = subShapesRegistry + .iterator(); + + while (iterator.hasNext()) { + final SubShape subShape = iterator.next(); + if (subShape.matchesGroup(groupIdentifier)) { + iterator.remove(); + cacheNeedsRebuild = true; + } + } + } + + /** + * Returns all sub-shapes belonging to the specified group. + * + * @param groupIdentifier the group identifier to match + * @return list of matching sub-shapes + */ + public List getGroup(final String groupIdentifier) { + final List result = new ArrayList<>(); + for (int i = 0; i < subShapesRegistry.size(); i++) { + final SubShape subShape = subShapesRegistry.get(i); + if (subShape.matchesGroup(groupIdentifier)) + result.add(subShape); + } + return result; + } + + /** + * Rebuilds the cached render list if shapes were added, removed, or + * visibility changed since the last rebuild. + * + * @param context the rendering context for logging + */ + private void rebuildRenderListIfNeeded(final RenderingContext context) { + if (cacheNeedsRebuild) + rebuildRenderList(context); + } + + /** + * Paint solid elements of this composite shape into given color. + * + *

Applies recursively to nested {@code AbstractCompositeShape} sub-shapes.

+ * + * @param color the color to apply to all solid sub-shapes + */ + public void setColor(final Color color) { + for (final SubShape subShape : getSubShapesRegistry()) { + final AbstractShape shape = subShape.getShape(); + + if (shape instanceof SolidPolygon) { + ((SolidPolygon) shape).setColor(color); + } else if (shape instanceof Line) { + ((Line) shape).color = color; + } else if (shape instanceof AbstractCompositeShape) { + ((AbstractCompositeShape) shape).setColor(color); + } + } + } + + /** + * Assigns a group identifier to all sub-shapes that currently have no group. + * + * @param groupIdentifier the group to assign to ungrouped shapes + */ + public void setGroupForUngrouped(final String groupIdentifier) { + for (final SubShape subShape : subShapesRegistry) + if (subShape.isUngrouped()) + subShape.setGroup(groupIdentifier); + } + + @Override + public void setMouseInteractionController( + final MouseInteractionController mouseInteractionController) { + super.setMouseInteractionController(mouseInteractionController); + + for (final SubShape subShape : subShapesRegistry) + subShape.getShape().setMouseInteractionController( + mouseInteractionController); + + cacheNeedsRebuild = true; + } + + /** + * Marks this composite as the root scene container. + * + *

Called by {@code ShapeCollection} to configure its root composite.

+ * + * @param isRoot {@code true} if this is the root composite, {@code false} otherwise + */ + public void setRootComposite(final boolean isRoot) { + this.isRootComposite = isRoot; + } + + /** + * Returns this composite's transform (position and orientation). + * + * @return the transform object + */ + public Transform getTransform() { + return transform; + } + + /** + * Sets the transform for this composite shape. + * + * @param transform the new transform + * @return this composite shape (for chaining) + */ + public AbstractCompositeShape setTransform(final Transform transform) { + this.transform = transform; + return this; + } + +/** + * Sets the cache rebuild flag on this composite and all nested composites recursively. + * + *

Used by {@code ShapeCollection} to trigger a render-list rebuild when + * clearing the scene or for other advanced use cases.

+ * + * @param needsRebuild {@code true} to force cache rebuild on next frame + */ + public void setCacheNeedsRebuild(final boolean needsRebuild) { + this.cacheNeedsRebuild = needsRebuild; + // Propagate to nested composites + for (final SubShape subShape : subShapesRegistry) { + final AbstractShape shape = subShape.getShape(); + if (shape instanceof AbstractCompositeShape composite) { + composite.setCacheNeedsRebuild(needsRebuild); + } + } + } + + /** + * Enables or disables shading for all SolidTriangle and SolidPolygon sub-shapes. + * When enabled, shapes use the global lighting manager from the rendering + * context to calculate flat shading based on light sources. + * + *

Applies recursively to nested {@code AbstractCompositeShape} sub-shapes.

+ * + * @param shadingEnabled {@code true} to enable shading, {@code false} to disable + * @return this composite shape (for chaining) + */ + public AbstractCompositeShape setShadingEnabled(final boolean shadingEnabled) { + for (final SubShape subShape : getSubShapesRegistry()) { + final AbstractShape shape = subShape.getShape(); + if (shape instanceof SolidPolygon) { + ((SolidPolygon) shape).setShadingEnabled(shadingEnabled); + } else if (shape instanceof AbstractCompositeShape) { + ((AbstractCompositeShape) shape).setShadingEnabled(shadingEnabled); + } + } + return this; + } + + /** + * Enables or disables backface culling for all SolidPolygon and TexturedTriangle sub-shapes. + * + *

Applies recursively to nested {@code AbstractCompositeShape} sub-shapes.

+ * + * @param backfaceCulling {@code true} to enable backface culling, {@code false} to disable + * @return this composite shape (for chaining) + */ + public AbstractCompositeShape setBackfaceCulling(final boolean backfaceCulling) { + for (final SubShape subShape : getSubShapesRegistry()) { + final AbstractShape shape = subShape.getShape(); + if (shape instanceof SolidPolygon) { + ((SolidPolygon) shape).setBackfaceCulling(backfaceCulling); + } else if (shape instanceof TexturedTriangle) { + ((TexturedTriangle) shape).setBackfaceCulling(backfaceCulling); + } else if (shape instanceof AbstractCompositeShape) { + ((AbstractCompositeShape) shape).setBackfaceCulling(backfaceCulling); + } + } + return this; + } + + /** + * Performs an in-place union with another composite shape. + * + *

This shape's SolidPolygon children are replaced with the union result. + * Non-SolidPolygon children from both shapes are preserved and combined.

+ * + *

CSG Operation: Union combines two shapes into one, keeping all + * geometry from both. Uses BSP tree algorithms for robust boolean operations.

+ * + *

Child handling:

+ *
    + *
  • SolidPolygon children from both shapes → replaced with union result
  • + *
  • Non-SolidPolygon children from this shape → preserved
  • + *
  • Non-SolidPolygon children from other shape → added to this shape
  • + *
  • Nested AbstractCompositeShape children → preserved unchanged (not recursively processed)
  • + *
+ * + * @param other the shape to union with + * @see #subtract(AbstractCompositeShape) + * @see #intersect(AbstractCompositeShape) + */ + public void union(final AbstractCompositeShape other) { + replaceSolidPolygons(Csg.union(extractSolidPolygons(), + other.extractSolidPolygons())); + mergeNonPolygonChildrenFrom(other); + } + + /** + * Performs an in-place subtraction with another composite shape. + * + *

This shape's SolidPolygon children are replaced with the difference result. + * The other shape acts as a "cutter" that carves out volume from this shape.

+ * + *

CSG Operation: Subtract removes the volume of the second shape + * from the first shape. Useful for creating holes, cavities, and cutouts.

+ * + *

Child handling:

+ *
    + *
  • SolidPolygon children from this shape → replaced with difference result
  • + *
  • Non-SolidPolygon children from this shape → preserved
  • + *
  • All children from other shape → discarded (other is just a cutter)
  • + *
  • Nested AbstractCompositeShape children → preserved unchanged
  • + *
+ * + * @param other the shape to subtract (the cutter) + * @see #union(AbstractCompositeShape) + * @see #intersect(AbstractCompositeShape) + */ + public void subtract(final AbstractCompositeShape other) { + replaceSolidPolygons(Csg.subtract(extractSolidPolygons(), + other.extractSolidPolygons())); + } + + /** + * Performs an in-place intersection with another composite shape. + * + *

This shape's SolidPolygon children are replaced with the intersection result. + * Only the overlapping volume between the two shapes remains.

+ * + *

CSG Operation: Intersect keeps only the volume where both shapes + * overlap. Useful for creating shapes constrained by multiple boundaries.

+ * + *

Child handling:

+ *
    + *
  • SolidPolygon children from this shape → replaced with intersection result
  • + *
  • Non-SolidPolygon children from this shape → preserved
  • + *
  • All children from other shape → discarded
  • + *
  • Nested AbstractCompositeShape children → preserved unchanged
  • + *
+ * + * @param other the shape to intersect with + * @see #union(AbstractCompositeShape) + * @see #subtract(AbstractCompositeShape) + */ + public void intersect(final AbstractCompositeShape other) { + replaceSolidPolygons(Csg.intersect(extractSolidPolygons(), + other.extractSolidPolygons())); + } + + /** + * Replaces this shape's SolidPolygon children with new polygons. + * + *

Preserves all non-SolidPolygon children (Lines, nested composites, etc.).

+ * + * @param newPolygons the polygons to replace with + */ + private void replaceSolidPolygons(final List newPolygons) { + // Remove all direct SolidPolygon children from this shape + final Iterator iterator = subShapesRegistry.iterator(); + while (iterator.hasNext()) { + final SubShape subShape = iterator.next(); + if (subShape.getShape() instanceof SolidPolygon) { + iterator.remove(); + } + } + + // Add all result polygons as new children + for (final SolidPolygon polygon : newPolygons) { + addShape(polygon); + } + + cacheNeedsRebuild = true; + } + + /** + * Merges non-SolidPolygon children from another shape into this shape. + * + *

Copies all non-SolidPolygon children (Lines, nested composites, etc.) + * from the other shape, preserving their group identifiers.

+ * + * @param other the shape to merge non-polygon children from + */ + private void mergeNonPolygonChildrenFrom(final AbstractCompositeShape other) { + if (other == null) { + return; + } + + for (final SubShape otherSubShape : other.subShapesRegistry) { + final AbstractShape otherShape = otherSubShape.getShape(); + if (!(otherShape instanceof SolidPolygon)) { + addShape(otherShape, otherSubShape.getGroupIdentifier()); + } + } + + cacheNeedsRebuild = true; + } + + /** + * Makes all sub-shapes belonging to the specified group visible. + * + * @param groupIdentifier the group to show + * @see #hideGroup(String) + */ + public void showGroup(final String groupIdentifier) { + for (int i = 0; i < subShapesRegistry.size(); i++) { + final SubShape subShape = subShapesRegistry.get(i); + if (subShape.matchesGroup(groupIdentifier)) { + subShape.setVisible(true); + cacheNeedsRebuild = true; + } + } + } + + /** + * Rebuilds the cached render list from the shape registry: + * textured triangles pass through as-is (perspective-correct scanline + * rendering needs no tessellation), N-vertex solid polygons are + * fan-triangulated, everything else passes through. + * Logs the operation to the debug log buffer if available. + * + * @param context the rendering context for logging, may be {@code null} + */ + private void rebuildRenderList(final RenderingContext context) { + cacheNeedsRebuild = false; + + final List result = new ArrayList<>(); + int texturedPolygonCount = 0; + int solidPolygonCount = 0; + int triangulatedPolygonCount = 0; + int otherShapeCount = 0; + + for (int i = 0; i < subShapesRegistry.size(); i++) { + final SubShape subShape = subShapesRegistry.get(i); + if (!subShape.isVisible()) + continue; + + final AbstractShape shape = subShape.getShape(); + + if (shape instanceof TexturedTriangle) { + result.add(shape); + texturedPolygonCount++; + } else if (shape instanceof SolidPolygon polygon) { + final int vertexCount = polygon.getVertexCount(); + + if (vertexCount == 3) { + result.add(polygon); + solidPolygonCount++; + } else { + triangulateSolidPolygon(polygon, result); + triangulatedPolygonCount++; + } + } else { + result.add(shape); + otherShapeCount++; + } + } + + cachedRenderList = postprocessRenderList(result); + renderListVersion++; + globalRenderListVersion.incrementAndGet(); + + if (context != null && context.debugLogBuffer != null) { + context.debugLogBuffer.log("rebuildRenderList: " + getClass().getSimpleName() + + " texturedPolygons=" + texturedPolygonCount + + " solidPolygons=" + solidPolygonCount + + " triangulatedPolygons=" + triangulatedPolygonCount + + " otherShapes=" + otherShapeCount); + } + } + + /** + * Returns the global render list version: incremented every time ANY + * composite's render list is rebuilt. Used by derived structures + * (BSP trees, GI scene snapshots) to detect that they must rebuild. + * + * @return monotonically increasing global version + */ + public static int getGlobalRenderListVersion() { + return globalRenderListVersion.get(); + } + + /** + * Collects the triangles of this composite's current render list, + * recursing into nested composites. These are the exact objects that + * get transformed and rendered: triangulated render-list polygons, or + * lightmapped wrappers for + * {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.LightmappedCompositeShape}. + * + *

Render lists are built lazily during transform; composites that + * have not been transformed yet (or are frustum-culled) contribute + * nothing. Vertices are in each composite's local space — callers + * combining several composites should require identity transforms.

+ * + * @param out list receiving the triangles + */ + public void collectRenderTriangles(final List out) { + if (cachedRenderList == null) + return; + for (final AbstractShape shape : cachedRenderList) { + if (shape instanceof SolidPolygon + || shape instanceof eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle) + out.add((AbstractCoordinateShape) shape); + else if (shape instanceof AbstractCompositeShape) + ((AbstractCompositeShape) shape).collectRenderTriangles(out); + } + } + + /** + * Hook: post-processes the freshly rebuilt render list before it becomes + * the rendering cache. The default implementation returns the list + * unchanged. Subclasses may replace the list — e.g. + * {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.LightmappedCompositeShape} + * wraps its polygons into lightmapped triangles here. + * + * @param renderList the render list built from the shape registry + * @return the render list to cache and render + */ + protected List postprocessRenderList(final List renderList) { + return renderList; + } + + /** + * Triangulates a convex solid polygon using fan triangulation. + * + *

Fan triangulation creates N-2 triangles from an N-vertex polygon by using + * vertex 0 as the anchor and connecting it to each adjacent pair of vertices.

+ * + *

Properties (color, shading, backface culling, mouse interaction) are + * propagated to each resulting triangle to ensure consistent behavior.

+ * + * @param polygon the polygon to triangulate (must have at least 4 vertices) + * @param result the list to add the resulting triangles to + */ + private void triangulateSolidPolygon(final SolidPolygon polygon, + final List result) { + + final Color color = polygon.getColor(); + final boolean shadingEnabled = polygon.isShadingEnabled(); + final boolean backfaceCulling = polygon.isBackfaceCullingEnabled(); + final MouseInteractionController mouseController = polygon.mouseInteractionController; + + final List vertices = polygon.vertices; + final Vertex v0 = vertices.get(0); + + for (int i = 1; i < vertices.size() - 1; i++) { + final Vertex v1 = vertices.get(i); + final Vertex v2 = vertices.get(i + 1); + + final SolidPolygon triangle = new SolidPolygon( + v0.coordinate, v1.coordinate, v2.coordinate, color); + + triangle.setShadingEnabled(shadingEnabled); + triangle.setBackfaceCulling(backfaceCulling); + triangle.setMouseInteractionController(mouseController); + + result.add(triangle); + } + } + + @Override + public void transform(final TransformStack transformPipe, + final RenderAggregator aggregator, final RenderingContext context) { + + // Add the current composite shape transform to the end of the transform + // pipeline. + transformPipe.addTransform(transform); + + // FRUSTUM CULLING: Check if this composite's bounds are visible + // Root composite skips this check (its bounds are always the full scene) + // Non-root composites check their aggregated bounds against the frustum + if (context.frustum != null && !isRootComposite) { + // Count this composite for culling statistics (before frustum test) + if (context.cullingStatistics != null) { + context.cullingStatistics.totalComposites.incrementAndGet(); + } + + final Box localBounds = getBoundingBox(); + + // Transform all 8 corners of the bounding box to view space + final double minX = localBounds.getMinX(); + final double maxX = localBounds.getMaxX(); + final double minY = localBounds.getMinY(); + final double maxY = localBounds.getMaxY(); + final double minZ = localBounds.getMinZ(); + final double maxZ = localBounds.getMaxZ(); + + final double[] xs = {minX, maxX}; + final double[] ys = {minY, maxY}; + final double[] zs = {minZ, maxZ}; + + double viewMinX = Double.MAX_VALUE; + double viewMaxX = -Double.MAX_VALUE; + double viewMinY = Double.MAX_VALUE; + double viewMaxY = -Double.MAX_VALUE; + double viewMinZ = Double.MAX_VALUE; + double viewMaxZ = -Double.MAX_VALUE; + + for (int i = 0; i < 8; i++) { + final double x = xs[(i & 1)]; + final double y = ys[(i >> 1) & 1]; + final double z = zs[(i >> 2) & 1]; + + final Point3D corner = transformPointToViewSpace(x, y, z, transformPipe); + + viewMinX = Math.min(viewMinX, corner.x); + viewMaxX = Math.max(viewMaxX, corner.x); + viewMinY = Math.min(viewMinY, corner.y); + viewMaxY = Math.max(viewMaxY, corner.y); + viewMinZ = Math.min(viewMinZ, corner.z); + viewMaxZ = Math.max(viewMaxZ, corner.z); + } + + final Box viewSpaceBounds = new Box( + new Point3D(viewMinX, viewMinY, viewMinZ), + new Point3D(viewMaxX, viewMaxY, viewMaxZ) + ); + + final Frustum frustum = context.frustum; + final boolean visible = frustum.intersectsAABB(viewSpaceBounds); + + if (!visible) { + // Entire composite outside frustum - skip processing all children + if (context.cullingStatistics != null) { + context.cullingStatistics.culledComposites.incrementAndGet(); + } + transformPipe.dropTransform(); + return; + } + } + + viewSpaceTracker.analyze(transformPipe, context); + + beforeTransformHook(transformPipe, context); + + rebuildRenderListIfNeeded(context); + + // transform rendered subshapes + if (shouldForkTransform(context)) { + transformChildrenParallel(transformPipe, aggregator, context); + } else { + transformChildrenSerial(transformPipe, aggregator, context); + } + + transformPipe.dropTransform(); + } + + /** + * Minimum number of children before the parallel fork is considered at + * all. A single child cannot be split; splitting happens in the child. + */ + private static final int PARALLEL_TRANSFORM_MIN_SHAPES = 2; + + /** + * Minimum total subtree weight (leaf primitives below this composite) + * before forking its transform into parallel chunks. Below this, the + * serial walk is cheaper than the fork overhead. Deliberately above + * one sphere's generated triangle count (~960 at 16 segments): + * measured 2026-09-04, forking those pays task overhead per chunk for + * negligible serial work (35k chunk tasks on a 3000-sphere scene were + * SLOWER than serial). + */ + private static final int PARALLEL_TRANSFORM_MIN_SUBTREE_WEIGHT = 2048; + + /** + * Minimum weight per parallel chunk task. Keeps chunk granularity + * coarse enough that task dispatch overhead stays negligible. + */ + private static final int PARALLEL_TASK_MIN_WEIGHT = 512; + + /** + * Cached subtree weight from {@link #getTransformWeight}, valid for + * {@link #subtreeWeightCycle} only. + */ + private int cachedSubtreeWeight; + + /** + * Transform cycle id the cached subtree weight was computed on. + * Keyed on the globally unique cycle id, not the per-context frame + * number, so alternating between rendering contexts cannot produce + * stale cache hits. + */ + private long subtreeWeightCycle = -1; + + /** + * Transform cycle id the cached subtree weight was last RECOMPUTED on. + * Separate from {@link #subtreeWeightCycle} (last read): the refresh + * gate measures the age of the computation, not of the last access. + */ + private long subtreeWeightComputeCycle = -1; + + /** + * {@link #renderListVersion} at the last weight recomputation. + */ + private int weightListVersion = -1; + + /** + * Bumped whenever ANY composite rebuilds its render list. Lets the + * weight shortcut react to structural changes anywhere in the tree + * within one cycle, at O(1) per node per cycle — scanning direct + * children's versions instead costs O(leaves) per frame because leaf + * lists dominate (measured 2026-09-04: +3-4 ms/frame on a 400-sphere + * scene). + */ + private static final AtomicInteger globalRenderListVersion = new AtomicInteger(); + + /** + * {@link #globalRenderListVersion} value seen at the last weight + * recomputation. + */ + private int weightGlobalVersion = -1; + + /** + * Bumped every time {@link #cachedRenderList} is rebuilt. Gates weight + * recomputation: while the list is unchanged, the cached weight is + * reused without re-walking the subtree. + */ + private int renderListVersion; + + /** + * Full subtree weight re-walks are O(total leaves below this node), + * which costs real milliseconds on big meshes. With an unchanged render + * list the cached weight is refreshed at most every this many cycles; + * structural changes (rebuilds) recompute immediately. + */ + private static final long WEIGHT_REFRESH_CYCLES = 16; + + /** + * Total transform weight of this composite: the sum of its children's + * weights, i.e. roughly the number of leaf primitives below it. + * Computed lazily; recomputed only when this node's render list was + * rebuilt or the cache is older than {@link #WEIGHT_REFRESH_CYCLES} + * cycles (children's internal rebuilds are picked up by the periodic + * refresh). Used solely for parallel fork load balancing, never for + * correctness, so brief staleness is harmless. + * + *

Thread safety: a composite's fork decision runs on exactly one + * thread per cycle. Chunk-thread reads are cycle-stamped cache hits + * published through the executor's happens-before edge.

+ * + * @param renderingContext the rendering context (cycle identity) + * @return subtree transform weight, at least 1 + */ + @Override + public int getTransformWeight(final RenderingContext renderingContext) { + final long cycle = renderingContext.transformCycleId; + if (subtreeWeightCycle == cycle) { + return cachedSubtreeWeight; + } + final int globalVersion = globalRenderListVersion.get(); + if (weightListVersion == renderListVersion + && weightGlobalVersion == globalVersion + && cycle - subtreeWeightComputeCycle < WEIGHT_REFRESH_CYCLES) { + // Nothing rebuilt anywhere and computation fresh: keep the + // value, just re-stamp the read. O(1) per node per cycle. + subtreeWeightCycle = cycle; + return cachedSubtreeWeight; + } + int weight = 0; + for (final AbstractShape child : cachedRenderList) { + weight += child.getTransformWeight(renderingContext); + } + cachedSubtreeWeight = Math.max(1, weight); + subtreeWeightCycle = cycle; + subtreeWeightComputeCycle = cycle; + weightListVersion = renderListVersion; + weightGlobalVersion = globalVersion; + return cachedSubtreeWeight; + } + + /** + * Decides whether this composite forks its children's transform into + * parallel chunks: enough children to split, and enough TOTAL weight + * below it to amortize the fork overhead. Weight (not local child + * count) is what matters: a deep narrow tree with two heavy children + * forks just like a flat mesh with thousands of leaves. + * + * @param context the rendering context (provides the coordinator) + * @return true when the parallel fork should be taken + */ + private boolean shouldForkTransform(final RenderingContext context) { + if (context.transformCoordinator == null) { + return false; + } + if (cachedRenderList.size() < PARALLEL_TRANSFORM_MIN_SHAPES) { + return false; + } + return getTransformWeight(context) >= PARALLEL_TRANSFORM_MIN_SUBTREE_WEIGHT; + } + + /** + * Target number of task chunks per available processor core. + * More tasks than cores gives the pool load balancing across + * shapes with uneven transform cost. + */ + private static final int PARALLEL_TASKS_PER_CORE = 4; + + /** + * Transforms all children serially on the calling thread. + * + * @param transformPipe the transform stack (includes this composite's transform) + * @param aggregator the aggregator to queue visible shapes into + * @param context the rendering context + */ + private void transformChildrenSerial(final TransformStack transformPipe, + final RenderAggregator aggregator, + final RenderingContext context) { + for (final AbstractShape shape : cachedRenderList) { + shape.transform(transformPipe, aggregator, context); + } + } + + /** + * Forks the children's transform into parallel chunk tasks on the + * frame's {@link ParallelTransformCoordinator} and returns immediately + * WITHOUT waiting for them. + * + *

Works at any nesting level: a heavy composite reached inside a + * chunk task forks its own children into the same coordinator. This is + * deadlock-safe because chunk tasks never block on other tasks; only + * the orchestrating render thread waits (in the coordinator's drain).

+ * + *

Stack snapshotting: this composite's transform is dropped from + * {@code transformPipe} right after this method returns, long before + * the chunk tasks run, so the pipe is copied HERE on the forking + * thread. Each task then copies the snapshot for its own working stack. + * The snapshot is never mutated after publication, so concurrent + * copying by chunk tasks is safe.

+ * + *

Thread-safety notes: sibling composites are exclusively owned by + * one chunk, so their per-instance caches (render list, bounding + * boxes, own transform's cached matrix) never race. The vertex + * frameNumber cache is a benign race: every thread writes the same + * value.

+ * + * @param transformPipe the transform stack (includes this composite's transform) + * @param aggregator the caller's aggregator, used ONLY when the fork + * bails out (too few chunks, or the frame task + * budget is exhausted) and the children fall back + * to a serial inline transform. The parallel fork + * itself queues into per-task aggregators merged + * by the coordinator's drain. + * @param context the rendering context (provides the coordinator) + */ + private void transformChildrenParallel(final TransformStack transformPipe, + final RenderAggregator aggregator, + final RenderingContext context) { + // Snapshot the render list reference. With the pipelined render + // loop, the NEXT pass's tree walk can already be running while + // this pass's chunk tasks are still queued (the drain happens in + // the async continuation, not before the next walk). That walk + // may rebuild this composite's render list, REASSIGNING + // cachedRenderList to a new list of a different size. The chunk + // ranges below are computed against this list instance, so the + // chunk tasks must index this same instance — re-reading the + // field inside the lambda raced with the rebuild and threw + // IndexOutOfBoundsException. (The old list stays alive and valid + // for this pass; each pass transforms into its own vertex slot.) + final List renderList = cachedRenderList; + final int size = renderList.size(); + final int totalWeight = getTransformWeight(context); + final int processors = Runtime.getRuntime().availableProcessors(); + final int targetTasks = processors * PARALLEL_TASKS_PER_CORE; + final int taskWeight = Math.max(PARALLEL_TASK_MIN_WEIGHT, + (totalWeight + targetTasks - 1) / targetTasks); + + // Pass 1: count chunks, cutting by ACCUMULATED WEIGHT so that a + // node with few but heavy children (e.g. two 25k-triangle halves + // of a fractal) still splits into multiple tasks + int taskCount = 0; + int accumulated = 0; + for (int i = 0; i < size; i++) { + accumulated += renderList.get(i).getTransformWeight(context); + if (accumulated >= taskWeight) { + taskCount++; + accumulated = 0; + } + } + if (accumulated > 0) { + taskCount++; + } + + if (taskCount < 2) { + transformChildrenSerial(transformPipe, aggregator, context); + return; + } + + final ParallelTransformCoordinator coordinator = context.transformCoordinator; + if (!coordinator.tryReserveTasks(taskCount)) { + // Frame-wide task budget exhausted: transform inline + transformChildrenSerial(transformPipe, aggregator, context); + return; + } + + final TransformStack snapshot = new TransformStack(transformPipe); + + // Pass 2: submit chunks (weights are frame-cached, cheap re-walk) + int from = 0; + accumulated = 0; + for (int i = 0; i < size; i++) { + accumulated += renderList.get(i).getTransformWeight(context); + if (accumulated >= taskWeight || i == size - 1) { + final int chunkFrom = from; + final int chunkTo = i + 1; + coordinator.submit(() -> { + // Pooled scratch: the stack (9.6 KB of arrays) and + // the chunk aggregator (queue keeps its capacity + // across frames) come from the coordinator's static + // pools — previously each chunk allocated both on + // every frame. + final TransformStack taskStack = + coordinator.borrowStack(snapshot); + try { + final RenderAggregator taskAggregator = + coordinator.borrowAggregator(); + for (int c = chunkFrom; c < chunkTo; c++) { + renderList.get(c).transform(taskStack, taskAggregator, context); + } + return taskAggregator; + } finally { + coordinator.returnStack(taskStack); + } + }); + from = i + 1; + accumulated = 0; + } + } + } + + /** + * Transforms a point to view space using the current transform stack. + * Helper method for frustum culling that transforms bounding box corners. + * + * @param x the X coordinate in local space + * @param y the Y coordinate in local space + * @param z the Z coordinate in local space + * @param transformPipe the current transform stack + * @return the transformed point in view space + */ + private Point3D transformPointToViewSpace(final double x, final double y, final double z, + final TransformStack transformPipe) { + final Point3D input = new Point3D(x, y, z); + final Point3D result = new Point3D(); + transformPipe.transform(input, result); + return result; + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/BspTree.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/BspTree.java new file mode 100644 index 0000000..6b92eca --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/BspTree.java @@ -0,0 +1,230 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base; + +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; + +import java.util.ArrayList; +import java.util.List; + +/** + * A Binary Space Partitioning (BSP) tree for CSG operations. + * + *

BSP trees are the data structure that makes CSG boolean operations possible. + * Each node divides 3D space into two half-spaces using a plane, enabling + * efficient spatial queries and polygon clipping.

+ * + *

BSP Tree Structure:

+ *
+ *                 [Node: plane P]
+ *                /               \
+ *        [Front subtree]     [Back subtree]
+ *     (same side as P's     (opposite side
+ *        normal)             of P's normal)
+ * 
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape + * @see Plane the plane type used for spatial partitioning + * @see SolidPolygon the polygon type stored in BSP nodes + */ +public class BspTree { + + /** + * Polygons that lie on this node's partitioning plane. + */ + public final List polygons = new ArrayList<>(); + + /** + * The partitioning plane for this node. + */ + public Plane plane; + + /** + * The front child subtree. + */ + public BspTree front; + + /** + * The back child subtree. + */ + public BspTree back; + + /** + * Creates an empty BSP tree with no plane or children. + */ + public BspTree() { + } + + /** + * Creates a BSP tree from a list of polygons. + * + * @param polygons the polygons to partition into a BSP tree + */ + public BspTree(final List polygons) { + addPolygons(polygons); + } + + /** + * Creates a deep clone of this BSP tree. + * + * @return a new BspTree with cloned data + */ + public BspTree clone() { + final BspTree tree = new BspTree(); + + tree.plane = plane != null ? plane.clone() : null; + tree.front = front != null ? front.clone() : null; + tree.back = back != null ? back.clone() : null; + + for (final SolidPolygon p : polygons) { + tree.polygons.add(p.deepClone()); + } + + return tree; + } + + /** + * Inverts this BSP tree, converting "inside" to "outside" and vice versa. + */ + public void invert() { + for (final SolidPolygon polygon : polygons) polygon.flip(); + + if (plane != null) plane.flip(); + if (front != null) front.invert(); + if (back != null) back.invert(); + + final BspTree temp = front; + front = back; + back = temp; + } + + /** + * Clips a list of polygons against this BSP tree, returning only the + * portions that lie outside the solid represented by this tree. + * + *

This is a core CSG operation used for boolean subtraction and + * intersection. The method recursively traverses the BSP tree, splitting + * polygons at each partitioning plane and discarding interior fragments.

+ * + *

Algorithm:

+ *
    + *
  1. At each node, split polygons by the partitioning plane
  2. + *
  3. Recursively clip front fragments against the front subtree
  4. + *
  5. Recursively clip back fragments against the back subtree
  6. + *
  7. Combine and return all surviving fragments
  8. + *
+ * + *

Leaf nodes: If this node has no plane (leaf node), all polygons + * are considered outside and returned unchanged.

+ * + * @param polygons the polygons to clip against this BSP tree + * @return a new list containing only the portions outside this solid + */ + public List clipPolygons(final List polygons) { + // Leaf node: no partitioning plane means all polygons are outside + if (plane == null) { + return new ArrayList<>(polygons); + } + + // Split polygons by this node's partitioning plane + final List frontList = new ArrayList<>(); + final List backList = new ArrayList<>(); + + for (final SolidPolygon polygon : polygons) + // Split by plane: coplanar polygons are classified by their normal direction + // (same-facing normal → frontList, opposite-facing normal → backList) + plane.splitPolygon(polygon, frontList, backList, frontList, backList); + + // Recursively clip front fragments against front subtree + List resultFront = frontList; + if (front != null) resultFront = front.clipPolygons(frontList); + + // Recursively clip back fragments against back subtree + List resultBack; + if (back != null) resultBack = back.clipPolygons(backList); + else resultBack = new ArrayList<>(); + + // Combine surviving fragments from both subtrees + final List result = new ArrayList<>(resultFront.size() + resultBack.size()); + result.addAll(resultFront); + result.addAll(resultBack); + return result; + } + + /** + * Clips this BSP tree against another BSP tree. + * + * @param bsp the BSP tree to clip against + */ + public void clipTo(final BspTree bsp) { + final List newPolygons = bsp.clipPolygons(polygons); + polygons.clear(); + polygons.addAll(newPolygons); + + if (front != null) front.clipTo(bsp); + if (back != null) back.clipTo(bsp); + } + + /** + * Collects all polygons from this BSP tree into a flat list. + * + * @return a new list containing all polygons in this tree + */ + public List allPolygons() { + final List result = new ArrayList<>(polygons); + + if (front != null) result.addAll(front.allPolygons()); + if (back != null) result.addAll(back.allPolygons()); + + return result; + } + + /** + * Adds polygons to this BSP tree, partitioning space recursively. + * + *

This method is the core BSP tree construction algorithm. It builds or + * extends the tree by choosing a partition plane and classifying each polygon:

+ * + *
    + *
  • Coplanar — polygons on the partition plane are stored in this node
  • + *
  • Front — polygons in the front half-space (same side as plane normal) + * go to the front child subtree
  • + *
  • Back — polygons in the back half-space (opposite to plane normal) + * go to the back child subtree
  • + *
  • Spanning — polygons crossing the plane are split into front and back + * fragments, each going to its respective subtree
  • + *
+ * + *

For an empty tree, the first polygon's plane becomes the partition plane. + * Child nodes are created lazily when polygons need to be stored in them.

+ * + *

Can be called multiple times to incrementally extend an existing tree, + * though the original partition planes remain unchanged.

+ * + * @param polygons the polygons to insert into this BSP tree + * @see Plane#splitPolygon the method that classifies and splits individual polygons + */ + public void addPolygons(final List polygons) { + if (polygons.isEmpty()) return; + + if (plane == null) plane = polygons.get(0).getPlane().clone(); + + final List frontList = new ArrayList<>(); + final List backList = new ArrayList<>(); + + for (final SolidPolygon polygon : polygons) + plane.splitPolygon(polygon, this.polygons, this.polygons, frontList, backList); + + if (!frontList.isEmpty()) { + if (front == null) front = new BspTree(); + front.addPolygons(frontList); + } + + if (!backList.isEmpty()) { + if (back == null) back = new BspTree(); + back.addPolygons(backList); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Csg.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Csg.java new file mode 100644 index 0000000..bf0eb5b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Csg.java @@ -0,0 +1,173 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base; + +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; + +import java.util.ArrayList; +import java.util.List; + +/** + * Pure CSG (Constructive Solid Geometry) boolean engine: union, subtract + * and intersect over lists of {@link SolidPolygon}s, built on BSP tree + * clip/invert sequences. + * + *

These are pure functions — they take polygon lists and return new + * polygon lists, touching no shape state. {@link AbstractCompositeShape}'s + * instance methods ({@code union/subtract/intersect}) delegate here and + * handle the child-registry bookkeeping themselves.

+ * + *

All operations clone their inputs first (BSP operations mutate + * polygons in place), so caller lists are never modified.

+ */ +public final class Csg { + + private Csg() { + // utility class + } + + /** + * Union of two polygon sets: every surface of both, interior faces + * removed. + * + * @param a first operand's polygons (not modified) + * @param b second operand's polygons (not modified) + * @return the union result polygons + */ + public static List union(final List a, + final List b) { + // Degenerate operands: the BSP sequence handles these, but the + // short-circuit is explicit (and skips the tree builds). + if (a.isEmpty()) return clonePolygons(b); + if (b.isEmpty()) return clonePolygons(a); + + final BspTree selfTree = new BspTree(clonePolygons(a)); + final BspTree otherTree = new BspTree(clonePolygons(b)); + + // Remove from self any polygons that are inside other (interior faces) + selfTree.clipTo(otherTree); + + // Remove from other any polygons that are inside self (interior faces) + otherTree.clipTo(selfTree); + + // Invert other to convert remaining polygons for the next clip step + otherTree.invert(); + + // Clip inverted other against self to remove back-facing coplanar polygons + otherTree.clipTo(selfTree); + + // Invert back to restore correct polygon orientation + otherTree.invert(); + + // Merge other's remaining polygons into self's BSP tree + selfTree.addPolygons(otherTree.allPolygons()); + + return selfTree.allPolygons(); + } + + /** + * Subtraction {@code a - b}: the cutter volume carved out of the + * target. + * + * @param a target polygons (not modified) + * @param b cutter polygons (not modified) + * @return the difference result polygons + */ + public static List subtract(final List a, + final List b) { + if (a.isEmpty() || b.isEmpty()) return clonePolygons(a); + + final BspTree target = new BspTree(clonePolygons(a)); + final BspTree cutter = new BspTree(clonePolygons(b)); + + // Invert target: convert "inside" to "outside" and vice versa + // This transforms the problem from "subtract B from A" to "intersect A's complement with B's complement" + target.invert(); + + // Clip target against cutter: removes parts of target that are INSIDE the cutter + // Since target is inverted, this removes parts that were OUTSIDE the original target + target.clipTo(cutter); + + // Clip cutter against (inverted) target: removes parts of cutter outside the inverted target + // This keeps only cutter polygons that are inside the inverted target = outside original target + cutter.clipTo(target); + + // Invert cutter to flip its inside/outside + cutter.invert(); + + // Clip inverted cutter against target: removes coplanar back-faces + cutter.clipTo(target); + + // Invert cutter back to correct orientation + cutter.invert(); + + // Merge cutter's polygons into target's BSP tree + target.addPolygons(cutter.allPolygons()); + + // Invert target back to restore correct inside/outside orientation + // Result: the carved-out volume (target minus cutter) + target.invert(); + + return target.allPolygons(); + } + + /** + * Intersection of two polygon sets: only the overlapping volume + * remains. + * + * @param a first operand's polygons (not modified) + * @param b second operand's polygons (not modified) + * @return the intersection result polygons + */ + public static List intersect(final List a, + final List b) { + // Degenerate operand: the classic BSP sequence returns A here + // (an empty tree classifies nothing as inside, so every clip is + // a no-op and the inverts cancel out) — but the intersection + // with an empty volume IS empty. Guard explicitly. + if (a.isEmpty() || b.isEmpty()) return List.of(); + + final BspTree selfTree = new BspTree(clonePolygons(a)); + final BspTree otherTree = new BspTree(clonePolygons(b)); + + // Invert self to convert "inside" to "outside" + // This transforms intersection into: keep parts that are "outside both inverted shapes" + selfTree.invert(); + + // Clip other against inverted self: keeps only parts of other that are INSIDE original self + // (because clipTo removes what's "outside" the BSP, and inverted self's "outside" = original self's "inside") + otherTree.clipTo(selfTree); + + // Invert other (which now represents the intersection region) + otherTree.invert(); + + // Clip inverted self against (inverted intersection): removes parts outside the intersection + selfTree.clipTo(otherTree); + + // Clip intersection result against inverted self: removes back-facing coplanar polygons + otherTree.clipTo(selfTree); + + // Build final BSP tree from the clipped intersection polygons + selfTree.addPolygons(otherTree.allPolygons()); + + // Invert back to restore correct inside/outside orientation + selfTree.invert(); + + return selfTree.allPolygons(); + } + + /** + * Deep clones of all polygons in the list: CSG operations modify + * polygons in-place via BSP tree operations, cloning preserves the + * originals. + */ + private static List clonePolygons(final List polygons) { + final List cloned = new ArrayList<>(polygons.size()); + for (final SolidPolygon p : polygons) { + cloned.add(p.deepClone()); + } + return cloned; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.java new file mode 100644 index 0000000..d405b42 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/Plane.java @@ -0,0 +1,228 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; + +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an infinite plane in 3D space using the Hesse normal form. + * + *

Planes are fundamental to BSP (Binary Space Partitioning) tree operations + * in CSG. They divide 3D space into two half-spaces.

+ * + * @see SolidPolygon polygons that reference their containing plane + * @see BspTree BSP trees that use planes for spatial partitioning + */ +public class Plane { + + /** + * Epsilon value used for floating-point comparisons in BSP operations. + * Smaller values provide higher precision but may cause issues with + * near-coplanar polygons. 1e-5 is a good balance for most 3D geometry. + */ + public static final double EPSILON = 1e-12; + + /** + * The unit normal vector perpendicular to the plane surface. + */ + public Point3D normal; + + /** + * The signed distance from the origin to the plane along the normal. + */ + public double distance; + + /** + * Creates a plane with the given normal and distance. + * + * @param normal the unit normal vector + * @param distance the signed distance from origin to the plane + */ + public Plane(final Point3D normal, final double distance) { + this.normal = normal; + this.distance = distance; + } + + /** + * Computes the unit normal vector for a triangle defined by three points. + * + *

Zero-allocation method: fills the result point instead of creating a new one. + * This is the shared implementation used by both {@link #fromPoints} and + * {@link SolidPolygon} for shading calculations.

+ * + *

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

+ * + * @param a first point (base point for edge vectors) + * @param b second point + * @param c third point + * @param result Point3D to receive the unit normal vector (modified in place) + * @return true if normal computed successfully, false if points are collinear + * (cross product magnitude less than EPSILON) + */ + public static boolean computeNormal(final Point3D a, final Point3D b, + final Point3D c, final Point3D result) { + // Edge vectors from a to b and a to c + final double ax = b.x - a.x; + final double ay = b.y - a.y; + final double az = b.z - a.z; + + final double bx = c.x - a.x; + final double by = c.y - a.y; + final double bz = c.z - a.z; + + // Cross product: (edge1 × edge2) + double nx = ay * bz - az * by; + double ny = az * bx - ax * bz; + double nz = ax * by - ay * bx; + + // Normalize + final double length = Math.sqrt(nx * nx + ny * ny + nz * nz); + if (length < EPSILON) { + result.x = result.y = result.z = 0; + return false; + } + + result.x = nx / length; + result.y = ny / length; + result.z = nz / length; + return true; + } + + /** + * Creates a plane from three non-collinear points. + * + *

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

+ * + * @param a the first point on the plane + * @param b the second point on the plane + * @param c the third point on the plane + * @return a new Plane passing through the three points + * @throws ArithmeticException if the points are collinear (cannot define a plane) + */ + public static Plane fromPoints(final Point3D a, final Point3D b, final Point3D c) { + final Point3D n = new Point3D(); + if (!computeNormal(a, b, c, n)) { + throw new ArithmeticException( + "Cannot create plane from collinear points: cross product is zero"); + } + return new Plane(n, n.dot(a)); + } + + /** + * Creates a deep clone of this plane. + * + * @return a new Plane with the same normal and distance + */ + public Plane clone() { + return new Plane(new Point3D(normal.x, normal.y, normal.z), distance); + } + + /** + * Flips the plane orientation by negating the normal and distance. + */ + public void flip() { + normal = normal.withNegated(); + distance = -distance; + } + + /** + * Splits a polygon by this plane, classifying and potentially dividing it. + * + * @param polygon the polygon to classify and potentially split + * @param coplanarFront list to receive coplanar polygons with same-facing normals + * @param coplanarBack list to receive coplanar polygons with opposite-facing normals + * @param front list to receive polygons in the front half-space + * @param back list to receive polygons in the back half-space + */ + public void splitPolygon(final SolidPolygon polygon, + final List coplanarFront, + final List coplanarBack, + final List front, + final List back) { + + PolygonType polygonType = PolygonType.COPLANAR; + final int vertexCount = polygon.getVertexCount(); + final PolygonType[] types = new PolygonType[vertexCount]; + + for (int i = 0; i < vertexCount; i++) { + final Vertex v = polygon.vertices.get(i); + final double t = normal.dot(v.coordinate) - distance; + final PolygonType type = (t < -EPSILON) ? PolygonType.BACK + : (t > EPSILON) ? PolygonType.FRONT : PolygonType.COPLANAR; + polygonType = polygonType.combine(type); + types[i] = type; + } + + switch (polygonType) { + case COPLANAR: + ((normal.dot(polygon.getPlane().normal) > 0) ? coplanarFront : coplanarBack).add(polygon); + break; + + case FRONT: + front.add(polygon); + break; + + case BACK: + back.add(polygon); + break; + + case SPANNING: + // Split spanning polygon by clipping each edge against the plane. + // Vertices on each side go to their respective lists. + // Edges crossing the plane create intersection vertices added to both lists. + final List frontVertices = new ArrayList<>(); + final List backVertices = new ArrayList<>(); + + for (int i = 0; i < vertexCount; i++) { + final int nextIndex = (i + 1) % vertexCount; + final PolygonType currentType = types[i]; + final PolygonType nextType = types[nextIndex]; + final Vertex currentVertex = polygon.vertices.get(i); + final Vertex nextVertex = polygon.vertices.get(nextIndex); + + // Add current vertex to the polygon on its side of the plane + if (currentType.isFront()) { + frontVertices.add(currentVertex.clone()); + } + if (currentType.isBack()) { + backVertices.add(currentVertex.clone()); + } + + // If edge crosses the plane, create intersection vertex for both polygons + if (currentType != nextType + && currentType != PolygonType.COPLANAR + && nextType != PolygonType.COPLANAR) { + // Calculate interpolation parameter t (0 = current, 1 = next) + // t represents where along the edge the plane intersection occurs + final double t = (distance - normal.dot(currentVertex.coordinate)) + / normal.dot(nextVertex.coordinate.withSubtracted(currentVertex.coordinate)); + + final Vertex intersectionVertex = currentVertex.interpolate(nextVertex, t); + frontVertices.add(intersectionVertex); + backVertices.add(intersectionVertex.clone()); + } + } + + if (frontVertices.size() >= 3) { + final SolidPolygon frontPoly = SolidPolygon.fromVertices( + frontVertices, polygon.getColor(), polygon.isShadingEnabled()); + front.add(frontPoly); + } + if (backVertices.size() >= 3) { + final SolidPolygon backPoly = SolidPolygon.fromVertices( + backVertices, polygon.getColor(), polygon.isShadingEnabled()); + back.add(backPoly); + } + break; + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/PolygonType.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/PolygonType.java new file mode 100644 index 0000000..506c9db --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/PolygonType.java @@ -0,0 +1,56 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base; + +/** + * Classification of a polygon's position relative to a plane. + * Used in BSP tree operations to determine how polygons should be split. + */ +public enum PolygonType { + /** Polygon lies on the plane. */ + COPLANAR, + /** Polygon is entirely in front of the plane. */ + FRONT, + /** Polygon is entirely behind the plane. */ + BACK, + /** Polygon straddles the plane (vertices on both sides). */ + SPANNING; + + /** + * Combines this type with another to compute the aggregate classification. + * When vertices are on both sides of a plane, the result is SPANNING. + * + * @param other the other polygon type to combine with + * @return the combined classification + */ + public PolygonType combine(final PolygonType other) { + if (this == other || other == COPLANAR) { + return this; + } + if (this == COPLANAR) { + return other; + } + // FRONT + BACK = SPANNING + return SPANNING; + } + + /** + * Checks if this type represents a vertex in front of the plane. + * + * @return true if FRONT or COPLANAR (treated as front for classification) + */ + public boolean isFront() { + return this == FRONT || this == COPLANAR; + } + + /** + * Checks if this type represents a vertex behind the plane. + * + * @return true if BACK or COPLANAR (treated as back for classification) + */ + public boolean isBack() { + return this == BACK || this == COPLANAR; + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/SubShape.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/SubShape.java new file mode 100644 index 0000000..19ed0d0 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/SubShape.java @@ -0,0 +1,128 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base; + +import java.util.Objects; + +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape; + +/** + * Wrapper around an {@link AbstractShape} within an {@link AbstractCompositeShape}, + * adding group membership and visibility control. + * + *

Sub-shapes can be organized into named groups so they can be shown, hidden, + * or removed together. This is useful for toggling parts of a composite shape, + * such as showing/hiding labels, highlights, or selection borders.

+ * + * @see AbstractCompositeShape#addShape(AbstractShape, String) + * @see AbstractCompositeShape#hideGroup(String) + * @see AbstractCompositeShape#showGroup(String) + */ +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 with default visibility (visible). + * + * @param shape the shape to wrap + */ + 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; + } + + /** + * Returns {@code true} if this sub-shape has no group assigned. + * + * @return {@code true} if ungrouped + */ + public boolean isUngrouped() { + return groupIdentifier == null; + } + + /** + * Checks whether this sub-shape belongs to the specified group. + * + * @param groupIdentifier the group identifier to match against, or {@code null} to match ungrouped shapes + * @return {@code true} if this sub-shape belongs to the specified group + */ + public boolean matchesGroup(final String groupIdentifier) { + return Objects.equals(this.groupIdentifier, groupIdentifier); + } + + /** + * Returns the group identifier for this sub-shape. + * + * @return the group identifier, or {@code null} if this shape is ungrouped + */ + public String getGroupIdentifier() { + return groupIdentifier; + } + + /** + * Assigns this sub-shape to a group. + * + * @param groupIdentifier the group identifier, or {@code null} to make it ungrouped + */ + public void setGroup(final String groupIdentifier) { + this.groupIdentifier = groupIdentifier; + } + + /** + * Returns the wrapped shape. + * + * @return the underlying shape + */ + public AbstractShape getShape() { + return shape; + } + + /** + * Returns whether this sub-shape is currently visible and will be rendered. + * + * @return {@code true} if visible + */ + public boolean isVisible() { + return visible; + } + + /** + * Sets the visibility of this sub-shape. + * + * @param visible {@code true} to make the shape visible, {@code false} to hide it + */ + public void setVisible(boolean visible) { + this.visible = visible; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/package-info.java new file mode 100644 index 0000000..a35e03c --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/package-info.java @@ -0,0 +1,24 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Base class and utilities for composite shapes. + * + *

{@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape} + * is the foundation for building complex 3D objects by grouping primitives.

+ * + *

Features:

+ *
    + *
  • Position and rotation in 3D space
  • + *
  • Named groups for selective visibility
  • + *
  • Automatic sub-shape management
  • + *
  • Integration with lighting and slicing
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.SubShape + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/package-info.java new file mode 100644 index 0000000..d75f5eb --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/package-info.java @@ -0,0 +1,23 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Composite shapes that group multiple primitives into compound 3D objects. + * + *

Composite shapes allow building complex objects from simpler primitives. + * They support grouping, visibility toggling, and hierarchical transformations.

+ * + *

Subpackages:

+ *
    + *
  • {@code base} - Base class for all composite shapes
  • + *
  • {@code solid} - Solid objects (cubes, spheres, cylinders)
  • + *
  • {@code wireframe} - Wireframe objects (boxes, grids, spheres)
  • + *
  • {@code textcanvas} - 3D text rendering canvas
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java new file mode 100644 index 0000000..072a8b3 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java @@ -0,0 +1,324 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.math.Matrix3x3; +import eu.svjatoslav.aukio.e3d.math.Quaternion; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A 3D arrow shape composed of a cylindrical body and a conical tip. + * + *

The arrow points from a start point to an end point, with the tip + * located at the end point. The arrow's appearance (size, color, transparency) + * can be customized through the constructor parameters.

+ * + *

Usage example:

+ *
{@code
+ * // Create a red arrow pointing from origin to (100, -50, 200)
+ * SolidPolygonArrow arrow = new SolidPolygonArrow(
+ *     new Point3D(0, 0, 0),      // start point
+ *     new Point3D(100, -50, 200), // end point
+ *     8,                         // body radius
+ *     20,                        // tip radius
+ *     40,                        // tip length
+ *     16,                        // segments
+ *     Color.RED                  // color
+ * );
+ * shapeCollection.addShape(arrow);
+ *
+ * // Create a semi-transparent blue arrow
+ * SolidPolygonArrow seeThroughArrow = new SolidPolygonArrow(
+ *     new Point3D(0, 100, 0),
+ *     new Point3D(0, -100, 0),
+ *     10, 25, 50, 12,
+ *     new Color(0, 0, 255, 128)  // blue with 50% transparency
+ * );
+ * }
+ * + * @see SolidPolygonCone + * @see SolidPolygonCylinder + */ +public class SolidPolygonArrow extends AbstractCompositeShape { + + /** + * + * Number of segments for arrow smoothness. + */ + private static final int SEGMENTS = 12; + + /** + * Arrow tip radius as a fraction of body radius (2.5x). + */ + private static final double TIP_RADIUS_FACTOR = 2.5; + + /** + * Arrow tip length as a fraction of body radius (5.0x). + */ + private static final double TIP_LENGTH_FACTOR = 5.0; + + /** + * Constructs a 3D arrow pointing from start to end with sensible defaults. + * + *

This simplified constructor automatically calculates the tip radius as + * 2.5 times the body radius, the tip length as 5 times the body radius, and + * uses 12 segments for smoothness. For custom tip dimensions or segment count, + * use the full constructor.

+ * + * @param startPoint the origin point of the arrow (where the body starts) + * @param endPoint the destination point of the arrow (where the tip points to) + * @param bodyRadius the radius of the cylindrical body; tip dimensions are + * calculated automatically from this value + * @param color the fill color (RGBA; alpha controls transparency) + */ + public SolidPolygonArrow(final Point3D startPoint, final Point3D endPoint, + final double bodyRadius, final Color color) { + super(); + + // Calculate direction and distance + final double dx = endPoint.x - startPoint.x; + final double dy = endPoint.y - startPoint.y; + final double dz = endPoint.z - startPoint.z; + final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + // Handle degenerate case: start and end are the same point + if (distance < 0.001) { + return; + } + + // Normalize direction vector + final double nx = dx / distance; + final double ny = dy / distance; + final double nz = dz / distance; + + // Calculate rotation to align Y-axis with direction + // Default arrow points in -Y direction (apex at lower Y) + // We need to rotate from (0, -1, 0) to (nx, ny, nz) + final Quaternion rotation = createRotationFromYAxis(nx, ny, nz); + final Matrix3x3 rotMatrix = rotation.toMatrix(); + + // Calculate body length (distance minus tip) + final double bodyLength = Math.max(0, distance - bodyRadius * TIP_LENGTH_FACTOR); + + // Build the arrow components + if (bodyLength > 0) { + addCylinderBody(startPoint, bodyRadius, bodyLength, SEGMENTS, color, rotMatrix, nx, ny, nz); + } + addConeTip(endPoint, bodyRadius * TIP_RADIUS_FACTOR, bodyRadius * TIP_LENGTH_FACTOR, SEGMENTS, color, rotMatrix, nx, ny, nz); + + setBackfaceCulling(true); + } + + /** + * Creates a quaternion that rotates from the -Y axis to the given direction. + * + *

The arrow by default points in the -Y direction. This method computes + * the rotation needed to align the arrow with the target direction vector.

+ * + * @param nx normalized direction X component + * @param ny normalized direction Y component + * @param nz normalized direction Z component + * @return quaternion representing the rotation + */ + private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) { + // Default direction is -Y (0, -1, 0) + // Target direction is (nx, ny, nz) + // Dot product: 0*nx + (-1)*ny + 0*nz = -ny + final double dot = -ny; + + // Check for parallel vectors + if (dot > 0.9999) { + // Direction is nearly -Y, no rotation needed + return Quaternion.identity(); + } + if (dot < -0.9999) { + // Direction is nearly +Y, rotate 180° around X axis + return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI); + } + + // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx) + // This gives the rotation axis + final double axisX = -nz; + final double axisY = 0; + final double axisZ = nx; + final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + final double normalizedAxisX = axisX / axisLength; + final double normalizedAxisZ = axisZ / axisLength; + + // Angle from dot product + final double angle = Math.acos(dot); + + return Quaternion.fromAxisAngle( + new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle); + } + + /** + * Adds the cylindrical body of the arrow. + * + *

The cylinder is created with its base at the start point and extends + * in the direction of the arrow for the specified body length.

+ * + *

Local coordinate system: The arrow points in -Y direction in local space. + * Therefore, local -Y is toward the tip (front), and local +Y is toward the start (back).

+ * + * @param startPoint the origin of the arrow body + * @param radius the radius of the cylinder + * @param length the length of the cylinder + * @param segments the number of segments around the circumference + * @param color the fill color + * @param rotMatrix the rotation matrix to apply + * @param dirX direction X component (for translation calculation) + * @param dirY direction Y component + * @param dirZ direction Z component + */ + private void addCylinderBody(final Point3D startPoint, final double radius, + final double length, final int segments, + final Color color, final Matrix3x3 rotMatrix, + final double dirX, final double dirY, final double dirZ) { + // Cylinder center is at startPoint + (length/2) * direction + final double centerX = startPoint.x + (length / 2.0) * dirX; + final double centerY = startPoint.y + (length / 2.0) * dirY; + final double centerZ = startPoint.z + (length / 2.0) * dirZ; + + // Generate ring vertices in local space, then rotate and translate + // Arrow points in -Y direction, so: + // - tipSideRing is at local -Y (toward arrow tip, front of cylinder) + // - startSideRing is at local +Y (toward arrow start, back of cylinder) + final Point3D[] tipSideRing = new Point3D[segments]; + final Point3D[] startSideRing = new Point3D[segments]; + + final double halfLength = length / 2.0; + + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double localX = radius * Math.cos(angle); + final double localZ = radius * Math.sin(angle); + + // Tip-side ring (at -halfLength in local Y = toward arrow tip) + final Point3D tipSideLocal = new Point3D(localX, -halfLength, localZ); + rotMatrix.transform(tipSideLocal, tipSideLocal); + tipSideLocal.x += centerX; + tipSideLocal.y += centerY; + tipSideLocal.z += centerZ; + tipSideRing[i] = tipSideLocal; + + // Start-side ring (at +halfLength in local Y = toward arrow start) + final Point3D startSideLocal = new Point3D(localX, halfLength, localZ); + rotMatrix.transform(startSideLocal, startSideLocal); + startSideLocal.x += centerX; + startSideLocal.y += centerY; + startSideLocal.z += centerZ; + startSideRing[i] = startSideLocal; + } + + // Create cylinder side faces (one quad per segment) + // Winding: tipSide[i] → startSide[i] → startSide[next] → tipSide[next] + // creates CCW winding when viewed from outside the cylinder + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + + addShape(SolidPolygon.quad( + tipSideRing[i], + startSideRing[i], + startSideRing[next], + tipSideRing[next], + color)); + } + + // Add back cap at the start point. + // Single N-vertex polygon that closes the loop to create segments triangles + // (segments+2 vertices → segments triangles via fan triangulation) + // The cap faces backward (away from arrow tip), opposite to arrow direction. + // Winding: center → ring[segments-1] → ... → ring[1] → ring[0] → ring[segments-1] + // (reverse order from ring array direction) + final Point3D[] backCapVertices = new Point3D[segments + 2]; + backCapVertices[0] = startPoint; + for (int i = 0; i < segments; i++) { + backCapVertices[i + 1] = startSideRing[segments - 1 - i]; + } + backCapVertices[segments + 1] = startSideRing[segments - 1]; // close the loop + addShape(new SolidPolygon(backCapVertices, color)); + } + + /** + * Adds the conical tip of the arrow. + * + *

The cone is created with its apex at the end point (the arrow tip) + * and its base pointing back towards the start point.

+ * + *

Local coordinate system: In local space, the cone points in -Y direction + * (apex at lower Y). The base ring is at Y=0, and the apex is at Y=-length.

+ * + * @param endPoint the position of the arrow tip (cone apex) + * @param radius the radius of the cone base + * @param length the length of the cone + * @param segments the number of segments around the circumference + * @param color the fill color + * @param rotMatrix the rotation matrix to apply + * @param dirX direction X component + * @param dirY direction Y component + * @param dirZ direction Z component + */ + private void addConeTip(final Point3D endPoint, final double radius, + final double length, final int segments, + final Color color, final Matrix3x3 rotMatrix, + final double dirX, final double dirY, final double dirZ) { + // Apex is at endPoint (the arrow tip) + // Base center is at endPoint - length * direction (toward arrow start) + final double baseCenterX = endPoint.x - length * dirX; + final double baseCenterY = endPoint.y - length * dirY; + final double baseCenterZ = endPoint.z - length * dirZ; + + // Generate base ring vertices + // In local space, cone points in -Y direction, so base is at Y=0 + final Point3D[] baseRing = new Point3D[segments]; + + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double localX = radius * Math.cos(angle); + final double localZ = radius * Math.sin(angle); + + // Base ring vertices at local Y=0 + final Point3D local = new Point3D(localX, 0, localZ); + rotMatrix.transform(local, local); + local.x += baseCenterX; + local.y += baseCenterY; + local.z += baseCenterZ; + baseRing[i] = local; + } + + // Apex point (the arrow tip) + final Point3D apex = new Point3D(endPoint.x, endPoint.y, endPoint.z); + + // Create cone side faces + // Winding: apex → current → next creates CCW winding when viewed from outside + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + + addShape(new SolidPolygon( + new Point3D(apex.x, apex.y, apex.z), + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z), + new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z), + color)); + } + + // Create base cap of the cone tip (fills the gap between cone and cylinder body) + // Single N-vertex polygon that closes the loop to create segments triangles + // (segments+2 vertices → segments triangles via fan triangulation) + // The base cap faces toward the arrow body/start, opposite to the cone's pointing direction. + // Winding: center → ring[segments-1] → ... → ring[1] → ring[0] → ring[segments-1] + final Point3D baseCenter = new Point3D(baseCenterX, baseCenterY, baseCenterZ); + final Point3D[] tipBaseCapVertices = new Point3D[segments + 2]; + tipBaseCapVertices[0] = baseCenter; + for (int i = 0; i < segments; i++) { + tipBaseCapVertices[i + 1] = baseRing[segments - 1 - i]; + } + tipBaseCapVertices[segments + 1] = baseRing[segments - 1]; // close the loop + addShape(new SolidPolygon(tipBaseCapVertices, color)); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java new file mode 100644 index 0000000..740b5b9 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java @@ -0,0 +1,268 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.math.Matrix3x3; +import eu.svjatoslav.aukio.e3d.math.Quaternion; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A solid cone that can be oriented in any direction. + * + *

The cone has a circular base and a single apex (tip) point. Two constructors + * are provided for different use cases:

+ * + *
    + *
  • Directional (recommended): Specify apex point and base center point. + * The cone points from apex toward the base center. This allows arbitrary + * orientation and is the most intuitive API.
  • + *
  • Y-axis aligned: Specify base center, radius, and height. The cone + * points in -Y direction (apex at lower Y). Useful for simple vertical cones.
  • + *
+ * + *

Usage examples:

+ *
{@code
+ * // Directional constructor: cone pointing from apex toward base
+ * SolidPolygonCone directionalCone = new SolidPolygonCone(
+ *     new Point3D(0, -100, 0),   // apex (tip of the cone)
+ *     new Point3D(0, 50, 0),     // baseCenter (cone points toward this)
+ *     50,                        // radius of the circular base
+ *     16,                        // segments
+ *     Color.RED
+ * );
+ *
+ * // Y-axis aligned constructor: cone pointing upward
+ * SolidPolygonCone verticalCone = new SolidPolygonCone(
+ *     new Point3D(0, 0, 300),    // baseCenter
+ *     50,                        // radius
+ *     100,                       // height
+ *     16,                        // segments
+ *     Color.RED
+ * );
+ * }
+ * + * @see SolidPolygonCylinder + * @see SolidPolygonArrow + * @see SolidPolygon + */ +public class SolidPolygonCone extends AbstractCompositeShape { + + /** + * Constructs a solid cone pointing from apex toward base center. + * + *

This is the recommended constructor for placing cones in 3D space. + * The cone's apex (tip) is at {@code apexPoint}, and the circular base + * is centered at {@code baseCenterPoint}. The cone points in the direction + * from apex to base center.

+ * + *

Coordinate interpretation:

+ *
    + *
  • {@code apexPoint} - the sharp tip of the cone
  • + *
  • {@code baseCenterPoint} - the center of the circular base; the cone + * "points" in this direction from the apex
  • + *
  • The distance between apex and base center determines the cone height
  • + *
+ * + * @param apexPoint the position of the cone's tip (apex) + * @param baseCenterPoint the center point of the circular base; the cone + * points from apex toward this point + * @param radius the radius of the circular base + * @param segments the number of segments around the circumference. + * Higher values create smoother cones. Minimum is 3. + * @param color the fill color applied to all faces of the cone + */ + public SolidPolygonCone(final Point3D apexPoint, final Point3D baseCenterPoint, + final double radius, final int segments, + final Color color) { + super(); + + // Calculate direction and height from apex to base center + final double dx = baseCenterPoint.x - apexPoint.x; + final double dy = baseCenterPoint.y - apexPoint.y; + final double dz = baseCenterPoint.z - apexPoint.z; + final double height = Math.sqrt(dx * dx + dy * dy + dz * dz); + + // Handle degenerate case: apex and base center are the same point + if (height < 0.001) { + return; + } + + // Normalize direction vector (from apex toward base) + final double nx = dx / height; + final double ny = dy / height; + final double nz = dz / height; + + // Calculate rotation to align Y-axis with direction + // Default cone points in -Y direction (apex at origin, base at -Y) + // We need to rotate from (0, -1, 0) to (nx, ny, nz) + final Quaternion rotation = createRotationFromYAxis(nx, ny, nz); + final Matrix3x3 rotMatrix = rotation.toMatrix(); + + // Generate base ring vertices in local space, then rotate and translate + // In local space: apex is at origin, base is at Y = -height + // (cone points in -Y direction in local space) + final Point3D[] baseRing = new Point3D[segments]; + + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double localX = radius * Math.cos(angle); + final double localZ = radius * Math.sin(angle); + + // Base ring vertex in local space (Y = -height) + final Point3D local = new Point3D(localX, -height, localZ); + rotMatrix.transform(local, local); + local.x += apexPoint.x; + local.y += apexPoint.y; + local.z += apexPoint.z; + baseRing[i] = local; + } + + // Apex point (the cone tip) + final Point3D apex = new Point3D(apexPoint.x, apexPoint.y, apexPoint.z); + + // Create side faces connecting each pair of adjacent base vertices to the apex + // Winding: apex → next → current creates CCW winding when viewed from outside + // (Base ring vertices go CCW when viewed from apex looking at base, so we reverse) + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + + addShape(new SolidPolygon( + new Point3D(apex.x, apex.y, apex.z), + new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z), + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z), + color)); + } + + // Create base cap (circular bottom face) + // Single N-vertex polygon that closes the loop to create segments triangles + // (segments+2 vertices → segments triangles via fan triangulation) + // The cap faces away from the apex (in the direction the cone points). + // Winding: center → ring[0] → ring[1] → ... → ring[segments-1] → ring[0] + final Point3D[] baseCapVertices = new Point3D[segments + 2]; + baseCapVertices[0] = baseCenterPoint; + for (int i = 0; i < segments; i++) { + baseCapVertices[i + 1] = baseRing[i]; + } + baseCapVertices[segments + 1] = baseRing[0]; // close the loop + addShape(new SolidPolygon(baseCapVertices, color)); + + setBackfaceCulling(true); + } + + /** + * Constructs a solid cone with circular base centered at the given point, + * pointing in the -Y direction. + * + *

This constructor creates a Y-axis aligned cone. The apex is positioned + * at {@code baseCenter.y - height} (above the base in the negative Y direction). + * For cones pointing in arbitrary directions, use + * {@link #SolidPolygonCone(Point3D, Point3D, double, int, Color)} instead.

+ * + *

Coordinate system: The cone points in -Y direction (apex at lower Y). + * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height. + * In Aukio 3D's coordinate system, "up" visually is negative Y.

+ * + * @param baseCenter the center point of the cone's circular base in 3D space + * @param radius the radius of the circular base + * @param height the height of the cone from base center to apex + * @param segments the number of segments around the circumference. + * Higher values create smoother cones. Minimum is 3. + * @param color the fill color applied to all faces of the cone + */ + public SolidPolygonCone(final Point3D baseCenter, final double radius, + final double height, final int segments, + final Color color) { + super(); + + // Apex is above the base (negative Y direction in this coordinate system) + final double apexY = baseCenter.y - height; + final Point3D apex = new Point3D(baseCenter.x, apexY, baseCenter.z); + + // Generate vertices around the circular base + // Vertices are ordered counter-clockwise when viewed from above (from +Y) + final Point3D[] baseRing = new Point3D[segments]; + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double x = baseCenter.x + radius * Math.cos(angle); + final double z = baseCenter.z + radius * Math.sin(angle); + baseRing[i] = new Point3D(x, baseCenter.y, z); + } + + // Create side faces connecting each pair of adjacent base vertices to the apex + // Winding: apex → next → current creates CCW winding when viewed from outside + // (Base ring vertices go CCW when viewed from apex looking at base, so we reverse) + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + + addShape(new SolidPolygon( + new Point3D(apex.x, apex.y, apex.z), + new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z), + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z), + color)); + } + + // Create base cap (circular bottom face) + // Single N-vertex polygon that closes the loop to create segments triangles + // (segments+2 vertices → segments triangles via fan triangulation) + // The base cap faces in +Y direction (downward, away from apex). + // Winding: center → ring[0] → ring[1] → ... → ring[segments-1] → ring[0] + final Point3D[] baseCapVertices = new Point3D[segments + 2]; + baseCapVertices[0] = baseCenter; + for (int i = 0; i < segments; i++) { + baseCapVertices[i + 1] = baseRing[i]; + } + baseCapVertices[segments + 1] = baseRing[0]; // close the loop + addShape(new SolidPolygon(baseCapVertices, color)); + + setBackfaceCulling(true); + } + + /** + * Creates a quaternion that rotates from the -Y axis to the given direction. + * + *

The cone by default points in the -Y direction (apex at origin, base at -Y). + * This method computes the rotation needed to align the cone with the target + * direction vector.

+ * + * @param nx normalized direction X component + * @param ny normalized direction Y component + * @param nz normalized direction Z component + * @return quaternion representing the rotation + */ + private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) { + // Default direction is -Y (0, -1, 0) + // Target direction is (nx, ny, nz) + // Dot product: 0*nx + (-1)*ny + 0*nz = -ny + final double dot = -ny; + + // Check for parallel vectors + if (dot > 0.9999) { + // Direction is nearly -Y, no rotation needed + return Quaternion.identity(); + } + if (dot < -0.9999) { + // Direction is nearly +Y, rotate 180° around X axis + return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI); + } + + // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx) + // This gives the rotation axis + final double axisX = -nz; + final double axisY = 0; + final double axisZ = nx; + final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + final double normalizedAxisX = axisX / axisLength; + final double normalizedAxisZ = axisZ / axisLength; + + // Angle from dot product + final double angle = Math.acos(dot); + + return Quaternion.fromAxisAngle( + new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCube.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCube.java new file mode 100755 index 0000000..e55eef6 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCube.java @@ -0,0 +1,45 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; + +/** + * A solid cube centered at a given point with equal side length along all axes. + * This is a convenience subclass of {@link SolidPolygonRectangularBox} that + * constructs a cube from a center point and a half-side length. + * + *

The cube extends {@code size} units in each direction from the center, + * resulting in a total edge length of {@code 2 * size}.

+ * + *

Usage example:

+ *
{@code
+ * SolidPolygonCube cube = new SolidPolygonCube(
+ *         new Point3D(0, 0, 300), 50, Color.GREEN);
+ * shapeCollection.addShape(cube);
+ * }
+ * + * @see SolidPolygonRectangularBox + * @see Color + */ +public class SolidPolygonCube extends SolidPolygonRectangularBox { + + /** + * Constructs a solid cube centered at the given point. + * + * @param center the center point of the cube in 3D space + * @param size the half-side length; the cube extends this distance from + * the center along each axis, giving a total edge length of + * {@code 2 * size} + * @param color the fill color applied to all faces of the cube + */ + public SolidPolygonCube(final Point3D center, final double size, + final Color color) { + super(new Point3D(center.x - size, center.y - size, center.z - size), + new Point3D(center.x + size, center.y + size, center.z + size), + color); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java new file mode 100644 index 0000000..3fd7b64 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java @@ -0,0 +1,200 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.math.Matrix3x3; +import eu.svjatoslav.aukio.e3d.math.Quaternion; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A solid cylinder defined by two end points. + * + *

The cylinder extends from startPoint to endPoint with circular caps at both + * ends. The number of segments determines the smoothness of the curved surface.

+ * + *

Usage example:

+ *
{@code
+ * // Create a vertical cylinder from Y=100 to Y=200
+ * SolidPolygonCylinder cylinder = new SolidPolygonCylinder(
+ *     new Point3D(0, 100, 0),   // start point (bottom)
+ *     new Point3D(0, 200, 0),   // end point (top)
+ *     10,                        // radius
+ *     16,                        // segments
+ *     Color.RED                  // color
+ * );
+ *
+ * // Create a horizontal cylinder along X axis
+ * SolidPolygonCylinder pipe = new SolidPolygonCylinder(
+ *     new Point3D(-50, 0, 0),
+ *     new Point3D(50, 0, 0),
+ *     5, 12, Color.BLUE
+ * );
+ * }
+ * + * @see SolidPolygonCone + * @see SolidPolygonArrow + * @see SolidPolygon + */ +public class SolidPolygonCylinder extends AbstractCompositeShape { + + /** + * Constructs a solid cylinder between two end points. + * + *

The cylinder has circular caps at both startPoint and endPoint, + * connected by a curved side surface. The orientation is automatically + * calculated from the direction between the two points.

+ * + * @param startPoint the center of the first cap + * @param endPoint the center of the second cap + * @param radius the radius of the cylinder + * @param segments the number of segments around the circumference. + * Higher values create smoother cylinders. Minimum is 3. + * @param color the fill color applied to all polygons + */ + public SolidPolygonCylinder(final Point3D startPoint, final Point3D endPoint, + final double radius, final int segments, + final Color color) { + super(); + + // Calculate direction and distance + final double dx = endPoint.x - startPoint.x; + final double dy = endPoint.y - startPoint.y; + final double dz = endPoint.z - startPoint.z; + final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + // Handle degenerate case: start and end are the same point + if (distance < 0.001) { + return; + } + + // Normalize direction vector + final double nx = dx / distance; + final double ny = dy / distance; + final double nz = dz / distance; + + // Calculate rotation to align Y-axis with direction + // Default cylinder is aligned along Y-axis + // We need to rotate from (0, 1, 0) to (nx, ny, nz) + final Quaternion rotation = createRotationFromYAxis(nx, ny, nz); + final Matrix3x3 rotMatrix = rotation.toMatrix(); + + // Cylinder center is at midpoint between start and end + final double centerX = (startPoint.x + endPoint.x) / 2.0; + final double centerY = (startPoint.y + endPoint.y) / 2.0; + final double centerZ = (startPoint.z + endPoint.z) / 2.0; + final double halfLength = distance / 2.0; + + // Generate ring vertices in local space, then rotate and translate + // In local space: cylinder is aligned along Y-axis + // - startSideRing is at local -Y (toward startPoint) + // - endSideRing is at local +Y (toward endPoint) + final Point3D[] startSideRing = new Point3D[segments]; + final Point3D[] endSideRing = new Point3D[segments]; + + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double localX = radius * Math.cos(angle); + final double localZ = radius * Math.sin(angle); + + // Start-side ring (at -halfLength in local Y = toward startPoint) + final Point3D startLocal = new Point3D(localX, -halfLength, localZ); + rotMatrix.transform(startLocal, startLocal); + startLocal.x += centerX; + startLocal.y += centerY; + startLocal.z += centerZ; + startSideRing[i] = startLocal; + + // End-side ring (at +halfLength in local Y = toward endPoint) + final Point3D endLocal = new Point3D(localX, halfLength, localZ); + rotMatrix.transform(endLocal, endLocal); + endLocal.x += centerX; + endLocal.y += centerY; + endLocal.z += centerZ; + endSideRing[i] = endLocal; + } + + // Create side faces (one quad per segment) + // Winding: startSide[i] → endSide[i] → endSide[next] → startSide[next] + // creates CCW winding when viewed from outside the cylinder + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + + addShape(SolidPolygon.quad( + startSideRing[i], + endSideRing[i], + endSideRing[next], + startSideRing[next], + color)); + } + + // Create start cap (at startPoint, faces outward from cylinder) + // Single N-vertex polygon that closes the loop to create segments triangles + // (segments+2 vertices → segments triangles via fan triangulation) + // Winding: center → ring[0] → ring[1] → ... → ring[segments-1] → ring[0] + final Point3D[] startCapVertices = new Point3D[segments + 2]; + startCapVertices[0] = startPoint; + for (int i = 0; i < segments; i++) { + startCapVertices[i + 1] = startSideRing[i]; + } + startCapVertices[segments + 1] = startSideRing[0]; // close the loop + addShape(new SolidPolygon(startCapVertices, color)); + + // Create end cap (at endPoint, faces outward from cylinder) + // Reverse winding for opposite-facing cap + // Winding: center → ring[segments-1] → ... → ring[1] → ring[0] → ring[segments-1] + final Point3D[] endCapVertices = new Point3D[segments + 2]; + endCapVertices[0] = endPoint; + for (int i = 0; i < segments; i++) { + endCapVertices[i + 1] = endSideRing[segments - 1 - i]; + } + endCapVertices[segments + 1] = endSideRing[segments - 1]; // close the loop + addShape(new SolidPolygon(endCapVertices, color)); + + setBackfaceCulling(true); + } + + /** + * Creates a quaternion that rotates from the +Y axis to the given direction. + * + * @param nx normalized direction X component + * @param ny normalized direction Y component + * @param nz normalized direction Z component + * @return quaternion representing the rotation + */ + private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) { + // Default direction is +Y (0, 1, 0) + // Target direction is (nx, ny, nz) + // Dot product: 0*nx + 1*ny + 0*nz = ny + final double dot = ny; + + // Check for parallel vectors + if (dot > 0.9999) { + // Direction is nearly +Y, no rotation needed + return Quaternion.identity(); + } + if (dot < -0.9999) { + // Direction is nearly -Y, rotate 180° around X axis + return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI); + } + + // Cross product: (0, 1, 0) x (nx, ny, nz) = (nz, 0, -nx) + // This gives the rotation axis + final double axisX = nz; + final double axisY = 0; + final double axisZ = -nx; + final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + final double normalizedAxisX = axisX / axisLength; + final double normalizedAxisZ = axisZ / axisLength; + + // Angle from dot product + final double angle = Math.acos(dot); + + return Quaternion.fromAxisAngle( + new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java new file mode 100644 index 0000000..90e51d3 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java @@ -0,0 +1,258 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.math.Matrix3x3; +import eu.svjatoslav.aukio.e3d.math.Quaternion; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A solid square-based pyramid that can be oriented in any direction. + * + *

The pyramid has a square base and four triangular faces meeting at an apex + * (tip). Two constructors are provided for different use cases:

+ * + *
    + *
  • Directional (recommended): Specify apex point and base center point. + * The pyramid points from apex toward the base center. This allows arbitrary + * orientation and is the most intuitive API.
  • + *
  • Y-axis aligned: Specify base center, base size, and height. The pyramid + * points in -Y direction (apex at lower Y). Useful for simple vertical pyramids.
  • + *
+ * + *

Usage examples:

+ *
{@code
+ * // Directional constructor: pyramid pointing from apex toward base
+ * SolidPolygonPyramid directionalPyramid = new SolidPolygonPyramid(
+ *     new Point3D(0, -100, 0),   // apex (tip of the pyramid)
+ *     new Point3D(0, 50, 0),     // baseCenter (pyramid points toward this)
+ *     50,                        // baseSize (half-width of square base)
+ *     Color.RED
+ * );
+ *
+ * // Y-axis aligned constructor: pyramid pointing upward
+ * SolidPolygonPyramid verticalPyramid = new SolidPolygonPyramid(
+ *     new Point3D(0, 0, 300),    // baseCenter
+ *     50,                        // baseSize (half-width of square base)
+ *     100,                       // height
+ *     Color.BLUE
+ * );
+ * }
+ * + * @see SolidPolygonCone + * @see SolidPolygonCube + * @see SolidPolygon + */ +public class SolidPolygonPyramid extends AbstractCompositeShape { + + /** + * Constructs a solid square-based pyramid pointing from apex toward base center. + * + *

This is the recommended constructor for placing pyramids in 3D space. + * The pyramid's apex (tip) is at {@code apexPoint}, and the square base + * is centered at {@code baseCenter}. The pyramid points in the direction + * from apex to base center.

+ * + *

Coordinate interpretation:

+ *
    + *
  • {@code apexPoint} - the sharp tip of the pyramid
  • + *
  • {@code baseCenter} - the center of the square base; the pyramid + * "points" in this direction from the apex
  • + *
  • {@code baseSize} - half the width of the square base; the base + * extends this distance from the center along perpendicular axes
  • + *
  • The distance between apex and base center determines the pyramid height
  • + *
+ * + * @param apexPoint the position of the pyramid's tip (apex) + * @param baseCenter the center point of the square base; the pyramid + * points from apex toward this point + * @param baseSize the half-width of the square base; the base extends + * this distance from the center, giving a total base + * edge length of {@code 2 * baseSize} + * @param color the fill color applied to all faces of the pyramid + */ + public SolidPolygonPyramid(final Point3D apexPoint, final Point3D baseCenter, + final double baseSize, final Color color) { + super(); + + // Calculate direction and height from apex to base center + final double dx = baseCenter.x - apexPoint.x; + final double dy = baseCenter.y - apexPoint.y; + final double dz = baseCenter.z - apexPoint.z; + final double height = Math.sqrt(dx * dx + dy * dy + dz * dz); + + // Handle degenerate case: apex and base center are the same point + if (height < 0.001) { + return; + } + + // Normalize direction vector (from apex toward base) + final double nx = dx / height; + final double ny = dy / height; + final double nz = dz / height; + + // Calculate rotation to align Y-axis with direction + // Default pyramid points in -Y direction (apex at origin, base at -Y) + // We need to rotate from (0, -1, 0) to (nx, ny, nz) + final Quaternion rotation = createRotationFromYAxis(nx, ny, nz); + final Matrix3x3 rotMatrix = rotation.toMatrix(); + + // Generate base corner vertices in local space, then rotate and translate + // In local space: apex is at origin, base is at Y = -height + // Base corners form a square centered at (0, -height, 0) + final double h = baseSize; + final Point3D[] baseCorners = new Point3D[4]; + + // Local space corner positions (before rotation) + // Arranged clockwise when viewed from apex (from +Y) + final double[][] localCorners = { + {-h, -height, -h}, // corner 0: negative X, negative Z + {+h, -height, -h}, // corner 1: positive X, negative Z + {+h, -height, +h}, // corner 2: positive X, positive Z + {-h, -height, +h} // corner 3: negative X, positive Z + }; + + for (int i = 0; i < 4; i++) { + final Point3D local = new Point3D(localCorners[i][0], localCorners[i][1], localCorners[i][2]); + rotMatrix.transform(local, local); + local.x += apexPoint.x; + local.y += apexPoint.y; + local.z += apexPoint.z; + baseCorners[i] = local; + } + + // Apex point (the pyramid tip) + final Point3D apex = new Point3D(apexPoint.x, apexPoint.y, apexPoint.z); + + // Create the four triangular faces connecting apex to base edges + // Winding: next → current → apex creates CCW winding when viewed from outside + // (Base corners go CW when viewed from apex, so we reverse to get outward normals) + for (int i = 0; i < 4; i++) { + final int next = (i + 1) % 4; + addShape(new SolidPolygon( + new Point3D(baseCorners[next].x, baseCorners[next].y, baseCorners[next].z), + new Point3D(baseCorners[i].x, baseCorners[i].y, baseCorners[i].z), + new Point3D(apex.x, apex.y, apex.z), + color)); + } + + // Create base cap (square bottom face with center) + // Single N-vertex polygon that closes the loop to create 4 triangles + // (6 vertices → 4 triangles via fan triangulation) + // The cap faces away from the apex (in the direction the pyramid points). + // Winding: center → corner[3] → corner[0] → corner[1] → corner[2] → corner[3] + // (CW when viewed from apex, CCW when viewed from base side) + final Point3D[] baseCapVertices = new Point3D[6]; + baseCapVertices[0] = baseCenter; + baseCapVertices[1] = baseCorners[3]; + baseCapVertices[2] = baseCorners[0]; + baseCapVertices[3] = baseCorners[1]; + baseCapVertices[4] = baseCorners[2]; + baseCapVertices[5] = baseCorners[3]; // close the loop + addShape(new SolidPolygon(baseCapVertices, color)); + + setBackfaceCulling(true); + } + + /** + * Constructs a solid square-based pyramid with base centered at the given point, + * pointing in the -Y direction. + * + *

This constructor creates a Y-axis aligned pyramid. The apex is positioned + * at {@code baseCenter.y - height} (above the base in the negative Y direction). + * For pyramids pointing in arbitrary directions, use + * {@link #SolidPolygonPyramid(Point3D, Point3D, double, Color)} instead.

+ * + *

Coordinate system: The pyramid points in -Y direction (apex at lower Y). + * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height. + * In Aukio 3D's coordinate system, "up" visually is negative Y.

+ * + * @param baseCenter the center point of the pyramid's base in 3D space + * @param baseSize the half-width of the square base; the base extends + * this distance from the center along X and Z axes, + * giving a total base edge length of {@code 2 * baseSize} + * @param height the height of the pyramid from base center to apex + * @param color the fill color applied to all faces of the pyramid + */ + public SolidPolygonPyramid(final Point3D baseCenter, final double baseSize, + final double height, final Color color) { + super(); + + final double halfBase = baseSize; + final double apexY = baseCenter.y - height; + final double baseY = baseCenter.y; + + // Base corners arranged clockwise when viewed from above (+Y) + // Naming: "negative/positive X" and "negative/positive Z" relative to base center + final Point3D negXnegZ = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z - halfBase); + final Point3D posXnegZ = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z - halfBase); + final Point3D posXposZ = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z + halfBase); + final Point3D negXposZ = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z + halfBase); + final Point3D apex = new Point3D(baseCenter.x, apexY, baseCenter.z); + + // Four triangular faces from apex to base edges + // Winding: apex → current → next creates CCW when viewed from outside + addShape(new SolidPolygon(negXnegZ, posXnegZ, apex, color)); + addShape(new SolidPolygon(posXnegZ, posXposZ, apex, color)); + addShape(new SolidPolygon(posXposZ, negXposZ, apex, color)); + addShape(new SolidPolygon(negXposZ, negXnegZ, apex, color)); + + // Base cap (square bottom face) + // Single quad using the 4 corner vertices + // Cap faces +Y (downward, away from apex). The base is at higher Y than apex. + // For outward normal (+Y direction), we need CCW ordering when viewed from +Y. + // Quad order: negXposZ → posXposZ → posXnegZ → negXnegZ (CCW from +Y) + addShape(SolidPolygon.quad(negXposZ, posXposZ, posXnegZ, negXnegZ, color)); + + setBackfaceCulling(true); + } + + /** + * Creates a quaternion that rotates from the -Y axis to the given direction. + * + *

The pyramid by default points in the -Y direction (apex at origin, base at -Y). + * This method computes the rotation needed to align the pyramid with the target + * direction vector.

+ * + * @param nx normalized direction X component + * @param ny normalized direction Y component + * @param nz normalized direction Z component + * @return quaternion representing the rotation + */ + private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) { + // Default direction is -Y (0, -1, 0) + // Target direction is (nx, ny, nz) + // Dot product: 0*nx + (-1)*ny + 0*nz = -ny + final double dot = -ny; + + // Check for parallel vectors + if (dot > 0.9999) { + // Direction is nearly -Y, no rotation needed + return Quaternion.identity(); + } + if (dot < -0.9999) { + // Direction is nearly +Y, rotate 180° around X axis + return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI); + } + + // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx) + // This gives the rotation axis + final double axisX = -nz; + final double axisY = 0; + final double axisZ = nx; + final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + final double normalizedAxisX = axisX / axisLength; + final double normalizedAxisZ = axisZ / axisLength; + + // Angle from dot product + final double angle = Math.acos(dot); + + return Quaternion.fromAxisAngle( + new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java new file mode 100755 index 0000000..38e5856 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java @@ -0,0 +1,122 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A solid (filled) rectangular box composed of 6 quadrilateral polygons (1 per face, + * covering all 6 faces). + * + *

The box is defined by two diagonally opposite corner points in 3D space. + * The box is axis-aligned, meaning its edges are parallel to the X, Y, and Z axes.

+ * + *

Vertex layout:

+ *
+ *         cornerB (max) ────────┐
+ *              /│              /│
+ *             / │             / │
+ *            /  │            /  │
+ *           ┌───┼───────────┐   │
+ *           │   │           │   │
+ *           │   │           │   │
+ *           │   └───────────│───┘
+ *           │  /            │  /
+ *           │ /             │ /
+ *           │/              │/
+ *           └───────────────┘ cornerA (min)
+ * 
+ * + *

The eight vertices are derived from the two corner points:

+ *
    + *
  • Corner A defines minimum X, Y, Z
  • + *
  • Corner B defines maximum X, Y, Z
  • + *
  • The other 6 vertices are computed from combinations of these coordinates
  • + *
+ * + *

Usage examples:

+ *
{@code
+ * // Create a box from two opposite corners
+ * SolidPolygonRectangularBox box = new SolidPolygonRectangularBox(
+ *     new Point3D(-50, -25, 100),  // cornerA (minimum X, Y, Z)
+ *     new Point3D(50, 25, 200),    // cornerB (maximum X, Y, Z)
+ *     Color.BLUE
+ * );
+ *
+ * // Create a cube using center + size (see SolidPolygonCube for convenience)
+ * double size = 50;
+ * SolidPolygonRectangularBox cube = new SolidPolygonRectangularBox(
+ *     new Point3D(0 - size, 0 - size, 200 - size),  // cornerA
+ *     new Point3D(0 + size, 0 + size, 200 + size),  // cornerB
+ *     Color.RED
+ * );
+ * }
+ * + * @see SolidPolygonCube + * @see SolidPolygon + */ +public class SolidPolygonRectangularBox extends AbstractCompositeShape { + + /** + * Constructs a solid rectangular box between two diagonally opposite corner + * points in 3D space. + * + *

The box is axis-aligned and fills the rectangular region between the + * two corners. The corner points do not need to be ordered (cornerA can have + * larger coordinates than cornerB); the constructor will determine the actual + * min/max bounds automatically.

+ * + * @param cornerA the first corner point (any of the 8 corners) + * @param cornerB the diagonally opposite corner point + * @param color the fill color applied to all 6 quadrilateral polygons + */ + public SolidPolygonRectangularBox(final Point3D cornerA, final Point3D cornerB, final Color color) { + super(); + + // Determine actual min/max bounds (corners may be in any order) + final double minX = Math.min(cornerA.x, cornerB.x); + final double maxX = Math.max(cornerA.x, cornerB.x); + final double minY = Math.min(cornerA.y, cornerB.y); + final double maxY = Math.max(cornerA.y, cornerB.y); + final double minZ = Math.min(cornerA.z, cornerB.z); + final double maxZ = Math.max(cornerA.z, cornerB.z); + + // Compute all 8 vertices from the bounds + // Naming convention: min/max indicates which bound the coordinate uses + // minMinMin = (minX, minY, minZ), maxMaxMax = (maxX, maxY, maxZ), etc. + final Point3D minMinMin = new Point3D(minX, minY, minZ); + final Point3D maxMinMin = new Point3D(maxX, minY, minZ); + final Point3D maxMinMax = new Point3D(maxX, minY, maxZ); + final Point3D minMinMax = new Point3D(minX, minY, maxZ); + + final Point3D minMaxMin = new Point3D(minX, maxY, minZ); + final Point3D maxMaxMin = new Point3D(maxX, maxY, minZ); + final Point3D minMaxMax = new Point3D(minX, maxY, maxZ); + final Point3D maxMaxMax = new Point3D(maxX, maxY, maxZ); + + // Bottom face (y = minY) - CCW when viewed from below + addShape(new SolidPolygon(new Point3D[]{minMinMin, maxMinMin, maxMinMax, minMinMax}, color)); + + // Top face (y = maxY) - CCW when viewed from above + addShape(new SolidPolygon(new Point3D[]{minMaxMin, minMaxMax, maxMaxMax, maxMaxMin}, color)); + + // Front face (z = minZ) - CCW when viewed from front + addShape(new SolidPolygon(new Point3D[]{minMinMin, minMaxMin, maxMaxMin, maxMinMin}, color)); + + // Back face (z = maxZ) - CCW when viewed from behind + addShape(new SolidPolygon(new Point3D[]{maxMinMax, maxMaxMax, minMaxMax, minMinMax}, color)); + + // Left face (x = minX) - CCW when viewed from left + addShape(new SolidPolygon(new Point3D[]{minMinMin, minMinMax, minMaxMax, minMaxMin}, color)); + + // Right face (x = maxX) - CCW when viewed from right + addShape(new SolidPolygon(new Point3D[]{maxMinMin, maxMaxMin, maxMaxMax, maxMinMax}, color)); + + setBackfaceCulling(true); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.java new file mode 100644 index 0000000..7ebb0cb --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/SolidPolygonSphere.java @@ -0,0 +1,84 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A solid sphere composed of triangular polygons. + * + *

The sphere is constructed using a latitude-longitude grid (UV sphere). + * The number of segments determines the smoothness - more segments create + * a smoother sphere but require more polygons.

+ * + *

Usage example:

+ *
{@code
+ * // Create a sphere with radius 50 and 16 segments (smooth)
+ * SolidPolygonSphere sphere = new SolidPolygonSphere(
+ *     new Point3D(0, 0, 200), 50, 16, Color.RED);
+ * shapeCollection.addShape(sphere);
+ * }
+ * + * @see SolidPolygonCube + * @see SolidPolygon + * @see AbstractCompositeShape + */ +public class SolidPolygonSphere extends AbstractCompositeShape { + + /** + * Constructs a solid sphere centered at the given point. + * + * @param center the center point of the sphere in 3D space + * @param radius the radius of the sphere + * @param segments the number of segments (latitude/longitude divisions). + * Higher values create smoother spheres. Minimum is 3. + * @param color the fill color applied to all triangular polygons + */ + public SolidPolygonSphere(final Point3D center, final double radius, + final int segments, final Color color) { + super(); + + final int rings = segments; + final int sectors = segments * 2; + + for (int i = 0; i < rings; i++) { + double lat0 = Math.PI * (-0.5 + (double) i / rings); + double lat1 = Math.PI * (-0.5 + (double) (i + 1) / rings); + + for (int j = 0; j < sectors; j++) { + double lon0 = 2 * Math.PI * (double) j / sectors; + double lon1 = 2 * Math.PI * (double) (j + 1) / sectors; + + Point3D p0 = sphericalToCartesian(center, radius, lat0, lon0); + Point3D p1 = sphericalToCartesian(center, radius, lat0, lon1); + Point3D p2 = sphericalToCartesian(center, radius, lat1, lon0); + Point3D p3 = sphericalToCartesian(center, radius, lat1, lon1); + + if (i > 0) { + addShape(new SolidPolygon(p0, p2, p1, color)); + } + + if (i < rings - 1) { + addShape(new SolidPolygon(p2, p3, p1, color)); + } + } + } + + setBackfaceCulling(true); + } + + private Point3D sphericalToCartesian(final Point3D center, + final double radius, + final double lat, + final double lon) { + double x = center.x + radius * Math.cos(lat) * Math.cos(lon); + double y = center.y + radius * Math.sin(lat); + double z = center.z + radius * Math.cos(lat) * Math.sin(lon); + return new Point3D(x, y, z); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/package-info.java new file mode 100644 index 0000000..0d33bac --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/solid/package-info.java @@ -0,0 +1,24 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Solid composite shapes built from SolidTriangle primitives. + * + *

These shapes render as filled surfaces with optional flat shading. + * Useful for creating opaque 3D objects like boxes, spheres, and cylinders.

+ * + *

Key classes:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube} - A solid cube
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonRectangularBox} - A solid box
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonSphere} - A solid sphere
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCylinder} - A solid cylinder
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonPyramid} - A solid pyramid
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/SdfGlyphCache.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/SdfGlyphCache.java new file mode 100644 index 0000000..7090806 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/SdfGlyphCache.java @@ -0,0 +1,278 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.textcanvas; + +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas.FONT_CHAR_HEIGHT_TEXTURE_PIXELS; +import static eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas.FONT_CHAR_WIDTH_TEXTURE_PIXELS; + +/** + * Per-glyph signed distance field (SDF) cache for sharp text rendering. + * + *

Instead of storing ink coverage (a photocopy of the glyph that blurs + * under any resampling), a distance field stores, per texel, the distance + * to the nearest glyph edge — negative inside the ink, positive outside. + * The rasterizer re-derives the edge per screen pixel from this smooth + * field, so text stays sharp at any magnification and degrades to clean + * gray under minification instead of aliasing.

+ * + *

Each glyph is rendered at {@link #SUPERSAMPLE}x the cell resolution + * with anti-aliasing (giving the contour sub-texel precision), converted + * to a signed distance field by an exact Euclidean distance transform + * (Felzenszwalb & Huttenlocher separable EDT), clamped to + * {@link #SPREAD_TEXELS} texels of gradient around the edge, and + * downsampled by averaging to the cell size. Results are cached per + * character; stamping a glyph into a canvas mask is a small block copy.

+ * + *

Mask values: 0 = deep inside ink, 127/128 = the edge, 255 = far + * outside any glyph. The matching coverage formula lives in + * {@code TexturedTriangle}'s SDF scanline path.

+ * + * @see TextCanvas + */ +public final class SdfGlyphCache { + + /** + * Width of the distance gradient around the glyph edge, in + * primary-texture texels. The normalized mask value 0.5 +/- 0.5 spans + * -SPREAD..+SPREAD texels of signed distance. + */ + public static final double SPREAD_TEXELS = 2.0; + + /** + * Glyph rasterization supersampling factor relative to the cell size. + */ + private static final int SUPERSAMPLE = 4; + + private static final int GLYPH_W = FONT_CHAR_WIDTH_TEXTURE_PIXELS; + private static final int GLYPH_H = FONT_CHAR_HEIGHT_TEXTURE_PIXELS; + private static final int HI_W = GLYPH_W * SUPERSAMPLE; + private static final int HI_H = GLYPH_H * SUPERSAMPLE; + + /** + * Signed distance clamp range in hi-res pixels. + */ + private static final float SPREAD_HI = (float) (SPREAD_TEXELS * SUPERSAMPLE); + + /** + * Hi-res scratch font, sized to fit the scratch (see static init). + * Liberation Mono Bold is metric-compatible with Courier New (same + * 0.6em advance, so the cell grid is unchanged) but sans-serif with + * uniform sturdy strokes — Courier's serifs and thin strokes decay + * into unresolvable noise when the distance field is minified. Falls + * back to the logical Monospaced family. + */ + private static final Font FONT_HI_SIZED; + + /** + * Baseline offset in the hi-res scratch, chosen from the font's + * actual metrics so the tallest glyph fits vertically centered. + */ + private static final int BASELINE_HI; + + static { + Font family = new Font("Liberation Mono", Font.BOLD, 12); + if (!family.getFamily().toLowerCase().contains("liberation")) { + family = new Font("Monospaced", Font.BOLD, 12); + } + + // Size the font so its metrics fit the scratch with margin: + // advance <= HI_W (wide glyphs must not clip the cell edge, which + // would corrupt the distance field there) and ascent+descent <= + // 95% of HI_H. + final BufferedImage probe = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + final Graphics2D pg = probe.createGraphics(); + int size = (int) (FONT_CHAR_HEIGHT_TEXTURE_PIXELS / 1.066) * SUPERSAMPLE; + int baseline = HI_H * 3 / 4; + while (size > 8) { + final Font f = family.deriveFont((float) size); + pg.setFont(f); + final java.awt.font.FontRenderContext frc = pg.getFontRenderContext(); + double maxAdvance = 0; + for (char c = 33; c < 127; c++) { + maxAdvance = Math.max(maxAdvance, + f.getStringBounds(new char[]{c}, 0, 1, frc).getWidth()); + } + final int ascent = pg.getFontMetrics().getMaxAscent(); + final int descent = pg.getFontMetrics().getMaxDescent(); + if (maxAdvance <= HI_W * 0.98 && ascent + descent <= HI_H * 0.95) { + baseline = (HI_H + ascent - descent) / 2; + break; + } + size--; + } + pg.dispose(); + BASELINE_HI = baseline; + FONT_HI_SIZED = family.deriveFont((float) size); + } + + private static final float INF = 1e15f; + + private static final Map CACHE = new ConcurrentHashMap<>(); + + /** + * Serializes glyph rasterization: ConcurrentHashMap.computeIfAbsent + * locks per bin, so two threads generating DIFFERENT glyphs could + * otherwise enter generate() concurrently and use the shared static + * FONT_HI_SIZED at the same time. libfreetype does not tolerate + * concurrent scaler access — observed as a SIGSEGV in + * FreetypeFontScaler.getGlyphMetricsNative on the pty-reader thread + * (aukio terminal panel) racing another text canvas. + */ + private static final Object FONT_RENDER_LOCK = new Object(); + + private SdfGlyphCache() { + } + + /** + * Returns the cached {@link #GLYPH_W} x {@link #GLYPH_H} distance + * field for the given character, generating it on first access. + * Values 0 (inside ink) .. 255 (far outside), edge at ~127.5. + * + * @param c the character + * @return the glyph mask (row-major, one int per texel, do not modify) + */ + public static int[] glyphMask(final char c) { + return CACHE.computeIfAbsent(c, SdfGlyphCache::generate); + } + + private static int[] generate(final char c) { + final int[] px; + synchronized (FONT_RENDER_LOCK) { + final BufferedImage scratch = new BufferedImage(HI_W, HI_H, BufferedImage.TYPE_INT_RGB); + final Graphics2D g = scratch.createGraphics(); + g.setColor(java.awt.Color.BLACK); + g.fillRect(0, 0, HI_W, HI_H); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setFont(FONT_HI_SIZED); + g.setColor(java.awt.Color.WHITE); + g.drawChars(new char[]{c}, 0, 1, 0, BASELINE_HI); + g.dispose(); + px = ((DataBufferInt) scratch.getRaster().getDataBuffer()).getData(); + } + + final boolean[] ink = new boolean[HI_W * HI_H]; + for (int i = 0; i < px.length; i++) { + ink[i] = (px[i] & 0xff) > 127; + } + + // Distance to nearest ink pixel (meaningful outside the glyph) + // and to nearest background pixel (meaningful inside it). + final float[] dOut = edt(ink, HI_W, HI_H); + final boolean[] background = new boolean[ink.length]; + for (int i = 0; i < ink.length; i++) { + background[i] = !ink[i]; + } + final float[] dIn = edt(background, HI_W, HI_H); + + // Signed distance, clamped to the spread and averaged down to + // cell resolution. Averaging the FIELD (not coverage) preserves + // the edge position under the downsample. + final int[] mask = new int[GLYPH_W * GLYPH_H]; + for (int y = 0; y < GLYPH_H; y++) { + for (int x = 0; x < GLYPH_W; x++) { + float sum = 0; + for (int sy = 0; sy < SUPERSAMPLE; sy++) { + final int rowBase = (y * SUPERSAMPLE + sy) * HI_W; + for (int sx = 0; sx < SUPERSAMPLE; sx++) { + final int i = rowBase + x * SUPERSAMPLE + sx; + float signed = dOut[i] - dIn[i]; + if (signed > SPREAD_HI) signed = SPREAD_HI; + else if (signed < -SPREAD_HI) signed = -SPREAD_HI; + sum += signed; + } + } + final float avg = sum / (SUPERSAMPLE * SUPERSAMPLE); + final int v = Math.round((avg / SPREAD_HI * 0.5f + 0.5f) * 255f); + mask[y * GLYPH_W + x] = 0xFF000000 | (v << 16) | (v << 8) | v; + } + } + return mask; + } + + /** + * Exact squared Euclidean distance transform of the given feature + * mask via two separable 1-D passes (Felzenszwalb & Huttenlocher). + * + * @param feature true at feature (zero-distance) pixels + * @param w grid width + * @param h grid height + * @return per-pixel distance to the nearest feature pixel + */ + private static float[] edt(final boolean[] feature, final int w, final int h) { + final float[] f = new float[w * h]; + for (int i = 0; i < f.length; i++) { + f[i] = feature[i] ? 0f : INF; + } + + final int maxDim = Math.max(w, h); + final int[] v = new int[maxDim]; + final float[] z = new float[maxDim + 1]; + final float[] colIn = new float[maxDim]; + final float[] colOut = new float[maxDim]; + + final float[] d = new float[w * h]; + for (int x = 0; x < w; x++) { + for (int y = 0; y < h; y++) { + colIn[y] = f[y * w + x]; + } + edt1d(colIn, colOut, h, v, z); + for (int y = 0; y < h; y++) { + d[y * w + x] = colOut[y]; + } + } + for (int y = 0; y < h; y++) { + System.arraycopy(d, y * w, colIn, 0, w); + edt1d(colIn, colOut, w, v, z); + System.arraycopy(colOut, 0, d, y * w, w); + } + + for (int i = 0; i < d.length; i++) { + d[i] = (float) Math.sqrt(d[i]); + } + return d; + } + + /** + * 1-D squared distance transform: d[q] = min over p of + * (q-p)^2 + f[p], via the lower envelope of parabolas. + */ + private static void edt1d(final float[] f, final float[] d, final int n, + final int[] v, final float[] z) { + int k = 0; + v[0] = 0; + z[0] = Float.NEGATIVE_INFINITY; + z[1] = Float.POSITIVE_INFINITY; + for (int q = 1; q < n; q++) { + float s = ((f[q] + (float) q * q) - (f[v[k]] + (float) v[k] * v[k])) + / (2f * q - 2f * v[k]); + while (s <= z[k]) { + k--; + s = ((f[q] + (float) q * q) - (f[v[k]] + (float) v[k] * v[k])) + / (2f * q - 2f * v[k]); + } + k++; + v[k] = q; + z[k] = s; + z[k + 1] = Float.POSITIVE_INFINITY; + } + k = 0; + for (int q = 0; q < n; q++) { + while (z[k + 1] < q) { + k++; + } + final float dv = q - v[k]; + d[q] = dv * dv + f[v[k]]; + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.java new file mode 100644 index 0000000..fdfe6c7 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/TextCanvas.java @@ -0,0 +1,363 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.textcanvas; + +import eu.svjatoslav.aukio.e3d.gui.TextPointer; +import eu.svjatoslav.aukio.e3d.math.Transform; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.TexturedRectangle; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; + +import static eu.svjatoslav.aukio.e3d.renderer.raster.Color.BLACK; +import static eu.svjatoslav.aukio.e3d.renderer.raster.Color.WHITE; + +/** + * A text rendering surface in 3D space that displays a grid of characters. + * + *

{@code TextCanvas} extends {@link TexturedRectangle} and renders a 2D grid of + * characters (rows and columns) onto a texture-mapped rectangle. Each character cell + * supports independent foreground and background colors.

+ * + *

Characters are rendered using a monospace font at a fixed cell size + * ({@value #FONT_CHAR_WIDTH_TEXTURE_PIXELS} x {@value #FONT_CHAR_HEIGHT_TEXTURE_PIXELS} + * texture pixels per character). The texture is a signed distance field (SDF): + * the mask stores per-texel distance to the nearest glyph edge (generated once + * per character by {@link SdfGlyphCache} and stamped per cell), while the + * primary bitmap and a separate foreground layer carry the cell colors. The + * rasterizer re-derives glyph edges per screen pixel from the distance field, + * so text stays sharp at any zoom and any angle from a single render path.

+ * + *

Usage example

+ *
{@code
+ * Transform location = new Transform(new Point3D(0, 0, 500));
+ * TextCanvas canvas = new TextCanvas(location, "Hello, World!",
+ *         Color.WHITE, Color.BLACK);
+ * shapeCollection.addShape(canvas);
+ *
+ * // Or create a blank canvas and write to it
+ * TextCanvas blank = new TextCanvas(location, new TextPointer(10, 40),
+ *         Color.GREEN, Color.BLACK);
+ * blank.locate(0, 0);
+ * blank.print("Line 1");
+ * blank.locate(1, 0);
+ * blank.print("Line 2");
+ * }
+ * + * @see SdfGlyphCache + * @see TexturedRectangle + */ +public class TextCanvas extends TexturedRectangle { + + /** + * Font character width in world coordinates. + */ + public static final int FONT_CHAR_WIDTH = 8; + + /** + * Font character height in world coordinates. + */ + public static final int FONT_CHAR_HEIGHT = 16; + + /** + * Font character width in texture pixels. + */ + public static final int FONT_CHAR_WIDTH_TEXTURE_PIXELS = 16; + + /** + * Font character height in texture pixels. + */ + public static final int FONT_CHAR_HEIGHT_TEXTURE_PIXELS = 32; + + private final TextPointer size; + private final TextPointer cursorLocation = new TextPointer(); + private Color backgroundColor = BLACK; + private Color foregroundColor = WHITE; + + /** + * Creates a text canvas initialized with the given text string. + * + *

The canvas dimensions are automatically computed from the text content + * (number of lines determines rows, the longest line determines columns).

+ * + * @param location the 3D transform positioning this canvas in the scene + * @param text the initial text content (may contain newlines for multiple rows) + * @param foregroundColor the default text color + * @param backgroundColor the default background color + */ + public TextCanvas(final Transform location, final String text, + final Color foregroundColor, final Color backgroundColor) { + this(location, getTextDimensions(text), foregroundColor, + backgroundColor); + setText(text); + } + + /** + * Creates a blank text canvas with the specified dimensions. + * + *

The canvas is initialized with spaces in every cell, filled with the + * specified background color. Characters can be written using + * {@link #putChar(char)}, {@link #print(String)}, or {@link #setText(String)}.

+ * + * @param dimensions the grid size as a {@link TextPointer} where + * {@code row} is the number of rows and {@code column} is the number of columns + * @param location the 3D transform positioning this canvas in the scene + * @param foregroundColor the default text color + * @param backgroundColor the default background color + */ + public TextCanvas(final Transform location, final TextPointer dimensions, + final Color foregroundColor, final Color backgroundColor) { + super(location); + + size = dimensions; + final int columns = dimensions.column; + final int rows = dimensions.row; + + this.backgroundColor = backgroundColor; + this.foregroundColor = foregroundColor; + + // initialize underlying textured rectangle + initialize( + columns * FONT_CHAR_WIDTH, + rows * FONT_CHAR_HEIGHT, + columns * FONT_CHAR_WIDTH_TEXTURE_PIXELS, + rows * FONT_CHAR_HEIGHT_TEXTURE_PIXELS, + 0); + + // SDF layers: the distance mask carries glyph SHAPES (bilinear + // sampled, footprint-scaled coverage at render time), the primary + // bitmap is the background color layer and sdfForeground the + // hard ink color layer. + getTexture().primaryBitmap.fillColor(backgroundColor); + + final Texture texture = getTexture(); + texture.sdfMask = new TextureBitmap( + columns * FONT_CHAR_WIDTH_TEXTURE_PIXELS, + rows * FONT_CHAR_HEIGHT_TEXTURE_PIXELS, 1); + texture.sdfMask.fillColor(WHITE); // 255 = far outside any glyph + texture.sdfForeground = new TextureBitmap( + columns * FONT_CHAR_WIDTH_TEXTURE_PIXELS, + rows * FONT_CHAR_HEIGHT_TEXTURE_PIXELS, 1); + texture.sdfForeground.fillColor(foregroundColor); + texture.sdfSpreadTexels = SdfGlyphCache.SPREAD_TEXELS; + } + + /** + * Computes the row and column dimensions needed to fit the given text. + * + * @param text the text content (may contain newlines) + * @return a {@link TextPointer} where {@code row} is the number of lines and + * {@code column} is the length of the longest line + */ + public static TextPointer getTextDimensions(final String text) { + + final BufferedReader reader = new BufferedReader(new StringReader(text)); + + int rows = 0; + int columns = 0; + + while (true) { + final String line; + try { + line = reader.readLine(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (line == null) + return new TextPointer(rows, columns); + + rows++; + columns = Math.max(columns, line.length()); + } + } + + /** + * Clears the entire canvas, resetting all characters to spaces with the default colors. + * + *

The SDF mask and both color layers are reset.

+ */ + public void clear() { + getTexture().primaryBitmap.fillColor(backgroundColor); + getTexture().sdfMask.fillColor(WHITE); + getTexture().sdfForeground.fillColor(foregroundColor); + } + + private void drawCharToTexture(final int row, final int column, + final char character, final Color foreground) { + final Texture texture = getTexture(); + final int px = column * FONT_CHAR_WIDTH_TEXTURE_PIXELS; + final int py = row * FONT_CHAR_HEIGHT_TEXTURE_PIXELS; + + // Background and ink color layers: hard per-cell fills (sampled + // nearest; only where the SDF coverage selects them). + texture.primaryBitmap.drawRectangle(px, py, + px + FONT_CHAR_WIDTH_TEXTURE_PIXELS, + py + FONT_CHAR_HEIGHT_TEXTURE_PIXELS, backgroundColor); + texture.sdfForeground.drawRectangle(px, py, + px + FONT_CHAR_WIDTH_TEXTURE_PIXELS, + py + FONT_CHAR_HEIGHT_TEXTURE_PIXELS, foreground); + + // Stamp the cached glyph distance field into the mask. + final int[] glyph = SdfGlyphCache.glyphMask(character); + final int[] dst = texture.sdfMask.pixels; + final int maskWidth = texture.sdfMask.width; + for (int gy = 0; gy < FONT_CHAR_HEIGHT_TEXTURE_PIXELS; gy++) { + System.arraycopy(glyph, gy * FONT_CHAR_WIDTH_TEXTURE_PIXELS, + dst, (py + gy) * maskWidth + px, FONT_CHAR_WIDTH_TEXTURE_PIXELS); + } + } + + /** + * Returns the dimensions of this text canvas. + * + * @return a {@link TextPointer} where {@code row} is the number of rows + * and {@code column} is the number of columns + */ + public TextPointer getSize() { + return size; + } + + /** + * Moves the internal cursor to the specified row and column. + * + *

Subsequent calls to {@link #putChar(char)} and {@link #print(String)} will + * begin writing at this position.

+ * + * @param row the target row (0-based) + * @param column the target column (0-based) + */ + public void locate(final int row, final int column) { + cursorLocation.row = row; + cursorLocation.column = column; + } + + /** + * Prints a string starting at the current cursor location, advancing the cursor after each character. + * + *

When the cursor reaches the end of a row, it wraps to the beginning of the next row.

+ * + * @param text the text to print + * @see #locate(int, int) + */ + public void print(final String text) { + for (int i = 0; i < text.length(); i++) + putChar(text.charAt(i)); + } + + /** + * Writes a character at the current cursor location and advances the cursor. + * + *

The cursor moves one column to the right. If it exceeds the row width, + * it wraps to column 0 of the next row.

+ * + * @param character the character to write + */ + public void putChar(final char character) { + putChar(cursorLocation, character); + + cursorLocation.column++; + if (cursorLocation.column >= size.column) { + cursorLocation.column = 0; + cursorLocation.row++; + } + } + + /** + * Writes a character at the specified row and column using the current foreground and background colors. + * + *

If the row or column is out of bounds, the call is silently ignored.

+ * + * @param row the row index (0-based) + * @param column the column index (0-based) + * @param character the character to write + */ + public void putChar(final int row, final int column, final char character) { + if (row < 0 || row >= size.row || column < 0 || column >= size.column) + return; + + drawCharToTexture(row, column, character, foregroundColor); + } + + /** + * Writes a character at the position specified by a {@link TextPointer}. + * + * @param location the row and column position + * @param character the character to write + */ + public void putChar(final TextPointer location, final char character) { + putChar(location.row, location.column, character); + } + + /** + * Sets the default background color for subsequent character writes. + * + * @param backgroundColor the new background color + */ + public void setBackgroundColor( + final eu.svjatoslav.aukio.e3d.renderer.raster.Color backgroundColor) { + this.backgroundColor = backgroundColor; + } + + /** + * Sets the default foreground (text) color for subsequent character writes. + * + * @param foregroundColor the new foreground color + */ + public void setForegroundColor( + final eu.svjatoslav.aukio.e3d.renderer.raster.Color foregroundColor) { + this.foregroundColor = foregroundColor; + } + + /** + * Replaces the entire canvas content with the given multi-line text string. + * + *

Each line of text (separated by newlines) is written to consecutive rows, + * starting from row 0. Characters beyond the canvas width are ignored.

+ * + * @param text the text to display (may contain newline characters) + */ + public void setText(final String text) { + final BufferedReader reader = new BufferedReader(new StringReader(text)); + + int row = 0; + + while (true) { + final String line; + try { + line = reader.readLine(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + if (line == null) + return; + + int column = 0; + for (int i = 0; i < line.length(); i++) { + putChar(row, column, line.charAt(i)); + column++; + } + row++; + } + } + + /** + * Sets the foreground color of the whole canvas. + * + *

Fills the SDF ink color layer; cell shapes are untouched, so + * text content and background colors are preserved.

+ * + * @param color the new foreground color + */ + public void setTextColor(final Color color) { + getTexture().sdfForeground.fillColor(color); + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/package-info.java new file mode 100644 index 0000000..1e8d0f3 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/textcanvas/package-info.java @@ -0,0 +1,9 @@ +/** + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + *

+ * + * Text canvas is a 2D canvas that can be used to render text. + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.textcanvas; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/Grid2D.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/Grid2D.java new file mode 100644 index 0000000..8f46cc3 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/Grid2D.java @@ -0,0 +1,80 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.geometry.Rectangle; +import eu.svjatoslav.aukio.e3d.math.Transform; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A 2D grid of line segments lying in the XY plane (Z = 0 in local space). + * The grid is divided into configurable numbers of cells along the X and Y axes, + * producing a regular rectangular mesh of lines. + * + *

This shape is useful for rendering floors, walls, reference planes, or any + * flat surface that needs a grid overlay. The grid is positioned and oriented + * in world space using a {@link Transform}.

+ * + *

Usage example:

+ *
{@code
+ * Transform transform = new Transform(new Point3D(0, 100, 0));
+ * Rectangle rect = new Rectangle(new Point2D(-500, -500), new Point2D(500, 500));
+ * LineAppearance appearance = new LineAppearance(1, Color.GRAY);
+ * Grid2D grid = new Grid2D(transform, rect, 10, 10, appearance);
+ * shapeCollection.addShape(grid);
+ * }
+ * + * @see Grid3D + * @see LineAppearance + * @see AbstractCompositeShape + */ +public class Grid2D extends AbstractCompositeShape { + + /** + * Constructs a 2D grid in the XY plane with the specified dimensions and + * number of divisions. + * + * @param transform the transform defining the grid's position and orientation + * in world space + * @param rectangle the rectangular dimensions of the grid in local XY space + * @param xDivisionCount the number of divisions (cells) along the X axis; + * produces {@code xDivisionCount + 1} vertical lines + * @param yDivisionCount the number of divisions (cells) along the Y axis; + * produces {@code yDivisionCount + 1} horizontal lines + * @param appearance the line appearance (color, width) used for all grid lines + */ + public Grid2D(final Transform transform, final Rectangle rectangle, + final int xDivisionCount, final int yDivisionCount, + final LineAppearance appearance) { + + super(transform); + + final double stepY = rectangle.getHeight() / yDivisionCount; + final double stepX = rectangle.getWidth() / xDivisionCount; + + for (int ySlice = 0; ySlice <= yDivisionCount; ySlice++) { + final double y = (ySlice * stepY) + rectangle.getLowerY(); + + for (int xSlice = 0; xSlice <= xDivisionCount; xSlice++) { + final double x = (xSlice * stepX) + rectangle.getLowerX(); + + final Point3D p1 = new Point3D(x, y, 0); + final Point3D p2 = new Point3D(x + stepX, y, 0); + final Point3D p3 = new Point3D(x, y + stepY, 0); + + if (xSlice < xDivisionCount) + addShape(appearance.getLine(p1, p2)); + + if (ySlice < yDivisionCount) + addShape(appearance.getLine(p1, p3)); + } + + } + + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/Grid3D.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/Grid3D.java new file mode 100755 index 0000000..5adff7b --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/Grid3D.java @@ -0,0 +1,87 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A 3D grid of line segments filling a rectangular volume defined by two + * diagonally opposite corner points. Lines run along all three axes (X, Y, and Z) + * at regular intervals determined by the step size. + * + *

At each grid intersection point, up to three line segments are created + * (one along each axis), forming a three-dimensional lattice.

+ * + *

This shape is useful for visualizing 3D space, voxel boundaries, or + * spatial reference grids in a scene.

+ * + *

Usage example:

+ *
{@code
+ * LineAppearance appearance = new LineAppearance(1, Color.GRAY);
+ * Point3D cornerA = new Point3D(-100, -100, -100);
+ * Point3D cornerB = new Point3D(100, 100, 100);
+ * Grid3D grid = new Grid3D(cornerA, cornerB, 50, appearance);
+ * shapeCollection.addShape(grid);
+ * }
+ * + * @see Grid2D + * @see LineAppearance + * @see AbstractCompositeShape + */ +public class Grid3D extends AbstractCompositeShape { + + /** + * Constructs a 3D grid filling the volume between two diagonally opposite + * corner points. + * + *

The corner points do not need to be in any particular min/max order; + * the constructor automatically normalizes them so that grid generation + * always proceeds from minimum to maximum coordinates.

+ * + * @param cornerA the first corner point defining the volume + * @param cornerB the diagonally opposite corner point + * @param step the spacing between grid lines along each axis; must be positive + * @param appearance the line appearance (color, width) used for all grid lines + */ + public Grid3D(final Point3D cornerA, final Point3D cornerB, final double step, + final LineAppearance appearance) { + + super(); + + // Determine actual min/max bounds (corners may be in any order) + final double minX = Math.min(cornerA.x, cornerB.x); + final double maxX = Math.max(cornerA.x, cornerB.x); + final double minY = Math.min(cornerA.y, cornerB.y); + final double maxY = Math.max(cornerA.y, cornerB.y); + final double minZ = Math.min(cornerA.z, cornerB.z); + final double maxZ = Math.max(cornerA.z, cornerB.z); + + for (double x = minX; x <= maxX; x += step) { + for (double y = minY; y <= maxY; y += step) { + for (double z = minZ; z <= maxZ; z += step) { + + final Point3D p = new Point3D(x, y, z); + + // Line along X axis + if ((x + step) <= maxX) { + addShape(appearance.getLine(p, new Point3D(x + step, y, z))); + } + + // Line along Y axis + if ((y + step) <= maxY) { + addShape(appearance.getLine(p, new Point3D(x, y + step, z))); + } + + // Line along Z axis + if ((z + step) <= maxZ) { + addShape(appearance.getLine(p, new Point3D(x, y, z + step))); + } + } + } + } + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeArrow.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeArrow.java new file mode 100644 index 0000000..0d4cca5 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeArrow.java @@ -0,0 +1,321 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.math.Matrix3x3; +import eu.svjatoslav.aukio.e3d.math.Quaternion; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A 3D wireframe arrow shape composed of a cylindrical body and a conical tip. + * + *

The arrow points from a start point to an end point, with the tip + * located at the end point. The wireframe consists of:

+ *
    + *
  • Body: Two circular rings connected by lines between corresponding vertices
  • + *
  • Tip: A circular ring at the cone base with lines to the apex
  • + *
+ * + *

Usage example:

+ *
{@code
+ * // Create a red arrow pointing from origin to (100, -50, 200)
+ * LineAppearance appearance = new LineAppearance(2, Color.RED);
+ * WireframeArrow arrow = new WireframeArrow(
+ *     new Point3D(0, 0, 0),      // start point
+ *     new Point3D(100, -50, 200), // end point
+ *     8,                         // body radius
+ *     20,                        // tip radius
+ *     40,                        // tip length
+ *     16,                        // segments
+ *     appearance
+ * );
+ * shapeCollection.addShape(arrow);
+ * }
+ * + * @see WireframeCone + * @see WireframeCylinder + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonArrow + */ +public class WireframeArrow extends AbstractCompositeShape { + +/** + * Default number of segments for arrow smoothness. + */ +private static final int DEFAULT_SEGMENTS = 12; + +/** + * Default tip radius as a fraction of body radius (2.5x). + */ +private static final double TIP_RADIUS_FACTOR = 2.5; + +/** + * Default tip length as a fraction of body radius (5.0x). + */ +private static final double TIP_LENGTH_FACTOR = 5.0; + +/** + * Constructs a 3D wireframe arrow pointing from start to end with sensible defaults. + * + *

This simplified constructor automatically calculates the tip radius as + * 2.5 times the body radius, the tip length as 5 times the body radius, and + * uses 12 segments for smoothness. For custom tip dimensions or segment count, + * use the full constructor.

+ * + * @param startPoint the origin point of the arrow (where the body starts) + * @param endPoint the destination point of the arrow (where the tip points to) + * @param bodyRadius the radius of the cylindrical body; tip dimensions are + * calculated automatically from this value + * @param appearance the line appearance (color, width) used for all lines + */ +public WireframeArrow(final Point3D startPoint, final Point3D endPoint, + final double bodyRadius, final LineAppearance appearance) { + this(startPoint, endPoint, bodyRadius, + bodyRadius * TIP_RADIUS_FACTOR, + bodyRadius * TIP_LENGTH_FACTOR, + DEFAULT_SEGMENTS, appearance); +} + +/** + * Constructs a 3D wireframe arrow pointing from start to end with full control over all dimensions. + * + *

The arrow consists of a cylindrical body extending from the start point + * towards the end, and a conical tip at the end point. If the distance between + * start and end is less than or equal to the tip length, only the cone tip + * is rendered.

+ * + * @param startPoint the origin point of the arrow (where the body starts) + * @param endPoint the destination point of the arrow (where the tip points to) + * @param bodyRadius the radius of the cylindrical body + * @param tipRadius the radius of the cone base at the tip + * @param tipLength the length of the conical tip + * @param segments the number of segments for cylinder and cone smoothness. + * Higher values create smoother arrows. Minimum is 3. + * @param appearance the line appearance (color, width) used for all lines + */ +public WireframeArrow(final Point3D startPoint, final Point3D endPoint, + final double bodyRadius, final double tipRadius, + final double tipLength, final int segments, + final LineAppearance appearance) { + super(); + + // Calculate direction and distance + final double dx = endPoint.x - startPoint.x; + final double dy = endPoint.y - startPoint.y; + final double dz = endPoint.z - startPoint.z; + final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + // Handle degenerate case: start and end are the same point + if (distance < 0.001) { + return; + } + + // Normalize direction vector + final double nx = dx / distance; + final double ny = dy / distance; + final double nz = dz / distance; + + // Calculate rotation to align Y-axis with direction + // Default arrow points in -Y direction (apex at lower Y) + // We need to rotate from (0, -1, 0) to (nx, ny, nz) + final Quaternion rotation = createRotationFromYAxis(nx, ny, nz); + final Matrix3x3 rotMatrix = rotation.toMatrix(); + + // Calculate body length (distance minus tip) + final double bodyLength = Math.max(0, distance - tipLength); + + // Build the arrow components + if (bodyLength > 0) { + addCylinderBody(startPoint, bodyRadius, bodyLength, segments, appearance, rotMatrix, nx, ny, nz); + } + addConeTip(endPoint, tipRadius, tipLength, segments, appearance, rotMatrix, nx, ny, nz); + } + + /** + * Creates a quaternion that rotates from the -Y axis to the given direction. + * + *

The arrow by default points in the -Y direction. This method computes + * the rotation needed to align the arrow with the target direction vector.

+ * + * @param nx normalized direction X component + * @param ny normalized direction Y component + * @param nz normalized direction Z component + * @return quaternion representing the rotation + */ + private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) { + // Default direction is -Y (0, -1, 0) + // Target direction is (nx, ny, nz) + // Dot product: 0*nx + (-1)*ny + 0*nz = -ny + final double dot = -ny; + + // Check for parallel vectors + if (dot > 0.9999) { + // Direction is nearly -Y, no rotation needed + return Quaternion.identity(); + } + if (dot < -0.9999) { + // Direction is nearly +Y, rotate 180° around X axis + return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI); + } + + // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx) + // This gives the rotation axis + final double axisX = -nz; + final double axisY = 0; + final double axisZ = nx; + final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + final double normalizedAxisX = axisX / axisLength; + final double normalizedAxisZ = axisZ / axisLength; + + // Angle from dot product + final double angle = Math.acos(dot); + + return Quaternion.fromAxisAngle( + new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle); + } + + /** + * Adds the cylindrical body of the arrow. + * + *

Local coordinate system: The arrow points in -Y direction in local space. + * Therefore, local -Y is toward the tip (front), and local +Y is toward the start (back).

+ * + * @param startPoint the origin of the arrow body + * @param radius the radius of the cylinder + * @param length the length of the cylinder + * @param segments the number of segments around the circumference + * @param appearance the line appearance + * @param rotMatrix the rotation matrix to apply + * @param dirX direction X component (for translation calculation) + * @param dirY direction Y component + * @param dirZ direction Z component + */ + private void addCylinderBody(final Point3D startPoint, final double radius, + final double length, final int segments, + final LineAppearance appearance, final Matrix3x3 rotMatrix, + final double dirX, final double dirY, final double dirZ) { + // Cylinder center is at startPoint + (length/2) * direction + final double centerX = startPoint.x + (length / 2.0) * dirX; + final double centerY = startPoint.y + (length / 2.0) * dirY; + final double centerZ = startPoint.z + (length / 2.0) * dirZ; + + // Generate ring vertices in local space, then rotate and translate + // Arrow points in -Y direction, so: + // - tipSideRing is at local -Y (toward arrow tip, front of cylinder) + // - startSideRing is at local +Y (toward arrow start, back of cylinder) + final Point3D[] tipSideRing = new Point3D[segments]; + final Point3D[] startSideRing = new Point3D[segments]; + + final double halfLength = length / 2.0; + + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double localX = radius * Math.cos(angle); + final double localZ = radius * Math.sin(angle); + + // Tip-side ring (at -halfLength in local Y = toward arrow tip) + final Point3D tipSideLocal = new Point3D(localX, -halfLength, localZ); + rotMatrix.transform(tipSideLocal, tipSideLocal); + tipSideLocal.x += centerX; + tipSideLocal.y += centerY; + tipSideLocal.z += centerZ; + tipSideRing[i] = tipSideLocal; + + // Start-side ring (at +halfLength in local Y = toward arrow start) + final Point3D startSideLocal = new Point3D(localX, halfLength, localZ); + rotMatrix.transform(startSideLocal, startSideLocal); + startSideLocal.x += centerX; + startSideLocal.y += centerY; + startSideLocal.z += centerZ; + startSideRing[i] = startSideLocal; + } + + // Create the circular rings + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + + // Tip-side ring line segment + addShape(appearance.getLine( + new Point3D(tipSideRing[i].x, tipSideRing[i].y, tipSideRing[i].z), + new Point3D(tipSideRing[next].x, tipSideRing[next].y, tipSideRing[next].z))); + + // Start-side ring line segment + addShape(appearance.getLine( + new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z), + new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z))); + } + + // Create vertical lines connecting the two rings + for (int i = 0; i < segments; i++) { + addShape(appearance.getLine( + new Point3D(tipSideRing[i].x, tipSideRing[i].y, tipSideRing[i].z), + new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z))); + } + } + + /** + * Adds the conical tip of the arrow. + * + *

Local coordinate system: In local space, the cone points in -Y direction + * (apex at lower Y). The base ring is at Y=0, and the apex is at Y=-length.

+ * + * @param endPoint the position of the arrow tip (cone apex) + * @param radius the radius of the cone base + * @param length the length of the cone + * @param segments the number of segments around the circumference + * @param appearance the line appearance + * @param rotMatrix the rotation matrix to apply + * @param dirX direction X component + * @param dirY direction Y component + * @param dirZ direction Z component + */ + private void addConeTip(final Point3D endPoint, final double radius, + final double length, final int segments, + final LineAppearance appearance, final Matrix3x3 rotMatrix, + final double dirX, final double dirY, final double dirZ) { + // Apex is at endPoint (the arrow tip) + // Base center is at endPoint - length * direction (toward arrow start) + final double baseCenterX = endPoint.x - length * dirX; + final double baseCenterY = endPoint.y - length * dirY; + final double baseCenterZ = endPoint.z - length * dirZ; + + // Generate base ring vertices + // In local space, cone points in -Y direction, so base is at Y=0 + final Point3D[] baseRing = new Point3D[segments]; + + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double localX = radius * Math.cos(angle); + final double localZ = radius * Math.sin(angle); + + // Base ring vertices at local Y=0 + final Point3D local = new Point3D(localX, 0, localZ); + rotMatrix.transform(local, local); + local.x += baseCenterX; + local.y += baseCenterY; + local.z += baseCenterZ; + baseRing[i] = local; + } + + // Apex point (the arrow tip) + final Point3D apex = new Point3D(endPoint.x, endPoint.y, endPoint.z); + + // Create the circular base ring + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + addShape(appearance.getLine( + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z), + new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z))); + } + + // Create lines from apex to each base vertex + for (int i = 0; i < segments; i++) { + addShape(appearance.getLine( + new Point3D(apex.x, apex.y, apex.z), + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z))); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java new file mode 100755 index 0000000..a4cc4b7 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java @@ -0,0 +1,104 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.aukio.e3d.geometry.Box; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A wireframe box (rectangular parallelepiped) composed of 12 line segments + * representing the edges of the box. The box is axis-aligned, defined by two + * diagonally opposite corner points. + * + *

The wireframe consists of four edges along each axis: four edges parallel + * to X, four parallel to Y, and four parallel to Z.

+ * + *

Vertex layout:

+ *
+ *         cornerB (max) ────────┐
+ *              /│              /│
+ *             / │             / │
+ *            /  │            /  │
+ *           ┌───┼───────────┐   │
+ *           │   │           │   │
+ *           │   │           │   │
+ *           │   └───────────│───┘
+ *           │  /            │  /
+ *           │ /             │ /
+ *           │/              │/
+ *           └───────────────┘ cornerA (min)
+ * 
+ * + *

Usage example:

+ *
{@code
+ * LineAppearance appearance = new LineAppearance(2, Color.GREEN);
+ * Point3D cornerA = new Point3D(-50, -50, -50);
+ * Point3D cornerB = new Point3D(50, 50, 50);
+ * WireframeBox box = new WireframeBox(cornerA, cornerB, appearance);
+ * shapeCollection.addShape(box);
+ * }
+ * + * @see WireframeCube + * @see Box + * @see LineAppearance + * @see AbstractCompositeShape + */ +public class WireframeBox extends AbstractCompositeShape { + + /** + * Constructs a wireframe box from a {@link Box} geometry object. + * + * @param box the axis-aligned box defining the two opposite corners + * @param appearance the line appearance (color, width) used for all 12 edges + */ + public WireframeBox(final Box box, + final LineAppearance appearance) { + + this(box.p1, box.p2, appearance); + } + + /** + * Constructs a wireframe box from two diagonally opposite corner points. + * The corners do not need to be in any particular min/max order; the constructor + * uses each coordinate independently to form all eight vertices of the box. + * + * @param cornerA the first corner point of the box + * @param cornerB the diagonally opposite corner point of the box + * @param appearance the line appearance (color, width) used for all 12 edges + */ + public WireframeBox(final Point3D cornerA, final Point3D cornerB, + final LineAppearance appearance) { + super(); + + // Determine actual min/max bounds (corners may be in any order) + final double minX = Math.min(cornerA.x, cornerB.x); + final double maxX = Math.max(cornerA.x, cornerB.x); + final double minY = Math.min(cornerA.y, cornerB.y); + final double maxY = Math.max(cornerA.y, cornerB.y); + final double minZ = Math.min(cornerA.z, cornerB.z); + final double maxZ = Math.max(cornerA.z, cornerB.z); + + // Generate the 12 edges of the box + // Four edges along X axis (varying X, fixed Y and Z) + addShape(appearance.getLine(new Point3D(minX, minY, minZ), new Point3D(maxX, minY, minZ))); + addShape(appearance.getLine(new Point3D(minX, maxY, minZ), new Point3D(maxX, maxY, minZ))); + addShape(appearance.getLine(new Point3D(minX, minY, maxZ), new Point3D(maxX, minY, maxZ))); + addShape(appearance.getLine(new Point3D(minX, maxY, maxZ), new Point3D(maxX, maxY, maxZ))); + + // Four edges along Y axis (varying Y, fixed X and Z) + addShape(appearance.getLine(new Point3D(minX, minY, minZ), new Point3D(minX, maxY, minZ))); + addShape(appearance.getLine(new Point3D(maxX, minY, minZ), new Point3D(maxX, maxY, minZ))); + addShape(appearance.getLine(new Point3D(minX, minY, maxZ), new Point3D(minX, maxY, maxZ))); + addShape(appearance.getLine(new Point3D(maxX, minY, maxZ), new Point3D(maxX, maxY, maxZ))); + + // Four edges along Z axis (varying Z, fixed X and Y) + addShape(appearance.getLine(new Point3D(minX, minY, minZ), new Point3D(minX, minY, maxZ))); + addShape(appearance.getLine(new Point3D(maxX, minY, minZ), new Point3D(maxX, minY, maxZ))); + addShape(appearance.getLine(new Point3D(minX, maxY, minZ), new Point3D(minX, maxY, maxZ))); + addShape(appearance.getLine(new Point3D(maxX, maxY, minZ), new Point3D(maxX, maxY, maxZ))); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCone.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCone.java new file mode 100644 index 0000000..9945e65 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCone.java @@ -0,0 +1,247 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.math.Matrix3x3; +import eu.svjatoslav.aukio.e3d.math.Quaternion; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A wireframe cone that can be oriented in any direction. + * + *

The cone has a circular base and a single apex (tip) point. The wireframe + * consists of:

+ *
    + *
  • A circular ring at the base
  • + *
  • Lines from each base vertex to the apex
  • + *
+ * + *

Two constructors are provided for different use cases:

+ * + *
    + *
  • Directional (recommended): Specify apex point and base center point. + * The cone points from apex toward the base center. This allows arbitrary + * orientation and is the most intuitive API.
  • + *
  • Y-axis aligned: Specify base center, radius, and height. The cone + * points in -Y direction (apex at lower Y). Useful for simple vertical cones.
  • + *
+ * + *

Usage examples:

+ *
{@code
+ * // Directional constructor: cone pointing from apex toward base
+ * LineAppearance appearance = new LineAppearance(2, Color.RED);
+ * WireframeCone directionalCone = new WireframeCone(
+ *     new Point3D(0, -100, 0),   // apex (tip of the cone)
+ *     new Point3D(0, 50, 0),     // baseCenter (cone points toward this)
+ *     50,                        // radius of the circular base
+ *     16,                        // segments
+ *     appearance
+ * );
+ *
+ * // Y-axis aligned constructor: cone pointing upward
+ * WireframeCone verticalCone = new WireframeCone(
+ *     new Point3D(0, 0, 300),    // baseCenter
+ *     50,                        // radius
+ *     100,                       // height
+ *     16,                        // segments
+ *     appearance
+ * );
+ * }
+ * + * @see WireframeCylinder + * @see WireframeArrow + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCone + */ +public class WireframeCone extends AbstractCompositeShape { + + /** + * Constructs a wireframe cone pointing from apex toward base center. + * + *

This is the recommended constructor for placing cones in 3D space. + * The cone's apex (tip) is at {@code apexPoint}, and the circular base + * is centered at {@code baseCenterPoint}. The cone points in the direction + * from apex to base center.

+ * + *

Coordinate interpretation:

+ *
    + *
  • {@code apexPoint} - the sharp tip of the cone
  • + *
  • {@code baseCenterPoint} - the center of the circular base; the cone + * "points" in this direction from the apex
  • + *
  • The distance between apex and base center determines the cone height
  • + *
+ * + * @param apexPoint the position of the cone's tip (apex) + * @param baseCenterPoint the center point of the circular base; the cone + * points from apex toward this point + * @param radius the radius of the circular base + * @param segments the number of segments around the circumference. + * Higher values create smoother cones. Minimum is 3. + * @param appearance the line appearance (color, width) used for all lines + */ + public WireframeCone(final Point3D apexPoint, final Point3D baseCenterPoint, + final double radius, final int segments, + final LineAppearance appearance) { + super(); + + // Calculate direction and height from apex to base center + final double dx = baseCenterPoint.x - apexPoint.x; + final double dy = baseCenterPoint.y - apexPoint.y; + final double dz = baseCenterPoint.z - apexPoint.z; + final double height = Math.sqrt(dx * dx + dy * dy + dz * dz); + + // Handle degenerate case: apex and base center are the same point + if (height < 0.001) { + return; + } + + // Normalize direction vector (from apex toward base) + final double nx = dx / height; + final double ny = dy / height; + final double nz = dz / height; + + // Calculate rotation to align Y-axis with direction + // Default cone points in -Y direction (apex at origin, base at -Y) + // We need to rotate from (0, -1, 0) to (nx, ny, nz) + final Quaternion rotation = createRotationFromYAxis(nx, ny, nz); + final Matrix3x3 rotMatrix = rotation.toMatrix(); + + // Generate base ring vertices in local space, then rotate and translate + // In local space: apex is at origin, base is at Y = -height + // (cone points in -Y direction in local space) + final Point3D[] baseRing = new Point3D[segments]; + + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double localX = radius * Math.cos(angle); + final double localZ = radius * Math.sin(angle); + + // Base ring vertex in local space (Y = -height) + final Point3D local = new Point3D(localX, -height, localZ); + rotMatrix.transform(local, local); + local.x += apexPoint.x; + local.y += apexPoint.y; + local.z += apexPoint.z; + baseRing[i] = local; + } + + // Apex point (the cone tip) + final Point3D apex = new Point3D(apexPoint.x, apexPoint.y, apexPoint.z); + + // Create the circular base ring + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + addShape(appearance.getLine( + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z), + new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z))); + } + + // Create lines from apex to each base vertex + for (int i = 0; i < segments; i++) { + addShape(appearance.getLine( + new Point3D(apex.x, apex.y, apex.z), + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z))); + } + } + + /** + * Constructs a wireframe cone with circular base centered at the given point, + * pointing in the -Y direction. + * + *

This constructor creates a Y-axis aligned cone. The apex is positioned + * at {@code baseCenter.y - height} (above the base in the negative Y direction). + * For cones pointing in arbitrary directions, use + * {@link #WireframeCone(Point3D, Point3D, double, int, LineAppearance)} instead.

+ * + *

Coordinate system: The cone points in -Y direction (apex at lower Y). + * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height. + * In Aukio 3D's coordinate system, "up" visually is negative Y.

+ * + * @param baseCenter the center point of the cone's circular base in 3D space + * @param radius the radius of the circular base + * @param height the height of the cone from base center to apex + * @param segments the number of segments around the circumference. + * Higher values create smoother cones. Minimum is 3. + * @param appearance the line appearance (color, width) used for all lines + */ + public WireframeCone(final Point3D baseCenter, final double radius, + final double height, final int segments, + final LineAppearance appearance) { + super(); + + // Apex is above the base (negative Y direction in this coordinate system) + final double apexY = baseCenter.y - height; + final Point3D apex = new Point3D(baseCenter.x, apexY, baseCenter.z); + + // Generate vertices around the circular base + final Point3D[] baseRing = new Point3D[segments]; + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double x = baseCenter.x + radius * Math.cos(angle); + final double z = baseCenter.z + radius * Math.sin(angle); + baseRing[i] = new Point3D(x, baseCenter.y, z); + } + + // Create the circular base ring + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + addShape(appearance.getLine( + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z), + new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z))); + } + + // Create lines from apex to each base vertex + for (int i = 0; i < segments; i++) { + addShape(appearance.getLine( + new Point3D(apex.x, apex.y, apex.z), + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z))); + } + } + + /** + * Creates a quaternion that rotates from the -Y axis to the given direction. + * + *

The cone by default points in the -Y direction (apex at origin, base at -Y). + * This method computes the rotation needed to align the cone with the target + * direction vector.

+ * + * @param nx normalized direction X component + * @param ny normalized direction Y component + * @param nz normalized direction Z component + * @return quaternion representing the rotation + */ + private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) { + // Default direction is -Y (0, -1, 0) + // Target direction is (nx, ny, nz) + // Dot product: 0*nx + (-1)*ny + 0*nz = -ny + final double dot = -ny; + + // Check for parallel vectors + if (dot > 0.9999) { + // Direction is nearly -Y, no rotation needed + return Quaternion.identity(); + } + if (dot < -0.9999) { + // Direction is nearly +Y, rotate 180° around X axis + return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI); + } + + // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx) + // This gives the rotation axis + final double axisX = -nz; + final double axisY = 0; + final double axisZ = nx; + final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + final double normalizedAxisX = axisX / axisLength; + final double normalizedAxisZ = axisZ / axisLength; + + // Angle from dot product + final double angle = Math.acos(dot); + + return Quaternion.fromAxisAngle( + new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.java new file mode 100755 index 0000000..7bbbd3f --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCube.java @@ -0,0 +1,45 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; + +/** + * A wireframe cube (equal-length sides) centered at a given point in 3D space. + * This is a convenience subclass of {@link WireframeBox} that constructs an + * axis-aligned cube from a center point and a half-side length. + * + *

The cube extends {@code size} units in each direction from the center, + * resulting in a total edge length of {@code 2 * size}.

+ * + *

Usage example:

+ *
{@code
+ * LineAppearance appearance = new LineAppearance(1, Color.CYAN);
+ * WireframeCube cube = new WireframeCube(new Point3D(0, 0, 200), 50, appearance);
+ * shapeCollection.addShape(cube);
+ * }
+ * + * @see WireframeBox + * @see LineAppearance + */ +public class WireframeCube extends WireframeBox { + + /** + * Constructs a wireframe cube centered at the given point. + * + * @param center the center point of the cube in 3D space + * @param size the half-side length; the cube extends this distance from + * the center along each axis, giving a total edge length + * of {@code 2 * size} + * @param appearance the line appearance (color, width) used for all 12 edges + */ + public WireframeCube(final Point3D center, final double size, + final LineAppearance appearance) { + super(new Point3D(center.x - size, center.y - size, center.z - size), + new Point3D(center.x + size, center.y + size, center.z + size), + appearance); + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCylinder.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCylinder.java new file mode 100644 index 0000000..30988fa --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeCylinder.java @@ -0,0 +1,188 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.math.Matrix3x3; +import eu.svjatoslav.aukio.e3d.math.Quaternion; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A wireframe cylinder defined by two end points. + * + *

The cylinder extends from startPoint to endPoint with circular rings at both + * ends. The number of segments determines the smoothness of the circular rings. + * The wireframe consists of:

+ *
    + *
  • Two circular rings at the start and end points
  • + *
  • Vertical lines connecting corresponding vertices between the rings
  • + *
+ * + *

Usage example:

+ *
{@code
+ * // Create a vertical cylinder from Y=100 to Y=200
+ * LineAppearance appearance = new LineAppearance(2, Color.RED);
+ * WireframeCylinder cylinder = new WireframeCylinder(
+ *     new Point3D(0, 100, 0),   // start point (bottom)
+ *     new Point3D(0, 200, 0),   // end point (top)
+ *     10,                        // radius
+ *     16,                        // segments
+ *     appearance
+ * );
+ *
+ * // Create a horizontal cylinder along X axis
+ * WireframeCylinder pipe = new WireframeCylinder(
+ *     new Point3D(-50, 0, 0),
+ *     new Point3D(50, 0, 0),
+ *     5, 12, appearance
+ * );
+ * }
+ * + * @see WireframeCone + * @see WireframeArrow + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCylinder + */ +public class WireframeCylinder extends AbstractCompositeShape { + + /** + * Constructs a wireframe cylinder between two end points. + * + *

The cylinder has circular rings at both startPoint and endPoint, + * connected by lines between corresponding vertices. The orientation is + * automatically calculated from the direction between the two points.

+ * + * @param startPoint the center of the first ring + * @param endPoint the center of the second ring + * @param radius the radius of the cylinder + * @param segments the number of segments around the circumference. + * Higher values create smoother cylinders. Minimum is 3. + * @param appearance the line appearance (color, width) used for all lines + */ + public WireframeCylinder(final Point3D startPoint, final Point3D endPoint, + final double radius, final int segments, + final LineAppearance appearance) { + super(); + + // Calculate direction and distance + final double dx = endPoint.x - startPoint.x; + final double dy = endPoint.y - startPoint.y; + final double dz = endPoint.z - startPoint.z; + final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + // Handle degenerate case: start and end are the same point + if (distance < 0.001) { + return; + } + + // Normalize direction vector + final double nx = dx / distance; + final double ny = dy / distance; + final double nz = dz / distance; + + // Calculate rotation to align Y-axis with direction + // Default cylinder is aligned along Y-axis + // We need to rotate from (0, 1, 0) to (nx, ny, nz) + final Quaternion rotation = createRotationFromYAxis(nx, ny, nz); + final Matrix3x3 rotMatrix = rotation.toMatrix(); + + // Cylinder center is at midpoint between start and end + final double centerX = (startPoint.x + endPoint.x) / 2.0; + final double centerY = (startPoint.y + endPoint.y) / 2.0; + final double centerZ = (startPoint.z + endPoint.z) / 2.0; + final double halfLength = distance / 2.0; + + // Generate ring vertices in local space, then rotate and translate + // In local space: cylinder is aligned along Y-axis + // - startRing is at local -Y (toward startPoint) + // - endRing is at local +Y (toward endPoint) + final Point3D[] startRing = new Point3D[segments]; + final Point3D[] endRing = new Point3D[segments]; + + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double localX = radius * Math.cos(angle); + final double localZ = radius * Math.sin(angle); + + // Start ring (at -halfLength in local Y = toward startPoint) + final Point3D startLocal = new Point3D(localX, -halfLength, localZ); + rotMatrix.transform(startLocal, startLocal); + startLocal.x += centerX; + startLocal.y += centerY; + startLocal.z += centerZ; + startRing[i] = startLocal; + + // End ring (at +halfLength in local Y = toward endPoint) + final Point3D endLocal = new Point3D(localX, halfLength, localZ); + rotMatrix.transform(endLocal, endLocal); + endLocal.x += centerX; + endLocal.y += centerY; + endLocal.z += centerZ; + endRing[i] = endLocal; + } + + // Create the circular rings + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + + // Start ring line segment + addShape(appearance.getLine( + new Point3D(startRing[i].x, startRing[i].y, startRing[i].z), + new Point3D(startRing[next].x, startRing[next].y, startRing[next].z))); + + // End ring line segment + addShape(appearance.getLine( + new Point3D(endRing[i].x, endRing[i].y, endRing[i].z), + new Point3D(endRing[next].x, endRing[next].y, endRing[next].z))); + } + + // Create vertical lines connecting the two rings + for (int i = 0; i < segments; i++) { + addShape(appearance.getLine( + new Point3D(startRing[i].x, startRing[i].y, startRing[i].z), + new Point3D(endRing[i].x, endRing[i].y, endRing[i].z))); + } + } + + /** + * Creates a quaternion that rotates from the +Y axis to the given direction. + * + * @param nx normalized direction X component + * @param ny normalized direction Y component + * @param nz normalized direction Z component + * @return quaternion representing the rotation + */ + private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) { + // Default direction is +Y (0, 1, 0) + // Target direction is (nx, ny, nz) + // Dot product: 0*nx + 1*ny + 0*nz = ny + final double dot = ny; + + // Check for parallel vectors + if (dot > 0.9999) { + // Direction is nearly +Y, no rotation needed + return Quaternion.identity(); + } + if (dot < -0.9999) { + // Direction is nearly -Y, rotate 180° around X axis + return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI); + } + + // Cross product: (0, 1, 0) x (nx, ny, nz) = (nz, 0, -nx) + // This gives the rotation axis + final double axisX = nz; + final double axisY = 0; + final double axisZ = -nx; + final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + final double normalizedAxisX = axisX / axisLength; + final double normalizedAxisZ = axisZ / axisLength; + + // Angle from dot product + final double angle = Math.acos(dot); + + return Quaternion.fromAxisAngle( + new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframePyramid.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframePyramid.java new file mode 100644 index 0000000..fe04179 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframePyramid.java @@ -0,0 +1,246 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.math.Matrix3x3; +import eu.svjatoslav.aukio.e3d.math.Quaternion; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A wireframe square-based pyramid that can be oriented in any direction. + * + *

The pyramid has a square base and four triangular faces meeting at an apex + * (tip). The wireframe consists of:

+ *
    + *
  • Four lines forming the square base
  • + *
  • Four lines from each base corner to the apex
  • + *
+ * + *

Two constructors are provided for different use cases:

+ * + *
    + *
  • Directional (recommended): Specify apex point and base center point. + * The pyramid points from apex toward the base center. This allows arbitrary + * orientation and is the most intuitive API.
  • + *
  • Y-axis aligned: Specify base center, base size, and height. The pyramid + * points in -Y direction (apex at lower Y). Useful for simple vertical pyramids.
  • + *
+ * + *

Usage examples:

+ *
{@code
+ * // Directional constructor: pyramid pointing from apex toward base
+ * LineAppearance appearance = new LineAppearance(2, Color.RED);
+ * WireframePyramid directionalPyramid = new WireframePyramid(
+ *     new Point3D(0, -100, 0),   // apex (tip of the pyramid)
+ *     new Point3D(0, 50, 0),     // baseCenter (pyramid points toward this)
+ *     50,                        // baseSize (half-width of square base)
+ *     appearance
+ * );
+ *
+ * // Y-axis aligned constructor: pyramid pointing upward
+ * WireframePyramid verticalPyramid = new WireframePyramid(
+ *     new Point3D(0, 0, 300),    // baseCenter
+ *     50,                        // baseSize (half-width of square base)
+ *     100,                       // height
+ *     appearance
+ * );
+ * }
+ * + * @see WireframeCone + * @see WireframeCube + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonPyramid + */ +public class WireframePyramid extends AbstractCompositeShape { + + /** + * Constructs a wireframe square-based pyramid pointing from apex toward base center. + * + *

This is the recommended constructor for placing pyramids in 3D space. + * The pyramid's apex (tip) is at {@code apexPoint}, and the square base + * is centered at {@code baseCenter}. The pyramid points in the direction + * from apex to base center.

+ * + *

Coordinate interpretation:

+ *
    + *
  • {@code apexPoint} - the sharp tip of the pyramid
  • + *
  • {@code baseCenter} - the center of the square base; the pyramid + * "points" in this direction from the apex
  • + *
  • {@code baseSize} - half the width of the square base; the base + * extends this distance from the center along perpendicular axes
  • + *
  • The distance between apex and base center determines the pyramid height
  • + *
+ * + * @param apexPoint the position of the pyramid's tip (apex) + * @param baseCenter the center point of the square base; the pyramid + * points from apex toward this point + * @param baseSize the half-width of the square base; the base extends + * this distance from the center, giving a total base + * edge length of {@code 2 * baseSize} + * @param appearance the line appearance (color, width) used for all lines + */ + public WireframePyramid(final Point3D apexPoint, final Point3D baseCenter, + final double baseSize, final LineAppearance appearance) { + super(); + + // Calculate direction and height from apex to base center + final double dx = baseCenter.x - apexPoint.x; + final double dy = baseCenter.y - apexPoint.y; + final double dz = baseCenter.z - apexPoint.z; + final double height = Math.sqrt(dx * dx + dy * dy + dz * dz); + + // Handle degenerate case: apex and base center are the same point + if (height < 0.001) { + return; + } + + // Normalize direction vector (from apex toward base) + final double nx = dx / height; + final double ny = dy / height; + final double nz = dz / height; + + // Calculate rotation to align Y-axis with direction + // Default pyramid points in -Y direction (apex at origin, base at -Y) + // We need to rotate from (0, -1, 0) to (nx, ny, nz) + final Quaternion rotation = createRotationFromYAxis(nx, ny, nz); + final Matrix3x3 rotMatrix = rotation.toMatrix(); + + // Generate base corner vertices in local space, then rotate and translate + // In local space: apex is at origin, base is at Y = -height + // Base corners form a square centered at (0, -height, 0) + final double h = baseSize; + final Point3D[] baseCorners = new Point3D[4]; + + // Local space corner positions (before rotation) + // Arranged counter-clockwise when viewed from apex (from +Y) + final double[][] localCorners = { + {-h, -height, -h}, // corner 0: negative X, negative Z + {+h, -height, -h}, // corner 1: positive X, negative Z + {+h, -height, +h}, // corner 2: positive X, positive Z + {-h, -height, +h} // corner 3: negative X, positive Z + }; + + for (int i = 0; i < 4; i++) { + final Point3D local = new Point3D(localCorners[i][0], localCorners[i][1], localCorners[i][2]); + rotMatrix.transform(local, local); + local.x += apexPoint.x; + local.y += apexPoint.y; + local.z += apexPoint.z; + baseCorners[i] = local; + } + + // Apex point (the pyramid tip) + final Point3D apex = new Point3D(apexPoint.x, apexPoint.y, apexPoint.z); + + // Create the four lines forming the square base + for (int i = 0; i < 4; i++) { + final int next = (i + 1) % 4; + addShape(appearance.getLine( + new Point3D(baseCorners[i].x, baseCorners[i].y, baseCorners[i].z), + new Point3D(baseCorners[next].x, baseCorners[next].y, baseCorners[next].z))); + } + + // Create the four lines from apex to each base corner + for (int i = 0; i < 4; i++) { + addShape(appearance.getLine( + new Point3D(apex.x, apex.y, apex.z), + new Point3D(baseCorners[i].x, baseCorners[i].y, baseCorners[i].z))); + } + } + + /** + * Constructs a wireframe square-based pyramid with base centered at the given point, + * pointing in the -Y direction. + * + *

This constructor creates a Y-axis aligned pyramid. The apex is positioned + * at {@code baseCenter.y - height} (above the base in the negative Y direction). + * For pyramids pointing in arbitrary directions, use + * {@link #WireframePyramid(Point3D, Point3D, double, LineAppearance)} instead.

+ * + *

Coordinate system: The pyramid points in -Y direction (apex at lower Y). + * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height. + * In Aukio 3D's coordinate system, "up" visually is negative Y.

+ * + * @param baseCenter the center point of the pyramid's base in 3D space + * @param baseSize the half-width of the square base; the base extends + * this distance from the center along X and Z axes, + * giving a total base edge length of {@code 2 * baseSize} + * @param height the height of the pyramid from base center to apex + * @param appearance the line appearance (color, width) used for all lines + */ + public WireframePyramid(final Point3D baseCenter, final double baseSize, + final double height, final LineAppearance appearance) { + super(); + + final double halfBase = baseSize; + final double apexY = baseCenter.y - height; + final double baseY = baseCenter.y; + + // Base corners arranged counter-clockwise when viewed from above (+Y) + // Naming: "negative/positive X" and "negative/positive Z" relative to base center + final Point3D negXnegZ = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z - halfBase); + final Point3D posXnegZ = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z - halfBase); + final Point3D posXposZ = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z + halfBase); + final Point3D negXposZ = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z + halfBase); + final Point3D apex = new Point3D(baseCenter.x, apexY, baseCenter.z); + + // Create the four lines forming the square base + addShape(appearance.getLine(negXnegZ, posXnegZ)); + addShape(appearance.getLine(posXnegZ, posXposZ)); + addShape(appearance.getLine(posXposZ, negXposZ)); + addShape(appearance.getLine(negXposZ, negXnegZ)); + + // Create the four lines from apex to each base corner + addShape(appearance.getLine(apex, negXnegZ)); + addShape(appearance.getLine(apex, posXnegZ)); + addShape(appearance.getLine(apex, posXposZ)); + addShape(appearance.getLine(apex, negXposZ)); + } + + /** + * Creates a quaternion that rotates from the -Y axis to the given direction. + * + *

The pyramid by default points in the -Y direction (apex at origin, base at -Y). + * This method computes the rotation needed to align the pyramid with the target + * direction vector.

+ * + * @param nx normalized direction X component + * @param ny normalized direction Y component + * @param nz normalized direction Z component + * @return quaternion representing the rotation + */ + private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) { + // Default direction is -Y (0, -1, 0) + // Target direction is (nx, ny, nz) + // Dot product: 0*nx + (-1)*ny + 0*nz = -ny + final double dot = -ny; + + // Check for parallel vectors + if (dot > 0.9999) { + // Direction is nearly -Y, no rotation needed + return Quaternion.identity(); + } + if (dot < -0.9999) { + // Direction is nearly +Y, rotate 180° around X axis + return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI); + } + + // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx) + // This gives the rotation axis + final double axisX = -nz; + final double axisY = 0; + final double axisZ = nx; + final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + final double normalizedAxisX = axisX / axisLength; + final double normalizedAxisZ = axisZ / axisLength; + + // Angle from dot product + final double angle = Math.acos(dot); + + return Quaternion.fromAxisAngle( + new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeSphere.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeSphere.java new file mode 100755 index 0000000..0a74e97 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/WireframeSphere.java @@ -0,0 +1,87 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +import java.util.ArrayList; + +/** + * A wireframe sphere approximation built from rings of connected line segments. + * The sphere is generated using parametric spherical coordinates, producing a + * latitude-longitude grid of vertices connected by lines. + * + *

The sphere is divided into 20 longitudinal slices and 20 latitudinal rings + * (using a step of {@code PI / 10} radians). Adjacent vertices within each ring + * are connected, and corresponding vertices between consecutive rings are also + * connected, forming a mesh that approximates a sphere surface.

+ * + *

Usage example:

+ *
{@code
+ * LineAppearance appearance = new LineAppearance(1, Color.WHITE);
+ * WireframeSphere sphere = new WireframeSphere(new Point3D(0, 0, 300), 100f, appearance);
+ * shapeCollection.addShape(sphere);
+ * }
+ * + * @see LineAppearance + * @see AbstractCompositeShape + */ +public class WireframeSphere extends AbstractCompositeShape { + + /** Stores the vertices of the previously generated ring for inter-ring connections. */ + ArrayList previousRing = new ArrayList<>(); + + /** + * Constructs a wireframe sphere at the given location with the specified radius. + * The sphere is approximated by a grid of line segments generated from + * parametric spherical coordinates. + * + * @param location the center point of the sphere in 3D space + * @param radius the radius of the sphere + * @param lineFactory the line appearance (color, width) used for all line segments + */ + public WireframeSphere(final Point3D location, final float radius, + final LineAppearance lineFactory) { + super(location); + + final double step = Math.PI / 10; + + final Point3D center = new Point3D(); + + int ringIndex = 0; + + for (double j = 0d; j <= (Math.PI * 2); j += step) { + + Point3D oldPoint = null; + int pointIndex = 0; + + for (double i = 0; i <= (Math.PI * 2); i += step) { + final Point3D newPoint = new Point3D(0, 0, radius); + newPoint.rotate(center, i, j); + + if (oldPoint != null) + addShape(lineFactory.getLine(newPoint, oldPoint)); + + if (ringIndex > 0) { + final Point3D previousRingPoint = previousRing + .get(pointIndex); + addShape(lineFactory.getLine(newPoint, previousRingPoint)); + + previousRing.set(pointIndex, newPoint); + } else + previousRing.add(newPoint); + + oldPoint = newPoint; + pointIndex++; + } + + ringIndex++; + } + + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/package-info.java new file mode 100644 index 0000000..1a63289 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/wireframe/package-info.java @@ -0,0 +1,24 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Wireframe composite shapes built from Line primitives. + * + *

These shapes render as edge-only outlines, useful for visualization, + * debugging, and architectural-style rendering.

+ * + *

Key classes:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe.WireframeBox} - A wireframe box
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe.WireframeCube} - A wireframe cube
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe.WireframeSphere} - A wireframe sphere
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe.Grid2D} - A 2D grid plane
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe.Grid3D} - A 3D grid volume
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe.WireframeBox + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/package-info.java new file mode 100644 index 0000000..d72bc51 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/package-info.java @@ -0,0 +1,25 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Renderable shape classes for the rasterization pipeline. + * + *

This package contains the shape hierarchy used for 3D rendering:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape} - Base class for all shapes
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape} - Base for shapes with vertices
  • + *
+ * + *

Subpackages organize shapes by type:

+ *
    + *
  • {@code basic} - Primitive shapes (lines, polygons, billboards)
  • + *
  • {@code composite} - Compound shapes built from primitives (boxes, grids, text)
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractShape + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes; \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.java new file mode 100644 index 0000000..5792de1 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/Texture.java @@ -0,0 +1,479 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.texture; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.awt.image.WritableRaster; + +import static java.util.Arrays.fill; + +/** + * Represents a 2D texture with mipmap support for level-of-detail rendering. + * + *

A {@code Texture} contains a primary bitmap at native resolution, along with + * cached upscaled and downscaled versions (mipmaps) that are lazily generated on demand. + * This mipmap chain enables efficient texture sampling at varying distances from the camera, + * avoiding aliasing artifacts for distant surfaces and pixelation for close-up views.

+ * + *

The texture also exposes a {@link java.awt.Graphics2D} context backed by the primary + * bitmap's {@link java.awt.image.BufferedImage}, allowing dynamic rendering of text, + * shapes, or other 2D content directly onto the texture surface. Anti-aliasing is + * enabled by default on this graphics context.

+ * + *

Mipmap levels

+ *
    + *
  • Primary bitmap -- the native resolution; always available.
  • + *
  • Downsampled bitmaps -- up to 8 levels, each half the size of the previous. + * Used when the texture is rendered at zoom levels below 1.0.
  • + *
  • Upsampled bitmaps -- configurable count (set at construction time), each + * double the size of the previous. Used when the texture is rendered at zoom levels + * above 2.0.
  • + *
+ * + *

Usage example

+ *
{@code
+ * Texture tex = new Texture(256, 256, 3);
+ * // Draw content using the Graphics2D context
+ * tex.graphics.setColor(java.awt.Color.RED);
+ * tex.graphics.fillRect(0, 0, 256, 256);
+ * // Invalidate cached mipmaps after modifying the primary bitmap
+ * tex.resetResampledBitmapCache();
+ * // Retrieve the appropriate mipmap for a given zoom level
+ * TextureBitmap bitmap = tex.getMipmapForScale(0.5);
+ * }
+ * + * @see TextureBitmap + * @see eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle + */ +public class Texture { + + /** + * When true, texture coordinates outside [0, width/height) wrap + * (tile) instead of clamping to the edge texel. Needed for + * world-planar UVs and tiled game meshes. + */ + public boolean wrap; + + /** + * When true, this texture carries meaningful alpha (cutout TEST or + * BLEND): in z-buffer mode its triangles paint in the second, + * back-to-front alpha pass — depth-tested but not depth-written, so + * they resolve against opaque geometry per-pixel yet keep + * painter-coherent overlap among themselves. Opaque-class triangles + * paint first, front-to-back, writing depth. Set by texture + * providers that know the alpha mode (default false = opaque). + */ + public boolean hasAlpha; + + /** + * The primary (native resolution) bitmap for this texture. + * All dynamic drawing via {@link #graphics} modifies this bitmap's backing data. + */ + public final TextureBitmap primaryBitmap; + + /** + * A {@link java.awt.Graphics2D} context for drawing 2D content onto the primary bitmap. + * Anti-aliasing for both geometry and text is enabled by default. + */ + public final java.awt.Graphics2D graphics; + + /** + * Cached upsampled (enlarged) versions of the primary bitmap. + * Index 0 is 2x the primary, index 1 is 4x, and so on. + * Entries are lazily populated on first access. + */ + TextureBitmap[] upSampled; + + /** + * Cached downsampled (reduced) versions of the primary bitmap. + * 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]; // TODO: consider renaming it to mipmap to use standard terminology + + /** + * Optional signed-distance-field mask for sharp text/vector-art + * rendering. When non-null, {@code TexturedTriangle} samples this + * mask with bilinear filtering and derives per-pixel coverage from + * it (edge at value ~127.5), mixing the background layer + * ({@link #primaryBitmap}) with {@link #sdfForeground} — instead of + * blending coverage stored in the bitmap itself. The mask is always + * sampled at primary resolution: minification is handled by widening + * the coverage window by the pixel footprint, so no mipmap chain is + * needed and there is no mip-level isosurface drift. + * + *

Values: 0 = deep inside ink, 255 = far outside any ink.

+ */ + public TextureBitmap sdfMask; + + /** + * Hard-edged foreground (ink) color layer for SDF rendering. Read + * only where the coverage derived from {@link #sdfMask} is non-zero, + * so flat per-region fills are sufficient; sampled nearest. + */ + public TextureBitmap sdfForeground; + + /** + * Width of the distance gradient encoded in {@link #sdfMask}, in + * primary-texture texels: mask value 0.5 +/- 0.5 spans + * -sdfSpreadTexels..+sdfSpreadTexels of signed distance. Set by + * whoever generated the mask (see {@code SdfGlyphCache#SPREAD_TEXELS}). + */ + public double sdfSpreadTexels = 2.0; + + /** + * Returns whether this texture renders through the SDF path + * (distance-field mask + separate bg/fg color layers). + * + * @return true when an SDF mask is attached + */ + public boolean isSdf() { + return sdfMask != null; + } + + /** + * Creates a new texture with the specified dimensions and upscale capacity. + * + *

The underlying {@link java.awt.image.BufferedImage} is created using + * {@link eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext#bufferedImageType} for + * compatibility with the raster rendering pipeline.

+ * + * @param width the width of the primary bitmap in pixels + * @param height the height of the primary bitmap in pixels + * @param maxUpscale the maximum number of upscaled mipmap levels to support + * (each level doubles the resolution) + */ + public Texture(final int width, final int height, final int maxUpscale) { + upSampled = new TextureBitmap[maxUpscale]; + + final BufferedImage bufferedImage = new BufferedImage(width, height, + BufferedImage.TYPE_INT_ARGB); + + final WritableRaster raster = bufferedImage.getRaster(); + final DataBufferInt dbi = (DataBufferInt) raster.getDataBuffer(); + graphics = (Graphics2D) bufferedImage.getGraphics(); + + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + primaryBitmap = new TextureBitmap(width, height, dbi.getData(), 1); + } + +/** + * Determines the appropriate downscale mipmap level for a given scale factor. + * + *

Iterates through the downscaled mipmap levels (each halving the size) + * and returns the index of the first level whose effective size falls below + * the requested scale.

+ * + * @param scale the scale factor (typically less than 1.0 for downscaling) + * @return the index into the {@code downSampled} array to use, clamped to the + * maximum available level + */ + public int getDownscaleMipmapLevel(final double scale) { + double size = 1; + for (int i = 0; i < downSampled.length; i++) { + size = size / 2; + if (size < scale) + return i; + } + + return downSampled.length - 1; + } + + /** + * Determines the appropriate upscale mipmap level for a given scale factor. + * + *

Iterates through the upscaled mipmap levels (each doubling the size) + * and returns the index of the first level whose effective size exceeds + * the requested scale.

+ * + * @param scale the scale factor (typically greater than 2.0 for upscaling) + * @return the index into the {@code upSampled} array to use, or -1 if no + * upscale is needed or available + */ + public int getUpscaleMipmapLevel(final double scale) { + double size = 2; + for (int i = 0; i < upSampled.length; i++) { + size = size * 2; + if (size > scale) + return i; + } + + return -1; + } + + /** + * Downscale given bitmap by factor of 2. + * + * @param originalBitmap Bitmap to downscale. + * @return Downscaled bitmap. + */ + public TextureBitmap downscaleBitmap(final TextureBitmap originalBitmap) { + int newWidth = originalBitmap.width / 2; + int newHeight = originalBitmap.height / 2; + + // Enforce minimum width and height + if (newWidth < 1) + newWidth = 1; + if (newHeight < 1) + newHeight = 1; + + final TextureBitmap downScaled = new TextureBitmap(newWidth, newHeight, + originalBitmap.multiplicationFactor / 2d); + + final int[] srcPixels = originalBitmap.pixels; + final int[] dstPixels = downScaled.pixels; + final int srcW = originalBitmap.width; + final int srcH = originalBitmap.height; + final int srcWMinus1 = srcW - 1; + final int srcHMinus1 = srcH - 1; + + for (int y = 0; y < newHeight; y++) { + final int srcYBase = y * 2; + final int srcY1 = Math.min(srcYBase, srcHMinus1); + final int srcY2 = Math.min(srcYBase + 1, srcHMinus1); + final int row1Offset = srcY1 * srcW; + final int row2Offset = srcY2 * srcW; + + for (int x = 0; x < newWidth; x++) { + final int srcXBase = x * 2; + final int srcX1 = Math.min(srcXBase, srcWMinus1); + final int srcX2 = Math.min(srcXBase + 1, srcWMinus1); + + final int p0 = srcPixels[row1Offset + srcX1]; + final int p1 = srcPixels[row1Offset + srcX2]; + final int p2 = srcPixels[row2Offset + srcX1]; + final int p3 = srcPixels[row2Offset + srcX2]; + + final int a = (((p0 >>> 24) + (p1 >>> 24) + (p2 >>> 24) + (p3 >>> 24)) >> 2); + final int r = ((((p0 >> 16) & 0xff) + ((p1 >> 16) & 0xff) + ((p2 >> 16) & 0xff) + ((p3 >> 16) & 0xff)) >> 2); + final int g = ((((p0 >> 8) & 0xff) + ((p1 >> 8) & 0xff) + ((p2 >> 8) & 0xff) + ((p3 >> 8) & 0xff)) >> 2); + final int b = (((p0 & 0xff) + (p1 & 0xff) + (p2 & 0xff) + (p3 & 0xff)) >> 2); + + dstPixels[y * newWidth + x] = (a << 24) | (r << 16) | (g << 8) | b; + } + } + + return downScaled; + } + + /** + * Returns a downscaled bitmap at the specified mipmap level, creating it lazily if needed. + * + *

Level 0 is half the primary resolution, level 1 is a quarter, and so on. + * Each level is derived by downscaling the previous level by a factor of 2.

+ * + * @param scaleFactor the downscale level index (0 = 1/2 size, 1 = 1/4 size, etc.) + * @return the cached or newly created downscaled {@link TextureBitmap} + * @see #downscaleBitmap(TextureBitmap) + */ + public TextureBitmap getDownscaledBitmap(final int scaleFactor) { + if (downSampled[scaleFactor] == null) { + + TextureBitmap largerBitmap; + if (scaleFactor == 0) + largerBitmap = primaryBitmap; + else + largerBitmap = getDownscaledBitmap(scaleFactor - 1); + + downSampled[scaleFactor] = downscaleBitmap(largerBitmap); + } + + return downSampled[scaleFactor]; + } + + /** + * Returns the bitmap that should be used for rendering at the given zoom + * + * @param scaleFactor The upscale factor + * @return The bitmap + */ + public TextureBitmap getUpscaledBitmap(final int scaleFactor) { + if (upSampled[scaleFactor] == null) { + + TextureBitmap smallerBitmap; + if (scaleFactor == 0) + smallerBitmap = primaryBitmap; + else + smallerBitmap = getUpscaledBitmap(scaleFactor - 1); + + upSampled[scaleFactor] = upscaleBitmap(smallerBitmap); + } + + return upSampled[scaleFactor]; + } + + /** + * Returns the appropriate mipmap level for rendering at the given scale. + * + *

Scale factor represents how large the texture appears on screen + * relative to its native resolution:

+ *
    + *
  • scale < 1.0: texture appears smaller (use downscaled mipmap)
  • + *
  • scale 1.0-2.0: texture appears near native size (use primary bitmap)
  • + *
  • scale > 2.0: texture appears much larger (use upscaled mipmap)
  • + *
+ * + * @param scale the apparent scale factor of the texture on screen + * @return the best-fit mipmap level as a {@link TextureBitmap} + */ + public TextureBitmap getMipmapForScale(final double scale) { + + if (scale < 1) { + final int mipmapLevel = getDownscaleMipmapLevel(scale); + return getDownscaledBitmap(mipmapLevel); + } else if (scale > 2) { + final int mipmapLevel = getUpscaleMipmapLevel(scale); + + if (mipmapLevel < 0) + return primaryBitmap; + + return getUpscaledBitmap(mipmapLevel); + } + + return primaryBitmap; + } + + /** + * Resets the cache of resampled bitmaps + */ + public void resetResampledBitmapCache() { + fill(upSampled, null); + + fill(downSampled, null); + } + + /** + * Upscales the given bitmap by a factor of 2 + * + * @param originalBitmap The bitmap to upscale + * @return The upscaled bitmap + */ + public TextureBitmap upscaleBitmap(final TextureBitmap originalBitmap) { + final int srcW = originalBitmap.width; + final int srcH = originalBitmap.height; + final int newWidth = srcW * 2; + final int newHeight = srcH * 2; + final int srcWMinus1 = srcW - 1; + final int srcHMinus1 = srcH - 1; + + final TextureBitmap upScaled = new TextureBitmap(newWidth, newHeight, + originalBitmap.multiplicationFactor * 2d); + + final int[] src = originalBitmap.pixels; + final int[] dst = upScaled.pixels; + + for (int y = 0; y < srcH; y++) { + final int srcRowOffset = y * srcW; + final int nextRowOffset = Math.min(y + 1, srcHMinus1) * srcW; + final int dstRow0Offset = (y * 2) * newWidth; + final int dstRow1Offset = (y * 2 + 1) * newWidth; + + for (int x = 0; x < srcW; x++) { + final int nx = Math.min(x + 1, srcWMinus1); + + final int p00 = src[srcRowOffset + x]; + final int p10 = src[srcRowOffset + nx]; + final int p01 = src[nextRowOffset + x]; + final int p11 = src[nextRowOffset + nx]; + + dst[dstRow0Offset + x * 2] = p00; + dst[dstRow0Offset + x * 2 + 1] = avg2(p00, p10); + dst[dstRow1Offset + x * 2] = avg2(p00, p01); + dst[dstRow1Offset + x * 2 + 1] = avg4(p00, p10, p01, p11); + } + } + + return upScaled; + } + + private static int avg2(final int p0, final int p1) { + return (((((p0 >>> 24) + (p1 >>> 24)) >> 1) << 24) + | (((((p0 >> 16) & 0xff) + ((p1 >> 16) & 0xff)) >> 1) << 16) + | (((((p0 >> 8) & 0xff) + ((p1 >> 8) & 0xff)) >> 1) << 8) + | (((p0 & 0xff) + (p1 & 0xff)) >> 1)); + } + + private static int avg4(final int p0, final int p1, final int p2, final int p3) { + return ((((p0 >>> 24) + (p1 >>> 24) + (p2 >>> 24) + (p3 >>> 24)) >> 2) << 24) + | (((((p0 >> 16) & 0xff) + ((p1 >> 16) & 0xff) + ((p2 >> 16) & 0xff) + ((p3 >> 16) & 0xff)) >> 2) << 16) + | (((((p0 >> 8) & 0xff) + ((p1 >> 8) & 0xff) + ((p2 >> 8) & 0xff) + ((p3 >> 8) & 0xff)) >> 2) << 8) + | (((p0 & 0xff) + (p1 & 0xff) + (p2 & 0xff) + (p3 & 0xff)) >> 2); + } + + /** + * A helper class that accumulates color values for a given area of a bitmap. + */ + public static class ColorAccumulator { + /** Accumulated red component. */ + public int r; + /** Accumulated green component. */ + public int g; + /** Accumulated blue component. */ + public int b; + /** Accumulated alpha component. */ + public int a; + + /** Number of pixels accumulated. */ + public int pixelCount = 0; + + /** + * Creates a new color accumulator with zero values. + */ + public ColorAccumulator() { + } + + /** + * Accumulates the color values of the given pixel + * + * @param bitmap The bitmap + * @param x The x coordinate of the pixel + * @param y The y coordinate of the pixel + */ + public void accumulate(final TextureBitmap bitmap, final int x, + final int y) { + final int pixel = bitmap.pixels[bitmap.getAddress(x, y)]; + a += (pixel >> 24) & 0xff; + r += (pixel >> 16) & 0xff; + g += (pixel >> 8) & 0xff; + b += pixel & 0xff; + pixelCount++; + } + + /** + * Resets the accumulator + */ + public void reset() { + a = 0; + r = 0; + g = 0; + b = 0; + pixelCount = 0; + } + + /** + * Stores the accumulated color values in the given bitmap + * + * @param bitmap The bitmap + * @param x The x coordinate of the pixel + * @param y The y coordinate of the pixel + */ + public void storeResult(final TextureBitmap bitmap, final int x, + final int y) { + final int avgA = a / pixelCount; + final int avgR = r / pixelCount; + final int avgG = g / pixelCount; + final int avgB = b / pixelCount; + bitmap.pixels[bitmap.getAddress(x, y)] = (avgA << 24) | (avgR << 16) | (avgG << 8) | avgB; + } + } + +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.java new file mode 100644 index 0000000..8969225 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureBitmap.java @@ -0,0 +1,291 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.texture; + +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; + +/** + * Represents a single resolution level of a texture as a raw int array. + * + *

Each pixel is stored as a single int in ARGB format: + * {@code (alpha << 24) | (red << 16) | (green << 8) | blue}. + * This matches the {@link java.awt.image.BufferedImage#TYPE_INT_ARGB} format.

+ * + *

{@code TextureBitmap} is used internally by {@link Texture} to represent + * individual mipmap levels. The {@link #multiplicationFactor} records the + * scale ratio relative to the primary (native) resolution -- for example, + * a value of 0.5 means this bitmap is half the original size, and 2.0 + * means it is double.

+ * + *

This class provides low-level pixel operations including:

+ *
    + *
  • Alpha-blended pixel transfer to a target raster ({@link #drawPixel(int, int[], int)})
  • + *
  • Direct pixel writes using engine {@link Color} ({@link #drawPixel(int, int, Color)})
  • + *
  • Filled rectangle drawing ({@link #drawRectangle(int, int, int, int, Color)})
  • + *
  • Full-surface color fill ({@link #fillColor(Color)})
  • + *
+ * + * @see Texture + * @see Color + */ +public class TextureBitmap { + + /** + * Raw pixel data in ARGB int format. + * Each int encodes: {@code (alpha << 24) | (red << 16) | (green << 8) | blue}. + * The array length is {@code width * height}. + */ + public final int[] pixels; + + /** + * The width of this bitmap in pixels. + */ + public final int width; + + /** + * The height of this bitmap in pixels. + */ + public final int height; + + /** + * The scale factor of this bitmap relative to the primary (native) texture resolution. + * A value of 1.0 indicates the native resolution, 0.5 indicates half-size, 2.0 indicates double-size, etc. + */ + public double multiplicationFactor; + +/** + * Creates a texture bitmap backed by an existing int array. + * + *

This constructor is typically used when the bitmap data is obtained from + * a {@link java.awt.image.BufferedImage}'s raster, allowing direct access to + * the image's pixel data without copying.

+ * + * @param width the bitmap width in pixels + * @param height the bitmap height in pixels + * @param pixels the raw pixel data array (must be at least {@code width * height} ints) + * @param multiplicationFactor the scale factor relative to the native texture resolution + */ + public TextureBitmap(final int width, final int height, final int[] pixels, + final double multiplicationFactor) { + + this.width = width; + this.height = height; + this.pixels = pixels; + this.multiplicationFactor = multiplicationFactor; + } + + /** + * Creates a texture bitmap with a newly allocated int array. + * + *

The pixel data array is initialized to all zeros (fully transparent black).

+ * + * @param width the bitmap width in pixels + * @param height the bitmap height in pixels + * @param multiplicationFactor the scale factor relative to the native texture resolution + */ + public TextureBitmap(final int width, final int height, + final double multiplicationFactor) { + + this(width, height, new int[width * height], multiplicationFactor); + } + + /** + * Transfer (render) one pixel from current {@link TextureBitmap} to target RGB raster. + * + *

This texture stores pixels in ARGB format. The target is RGB format (no alpha). + * Alpha blending is performed based on the source pixel's alpha value.

+ * + *

Performance note: Uses bit-shift instead of division for alpha blending, + * and pre-multiplies source colors to reduce per-pixel operations.

+ * + * @param sourcePixelAddress Pixel index within current texture. + * @param targetBitmap Target RGB pixel array. + * @param targetPixelAddress Pixel index within target image. + */ + public void drawPixel(final int sourcePixelAddress, + final int[] targetBitmap, final int targetPixelAddress) { + + final int sourcePixel = pixels[sourcePixelAddress]; + final int textureAlpha = (sourcePixel >> 24) & 0xff; + + if (textureAlpha == 0) + return; + + if (textureAlpha == 255) { + targetBitmap[targetPixelAddress] = sourcePixel; + return; + } + + final int backgroundAlpha = 255 - textureAlpha; + + // Pre-multiply source colors by alpha to reduce operations in blend + final int srcR = ((sourcePixel >> 16) & 0xff) * textureAlpha; + final int srcG = ((sourcePixel >> 8) & 0xff) * textureAlpha; + final int srcB = (sourcePixel & 0xff) * textureAlpha; + + final int destPixel = targetBitmap[targetPixelAddress]; + final int destR = (destPixel >> 16) & 0xff; + final int destG = (destPixel >> 8) & 0xff; + final int destB = destPixel & 0xff; + + // Use bit-shift instead of division for faster blending + final int r = ((destR * backgroundAlpha) + srcR) >> 8; + final int g = ((destG * backgroundAlpha) + srcG) >> 8; + final int b = ((destB * backgroundAlpha) + srcB) >> 8; + + targetBitmap[targetPixelAddress] = (r << 16) | (g << 8) | b; + } + + /** + * Renders a scanline using pre-computed source pixel addresses. + * + *

This variant is optimized for cases where source addresses are computed + * externally (e.g., by a caller that already has the stepping logic). + * The sourceAddresses array must contain valid indices into {@link #pixels}.

+ * + * @param sourceAddresses array of source pixel addresses (indices into pixels array) + * @param targetBitmap target RGB pixel array + * @param targetStartAddress starting index in the target array + * @param pixelCount number of pixels to render + */ + public void drawScanlineWithAddresses(final int[] sourceAddresses, + final int[] targetBitmap, final int targetStartAddress, + final int pixelCount) { + + int targetOffset = targetStartAddress; + + for (int i = 0; i < pixelCount; i++) { + final int sourcePixel = pixels[sourceAddresses[i]]; + final int textureAlpha = (sourcePixel >> 24) & 0xff; + + if (textureAlpha == 255) { + targetBitmap[targetOffset] = sourcePixel; + } else if (textureAlpha != 0) { + final int backgroundAlpha = 255 - textureAlpha; + + final int srcR = ((sourcePixel >> 16) & 0xff) * textureAlpha; + final int srcG = ((sourcePixel >> 8) & 0xff) * textureAlpha; + final int srcB = (sourcePixel & 0xff) * textureAlpha; + + final int destPixel = targetBitmap[targetOffset]; + final int destR = (destPixel >> 16) & 0xff; + final int destG = (destPixel >> 8) & 0xff; + final int destB = destPixel & 0xff; + + final int r = ((destR * backgroundAlpha) + srcR) >> 8; + final int g = ((destG * backgroundAlpha) + srcG) >> 8; + final int b = ((destB * backgroundAlpha) + srcB) >> 8; + + targetBitmap[targetOffset] = (r << 16) | (g << 8) | b; + } + + targetOffset++; + } + } + + /** + * Draws a single pixel at the specified coordinates using the given color. + * + *

The color components are written directly without alpha blending. + * Coordinates are clamped to the bitmap bounds by {@link #getAddress(int, int)}.

+ * + * @param x the x coordinate of the pixel + * @param y the y coordinate of the pixel + * @param color the color to write + */ + public void drawPixel(final int x, final int y, final Color color) { + pixels[getAddress(x, y)] = (color.a << 24) | (color.r << 16) | (color.g << 8) | color.b; + } + + /** + * Fills a rectangular region with the specified color. + * + *

If {@code x1 > x2}, the coordinates are swapped to ensure correct rendering. + * The same applies to {@code y1} and {@code y2}. The rectangle is exclusive of the + * right and bottom edges.

+ * + *

Performance: Uses {@link java.util.Arrays#fill(int[], int, int, int)} + * per scanline for optimal JVM-optimized memory writes.

+ * + * @param x1 the left x coordinate + * @param y1 the top y coordinate + * @param x2 the right x coordinate (exclusive) + * @param y2 the bottom y coordinate (exclusive) + * @param color the fill color + */ + public void drawRectangle(int x1, int y1, int x2, int y2, + final Color color) { + + if (x1 > x2) { + final int tmp = x1; + x1 = x2; + x2 = tmp; + } + + if (y1 > y2) { + final int tmp = y1; + y1 = y2; + y2 = tmp; + } + + // Clamp to bitmap bounds + if (x1 < 0) x1 = 0; + if (y1 < 0) y1 = 0; + if (x2 > width) x2 = width; + if (y2 > height) y2 = height; + + final int pixel = (color.a << 24) | (color.r << 16) | (color.g << 8) | color.b; + final int rowWidth = x2 - x1; + + if (rowWidth <= 0) + return; + + // Fill each scanline using Arrays.fill for optimal performance + for (int y = y1; y < y2; y++) { + final int rowStart = y * width + x1; + java.util.Arrays.fill(pixels, rowStart, rowStart + rowWidth, pixel); + } + } + + /** + * Fills the entire bitmap with the specified color. + * + *

Every pixel in the bitmap is set to the given color value, + * overwriting all existing content.

+ * + * @param color the color to fill the entire bitmap with + */ + public void fillColor(final Color color) { + final int pixel = (color.a << 24) | (color.r << 16) | (color.g << 8) | color.b; + java.util.Arrays.fill(pixels, pixel); + } + + /** + * Computes the index into the {@link #pixels} array for the pixel at ({@code x}, {@code y}). + * + *

Coordinates are clamped to the valid range {@code [0, width-1]} and + * {@code [0, height-1]} so that out-of-bounds accesses are safely handled + * by sampling the nearest edge pixel.

+ * + * @param x the x coordinate of the pixel + * @param y the y coordinate of the pixel + * @return the index into the pixels array for the specified pixel + */ + public int getAddress(int x, int y) { + if (x < 0) + x = 0; + + if (x >= width) + x = width - 1; + + if (y < 0) + y = 0; + + if (y >= height) + y = height - 1; + + return (y * width) + x; + } +} diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureGenerator.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureGenerator.java new file mode 100644 index 0000000..98ef154 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/TextureGenerator.java @@ -0,0 +1,327 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.texture; + +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; + +import java.lang.ref.WeakReference; +import java.util.HashMap; +import java.util.Map; + +import static java.lang.Math.pow; +import static java.lang.Math.sqrt; + +/** + * Factory class for generating reusable textures with configurable borders and glow effects. + * + *

Provides static factory methods to create common texture patterns:

+ *
    + *
  • {@link #solidWithBorder} - solid fill color with opaque border (for bordered polygons)
  • + *
  • {@link #glowingBorder} - transparent center with glowing edges (for wireframe-effect shapes)
  • + *
  • {@link #radialGlow} - circular radial gradient (for point/billboard glows)
  • + *
+ * + *

Texture caching: Textures are cached by their generation parameters using a + * {@link WeakReference}-based cache. When textures are no longer referenced elsewhere, + * they are automatically garbage collected. This reduces memory usage when many shapes + * share identical textures.

+ * + *

Example usage:

+ *
{@code
+ * // RGB cube plate with black border
+ * Texture tex1 = TextureGenerator.solidWithBorder(64, new Color(255, 0, 0), new Color(0, 0, 0), 3);
+ *
+ * // Wireframe-effect texture (glowing cyan edges, transparent center)
+ * Texture tex2 = TextureGenerator.glowingBorder(64, new Color(0, 255, 255), 6, 120, true);
+ *
+ * // Circular glow point
+ * Texture tex3 = TextureGenerator.radialGlow(100, new Color(255, 200, 100));
+ * }
+ * + * @see Texture + * @see Color + */ +public final class TextureGenerator { + + /** + * Cache of generated textures, keyed by configuration parameters. + * Uses WeakReference so textures are GC'd when no longer referenced elsewhere. + */ + private static final Map> textureCache = new HashMap<>(); + + /** + * Private constructor to prevent instantiation. + * This class only provides static factory methods. + */ + private TextureGenerator() { + } + + /** + * Creates a texture with a solid fill color and an opaque border. + * + *

The fill color fills the entire texture except for the border region. + * The border is drawn as an opaque rectangle inset from the edges.

+ * + *

Caching: Identical parameters produce the same cached texture instance.

+ * + * @param size the texture width and height in pixels + * @param fillColor the color filling the interior + * @param borderColor the color of the border + * @param borderWidth the width of the border in pixels + * @param maxUpscale the maximum number of upscaled mipmap levels (0 = no upscale, 1 = 2x upscale, 2 = 4x upscale) + * @return a texture with solid fill and opaque border + */ + public static Texture solidWithBorder(final int size, final Color fillColor, + final Color borderColor, final int borderWidth, + final int maxUpscale) { + final TextureKey key = new TextureKey(size, fillColor, borderColor, borderWidth, + TextureType.SOLID_WITH_BORDER, 0, false, maxUpscale); + + return getOrCreate(key, () -> generateSolidWithBorder(size, fillColor, borderColor, borderWidth, maxUpscale)); + } + + /** + * Creates a texture with a transparent center and glowing border edges. + * + *

This texture is useful for creating wireframe-effect shapes: the polygon + * appears to have only edges, with the interior being transparent. The border + * has a glow effect where intensity decreases from the edge toward the center.

+ * + *

Glow effect: Multiple concentric border lines are drawn with decreasing + * alpha values, creating a luminous edge appearance. The glow intensity controls + * how bright the innermost edge appears.

+ * + *

Caching: Identical parameters produce the same cached texture instance.

+ * + * @param size the texture width and height in pixels + * @param borderColor the color of the glowing border + * @param borderWidth the width of the border region in pixels (where glow appears) + * @param glowIntensity the base intensity added to the border color (0-255 range) + * @param transparent if true, center is fully transparent; if false, has slight tint + * @param maxUpscale the maximum number of upscaled mipmap levels (0 = no upscale, 1 = 2x upscale, 2 = 4x upscale) + * @return a texture with glowing edges and transparent center + */ + public static Texture glowingBorder(final int size, final Color borderColor, + final int borderWidth, final int glowIntensity, + final boolean transparent, final int maxUpscale) { + final TextureKey key = new TextureKey(size, borderColor, Color.TRANSPARENT, borderWidth, + TextureType.GLOWING_BORDER, glowIntensity, transparent, maxUpscale); + + return getOrCreate(key, () -> generateGlowingBorder(size, borderColor, borderWidth, + glowIntensity, transparent, maxUpscale)); + } + + /** + * Creates a texture with a circular radial gradient glow. + * + *

The texture has a circular alpha gradient: fully opaque at the center, + * transitioning to fully transparent at the edges. The color intensity + * remains constant while alpha decreases radially.

+ * + *

This is suitable for rendering glowing points or circular billboards. + * The center of the texture is the brightest point, fading outward.

+ * + *

Caching: Identical parameters produce the same cached texture instance.

+ * + * @param size the texture width and height in pixels (should be even) + * @param color the color of the glow (alpha is overridden by radial gradient) + * @return a texture with circular radial alpha gradient + */ + public static Texture radialGlow(final int size, final Color color) { + final TextureKey key = new TextureKey(size, color, Color.TRANSPARENT, 0, + TextureType.RADIAL_GLOW, 0, false, 1); + + return getOrCreate(key, () -> generateRadialGlow(size, color)); + } + + /** + * Retrieves a cached texture or creates a new one if not cached. + * + * @param key the cache key identifying the texture configuration + * @param generator the function to create the texture if not cached + * @return the cached or newly created texture + */ + private static Texture getOrCreate(final TextureKey key, final TextureSupplier generator) { + synchronized (textureCache) { + final WeakReference ref = textureCache.get(key); + if (ref != null) { + final Texture cached = ref.get(); + if (cached != null) { + return cached; + } + // Reference was cleared, remove stale entry + textureCache.remove(key); + } + + final Texture texture = generator.create(); + textureCache.put(key, new WeakReference<>(texture)); + return texture; + } + } + + /** + * Generates a texture with solid fill and opaque border. + */ + private static Texture generateSolidWithBorder(final int size, final Color fillColor, + final Color borderColor, final int borderWidth, + final int maxUpscale) { + final Texture texture = new Texture(size, size, maxUpscale); + + // Fill interior with fill color + texture.primaryBitmap.drawRectangle(borderWidth, borderWidth, + size - borderWidth, size - borderWidth, fillColor); + + // Draw border regions (top, bottom, left, right edges) + // Top border + texture.primaryBitmap.drawRectangle(0, 0, size, borderWidth, borderColor); + // Bottom border + texture.primaryBitmap.drawRectangle(0, size - borderWidth, size, size, borderColor); + // Left border (excluding top/bottom corners already filled) + texture.primaryBitmap.drawRectangle(0, borderWidth, borderWidth, size - borderWidth, borderColor); + // Right border + texture.primaryBitmap.drawRectangle(size - borderWidth, borderWidth, size, size - borderWidth, borderColor); + + texture.resetResampledBitmapCache(); + return texture; + } + + /** + * Generates a texture with glowing border and transparent center. + */ + private static Texture generateGlowingBorder(final int size, final Color borderColor, + final int borderWidth, final int glowIntensity, + final boolean transparent, final int maxUpscale) { + final Texture texture = new Texture(size, size, maxUpscale); + + // Clear to transparent or slight tint + final int centerAlpha = transparent ? 0 : 30; + final java.awt.Color bgColor = new java.awt.Color(borderColor.r, borderColor.g, borderColor.b, centerAlpha); + texture.graphics.setBackground(bgColor); + texture.graphics.clearRect(0, 0, size, size); + + // Draw concentric glow lines from outer to inner + for (int i = 0; i < borderWidth; i++) { + final int intensity = (int) (glowIntensity * (borderWidth - i) / borderWidth); + final int alpha = Math.max(0, 200 - i * 30); + + final java.awt.Color glowColor = new java.awt.Color( + Math.min(255, borderColor.r + intensity), + Math.min(255, borderColor.g + intensity), + Math.min(255, borderColor.b + intensity), + alpha + ); + + texture.graphics.setColor(glowColor); + texture.graphics.drawRect(i, i, size - 1 - 2 * i, size - 1 - 2 * i); + } + + texture.graphics.dispose(); + texture.resetResampledBitmapCache(); + return texture; + } + + /** + * Generates a texture with circular radial alpha gradient. + */ + private static Texture generateRadialGlow(final int size, final Color color) { + final Texture texture = new Texture(size, size, 1); + final int halfSize = size / 2; + + for (int x = 0; x < size; x++) { + for (int y = 0; y < size; y++) { + final int distanceFromCenter = (int) sqrt(pow(halfSize - x, 2) + pow(halfSize - y, 2)); + + int alpha = 255 - ((270 * distanceFromCenter) / halfSize); + if (alpha < 0) { + alpha = 0; + } + + texture.primaryBitmap.pixels[texture.primaryBitmap.getAddress(x, y)] = + (alpha << 24) | (color.r << 16) | (color.g << 8) | color.b; + } + } + + texture.resetResampledBitmapCache(); + return texture; + } + + /** + * Functional interface for texture creation. + */ + @FunctionalInterface + private interface TextureSupplier { + Texture create(); + } + + /** + * Enumeration of texture generation types for cache key differentiation. + */ + private enum TextureType { + SOLID_WITH_BORDER, + GLOWING_BORDER, + RADIAL_GLOW + } + + /** + * Cache key for texture lookup based on generation parameters. + * + *

Two textures with identical parameters should produce the same key, + * enabling cache reuse. The key includes all parameters that affect + * the visual appearance of the generated texture.

+ */ + private static final class TextureKey { + private final int size; + private final Color primaryColor; + private final Color secondaryColor; + private final int borderWidth; + private final TextureType type; + private final int glowIntensity; + private final boolean transparent; + private final int maxUpscale; + + TextureKey(final int size, final Color primaryColor, final Color secondaryColor, + final int borderWidth, final TextureType type, final int glowIntensity, + final boolean transparent, final int maxUpscale) { + this.size = size; + this.primaryColor = primaryColor; + this.secondaryColor = secondaryColor; + this.borderWidth = borderWidth; + this.type = type; + this.glowIntensity = glowIntensity; + this.transparent = transparent; + this.maxUpscale = maxUpscale; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final TextureKey that = (TextureKey) o; + + if (size != that.size) return false; + if (borderWidth != that.borderWidth) return false; + if (glowIntensity != that.glowIntensity) return false; + if (transparent != that.transparent) return false; + if (maxUpscale != that.maxUpscale) return false; + if (type != that.type) return false; + if (!primaryColor.equals(that.primaryColor)) return false; + return secondaryColor.equals(that.secondaryColor); + } + + @Override + public int hashCode() { + int result = size; + result = 31 * result + primaryColor.hashCode(); + result = 31 * result + secondaryColor.hashCode(); + result = 31 * result + borderWidth; + result = 31 * result + type.hashCode(); + result = 31 * result + glowIntensity; + result = 31 * result + (transparent ? 1 : 0); + result = 31 * result + maxUpscale; + return result; + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/package-info.java b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/package-info.java new file mode 100644 index 0000000..d319ae7 --- /dev/null +++ b/src/main/java/eu/svjatoslav/aukio/e3d/renderer/raster/texture/package-info.java @@ -0,0 +1,22 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Texture support with mipmap chains for level-of-detail rendering. + * + *

Textures provide 2D image data that can be mapped onto polygons. The mipmap + * system automatically generates scaled versions for efficient rendering at + * various distances.

+ * + *

Key classes:

+ *
    + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture} - Main texture class with mipmap support
  • + *
  • {@link eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap} - Raw pixel data for a single mipmap level
  • + *
+ * + * @see eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture + */ + +package eu.svjatoslav.aukio.e3d.renderer.raster.texture; \ No newline at end of file diff --git a/src/main/resources/eu/svjatoslav/aukio/e3d/examples/hourglass.png b/src/main/resources/eu/svjatoslav/aukio/e3d/examples/hourglass.png new file mode 100644 index 0000000000000000000000000000000000000000..47a1638610ced7ff9f6bf98d7f98c8d923343836 GIT binary patch literal 2161 zcmZ`)X*AT07yivy!fO~?3<@QZJ+e#_jb<>tvai{)4w5Wm-)6*1j9p|I*-F{B5XKTo zNwz4m56PPlMvO6Def)p;pL?GBocrASoO8e2a~~QT>OnY!IRF5FVDwR@CoKL4Y^*0a zAS`V6gqX3&TSx$?PyLJT#Bx%D0!;OEfSQk@YkwwP^i6L8K$sK&;9~)Rb|T@w13;h> z0Q`Fg0N{@SK+w0K%>;4MV0Hdm4+R|mi6U}&=83}Yt8Wtk0DtlQ0SL$|5CVWx?idu( z+`9Lr)m>xrRajJKz*L-D&Y;?Bh4m7G&~iybOv^_hwCb;%(}$v&T?IS3739_%u)*Be;jdzLQuT5= zd0GLT+N(#MJBZE0sQK`@R0&Qv9G>?0v1H-3aJ3z;-ia&8$;mr04Tya6n^LBf8^vh^ z^D#VfMbmI8WNt_(IET`zkSS9vtEgD(o<3yxC^M4+2Kv90Azr&GDn80N|IT^h^y!UI za!E-;w@-`oKtt6T6MiC*h)?L$ATbr4DiUPwxs)Zn#ZZ1U$c4zg&!imGPO@4cWsDuEIzDQ(Nc!vlg!@N*FSL^V<#R8$Dz zxL;GK*UQHxBqRtp_-w9bz_%>r{y2f|*RZJlQZdTz+IU{6p7zDOob2WapBBo_aQ-50 z&uM@XCy6oUXAyl{G~wkmA_C>(OU}wNm$PdHh!n(IhW|QC(uBW{9uoPbDK1X!92gaw zs>Ns>`9U2MEUvipuEC(^PFf5ymmggmKk84>U9Fj|eHsljEWH`!J$T@Q`VL{^496CN0pC}4z8a#fA&VXM77wYf>;}(p#p{~r@KN!~ ztdH0lBpAOkm`zg2<>kPcn5bjJ=E?;Kes=9akmhx7&ej6;C8926WML$6 zd3jkY^PQbIhLNqEo%JChq8|(tRaZj>t(IRf@W*F`z6!x$Hdbb~wz~^GOrb?x(f#09 zSS_$cx?ihz_;YVz$hP`slf><>v$I1Y$^Et4FV;2*e#i>HRQYn7$*9N+63sb4$7 z&268l^_e%`+4Pc>vgyo7@T9C+RPtaWRD=9H$*Gc7wdb~KZrA8O^!e}3 z@;a!Z&r}9&xDh)Xf;byUPYvE81wXH;iCW^Gnwy)8UijJK$Ru84o%@@b)7+1Tu^2mJUxdn5k)WVLPSIXIj(>aac1Hi0dBdHFaQ z;anxHf!p6w$QiuQ($X^C{IhnfikVN@LEI4f&=2uYSl-Rm&BPDG4uL>S?%CT@0)9+B zU7sNzF0ux>e`$`qI*HgZa1IQlPRLr-jBJ-%LZuV>l7x9xrxhWFP??+ei%9K3W>{?b zZF6(F6c54AEGJK<8jVITgfgbeBnJnzBPm$tTleHc3ZFEQory>jepOXf{HkU%Zq1bO zMqV+p!E;opP6&p=DvWa~|LwBk7i-hDRI4AKW0cb>481R&qtz;NSy)K0f$0SN zvzITaR;Ren&RCl`oVDCqmJt^qleu@(dD4@@!ozK79jax|5L|k}+%W{eyo4PYJ33eB zCmK?yrxStr;La!6C8jlBmyRdK=TGW!Y=t1U4kG-Gvkzg`6&1Wfl(ekJY}J-m!(dLF zs^5OK3Rzf4Z=^(FVxEt7Uqv9~FpOOGfq@Tw9ZgMyPoJ_J+iCXUn>d^T^olT9oUI^y z1X5Bj0&xL6=!EVaD2j(ExXqPhxN2ZDtWkf5LGRkz-Hn}BS>$vHbrV#uPps{V$KHyZF1E5O7&ZSw&t+P5!c~xw1O^iV9p=O;$+- luA~HGGCKGl!@c{ip6;Rl-{9f!8+~E`Fla+mjn18z{{SeC1P}lK literal 0 HcmV?d00001 diff --git a/src/test/java/eu/svjatoslav/aukio/cfg/AukioConfigTest.java b/src/test/java/eu/svjatoslav/aukio/cfg/AukioConfigTest.java new file mode 100644 index 0000000..ca8f8c7 --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/cfg/AukioConfigTest.java @@ -0,0 +1,206 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.cfg; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.yaml.snakeyaml.Yaml; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Verifies {@link AukioConfig}: typed getters with defaults, mtime + * freshness on external edits, locked merge preserving hand edits, + * atomic concurrent writes, section flattening. + */ +public class AukioConfigTest { + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + + private Path newConfig(final String content) throws Exception { + final Path file = tmp.newFile("config.yaml").toPath(); + Files.writeString(file, content); + return file; + } + + /** Emulates an external writer (editor, other JVM): tmp + rename. */ + private static void replaceAtomically(final Path file, + final String content) + throws Exception { + final Path swap = file.resolveSibling("swap-" + file.getFileName()); + Files.writeString(swap, content); + Files.move(swap, file, StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } + + @Test + public void missingFileYieldsDefaults() throws Exception { + final Path absent = tmp.getRoot().toPath().resolve("absent.yaml"); + final AukioConfig cfg = AukioConfig.forFile(absent); + assertEquals("d", cfg.getString("any.key", "d")); + assertEquals(6.5, cfg.getDouble("any.key", 6.5), 0.0); + assertEquals(5, cfg.getInt("any.key", 5)); + assertTrue(cfg.getBoolean("any.key", true)); + assertTrue(cfg.getStringMap("fo4").isEmpty()); + } + + @Test + public void setCreatesMissingFileAndParents() throws Exception { + final Path nested = tmp.getRoot().toPath() + .resolve("sub/dir/config.yaml"); + AukioConfig.forFile(nested).setString("app.state.lastEnv", "fo4"); + assertEquals("fo4", AukioConfig.forFile(nested) + .getString("app.state.lastEnv", null)); + } + + @Test + public void typedGettersReadYamlScalars() throws Exception { + final Path file = newConfig( + "version: 1\ne3d:\n" + + " ipdCm: 6.3\n" + + " telemetryIntervalSeconds: 9\n" + + " logDir: /tmp/x\n" + + " stereo: true\n"); + final AukioConfig cfg = AukioConfig.forFile(file); + assertEquals(6.3, cfg.getDouble("e3d.ipdCm", 0.0), 1e-9); + assertEquals(9, cfg.getInt("e3d.telemetryIntervalSeconds", 0)); + assertEquals("/tmp/x", cfg.getString("e3d.logDir", null)); + assertTrue(cfg.getBoolean("e3d.stereo", false)); + assertEquals(6.5, cfg.getDouble("e3d.absent", 6.5), 0.0); + assertNull(cfg.getString("e3d.logDir.nested", null)); + } + + @Test + public void externalEditBecomesVisibleOnNextGet() throws Exception { + final Path file = newConfig("e3d:\n ipdCm: 6.1\n"); + final AukioConfig cfg = AukioConfig.forFile(file); + assertEquals(6.1, cfg.getDouble("e3d.ipdCm", 0.0), 1e-9); + replaceAtomically(file, "e3d:\n ipdCm: 6.9\n"); + assertEquals(6.9, cfg.getDouble("e3d.ipdCm", 0.0), 1e-9); + } + + @Test + public void setMergesWithoutDroppingOtherKeys() throws Exception { + final Path file = newConfig("version: 1\nfo4:\n" + + " path: /games/FO4\n spawnAt: \"1,2,3,4\"\n"); + final AukioConfig cfg = AukioConfig.forFile(file); + cfg.setDouble("e3d.ipdCm", 6.3); + final Map raw = + new Yaml().load(Files.readString(file)); + final Map fo4 = cast(raw.get("fo4")); + assertEquals("/games/FO4", fo4.get("path")); + assertEquals("1,2,3,4", fo4.get("spawnAt")); + assertEquals(1, raw.get("version")); + assertEquals(6.3, + ((Map) raw.get("e3d")).get("ipdCm")); + } + + @Test + public void setSurvivesInterleavedHandEdit() throws Exception { + final Path file = newConfig("fo4:\n path: /games/FO4\n" + + " spawnAt: \"1,2,3,4\"\ne3d:\n ipdCm: 6.1\n"); + final AukioConfig cfg = AukioConfig.forFile(file); + cfg.setDouble("e3d.ipdCm", 6.3); + // user rewrites the file between the writer's read and write, + // changing fo4 values but keeping every section + replaceAtomically(file, "fo4:\n path: /other/FO4\n" + + " spawnAt: \"5,6,7,8\"\ne3d:\n ipdCm: 6.1\n"); + cfg.setString("app.state.lastEnv", "fallout4"); + final Map raw = + new Yaml().load(Files.readString(file)); + final Map fo4 = cast(raw.get("fo4")); + assertEquals("hand edit must survive the app's write", + "/other/FO4", fo4.get("path")); + assertEquals("5,6,7,8", fo4.get("spawnAt")); + assertEquals(6.1, + ((Map) raw.get("e3d")).get("ipdCm")); + assertEquals("fallout4", + ((Map) ((Map) + raw.get("app")).get("state")).get("lastEnv")); + } + + @Test + public void concurrentSetsFromManyThreadsAllPersist() throws Exception { + final Path file = newConfig("app:\n state: {}\n"); + final AukioConfig cfg = AukioConfig.forFile(file); + final int threads = 8; + final int perThread = 25; + final ExecutorService pool = Executors.newFixedThreadPool(threads); + final CountDownLatch start = new CountDownLatch(1); + final java.util.List> futures = + new java.util.ArrayList<>(); + for (int t = 0; t < threads; t++) { + final int id = t; + futures.add(pool.submit(() -> { + try { + start.await(); + for (int i = 0; i < perThread; i++) + cfg.setString("app.state.k" + id + "_" + i, + "v" + id + "_" + i); + } catch (final Exception e) { + throw new RuntimeException(e); + } + })); + } + start.countDown(); + pool.shutdown(); + assertTrue("writers finished in time", + pool.awaitTermination(60, TimeUnit.SECONDS)); + for (final java.util.concurrent.Future f : futures) + f.get(); // surface writer failures the pool would hide + + final Map raw = + new Yaml().load(Files.readString(file)); + final Map state = cast( + cast(raw.get("app")).get("state")); + assertEquals(threads * perThread, state.size()); + for (int t = 0; t < threads; t++) + for (int i = 0; i < perThread; i++) + assertEquals("v" + t + "_" + i, + state.get("k" + t + "_" + i)); + } + + @Test + public void stringMapFlattensSectionScalars() throws Exception { + final Path file = newConfig("fo4:\n" + + " path: /games/FO4\n" + + " spawnAt: \"1,2,3,4\"\n" + + " nested:\n leaf: 7\n"); + final Map fo4 = + AukioConfig.forFile(file).getStringMap("fo4"); + assertEquals("/games/FO4", fo4.get("path")); + assertEquals("1,2,3,4", fo4.get("spawnAt")); + assertEquals("7", fo4.get("nested.leaf")); + assertFalse(fo4.containsKey("absent")); + assertTrue(AukioConfig.forFile(file).getStringMap("absent") + .isEmpty()); + } + + @Test + public void samePathReturnsSameInstance() throws Exception { + final Path file = newConfig("e3d:\n ipdCm: 6.5\n"); + assertTrue(AukioConfig.forFile(file) + == AukioConfig.forFile(file.toAbsolutePath().normalize())); + } + + @SuppressWarnings("unchecked") + private static Map cast(final Object map) { + return (Map) map; + } +} diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/TextLineTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/TextLineTest.java new file mode 100644 index 0000000..593fc41 --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/TextLineTest.java @@ -0,0 +1,116 @@ +/* + * Aukio - System for data storage, computation, exploration and interaction. + * Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + * +*/ + +package eu.svjatoslav.aukio.e3d.gui.textEditorComponent; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TextLineTest { + + @Test + public void testAddIndent() { + TextLine textLine = new TextLine("test"); + textLine.addIndent(4); + assertEquals(" test", textLine.toString()); + + textLine = new TextLine(); + textLine.addIndent(4); + assertEquals("", textLine.toString()); + } + + @Test + public void testCutFromBeginning() { + TextLine textLine = new TextLine("test"); + textLine.cutFromBeginning(2); + assertEquals("st", textLine.toString()); + + textLine = new TextLine("test"); + textLine.cutFromBeginning(4); + assertEquals("", textLine.toString()); + + textLine = new TextLine("test"); + textLine.cutFromBeginning(5); + assertEquals("", textLine.toString()); + + textLine = new TextLine("test"); + textLine.cutFromBeginning(100); + assertEquals("", textLine.toString()); + } + + @Test + public void testCutSubString() { + TextLine textLine = new TextLine("test"); + assertEquals("es", textLine.cutSubString(1, 3)); + assertEquals("tt", textLine.toString()); + + textLine = new TextLine("test"); + assertEquals("st ", textLine.cutSubString(2, 5)); + assertEquals("te", textLine.toString()); + } + + @Test + public void testGetCharForLocation() { + final TextLine textLine = new TextLine("test"); + assertEquals('s', textLine.getCharForLocation(2)); + assertEquals('t', textLine.getCharForLocation(3)); + assertEquals(' ', textLine.getCharForLocation(4)); + } + + @Test + public void testGetIndent() { + final TextLine textLine = new TextLine(" test"); + assertEquals(3, textLine.getIndent()); + } + + @Test + public void testGetLength() { + final TextLine textLine = new TextLine("test"); + assertEquals(4, textLine.getLength()); + } + + @Test + public void testInsertCharacter() { + TextLine textLine = new TextLine("test"); + textLine.insertCharacter(1, 'o'); + assertEquals("toest", textLine.toString()); + + textLine = new TextLine("test"); + textLine.insertCharacter(5, 'o'); + assertEquals("test o", textLine.toString()); + + } + + @Test + public void testIsEmpty() { + TextLine textLine = new TextLine(""); + assertEquals(true, textLine.isEmpty()); + + textLine = new TextLine(" "); + assertEquals(true, textLine.isEmpty()); + + textLine = new TextLine("l"); + assertEquals(false, textLine.isEmpty()); + } + + @Test + public void testRemoveCharacter() { + TextLine textLine = new TextLine("test"); + textLine.removeCharacter(0); + assertEquals("est", textLine.toString()); + + textLine = new TextLine("test"); + textLine.removeCharacter(3); + assertEquals("tes", textLine.toString()); + + textLine = new TextLine("test"); + textLine.removeCharacter(4); + assertEquals("test", textLine.toString()); + } + +} diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/package-info.java b/src/test/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/package-info.java new file mode 100644 index 0000000..dfcdecd --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/e3d/gui/textEditorComponent/package-info.java @@ -0,0 +1,13 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ + +/** + * Unit tests for the text editor component. + * + *

Tests for {@link eu.svjatoslav.aukio.e3d.gui.textEditorComponent.TextLine} + * and related text processing functionality.

+ */ + +package eu.svjatoslav.aukio.e3d.gui.textEditorComponent; \ No newline at end of file diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/headless/HeadlessToolkitTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/headless/HeadlessToolkitTest.java new file mode 100644 index 0000000..c75d42c --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/e3d/headless/HeadlessToolkitTest.java @@ -0,0 +1,85 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.headless; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.geometry.Camera; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.ShapeCollection; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import org.junit.Test; + +import java.awt.image.BufferedImage; +import java.io.File; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Verifies the headless toolkit: pose parsing round-trip, snapshot + * rendering actually paints, pixel assertions agree with the render, + * golden comparison passes/fails deterministically. + */ +public class HeadlessToolkitTest { + + /** A red triangle 100 units in front of the origin-facing camera. */ + private static ShapeCollection triangleScene() { + final ShapeCollection scene = new ShapeCollection(); + scene.addShape(new SolidPolygon( + new Point3D(-100, 0, 100), new Point3D(100, 0, 100), + new Point3D(0, 100, 100), Color.RED)); + return scene; + } + + @Test + public void poseRoundTrip() { + final Camera camera = Snapshot.cameraFromPose("290.31, -35.59, -2.10, -0.58, -0.15, -0.00"); + final String pose = Snapshot.poseString(camera); + // parsed back, the pose string must be identical (same 2-decimal precision) + assertEquals("290.31, -35.59, -2.10, -0.58, -0.15, -0.00", pose); + } + + @Test + public void renderPaintsTriangle() { + final BufferedImage image = Snapshot.render(triangleScene(), null, + "0, 0, 0, 0, 0, 0", 320, 240); + final long red = PixelAssertions.countColor(image, 0xFF0000); + assertTrue("red triangle should paint, redPixels=" + red, red > 5000); + assertTrue("most of the frame stays background", + PixelAssertions.unpaintedFraction(image, 0) > 0.5); + } + + @Test + public void unpaintedFractionDetectsRegion() { + final BufferedImage image = Snapshot.render(triangleScene(), null, + "0, 0, 0, 0, 0, 0", 320, 240); + // the triangle is centered; corners must be unpainted + final double cornerBand = PixelAssertions.unpaintedFraction(image, 0, 0, 0, 0.1, 0.1); + assertEquals(1.0, cornerBand, 0.001); + } + + @Test + public void goldenCompareExact() throws Exception { + final BufferedImage image = Snapshot.render(triangleScene(), null, + "0, 0, 0, 0, 0, 0", 320, 240); + final File golden = File.createTempFile("golden", ".png"); + golden.deleteOnExit(); + Snapshot.save(image, golden.getAbsolutePath()); + + final GoldenImage.Result exact = GoldenImage.compare(image, golden, 0, 0.0); + assertTrue(exact.toString(), exact.passed); + assertEquals(0, exact.diffPixels); + + // paint one pixel differently: comparison must now fail at tolerance 0 + image.setRGB(10, 10, 0x00FF00); + final GoldenImage.Result drifted = GoldenImage.compare(image, golden, 0, 0.0); + assertTrue(!drifted.passed); + assertEquals(1, drifted.diffPixels); + + // ... but pass when one differing pixel is within the allowed fraction + final GoldenImage.Result tolerated = GoldenImage.compare(image, golden, 0, 0.001); + assertTrue(tolerated.toString(), tolerated.passed); + } +} diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/math/QuaternionTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/math/QuaternionTest.java new file mode 100644 index 0000000..8745219 --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/e3d/math/QuaternionTest.java @@ -0,0 +1,59 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.math; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class QuaternionTest { + + @Test + public void testFromAnglesProducesValidMatrix() { + final Quaternion quaternion = Quaternion.fromAngles(0.5, 0.3); + 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(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/aukio/e3d/math/TransformStackTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/math/TransformStackTest.java new file mode 100644 index 0000000..ed5275b --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/e3d/math/TransformStackTest.java @@ -0,0 +1,139 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.math; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import org.junit.Test; + +import java.util.Random; + +import static org.junit.Assert.assertEquals; + +public class TransformStackTest { + + private static final double EPSILON = 1e-6; + + @Test + public void transformWithEmptyStackIsIdentity() { + final TransformStack stack = new TransformStack(); + final Point3D p = new Point3D(10, 20, 30); + final Point3D result = new Point3D(); + + stack.transform(p, result); + + assertEquals(p.x, result.x, EPSILON); + assertEquals(p.y, result.y, EPSILON); + assertEquals(p.z, result.z, EPSILON); + } + + @Test + public void transformMatchesSequentialApplication() { + final Random rnd = new Random(42); + + for (int depth = 1; depth <= 8; depth++) { + final Transform[] chain = new Transform[depth]; + final TransformStack stack = new TransformStack(); + for (int i = 0; i < depth; i++) { + chain[i] = Transform.fromAngles( + (rnd.nextDouble() - 0.5) * 1000, + (rnd.nextDouble() - 0.5) * 1000, + (rnd.nextDouble() - 0.5) * 1000, + (rnd.nextDouble() - 0.5) * Math.PI * 2, + (rnd.nextDouble() - 0.5) * Math.PI, + (rnd.nextDouble() - 0.5) * Math.PI); + stack.addTransform(chain[i]); + } + + for (int k = 0; k < 100; k++) { + final Point3D p = new Point3D( + (rnd.nextDouble() - 0.5) * 2000, + (rnd.nextDouble() - 0.5) * 2000, + (rnd.nextDouble() - 0.5) * 2000); + + // Oracle: documented semantics — transforms applied in reverse + // order of insertion (last added = first applied) + final Point3D expected = new Point3D(p); + for (int i = depth - 1; i >= 0; i--) { + chain[i].transform(expected); + } + + final Point3D result = new Point3D(); + stack.transform(p, result); + + assertEquals("depth " + depth + " x", expected.x, result.x, EPSILON); + assertEquals("depth " + depth + " y", expected.y, result.y, EPSILON); + assertEquals("depth " + depth + " z", expected.z, result.z, EPSILON); + } + } + } + + @Test + public void dropTransformRestoresParentState() { + final Transform a = Transform.fromAngles(100, 0, 0, 0.3, 0.1, 0); + final Transform b = Transform.fromAngles(0, 50, 0, 0, 0.5, 0.2); + final Transform c = Transform.fromAngles(0, 0, 500, 1.0, 0, 0.4); + + final TransformStack stack = new TransformStack(); + stack.addTransform(a); + stack.addTransform(b); + stack.dropTransform(); + stack.addTransform(c); + + final TransformStack reference = new TransformStack(); + reference.addTransform(a); + reference.addTransform(c); + + final Point3D p = new Point3D(7, -13, 42); + final Point3D result = new Point3D(); + final Point3D expected = new Point3D(); + stack.transform(p, result); + reference.transform(p, expected); + + assertEquals(expected.x, result.x, EPSILON); + assertEquals(expected.y, result.y, EPSILON); + assertEquals(expected.z, result.z, EPSILON); + } + + @Test + public void transformComposesEagerlyAtPushTime() { + final Transform transform = Transform.fromAngles(10, 20, 30, 0.5, 0.2, 0.1); + + final TransformStack stack = new TransformStack(); + stack.addTransform(transform); + + // Expected result uses the values the transform had when pushed + final Point3D p = new Point3D(1, 2, 3); + final Point3D expected = new Point3D(p); + final Transform snapshot = transform.clone(); + snapshot.transform(expected); + + // Mutating the transform after pushing must NOT affect the stack: + // composition is an eager snapshot taken at push time + transform.set(-999, 888, -777, 2.5, -1.5, 0.9); + + final Point3D result = new Point3D(); + stack.transform(p, result); + + assertEquals(expected.x, result.x, EPSILON); + assertEquals(expected.y, result.y, EPSILON); + assertEquals(expected.z, result.z, EPSILON); + } + + @Test + public void clearResetsStackToIdentity() { + final TransformStack stack = new TransformStack(); + stack.addTransform(Transform.fromAngles(1, 2, 3, 0.5, 0.2, 0.1)); + stack.addTransform(Transform.fromAngles(4, 5, 6, 0.1, 0.9, 0.3)); + stack.clear(); + + final Point3D p = new Point3D(10, 20, 30); + final Point3D result = new Point3D(); + stack.transform(p, result); + + assertEquals(p.x, result.x, EPSILON); + assertEquals(p.y, result.y, EPSILON); + assertEquals(p.z, result.z, EPSILON); + } +} diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolumeTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolumeTest.java new file mode 100644 index 0000000..64bd768 --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/octree/OctreeVolumeTest.java @@ -0,0 +1,49 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.octree; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Cell-pool behavior of {@link OctreeVolume} — most notably the + * exhaustion path, which used to hang in an infinite rescan loop. + */ +public class OctreeVolumeTest { + + /** + * Allocating more cells than the pool holds must fail loudly + * ({@link IllegalStateException}), not hang. The 5s timeout fails the + * test on the pre-fix implementation (infinite loop). + */ + @Test(timeout = 5000) + public void cellPoolExhaustionFailsLoudly() { + final OctreeVolume volume = new OctreeVolume(); + volume.initWorld(8, 64); // tiny pool: 8 cells + + try { + // master cell + up to 8 more allocations — must throw by then + for (int i = 0; i < 16; i++) + volume.makeNewCell(0x808080, 0); + fail("expected IllegalStateException on pool exhaustion"); + } catch (final IllegalStateException e) { + assertTrue("message should name the pool capacity: " + e.getMessage(), + e.getMessage().contains("8")); + } + } + + /** Sanity: within capacity, allocation keeps working and cells are solid. */ + @Test(timeout = 5000) + public void allocationWithinCapacityWorks() { + final OctreeVolume volume = new OctreeVolume(); + volume.initWorld(16, 64); + + final int pointer = volume.makeNewCell(0x123456, 7); + assertTrue(pointer >= 0); + assertTrue(volume.isCellSolid(pointer)); + } +} diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformTest.java new file mode 100644 index 0000000..6eb00d5 --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/ParallelTransformTest.java @@ -0,0 +1,286 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; +import eu.svjatoslav.aukio.e3d.math.Transform; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonSphere; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe.WireframeCube; +import org.junit.After; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import eu.svjatoslav.aukio.e3d.math.TransformStack; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Verifies that the parallel transform+sort pipeline produces exactly the + * same render queue as the serial pipeline: same shapes, same order, same + * culling decisions. + */ +public class ParallelTransformTest { + + private static final int W = 1280, H = 720; + private static final double EPSILON = 1e-9; + + private ExecutorService executor; + + @After + public void tearDown() { + if (executor != null) { + executor.shutdownNow(); + } + } + + private ShapeCollection buildScene(final ViewPanel panel) { + final ShapeCollection scene = panel.getRootShapeCollection(); + panel.getCamera().getTransform().setTranslation(new Point3D(0, 0, -600)); + + // Top-level spheres: each expands to hundreds of triangles internally + for (int i = 0; i < 40; i++) { + scene.addShape(new SolidPolygonSphere( + new Point3D((i % 8 - 4) * 60, (i / 8 - 2) * 60, 400), + 25, 12, Color.GREEN)); + } + + // Many cheap wireframe cubes -> enough top-level items to fork on + final LineAppearance appearance = new LineAppearance(2.0, Color.CYAN); + for (int i = 0; i < 200; i++) { + scene.addShape(new WireframeCube( + new Point3D((i % 20 - 10) * 40, (i / 20 - 5) * 40, 700), + 15, appearance)); + } + + // Nested composite with its own transform -> transform stacking + final AbstractCompositeShape nested = new AbstractCompositeShape(new Point3D(50, -50, 500)); + nested.setTransform(Transform.fromAngles(50, -50, 500, 0.3, 0.2, 0.1)); + nested.addShape(new SolidPolygonCube(new Point3D(0, 0, 0), 30, Color.RED)); + nested.addShape(new SolidPolygonSphere(new Point3D(80, 0, 40), 20, 10, Color.BLUE)); + scene.addShape(nested); + + // Composite entirely behind the camera -> frustum culling inside workers + final AbstractCompositeShape offscreen = new AbstractCompositeShape(new Point3D(0, 0, -5000)); + offscreen.addShape(new SolidPolygonCube(new Point3D(0, 0, 0), 30, Color.RED)); + scene.addShape(offscreen); + + // Quad -> N-gon triangulation path + scene.addShape(SolidPolygon.quad( + new Point3D(-100, -100, 300), new Point3D(100, -100, 300), + new Point3D(100, 100, 300), new Point3D(-100, 100, 300), + Color.WHITE)); + + return scene; + } + + private int[] snapshotIds(final ShapeCollection scene) { + final List queue = scene.getQueuedShapes(); + final int[] ids = new int[queue.size()]; + for (int i = 0; i < ids.length; i++) { + ids[i] = queue.get(i).shapeId; + } + return ids; + } + + private double[] snapshotZs(final ShapeCollection scene) { + final List queue = scene.getQueuedShapes(); + final double[] zs = new double[queue.size()]; + for (int i = 0; i < zs.length; i++) { + zs[i] = queue.get(i).getZ(0); + } + return zs; + } + + @Test + public void parallelTransformMatchesSerialPipeline() { + System.setProperty("java.awt.headless", "true"); + final ViewPanel panel = new ViewPanel(); + final ShapeCollection scene = buildScene(panel); + + // Serial run: no executor -> serial fallback path + final RenderingContext serialCtx = new RenderingContext(W, H, 1); + serialCtx.prepareForNewFrameRendering(); + scene.transformShapes(panel.getCamera(), serialCtx); + scene.sortShapes(); + final int[] serialIds = snapshotIds(scene); + final double[] serialZs = snapshotZs(scene); + final int serialTotal = serialCtx.cullingStatistics.totalComposites.get(); + final int serialCulled = serialCtx.cullingStatistics.culledComposites.get(); + + // Parallel run: executor set -> forked traversal + executor = Executors.newFixedThreadPool(8); + final RenderingContext parallelCtx = new RenderingContext(W, H, 1); + parallelCtx.transformExecutor = executor; + parallelCtx.prepareForNewFrameRendering(); + parallelCtx.prepareForNewFrameRendering(); // distinct frameNumber -> full re-transform + scene.transformShapes(panel.getCamera(), parallelCtx); + scene.sortShapes(); + final int[] parallelIds = snapshotIds(scene); + final double[] parallelZs = snapshotZs(scene); + + // Same render queue, same order (sort is deterministic: Z then shapeId) + assertEquals("queued shape count", serialIds.length, parallelIds.length); + assertTrue("scene must be big enough to exercise parallel paths", + serialIds.length > 8192); + for (int i = 0; i < serialIds.length; i++) { + assertEquals("shapeId at position " + i, serialIds[i], parallelIds[i]); + assertEquals("Z at position " + i, serialZs[i], parallelZs[i], EPSILON); + } + + // Same culling decisions (and thread-safe counters) + assertEquals(serialTotal, parallelCtx.cullingStatistics.totalComposites.get()); + assertEquals(serialCulled, parallelCtx.cullingStatistics.culledComposites.get()); + assertTrue("offscreen composite must be culled", serialCulled >= 1); + + // Sortedness property: Z descending, shapeId ascending tiebreak. + // Tiebreak comparison must be exact, matching the comparator's + // double semantics — nearly-equal Z values are NOT a tie. + for (int i = 1; i < parallelIds.length; i++) { + assertTrue("Z order at " + i, parallelZs[i - 1] >= parallelZs[i]); + if (parallelZs[i - 1] == parallelZs[i]) { + assertTrue("shapeId tiebreak at " + i, parallelIds[i - 1] < parallelIds[i]); + } + } + } + + @Test + public void nestedHeavyCompositeForksInternally() { + System.setProperty("java.awt.headless", "true"); + final ViewPanel panel = new ViewPanel(); + final ShapeCollection scene = panel.getRootShapeCollection(); + panel.getCamera().getTransform().setTranslation(new Point3D(0, 0, -600)); + + // Few root children: root stays below the parallel threshold and + // transforms serially, so any fork must come from the nested level + scene.addShape(new SolidPolygonSphere(new Point3D(-200, 0, 400), 25, 10, Color.GREEN)); + scene.addShape(new SolidPolygonSphere(new Point3D(200, 0, 400), 25, 10, Color.RED)); + + // One outsized nested composite, well above the fork threshold + final AbstractCompositeShape giant = new AbstractCompositeShape(new Point3D(0, 0, 300)); + final LineAppearance appearance = new LineAppearance(2.0, Color.CYAN); + for (int i = 0; i < 200; i++) { + giant.addShape(new WireframeCube( + new Point3D((i % 20 - 10) * 40, (i / 20 - 5) * 40, 200), + 15, appearance)); + } + scene.addShape(giant); + + // Serial run + final RenderingContext serialCtx = new RenderingContext(W, H, 1); + serialCtx.prepareForNewFrameRendering(); + scene.transformShapes(panel.getCamera(), serialCtx); + scene.sortShapes(); + final int[] serialIds = snapshotIds(scene); + final double[] serialZs = snapshotZs(scene); + + // Parallel run + executor = Executors.newFixedThreadPool(8); + final RenderingContext parallelCtx = new RenderingContext(W, H, 1); + parallelCtx.transformExecutor = executor; + parallelCtx.prepareForNewFrameRendering(); + parallelCtx.prepareForNewFrameRendering(); + scene.transformShapes(panel.getCamera(), parallelCtx); + scene.sortShapes(); + final int[] parallelIds = snapshotIds(scene); + final double[] parallelZs = snapshotZs(scene); + + // The nested composite must have forked: root has only 3 children + // (below the threshold), so all chunk tasks are nested-level + assertTrue("nested composite must fork its own children", + parallelCtx.lastTransformTaskCount >= 2); + + // Identical render queue + assertEquals("queued shape count", serialIds.length, parallelIds.length); + assertTrue("scene must be big enough to exercise the fork", + serialIds.length > 1000); + for (int i = 0; i < serialIds.length; i++) { + assertEquals("shapeId at position " + i, serialIds[i], parallelIds[i]); + assertEquals("Z at position " + i, serialZs[i], parallelZs[i], EPSILON); + } + } + + @Test + public void renderListRebuildDuringInFlightChunkTasksUsesSnapshottedRenderList() throws Exception { + System.setProperty("java.awt.headless", "true"); + + // One composite with many children: 4 permanent + 4096 in a group + // that will be hidden mid-flight to force a render-list rebuild that + // REASSIGNS cachedRenderList to a much smaller list. + final AbstractCompositeShape composite = + new AbstractCompositeShape(new Point3D(0, 0, 400)); + composite.setRootComposite(true); // skip frustum culling entirely + for (int i = 0; i < 4; i++) { + composite.addShape(SolidPolygon.triangle( + new Point3D(i * 10, 0, 0), new Point3D(i * 10 + 5, 0, 0), + new Point3D(i * 10, 5, 0), Color.GREEN), "keep"); + } + for (int i = 0; i < 4096; i++) { + composite.addShape(SolidPolygon.triangle( + new Point3D(i % 64, i / 64, 0), new Point3D(i % 64 + 1, i / 64, 0), + new Point3D(i % 64, i / 64 + 1, 0), Color.RED), "bulk"); + } + + executor = Executors.newFixedThreadPool(4); + + // Occupy every pool thread: the chunk tasks submitted below queue + // up but cannot start, reproducing the pipeline state where the + // NEXT pass's tree walk begins while this pass's chunks are + // still pending (the drain runs in the async continuation). + final CountDownLatch blockersStarted = new CountDownLatch(4); + final CountDownLatch releaseBlockers = new CountDownLatch(1); + for (int i = 0; i < 4; i++) { + executor.submit(() -> { + blockersStarted.countDown(); + try { + releaseBlockers.await(30, TimeUnit.SECONDS); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + } + assertTrue("pool blockers must be running", + blockersStarted.await(30, TimeUnit.SECONDS)); + + // Pass P: forks chunk tasks against the 4100-entry render list. + final ParallelTransformCoordinator coordinator = + new ParallelTransformCoordinator(executor); + final RenderingContext passContext = new RenderingContext(W, H, 1); + passContext.prepareForNewFrameRendering(); + passContext.transformCoordinator = coordinator; + final RenderAggregator aggregator = new RenderAggregator(); + composite.transform(new TransformStack(), aggregator, passContext); + assertTrue("composite must have forked chunk tasks", + coordinator.getSubmittedTaskCount() >= 2); + + // Pass P+1's tree walk arrives while P's chunk tasks are still + // queued: hiding "bulk" forces a render-list rebuild that reassigns + // cachedRenderList to a 4-entry list. (No coordinator on this + // context -> serial path, itself immune to the race.) + composite.hideGroup("bulk"); + final RenderingContext nextContext = new RenderingContext(W, H, 1); + nextContext.prepareForNewFrameRendering(); + composite.transform(new TransformStack(), new RenderAggregator(), nextContext); + + // Now P's chunk tasks run. They must iterate the SAME list + // instance their chunk ranges were computed against — re-reading + // the reassigned field threw IndexOutOfBoundsException (observed + // in production: "Index 258 out of bounds for length 24"). + releaseBlockers.countDown(); + coordinator.drainAndMergeInto(aggregator); + } +} diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSortTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSortTest.java new file mode 100644 index 0000000..98d318c --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/RadixLongSortTest.java @@ -0,0 +1,143 @@ +package eu.svjatoslav.aukio.e3d.renderer.raster; + +import org.junit.Test; + +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Verifies the radix sort core: key mapping reproduces the Z-comparator's + * ordering semantics, and the pair sort is sorted and stable. + */ +public class RadixLongSortTest { + + /** The comparator's Z comparison: -1/0/+1 with NaN treated as "equal". */ + private static int compareZ(final double z1, final double z2) { + if (z1 < z2) + return 1; // painter order: larger z first + if (z1 > z2) + return -1; + return 0; + } + + @Test + public void keyOrderMatchesComparator() { + final double[] specials = { + 0.0d, -0.0d, 1.0d, -1.0d, 5e-3d, 96.0d, 1e6d, + Double.MIN_VALUE, -Double.MIN_VALUE, + Double.MAX_VALUE, -Double.MAX_VALUE, + Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, + 123456.789d, -123456.789d + }; + final Random random = new Random(42); + final double[] values = new double[specials.length + 500]; + System.arraycopy(specials, 0, values, 0, specials.length); + for (int i = specials.length; i < values.length; i++) + values[i] = (random.nextDouble() - 0.5) * 2e6; + for (final double z1 : values) + for (final double z2 : values) { + final int expected = compareZ(z1, z2); + final int actual = Long.compareUnsigned( + RadixLongSort.zSortKey(z1), RadixLongSort.zSortKey(z2)); + assertTrue("z1=" + z1 + " z2=" + z2 + " expected sign " + + expected + " got " + actual, + Integer.signum(expected) == Integer.signum(actual)); + } + } + + @Test + public void minusZeroAndPlusZeroShareOneKey() { + assertEquals(RadixLongSort.zSortKey(0.0d), + RadixLongSort.zSortKey(-0.0d)); + } + + @Test + public void sortPairsIsSortedAndStable() { + final Random random = new Random(7); + for (final int n : new int[]{0, 1, 2, 100, 10000}) { + final long[] keys = new long[Math.max(n, 1)]; + final long[] keysTmp = new long[Math.max(n, 1)]; + final int[] idx = new int[Math.max(n, 1)]; + final int[] idxTmp = new int[Math.max(n, 1)]; + // Duplicate-heavy keys exercise stability + for (int i = 0; i < n; i++) { + keys[i] = random.nextInt(50); + idx[i] = i; + } + RadixLongSort.sortPairs(keys, idx, n, keysTmp, idxTmp); + for (int i = 1; i < n; i++) { + assertTrue("sorted at " + i, + Long.compareUnsigned(keys[i - 1], keys[i]) <= 0); + if (keys[i - 1] == keys[i]) + assertTrue("stable at " + i, idx[i - 1] < idx[i]); + } + } + } + + @Test + public void sortPairsHandlesFullUnsignedRange() { + final Random random = new Random(99); + final int n = 5000; + final long[] keys = new long[n]; + final long[] keysTmp = new long[n]; + final int[] idx = new int[n]; + final int[] idxTmp = new int[n]; + for (int i = 0; i < n; i++) { + keys[i] = random.nextLong(); // full range incl. "negative" + idx[i] = i; + } + RadixLongSort.sortPairs(keys, idx, n, keysTmp, idxTmp); + for (int i = 1; i < n; i++) + assertTrue(Long.compareUnsigned(keys[i - 1], keys[i]) <= 0); + } + + /** + * The parallel pair sort must produce output BIT-IDENTICAL to the + * serial one for any chunk count (stability makes the partitioning + * invisible) — golden-image determinism depends on it. + */ + @Test + public void parallelMatchesSerialBitExactly() throws Exception { + final java.util.concurrent.ExecutorService executor = + java.util.concurrent.Executors.newFixedThreadPool(4); + try { + final Random random = new Random(1234); + for (final int n : new int[]{0, 1, 7, 1000, 65536, 250000}) { + for (final int threads : new int[]{1, 2, 3, 8}) { + final long[] keys = new long[Math.max(n, 1)]; + final int[] idx = new int[Math.max(n, 1)]; + // duplicate-heavy + full-range mix exercises both + // stability and unsigned digit handling + for (int i = 0; i < n; i++) { + keys[i] = (i & 1) == 0 + ? random.nextInt(37) + : random.nextLong(); + idx[i] = i; + } + final long[] serialKeys = keys.clone(); + final int[] serialIdx = idx.clone(); + RadixLongSort.sortPairs(serialKeys, serialIdx, n, + new long[Math.max(n, 1)], new int[Math.max(n, 1)]); + + final long[] parKeys = keys.clone(); + final int[] parIdx = idx.clone(); + RadixLongSort.sortPairsParallel(parKeys, parIdx, n, + new long[Math.max(n, 1)], new int[Math.max(n, 1)], + new int[Math.max(threads, 1) * 512], + executor, threads); + + org.junit.Assert.assertArrayEquals( + "keys n=" + n + " threads=" + threads, + serialKeys, parKeys); + org.junit.Assert.assertArrayEquals( + "idx n=" + n + " threads=" + threads, + serialIdx, parIdx); + } + } + } finally { + executor.shutdownNow(); + } + } +} diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentBinningTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentBinningTest.java new file mode 100644 index 0000000..a93dec7 --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/SegmentBinningTest.java @@ -0,0 +1,286 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.renderer.raster.SegmentRenderingContext; +import eu.svjatoslav.aukio.e3d.gui.ViewPanel; +import eu.svjatoslav.aukio.e3d.math.Transform; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.AbstractCoordinateShape; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.GlowingPoint; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.Line; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonSphere; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.wireframe.WireframeCube; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.Texture; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureGenerator; +import org.junit.After; +import org.junit.Test; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Verifies that paint tile binning produces pixel-identical output to + * painting the full sorted queue in every tile: binning must only skip + * shapes that cannot touch a tile's rectangle, never shapes that can. + * + *

The scene deliberately includes shapes whose paint output extends + * past their vertex bounds on both axes (thick lines, glowing point + * billboards, text glyphs) to exercise the per-shape X/Y margins.

+ */ +public class SegmentBinningTest { + + private static final int W = 1280, H = 720; + private static final int TILES_X = 4, TILES_Y = 3; + private static final int TILE_COUNT = TILES_X * TILES_Y; + + private ExecutorService executor; + + @After + public void tearDown() { + if (executor != null) { + executor.shutdownNow(); + } + } + + private ShapeCollection buildScene(final ViewPanel panel, final int sphereCount) { + final ShapeCollection scene = panel.getRootShapeCollection(); + panel.getCamera().getTransform().setTranslation(new Point3D(0, 0, -600)); + + // Solid spheres spread across the whole viewport + for (int i = 0; i < sphereCount; i++) { + scene.addShape(new SolidPolygonSphere( + new Point3D((i % 6 - 3) * 90, (i / 6 - 2) * 80, 400), + 30, 10, Color.GREEN)); + } + + // Wireframe cubes (thin lines) + final LineAppearance appearance = new LineAppearance(2.0, Color.CYAN); + for (int i = 0; i < 30; i++) { + scene.addShape(new WireframeCube( + new Point3D((i % 10 - 5) * 70, (i / 10 - 1) * 90, 700), + 20, appearance)); + } + + // Thick lines crossing tile boundaries on both axes + scene.addShape(new Line( + new Point3D(-300, -200, 300), new Point3D(300, 250, 300), + Color.YELLOW, 8.0)); + scene.addShape(new Line( + new Point3D(-250, 250, 350), new Point3D(250, -250, 350), + Color.RED, 6.0)); + // Vertical thick line: X margin matters, Y range covers all rows + scene.addShape(new Line( + new Point3D(0, -300, 300), new Point3D(0, 300, 300), + Color.GREEN, 8.0)); + + // Glowing points (billboards): quad extends far beyond the vertex + for (int i = 0; i < 8; i++) { + scene.addShape(new GlowingPoint( + new Point3D((i % 4 - 2) * 120, (i / 4 - 0.5) * 150, 250), + 40, Color.WHITE)); + } + + // Text canvas: glyph extends beyond the character cell vertices + final TextCanvas text = new TextCanvas( + new Transform(new Point3D(-100, -50, 500)), + "Binning!", Color.WHITE, Color.BLUE); + scene.addShape(text); + + // A quad and a cube for variety + scene.addShape(SolidPolygon.quad( + new Point3D(-120, -120, 300), new Point3D(120, -120, 300), + new Point3D(120, 120, 300), new Point3D(-120, 120, 300), + Color.WHITE)); + scene.addShape(new SolidPolygonCube(new Point3D(200, 100, 350), 40, Color.RED)); + + // Large textured triangles spanning multiple tile rows. Big + // on screen — exercises textured-triangle binning, whose bounds + // formerly stayed (0,0) and binned it into the topmost segment only. + final Texture texture = TextureGenerator.solidWithBorder( + 32, Color.YELLOW, Color.WHITE, 2, 1); + scene.addShape(new TexturedTriangle( + new Vertex(new Point3D(-400, -300, 250), new Point2D(0, 0)), + new Vertex(new Point3D(400, -100, 250), new Point2D(1, 0)), + new Vertex(new Point3D(0, 400, 250), new Point2D(0.5, 1)), + texture)); + scene.addShape(new TexturedTriangle( + new Vertex(new Point3D(-500, 100, 300), new Point2D(0, 0)), + new Vertex(new Point3D(-100, -50, 300), new Point2D(1, 0)), + new Vertex(new Point3D(-300, 500, 300), new Point2D(0.5, 1)), + texture)); + + return scene; + } + + /** + * Paints the current frame's sorted queue into a fresh context using + * one full-range pass (no binning possible). + */ + private RenderingContext paintReference(final ShapeCollection scene) { + final RenderingContext reference = new RenderingContext(W, H, TILES_X, TILES_Y, 1); + scene.paintShapes(reference); + return reference; + } + + /** + * Paints the current frame's sorted queue tile-by-tile into a fresh + * context, using whatever bins are currently active. + */ + private RenderingContext paintTiled(final ShapeCollection scene) { + final RenderingContext tiled = new RenderingContext(W, H, TILES_X, TILES_Y, 1); + final int tileW = W / TILES_X; + final int tileH = H / TILES_Y; + for (int ty = 0; ty < TILES_Y; ty++) { + final int minY = ty * tileH; + final int maxY = (ty == TILES_Y - 1) ? H : (ty + 1) * tileH; + for (int tx = 0; tx < TILES_X; tx++) { + final int minX = tx * tileW; + final int maxX = (tx == TILES_X - 1) ? W : (tx + 1) * tileW; + final SegmentRenderingContext tileContext = new SegmentRenderingContext( + tiled, minY, maxY, ty * TILES_X + tx); + tileContext.renderMinX = minX; + tileContext.renderMaxX = maxX; + scene.paintShapes(tileContext); + } + } + return tiled; + } + + private void assertPixelsEqual(final RenderingContext expected, + final RenderingContext actual) { + final int tileW = W / TILES_X; + final int tileH = H / TILES_Y; + for (int i = 0; i < expected.pixels.length; i++) { + if (expected.pixels[i] != actual.pixels[i]) { + final int x = i % W; + final int y = i / W; + fail("pixel mismatch at (" + x + "," + y + ") tile (" + + Math.min(x / tileW, TILES_X - 1) + "," + + Math.min(y / tileH, TILES_Y - 1) + ")" + + ": expected=" + Integer.toHexString(expected.pixels[i]) + + " actual=" + Integer.toHexString(actual.pixels[i])); + } + } + } + + private void transformAndSort(final ViewPanel panel, final ShapeCollection scene, + final RenderingContext context) { + context.prepareForNewFrameRendering(); + scene.transformShapes(panel.getCamera(), context); + scene.sortShapes(); + } + + @Test + public void serialBinningMatchesFullQueuePaint() { + System.setProperty("java.awt.headless", "true"); + final ViewPanel panel = new ViewPanel(); + final ShapeCollection scene = buildScene(panel, 24); + + // Transform and sort once; paint is read-only on shape state, + // so the same frame can be painted into multiple buffers. + transformAndSort(panel, scene, new RenderingContext(W, H, TILES_X, TILES_Y, 1)); + + // The scene must contain textured triangles spanning multiple + // tiles, otherwise the binning-bounds regression (bounds left at + // 0,0 -> binned into the topmost segment only) is not covered + boolean anyTextured = false; + for (final AbstractCoordinateShape shape : scene.getQueuedShapes()) { + if (shape instanceof TexturedTriangle) { + anyTextured = true; + break; + } + } + assertTrue("scene must contain textured triangles", + anyTextured); + + final RenderingContext reference = paintReference(scene); + + // Serial binning (null executor) + scene.binShapesForTiles(TILES_X, TILES_Y, 0, W, H, null); + final int[] binSizes = scene.getBinSizes(); + assertNotNull("bins must be built", binSizes); + assertEquals(TILE_COUNT, binSizes.length); + + // Binning must reduce total per-tile iterations versus every + // tile iterating the whole queue + final int queueSize = scene.getQueuedShapeCount(); + int totalBinEntries = 0; + for (final int size : binSizes) { + totalBinEntries += size; + } + assertTrue("binning must reduce total paint iterations (" + + totalBinEntries + " >= " + (TILE_COUNT * queueSize) + ")", + totalBinEntries < TILE_COUNT * queueSize); + + assertPixelsEqual(reference, paintTiled(scene)); + } + + @Test + public void parallelBinningMatchesSerialAndFullQueuePaint() { + System.setProperty("java.awt.headless", "true"); + final ViewPanel panel = new ViewPanel(); + // Enough shapes to exceed the parallel binning threshold (8192) + final ShapeCollection scene = buildScene(panel, 80); + + transformAndSort(panel, scene, new RenderingContext(W, H, TILES_X, TILES_Y, 1)); + final int queueSize = scene.getQueuedShapeCount(); + assertTrue("scene must exceed the parallel binning threshold", + queueSize > 8192); + final RenderingContext reference = paintReference(scene); + + // Serial bins -> tiled paint A + scene.binShapesForTiles(TILES_X, TILES_Y, 0, W, H, null); + final int[] serialBinSizes = scene.getBinSizes(); + final RenderingContext serialTiled = paintTiled(scene); + + // Parallel bins -> tiled paint B + executor = Executors.newFixedThreadPool(8); + scene.binShapesForTiles(TILES_X, TILES_Y, 0, W, H, executor); + final int[] parallelBinSizes = scene.getBinSizes(); + final RenderingContext parallelTiled = paintTiled(scene); + + // Same bin layout, same pixels + org.junit.Assert.assertArrayEquals(serialBinSizes, parallelBinSizes); + assertPixelsEqual(reference, serialTiled); + assertPixelsEqual(reference, parallelTiled); + } + + @Test + public void fullRangeContextFallsBackToFullQueue() { + System.setProperty("java.awt.headless", "true"); + final ViewPanel panel = new ViewPanel(); + final ShapeCollection scene = buildScene(panel, 24); + + final RenderingContext context = new RenderingContext(W, H, TILES_X, TILES_Y, 1); + transformAndSort(panel, scene, context); + scene.binShapesForTiles(TILES_X, TILES_Y, 0, W, H, null); + + // A full-range context matches no single tile: must paint the full + // queue (verified by it producing non-background pixels at all) + scene.paintShapes(context); + boolean anyPainted = false; + for (final int pixel : context.pixels) { + if (pixel != 0) { + anyPainted = true; + break; + } + } + assertTrue("full-range paint must render shapes", anyPainted); + } +} diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangleBlendTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangleBlendTest.java new file mode 100644 index 0000000..50b9418 --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangleBlendTest.java @@ -0,0 +1,239 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.util.Random; + +import static org.junit.Assert.assertEquals; + +/** + * Pixel-exactness proof for the optimized textured scanline renderer: + * the one-multiply alpha blend and the clamp-free fast path must produce + * output identical to the legacy implementation, bit for bit. + */ +public class TexturedTriangleBlendTest { + + /** Legacy two-multiply blend, the original semantics. */ + private static int legacyBlendChannel(final int src, final int dest, final int alpha) { + return ((dest * (255 - alpha)) + (src * alpha)) >> 8; + } + + /** Optimized one-multiply blend, must equal legacy for every input. */ + private static int fastBlendChannel(final int src, final int dest, final int alpha) { + return dest + ((alpha * (src - dest) - dest) >> 8); + } + + @Test + public void oneMultiplyBlendMatchesLegacyBlend() { + final int[] channelValues = {0, 1, 2, 63, 127, 128, 200, 254, 255}; + for (int alpha = 0; alpha <= 255; alpha++) { + for (final int src : channelValues) { + for (final int dest : channelValues) { + assertEquals("src=" + src + " dest=" + dest + " alpha=" + alpha, + legacyBlendChannel(src, dest, alpha), + fastBlendChannel(src, dest, alpha)); + } + } + } + final Random random = new Random(42); + for (int i = 0; i < 1_000_000; i++) { + final int src = random.nextInt(256); + final int dest = random.nextInt(256); + final int alpha = random.nextInt(256); + assertEquals("src=" + src + " dest=" + dest + " alpha=" + alpha, + legacyBlendChannel(src, dest, alpha), + fastBlendChannel(src, dest, alpha)); + } + } + + /** + * Legacy scanline implementation (pre-optimization), used as the + * oracle: the optimized drawHorizontalLineZ must match it exactly. + */ + private static void legacyDrawHorizontalLine( + final PolygonBorderInterpolator line1, final PolygonBorderInterpolator line2, + final int y, final int[] renderBufferPixels, final int width, + final int renderMinX, final int renderMaxX, + final TextureBitmap textureBitmap) { + line1.setCurrentY(y); + line2.setCurrentY(y); + + int x1 = line1.getX(); + int x2 = line2.getX(); + + final double tx2, ty2; + final double tx1, ty1; + + if (x1 <= x2) { + tx1 = line1.getTX() * textureBitmap.multiplicationFactor; + ty1 = line1.getTY() * textureBitmap.multiplicationFactor; + tx2 = line2.getTX() * textureBitmap.multiplicationFactor; + ty2 = line2.getTY() * textureBitmap.multiplicationFactor; + } else { + final int tmp = x1; + x1 = x2; + x2 = tmp; + tx1 = line2.getTX() * textureBitmap.multiplicationFactor; + ty1 = line2.getTY() * textureBitmap.multiplicationFactor; + tx2 = line1.getTX() * textureBitmap.multiplicationFactor; + ty2 = line1.getTY() * textureBitmap.multiplicationFactor; + } + + final double realWidth = x2 - x1; + final double realX1 = x1; + + if (x1 < renderMinX) + x1 = renderMinX; + if (x2 >= renderMaxX) + x2 = renderMaxX; + + int renderBufferOffset = (y * width) + x1; + + final double twidth = tx2 - tx1; + final double theight = ty2 - ty1; + + final double txStep = twidth / realWidth; + final double tyStep = theight / realWidth; + + double tx = tx1 + txStep * (x1 - realX1); + double ty = ty1 + tyStep * (x1 - realX1); + + final int[] texPixels = textureBitmap.pixels; + final int texW = textureBitmap.width; + final int texH = textureBitmap.height; + final int texWMinus1 = texW - 1; + final int texHMinus1 = texH - 1; + + for (int x = x1; x < x2; x++) { + int itx = (int) tx; + int ity = (int) ty; + + if (itx < 0) itx = 0; + else if (itx > texWMinus1) itx = texWMinus1; + + if (ity < 0) ity = 0; + else if (ity > texHMinus1) ity = texHMinus1; + + final int srcPixel = texPixels[ity * texW + itx]; + final int srcAlpha = (srcPixel >> 24) & 0xff; + + if (srcAlpha != 0) { + if (srcAlpha == 255) { + renderBufferPixels[renderBufferOffset] = srcPixel; + } else { + final int destPixel = renderBufferPixels[renderBufferOffset]; + final int destR = (destPixel >> 16) & 0xff; + final int destG = (destPixel >> 8) & 0xff; + final int destB = destPixel & 0xff; + + final int r = legacyBlendChannel((srcPixel >> 16) & 0xff, destR, srcAlpha); + final int g = legacyBlendChannel((srcPixel >> 8) & 0xff, destG, srcAlpha); + final int b = legacyBlendChannel(srcPixel & 0xff, destB, srcAlpha); + + renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b; + } + } + + tx += txStep; + ty += tyStep; + renderBufferOffset++; + } + } + + @Test + public void scanlineMatchesLegacyImplementation() throws Exception { + final int width = 96; + final int height = 8; + final Random random = new Random(1337); + + // Texture with a mix of transparent, semi-transparent and opaque pixels + final int texW = 16, texH = 16; + final int[] texPixels = new int[texW * texH]; + for (int i = 0; i < texPixels.length; i++) { + final int alpha; + switch (random.nextInt(4)) { + case 0: alpha = 0; break; + case 1: alpha = 255; break; + default: alpha = 1 + random.nextInt(254); + } + texPixels[i] = (alpha << 24) | (random.nextInt(256) << 16) + | (random.nextInt(256) << 8) | random.nextInt(256); + } + final TextureBitmap textureBitmap = new TextureBitmap(texW, texH, texPixels, 1.0); + + final TexturedTriangle triangle = new TexturedTriangle( + new Vertex(new Point3D(0, 0, 0), new Point2D(0, 0)), + new Vertex(new Point3D(1, 0, 0), new Point2D(1, 0)), + new Vertex(new Point3D(0, 1, 0), new Point2D(0, 1)), null); + + final Method draw = TexturedTriangle.class.getDeclaredMethod("drawHorizontalLineZ", + PolygonBorderInterpolator.class, PolygonBorderInterpolator.class, + int.class, RenderingContext.class, TextureBitmap.class); + draw.setAccessible(true); + + for (int iteration = 0; iteration < 5000; iteration++) { + // Random span endpoints, including out-of-texture and + // out-of-render-bounds cases, and reversed X order + final double sx1 = random.nextDouble() * width * 1.5 - width * 0.25; + final double sx2 = random.nextDouble() * width * 1.5 - width * 0.25; + final double u1 = random.nextDouble() * 2.0 - 0.5; + final double v1 = random.nextDouble() * 2.0 - 0.5; + final double u2 = random.nextDouble() * 2.0 - 0.5; + final double v2 = random.nextDouble() * 2.0 - 0.5; + final int y = 1 + random.nextInt(height - 2); + + final PolygonBorderInterpolator line1 = new PolygonBorderInterpolator(); + final PolygonBorderInterpolator line2 = new PolygonBorderInterpolator(); + line1.setPoints(new Point2D(sx1, y), new Point2D(sx1, y + 1), + new Point2D(u1, v1), new Point2D(u1, v1)); + line2.setPoints(new Point2D(sx2, y), new Point2D(sx2, y + 1), + new Point2D(u2, v2), new Point2D(u2, v2)); + + final int[] actual = new int[width * height]; + final int[] expected = new int[width * height]; + for (int i = 0; i < actual.length; i++) { + actual[i] = expected[i] = 0xFF000000 | random.nextInt(0xFFFFFF); + } + + final RenderingContext context = new RenderingContext(width, height, 1); + System.arraycopy(actual, 0, context.pixels, 0, actual.length); + context.renderMinX = 0; + context.renderMaxX = width; + + // Fresh interpolators for the oracle (setCurrentY mutates them) + final PolygonBorderInterpolator oLine1 = new PolygonBorderInterpolator(); + final PolygonBorderInterpolator oLine2 = new PolygonBorderInterpolator(); + oLine1.setPoints(new Point2D(sx1, y), new Point2D(sx1, y + 1), + new Point2D(u1, v1), new Point2D(u1, v1)); + oLine2.setPoints(new Point2D(sx2, y), new Point2D(sx2, y + 1), + new Point2D(u2, v2), new Point2D(u2, v2)); + + java.util.Arrays.fill(context.depth, Float.NEGATIVE_INFINITY); + draw.invoke(triangle, line1, line2, y, context, textureBitmap); + legacyDrawHorizontalLine(oLine1, oLine2, y, expected, width, + 0, width, textureBitmap); + + for (int i = 0; i < expected.length; i++) { + if (expected[i] != context.pixels[i]) { + final int px = i % width, py = i / width; + throw new AssertionError("iteration " + iteration + + " pixel(" + px + "," + py + "): expected " + + Integer.toHexString(expected[i]) + " but got " + + Integer.toHexString(context.pixels[i]) + + " [span " + sx1 + ".." + sx2 + " uv (" + + u1 + "," + v1 + ")->(" + u2 + "," + v2 + ")]"); + } + } + } + } +} diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTrianglePerspectiveTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTrianglePerspectiveTest.java new file mode 100644 index 0000000..4fe9ef3 --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTrianglePerspectiveTest.java @@ -0,0 +1,305 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.texturedpolygon; + +import eu.svjatoslav.aukio.e3d.geometry.Point2D; +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.RenderingContext; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.texture.TextureBitmap; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.util.Random; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Correctness proof for the Quake-style subdivided perspective scanline + * renderer: compared against an exact per-pixel divide oracle that mirrors + * the production span walk, texel selection must never deviate by more + * than one texel per axis (the ulp-boundary artifact inherent to + * truncation, present in the affine path as well). + */ +public class TexturedTrianglePerspectiveTest { + + private static final int TEX_W = 64; + private static final int TEX_H = 64; + + private static TextureBitmap numberedTexture() { + final int[] texPixels = new int[TEX_W * TEX_H]; + for (int i = 0; i < texPixels.length; i++) + texPixels[i] = 0xFF000000 | i; + return new TextureBitmap(TEX_W, TEX_H, texPixels, 1.0); + } + + /** + * Exact oracle: replicates the production span setup (rounded + * endpoints, gradients over the unclipped width, clip compensation), + * but recovers u/v with a division at EVERY pixel. + */ + private static void exactDrawHorizontalLine( + final double su1, final double sv1, final double sw1, + final double su2, final double sv2, final double sw2, + final int rx1, final int rx2, + final int clipMinX, final int clipMaxX, + final int y, final int[] renderBufferPixels, final int width, + final TextureBitmap textureBitmap) { + + final double realWidth = rx2 - rx1; + int x1 = Math.max(rx1, clipMinX); + int x2 = Math.min(rx2, clipMaxX); + if (x2 - x1 <= 0) + return; + + final double dsu = (su2 - su1) / realWidth; + final double dsv = (sv2 - sv1) / realWidth; + final double dsw = (sw2 - sw1) / realWidth; + + double su = su1 + dsu * (x1 - rx1); + double sv = sv1 + dsv * (x1 - rx1); + double sw = sw1 + dsw * (x1 - rx1); + + int renderBufferOffset = (y * width) + x1; + + final int[] texPixels = textureBitmap.pixels; + + for (int x = x1; x < x2; x++) { + final double invW = 1d / sw; + int itx = (int) (su * invW); + int ity = (int) (sv * invW); + + if (itx < 0) itx = 0; + else if (itx > TEX_W - 1) itx = TEX_W - 1; + if (ity < 0) ity = 0; + else if (ity > TEX_H - 1) ity = TEX_H - 1; + + renderBufferPixels[renderBufferOffset] = texPixels[ity * TEX_W + itx]; + + su += dsu; + sv += dsv; + sw += dsw; + renderBufferOffset++; + } + } + + @Test + public void subdividedPerspectiveStaysWithinOneTexelOfExact() throws Exception { + final int width = 256; + final int height = 8; + final Random random = new Random(2026); + final TextureBitmap textureBitmap = numberedTexture(); + + final TexturedTriangle triangle = new TexturedTriangle( + new Vertex(new Point3D(0, 0, 0), new Point2D(0, 0)), + new Vertex(new Point3D(1, 0, 0), new Point2D(1, 0)), + new Vertex(new Point3D(0, 1, 0), new Point2D(0, 1)), null); + + final Method draw = TexturedTriangle.class.getDeclaredMethod( + "drawHorizontalLinePerspectiveZ", + PerspectiveBorderInterpolator.class, PerspectiveBorderInterpolator.class, + int.class, RenderingContext.class, TextureBitmap.class); + draw.setAccessible(true); + + long totalPixels = 0; + long identicalPixels = 0; + int maxDeviation = 0; + + for (int iteration = 0; iteration < 3000; iteration++) { + // Random steep-perspective span: z varies up to 60x across the + // span, texture coords may overshoot the texture (clamp path), + // and the span may extend past the render bounds (clip path) + final double sx1 = random.nextDouble() * width * 0.5; + final double sx2 = sx1 + 4 + random.nextDouble() * (width - 8); + final double z1 = 0.5 + random.nextDouble() * 31.5; + final double z2 = 0.5 + random.nextDouble() * 31.5; + final double u1 = random.nextDouble() * 144 - 16; + final double v1 = random.nextDouble() * 144 - 16; + final double u2 = random.nextDouble() * 144 - 16; + final double v2 = random.nextDouble() * 144 - 16; + final int y = 1 + random.nextInt(height - 2); + + final double sw1 = 1d / z1, sw2 = 1d / z2; + final double su1 = u1 * sw1, sv1 = v1 * sw1; + final double su2 = u2 * sw2, sv2 = v2 * sw2; + + final PerspectiveBorderInterpolator line1 = new PerspectiveBorderInterpolator(); + final PerspectiveBorderInterpolator line2 = new PerspectiveBorderInterpolator(); + line1.setPoints(new Point2D(sx1, y), new Point2D(sx1, y + 1), su1, sv1, sw1, su1, sv1, sw1); + line2.setPoints(new Point2D(sx2, y), new Point2D(sx2, y + 1), su2, sv2, sw2, su2, sv2, sw2); + + final RenderingContext context = new RenderingContext(width, height, 1); + context.renderMinX = 0; + context.renderMaxX = width; + + java.util.Arrays.fill(context.depth, Float.NEGATIVE_INFINITY); + draw.invoke(triangle, line1, line2, y, context, textureBitmap); + + final int rx1 = (int) Math.round(sx1); + final int rx2 = (int) Math.round(sx2); + final int[] expected = new int[width * height]; + exactDrawHorizontalLine(su1, sv1, sw1, su2, sv2, sw2, + rx1, rx2, 0, width, y, expected, width, textureBitmap); + + final int cx1 = Math.max(rx1, 0); + final int cx2 = Math.min(rx2, width); + for (int x = cx1; x < cx2; x++) { + final int a = context.pixels[y * width + x] & 0xFFFFFF; + final int e = expected[y * width + x] & 0xFFFFFF; + totalPixels++; + if (a == e) { + identicalPixels++; + } else { + final int deviation = Math.max( + Math.abs((a % TEX_W) - (e % TEX_W)), + Math.abs((a / TEX_W) - (e / TEX_W))); + maxDeviation = Math.max(maxDeviation, deviation); + } + } + } + + final double identicalRatio = (double) identicalPixels / totalPixels; + if (maxDeviation > 1) { + fail("texel deviation " + maxDeviation + " exceeds 1 (identical=" + + (identicalRatio * 100) + "% over " + totalPixels + " pixels)"); + } + assertTrue("suspiciously few pixels tested: " + totalPixels, totalPixels > 100000); + System.out.println("perspective-16 vs exact: identical=" + (identicalRatio * 100) + + "% maxTexelDeviation=" + maxDeviation + + " over " + totalPixels + " pixels"); + } + + @Test + public void affineWithinHalfTexelBound() throws Exception { + // The paint() shortcut uses affine mapping when + // texelSpan * (zRatio-1) < 2. Verify: for random spans satisfying + // that bound, the affine renderer stays within one texel of the + // exact per-pixel divide oracle. + final int width = 320; + final int height = 8; + final Random random = new Random(77); + final TextureBitmap textureBitmap = numberedTexture(); + + final TexturedTriangle triangle = new TexturedTriangle( + new Vertex(new Point3D(0, 0, 0), new Point2D(0, 0)), + new Vertex(new Point3D(1, 0, 0), new Point2D(1, 0)), + new Vertex(new Point3D(0, 1, 0), new Point2D(0, 1)), null); + + final Method drawAffine = TexturedTriangle.class.getDeclaredMethod( + "drawHorizontalLineZ", + PolygonBorderInterpolator.class, PolygonBorderInterpolator.class, + int.class, RenderingContext.class, TextureBitmap.class); + drawAffine.setAccessible(true); + + long totalPixels = 0; + int maxDeviation = 0; + + for (int iteration = 0; iteration < 3000; iteration++) { + final double sx1 = random.nextDouble() * width * 0.5; + final double spanD = 4 + random.nextDouble() * 296; + final double sx2 = Math.min(sx1 + spanD, width * 1.2); + final double u1 = random.nextDouble() * 56; + final double v1 = random.nextDouble() * 56; + final double u2 = random.nextDouble() * 56; + final double v2 = random.nextDouble() * 56; + // z ratio strictly inside the bound, driven by the TEXEL span + final double texelSpan = Math.max(Math.abs(u2 - u1), Math.abs(v2 - v1)); + final double z1 = 1 + random.nextDouble() * 30; + final double r = 1 + random.nextDouble() * (1.9 / Math.max(texelSpan, 0.5)); + final double z2 = z1 * r; + final int y = 1 + random.nextInt(height - 2); + + final PolygonBorderInterpolator line1 = new PolygonBorderInterpolator(); + final PolygonBorderInterpolator line2 = new PolygonBorderInterpolator(); + line1.setPoints(new Point2D(sx1, y), new Point2D(sx1, y + 1), + new Point2D(u1, v1), new Point2D(u1, v1)); + line2.setPoints(new Point2D(sx2, y), new Point2D(sx2, y + 1), + new Point2D(u2, v2), new Point2D(u2, v2)); + + final RenderingContext context = new RenderingContext(width, height, 1); + context.renderMinX = 0; + context.renderMaxX = width; + + java.util.Arrays.fill(context.depth, Float.NEGATIVE_INFINITY); + drawAffine.invoke(triangle, line1, line2, y, context, textureBitmap); + + // Exact oracle over the same span (gradients from z1/z2) + final double sw1 = 1d / z1, sw2 = 1d / z2; + final int rx1 = (int) Math.round(sx1); + final int rx2 = (int) Math.round(sx2); + final int[] expected = new int[width * height]; + exactDrawHorizontalLine(u1 * sw1, v1 * sw1, sw1, u2 * sw2, v2 * sw2, sw2, + rx1, rx2, 0, width, y, expected, width, textureBitmap); + + final int cx1 = Math.max(rx1, 0); + final int cx2 = Math.min(rx2, width); + for (int x = cx1; x < cx2; x++) { + final int a = context.pixels[y * width + x] & 0xFFFFFF; + final int e = expected[y * width + x] & 0xFFFFFF; + totalPixels++; + final int deviation = Math.max( + Math.abs((a % TEX_W) - (e % TEX_W)), + Math.abs((a / TEX_W) - (e / TEX_W))); + maxDeviation = Math.max(maxDeviation, deviation); + } + } + + if (maxDeviation > 1) { + fail("affine deviation " + maxDeviation + " exceeds 1 texel within the bound" + + " over " + totalPixels + " pixels"); + } + assertTrue("suspiciously few pixels tested: " + totalPixels, totalPixels > 100000); + System.out.println("affine within bound: maxTexelDeviation=" + maxDeviation + + " over " + totalPixels + " pixels"); + } + + @Test + public void faceOnSpanMatchesAffineWithinOneTexel() throws Exception { + // Constant z across the span: perspective correction must reduce + // to the affine mapping (u linear in x), modulo the ulp-boundary + // truncation artifact. + final int width = 200; + final int height = 4; + final TextureBitmap textureBitmap = numberedTexture(); + + final TexturedTriangle triangle = new TexturedTriangle( + new Vertex(new Point3D(0, 0, 0), new Point2D(0, 0)), + new Vertex(new Point3D(1, 0, 0), new Point2D(1, 0)), + new Vertex(new Point3D(0, 1, 0), new Point2D(0, 1)), null); + + final Method draw = TexturedTriangle.class.getDeclaredMethod( + "drawHorizontalLinePerspectiveZ", + PerspectiveBorderInterpolator.class, PerspectiveBorderInterpolator.class, + int.class, RenderingContext.class, TextureBitmap.class); + draw.setAccessible(true); + + final double z = 5.0; + final double sw = 1d / z; + final int y = 1; + final double u1 = 2.0, v1 = 3.0, u2 = 30.0, v2 = 10.0; + + final PerspectiveBorderInterpolator line1 = new PerspectiveBorderInterpolator(); + final PerspectiveBorderInterpolator line2 = new PerspectiveBorderInterpolator(); + line1.setPoints(new Point2D(10, y), new Point2D(10, y + 1), u1 * sw, v1 * sw, sw, u1 * sw, v1 * sw, sw); + line2.setPoints(new Point2D(190, y), new Point2D(190, y + 1), u2 * sw, v2 * sw, sw, u2 * sw, v2 * sw, sw); + + final RenderingContext context = new RenderingContext(width, height, 1); + context.renderMinX = 0; + context.renderMaxX = width; + java.util.Arrays.fill(context.depth, Float.NEGATIVE_INFINITY); + draw.invoke(triangle, line1, line2, y, context, textureBitmap); + + for (int x = 10; x < 190; x++) { + final double exactU = u1 + (u2 - u1) * (x - 10) / 180.0; + final double exactV = v1 + (v2 - v1) * (x - 10) / 180.0; + final int actualTexel = context.pixels[y * width + x] & 0xFFFFFF; + final int du = Math.abs((actualTexel % TEX_W) - ((int) exactU)); + final int dv = Math.abs((actualTexel / TEX_W) - ((int) exactV)); + assertTrue("pixel " + x + ": texel deviation u=" + du + " v=" + dv, + du <= 1 && dv <= 1); + } + } +} diff --git a/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/CsgTest.java b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/CsgTest.java new file mode 100644 index 0000000..786e033 --- /dev/null +++ b/src/test/java/eu/svjatoslav/aukio/e3d/renderer/raster/shapes/composite/base/CsgTest.java @@ -0,0 +1,172 @@ +/* + * Aukio 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.base; + +import eu.svjatoslav.aukio.e3d.geometry.Point3D; +import eu.svjatoslav.aukio.e3d.renderer.raster.Color; +import eu.svjatoslav.aukio.e3d.renderer.raster.Vertex; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.aukio.e3d.renderer.raster.shapes.composite.solid.SolidPolygonRectangularBox; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for the {@link Csg} boolean engine (BSP-based union / + * subtract / intersect over polygon lists). This machinery previously + * had zero coverage anywhere — no unit test, no golden scene. + */ +public class CsgTest { + + private static final double EPS = 1e-9; + + /** Axis-aligned box as a CSG-ready polygon list. */ + private static List box(final double x1, final double y1, + final double z1, final double x2, + final double y2, final double z2) { + return new SolidPolygonRectangularBox( + new Point3D(x1, y1, z1), new Point3D(x2, y2, z2), Color.RED) + .extractSolidPolygons(); + } + + private static double[] centroid(final SolidPolygon polygon) { + double cx = 0, cy = 0, cz = 0; + for (final Vertex v : polygon.vertices) { + cx += v.coordinate.x; + cy += v.coordinate.y; + cz += v.coordinate.z; + } + final int n = polygon.vertices.size(); + return new double[]{cx / n, cy / n, cz / n}; + } + + /** Strictly inside the box (boundary does NOT count). */ + private static boolean strictlyInside(final double[] point, + final double x1, final double y1, + final double z1, final double x2, + final double y2, final double z2) { + return point[0] > x1 + EPS && point[0] < x2 - EPS + && point[1] > y1 + EPS && point[1] < y2 - EPS + && point[2] > z1 + EPS && point[2] < z2 - EPS; + } + + /** Inside or on the box boundary. */ + private static boolean insideOrOn(final double x, final double y, + final double z, + final double x1, final double y1, + final double z1, final double x2, + final double y2, final double z2) { + return x >= x1 - EPS && x <= x2 + EPS + && y >= y1 - EPS && y <= y2 + EPS + && z >= z1 - EPS && z <= z2 + EPS; + } + + private static final List A = box(0, 0, 0, 1, 1, 1); + private static final List B = box(0.5, 0.5, 0.5, 1.5, 1.5, 1.5); + private static final List FAR = box(10, 10, 10, 11, 11, 11); + + @Test + public void unionOfDisjointBoxesKeepsAllFaces() { + final List result = Csg.union(A, FAR); + assertEquals("6 + 6 faces, no interior to remove", + A.size() + FAR.size(), result.size()); + } + + @Test + public void unionOfOverlappingBoxesRemovesInteriorFaces() { + final List result = Csg.union(A, B); + assertTrue("union of two boxes must produce geometry", !result.isEmpty()); + // Note: no polygon-COUNT assertion — BSP splits boundary-crossing + // faces (raising the count) while removing interior fragments + // (lowering it); the net count says nothing. The interior-face + // invariant below is the meaningful one. + for (final SolidPolygon polygon : result) { + final double[] c = centroid(polygon); + assertTrue("union must not contain a face strictly inside A", + !strictlyInside(c, 0, 0, 0, 1, 1, 1)); + assertTrue("union must not contain a face strictly inside B", + !strictlyInside(c, 0.5, 0.5, 0.5, 1.5, 1.5, 1.5)); + } + } + + @Test + public void subtractOfDisjointBoxIsIdentity() { + final List result = Csg.subtract(A, FAR); + assertEquals(A.size(), result.size()); + } + + @Test + public void subtractLeavesNothingInsideTheCutter() { + final List result = Csg.subtract(A, B); + assertTrue("carving a corner out of a box must leave geometry", + !result.isEmpty()); + for (final SolidPolygon polygon : result) { + final double[] c = centroid(polygon); + assertTrue("difference must not contain faces strictly inside the cutter", + !strictlyInside(c, 0.5, 0.5, 0.5, 1.5, 1.5, 1.5)); + } + } + + @Test + public void intersectOfOverlappingBoxesIsTheOverlapRegion() { + final List result = Csg.intersect(A, B); + assertTrue("overlap of [0,1]³ and [0.5,1.5]³ must be non-empty", + !result.isEmpty()); + for (final SolidPolygon polygon : result) + for (final Vertex v : polygon.vertices) { + assertTrue("every vertex must lie inside-or-on A", + insideOrOn(v.coordinate.x, v.coordinate.y, v.coordinate.z, + 0, 0, 0, 1, 1, 1)); + assertTrue("every vertex must lie inside-or-on B", + insideOrOn(v.coordinate.x, v.coordinate.y, v.coordinate.z, + 0.5, 0.5, 0.5, 1.5, 1.5, 1.5)); + } + } + + @Test + public void intersectOfDisjointBoxesIsEmpty() { + assertTrue(Csg.intersect(A, FAR).isEmpty()); + } + + @Test + public void emptyOperandBehaves() { + assertEquals(A.size(), Csg.union(A, List.of()).size()); + assertEquals(A.size(), Csg.subtract(A, List.of()).size()); + assertTrue(Csg.intersect(A, List.of()).isEmpty()); + } + + @Test + public void resultsAreDeterministic() { + final List first = Csg.union(A, B); + final List second = Csg.union(A, B); + assertEquals(first.size(), second.size()); + for (int i = 0; i < first.size(); i++) { + final List v1 = first.get(i).vertices; + final List v2 = second.get(i).vertices; + assertEquals(v1.size(), v2.size()); + for (int j = 0; j < v1.size(); j++) { + assertEquals(v1.get(j).coordinate.x, v2.get(j).coordinate.x, 0.0); + assertEquals(v1.get(j).coordinate.y, v2.get(j).coordinate.y, 0.0); + assertEquals(v1.get(j).coordinate.z, v2.get(j).coordinate.z, 0.0); + } + } + } + + /** The inputs are never mutated (Csg clones before BSP-ing). */ + @Test + public void inputsAreNotMutated() { + final List a = box(0, 0, 0, 1, 1, 1); + final double firstX = a.get(0).vertices.get(0).coordinate.x; + final int size = a.size(); + Csg.subtract(a, B); + Csg.union(a, B); + Csg.intersect(a, B); + assertEquals(size, a.size()); + assertEquals(firstX, a.get(0).vertices.get(0).coordinate.x, 0.0); + } +} -- 2.20.1