From: Svjatoslav Agejenko Date: Mon, 30 Mar 2026 16:44:01 +0000 (+0300) Subject: feat: unify polygon type for CSG and rendering X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=refs%2Fheads%2Ffeat;p=sixth-3d.git 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 --- 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 d028e61..0000000 Binary files a/doc/Axis.png and /dev/null differ diff --git a/doc/Minimal example.png b/doc/Minimal example.png deleted file mode 100644 index b2ceac7..0000000 Binary files a/doc/Minimal example.png and /dev/null differ diff --git a/doc/Winding order demo.png b/doc/Winding order demo.png deleted file mode 100644 index e9f9e25..0000000 Binary files a/doc/Winding order demo.png and /dev/null differ 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:

- * - * - *

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/TexturedPolygon.java deleted file mode 100644 index b5ba635..0000000 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java +++ /dev/null @@ -1,365 +0,0 @@ -/* - * Sixth 3D engine. Author: Svjatoslav Agejenko. - * This project is released under Creative Commons Zero (CC0) license. - */ -package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon; - -import eu.svjatoslav.sixth.e3d.geometry.Point2D; -import eu.svjatoslav.sixth.e3d.gui.RenderingContext; -import eu.svjatoslav.sixth.e3d.math.Vertex; -import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape; -import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture; -import eu.svjatoslav.sixth.e3d.renderer.raster.texture.TextureBitmap; - -import java.awt.*; - -import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon; - -/** - * A textured triangle renderer with perspective-correct texture mapping. - * - *

This class renders triangles with UV-mapped textures. For large triangles, - * the rendering may be sliced into smaller pieces for better perspective correction.

- * - *

Perspective-correct texture rendering:

- *
    - *
  • Small polygons are rendered without perspective correction
  • - *
  • Larger polygons are sliced into smaller pieces for accurate perspective
  • - *
- * - * @see Texture - * @see Vertex#textureCoordinate - */ -public class TexturedPolygon extends AbstractCoordinateShape { - - private static final ThreadLocal INTERPOLATORS = - ThreadLocal.withInitial(() -> new PolygonBorderInterpolator[]{ - new PolygonBorderInterpolator(), new PolygonBorderInterpolator(), new PolygonBorderInterpolator() - }); - - /** - * The texture to apply to this polygon. - */ - public final Texture texture; - - private boolean backfaceCulling = false; - - private double totalTextureDistance = -1; - - /** - * Creates a textured triangle with the specified vertices and texture. - * - * @param p1 the first vertex (must have textureCoordinate set) - * @param p2 the second vertex (must have textureCoordinate set) - * @param p3 the third vertex (must have textureCoordinate set) - * @param texture the texture to apply - */ - public TexturedPolygon(Vertex p1, Vertex p2, Vertex p3, final Texture texture) { - - super(p1, p2, p3); - this.texture = texture; - } - - /** - * Computes the total UV distance between all texture coordinate pairs. - * 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); - } - - /** - * Draws a horizontal scanline between two edge interpolators with texture sampling. - * - * @param line1 the left edge interpolator - * @param line2 the right edge interpolator - * @param y the Y coordinate of the scanline - * @param renderBuffer the rendering context to draw into - * @param textureBitmap the texture bitmap to sample from - */ - private void drawHorizontalLine(final PolygonBorderInterpolator line1, - final PolygonBorderInterpolator line2, final int y, - final RenderingContext renderBuffer, - final TextureBitmap textureBitmap) { - - line1.setCurrentY(y); - line2.setCurrentY(y); - - int x1 = line1.getX(); - int x2 = line2.getX(); - - final double tx2, ty2; - final double tx1, ty1; - - if (x1 <= x2) { - - tx1 = line1.getTX() * textureBitmap.multiplicationFactor; - ty1 = line1.getTY() * textureBitmap.multiplicationFactor; - - tx2 = line2.getTX() * textureBitmap.multiplicationFactor; - ty2 = line2.getTY() * textureBitmap.multiplicationFactor; - - } else { - final int tmp = x1; - x1 = x2; - x2 = tmp; - - tx1 = line2.getTX() * textureBitmap.multiplicationFactor; - ty1 = line2.getTY() * textureBitmap.multiplicationFactor; - - tx2 = line1.getTX() * textureBitmap.multiplicationFactor; - ty2 = line1.getTY() * textureBitmap.multiplicationFactor; - } - - final double realWidth = x2 - x1; - final double realX1 = x1; - - if (x1 < 0) - x1 = 0; - - if (x2 >= renderBuffer.width) - x2 = renderBuffer.width - 1; - - int renderBufferOffset = (y * renderBuffer.width) + x1; - final int[] renderBufferPixels = renderBuffer.pixels; - - final double twidth = tx2 - tx1; - final double theight = ty2 - ty1; - - final double txStep = twidth / realWidth; - final double tyStep = theight / realWidth; - - double tx = tx1 + txStep * (x1 - realX1); - double ty = ty1 + tyStep * (x1 - realX1); - - final int[] texPixels = textureBitmap.pixels; - final int texW = textureBitmap.width; - final int texH = textureBitmap.height; - final int texWMinus1 = texW - 1; - final int texHMinus1 = texH - 1; - - for (int x = x1; x < x2; x++) { - - int itx = (int) tx; - int ity = (int) ty; - - if (itx < 0) itx = 0; - else if (itx > texWMinus1) itx = texWMinus1; - - if (ity < 0) ity = 0; - else if (ity > texHMinus1) ity = texHMinus1; - - final int srcPixel = texPixels[ity * texW + itx]; - final int srcAlpha = (srcPixel >> 24) & 0xff; - - if (srcAlpha != 0) { - if (srcAlpha == 255) { - renderBufferPixels[renderBufferOffset] = srcPixel; - } else { - final int backgroundAlpha = 255 - srcAlpha; - - final int srcR = ((srcPixel >> 16) & 0xff) * srcAlpha; - final int srcG = ((srcPixel >> 8) & 0xff) * srcAlpha; - final int srcB = (srcPixel & 0xff) * srcAlpha; - - final int destPixel = renderBufferPixels[renderBufferOffset]; - final int destR = (destPixel >> 16) & 0xff; - final int destG = (destPixel >> 8) & 0xff; - final int destB = destPixel & 0xff; - - final int r = ((destR * backgroundAlpha) + srcR) >> 8; - final int g = ((destG * backgroundAlpha) + srcG) >> 8; - final int b = ((destB * backgroundAlpha) + srcB) >> 8; - - renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b; - } - } - - tx += txStep; - ty += tyStep; - renderBufferOffset++; - } - - } - - /** - * Renders this textured triangle to the screen. - * - *

This method performs:

- *
    - *
  • Backface culling check (if enabled)
  • - *
  • Mouse interaction detection
  • - *
  • Mipmap level selection based on screen coverage
  • - *
  • Scanline rasterization with texture sampling
  • - *
- * - * @param renderBuffer the rendering context containing the pixel buffer - */ - @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; - - if (backfaceCulling) { - final double signedArea = (projectedPoint2.x - projectedPoint1.x) - * (projectedPoint3.y - projectedPoint1.y) - - (projectedPoint3.x - projectedPoint1.x) - * (projectedPoint2.y - projectedPoint1.y); - if (signedArea >= 0) - return; - } - - projectedPoint1.roundToInteger(); - projectedPoint2.roundToInteger(); - projectedPoint3.roundToInteger(); - - if (mouseInteractionController != null) - if (renderBuffer.getMouseEvent() != null) - if (pointWithinPolygon( - renderBuffer.getMouseEvent().coordinate, projectedPoint1, - projectedPoint2, projectedPoint3)) - renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController); - - // Show polygon boundaries (for debugging) - if (renderBuffer.developerTools != null && renderBuffer.developerTools.showPolygonBorders) - showBorders(renderBuffer); - - // find top-most point - int yTop = (int) projectedPoint1.y; - - if (projectedPoint2.y < yTop) - yTop = (int) projectedPoint2.y; - - if (projectedPoint3.y < yTop) - yTop = (int) projectedPoint3.y; - - if (yTop < 0) - yTop = 0; - - // find bottom-most point - int yBottom = (int) projectedPoint1.y; - - if (projectedPoint2.y > yBottom) - yBottom = (int) projectedPoint2.y; - - if (projectedPoint3.y > yBottom) - yBottom = (int) projectedPoint3.y; - - if (yBottom >= renderBuffer.height) - yBottom = renderBuffer.height - 1; - - // clamp to render Y bounds - yTop = Math.max(yTop, renderBuffer.renderMinY); - yBottom = Math.min(yBottom, renderBuffer.renderMaxY); - if (yTop >= yBottom) - return; - - // paint - double totalVisibleDistance = projectedPoint1.getDistanceTo(projectedPoint2); - 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); - - final PolygonBorderInterpolator[] interp = INTERPOLATORS.get(); - final PolygonBorderInterpolator polygonBorder1 = interp[0]; - 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); - - // Inline sort for 3 elements to avoid array allocation - PolygonBorderInterpolator a = polygonBorder1; - PolygonBorderInterpolator b = polygonBorder2; - PolygonBorderInterpolator c = polygonBorder3; - PolygonBorderInterpolator t; - if (a.compareTo(b) > 0) { - t = a; - a = b; - b = t; - } - if (b.compareTo(c) > 0) { - t = b; - b = c; - c = t; - } - if (a.compareTo(b) > 0) { - t = a; - a = b; - b = t; - } - - for (int y = yTop; y < yBottom; y++) - if (a.containsY(y)) { - if (b.containsY(y)) - drawHorizontalLine(a, b, y, renderBuffer, zoomedBitmap); - else if (c.containsY(y)) - drawHorizontalLine(a, c, y, renderBuffer, zoomedBitmap); - } else if (b.containsY(y)) - if (c.containsY(y)) - drawHorizontalLine(b, c, y, renderBuffer, zoomedBitmap); - - } - - /** - * 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; - } - - /** - * Draws the polygon 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 int x1 = (int) projectedPoint1.x; - final int y1 = (int) projectedPoint1.y; - final int x2 = (int) projectedPoint2.x; - final int y2 = (int) projectedPoint2.y; - final int x3 = (int) projectedPoint3.x; - final int y3 = (int) projectedPoint3.y; - - renderBuffer.executeWithGraphics(g -> { - g.setColor(Color.YELLOW); - g.drawLine(x1, y1, x2, y2); - g.drawLine(x3, y3, x2, y2); - g.drawLine(x1, y1, x3, y3); - }); - } - -} diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java new file mode 100644 index 0000000..6a015b5 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedTriangle.java @@ -0,0 +1,373 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon; + +import eu.svjatoslav.sixth.e3d.geometry.Point2D; +import eu.svjatoslav.sixth.e3d.gui.RenderingContext; +import eu.svjatoslav.sixth.e3d.math.Vertex; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape; +import eu.svjatoslav.sixth.e3d.renderer.raster.texture.Texture; +import eu.svjatoslav.sixth.e3d.renderer.raster.texture.TextureBitmap; + +import java.awt.*; + +import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon; + +/** + * A textured triangle renderer with perspective-correct texture mapping. + * + *

This class renders triangles with UV-mapped textures. For large triangles, + * the rendering may be sliced into smaller pieces for better perspective correction.

+ * + *

Perspective-correct texture rendering:

+ *
    + *
  • Small triangles are rendered without perspective correction
  • + *
  • Larger triangles are sliced into smaller pieces for accurate perspective
  • + *
+ * + * @see Texture + * @see Vertex#textureCoordinate + */ +public class TexturedTriangle extends AbstractCoordinateShape { + + private static final ThreadLocal INTERPOLATORS = + ThreadLocal.withInitial(() -> new PolygonBorderInterpolator[]{ + new PolygonBorderInterpolator(), new PolygonBorderInterpolator(), new PolygonBorderInterpolator() + }); + + /** + * The texture to apply to this triangle. + */ + public final Texture texture; + + private boolean backfaceCulling = false; + + /** + * Total UV distance between all texture coordinate pairs. + * Computed at construction time to determine appropriate mipmap level. + */ + private double totalTextureDistance; + + /** + * Creates a textured triangle with the specified vertices and texture. + * + * @param p1 the first vertex (must have textureCoordinate set) + * @param p2 the second vertex (must have textureCoordinate set) + * @param p3 the third vertex (must have textureCoordinate set) + * @param texture the texture to apply + */ + public TexturedTriangle(Vertex p1, Vertex p2, Vertex p3, final Texture texture) { + + super(p1, p2, p3); + this.texture = texture; + computeTotalTextureDistance(); + } + + /** + * Computes the total UV distance between all texture coordinate pairs. + * Used to determine appropriate mipmap level. + */ + private void computeTotalTextureDistance() { + totalTextureDistance = vertices.get(0).textureCoordinate.getDistanceTo(vertices.get(1).textureCoordinate); + totalTextureDistance += vertices.get(0).textureCoordinate.getDistanceTo(vertices.get(2).textureCoordinate); + totalTextureDistance += vertices.get(1).textureCoordinate.getDistanceTo(vertices.get(2).textureCoordinate); + } + + /** + * Draws a horizontal scanline between two edge interpolators with texture sampling. + * + * @param line1 the left edge interpolator + * @param line2 the right edge interpolator + * @param y the Y coordinate of the scanline + * @param renderBuffer the rendering context to draw into + * @param textureBitmap the texture bitmap to sample from + */ + private void drawHorizontalLine(final PolygonBorderInterpolator line1, + final PolygonBorderInterpolator line2, final int y, + final RenderingContext renderBuffer, + final TextureBitmap textureBitmap) { + + line1.setCurrentY(y); + line2.setCurrentY(y); + + int x1 = line1.getX(); + int x2 = line2.getX(); + + final double tx2, ty2; + final double tx1, ty1; + + if (x1 <= x2) { + + tx1 = line1.getTX() * textureBitmap.multiplicationFactor; + ty1 = line1.getTY() * textureBitmap.multiplicationFactor; + + tx2 = line2.getTX() * textureBitmap.multiplicationFactor; + ty2 = line2.getTY() * textureBitmap.multiplicationFactor; + + } else { + final int tmp = x1; + x1 = x2; + x2 = tmp; + + tx1 = line2.getTX() * textureBitmap.multiplicationFactor; + ty1 = line2.getTY() * textureBitmap.multiplicationFactor; + + tx2 = line1.getTX() * textureBitmap.multiplicationFactor; + ty2 = line1.getTY() * textureBitmap.multiplicationFactor; + } + + final double realWidth = x2 - x1; + final double realX1 = x1; + + if (x1 < 0) + x1 = 0; + + if (x2 >= renderBuffer.width) + x2 = renderBuffer.width - 1; + + int renderBufferOffset = (y * renderBuffer.width) + x1; + final int[] renderBufferPixels = renderBuffer.pixels; + + final double twidth = tx2 - tx1; + final double theight = ty2 - ty1; + + final double txStep = twidth / realWidth; + final double tyStep = theight / realWidth; + + double tx = tx1 + txStep * (x1 - realX1); + double ty = ty1 + tyStep * (x1 - realX1); + + final int[] texPixels = textureBitmap.pixels; + final int texW = textureBitmap.width; + final int texH = textureBitmap.height; + final int texWMinus1 = texW - 1; + final int texHMinus1 = texH - 1; + + for (int x = x1; x < x2; x++) { + + int itx = (int) tx; + int ity = (int) ty; + + if (itx < 0) itx = 0; + else if (itx > texWMinus1) itx = texWMinus1; + + if (ity < 0) ity = 0; + else if (ity > texHMinus1) ity = texHMinus1; + + final int srcPixel = texPixels[ity * texW + itx]; + final int srcAlpha = (srcPixel >> 24) & 0xff; + + if (srcAlpha != 0) { + if (srcAlpha == 255) { + renderBufferPixels[renderBufferOffset] = srcPixel; + } else { + final int backgroundAlpha = 255 - srcAlpha; + + final int srcR = ((srcPixel >> 16) & 0xff) * srcAlpha; + final int srcG = ((srcPixel >> 8) & 0xff) * srcAlpha; + final int srcB = (srcPixel & 0xff) * srcAlpha; + + final int destPixel = renderBufferPixels[renderBufferOffset]; + final int destR = (destPixel >> 16) & 0xff; + final int destG = (destPixel >> 8) & 0xff; + final int destB = destPixel & 0xff; + + final int r = ((destR * backgroundAlpha) + srcR) >> 8; + final int g = ((destG * backgroundAlpha) + srcG) >> 8; + final int b = ((destB * backgroundAlpha) + srcB) >> 8; + + renderBufferPixels[renderBufferOffset] = (r << 16) | (g << 8) | b; + } + } + + tx += txStep; + ty += tyStep; + renderBufferOffset++; + } + + } + + /** + * Renders this textured triangle to the screen. + * + *

This method performs:

+ *
    + *
  • Backface culling check (if enabled)
  • + *
  • Mouse interaction detection
  • + *
  • Mipmap level selection based on screen coverage
  • + *
  • Scanline rasterization with texture sampling
  • + *
+ * + * @param renderBuffer the rendering context containing the pixel buffer + */ + @Override + public void paint(final RenderingContext renderBuffer) { + + 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) + * (projectedPoint3.y - projectedPoint1.y) + - (projectedPoint3.x - projectedPoint1.x) + * (projectedPoint2.y - projectedPoint1.y); + if (signedArea >= 0) + return; + } + + // 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, x1, y1, x2, y2, x3, y3)) + renderBuffer.setCurrentObjectUnderMouseCursor(mouseInteractionController); + + // Show polygon boundaries (for debugging) + if (renderBuffer.developerTools != null && renderBuffer.developerTools.showPolygonBorders) + showBorders(renderBuffer); + + // 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 = y1; + + if (y2 > yBottom) + yBottom = y2; + + if (y3 > yBottom) + yBottom = y3; + + if (yBottom >= renderBuffer.height) + yBottom = renderBuffer.height - 1; + + // clamp to render Y bounds + yTop = Math.max(yTop, renderBuffer.renderMinY); + yBottom = Math.min(yBottom, renderBuffer.renderMaxY); + if (yTop >= yBottom) + return; + + // paint + double totalVisibleDistance = projectedPoint1.getDistanceTo(projectedPoint2); + totalVisibleDistance += projectedPoint1.getDistanceTo(projectedPoint3); + totalVisibleDistance += projectedPoint2.getDistanceTo(projectedPoint3); + + final double scaleFactor = (totalVisibleDistance / totalTextureDistance) * 1.2d; + + final TextureBitmap zoomedBitmap = texture.getZoomedBitmap(scaleFactor); + + final PolygonBorderInterpolator[] interp = INTERPOLATORS.get(); + final PolygonBorderInterpolator polygonBorder1 = interp[0]; + final PolygonBorderInterpolator polygonBorder2 = interp[1]; + final PolygonBorderInterpolator polygonBorder3 = interp[2]; + + // 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; + PolygonBorderInterpolator b = polygonBorder2; + PolygonBorderInterpolator c = polygonBorder3; + PolygonBorderInterpolator t; + if (a.compareTo(b) > 0) { + t = a; + a = b; + b = t; + } + if (b.compareTo(c) > 0) { + t = b; + b = c; + c = t; + } + if (a.compareTo(b) > 0) { + t = a; + a = b; + b = t; + } + + for (int y = yTop; y < yBottom; y++) + if (a.containsY(y)) { + if (b.containsY(y)) + drawHorizontalLine(a, b, y, renderBuffer, zoomedBitmap); + else if (c.containsY(y)) + drawHorizontalLine(a, c, y, renderBuffer, zoomedBitmap); + } else if (b.containsY(y)) + if (c.containsY(y)) + drawHorizontalLine(b, c, y, renderBuffer, zoomedBitmap); + + } + + /** + * Checks if backface culling is enabled for this triangle. + * + * @return {@code true} if backface culling is enabled + */ + public boolean isBackfaceCullingEnabled() { + return backfaceCulling; + } + + /** + * Enables or disables backface culling for this triangle. + * + * @param backfaceCulling {@code true} to enable backface culling + */ + public void setBackfaceCulling(final boolean backfaceCulling) { + this.backfaceCulling = backfaceCulling; + } + + /** + * Draws the triangle border edges in yellow (for debugging). + * + * @param renderBuffer the rendering context + */ + private void showBorders(final RenderingContext renderBuffer) { + + final Point2D projectedPoint1 = vertices.get(0).onScreenCoordinate; + 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; + final int x2 = (int) projectedPoint2.x; + final int y2 = (int) projectedPoint2.y; + final int x3 = (int) projectedPoint3.x; + final int y3 = (int) projectedPoint3.y; + + renderBuffer.executeWithGraphics(g -> { + g.setColor(Color.YELLOW); + g.drawLine(x1, y1, x2, y2); + g.drawLine(x3, y3, x2, y2); + g.drawLine(x1, y1, x3, y3); + }); + } + +} diff --git a/src/main/java/eu/svjatoslav/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 {