From 54d3ee5029832f4ecb802a67cfdf006c74ca73e3 Mon Sep 17 00:00:00 2001 From: Svjatoslav Agejenko Date: Mon, 30 Mar 2026 19:44:01 +0300 Subject: [PATCH] feat: unify polygon type for CSG and rendering Extend SolidPolygon to support N-vertex convex polygons, enabling direct use in CSG operations without triangulation. Move CSG boolean operations (union, subtract, intersect) from the standalone CSG class to AbstractCompositeShape for in-place modifications with simpler API. - SolidPolygon now handles arbitrary convex polygons via fan triangulation - CSG operations work directly on SolidPolygon, eliminating CSGPolygon - Add chainable setters to AbstractCompositeShape for fluent API - Add non-mutating methods to Point2D/Point3D/Transform - Rename TexturedPolygon to TexturedTriangle for consistency - Fix vertex cloning, polygon validation, collinear point detection - Use ThreadLocal fields for thread-safe rendering state --- AGENTS.md | 29 +- doc/Axis.png | Bin 74804 -> 0 bytes doc/Minimal example.png | Bin 3601 -> 0 bytes doc/Winding order demo.png | Bin 2331 -> 0 bytes doc/index.org | 172 +---- doc/perspective-correct-textures/index.org | 4 +- .../java/eu/svjatoslav/sixth/e3d/csg/CSG.java | 368 --------- .../eu/svjatoslav/sixth/e3d/csg/CSGNode.java | 192 +---- .../eu/svjatoslav/sixth/e3d/csg/CSGPlane.java | 159 ++-- .../svjatoslav/sixth/e3d/csg/CSGPolygon.java | 91 --- .../eu/svjatoslav/sixth/e3d/geometry/Box.java | 4 +- .../sixth/e3d/geometry/Point2D.java | 120 ++- .../sixth/e3d/geometry/Point3D.java | 118 ++- .../sixth/e3d/geometry/Polygon.java | 22 + .../sixth/e3d/geometry/Rectangle.java | 2 +- .../eu/svjatoslav/sixth/e3d/gui/Camera.java | 2 +- .../svjatoslav/sixth/e3d/math/Transform.java | 27 +- .../eu/svjatoslav/sixth/e3d/math/Vertex.java | 2 +- .../renderer/octree/raytracer/CameraView.java | 2 +- .../sixth/e3d/renderer/raster/Color.java | 36 +- .../shapes/AbstractCoordinateShape.java | 38 +- .../raster/shapes/basic/Billboard.java | 6 +- .../raster/shapes/basic/line/Line.java | 53 +- .../basic/solidpolygon/LineInterpolator.java | 20 + .../basic/solidpolygon/SolidPolygon.java | 729 ++++++++++++++---- .../basic/solidpolygon/package-info.java | 10 +- .../PolygonBorderInterpolator.java | 13 +- ...uredPolygon.java => TexturedTriangle.java} | 98 +-- .../basic/texturedpolygon/package-info.java | 8 +- .../raster/shapes/composite/Graph.java | 16 +- .../shapes/composite/TexturedRectangle.java | 12 +- .../base/AbstractCompositeShape.java | 226 +++++- .../shapes/composite/base/SubShape.java | 9 + .../composite/solid/SolidPolygonArrow.java | 54 +- .../composite/solid/SolidPolygonCone.java | 32 +- .../composite/solid/SolidPolygonCylinder.java | 51 +- .../composite/solid/SolidPolygonMesh.java | 16 +- .../composite/solid/SolidPolygonPyramid.java | 45 +- .../solid/SolidPolygonRectangularBox.java | 34 +- .../shapes/composite/solid/package-info.java | 2 +- .../composite/textcanvas/CanvasCharacter.java | 31 +- .../e3d/renderer/raster/slicer/Slicer.java | 22 +- .../e3d/renderer/raster/texture/Texture.java | 2 +- 43 files changed, 1448 insertions(+), 1429 deletions(-) delete mode 100644 doc/Axis.png delete mode 100644 doc/Minimal example.png delete mode 100644 doc/Winding order demo.png delete mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/csg/CSG.java delete mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPolygon.java rename src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/{TexturedPolygon.java => TexturedTriangle.java} (78%) diff --git a/AGENTS.md b/AGENTS.md index 26fba88..5c1ee6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ sixth-3d-engine is a Java-based 3D rendering engine. It provides: ├── octree/ — Octree volume representation and ray tracer └── raster/ — Rasterization pipeline ├── shapes/ - │ ├── basic/ — Primitive shapes: Line, SolidPolygon, TexturedPolygon + │ ├── basic/ — Primitive shapes: Line, SolidPolygon, TexturedTriangle │ └── composite/ — Composite shapes: AbstractCompositeShape, TextCanvas, │ WireframeBox, SolidPolygonRectangularBox ├── slicer/ — Geometry slicing for level-of-detail @@ -103,13 +103,21 @@ All Java files must start with this exact header: Sixth 3D uses a **left-handed coordinate system** matching standard 2D screen coordinates: -| Axis | Positive Direction | Meaning | -|------|-------------------|--------------------------------------| -| X | RIGHT | Larger X = further right | -| Y | DOWN | Smaller Y = higher visually (up) | -| Z | AWAY from viewer | Negative Z = closer to camera | +| Axis | Positive Direction | Meaning | +|------|--------------------|----------------------------------| +| X | RIGHT | Larger X = further right | +| Y | DOWN | Smaller Y = higher visually (up) | +| Z | AWAY from viewer | Negative Z = closer to camera | + +**Important positioning rules:** + +- To place object A **above** object B, give A a **smaller Y value** (`y - offset`) +- To place object A **below** object B, give A a **larger Y value** (`y + offset`) +- This is the opposite of many 3D engines (OpenGL, Unity, Blender) which use Y-up + +**Common mistake:** If you're used to Y-up engines, you may accidentally place elements above when you intend below (or +vice versa). Always verify: positive Y = down in Sixth 3D. -- To place object A "above" object B, give A a **smaller Y value** - `Point2D` and `Point3D` are mutable value types with public fields (`x`, `y`, `z`) - Points support fluent/chaining API — mutation methods return `this` - `Vertex` wraps a `Point3D` and adds `transformedCoordinate` for viewer-relative positioning @@ -123,9 +131,9 @@ Sixth 3D uses a **left-handed coordinate system** matching standard 2D screen co ## Shape Hierarchy - `AbstractShape` — base class with optional `MouseInteractionController` -- `AbstractCoordinateShape` — has `Vertex[]` coordinates and `onScreenZ` for depth sorting +- `AbstractCoordinateShape` — has `List` coordinates and `onScreenZ` for depth sorting - `AbstractCompositeShape` — groups sub-shapes with group IDs and visibility toggles -- Concrete shapes: `Line`, `SolidPolygon`, `TexturedPolygon`, `TextCanvas`, `WireframeBox` +- Concrete shapes: `Line`, `SolidPolygon`, `TexturedTriangle`, `TextCanvas`, `WireframeBox` ## Rendering @@ -155,5 +163,6 @@ Sixth 3D uses a **left-handed coordinate system** matching standard 2D screen co 4. **Render pipeline:** Shapes must implement `transform()` and `paint()` methods 5. **Depth sorting:** Set `onScreenZ` correctly during `transform()` for proper rendering order 6. **Backface culling:** Uses signed area in screen space; `signedArea < 0` = front-facing (CCW) -7. **Polygon winding:** CCW in screen space = front face. Vertex order: top → lower-left → lower-right (as seen from camera). See `WindingOrderDemo` in sixth-3d-demos. +7. **Polygon winding:** CCW in screen space = front face. Vertex order: top → lower-left → lower-right (as seen from + camera). See `WindingOrderDemo` in sixth-3d-demos. 8. **Testing:** Write JUnit 4 tests in `src/test/java/` with matching package structure diff --git a/doc/Axis.png b/doc/Axis.png deleted file mode 100644 index d028e6169a6d0c9868b473eeefdeb6dfdd211c16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74804 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 WP+WWc@{f27fQ4Wt6<13m>AWBpd0aGC0^c% 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 Hd&K=8S+WHD diff --git a/doc/Minimal example.png b/doc/Minimal example.png deleted file mode 100644 index b2ceac7f660fa855ac897fc516c77aab6bfc71ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3601 zcmeAS@N?(olHy`uVBq!ia0y~yV0y~H!1$1b87N|%d)JMDfr~9OB%&n3*T*V3KUXg? zB|j-uuOhbqD9^xPV_#8_n4FzjqL7rDo|$K>^nUk#C56lsTcvPQUjyF)=hTc$kE){7 z;3~h6MhJ1(0FE1&_nsU?XD6}dTi#a0!zN?>!XfNYSkzLEl1NlCV?QiN}S zf^&XRs)C80iJpP3Yei<6k&+#kf=y9MnpKdC8`KCO&sHg;q@=(~U%$M(T(8_%FTW^V z-_X+1Qs2Nx-^fT8s6w~6GOr}DLN~8i8Ds>+442g6}$%~k=K6{$H9E}6NhdBs2{ z*eaQu7=jH$$VMW`npi+&K_*#w<|d}6hG(XfWFWL5Bz#iy(m^sPTKtPLQ-NUs@~y3s zp^`o*B=w;|35*$ic+`NxhxTC95|YArG`L2Ci=+@BN%3gv8VxRzLVzU2qp53X1s4~h zahIA0Y|)hin{G-jnx(*IpIm@Xi0glF3($dqVLtZ6z`z!Om z%=mfa+0Ki}&xB_`cgvr7J||9kZ>rtq%G@tgeqMRD@nZ0^;MvFB^k<&Wy(hId#cq3L z_Lm7ipFCT6(fQfr*~{JD&p4kKC$%@(Zg*wom!6+ro-Mp+{Ot1VcDMC2&gb8g+?!;# zzcT$x$IrgA#*5w0l;_No+?%*B{qw08yq|4nhr9XDcz?^rsOnJ7^=A_=3O|!SzRhl? zar!x6m;<9=ltINn_`m;jWwiOT`n&$OcNx{6L`tcoq)6lp1J6P@bIwohnf45yIIQ&*dM?}GT)E@;&q`^Q@se*`&vOoeRfdH$bpooYB5Yrx=b5kTHP%9F= zk}ePg1LDn=B;Bi2%ITeLG-Yh9P|$E}_9E2+SH&N1>fr#JLt>+s-QGXW-OSC+-5)#8 zexG^w`+oL$-`RIhAn9U1-#}jog8ZbCq-_xN66wA6%$ehT|EzdjVPzjGs$m~^{F*Vd-I zwqpNJE0%vAQdKwl=IFrFN9X_k{msCycl@Bxl*HutNc0;>qi%f(xuxYoJwNRhTT6qO zdVL9xsrm01nVK2IOJfFk>;L$Wx9R`hCLNmwXX7RB=+R9kMoG2lG1K zEKp{F3Vb}w6J__C8q9DPGIo1DaXaPWeB~yPWf*V9-XlAs(ku}ik8n84l0#gQcH7WP zYE)m*RzmKWIIz@tjl3ZombrDZvVL(ih-C*43x{`EvWO$n?vtQs=HW`uPPX$dA}95t zuW}VAiyB_V{meM}-FoopVcJ(&3exfMP|sd-&b@X$eZ;`eU5xF-e;qj-PU(#Cbs&A_ zK&a=aOn>gT;!lvW=J5b0OmeTI?P|LI**>=QR^oQ*#d*pgfXOWx@GVJYsNHz>&R`hj zFnem$D)GIU~hxDh0MErEz&fi zDA8`>o&t~5NvZK`nfdC2o#%~uQQK~aC#C1OAl<#iVCxs3rgiKlEyv+3!5XTBjO|DFz@)LXiP;-9^IccS zeB59ERleCixrnd%hDF{@WHi})l}&V7bZ`^48nhO%DGN0h&`hG;Sb7?CsFS3l8(HMV z&hsX{uq{+GB@^G{N!|BZWCGzv2EXcD0p>@wHDZh~Yc0j$TtOK17TGKpgmW2T8h;?v zTt;O?f$_~gG#%dW6=p@os@!VOh{@H+w0?60IHBMN!}SyqYm{7+t$ zGE6_R1bYYPgj?UB6no5N_X>3V^je^s7+UN+K^_rW?-#1J!;Z@`tYIV&OT#&DSXWYt zedg)00>A6@3UFg$D8OkTHwmr$B2_BvxGKZyM>yCToD*RUp%ij+*;s+@I_*vL@FO2r zCU7I22E#}l787mf8F_cQLZ|{$zsOo$WP!Pg1LnCB?bUXeI%GO4vYLx5o4JaEW~}+6 z6XJ4WZt7C4N_PWQ!j2&sv!yU6lF@X|&(+cj?x;JyG{CzB5u9&v&YIjnXUsI*cVxcN|) diff --git a/doc/index.org b/doc/index.org index 532b066..dc1de32 100644 --- a/doc/index.org +++ b/doc/index.org @@ -104,125 +104,6 @@ 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/sixth-3d-demos/][demos]] for examples of what it can do today. -* Minimal example -:PROPERTIES: -:CUSTOM_ID: tutorial -:ID: 19a0e3f9-5225-404e-a48b-584b099fccf9 -:END: - -*Resources to help you understand the Sixth 3D library:* -- Read online [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/][JavaDoc]]. -- See [[https://www3.svjatoslav.eu/projects/sixth-3d/graphs/][Sixth 3D class diagrams]]. (Diagrams were generated by using - [[https://www3.svjatoslav.eu/projects/javainspect/][JavaInspect]] utility) -- Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]]. - - -*Brief tutorial:* - -Here we guide you through creating your first 3D scene with Sixth 3D -engine. - -Prerequisites: -- Java 21 or later installed -- Maven 3.x -- Basic Java knowledge - -** Add Dependency to Your Project -:PROPERTIES: -:CUSTOM_ID: add-dependency-to-your-project -:ID: 3fffc32e-ae66-40b7-ad7d-fab6093c778b -:END: - -Add Sixth 3D to your pom.xml: - -#+BEGIN_SRC xml - - - eu.svjatoslav - sixth-3d - 1.3 - - - - - - svjatoslav.eu - Svjatoslav repository - https://www3.svjatoslav.eu/maven/ - - -#+END_SRC - -** Create Your First 3D Scene -:PROPERTIES: -:CUSTOM_ID: create-your-first-3d-scene -:ID: 564fa596-9b2b-418a-9df9-baa46f0d0a66 -:END: - -Here is a [[https://www2.svjatoslav.eu/gitweb/?p=sixth-3d-demos.git;a=blob;f=src/main/java/eu/svjatoslav/sixth/e3d/examples/MinimalExample.java;h=af755e8a159c64b3ab8a14c8e76441608ecbf8ee;hb=HEAD][minimal working example]]: - -#+BEGIN_SRC java - import eu.svjatoslav.sixth.e3d.geometry.Point3D; - import eu.svjatoslav.sixth.e3d.gui.ViewFrame; - import eu.svjatoslav.sixth.e3d.math.Transform; - import eu.svjatoslav.sixth.e3d.renderer.raster.Color; - import eu.svjatoslav.sixth.e3d.renderer.raster.ShapeCollection; - import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonRectangularBox; - - public class MyFirstScene { - public static void main(String[] args) { - // Create the application window - ViewFrame viewFrame = new ViewFrame(); - - // Get the collection where you add 3D shapes - ShapeCollection shapes = viewFrame.getViewPanel().getRootShapeCollection(); - - // Add a red box at position (0, 0, 0) - Transform boxTransform = new Transform(new Point3D(0, 0, 0), 0, 0); - SolidPolygonRectangularBox box = new SolidPolygonRectangularBox( - new Point3D(-50, -50, -50), - new Point3D(50, 50, 50), - Color.RED - ); - box.setTransform(boxTransform); - shapes.addShape(box); - - // Position your camera - viewFrame.getViewPanel().getCamera().getTransform().setTranslation(new Point3D(0, -100, -300)); - - // Start the render thread - viewFrame.getViewPanel().ensureRenderThreadStarted(); - } - } -#+END_SRC - -Compile and run *MyFirstScene* class. A new window should open that will -display 3D scene with red box. - -This example is available in the [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]] project. Run it directly: - -: java -cp sixth-3d-demos.jar eu.svjatoslav.sixth.e3d.examples.MyFirstScene - -You should see this: - -[[file:Minimal example.png]] - - -*Navigating the scene:* - -| Input | Action | -|---------------------+-------------------------------------| -| Arrow Up / W | Move forward | -| Arrow Down / S | Move backward | -| Arrow Left | Move left (strafe) | -| Arrow Right | Move right (strafe) | -| Mouse drag | Look around (rotate camera) | -| Mouse scroll wheel | Move up / down | - -Movement uses physics-based acceleration for smooth, natural -motion. The faster you're moving, the more acceleration builds up, -creating an intuitive flying experience. - * Understanding 3D engine :PROPERTIES: :CUSTOM_ID: defining-scene @@ -234,7 +115,8 @@ creating an intuitive flying experience. - To understand perspective-correct texture mapping, see dedicated page: [[file:perspective-correct-textures/][Perspective-correct textures]] -- Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]] for practical examples. +- Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]] for practical examples. Start with [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/#minimal-example][minimal + example]]. ** Coordinate System (X, Y, Z) :PROPERTIES: @@ -281,6 +163,10 @@ graphics background. - To place object A "above" object B, give A a **smaller Y value** than B. +The [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/#coordinate-system][sixth-3d-demos]] project includes an interactive +coordinate system reference showing X, Y, Z axes as colored arrows +with a grid plane for spatial context. + ** Vertex :PROPERTIES: :CUSTOM_ID: vertex @@ -373,7 +259,7 @@ A *face* is a flat surface enclosed by edges. In most 3D engines, the fundamenta - Always guaranteed to be coplanar - Quads (4 vertices) = 2 triangles - Complex shapes = many triangles (a "mesh") -- Face maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]] or [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.html][TexturedPolygon]] in Sixth 3D. +- Face maps to [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidTriangle.html][SolidTriangle]], [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html][SolidPolygon]], or [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] in Sixth 3D. ** Normal Vector :PROPERTIES: @@ -453,6 +339,10 @@ A *mesh* is a collection of vertices, edges, and faces that together define the - [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.html][AbstractCoordinateShape]]: base class for single shapes with vertices (triangles, lines). Use when creating one primitive. - [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html][AbstractCompositeShape]]: groups multiple shapes into one object. Use for complex models that move/rotate together. +See the [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/#shape-gallery][Shape Gallery demo]] for a visual showcase of +all primitive shapes available in Sixth 3D, rendered in both +wireframe and solid polygon styles with dynamic lighting. + ** Winding Order & Backface Culling :PROPERTIES: :CUSTOM_ID: winding-order-backface-culling @@ -511,45 +401,9 @@ optimization. (in Y-down screen coordinates, negative signed area corresponds to visually CCW winding) - -*Minimal Example: WindingOrderDemo* - -The [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][sixth-3d-demos]] project includes a [[https://www2.svjatoslav.eu/gitweb/?p=sixth-3d-demos.git;a=blob;f=src/main/java/eu/svjatoslav/sixth/e3d/examples/WindingOrderDemo.java][winding order demo]] to -demonstrate how winding order affects backface culling: - -#+BEGIN_SRC java -// WindingOrderDemo.java - validates CCW winding = front face -public class WindingOrderDemo { - public static void main(String[] args) { - ViewFrame viewFrame = new ViewFrame(); - ShapeCollection shapes = viewFrame.getViewPanel().getRootShapeCollection(); - - double size = 150; - - // CCW winding: top → lower-left → lower-right - Point3D upperCenter = new Point3D(0, -size, 0); - Point3D lowerLeft = new Point3D(-size, +size, 0); - Point3D lowerRight = new Point3D(+size, +size, 0); - - SolidPolygon triangle = new SolidPolygon(upperCenter, lowerLeft, lowerRight, Color.GREEN); - triangle.setBackfaceCulling(true); - - shapes.addShape(triangle); - - viewFrame.getViewPanel().getCamera().getTransform().setTranslation(new Point3D(0, 0, -500)); - viewFrame.getViewPanel().ensureRenderThreadStarted(); - } -} -#+END_SRC - -Run this demo: if the green triangle is visible, the winding order is -correct (CCW = front face) - -[[file:Winding order demo.png]] - In Sixth 3D, backface culling is *optional* and disabled by default. Enable it per-shape: -- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.html#setBackfaceCulling(boolean)][SolidPolygon.setBackfaceCulling(true)]] -- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.html#setBackfaceCulling(boolean)][TexturedPolygon.setBackfaceCulling(true)]] +- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidTriangle.html#setBackfaceCulling(boolean)][SolidTriangle.setBackfaceCulling(true)]] +- [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html#setBackfaceCulling(boolean)][TexturedTriangle.setBackfaceCulling(true)]] - [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.html#setBackfaceCulling(boolean)][AbstractCompositeShape.setBackfaceCulling(true)]] (applies to all sub-shapes) diff --git a/doc/perspective-correct-textures/index.org b/doc/perspective-correct-textures/index.org index 35f2cce..3b734a0 100644 --- a/doc/perspective-correct-textures/index.org +++ b/doc/perspective-correct-textures/index.org @@ -78,7 +78,7 @@ negligible. The [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.html][Slicer]] class recursively splits triangles: #+BEGIN_SRC java -void slice(TexturedPolygon polygon) { +void slice(TexturedTriangle polygon) { // Find the longest edge BorderLine longest = findLongestEdge(polygon); @@ -214,7 +214,7 @@ This visualization helps you: | Class | Purpose | |-----------------+--------------------------------------| -| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.html][TexturedPolygon]] | Textured triangle shape | +| [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.html][TexturedTriangle]] | Textured triangle shape | | [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.html][Slicer]] | Recursive triangle subdivision | | [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.html][Texture]] | Mipmap container with Graphics2D | | [[https://www3.svjatoslav.eu/projects/sixth-3d/apidocs/eu/svjatoslav/sixth/e3d/renderer/raster/texture/TextureBitmap.html][TextureBitmap]] | Raw pixel array for one mipmap level | diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSG.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSG.java deleted file mode 100644 index 2d26a98..0000000 --- a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSG.java +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Sixth 3D engine. Author: Svjatoslav Agejenko. - * This project is released under Creative Commons Zero (CC0) license. - */ -package eu.svjatoslav.sixth.e3d.csg; - -import eu.svjatoslav.sixth.e3d.geometry.Point3D; -import eu.svjatoslav.sixth.e3d.math.Vertex; -import eu.svjatoslav.sixth.e3d.renderer.raster.Color; -import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; -import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; -import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonMesh; - -import java.util.ArrayList; -import java.util.List; - -/** - * Represents a solid for Constructive Solid Geometry (CSG) operations. - * - *

CSG allows combining 3D shapes using boolean operations:

- *
    - *
  • Union: Combine two shapes into one
  • - *
  • Subtract: Carve one shape out of another
  • - *
  • Intersect: Keep only the overlapping volume
  • - *
- * - *

Usage example:

- *
{@code
- * // Create shapes from existing composite shapes
- * SolidPolygonCube cube = new SolidPolygonCube(new Point3D(0, 0, 0), 80, Color.RED);
- * SolidPolygonSphere sphere = new SolidPolygonSphere(new Point3D(0, 0, 0), 96, 12, Color.BLUE);
- *
- * // Convert to CSG solids
- * CSG cubeCSG = CSG.fromCompositeShape(cube);
- * CSG sphereCSG = CSG.fromCompositeShape(sphere);
- *
- * // Perform boolean operation
- * CSG result = cubeCSG.subtract(sphereCSG);
- *
- * // Render the result
- * SolidPolygonMesh mesh = result.toMesh(new Color(255, 100, 100), new Point3D(0, 0, 0));
- * shapes.addShape(mesh);
- * }
- * - * @see CSGNode the BSP tree node used internally - * @see CSGPolygon the N-gon polygon type used for BSP operations - * @see SolidPolygonMesh the renderable mesh created from CSG results - */ -public class CSG { - - /** - * The list of polygons that make up this solid. - */ - public final List polygons = new ArrayList<>(); - - /** - * Creates an empty CSG solid. - */ - public CSG() { - } - - /** - * Creates a CSG solid from a list of CSG polygons. - * - * @param polygonList the polygons to include - * @return a new CSG solid - */ - public static CSG fromPolygons(final List polygonList) { - final CSG csg = new CSG(); - csg.polygons.addAll(polygonList); - return csg; - } - - /** - * Creates a CSG solid from a list of SolidPolygon triangles. - * - *

Each SolidPolygon is converted to a CSGPolygon (3-vertex N-gon). - * The color from each SolidPolygon is preserved.

- * - * @param solidPolygons the triangles to convert - * @return a new CSG solid - */ - public static CSG fromSolidPolygons(final List solidPolygons) { - final List csgPolygons = new ArrayList<>(solidPolygons.size()); - - for (final SolidPolygon sp : solidPolygons) { - final List vertices = new ArrayList<>(3); - for (int i = 0; i < 3; i++) { - final Vertex v = new Vertex(sp.vertices[i].coordinate); - v.normal = sp.vertices[i].normal; - vertices.add(v); - } - - final CSGPolygon csgPoly = new CSGPolygon(vertices, sp.getColor()); - csgPolygons.add(csgPoly); - } - - return fromPolygons(csgPolygons); - } - - /** - * Creates a CSG solid from a composite shape. - * - *

Extracts all SolidPolygon triangles from the composite shape - * and converts them to CSGPolygons. This allows using shapes like - * {@code SolidPolygonCube}, {@code SolidPolygonSphere}, etc. with CSG operations.

- * - * @param shape the composite shape to convert - * @return a new CSG solid containing all triangles from the shape - */ - public static CSG fromCompositeShape(final AbstractCompositeShape shape) { - return fromSolidPolygons(shape.extractSolidPolygons()); - } - - /** - * Creates a deep clone of this CSG solid. - * - * @return a new CSG solid with cloned polygons - */ - public CSG clone() { - final CSG csg = new CSG(); - for (final CSGPolygon p : polygons) { - csg.polygons.add(p.clone()); - } - return csg; - } - - /** - * Returns the list of polygons in this solid. - * - * @return the polygon list - */ - public List toPolygons() { - return polygons; - } - - /** - * Performs a union operation with another CSG solid. - * - *

The result contains all points that are in either solid.

- * - *

Algorithm:

- *
-     * Union(A, B) = clip(A to outside B) + clip(B to outside A)
-     * 
- *
    - *
  1. Clip A's polygons to keep only parts outside B
  2. - *
  3. Clip B's polygons to keep only parts outside A
  4. - *
  5. Invert B, clip to A, invert again (keeps B's surface inside A)
  6. - *
  7. Build final tree from all remaining polygons
  8. - *
- * - * @param csg the other solid to union with - * @return a new CSG solid representing the union - */ - public CSG union(final CSG csg) { - // Create BSP trees from both solids - final CSGNode a = new CSGNode(clone().polygons); - final CSGNode b = new CSGNode(csg.clone().polygons); - - // Remove from A any parts that are inside B - a.clipTo(b); - - // Remove from B any parts that are inside A - b.clipTo(a); - - // Invert B temporarily to capture B's interior surface that touches A - b.invert(); - b.clipTo(a); - b.invert(); - - // Combine all polygons into A's tree - a.build(b.allPolygons()); - - return CSG.fromPolygons(a.allPolygons()); - } - - /** - * Performs a subtraction operation with another CSG solid. - * - *

The result contains all points that are in this solid but not in the other. - * This effectively carves the other solid out of this one.

- * - *

Algorithm:

- *
-     * Subtract(A, B) = A - B = clip(inverted A to B) inverted
-     * 
- *
    - *
  1. Invert A (turning solid into cavity, cavity into solid)
  2. - *
  3. Clip inverted A to keep only parts inside B
  4. - *
  5. Clip B to keep only parts inside inverted A
  6. - *
  7. Invert B twice to get B's cavity surface
  8. - *
  9. Combine and invert final result
  10. - *
- * - *

The inversion trick converts "subtract B from A" into "intersect A - * with the inverse of B", which the BSP algorithm handles naturally.

- * - * @param csg the solid to subtract - * @return a new CSG solid representing the difference - */ - public CSG subtract(final CSG csg) { - // Create BSP trees from both solids - final CSGNode a = new CSGNode(clone().polygons); - final CSGNode b = new CSGNode(csg.clone().polygons); - - // Invert A: what was solid becomes empty, what was empty becomes solid - // This transforms the problem into finding the intersection of inverted-A and B - a.invert(); - - // Remove from inverted-A any parts outside B (keep intersection) - a.clipTo(b); - - // Remove from B any parts outside inverted-A (keep intersection) - b.clipTo(a); - - // Capture B's interior surface - b.invert(); - b.clipTo(a); - b.invert(); - - // Combine B's interior surface with A - a.build(b.allPolygons()); - - // Invert result to convert back from "intersection with inverse" to "subtraction" - a.invert(); - - return CSG.fromPolygons(a.allPolygons()); - } - - /** - * Performs an intersection operation with another CSG solid. - * - *

The result contains only the points that are in both solids.

- * - *

Algorithm:

- *
-     * Intersect(A, B) = clip(inverted A to outside B) inverted
-     * 
- *
    - *
  1. Invert A (swap inside/outside)
  2. - *
  3. Clip inverted-A to B, keeping parts outside B
  4. - *
  5. Invert B, clip to A (captures B's interior surface)
  6. - *
  7. Clip B again to ensure proper boundaries
  8. - *
  9. Combine and invert final result
  10. - *
- * - *

This uses the principle: A ∩ B = ¬(¬A ∪ ¬B)

- * - * @param csg the other solid to intersect with - * @return a new CSG solid representing the intersection - */ - public CSG intersect(final CSG csg) { - // Create BSP trees from both solids - final CSGNode a = new CSGNode(clone().polygons); - final CSGNode b = new CSGNode(csg.clone().polygons); - - // Invert A to transform intersection into a union-like operation - a.invert(); - - // Clip B to keep only parts inside inverted-A (outside original A) - b.clipTo(a); - - // Invert B to capture its interior surface - b.invert(); - - // Clip A to keep only parts inside inverted-B (outside original B) - a.clipTo(b); - - // Clip B again to ensure proper boundary handling - b.clipTo(a); - - // Combine B's interior surface with A - a.build(b.allPolygons()); - - // Invert result to get the actual intersection - a.invert(); - - return CSG.fromPolygons(a.allPolygons()); - } - - /** - * Returns the inverse of this solid. - * - *

The inverse has all polygons flipped, effectively turning the solid inside-out.

- * - * @return a new CSG solid representing the inverse - */ - public CSG inverse() { - final CSG csg = clone(); - for (final CSGPolygon p : csg.polygons) { - p.flip(); - } - return csg; - } - - /** - * Converts this CSG solid to a renderable mesh with a uniform color. - * - *

All polygons are rendered with the specified color, ignoring - * any colors stored in the CSGPolygons.

- * - * @param color the color to apply to all triangles - * @param location the position in 3D space for the mesh - * @return a renderable mesh containing triangles - */ - public SolidPolygonMesh toMesh(final Color color, final Point3D location) { - final List triangles = new ArrayList<>(); - - for (final CSGPolygon polygon : polygons) { - triangulatePolygon(polygon, color, triangles); - } - - return new SolidPolygonMesh(triangles, location); - } - - /** - * Triangulates a CSGPolygon using fan triangulation. - * - *

Fan triangulation works by selecting the first vertex as a central point - * and connecting it to each pair of consecutive vertices. For an N-gon, - * this produces (N-2) triangles:

- * - *
-     * Original N-gon:    v0-v1-v2-v3-v4...
-     * Triangles:         v0-v1-v2, v0-v2-v3, v0-v3-v4, ...
-     * 
- * - *

This method is suitable for convex polygons. For concave polygons, - * it may produce overlapping triangles, but CSG operations typically - * generate convex polygon fragments.

- * - * @param polygon the polygon to triangulate (may have 3+ vertices) - * @param color the color to apply to all resulting triangles - * @param triangles the list to add the resulting SolidPolygon triangles to - */ - private void triangulatePolygon(final CSGPolygon polygon, final Color color, - final List triangles) { - final int vertexCount = polygon.vertices.size(); - - // Skip degenerate polygons (less than 3 vertices cannot form a triangle) - if (vertexCount < 3) { - return; - } - - // Use the first vertex as the "pivot" of the fan - final Point3D v0 = polygon.vertices.get(0).coordinate; - - // Create triangles by connecting v0 to each consecutive pair of vertices - // For a polygon with vertices [v0, v1, v2, v3], we create: - // - Triangle 1: v0, v1, v2 (i=1) - // - Triangle 2: v0, v2, v3 (i=2) - for (int i = 1; i < vertexCount - 1; i++) { - final Point3D v1 = polygon.vertices.get(i).coordinate; - final Point3D v2 = polygon.vertices.get(i + 1).coordinate; - - // Clone the points to avoid sharing references with the original polygon - final SolidPolygon triangle = new SolidPolygon( - new Point3D(v0), - new Point3D(v1), - new Point3D(v2), - color - ); - - triangles.add(triangle); - } - } -} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java index 0766122..86d1490 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java @@ -4,6 +4,8 @@ */ package eu.svjatoslav.sixth.e3d.csg; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; + import java.util.ArrayList; import java.util.List; @@ -14,7 +16,7 @@ import java.util.List; * Each node divides 3D space into two half-spaces using a plane, enabling * efficient spatial queries and polygon clipping.

* - *

BSP Tree Structure:

+ *

BSP Tree Structure:

*
  *                 [Node: plane P]
  *                /               \
@@ -23,73 +25,34 @@ import java.util.List;
  *        normal)             of P's normal)
  * 
* - *

Key Properties:

- *
    - *
  • polygons: Polygons coplanar with this node's partitioning plane
  • - *
  • plane: The partitioning plane that divides space
  • - *
  • front: Subtree for the half-space the plane normal points toward
  • - *
  • back: Subtree for the opposite half-space
  • - *
- * - *

CSG Algorithm Overview:

- *

CSG boolean operations (union, subtraction, intersection) work by:

- *
    - *
  1. Building BSP trees from both input solids
  2. - *
  3. Clipping each tree against the other (removing overlapping geometry)
  4. - *
  5. Optionally inverting trees (for subtraction and intersection)
  6. - *
  7. Collecting the resulting polygons
  8. - *
- * - * @see CSG the main CSG class that provides the boolean operation API + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape * @see CSGPlane the plane type used for spatial partitioning - * @see CSGPolygon the polygon type stored in BSP nodes + * @see SolidPolygon the polygon type stored in BSP nodes */ public class CSGNode { /** * Polygons that lie on this node's partitioning plane. - * - *

These polygons are coplanar with the plane and are stored directly - * in this node rather than being pushed down to child nodes. This includes - * both polygons originally on this plane and polygons split by planes above - * that ended up coplanar here.

*/ - public final List polygons = new ArrayList<>(); + public final List polygons = new ArrayList<>(); /** * The partitioning plane for this node. - * - *

This plane divides 3D space into two half-spaces: front (where the - * normal points) and back. All polygons in this node are coplanar with - * this plane. Child nodes contain polygons on their respective sides.

- * - *

Null for leaf nodes (empty subtrees).

*/ public CSGPlane plane; /** * The front child subtree. - * - *

Contains polygons that lie in the front half-space of this node's plane - * (the side the normal points toward). May be null if no polygons exist - * in the front half-space.

*/ public CSGNode front; /** * The back child subtree. - * - *

Contains polygons that lie in the back half-space of this node's plane - * (the side opposite the normal direction). May be null if no polygons exist - * in the back half-space.

*/ public CSGNode back; /** * Creates an empty BSP node with no plane or children. - * - *

This constructor creates a leaf node. The plane, front, and back - * fields will be populated when polygons are added via {@link #build(List)}.

*/ public CSGNode() { } @@ -97,35 +60,26 @@ public class CSGNode { /** * Creates a BSP tree from a list of polygons. * - *

Delegates to {@link #build(List)} to construct the tree.

- * * @param polygons the polygons to partition into a BSP tree */ - public CSGNode(final List polygons) { + public CSGNode(final List polygons) { build(polygons); } /** * Creates a deep clone of this BSP tree. * - *

Recursively clones all child nodes and polygons. The resulting tree - * is completely independent of the original.

- * * @return a new CSGNode tree with cloned data */ public CSGNode clone() { final CSGNode node = new CSGNode(); - // Clone the plane if present node.plane = plane != null ? plane.clone() : null; - - // Recursively clone child subtrees node.front = front != null ? front.clone() : null; node.back = back != null ? back.clone() : null; - // Clone each polygon in this node - for (final CSGPolygon p : polygons) { - node.polygons.add(p.clone()); + for (final SolidPolygon p : polygons) { + node.polygons.add(p.deepClone()); } return node; @@ -133,36 +87,16 @@ public class CSGNode { /** * Inverts this BSP tree, converting "inside" to "outside" and vice versa. - * - *

This operation is fundamental to CSG subtraction and intersection:

- *
    - *
  • All polygon normals are flipped (reversing their facing direction)
  • - *
  • All plane normals are flipped
  • - *
  • Front and back subtrees are swapped
  • - *
- * - *

After inversion:

- *
    - *
  • What was solid becomes empty space
  • - *
  • What was empty space becomes solid
  • - *
  • Front/back relationships are reversed throughout the tree
  • - *
- * - *

This is used in CSG subtraction where solid B "carves out" of solid A - * by inverting B, unioning, then inverting the result.

*/ public void invert() { - // Flip all polygons at this node - for (final CSGPolygon polygon : polygons) { + for (final SolidPolygon polygon : polygons) { polygon.flip(); } - // Flip the partitioning plane if (plane != null) { plane.flip(); } - // Recursively invert child subtrees if (front != null) { front.invert(); } @@ -170,7 +104,6 @@ public class CSGNode { back.invert(); } - // Swap front and back children since the half-spaces are now reversed final CSGNode temp = front; front = back; back = temp; @@ -179,58 +112,34 @@ public class CSGNode { /** * Clips a list of polygons against this BSP tree. * - *

This recursively removes the portions of the input polygons that lie - * inside the solid represented by this BSP tree. The result contains only - * the portions that are outside this solid.

- * - *

Algorithm:

- *
    - *
  1. At each node, split input polygons by the node's plane
  2. - *
  3. Polygons in front go to front child for further clipping
  4. - *
  5. Polygons in back go to back child for further clipping
  6. - *
  7. Coplanar polygons are kept (they're on the surface)
  8. - *
  9. If no back child exists, back polygons are discarded (they're inside)
  10. - *
- * - *

This is used during CSG operations to remove overlapping geometry.

- * * @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) { - // Base case: if this is a leaf node, return copies of all polygons + public List clipPolygons(final List polygons) { if (plane == null) { return new ArrayList<>(polygons); } - // Split all input polygons by this node's plane - final List frontList = new ArrayList<>(); - final List backList = new ArrayList<>(); + final List frontList = new ArrayList<>(); + final List backList = new ArrayList<>(); - for (final CSGPolygon polygon : polygons) { - // Split polygon into front/back/coplanar parts - // Note: coplanar polygons go into both front and back lists + for (final SolidPolygon polygon : polygons) { plane.splitPolygon(polygon, frontList, backList, frontList, backList); } - // Recursively clip front polygons against front subtree - List resultFront = frontList; + List resultFront = frontList; if (front != null) { resultFront = front.clipPolygons(frontList); } - // Recursively clip back polygons against back subtree - List resultBack = backList; + List resultBack = backList; if (back != null) { resultBack = back.clipPolygons(backList); } else { - // No back child means this is a boundary - discard back polygons - // (they would be inside the solid we're clipping against) resultBack = new ArrayList<>(); } - // Combine the clipped results - final List result = new ArrayList<>(resultFront.size() + resultBack.size()); + final List result = new ArrayList<>(resultFront.size() + resultBack.size()); result.addAll(resultFront); result.addAll(resultBack); return result; @@ -239,22 +148,13 @@ public class CSGNode { /** * Clips this BSP tree against another BSP tree. * - *

This removes from this tree all polygons that lie inside the solid - * represented by the other BSP tree. Used during CSG operations to - * eliminate overlapping geometry.

- * - *

The operation modifies this tree in place, replacing all polygons - * with their clipped versions.

- * - * @param bsp the BSP tree to clip against (the "cutter") + * @param bsp the BSP tree to clip against */ public void clipTo(final CSGNode bsp) { - // Clip all polygons at this node against the other BSP tree - final List newPolygons = bsp.clipPolygons(polygons); + final List newPolygons = bsp.clipPolygons(polygons); polygons.clear(); polygons.addAll(newPolygons); - // Recursively clip child subtrees if (front != null) { front.clipTo(bsp); } @@ -266,16 +166,11 @@ public class CSGNode { /** * Collects all polygons from this BSP tree into a flat list. * - *

Recursively traverses the entire tree and collects all polygons - * from all nodes. This is used after CSG operations to extract the - * final result as a simple polygon list.

- * * @return a new list containing all polygons in this tree */ - public List allPolygons() { - final List result = new ArrayList<>(polygons); + public List allPolygons() { + final List result = new ArrayList<>(polygons); - // Recursively collect polygons from child subtrees if (front != null) { result.addAll(front.allPolygons()); } @@ -289,58 +184,24 @@ public class CSGNode { /** * Builds or extends this BSP tree from a list of polygons. * - *

This is the core BSP tree construction algorithm. It partitions - * space by selecting a splitting plane and recursively building subtrees.

- * - *

Algorithm:

- *
    - *
  1. If this node has no plane, use the first polygon's plane as the partitioning plane
  2. - *
  3. For each polygon: - *
      - *
    • Coplanar polygons are stored in this node
    • - *
    • Front polygons go to the front list
    • - *
    • Back polygons go to the back list
    • - *
    • Spanning polygons are split into front and back parts
    • - *
    - *
  4. - *
  5. Recursively build front subtree with front polygons
  6. - *
  7. Recursively build back subtree with back polygons
  8. - *
- * - *

Calling Conventions:

- *
    - *
  • Can be called multiple times to add more polygons to an existing tree
  • - *
  • Empty polygon list is a no-op
  • - *
  • Creates child nodes as needed
  • - *
- * * @param polygonList the polygons to add to this BSP tree */ - public void build(final List polygonList) { - // Base case: no polygons to add + public void build(final List polygonList) { if (polygonList.isEmpty()) { return; } - // Initialize the partitioning plane if this is a new node - // Use the first polygon's plane as the splitting plane if (plane == null) { - plane = polygonList.get(0).plane.clone(); + plane = polygonList.get(0).getPlane().clone(); } - // Classify each polygon relative to this node's plane - final List frontList = new ArrayList<>(); - final List backList = new ArrayList<>(); + final List frontList = new ArrayList<>(); + final List backList = new ArrayList<>(); - for (final CSGPolygon polygon : polygonList) { - // Split the polygon and distribute to appropriate lists: - // - coplanarFront/coplanarBack → this node's polygons list - // - front → frontList (for front subtree) - // - back → backList (for back subtree) + for (final SolidPolygon polygon : polygonList) { plane.splitPolygon(polygon, polygons, polygons, frontList, backList); } - // Recursively build front subtree if (!frontList.isEmpty()) { if (front == null) { front = new CSGNode(); @@ -348,7 +209,6 @@ public class CSGNode { front.build(frontList); } - // Recursively build back subtree if (!backList.isEmpty()) { if (back == null) { back = new CSGNode(); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java index 473608b..c14f671 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java @@ -6,6 +6,7 @@ package eu.svjatoslav.sixth.e3d.csg; import eu.svjatoslav.sixth.e3d.geometry.Point3D; import eu.svjatoslav.sixth.e3d.math.Vertex; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; import java.util.ArrayList; import java.util.List; @@ -13,173 +14,103 @@ import java.util.List; /** * Represents an infinite plane in 3D space using the Hesse normal form. * - *

A plane is defined by a normal vector (perpendicular to the plane surface) - * and a scalar value 'w' representing the signed distance from the origin. - * The plane equation is: {@code normal.x * x + normal.y * y + normal.z * z = w}

- * *

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

- *
    - *
  • Front half-space: Points where {@code normal · point > w}
  • - *
  • Back half-space: Points where {@code normal · point < w}
  • - *
- * - *

Planes are used to:

- *
    - *
  • Define the surface orientation of {@link CSGPolygon} faces
  • - *
  • Split polygons that cross BSP partition boundaries
  • - *
  • Determine which side of a BSP node a polygon lies on
  • - *
+ * in CSG. They divide 3D space into two half-spaces.

* - * @see CSGPolygon polygons that reference their containing plane + * @see SolidPolygon polygons that reference their containing plane * @see CSGNode BSP tree nodes that use planes for spatial partitioning */ public class CSGPlane { /** * Epsilon value used for floating-point comparisons. - * - *

When determining which side of a plane a point lies on, values within - * this threshold are considered coplanar (on the plane). This prevents - * numerical instability from causing infinite recursion or degenerate - * polygons during BSP operations.

*/ public static final double EPSILON = 0.01; /** * The unit normal vector perpendicular to the plane surface. - * - *

The direction of the normal determines which side is "front" - * and which is "back". The front is the side the normal points toward.

*/ public Point3D normal; /** * The signed distance from the origin to the plane along the normal. - * - *

This is equivalent to the dot product of the normal with any point - * on the plane. For a plane defined by point P and normal N: - * {@code w = N · P}

*/ - public double w; + public double distance; /** * Creates a plane with the given normal and distance. * - * @param normal the unit normal vector (caller must ensure it's normalized) - * @param w the signed distance from origin to the plane + * @param normal the unit normal vector + * @param distance the signed distance from origin to the plane */ - public CSGPlane(final Point3D normal, final double w) { + public CSGPlane(final Point3D normal, final double distance) { this.normal = normal; - this.w = w; + this.distance = distance; } /** * Creates a plane from three non-collinear points. * - *

The plane passes through all three points. The normal is computed - * using the cross product of vectors (b-a) and (c-a), then normalized. - * The winding order of the points determines the normal direction:

- *
    - *
  • Counter-clockwise (CCW) winding → normal points toward viewer
  • - *
  • Clockwise (CW) winding → normal points away from viewer
  • - *
- * * @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 CSGPlane passing through the three points - * @throws ArithmeticException if the points are collinear (cross product is zero) */ public static CSGPlane fromPoints(final Point3D a, final Point3D b, final Point3D c) { - // Compute two edge vectors from point a - final Point3D edge1 = b.minus(a); - final Point3D edge2 = c.minus(a); + final Point3D edge1 = b.withSubtracted(a); + final Point3D edge2 = c.withSubtracted(a); + + final Point3D cross = edge1.cross(edge2); + + if (cross.getVectorLength() < EPSILON) { + throw new ArithmeticException( + "Cannot create plane from collinear points: cross product is zero"); + } - // Cross product gives the normal direction (perpendicular to both edges) - final Point3D n = edge1.cross(edge2).unit(); + final Point3D n = cross.unit(); - // Distance from origin is the projection of any point on the plane onto the normal return new CSGPlane(n, n.dot(a)); } /** * Creates a deep clone of this plane. * - *

The normal vector is cloned to avoid shared references.

- * * @return a new CSGPlane with the same normal and distance */ public CSGPlane clone() { - return new CSGPlane(new Point3D(normal.x, normal.y, normal.z), w); + return new CSGPlane(new Point3D(normal.x, normal.y, normal.z), distance); } /** * Flips the plane orientation by negating the normal and distance. - * - *

This effectively swaps the front and back half-spaces. After flipping:

- *
    - *
  • Points that were in front are now in back
  • - *
  • Points that were in back are now in front
  • - *
  • Coplanar points remain coplanar
  • - *
- * - *

Used during CSG operations when inverting solids (converting "inside" - * to "outside" and vice versa).

*/ public void flip() { - normal = normal.negated(); - w = -w; + normal = normal.withNegated(); + distance = -distance; } /** * Splits a polygon by this plane, classifying and potentially dividing it. * - *

This is the core operation for BSP tree construction. The polygon is - * classified based on where its vertices lie relative to the plane:

- * - *

Classification types:

- *
    - *
  • COPLANAR (0): All vertices lie on the plane (within EPSILON)
  • - *
  • FRONT (1): All vertices are in the front half-space
  • - *
  • BACK (2): All vertices are in the back half-space
  • - *
  • SPANNING (3): Vertices are on both sides (polygon crosses the plane)
  • - *
- * - *

Destination lists:

- *
    - *
  • coplanarFront: Coplanar polygons with same-facing normals
  • - *
  • coplanarBack: Coplanar polygons with opposite-facing normals
  • - *
  • front: Polygons entirely in front half-space
  • - *
  • back: Polygons entirely in back half-space
  • - *
- * - *

Spanning polygon handling:

- *

When a polygon spans the plane, it is split into two polygons:

- *
    - *
  1. Vertices on the front side become a new polygon (added to front list)
  2. - *
  3. Vertices on the back side become a new polygon (added to back list)
  4. - *
  5. Intersection points are computed and added to both polygons
  6. - *
- * * @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 CSGPolygon polygon, - final List coplanarFront, - final List coplanarBack, - final List front, - final List back) { + public void splitPolygon(final SolidPolygon polygon, + final List coplanarFront, + final List coplanarBack, + final List front, + final List back) { PolygonType polygonType = PolygonType.COPLANAR; - final PolygonType[] types = new PolygonType[polygon.vertices.size()]; + final int vertexCount = polygon.getVertexCount(); + final PolygonType[] types = new PolygonType[vertexCount]; - for (int i = 0; i < polygon.vertices.size(); i++) { + for (int i = 0; i < vertexCount; i++) { final Vertex v = polygon.vertices.get(i); - final double t = normal.dot(v.coordinate) - w; + 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); @@ -188,7 +119,7 @@ public class CSGPlane { switch (polygonType) { case COPLANAR: - ((normal.dot(polygon.plane.normal) > 0) ? coplanarFront : coplanarBack).add(polygon); + ((normal.dot(polygon.getPlane().normal) > 0) ? coplanarFront : coplanarBack).add(polygon); break; case FRONT: @@ -200,37 +131,37 @@ public class CSGPlane { break; case SPANNING: - final List f = new ArrayList<>(); - final List b = new ArrayList<>(); + final List frontVertices = new ArrayList<>(); + final List backVertices = new ArrayList<>(); - for (int i = 0; i < polygon.vertices.size(); i++) { - final int j = (i + 1) % polygon.vertices.size(); + for (int i = 0; i < vertexCount; i++) { + final int j = (i + 1) % vertexCount; final PolygonType ti = types[i]; final PolygonType tj = types[j]; final Vertex vi = polygon.vertices.get(i); final Vertex vj = polygon.vertices.get(j); if (ti.isFront()) { - f.add(vi); + frontVertices.add(vi.clone()); } if (ti.isBack()) { - b.add(ti == PolygonType.COPLANAR ? vi.clone() : vi); + backVertices.add(vi.clone()); } if (ti != tj && ti != PolygonType.COPLANAR && tj != PolygonType.COPLANAR) { - final double t = (w - normal.dot(vi.coordinate)) - / normal.dot(vj.coordinate.minus(vi.coordinate)); + final double t = (distance - normal.dot(vi.coordinate)) + / normal.dot(vj.coordinate.withSubtracted(vi.coordinate)); final Vertex v = vi.interpolate(vj, t); - f.add(v); - b.add(v.clone()); + frontVertices.add(v); + backVertices.add(v.clone()); } } - if (f.size() >= 3) { - final CSGPolygon frontPoly = new CSGPolygon(f, polygon.color); + if (frontVertices.size() >= 3) { + final SolidPolygon frontPoly = new SolidPolygon(frontVertices, polygon.getColor(), true); front.add(frontPoly); } - if (b.size() >= 3) { - final CSGPolygon backPoly = new CSGPolygon(b, polygon.color); + if (backVertices.size() >= 3) { + final SolidPolygon backPoly = new SolidPolygon(backVertices, polygon.getColor(), true); back.add(backPoly); } break; diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPolygon.java deleted file mode 100644 index 9ba8ceb..0000000 --- a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPolygon.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Sixth 3D engine. Author: Svjatoslav Agejenko. - * This project is released under Creative Commons Zero (CC0) license. - */ -package eu.svjatoslav.sixth.e3d.csg; - -import eu.svjatoslav.sixth.e3d.math.Vertex; -import eu.svjatoslav.sixth.e3d.renderer.raster.Color; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * An N-gon polygon used for CSG BSP tree operations. - * - *

During BSP tree traversal, polygons may be split by planes, resulting - * in polygons with varying vertex counts (3 or more). The polygon stores - * its vertices, the plane it lies on, and material properties (color).

- * - *

The color is preserved through CSG operations - split polygons inherit - * the color from their parent.

- * - * @see CSG the main CSG solid class - * @see CSGPlane used for splitting polygons - */ -public class CSGPolygon { - - /** - * The vertices defining this polygon's geometry. - * For CSG operations, this can be 3 or more vertices (N-gon). - */ - public final List vertices; - - /** - * The plane that contains this polygon. - * Cached for efficient BSP operations. - */ - public final CSGPlane plane; - - /** - * The color of this polygon. - * Preserved through CSG operations; split polygons inherit this color. - */ - public Color color; - - /** - * Creates a polygon with vertices and a color. - * - * @param vertices the vertices defining this polygon (must be at least 3) - * @param color the color of this polygon - */ - public CSGPolygon(final List vertices, final Color color) { - this.vertices = vertices; - this.color = color; - this.plane = CSGPlane.fromPoints( - vertices.get(0).coordinate, - vertices.get(1).coordinate, - vertices.get(2).coordinate - ); - } - - /** - * Creates a deep clone of this polygon. - * - *

Clones all vertices and preserves the color.

- * - * @return a new CSGPolygon with cloned data - */ - public CSGPolygon clone() { - final List clonedVertices = new ArrayList<>(vertices.size()); - for (final Vertex v : vertices) { - clonedVertices.add(v.clone()); - } - return new CSGPolygon(clonedVertices, this.color); - } - - /** - * Flips the orientation of this polygon. - * - *

Reverses the vertex order and negates vertex normals. - * Also flips the plane. Used during CSG operations when inverting solids.

- */ - public void flip() { - Collections.reverse(vertices); - for (final Vertex v : vertices) { - v.flip(); - } - plane.flip(); - } -} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java index abb48b0..6558a07 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Box.java @@ -136,8 +136,8 @@ public class Box implements Cloneable { * @param size {@link Point3D} specifies box size in x, y and z axis. */ public void setBoxSize(final Point3D size) { - p2.clone(size).scaleDown(2); - p1.clone(p2).invert(); + p2.clone(size).divide(2); + p1.clone(p2).negate(); } } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java index 6e6ac97..78a9b79 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point2D.java @@ -16,9 +16,24 @@ import static java.lang.Math.sqrt; *

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

*
{@code
  * Point2D p = new Point2D(10, 20)
+ *     .multiply(2.0)
  *     .add(new Point2D(5, 5))
- *     .invert();
- * // p is now (-15, -25)
+ *     .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 @@ -59,10 +74,12 @@ public class Point2D implements Cloneable { /** - * Adds another point to this point. The other point is not modified. + * 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; @@ -145,11 +162,13 @@ public class Point2D implements Cloneable { } /** - * Inverts this point's coordinates (negates both x and 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 invert() { + public Point2D negate() { x = -x; y = -y; return this; @@ -164,10 +183,12 @@ public class Point2D implements Cloneable { } /** - * Subtracts another point from this point. The other point is not modified. + * 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; @@ -175,6 +196,34 @@ public class Point2D implements Cloneable { 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. * @@ -202,4 +251,63 @@ public class Point2D implements Cloneable { ", 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/sixth/e3d/geometry/Point3D.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java index c4e6b52..1f51c1c 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java @@ -18,7 +18,7 @@ import static java.lang.Math.*; *

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

*
{@code
  * Point3D p = new Point3D(10, 20, 30)
- *     .scaleUp(2.0)
+ *     .multiply(2.0)
  *     .translateX(5)
  *     .add(new Point3D(1, 1, 1));
  * // p is now (25, 41, 61)
@@ -27,9 +27,9 @@ import static java.lang.Math.*;
  * 

Common operations:

*
{@code
  * // Create points
- * Point3D origin = new Point3D();              // (0, 0, 0)
- * Point3D pos = new Point3D(100, 200, 300);
- * Point3D copy = new Point3D(pos);             // clone
+ * 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);
@@ -38,10 +38,18 @@ import static java.lang.Math.*;
  * pos.rotate(origin, Math.PI / 4, 0);  // rotate 45 degrees on XZ plane
  *
  * // Scale
- * pos.scaleUp(2.0);   // double all coordinates
- * pos.scaleDown(2.0);  // halve all coordinates
+ * 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
@@ -129,10 +137,33 @@ public class Point3D implements Cloneable {
     }
 
     /**
-     * Add other point to current point. Value of other point will not be changed.
+     * Returns a new point at the origin (0, 0, 0).
      *
-     * @param otherPoint point to add.
-     * @return current point.
+     * @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;
@@ -252,11 +283,13 @@ public class Point3D implements Cloneable {
     }
 
     /**
-     * Invert current point coordinates.
+     * Negates this point's coordinates in place.
+     * This point is modified.
      *
-     * @return current point.
+     * @return this point (for chaining)
+     * @see #withNegated() for the non-mutating version that returns a new point
      */
-    public Point3D invert() {
+    public Point3D negate() {
         x = -x;
         y = -y;
         z = -z;
@@ -319,13 +352,14 @@ public class Point3D implements Cloneable {
     }
 
     /**
-     * Scale down current point by factor.
-     * All coordinates will be divided by factor.
+     * Divides all coordinates by a factor.
+     * This point is modified.
      *
-     * @param factor factor to scale by.
-     * @return current point.
+     * @param factor the divisor
+     * @return this point (for chaining)
+     * @see #withDivided(double) for the non-mutating version that returns a new point
      */
-    public Point3D scaleDown(final double factor) {
+    public Point3D divide(final double factor) {
         x /= factor;
         y /= factor;
         z /= factor;
@@ -333,13 +367,14 @@ public class Point3D implements Cloneable {
     }
 
     /**
-     * Scale up current point by factor.
-     * All coordinates will be multiplied by factor.
+     * Multiplies all coordinates by a factor.
+     * This point is modified.
      *
-     * @param factor factor to scale by.
-     * @return current point.
+     * @param factor the multiplier
+     * @return this point (for chaining)
+     * @see #withMultiplied(double) for the non-mutating version that returns a new point
      */
-    public Point3D scaleUp(final double factor) {
+    public Point3D multiply(final double factor) {
         x *= factor;
         y *= factor;
         z *= factor;
@@ -360,10 +395,12 @@ public class Point3D implements Cloneable {
     }
 
     /**
-     * Subtracts another point from this point.
+     * 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;
@@ -432,8 +469,6 @@ public class Point3D implements Cloneable {
         return this;
     }
 
-    // ========== Non-mutating vector operations (return new Point3D) ==========
-
     /**
      * Computes the dot product of this vector with another.
      *
@@ -445,11 +480,11 @@ public class Point3D implements Cloneable {
     }
 
     /**
-     * Computes the cross product of this vector with another.
+     * 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
+     * @return a new Point3D representing the cross-product
      */
     public Point3D cross(final Point3D other) {
         return new Point3D(
@@ -461,23 +496,25 @@ public class Point3D implements Cloneable {
 
     /**
      * Returns a new point that is the sum of this point and another.
-     * Neither point is modified.
+     * 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 plus(final Point3D other) {
+    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.
-     * Neither point is modified.
+     * 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 minus(final Point3D other) {
+    public Point3D withSubtracted(final Point3D other) {
         return new Point3D(x - other.x, y - other.y, z - other.z);
     }
 
@@ -486,8 +523,9 @@ public class Point3D implements Cloneable {
      * This point is not modified.
      *
      * @return a new Point3D with negated coordinates
+     * @see #negate() for the mutating version
      */
-    public Point3D negated() {
+    public Point3D withNegated() {
         return new Point3D(-x, -y, -z);
     }
 
@@ -523,23 +561,25 @@ public class Point3D implements Cloneable {
 
     /**
      * Returns a new point with coordinates multiplied by a factor.
-     * This point is not modified. Unlike {@link #scaleUp}, this returns a new instance.
+     * This point is not modified.
      *
-     * @param factor the scaling factor
-     * @return a new scaled Point3D
+     * @param factor the multiplier
+     * @return a new Point3D with multiplied coordinates
+     * @see #multiply(double) for the mutating version
      */
-    public Point3D times(final double factor) {
+    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. Unlike {@link #scaleDown}, this returns a new instance.
+     * This point is not modified.
      *
      * @param factor the divisor
-     * @return a new scaled Point3D
+     * @return a new Point3D with divided coordinates
+     * @see #divide(double) for the mutating version
      */
-    public Point3D dividedBy(final double factor) {
+    public Point3D withDivided(final double factor) {
         return new Point3D(x / factor, y / factor, z / factor);
     }
 
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java
index 50f9dc6..71176b7 100644
--- a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Polygon.java
@@ -82,4 +82,26 @@ public class Polygon {
 
     }
 
+    /**
+     * Tests whether a point lies inside a triangle using integer coordinates.
+     *
+     * 

This overload creates temporary Point2D objects for the vertices, + * suitable when the caller has pre-computed integer coordinates.

+ * + * @param point the point to test + * @param x1 the x coordinate of the first vertex + * @param y1 the y coordinate of the first vertex + * @param x2 the x coordinate of the second vertex + * @param y2 the y coordinate of the second vertex + * @param x3 the x coordinate of the third vertex + * @param y3 the y coordinate of the third vertex + * @return {@code true} if the point is inside the triangle + */ + public static boolean pointWithinPolygon(final Point2D point, + final int x1, final int y1, + final int x2, final int y2, + final int x3, final int y3) { + return pointWithinPolygon(point, new Point2D(x1, y1), new Point2D(x2, y2), new Point2D(x3, y3)); + } + } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java index 23c2079..95c3f92 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Rectangle.java @@ -30,7 +30,7 @@ public class Rectangle { */ public Rectangle(final double size) { p2 = new Point2D(size / 2, size / 2); - p1 = p2.clone().invert(); + p1 = p2.clone().negate(); } /** diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java index 8737a28..d453700 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/gui/Camera.java @@ -119,7 +119,7 @@ public class Camera implements FrameListener { if (currentSpeed <= SPEED_LIMIT) return; - movementVector.scaleDown(currentSpeed / SPEED_LIMIT); + movementVector.divide(currentSpeed / SPEED_LIMIT); } /** diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java index 46c628e..d3cf13e 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Transform.java @@ -11,6 +11,14 @@ import eu.svjatoslav.sixth.e3d.geometry.Point3D; * *

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

* + *

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
  • + *
+ * * @see Quaternion * @see Point3D */ @@ -120,21 +128,38 @@ public class Transform implements Cloneable { * Applies this transform to a point: rotation followed by translation. * * @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) { rotation.toMatrix().transform(point, point); point.add(translation); } + /** + * 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 void setTranslation(final Point3D translation) { + public Transform setTranslation(final Point3D translation) { this.translation.x = translation.x; this.translation.y = translation.y; this.translation.z = translation.z; + return this; } /** diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java index ba63d6c..f1eed76 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java @@ -153,7 +153,7 @@ public class Vertex { */ public void flip() { if (normal != null) { - normal = normal.negated(); + normal = normal.withNegated(); } } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java index 900c508..63fca47 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/octree/raytracer/CameraView.java @@ -49,7 +49,7 @@ public class CameraView { temp.clone(bottomRight); m.transform(temp, bottomRight); - camera.getTransform().getTranslation().clone().scaleDown(zoom).addTo(cameraCenter, topLeft, topRight, bottomLeft, bottomRight); + camera.getTransform().getTranslation().clone().divide(zoom).addTo(cameraCenter, topLeft, topRight, bottomLeft, bottomRight); } } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java index c594571..390027d 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/Color.java @@ -15,24 +15,18 @@ package eu.svjatoslav.sixth.e3d.renderer.raster; *
{@code
  * // Use predefined color constants
  * Color red = Color.RED;
- * Color semiTransparent = new Color(255, 0, 0, 128);
+ * 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 floating-point components (0.0-1.0)
- * Color half = new Color(0.5, 0.5, 0.5, 1.0);
- *
- * // Create from hex string
- * Color hex6 = new Color("FF8800");     // RGB, fully opaque
- * Color hex8 = new Color("FF880080");   // RGBA with alpha
- * Color hex3 = new Color("F80");        // Short RGB format
- *
  * // Create from packed RGB integer
  * Color packed = new Color(0xFF8800);
- *
- * // Convert to AWT for interop with Java Swing
- * java.awt.Color awtColor = custom.toAwtColor();
  * }
* *

Important: Always use this class instead of {@link java.awt.Color} when @@ -64,6 +58,24 @@ public final class Color { /** Fully transparent (alpha = 0). */ public static final Color TRANSPARENT = new Color(0, 0, 0, 0); + /** + * 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); + } + /** * Red component. 0-255. */ diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java index 3112fde..c9b6fee 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java @@ -9,13 +9,16 @@ import eu.svjatoslav.sixth.e3d.math.TransformStack; import eu.svjatoslav.sixth.e3d.math.Vertex; import eu.svjatoslav.sixth.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 an array of vertex coordinates. + * 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 fixed number of vertices + * 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 @@ -35,7 +38,7 @@ import java.util.concurrent.atomic.AtomicInteger; * * public void paint(RenderingContext ctx) { * // Custom painting logic using ctx.graphics and - * // coordinates[i].transformedCoordinate for screen positions + * // vertices.get(i).transformedCoordinate for screen positions * } * } * }

@@ -62,8 +65,11 @@ public abstract class AbstractCoordinateShape extends AbstractShape { * 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 Vertex[] vertices; + public final List vertices; /** * Average Z-depth of this shape in screen space after transformation. @@ -79,10 +85,10 @@ public abstract class AbstractCoordinateShape extends AbstractShape { * @param vertexCount the number of vertices in this shape */ public AbstractCoordinateShape(final int vertexCount) { - vertices = new Vertex[vertexCount]; - for (int i = 0; i < vertexCount; i++) - vertices[i] = new Vertex(); - + vertices = new ArrayList<>(vertexCount); + for (int i = 0; i < vertexCount; i++) { + vertices.add(new Vertex()); + } shapeId = lastShapeId.getAndIncrement(); } @@ -92,8 +98,17 @@ public abstract class AbstractCoordinateShape extends AbstractShape { * @param vertices the vertices defining this shape's geometry */ public AbstractCoordinateShape(final Vertex... vertices) { - this.vertices = 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(); } @@ -137,12 +152,13 @@ public abstract class AbstractCoordinateShape extends AbstractShape { accumulatedZ += geometryPoint.transformedCoordinate.z; - if (!geometryPoint.transformedCoordinate.isVisible()) + if (!geometryPoint.transformedCoordinate.isVisible()) { paint = false; + } } if (paint) { - onScreenZ = accumulatedZ / vertices.length; + onScreenZ = accumulatedZ / vertices.size(); aggregator.queueShapeForRendering(this); } } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java index 974e285..1e661a4 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java @@ -77,7 +77,7 @@ public class Billboard extends AbstractCoordinateShape { public void paint(final RenderingContext targetRenderingArea) { // distance from camera/viewer to center of the texture - final double z = vertices[0].transformedCoordinate.z; + final double z = vertices.get(0).transformedCoordinate.z; // compute forward oriented texture visible distance from center final double visibleHorizontalDistanceFromCenter = (targetRenderingArea.width @@ -92,7 +92,7 @@ public class Billboard extends AbstractCoordinateShape { final TextureBitmap textureBitmap = texture.getZoomedBitmap(zoom); - final Point2D onScreenCoordinate = vertices[0].onScreenCoordinate; + final Point2D onScreenCoordinate = vertices.get(0).onScreenCoordinate; // compute Y final int onScreenUncappedYStart = (int) (onScreenCoordinate.y - visibleVerticalDistanceFromCenter); @@ -219,7 +219,7 @@ public class Billboard extends AbstractCoordinateShape { * @return the center position in world coordinates */ public Point3D getLocation() { - return vertices[0].coordinate; + return vertices.get(0).coordinate; } } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java index 4b53da1..895ca2c 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java @@ -35,11 +35,23 @@ public class Line extends AbstractCoordinateShape { 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; - final LineInterpolator[] lineInterpolators = new LineInterpolator[4]; /** * Color of the line. @@ -52,8 +64,8 @@ public class Line extends AbstractCoordinateShape { * @param parentLine the line to copy */ public Line(final Line parentLine) { - this(parentLine.vertices[0].coordinate.clone(), - parentLine.vertices[1].coordinate.clone(), + this(parentLine.vertices.get(0).coordinate.clone(), + parentLine.vertices.get(1).coordinate.clone(), new Color(parentLine.color), parentLine.width); } @@ -75,10 +87,6 @@ public class Line extends AbstractCoordinateShape { this.color = color; this.width = width; - - for (int i = 0; i < lineInterpolators.length; i++) - lineInterpolators[i] = new LineInterpolator(); - } /** @@ -164,8 +172,8 @@ public class Line extends AbstractCoordinateShape { private void drawSinglePixelHorizontalLine(final RenderingContext buffer, final int alpha) { - final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate; - final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate; + final Point2D onScreenPoint1 = vertices.get(0).onScreenCoordinate; + final Point2D onScreenPoint2 = vertices.get(1).onScreenCoordinate; int xStart = (int) onScreenPoint1.x; int xEnd = (int) onScreenPoint2.x; @@ -232,8 +240,8 @@ public class Line extends AbstractCoordinateShape { private void drawSinglePixelVerticalLine(final RenderingContext buffer, final int alpha) { - final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate; - final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate; + final Point2D onScreenPoint1 = vertices.get(0).onScreenCoordinate; + final Point2D onScreenPoint2 = vertices.get(1).onScreenCoordinate; int yStart = (int) onScreenPoint1.y; int yEnd = (int) onScreenPoint2.y; @@ -292,11 +300,13 @@ public class Line extends AbstractCoordinateShape { /** * Finds the index of the first interpolator (starting from startPointer) that contains the given Y coordinate. * - * @param startPointer the index to start searching from - * @param y the Y coordinate to search for + * @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 int startPointer, final int y) { + 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)) @@ -320,16 +330,16 @@ public class Line extends AbstractCoordinateShape { @Override public void paint(final RenderingContext buffer) { - final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate; - final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate; + final Point2D onScreenPoint1 = vertices.get(0).onScreenCoordinate; + final Point2D onScreenPoint2 = vertices.get(1).onScreenCoordinate; final double xp = onScreenPoint2.x - onScreenPoint1.x; final double yp = onScreenPoint2.y - onScreenPoint1.y; final double point1radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width) - / vertices[0].transformedCoordinate.z; + / vertices.get(0).transformedCoordinate.z; final double point2radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width) - / vertices[1].transformedCoordinate.z; + / vertices.get(1).transformedCoordinate.z; if ((point1radius < MINIMUM_WIDTH_THRESHOLD) || (point2radius < MINIMUM_WIDTH_THRESHOLD)) { @@ -370,6 +380,9 @@ public class Line extends AbstractCoordinateShape { 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); @@ -403,9 +416,9 @@ public class Line extends AbstractCoordinateShape { return; for (int y = (int) ymin; y <= ymax; y++) { - final int li1 = getLineInterpolator(0, y); + final int li1 = getLineInterpolator(lineInterpolators, 0, y); if (li1 != -1) { - final int li2 = getLineInterpolator(li1 + 1, y); + final int li2 = getLineInterpolator(lineInterpolators, li1 + 1, y); if (li2 != -1) drawHorizontalLine(lineInterpolators[li1], lineInterpolators[li2], y, buffer); } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java index fa14d14..629d16e 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/LineInterpolator.java @@ -129,4 +129,24 @@ public class LineInterpolator implements Comparable { absoluteHeight = Math.abs(height); } + /** + * Sets the two endpoints of this edge using integer coordinates. + * + *

This method creates new Point2D objects to avoid storing references to shared + * vertex data, which is essential for thread safety during parallel rendering.

+ * + * @param x1 the x coordinate of the first endpoint + * @param y1 the y coordinate of the first endpoint + * @param x2 the x coordinate of the second endpoint + * @param y2 the y coordinate of the second endpoint + */ + public void setPoints(final int x1, final int y1, final int x2, final int y2) { + this.p1 = new Point2D(x1, y1); + this.p2 = new Point2D(x2, y2); + height = y2 - y1; + width = x2 - x1; + + absoluteHeight = Math.abs(height); + } + } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java index 9d03c8f..fd54e21 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java @@ -4,6 +4,7 @@ */ package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon; +import eu.svjatoslav.sixth.e3d.csg.CSGPlane; import eu.svjatoslav.sixth.e3d.geometry.Point2D; import eu.svjatoslav.sixth.e3d.geometry.Point3D; import eu.svjatoslav.sixth.e3d.gui.RenderingContext; @@ -12,52 +13,375 @@ import eu.svjatoslav.sixth.e3d.math.Vertex; import eu.svjatoslav.sixth.e3d.renderer.raster.Color; import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon; /** - * A solid-color triangle renderer with mouse interaction support. - *

- * This class implements a high-performance triangle rasterizer using scanline - * algorithms. It handles: - * - Perspective-correct edge interpolation - * - Alpha blending with background pixels - * - Viewport clipping - * - Mouse hover detection via point-in-polygon tests - * - Optional flat shading based on light sources - *

- * The static drawPolygon method is designed for reuse by other polygon types. + * 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 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 CSGPlane 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() }); - private final Point3D cachedNormal = new Point3D(); - private final Point3D cachedCenter = new Point3D(); + /** + * Cached plane containing this polygon, used for CSG operations. + * + *

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

+ */ + private CSGPlane plane; + + /** + * Flag indicating whether the plane has been computed. + */ + private boolean planeComputed = false; + + /** + * Thread-local cached normal vector for shading calculations. + * Each rendering thread gets its own instance to avoid race conditions. + */ + private static final ThreadLocal CACHED_NORMAL = + ThreadLocal.withInitial(Point3D::new); + + /** + * Thread-local cached centroid for lighting calculations. + * Each rendering thread gets its own instance to avoid race conditions. + */ + private static final ThreadLocal CACHED_CENTER = + ThreadLocal.withInitial(Point3D::new); + + /** + * 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<>(); + + /** + * 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; + } + + /** + * Creates a solid polygon from a vertex list and color. + * + *

This constructor is used for CSG operations where vertices already exist.

+ * + * @param vertices the list of Vertex objects (will be used directly, not copied) + * @param color the fill color of the polygon + * @param dummy dummy parameter to distinguish from List<Point3D> constructor + * @throws IllegalArgumentException if vertices is null or has fewer than 3 vertices + */ + public SolidPolygon(final List vertices, final Color color, final boolean dummy) { + super(vertices); + if (vertices == null || vertices.size() < 3) { + throw new IllegalArgumentException( + "Polygon must have at least 3 vertices, but got " + + (vertices == null ? "null" : vertices.size())); + } + 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 of the triangle + * @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) - ); + super(new Vertex(point1), new Vertex(point2), new Vertex(point3)); this.color = color; } + // ==================== 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; + } + + /** + * Returns the number of vertices in this polygon. + * + * @return the vertex count + */ + public int getVertexCount() { + return vertices.size(); + } + + // ==================== PROPERTIES ==================== + + /** + * 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; + } + + /** + * 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; + } + + // ==================== CSG SUPPORT ==================== + + /** + * Returns the plane containing this polygon. + * + *

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

+ * + * @return the CSGPlane containing this polygon + */ + public CSGPlane getPlane() { + if (!planeComputed) { + plane = CSGPlane.fromPoints( + vertices.get(0).coordinate, + vertices.get(1).coordinate, + vertices.get(2).coordinate + ); + planeComputed = true; + } + return plane; + } + + /** + * 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 v : vertices) { + v.flip(); + } + if (planeComputed) { + plane.flip(); + } + } + + /** + * Creates a deep clone of this polygon. + * + *

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

+ * + * @return a new SolidPolygon with cloned data + */ + public SolidPolygon deepClone() { + final List clonedVertices = new ArrayList<>(vertices.size()); + for (final Vertex v : vertices) { + clonedVertices.add(v.clone()); + } + return new SolidPolygon(clonedVertices, color, true); + } + + // ==================== RENDERING ==================== + /** * Draws a horizontal scanline between two edge interpolators with alpha blending. * @@ -68,8 +392,8 @@ public class SolidPolygon extends AbstractCoordinateShape { * @param color the color to draw with */ public static void drawHorizontalLine(final LineInterpolator line1, - final LineInterpolator line2, final int y, - final RenderingContext renderBuffer, final Color color) { + final LineInterpolator line2, final int y, + final RenderingContext renderBuffer, final Color color) { int x1 = line1.getX(y); int x2 = line2.getX(y); @@ -80,11 +404,13 @@ public class SolidPolygon extends AbstractCoordinateShape { x2 = tmp; } - if (x1 < 0) + if (x1 < 0) { x1 = 0; + } - if (x2 >= renderBuffer.width) + if (x2 >= renderBuffer.width) { x2 = renderBuffer.width - 1; + } final int width = x2 - x1; @@ -121,16 +447,15 @@ public class SolidPolygon extends AbstractCoordinateShape { pixels[offset++] = (newR << 16) | (newG << 8) | newB; } } - } /** - * Renders a triangle with mouse interaction support and optional backface culling. + * Renders a triangle using scanline rasterization. * *

This static method handles:

*
    *
  • Rounding vertices to integer screen coordinates
  • - *
  • Mouse hover detection via point-in-polygon test
  • + *
  • Mouse hover detection via point-in-triangle test
  • *
  • Viewport clipping
  • *
  • Scanline rasterization with alpha blending
  • *
@@ -142,64 +467,76 @@ public class SolidPolygon extends AbstractCoordinateShape { * @param mouseInteractionController optional controller for mouse events, or null * @param color the fill color */ - public static void drawPolygon(final RenderingContext context, - final Point2D onScreenPoint1, final Point2D onScreenPoint2, - final Point2D onScreenPoint3, - final MouseInteractionController mouseInteractionController, - final Color color) { - - onScreenPoint1.roundToInteger(); - onScreenPoint2.roundToInteger(); - onScreenPoint3.roundToInteger(); - - if (mouseInteractionController != null) - if (context.getMouseEvent() != null) + public static void drawTriangle(final RenderingContext context, + final Point2D onScreenPoint1, final Point2D onScreenPoint2, + final Point2D onScreenPoint3, + final MouseInteractionController mouseInteractionController, + final Color color) { + + // Copy and round coordinates to local variables (don't modify original Point2D) + // This is thread-safe: multiple threads may paint the same polygon across different + // Y segments, so we must not mutate shared vertex data + final int x1 = (int) onScreenPoint1.x; + final int y1 = (int) onScreenPoint1.y; + final int x2 = (int) onScreenPoint2.x; + final int y2 = (int) onScreenPoint2.y; + final int x3 = (int) onScreenPoint3.x; + final int y3 = (int) onScreenPoint3.y; + + if (mouseInteractionController != null) { + if (context.getMouseEvent() != null) { if (pointWithinPolygon(context.getMouseEvent().coordinate, - onScreenPoint1, onScreenPoint2, onScreenPoint3)) + x1, y1, x2, y2, x3, y3)) { context.setCurrentObjectUnderMouseCursor(mouseInteractionController); + } + } + } - if (color.isTransparent()) + if (color.isTransparent()) { return; + } - // find top-most point - int yTop = (int) onScreenPoint1.y; - - if (onScreenPoint2.y < yTop) - yTop = (int) onScreenPoint2.y; - - if (onScreenPoint3.y < yTop) - yTop = (int) onScreenPoint3.y; - - if (yTop < 0) + // Find top-most point + int yTop = y1; + if (y2 < yTop) { + yTop = y2; + } + if (y3 < yTop) { + yTop = y3; + } + if (yTop < 0) { yTop = 0; + } - // find bottom-most point - int yBottom = (int) onScreenPoint1.y; - - if (onScreenPoint2.y > yBottom) - yBottom = (int) onScreenPoint2.y; - - if (onScreenPoint3.y > yBottom) - yBottom = (int) onScreenPoint3.y; - - if (yBottom >= context.height) + // Find bottom-most point + int yBottom = y1; + if (y2 > yBottom) { + yBottom = y2; + } + if (y3 > yBottom) { + yBottom = y3; + } + if (yBottom >= context.height) { yBottom = context.height - 1; + } - // clamp to render Y bounds + // Clamp to render Y bounds yTop = Math.max(yTop, context.renderMinY); yBottom = Math.min(yBottom, context.renderMaxY); - if (yTop >= yBottom) + if (yTop >= yBottom) { return; + } - // paint + // Paint using line interpolators final LineInterpolator[] interp = INTERPOLATORS.get(); final LineInterpolator polygonBoundary1 = interp[0]; final LineInterpolator polygonBoundary2 = interp[1]; final LineInterpolator polygonBoundary3 = interp[2]; - polygonBoundary1.setPoints(onScreenPoint1, onScreenPoint2); - polygonBoundary2.setPoints(onScreenPoint1, onScreenPoint3); - polygonBoundary3.setPoints(onScreenPoint2, onScreenPoint3); + // Use rounded integer coordinates for interpolation + polygonBoundary1.setPoints(x1, y1, x2, y2); + polygonBoundary2.setPoints(x1, y1, x3, y3); + polygonBoundary3.setPoints(x2, y2, x3, y3); // Inline sort for 3 elements to avoid array allocation LineInterpolator a = polygonBoundary1; @@ -222,93 +559,43 @@ public class SolidPolygon extends AbstractCoordinateShape { b = t; } - for (int y = yTop; y < yBottom; y++) + for (int y = yTop; y < yBottom; y++) { if (a.containsY(y)) { - if (b.containsY(y)) + if (b.containsY(y)) { drawHorizontalLine(a, b, y, context, color); - else if (c.containsY(y)) + } else if (c.containsY(y)) { drawHorizontalLine(a, c, y, context, color); - } else if (b.containsY(y)) - if (c.containsY(y)) + } + } else if (b.containsY(y)) { + if (c.containsY(y)) { drawHorizontalLine(b, c, y, context, color); + } + } + } } /** - * 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. - * When enabled, the polygon uses the global lighting manager from the - * rendering context to calculate flat shading based on light sources. - * - * @param shadingEnabled true to enable shading, false to disable - */ - public void setShadingEnabled(final boolean shadingEnabled) { - this.shadingEnabled = shadingEnabled; - } - - /** - * 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. - * - *

When enabled, polygons facing away from the camera (determined by - * screen-space winding order) are not rendered.

- * - * @param backfaceCulling {@code true} to enable backface culling - */ - public void setBackfaceCulling(final boolean backfaceCulling) { - this.backfaceCulling = backfaceCulling; - } - - /** - * Calculates the unit normal vector of this triangle. + * Calculates the unit normal vector of this polygon. * * @param result the point to store the normal vector in */ private void calculateNormal(final Point3D result) { - final Point3D v1 = vertices[0].coordinate; - final Point3D v2 = vertices[1].coordinate; - final Point3D v3 = vertices[2].coordinate; + if (vertices.size() < 3) { + result.x = result.y = result.z = 0; + return; + } - final double ax = v2.x - v1.x; - final double ay = v2.y - v1.y; - final double az = v2.z - v1.z; + final Point3D v0 = vertices.get(0).coordinate; + final Point3D v1 = vertices.get(1).coordinate; + final Point3D v2 = vertices.get(2).coordinate; - final double bx = v3.x - v1.x; - final double by = v3.y - v1.y; - final double bz = v3.z - v1.z; + final double ax = v1.x - v0.x; + final double ay = v1.y - v0.y; + final double az = v1.z - v0.z; + + final double bx = v2.x - v0.x; + final double by = v2.y - v0.y; + final double bz = v2.z - v0.z; double nx = ay * bz - az * by; double ny = az * bx - ax * bz; @@ -327,59 +614,169 @@ public class SolidPolygon extends AbstractCoordinateShape { } /** - * Calculates the centroid (geometric center) of this triangle. + * Calculates the centroid (geometric center) of this polygon. * * @param result the point to store the center in */ private void calculateCenter(final Point3D result) { - final Point3D v1 = vertices[0].coordinate; - final Point3D v2 = vertices[1].coordinate; - final Point3D v3 = vertices[2].coordinate; + 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 = (v1.x + v2.x + v3.x) / 3.0; - result.y = (v1.y + v2.y + v3.y) / 3.0; - result.z = (v1.z + v2.z + v3.z) / 3.0; + result.x = sumX / vertices.size(); + result.y = sumY / vertices.size(); + result.z = sumZ / vertices.size(); } /** - * Renders this triangle to the screen. + * Calculates the signed area of this polygon in screen space. * - *

This method performs:

- *
    - *
  • Backface culling check (if enabled)
  • - *
  • Flat shading calculation (if lighting is enabled)
  • - *
  • Triangle rasterization using the static drawPolygon method
  • - *
+ * @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) { + if (vertices.size() < 3 || color.isTransparent()) { + return; + } - final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate; - final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate; - final Point2D onScreenPoint3 = vertices[2].onScreenCoordinate; + // Get thread-local screen points array + final Point2D[] screenPoints = getScreenPoints(vertices.size()); + // Get screen coordinates + for (int i = 0; i < vertices.size(); i++) { + screenPoints[i] = vertices.get(i).onScreenCoordinate; + } + + // Backface culling check if (backfaceCulling) { - final double signedArea = (onScreenPoint2.x - onScreenPoint1.x) - * (onScreenPoint3.y - onScreenPoint1.y) - - (onScreenPoint3.x - onScreenPoint1.x) - * (onScreenPoint2.y - onScreenPoint1.y); - if (signedArea >= 0) + final double signedArea = calculateSignedArea(screenPoints, vertices.size()); + if (signedArea >= 0) { return; + } } + // Determine paint color (with optional shading) Color paintColor = color; - if (shadingEnabled && renderBuffer.lightingManager != null) { + final Point3D cachedCenter = CACHED_CENTER.get(); + final Point3D cachedNormal = CACHED_NORMAL.get(); calculateCenter(cachedCenter); calculateNormal(cachedNormal); paintColor = renderBuffer.lightingManager.calculateLighting(cachedCenter, cachedNormal, color); } - drawPolygon(renderBuffer, onScreenPoint1, onScreenPoint2, - onScreenPoint3, mouseInteractionController, paintColor); + // Mouse interaction + if (mouseInteractionController != null && renderBuffer.getMouseEvent() != null) { + if (isPointInsidePolygon(renderBuffer.getMouseEvent().coordinate, screenPoints, vertices.size())) { + renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController); + } + } + + // For triangles, use direct triangle rendering + if (vertices.size() == 3) { + drawTriangle(renderBuffer, screenPoints[0], screenPoints[1], screenPoints[2], + mouseInteractionController, paintColor); + return; + } + + // Fan triangulation for N-vertex polygons + final Point2D v0 = screenPoints[0]; + for (int i = 1; i < vertices.size() - 1; i++) { + final Point2D v1 = screenPoints[i]; + final Point2D v2 = screenPoints[i + 1]; + drawTriangle(renderBuffer, v0, v1, v2, null, paintColor); + } } -} + /** + * 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; + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java index 79b79d5..71683a5 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/package-info.java @@ -4,15 +4,15 @@ */ /** - * Solid-color triangle rendering with scanline rasterization. + * Solid-color polygon rendering with scanline rasterization. * - *

Solid polygons are the primary building blocks for opaque 3D surfaces. - * The rasterizer handles perspective-correct interpolation, alpha blending, - * viewport clipping, and optional flat shading.

+ *

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.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon} - The solid triangle shape
  • + *
  • {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon} - Unified polygon for rendering and CSG
  • *
  • {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.LineInterpolator} - Edge interpolation for scanlines
  • *
* diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java index ec4caeb..3081b27 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/PolygonBorderInterpolator.java @@ -139,6 +139,10 @@ public class PolygonBorderInterpolator implements /** * Sets the screen and texture coordinates for this edge. * + *

Screen coordinates are copied to new Point2D objects to avoid + * storing references to shared vertex data, which is essential for + * thread safety during parallel rendering.

+ * * @param onScreenPoint1 the first screen-space endpoint * @param onScreenPoint2 the second screen-space endpoint * @param texturePoint1 the texture coordinate for the first endpoint @@ -147,13 +151,14 @@ public class PolygonBorderInterpolator implements public void setPoints(final Point2D onScreenPoint1, final Point2D onScreenPoint2, final Point2D texturePoint1, final Point2D texturePoint2) { - this.onScreenPoint1 = onScreenPoint1; - this.onScreenPoint2 = onScreenPoint2; + // Copy screen coordinates to avoid race conditions with shared vertex data + this.onScreenPoint1 = new Point2D(onScreenPoint1.x, onScreenPoint1.y); + this.onScreenPoint2 = new Point2D(onScreenPoint2.x, onScreenPoint2.y); this.texturePoint1 = texturePoint1; this.texturePoint2 = texturePoint2; - onScreenHeight = (int) (onScreenPoint2.y - onScreenPoint1.y); - onScreenWidth = (int) (onScreenPoint2.x - onScreenPoint1.x); + onScreenHeight = (int) (this.onScreenPoint2.y - this.onScreenPoint1.y); + onScreenWidth = (int) (this.onScreenPoint2.x - this.onScreenPoint1.x); onscreenAbsoluteHeight = abs(onScreenHeight); textureWidth = texturePoint2.x - texturePoint1.x; diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java similarity index 78% rename from src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java rename to src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java index b5ba635..6a015b5 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java @@ -23,14 +23,14 @@ import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon; * *

Perspective-correct texture rendering:

*
    - *
  • Small polygons are rendered without perspective correction
  • - *
  • Larger polygons are sliced into smaller pieces for accurate perspective
  • + *
  • Small triangles are rendered without perspective correction
  • + *
  • Larger triangles are sliced into smaller pieces for accurate perspective
  • *
* * @see Texture * @see Vertex#textureCoordinate */ -public class TexturedPolygon extends AbstractCoordinateShape { +public class TexturedTriangle extends AbstractCoordinateShape { private static final ThreadLocal INTERPOLATORS = ThreadLocal.withInitial(() -> new PolygonBorderInterpolator[]{ @@ -38,13 +38,17 @@ public class TexturedPolygon extends AbstractCoordinateShape { }); /** - * The texture to apply to this polygon. + * The texture to apply to this triangle. */ public final Texture texture; private boolean backfaceCulling = false; - private double totalTextureDistance = -1; + /** + * 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. @@ -54,10 +58,11 @@ public class TexturedPolygon extends AbstractCoordinateShape { * @param p3 the third vertex (must have textureCoordinate set) * @param texture the texture to apply */ - public TexturedPolygon(Vertex p1, Vertex p2, Vertex p3, final Texture texture) { + public TexturedTriangle(Vertex p1, Vertex p2, Vertex p3, final Texture texture) { super(p1, p2, p3); this.texture = texture; + computeTotalTextureDistance(); } /** @@ -65,10 +70,9 @@ public class TexturedPolygon extends AbstractCoordinateShape { * Used to determine appropriate mipmap level. */ private void computeTotalTextureDistance() { - // compute total texture distance - totalTextureDistance = vertices[0].textureCoordinate.getDistanceTo(vertices[1].textureCoordinate); - totalTextureDistance += vertices[0].textureCoordinate.getDistanceTo(vertices[2].textureCoordinate); - totalTextureDistance += vertices[1].textureCoordinate.getDistanceTo(vertices[2].textureCoordinate); + 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); } /** @@ -201,9 +205,9 @@ public class TexturedPolygon extends AbstractCoordinateShape { @Override public void paint(final RenderingContext renderBuffer) { - final Point2D projectedPoint1 = vertices[0].onScreenCoordinate; - final Point2D projectedPoint2 = vertices[1].onScreenCoordinate; - final Point2D projectedPoint3 = vertices[2].onScreenCoordinate; + final Point2D projectedPoint1 = vertices.get(0).onScreenCoordinate; + final Point2D projectedPoint2 = vertices.get(1).onScreenCoordinate; + final Point2D projectedPoint3 = vertices.get(2).onScreenCoordinate; if (backfaceCulling) { final double signedArea = (projectedPoint2.x - projectedPoint1.x) @@ -214,15 +218,20 @@ public class TexturedPolygon extends AbstractCoordinateShape { return; } - projectedPoint1.roundToInteger(); - projectedPoint2.roundToInteger(); - projectedPoint3.roundToInteger(); + // Copy and round coordinates to local variables (don't modify original Point2D) + // This is thread-safe: multiple threads may paint the same polygon across different + // Y segments, so we must not mutate shared vertex data + 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; if (mouseInteractionController != null) if (renderBuffer.getMouseEvent() != null) if (pointWithinPolygon( - renderBuffer.getMouseEvent().coordinate, projectedPoint1, - projectedPoint2, projectedPoint3)) + renderBuffer.getMouseEvent().coordinate, x1, y1, x2, y2, x3, y3)) renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController); // Show polygon boundaries (for debugging) @@ -230,25 +239,25 @@ public class TexturedPolygon extends AbstractCoordinateShape { showBorders(renderBuffer); // find top-most point - int yTop = (int) projectedPoint1.y; + int yTop = y1; - if (projectedPoint2.y < yTop) - yTop = (int) projectedPoint2.y; + if (y2 < yTop) + yTop = y2; - if (projectedPoint3.y < yTop) - yTop = (int) projectedPoint3.y; + if (y3 < yTop) + yTop = y3; if (yTop < 0) yTop = 0; // find bottom-most point - int yBottom = (int) projectedPoint1.y; + int yBottom = y1; - if (projectedPoint2.y > yBottom) - yBottom = (int) projectedPoint2.y; + if (y2 > yBottom) + yBottom = y2; - if (projectedPoint3.y > yBottom) - yBottom = (int) projectedPoint3.y; + if (y3 > yBottom) + yBottom = y3; if (yBottom >= renderBuffer.height) yBottom = renderBuffer.height - 1; @@ -264,8 +273,6 @@ public class TexturedPolygon extends AbstractCoordinateShape { totalVisibleDistance += projectedPoint1.getDistanceTo(projectedPoint3); totalVisibleDistance += projectedPoint2.getDistanceTo(projectedPoint3); - if (totalTextureDistance == -1) - computeTotalTextureDistance(); final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d; final TextureBitmap zoomedBitmap = texture.getZoomedBitmap(scaleFactor); @@ -275,15 +282,16 @@ public class TexturedPolygon extends AbstractCoordinateShape { final PolygonBorderInterpolator polygonBorder2 = interp[1]; final PolygonBorderInterpolator polygonBorder3 = interp[2]; - polygonBorder1.setPoints(projectedPoint1, projectedPoint2, - vertices[0].textureCoordinate, - vertices[1].textureCoordinate); - polygonBorder2.setPoints(projectedPoint1, projectedPoint3, - vertices[0].textureCoordinate, - vertices[2].textureCoordinate); - polygonBorder3.setPoints(projectedPoint2, projectedPoint3, - vertices[1].textureCoordinate, - vertices[2].textureCoordinate); + // Use rounded integer coordinates for screen positions + polygonBorder1.setPoints(new Point2D(x1, y1), new Point2D(x2, y2), + vertices.get(0).textureCoordinate, + vertices.get(1).textureCoordinate); + polygonBorder2.setPoints(new Point2D(x1, y1), new Point2D(x3, y3), + vertices.get(0).textureCoordinate, + vertices.get(2).textureCoordinate); + polygonBorder3.setPoints(new Point2D(x2, y2), new Point2D(x3, y3), + vertices.get(1).textureCoordinate, + vertices.get(2).textureCoordinate); // Inline sort for 3 elements to avoid array allocation PolygonBorderInterpolator a = polygonBorder1; @@ -319,7 +327,7 @@ public class TexturedPolygon extends AbstractCoordinateShape { } /** - * Checks if backface culling is enabled for this polygon. + * Checks if backface culling is enabled for this triangle. * * @return {@code true} if backface culling is enabled */ @@ -328,7 +336,7 @@ public class TexturedPolygon extends AbstractCoordinateShape { } /** - * Enables or disables backface culling for this polygon. + * Enables or disables backface culling for this triangle. * * @param backfaceCulling {@code true} to enable backface culling */ @@ -337,15 +345,15 @@ public class TexturedPolygon extends AbstractCoordinateShape { } /** - * Draws the polygon border edges in yellow (for debugging). + * Draws the triangle border edges in yellow (for debugging). * * @param renderBuffer the rendering context */ private void showBorders(final RenderingContext renderBuffer) { - final Point2D projectedPoint1 = vertices[0].onScreenCoordinate; - final Point2D projectedPoint2 = vertices[1].onScreenCoordinate; - final Point2D projectedPoint3 = vertices[2].onScreenCoordinate; + final Point2D projectedPoint1 = vertices.get(0).onScreenCoordinate; + final Point2D projectedPoint2 = vertices.get(1).onScreenCoordinate; + final Point2D projectedPoint3 = vertices.get(2).onScreenCoordinate; final int x1 = (int) projectedPoint1.x; final int y1 = (int) projectedPoint1.y; diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java index f893beb..44489af 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/package-info.java @@ -6,16 +6,16 @@ /** * Textured triangle rendering with perspective-correct UV mapping. * - *

Textured polygons apply 2D textures to 3D triangles using UV coordinates. - * Large polygons may be sliced into smaller pieces for accurate perspective correction.

+ *

Textured triangles apply 2D textures to 3D triangles using UV coordinates. + * Large triangles may be sliced into smaller pieces for accurate perspective correction.

* *

Key classes:

*
    - *
  • {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon} - The textured triangle shape
  • + *
  • {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle} - The textured triangle shape
  • *
  • {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.PolygonBorderInterpolator} - Edge interpolation with UVs
  • *
* - * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle * @see eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture */ diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java index c7f71d7..7cf643b 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/Graph.java @@ -103,7 +103,7 @@ public class Graph extends AbstractCompositeShape { plotData(scale, data); final Point3D labelLocation = new Point3D(width / 2, yMax + 0.5, 0) - .scaleUp(scale); + .multiply(scale); final TextCanvas labelCanvas = new TextCanvas(new Transform( labelLocation), label, Color.WHITE, Color.TRANSPARENT); @@ -114,16 +114,16 @@ public class Graph extends AbstractCompositeShape { private void addHorizontalLinesAndLabels(final double scale) { for (double y = yMin; y <= yMax; y += verticalStep) { - final Point3D p1 = new Point3D(0, y, 0).scaleUp(scale); + final Point3D p1 = new Point3D(0, y, 0).multiply(scale); - final Point3D p2 = new Point3D(width, y, 0).scaleUp(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) - .scaleUp(scale); + .multiply(scale); final TextCanvas label = new TextCanvas( new Transform(labelLocation), String.valueOf(y), @@ -137,8 +137,8 @@ public class Graph extends AbstractCompositeShape { private void addVerticalLines(final double scale) { for (double x = 0; x <= width; x += horizontalStep) { - final Point3D p1 = new Point3D(x, yMin, 0).scaleUp(scale); - final Point3D p2 = new Point3D(x, yMax, 0).scaleUp(scale); + 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); @@ -150,7 +150,7 @@ public class Graph extends AbstractCompositeShape { 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) - .scaleUp(scale); + .multiply(scale); final TextCanvas label = new TextCanvas( new Transform(labelLocation), String.valueOf(x), @@ -164,7 +164,7 @@ public class Graph extends AbstractCompositeShape { Point3D previousPoint = null; for (final Point2D point : data) { - final Point3D p3d = new Point3D(point.x, point.y, 0).scaleUp(scale); + final Point3D p3d = new Point3D(point.x, point.y, 0).multiply(scale); if (previousPoint != null) { diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java index aa52272..f1efcaa 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/TexturedRectangle.java @@ -8,7 +8,7 @@ import eu.svjatoslav.sixth.e3d.geometry.Point2D; import eu.svjatoslav.sixth.e3d.geometry.Point3D; import eu.svjatoslav.sixth.e3d.math.Transform; import eu.svjatoslav.sixth.e3d.math.Vertex; -import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle; import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture; @@ -16,7 +16,7 @@ import eu.svjatoslav.sixth.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 TexturedPolygon} triangles that share a common {@link Texture}. The rectangle + * 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.

* @@ -39,7 +39,7 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture; * shapeCollection.addShape(rect); * }
* - * @see TexturedPolygon + * @see TexturedTriangle * @see Texture * @see AbstractCompositeShape */ @@ -129,7 +129,7 @@ public class TexturedRectangle extends AbstractCompositeShape { * *

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 TexturedPolygon} triangles are created to cover the full rectangle, + * 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 @@ -157,7 +157,7 @@ public class TexturedRectangle extends AbstractCompositeShape { - final TexturedPolygon texturedPolygon1 = new TexturedPolygon( + final TexturedTriangle texturedPolygon1 = new TexturedTriangle( new Vertex(topLeft, textureTopLeft), new Vertex(topRight, textureTopRight), new Vertex(bottomRight, textureBottomRight), texture); @@ -165,7 +165,7 @@ public class TexturedRectangle extends AbstractCompositeShape { texturedPolygon1 .setMouseInteractionController(mouseInteractionController); - final TexturedPolygon texturedPolygon2 = new TexturedPolygon( + final TexturedTriangle texturedPolygon2 = new TexturedTriangle( new Vertex(topLeft, textureTopLeft), new Vertex(bottomLeft, textureBottomLeft), new Vertex(bottomRight, textureBottomRight), texture); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java index d9bfe23..6752ce5 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java @@ -4,6 +4,7 @@ */ package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base; +import eu.svjatoslav.sixth.e3d.csg.CSGNode; import eu.svjatoslav.sixth.e3d.geometry.Point3D; import eu.svjatoslav.sixth.e3d.gui.RenderingContext; import eu.svjatoslav.sixth.e3d.gui.ViewSpaceTracker; @@ -15,10 +16,11 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.RenderAggregator; import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractShape; import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.Line; import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; -import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle; import eu.svjatoslav.sixth.e3d.renderer.raster.slicer.Slicer; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; /** @@ -184,22 +186,13 @@ public class AbstractCompositeShape extends AbstractShape { } /** - * Extracts all SolidPolygon triangles from this composite shape. + * Extracts all SolidPolygon instances from this composite shape. * *

Recursively traverses the shape hierarchy and collects all - * {@link SolidPolygon} instances. Useful for CSG operations where - * you need the raw triangles from a composite shape like - * {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCube} - * or {@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonSphere}.

+ * SolidPolygon instances. Used for CSG operations where polygons + * are needed directly without conversion.

* - *

Example:

- *
{@code
-     * SolidPolygonCube cube = new SolidPolygonCube(new Point3D(0, 0, 0), 50, Color.RED);
-     * List triangles = cube.extractSolidPolygons();
-     * CSG csg = CSG.fromSolidPolygons(triangles);
-     * }
- * - * @return list of all SolidPolygon sub-shapes + * @return list of SolidPolygon instances from this shape hierarchy */ public List extractSolidPolygons() { final List result = new ArrayList<>(); @@ -362,18 +355,26 @@ public class AbstractCompositeShape extends AbstractShape { * * @param transform the new transform to apply */ - public void setTransform(final Transform 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; } /** - * Enables or disables shading for all SolidPolygon sub-shapes. - * When enabled, polygons use the global lighting manager from the rendering + * 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. * * @param shadingEnabled {@code true} to enable shading, {@code false} to disable + * @return this composite shape (for chaining) */ - public void setShadingEnabled(final boolean shadingEnabled) { + public AbstractCompositeShape setShadingEnabled(final boolean shadingEnabled) { for (final SubShape subShape : getOriginalSubShapes()) { final AbstractShape shape = subShape.getShape(); if (shape instanceof SolidPolygon) { @@ -382,22 +383,199 @@ public class AbstractCompositeShape extends AbstractShape { // TODO: if shape is abstract composite, it seems that it would be good to enabled sharding recursively there too } + return this; } /** - * Enables or disables backface culling for all SolidPolygon and TexturedPolygon sub-shapes. + * Enables or disables backface culling for all SolidPolygon and TexturedTriangle sub-shapes. * * @param backfaceCulling {@code true} to enable backface culling, {@code false} to disable + * @return this composite shape (for chaining) */ - public void setBackfaceCulling(final boolean backfaceCulling) { + public AbstractCompositeShape setBackfaceCulling(final boolean backfaceCulling) { for (final SubShape subShape : getOriginalSubShapes()) { final AbstractShape shape = subShape.getShape(); if (shape instanceof SolidPolygon) { ((SolidPolygon) shape).setBackfaceCulling(backfaceCulling); - } else if (shape instanceof TexturedPolygon) { - ((TexturedPolygon) shape).setBackfaceCulling(backfaceCulling); + } else if (shape instanceof TexturedTriangle) { + ((TexturedTriangle) 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) { + final List selfPolygons = clonePolygons(extractSolidPolygons()); + final List otherPolygons = clonePolygons(other.extractSolidPolygons()); + + final CSGNode a = new CSGNode(selfPolygons); + final CSGNode b = new CSGNode(otherPolygons); + + a.clipTo(b); + b.clipTo(a); + b.invert(); + b.clipTo(a); + b.invert(); + a.build(b.allPolygons()); + + replaceSolidPolygons(a.allPolygons(), other, true); + } + + /** + * 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) { + final List selfPolygons = clonePolygons(extractSolidPolygons()); + final List otherPolygons = clonePolygons(other.extractSolidPolygons()); + + final CSGNode a = new CSGNode(selfPolygons); + final CSGNode b = new CSGNode(otherPolygons); + + a.invert(); + a.clipTo(b); + b.clipTo(a); + b.invert(); + b.clipTo(a); + b.invert(); + a.build(b.allPolygons()); + a.invert(); + + replaceSolidPolygons(a.allPolygons(), other, false); + } + + /** + * 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) { + final List selfPolygons = clonePolygons(extractSolidPolygons()); + final List otherPolygons = clonePolygons(other.extractSolidPolygons()); + + final CSGNode a = new CSGNode(selfPolygons); + final CSGNode b = new CSGNode(otherPolygons); + + a.invert(); + b.clipTo(a); + b.invert(); + a.clipTo(b); + b.clipTo(a); + a.build(b.allPolygons()); + a.invert(); + + replaceSolidPolygons(a.allPolygons(), other, false); + } + + /** + * Creates deep clones of all polygons in the list. + * + *

CSG operations modify polygons in-place via BSP tree operations. + * Cloning ensures the original polygon data is preserved.

+ * + * @param polygons the polygons to clone + * @return a new list containing deep clones of all polygons + */ + private List clonePolygons(final List polygons) { + final List cloned = new ArrayList<>(polygons.size()); + for (final SolidPolygon p : polygons) { + cloned.add(p.deepClone()); + } + return cloned; + } + + /** + * Replaces this shape's SolidPolygon children with new polygons. + * + *

Preserves all non-SolidPolygon children (Lines, nested composites, etc.). + * Optionally carries over non-SolidPolygon children from another shape.

+ * + * @param newPolygons the polygons to replace with + * @param other the other shape (may be null) + * @param carryOtherNonPolygons whether to add other's non-SolidPolygon children to this shape + */ + private void replaceSolidPolygons(final List newPolygons, + final AbstractCompositeShape other, + final boolean carryOtherNonPolygons) { + // Remove all direct SolidPolygon children from this shape + final Iterator iterator = originalSubShapes.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); + } + + // Optionally carry over non-SolidPolygon children from other shape + if (carryOtherNonPolygons && other != null) { + for (final SubShape otherSubShape : other.originalSubShapes) { + final AbstractShape otherShape = otherSubShape.getShape(); + if (!(otherShape instanceof SolidPolygon)) { + addShape(otherShape, otherSubShape.getGroupIdentifier()); + } + } + } + + slicingOutdated = true; } /** @@ -434,8 +612,8 @@ public class AbstractCompositeShape extends AbstractShape { for (int i = 0; i < originalSubShapes.size(); i++) { final SubShape subShape = originalSubShapes.get(i); if (subShape.isVisible()) { - if (subShape.getShape() instanceof TexturedPolygon) { - slicer.slice((TexturedPolygon) subShape.getShape()); + if (subShape.getShape() instanceof TexturedTriangle) { + slicer.slice((TexturedTriangle) subShape.getShape()); texturedPolygonCount++; } else { result.add(subShape.getShape()); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java index b139a2b..b3bfc81 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/SubShape.java @@ -81,6 +81,15 @@ public class SubShape { 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. * diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java index d8a9236..29c2c4e 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java @@ -216,37 +216,33 @@ public class SolidPolygonArrow extends AbstractCompositeShape { startSideRing[i] = startSideLocal; } - // Create cylinder side faces (two triangles per segment) - // Winding: tipSide → startSide → tipSide+next, then tipSide+next → startSide → startSide+next - // This creates CCW winding when viewed from outside the cylinder + // 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(new SolidPolygon( - new Point3D(tipSideRing[i].x, tipSideRing[i].y, tipSideRing[i].z), - new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z), - new Point3D(tipSideRing[next].x, tipSideRing[next].y, tipSideRing[next].z), - color)); - - addShape(new SolidPolygon( - new Point3D(tipSideRing[next].x, tipSideRing[next].y, tipSideRing[next].z), - new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z), - new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z), + 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 → next → current creates CCW winding when viewed from behind. - // (Ring vertices are ordered CCW when viewed from the tip; reversing gives CCW from behind) + // 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++) { - final int next = (i + 1) % segments; - addShape(new SolidPolygon( - new Point3D(startPoint.x, startPoint.y, startPoint.z), - new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z), - new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z), - color)); + backCapVertices[i + 1] = startSideRing[segments - 1 - i]; } + backCapVertices[segments + 1] = startSideRing[segments - 1]; // close the loop + addShape(new SolidPolygon(backCapVertices, color)); } /** @@ -312,15 +308,17 @@ public class SolidPolygonArrow extends AbstractCompositeShape { } // 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 → next → current gives CCW when viewed from the body side. + // 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++) { - final int next = (i + 1) % segments; - addShape(new SolidPolygon( - new Point3D(baseCenterX, baseCenterY, baseCenterZ), - new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z), - new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z), - color)); + 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/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java index 05ceafc..3a4327f 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCone.java @@ -139,17 +139,17 @@ public class SolidPolygonCone extends AbstractCompositeShape { } // 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 → current → next creates CCW winding when viewed from - // outside (away from apex). + // 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++) { - final int next = (i + 1) % segments; - addShape(new SolidPolygon( - new Point3D(baseCenterPoint.x, baseCenterPoint.y, baseCenterPoint.z), - new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z), - new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z), - color)); + baseCapVertices[i + 1] = baseRing[i]; } + baseCapVertices[segments + 1] = baseRing[0]; // close the loop + addShape(new SolidPolygon(baseCapVertices, color)); setBackfaceCulling(true); } @@ -207,17 +207,17 @@ public class SolidPolygonCone extends AbstractCompositeShape { } // 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). - // Base ring vertices go CCW when viewed from above (+Y), so center → current → next - // maintains CCW for the cap when viewed from +Y (the correct direction). + // 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++) { - final int next = (i + 1) % segments; - addShape(new SolidPolygon( - new Point3D(baseCenter.x, baseCenter.y, baseCenter.z), - new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z), - new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z), - color)); + baseCapVertices[i + 1] = baseRing[i]; } + baseCapVertices[segments + 1] = baseRing[0]; // close the loop + addShape(new SolidPolygon(baseCapVertices, color)); setBackfaceCulling(true); } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java index 276e6d4..b4673f6 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonCylinder.java @@ -118,47 +118,42 @@ public class SolidPolygonCylinder extends AbstractCompositeShape { endSideRing[i] = endLocal; } - // Create side faces (two triangles per segment) - // Winding: startSide → endSide → startSide+next, then startSide+next → endSide → endSide+next - // This creates CCW winding when viewed from outside the cylinder + // 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(new SolidPolygon( - new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z), - new Point3D(endSideRing[i].x, endSideRing[i].y, endSideRing[i].z), - new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z), - color)); - - addShape(new SolidPolygon( - new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z), - new Point3D(endSideRing[i].x, endSideRing[i].y, endSideRing[i].z), - new Point3D(endSideRing[next].x, endSideRing[next].y, endSideRing[next].z), + addShape(SolidPolygon.quad( + startSideRing[i], + endSideRing[i], + endSideRing[next], + startSideRing[next], color)); } // Create start cap (at startPoint, faces outward from cylinder) - // Winding: center → current → next creates CCW winding when viewed from outside + // 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++) { - final int next = (i + 1) % segments; - addShape(new SolidPolygon( - new Point3D(startPoint.x, startPoint.y, startPoint.z), - new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z), - new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z), - color)); + 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) - // Winding: center → next → current creates CCW winding when viewed from outside - // (opposite to start cap because end cap faces the opposite direction) + // 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++) { - final int next = (i + 1) % segments; - addShape(new SolidPolygon( - new Point3D(endPoint.x, endPoint.y, endPoint.z), - new Point3D(endSideRing[next].x, endSideRing[next].y, endSideRing[next].z), - new Point3D(endSideRing[i].x, endSideRing[i].y, endSideRing[i].z), - color)); + endCapVertices[i + 1] = endSideRing[segments - 1 - i]; } + endCapVertices[segments + 1] = endSideRing[segments - 1]; // close the loop + addShape(new SolidPolygon(endCapVertices, color)); setBackfaceCulling(true); } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java index 3014ef7..7481894 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java @@ -15,21 +15,17 @@ import java.util.List; * A renderable mesh composed of SolidPolygon triangles. * *

This is a generic composite shape that holds a collection of triangles. - * It can be constructed from any source of triangles, such as CSG operation - * results or procedural geometry generation.

+ * It can be constructed from any source of triangles, such as procedural + * geometry generation or loaded mesh data.

* *

Usage:

*
{@code
- * // From CSG result
- * CSG result = cubeCSG.subtract(sphereCSG);
- * SolidPolygonMesh mesh = result.toMesh(new Point3D(0, 0, 0));
- * mesh.setShadingEnabled(true);
- * mesh.setBackfaceCulling(true);
- * shapes.addShape(mesh);
- *
  * // From list of triangles
  * List triangles = ...;
- * SolidPolygonMesh mesh = new SolidPolygonMesh(triangles, new Point3D(0, 0, 0));
+ * SolidPolygonMesh mesh = new SolidPolygonMesh(triangles, location);
+ *
+ * // With fluent configuration
+ * shapes.addShape(mesh.setShadingEnabled(true).setBackfaceCulling(true));
  * }
* * @see SolidPolygon the triangle type for rendering diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java index e3c038e..fbf6eb6 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonPyramid.java @@ -141,32 +141,20 @@ public class SolidPolygonPyramid extends AbstractCompositeShape { color)); } - // Create base cap (square bottom face) + // 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). - // Base corners go CW when viewed from apex, so CW when viewed from apex means - // CCW when viewed from outside (base side). Use CCW ordering for outward normal. - // Triangulate the square base: (center, 3, 0) and (center, 0, 1) and - // (center, 1, 2) and (center, 2, 3) - addShape(new SolidPolygon( - new Point3D(baseCenter.x, baseCenter.y, baseCenter.z), - new Point3D(baseCorners[3].x, baseCorners[3].y, baseCorners[3].z), - new Point3D(baseCorners[0].x, baseCorners[0].y, baseCorners[0].z), - color)); - addShape(new SolidPolygon( - new Point3D(baseCenter.x, baseCenter.y, baseCenter.z), - new Point3D(baseCorners[0].x, baseCorners[0].y, baseCorners[0].z), - new Point3D(baseCorners[1].x, baseCorners[1].y, baseCorners[1].z), - color)); - addShape(new SolidPolygon( - new Point3D(baseCenter.x, baseCenter.y, baseCenter.z), - new Point3D(baseCorners[1].x, baseCorners[1].y, baseCorners[1].z), - new Point3D(baseCorners[2].x, baseCorners[2].y, baseCorners[2].z), - color)); - addShape(new SolidPolygon( - new Point3D(baseCenter.x, baseCenter.y, baseCenter.z), - new Point3D(baseCorners[2].x, baseCorners[2].y, baseCorners[2].z), - new Point3D(baseCorners[3].x, baseCorners[3].y, baseCorners[3].z), - color)); + // 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); } @@ -215,12 +203,11 @@ public class SolidPolygonPyramid extends AbstractCompositeShape { 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. - // Base corners go CW when viewed from apex (looking in +Y direction). // For outward normal (+Y direction), we need CCW ordering when viewed from +Y. - // CCW from +Y is: 3 → 2 → 1 → 0, so triangles: (3, 2, 1) and (3, 1, 0) - addShape(new SolidPolygon(negXposZ, posXposZ, posXnegZ, color)); - addShape(new SolidPolygon(negXposZ, posXnegZ, negXnegZ, color)); + // Quad order: negXposZ → posXposZ → posXnegZ → negXnegZ (CCW from +Y) + addShape(SolidPolygon.quad(negXposZ, posXposZ, posXnegZ, negXnegZ, color)); setBackfaceCulling(true); } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java index fa67cfc..1293c62 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonRectangularBox.java @@ -10,7 +10,7 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPo import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; /** - * A solid (filled) rectangular box composed of 12 triangular polygons (2 per face, + * 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. @@ -73,7 +73,7 @@ public class SolidPolygonRectangularBox extends AbstractCompositeShape { * * @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 12 triangular polygons + * @param color the fill color applied to all 6 quadrilateral polygons */ public SolidPolygonRectangularBox(final Point3D cornerA, final Point3D cornerB, final Color color) { super(); @@ -99,29 +99,23 @@ public class SolidPolygonRectangularBox extends AbstractCompositeShape { final Point3D minMaxMax = new Point3D(minX, maxY, maxZ); final Point3D maxMaxMax = new Point3D(maxX, maxY, maxZ); - // Bottom face (y = minY) - addShape(new SolidPolygon(minMinMin, maxMinMin, maxMinMax, color)); - addShape(new SolidPolygon(minMinMin, maxMinMax, minMinMax, color)); + // Bottom face (y = minY) - CCW when viewed from below + addShape(new SolidPolygon(new Point3D[]{minMinMin, maxMinMin, maxMinMax, minMinMax}, color)); - // Top face (y = maxY) - addShape(new SolidPolygon(minMaxMin, minMaxMax, maxMaxMax, color)); - addShape(new SolidPolygon(minMaxMin, maxMaxMax, maxMaxMin, color)); + // Top face (y = maxY) - CCW when viewed from above + addShape(new SolidPolygon(new Point3D[]{minMaxMin, minMaxMax, maxMaxMax, maxMaxMin}, color)); - // Front face (z = minZ) - addShape(new SolidPolygon(minMinMin, minMaxMin, maxMaxMin, color)); - addShape(new SolidPolygon(minMinMin, maxMaxMin, maxMinMin, color)); + // Front face (z = minZ) - CCW when viewed from front + addShape(new SolidPolygon(new Point3D[]{minMinMin, minMaxMin, maxMaxMin, maxMinMin}, color)); - // Back face (z = maxZ) - addShape(new SolidPolygon(maxMinMax, maxMaxMax, minMaxMax, color)); - addShape(new SolidPolygon(maxMinMax, minMaxMax, minMinMax, color)); + // Back face (z = maxZ) - CCW when viewed from behind + addShape(new SolidPolygon(new Point3D[]{maxMinMax, maxMaxMax, minMaxMax, minMinMax}, color)); - // Left face (x = minX) - addShape(new SolidPolygon(minMinMin, minMinMax, minMaxMax, color)); - addShape(new SolidPolygon(minMinMin, minMaxMax, minMaxMin, color)); + // Left face (x = minX) - CCW when viewed from left + addShape(new SolidPolygon(new Point3D[]{minMinMin, minMinMax, minMaxMax, minMaxMin}, color)); - // Right face (x = maxX) - addShape(new SolidPolygon(maxMinMin, maxMaxMin, maxMaxMax, color)); - addShape(new SolidPolygon(maxMinMin, maxMaxMax, maxMinMax, 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/sixth/e3d/renderer/raster/shapes/composite/solid/package-info.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/package-info.java index 3a3c501..d812d4b 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/package-info.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/package-info.java @@ -4,7 +4,7 @@ */ /** - * Solid composite shapes built from SolidPolygon primitives. + * 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.

diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java index 59fdb2b..989f028 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java @@ -11,7 +11,8 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape; import java.awt.*; -import static eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon.drawPolygon; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import static eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon.drawTriangle; import static eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas.FONT_CHAR_HEIGHT; import static eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.textcanvas.TextCanvas.FONT_CHAR_WIDTH; import static java.lang.String.valueOf; @@ -63,25 +64,25 @@ public class CanvasCharacter extends AbstractCoordinateShape { this.backgroundColor = backgroundColor; - vertices[0].coordinate = centerLocation; + vertices.get(0).coordinate = centerLocation; final double halfWidth = FONT_CHAR_WIDTH / 2d; final double halfHeight = FONT_CHAR_HEIGHT / 2d; // upper left - vertices[1].coordinate = centerLocation.clone().translateX(-halfWidth) + vertices.get(1).coordinate = centerLocation.clone().translateX(-halfWidth) .translateY(-halfHeight); // upper right - vertices[2].coordinate = centerLocation.clone().translateX(halfWidth) + vertices.get(2).coordinate = centerLocation.clone().translateX(halfWidth) .translateY(-halfHeight); // lower right - vertices[3].coordinate = centerLocation.clone().translateX(halfWidth) + vertices.get(3).coordinate = centerLocation.clone().translateX(halfWidth) .translateY(halfHeight); // lower left - vertices[4].coordinate = centerLocation.clone().translateX(-halfWidth) + vertices.get(4).coordinate = centerLocation.clone().translateX(-halfWidth) .translateY(halfHeight); } @@ -150,17 +151,17 @@ public class CanvasCharacter extends AbstractCoordinateShape { public void paint(final RenderingContext renderingContext) { // Draw background rectangle first. It is composed of two triangles. - drawPolygon(renderingContext, - vertices[1].onScreenCoordinate, - vertices[2].onScreenCoordinate, - vertices[3].onScreenCoordinate, + drawTriangle(renderingContext, + vertices.get(1).onScreenCoordinate, + vertices.get(2).onScreenCoordinate, + vertices.get(3).onScreenCoordinate, mouseInteractionController, backgroundColor); - drawPolygon(renderingContext, - vertices[1].onScreenCoordinate, - vertices[3].onScreenCoordinate, - vertices[4].onScreenCoordinate, + drawTriangle(renderingContext, + vertices.get(1).onScreenCoordinate, + vertices.get(3).onScreenCoordinate, + vertices.get(4).onScreenCoordinate, mouseInteractionController, backgroundColor); @@ -170,7 +171,7 @@ public class CanvasCharacter extends AbstractCoordinateShape { if (desiredFontSize >= MAX_FONT_SIZE) return; - final Point2D onScreenLocation = vertices[0].onScreenCoordinate; + final Point2D onScreenLocation = vertices.get(0).onScreenCoordinate; // screen borders check if (onScreenLocation.x < 0) diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java index c023f3f..9226e5d 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java @@ -5,7 +5,7 @@ package eu.svjatoslav.sixth.e3d.renderer.raster.slicer; import eu.svjatoslav.sixth.e3d.math.Vertex; -import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle; import java.util.ArrayList; import java.util.List; @@ -33,7 +33,7 @@ import java.util.List; * to break large composite shapes into appropriately-sized sub-polygons.

* * @see BorderLine - * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle */ public class Slicer { @@ -51,7 +51,7 @@ public class Slicer { /** * Result of slicing. */ - private final List result = new ArrayList<>(); + private final List result = new ArrayList<>(); /** * Creates a new slicer with the specified maximum edge length. @@ -66,7 +66,7 @@ public class Slicer { private void considerSlicing(final Vertex c1, final Vertex c2, final Vertex c3, - final TexturedPolygon originalPolygon) { + final TexturedTriangle originalPolygon) { line1.set(c1, c2, 1); line2.set(c2, c3, 2); @@ -96,7 +96,7 @@ public class Slicer { final BorderLine longestLine = c; if (longestLine.getLength() < maxDistance) { - final TexturedPolygon polygon = new TexturedPolygon(c1, c2, c3, + final TexturedTriangle polygon = new TexturedTriangle(c1, c2, c3, originalPolygon.texture); polygon.setMouseInteractionController(originalPolygon.mouseInteractionController); @@ -126,9 +126,9 @@ public class Slicer { /** * Returns the list of subdivided polygons produced by the slicing process. * - * @return an unmodifiable view of the resulting {@link TexturedPolygon} list + * @return an unmodifiable view of the resulting {@link TexturedTriangle} list */ - public List getResult() { + public List getResult() { return result; } @@ -141,12 +141,12 @@ public class Slicer { * * @param originalPolygon the polygon to subdivide */ - public void slice(final TexturedPolygon originalPolygon) { + public void slice(final TexturedTriangle originalPolygon) { considerSlicing( - originalPolygon.vertices[0], - originalPolygon.vertices[1], - originalPolygon.vertices[2], + originalPolygon.vertices.get(0), + originalPolygon.vertices.get(1), + originalPolygon.vertices.get(2), originalPolygon); } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java index 689d0c3..e208058 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/texture/Texture.java @@ -47,7 +47,7 @@ import static java.util.Arrays.fill; * } * * @see TextureBitmap - * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon + * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle */ public class Texture { -- 2.20.1