├── 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
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
## Shape Hierarchy
- `AbstractShape` — base class with optional `MouseInteractionController`
-- `AbstractCoordinateShape` — has `Vertex[]` coordinates and `onScreenZ` for depth sorting
+- `AbstractCoordinateShape` — has `List<Vertex>` 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
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
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
-<dependencies>
- <dependency>
- <groupId>eu.svjatoslav</groupId>
- <artifactId>sixth-3d</artifactId>
- <version>1.3</version>
- </dependency>
-</dependencies>
-
-<repositories>
- <repository>
- <id>svjatoslav.eu</id>
- <name>Svjatoslav repository</name>
- <url>https://www3.svjatoslav.eu/maven/</url>
- </repository>
-</repositories>
-#+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
- 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:
- 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
- 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:
- [[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
(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)
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);
| 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 |
+++ /dev/null
-/*
- * 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.
- *
- * <p>CSG allows combining 3D shapes using boolean operations:</p>
- * <ul>
- * <li><b>Union:</b> Combine two shapes into one</li>
- * <li><b>Subtract:</b> Carve one shape out of another</li>
- * <li><b>Intersect:</b> Keep only the overlapping volume</li>
- * </ul>
- *
- * <p><b>Usage example:</b></p>
- * <pre>{@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);
- * }</pre>
- *
- * @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<CSGPolygon> 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<CSGPolygon> polygonList) {
- final CSG csg = new CSG();
- csg.polygons.addAll(polygonList);
- return csg;
- }
-
- /**
- * Creates a CSG solid from a list of SolidPolygon triangles.
- *
- * <p>Each SolidPolygon is converted to a CSGPolygon (3-vertex N-gon).
- * The color from each SolidPolygon is preserved.</p>
- *
- * @param solidPolygons the triangles to convert
- * @return a new CSG solid
- */
- public static CSG fromSolidPolygons(final List<SolidPolygon> solidPolygons) {
- final List<CSGPolygon> csgPolygons = new ArrayList<>(solidPolygons.size());
-
- for (final SolidPolygon sp : solidPolygons) {
- final List<Vertex> 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.
- *
- * <p>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.</p>
- *
- * @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<CSGPolygon> toPolygons() {
- return polygons;
- }
-
- /**
- * Performs a union operation with another CSG solid.
- *
- * <p>The result contains all points that are in either solid.</p>
- *
- * <h3>Algorithm:</h3>
- * <pre>
- * Union(A, B) = clip(A to outside B) + clip(B to outside A)
- * </pre>
- * <ol>
- * <li>Clip A's polygons to keep only parts outside B</li>
- * <li>Clip B's polygons to keep only parts outside A</li>
- * <li>Invert B, clip to A, invert again (keeps B's surface inside A)</li>
- * <li>Build final tree from all remaining polygons</li>
- * </ol>
- *
- * @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.
- *
- * <p>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.</p>
- *
- * <h3>Algorithm:</h3>
- * <pre>
- * Subtract(A, B) = A - B = clip(inverted A to B) inverted
- * </pre>
- * <ol>
- * <li>Invert A (turning solid into cavity, cavity into solid)</li>
- * <li>Clip inverted A to keep only parts inside B</li>
- * <li>Clip B to keep only parts inside inverted A</li>
- * <li>Invert B twice to get B's cavity surface</li>
- * <li>Combine and invert final result</li>
- * </ol>
- *
- * <p>The inversion trick converts "subtract B from A" into "intersect A
- * with the inverse of B", which the BSP algorithm handles naturally.</p>
- *
- * @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.
- *
- * <p>The result contains only the points that are in both solids.</p>
- *
- * <h3>Algorithm:</h3>
- * <pre>
- * Intersect(A, B) = clip(inverted A to outside B) inverted
- * </pre>
- * <ol>
- * <li>Invert A (swap inside/outside)</li>
- * <li>Clip inverted-A to B, keeping parts outside B</li>
- * <li>Invert B, clip to A (captures B's interior surface)</li>
- * <li>Clip B again to ensure proper boundaries</li>
- * <li>Combine and invert final result</li>
- * </ol>
- *
- * <p>This uses the principle: A ∩ B = ¬(¬A ∪ ¬B)</p>
- *
- * @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.
- *
- * <p>The inverse has all polygons flipped, effectively turning the solid inside-out.</p>
- *
- * @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.
- *
- * <p>All polygons are rendered with the specified color, ignoring
- * any colors stored in the CSGPolygons.</p>
- *
- * @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<SolidPolygon> triangles = new ArrayList<>();
-
- for (final CSGPolygon polygon : polygons) {
- triangulatePolygon(polygon, color, triangles);
- }
-
- return new SolidPolygonMesh(triangles, location);
- }
-
- /**
- * Triangulates a CSGPolygon using fan triangulation.
- *
- * <p>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:</p>
- *
- * <pre>
- * Original N-gon: v0-v1-v2-v3-v4...
- * Triangles: v0-v1-v2, v0-v2-v3, v0-v3-v4, ...
- * </pre>
- *
- * <p>This method is suitable for convex polygons. For concave polygons,
- * it may produce overlapping triangles, but CSG operations typically
- * generate convex polygon fragments.</p>
- *
- * @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<SolidPolygon> 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
*/
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;
* Each node divides 3D space into two half-spaces using a plane, enabling
* efficient spatial queries and polygon clipping.</p>
*
- * <h3>BSP Tree Structure:</h3>
+ * <p><b>BSP Tree Structure:</b></p>
* <pre>
* [Node: plane P]
* / \
* normal) of P's normal)
* </pre>
*
- * <h3>Key Properties:</h3>
- * <ul>
- * <li><b>polygons:</b> Polygons coplanar with this node's partitioning plane</li>
- * <li><b>plane:</b> The partitioning plane that divides space</li>
- * <li><b>front:</b> Subtree for the half-space the plane normal points toward</li>
- * <li><b>back:</b> Subtree for the opposite half-space</li>
- * </ul>
- *
- * <h3>CSG Algorithm Overview:</h3>
- * <p>CSG boolean operations (union, subtraction, intersection) work by:</p>
- * <ol>
- * <li>Building BSP trees from both input solids</li>
- * <li>Clipping each tree against the other (removing overlapping geometry)</li>
- * <li>Optionally inverting trees (for subtraction and intersection)</li>
- * <li>Collecting the resulting polygons</li>
- * </ol>
- *
- * @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.
- *
- * <p>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.</p>
*/
- public final List<CSGPolygon> polygons = new ArrayList<>();
+ public final List<SolidPolygon> polygons = new ArrayList<>();
/**
* The partitioning plane for this node.
- *
- * <p>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.</p>
- *
- * <p>Null for leaf nodes (empty subtrees).</p>
*/
public CSGPlane plane;
/**
* The front child subtree.
- *
- * <p>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.</p>
*/
public CSGNode front;
/**
* The back child subtree.
- *
- * <p>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.</p>
*/
public CSGNode back;
/**
* Creates an empty BSP node with no plane or children.
- *
- * <p>This constructor creates a leaf node. The plane, front, and back
- * fields will be populated when polygons are added via {@link #build(List)}.</p>
*/
public CSGNode() {
}
/**
* Creates a BSP tree from a list of polygons.
*
- * <p>Delegates to {@link #build(List)} to construct the tree.</p>
- *
* @param polygons the polygons to partition into a BSP tree
*/
- public CSGNode(final List<CSGPolygon> polygons) {
+ public CSGNode(final List<SolidPolygon> polygons) {
build(polygons);
}
/**
* Creates a deep clone of this BSP tree.
*
- * <p>Recursively clones all child nodes and polygons. The resulting tree
- * is completely independent of the original.</p>
- *
* @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;
/**
* Inverts this BSP tree, converting "inside" to "outside" and vice versa.
- *
- * <p>This operation is fundamental to CSG subtraction and intersection:</p>
- * <ul>
- * <li>All polygon normals are flipped (reversing their facing direction)</li>
- * <li>All plane normals are flipped</li>
- * <li>Front and back subtrees are swapped</li>
- * </ul>
- *
- * <p>After inversion:</p>
- * <ul>
- * <li>What was solid becomes empty space</li>
- * <li>What was empty space becomes solid</li>
- * <li>Front/back relationships are reversed throughout the tree</li>
- * </ul>
- *
- * <p>This is used in CSG subtraction where solid B "carves out" of solid A
- * by inverting B, unioning, then inverting the result.</p>
*/
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();
}
back.invert();
}
- // Swap front and back children since the half-spaces are now reversed
final CSGNode temp = front;
front = back;
back = temp;
/**
* Clips a list of polygons against this BSP tree.
*
- * <p>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.</p>
- *
- * <h3>Algorithm:</h3>
- * <ol>
- * <li>At each node, split input polygons by the node's plane</li>
- * <li>Polygons in front go to front child for further clipping</li>
- * <li>Polygons in back go to back child for further clipping</li>
- * <li>Coplanar polygons are kept (they're on the surface)</li>
- * <li>If no back child exists, back polygons are discarded (they're inside)</li>
- * </ol>
- *
- * <p>This is used during CSG operations to remove overlapping geometry.</p>
- *
* @param polygons the polygons to clip against this BSP tree
* @return a new list containing only the portions outside this solid
*/
- public List<CSGPolygon> clipPolygons(final List<CSGPolygon> polygons) {
- // Base case: if this is a leaf node, return copies of all polygons
+ public List<SolidPolygon> clipPolygons(final List<SolidPolygon> polygons) {
if (plane == null) {
return new ArrayList<>(polygons);
}
- // Split all input polygons by this node's plane
- final List<CSGPolygon> frontList = new ArrayList<>();
- final List<CSGPolygon> backList = new ArrayList<>();
+ final List<SolidPolygon> frontList = new ArrayList<>();
+ final List<SolidPolygon> 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<CSGPolygon> resultFront = frontList;
+ List<SolidPolygon> resultFront = frontList;
if (front != null) {
resultFront = front.clipPolygons(frontList);
}
- // Recursively clip back polygons against back subtree
- List<CSGPolygon> resultBack = backList;
+ List<SolidPolygon> 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<CSGPolygon> result = new ArrayList<>(resultFront.size() + resultBack.size());
+ final List<SolidPolygon> result = new ArrayList<>(resultFront.size() + resultBack.size());
result.addAll(resultFront);
result.addAll(resultBack);
return result;
/**
* Clips this BSP tree against another BSP tree.
*
- * <p>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.</p>
- *
- * <p>The operation modifies this tree in place, replacing all polygons
- * with their clipped versions.</p>
- *
- * @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<CSGPolygon> newPolygons = bsp.clipPolygons(polygons);
+ final List<SolidPolygon> newPolygons = bsp.clipPolygons(polygons);
polygons.clear();
polygons.addAll(newPolygons);
- // Recursively clip child subtrees
if (front != null) {
front.clipTo(bsp);
}
/**
* Collects all polygons from this BSP tree into a flat list.
*
- * <p>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.</p>
- *
* @return a new list containing all polygons in this tree
*/
- public List<CSGPolygon> allPolygons() {
- final List<CSGPolygon> result = new ArrayList<>(polygons);
+ public List<SolidPolygon> allPolygons() {
+ final List<SolidPolygon> result = new ArrayList<>(polygons);
- // Recursively collect polygons from child subtrees
if (front != null) {
result.addAll(front.allPolygons());
}
/**
* Builds or extends this BSP tree from a list of polygons.
*
- * <p>This is the core BSP tree construction algorithm. It partitions
- * space by selecting a splitting plane and recursively building subtrees.</p>
- *
- * <h3>Algorithm:</h3>
- * <ol>
- * <li>If this node has no plane, use the first polygon's plane as the partitioning plane</li>
- * <li>For each polygon:
- * <ul>
- * <li>Coplanar polygons are stored in this node</li>
- * <li>Front polygons go to the front list</li>
- * <li>Back polygons go to the back list</li>
- * <li>Spanning polygons are split into front and back parts</li>
- * </ul>
- * </li>
- * <li>Recursively build front subtree with front polygons</li>
- * <li>Recursively build back subtree with back polygons</li>
- * </ol>
- *
- * <h3>Calling Conventions:</h3>
- * <ul>
- * <li>Can be called multiple times to add more polygons to an existing tree</li>
- * <li>Empty polygon list is a no-op</li>
- * <li>Creates child nodes as needed</li>
- * </ul>
- *
* @param polygonList the polygons to add to this BSP tree
*/
- public void build(final List<CSGPolygon> polygonList) {
- // Base case: no polygons to add
+ public void build(final List<SolidPolygon> 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<CSGPolygon> frontList = new ArrayList<>();
- final List<CSGPolygon> backList = new ArrayList<>();
+ final List<SolidPolygon> frontList = new ArrayList<>();
+ final List<SolidPolygon> 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();
front.build(frontList);
}
- // Recursively build back subtree
if (!backList.isEmpty()) {
if (back == null) {
back = new CSGNode();
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;
/**
* Represents an infinite plane in 3D space using the Hesse normal form.
*
- * <p>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}</p>
- *
* <p>Planes are fundamental to BSP (Binary Space Partitioning) tree operations
- * in CSG. They divide 3D space into two half-spaces:</p>
- * <ul>
- * <li><b>Front half-space:</b> Points where {@code normal · point > w}</li>
- * <li><b>Back half-space:</b> Points where {@code normal · point < w}</li>
- * </ul>
- *
- * <p>Planes are used to:</p>
- * <ul>
- * <li>Define the surface orientation of {@link CSGPolygon} faces</li>
- * <li>Split polygons that cross BSP partition boundaries</li>
- * <li>Determine which side of a BSP node a polygon lies on</li>
- * </ul>
+ * in CSG. They divide 3D space into two half-spaces.</p>
*
- * @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.
- *
- * <p>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.</p>
*/
public static final double EPSILON = 0.01;
/**
* The unit normal vector perpendicular to the plane surface.
- *
- * <p>The direction of the normal determines which side is "front"
- * and which is "back". The front is the side the normal points toward.</p>
*/
public Point3D normal;
/**
* The signed distance from the origin to the plane along the normal.
- *
- * <p>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}</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.
*
- * <p>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:</p>
- * <ul>
- * <li>Counter-clockwise (CCW) winding → normal points toward viewer</li>
- * <li>Clockwise (CW) winding → normal points away from viewer</li>
- * </ul>
- *
* @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.
*
- * <p>The normal vector is cloned to avoid shared references.</p>
- *
* @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.
- *
- * <p>This effectively swaps the front and back half-spaces. After flipping:</p>
- * <ul>
- * <li>Points that were in front are now in back</li>
- * <li>Points that were in back are now in front</li>
- * <li>Coplanar points remain coplanar</li>
- * </ul>
- *
- * <p>Used during CSG operations when inverting solids (converting "inside"
- * to "outside" and vice versa).</p>
*/
public void flip() {
- normal = normal.negated();
- w = -w;
+ normal = normal.withNegated();
+ distance = -distance;
}
/**
* Splits a polygon by this plane, classifying and potentially dividing it.
*
- * <p>This is the core operation for BSP tree construction. The polygon is
- * classified based on where its vertices lie relative to the plane:</p>
- *
- * <h3>Classification types:</h3>
- * <ul>
- * <li><b>COPLANAR (0):</b> All vertices lie on the plane (within EPSILON)</li>
- * <li><b>FRONT (1):</b> All vertices are in the front half-space</li>
- * <li><b>BACK (2):</b> All vertices are in the back half-space</li>
- * <li><b>SPANNING (3):</b> Vertices are on both sides (polygon crosses the plane)</li>
- * </ul>
- *
- * <h3>Destination lists:</h3>
- * <ul>
- * <li><b>coplanarFront:</b> Coplanar polygons with same-facing normals</li>
- * <li><b>coplanarBack:</b> Coplanar polygons with opposite-facing normals</li>
- * <li><b>front:</b> Polygons entirely in front half-space</li>
- * <li><b>back:</b> Polygons entirely in back half-space</li>
- * </ul>
- *
- * <h3>Spanning polygon handling:</h3>
- * <p>When a polygon spans the plane, it is split into two polygons:</p>
- * <ol>
- * <li>Vertices on the front side become a new polygon (added to front list)</li>
- * <li>Vertices on the back side become a new polygon (added to back list)</li>
- * <li>Intersection points are computed and added to both polygons</li>
- * </ol>
- *
* @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<CSGPolygon> coplanarFront,
- final List<CSGPolygon> coplanarBack,
- final List<CSGPolygon> front,
- final List<CSGPolygon> back) {
+ public void splitPolygon(final SolidPolygon polygon,
+ final List<SolidPolygon> coplanarFront,
+ final List<SolidPolygon> coplanarBack,
+ final List<SolidPolygon> front,
+ final List<SolidPolygon> 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);
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:
break;
case SPANNING:
- final List<Vertex> f = new ArrayList<>();
- final List<Vertex> b = new ArrayList<>();
+ final List<Vertex> frontVertices = new ArrayList<>();
+ final List<Vertex> 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;
+++ /dev/null
-/*
- * 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.
- *
- * <p>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).</p>
- *
- * <p>The color is preserved through CSG operations - split polygons inherit
- * the color from their parent.</p>
- *
- * @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<Vertex> 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<Vertex> 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.
- *
- * <p>Clones all vertices and preserves the color.</p>
- *
- * @return a new CSGPolygon with cloned data
- */
- public CSGPolygon clone() {
- final List<Vertex> 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.
- *
- * <p>Reverses the vertex order and negates vertex normals.
- * Also flips the plane. Used during CSG operations when inverting solids.</p>
- */
- public void flip() {
- Collections.reverse(vertices);
- for (final Vertex v : vertices) {
- v.flip();
- }
- plane.flip();
- }
-}
\ No newline at end of file
* @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();
}
}
* <p>All mutation methods return {@code this} for fluent chaining:</p>
* <pre>{@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)
+ * }</pre>
+ *
+ * <p><b>Mutability convention:</b></p>
+ * <ul>
+ * <li><b>Imperative verbs</b> ({@code add}, {@code subtract}, {@code negate}, {@code multiply},
+ * {@code divide}) mutate this point and return {@code this}</li>
+ * <li><b>{@code with}-prefixed methods</b> ({@code withAdded}, {@code withSubtracted}, {@code withNegated},
+ * {@code withMultiplied}, {@code withDivided}) return a new point without modifying this one</li>
+ * </ul>
+ *
+ * <p><b>Warning:</b> This class is mutable with public fields. Clone before storing
+ * references that should not be shared:</p>
+ * <pre>{@code
+ * Point2D safeCopy = original.clone();
* }</pre>
*
* @see Point3D the 3D equivalent
/**
- * 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;
}
/**
- * 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;
}
/**
- * 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;
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.
*
", 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);
+ }
}
* <p>All mutation methods return {@code this} for fluent chaining:</p>
* <pre>{@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)
* <p><b>Common operations:</b></p>
* <pre>{@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);
* 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
* }</pre>
*
+ * <p><b>Mutability convention:</b></p>
+ * <ul>
+ * <li><b>Imperative verbs</b> ({@code add}, {@code subtract}, {@code negate}, {@code multiply},
+ * {@code divide}) mutate this point and return {@code this}</li>
+ * <li><b>{@code with}-prefixed methods</b> ({@code withAdded}, {@code withSubtracted}, {@code withNegated},
+ * {@code withMultiplied}, {@code withDivided}) return a new point without modifying this one</li>
+ * </ul>
+ *
* <p><b>Warning:</b> This class is mutable with public fields. Clone before storing
* references that should not be shared:</p>
* <pre>{@code
}
/**
- * 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;
}
/**
- * 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;
}
/**
- * 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;
}
/**
- * 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;
}
/**
- * 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;
return this;
}
- // ========== Non-mutating vector operations (return new Point3D) ==========
-
/**
* Computes the dot product of this vector with another.
*
}
/**
- * 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(
/**
* 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);
}
* 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);
}
/**
* 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);
}
}
+ /**
+ * Tests whether a point lies inside a triangle using integer coordinates.
+ *
+ * <p>This overload creates temporary Point2D objects for the vertices,
+ * suitable when the caller has pre-computed integer coordinates.</p>
+ *
+ * @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));
+ }
+
}
*/
public Rectangle(final double size) {
p2 = new Point2D(size / 2, size / 2);
- p1 = p2.clone().invert();
+ p1 = p2.clone().negate();
}
/**
if (currentSpeed <= SPEED_LIMIT)
return;
- movementVector.scaleDown(currentSpeed / SPEED_LIMIT);
+ movementVector.divide(currentSpeed / SPEED_LIMIT);
}
/**
*
* <p>Transformations are applied in order: rotation first, then translation.</p>
*
+ * <p><b>Mutability convention:</b></p>
+ * <ul>
+ * <li><b>Imperative verbs</b> ({@code set}, {@code setTranslation}, {@code transform})
+ * mutate this transform or the input point</li>
+ * <li><b>{@code with}-prefixed methods</b> ({@code withTransformed})
+ * return a new instance without modifying the original</li>
+ * </ul>
+ *
* @see Quaternion
* @see Point3D
*/
* 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;
}
/**
*/
public void flip() {
if (normal != null) {
- normal = normal.negated();
+ normal = normal.withNegated();
}
}
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);
}
}
* <pre>{@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();
* }</pre>
*
* <p><b>Important:</b> Always use this class instead of {@link java.awt.Color} when
/** Fully transparent (alpha = 0). */
public static final Color TRANSPARENT = new Color(0, 0, 0, 0);
+ /**
+ * Creates a color from a hexadecimal string.
+ *
+ * <p>Supported formats:</p>
+ * <ul>
+ * <li>{@code RGB} - 3 hex digits, fully opaque</li>
+ * <li>{@code RGBA} - 4 hex digits</li>
+ * <li>{@code RRGGBB} - 6 hex digits, fully opaque</li>
+ * <li>{@code RRGGBBAA} - 8 hex digits</li>
+ * </ul>
+ *
+ * @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.
*/
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.
*
* <p>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.</p>
*
* <p>During each render frame, the {@link #transform} method projects all vertices
*
* 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
* }
* }
* }</pre>
* 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}.
+ *
+ * <p>Stored as a mutable list to support CSG operations that modify
+ * polygon vertices in place (splitting, flipping).</p>
*/
- public final Vertex[] vertices;
+ public final List<Vertex> vertices;
/**
* Average Z-depth of this shape in screen space after transformation.
* @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();
}
* @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<Vertex> vertices) {
+ this.vertices = vertices;
shapeId = lastShapeId.getAndIncrement();
}
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);
}
}
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
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);
* @return the center position in world coordinates
*/
public Point3D getLocation() {
- return vertices[0].coordinate;
+ return vertices.get(0).coordinate;
}
}
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<LineInterpolator[]> 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.
* @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);
}
this.color = color;
this.width = width;
-
- for (int i = 0; i < lineInterpolators.length; i++)
- lineInterpolators[i] = new LineInterpolator();
-
}
/**
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;
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;
/**
* 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))
@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)) {
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);
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);
}
absoluteHeight = Math.abs(height);
}
+ /**
+ * Sets the two endpoints of this edge using integer coordinates.
+ *
+ * <p>This method creates new Point2D objects to avoid storing references to shared
+ * vertex data, which is essential for thread safety during parallel rendering.</p>
+ *
+ * @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);
+ }
+
}
*/
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;
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.
- * <p>
- * 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
- * <p>
- * The static drawPolygon method is designed for reuse by other polygon types.
+ * A solid-color convex polygon renderer supporting N vertices (N >= 3).
+ *
+ * <p>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.</p>
+ *
+ * <p><b>Rendering:</b></p>
+ * <ul>
+ * <li>Fan triangulation for N-vertex polygons (N-2 triangles)</li>
+ * <li>Scanline rasterization with alpha blending</li>
+ * <li>Backface culling and flat shading support</li>
+ * <li>Mouse interaction via point-in-polygon testing</li>
+ * </ul>
+ *
+ * <p><b>CSG Support:</b></p>
+ * <ul>
+ * <li>Lazy-computed plane for BSP operations</li>
+ * <li>{@link #flip()} for inverting polygon orientation</li>
+ * <li>{@link #deepClone()} for creating independent copies</li>
+ * </ul>
+ *
+ * <p><b>Usage examples:</b></p>
+ * <pre>{@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);
+ * }</pre>
+ *
+ * @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.
+ *
+ * <p>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.</p>
+ */
private static final ThreadLocal<LineInterpolator[]> 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.
+ *
+ * <p>Lazy-computed on first call to {@link #getPlane()}.</p>
+ */
+ 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<Point3D> 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<Point3D> 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<Point2D[]> 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<Point3D> 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.
+ *
+ * <p>This constructor is used for CSG operations where vertices already exist.</p>
+ *
+ * @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<Vertex> 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<Vertex> createVerticesFromPoints(final Point3D[] points) {
+ if (points == null || points.length < 3) {
+ return new ArrayList<>();
+ }
+ final List<Vertex> 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<Vertex> createVerticesFromPoints(final List<Point3D> points) {
+ if (points == null || points.size() < 3) {
+ return new ArrayList<>();
+ }
+ final List<Vertex> 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.
+ *
+ * <p>Computed from the first three vertices and cached for reuse.
+ * Used by CSG operations for BSP tree construction.</p>
+ *
+ * @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.
+ *
+ * <p>Reverses the vertex order and negates vertex normals.
+ * Also flips the cached plane if computed. Used during CSG operations
+ * when inverting solids.</p>
+ */
+ public void flip() {
+ Collections.reverse(vertices);
+ for (final Vertex v : vertices) {
+ v.flip();
+ }
+ if (planeComputed) {
+ plane.flip();
+ }
+ }
+
+ /**
+ * Creates a deep clone of this polygon.
+ *
+ * <p>Clones all vertices and preserves the color. Used by CSG operations
+ * to create independent copies before modification.</p>
+ *
+ * @return a new SolidPolygon with cloned data
+ */
+ public SolidPolygon deepClone() {
+ final List<Vertex> 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.
*
* @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);
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;
pixels[offset++] = (newR << 16) | (newG << 8) | newB;
}
}
-
}
/**
- * Renders a triangle with mouse interaction support and optional backface culling.
+ * Renders a triangle using scanline rasterization.
*
* <p>This static method handles:</p>
* <ul>
* <li>Rounding vertices to integer screen coordinates</li>
- * <li>Mouse hover detection via point-in-polygon test</li>
+ * <li>Mouse hover detection via point-in-triangle test</li>
* <li>Viewport clipping</li>
* <li>Scanline rasterization with alpha blending</li>
* </ul>
* @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;
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.
- *
- * <p>When enabled, polygons facing away from the camera (determined by
- * screen-space winding order) are not rendered.</p>
- *
- * @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;
}
/**
- * 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.
*
- * <p>This method performs:</p>
- * <ul>
- * <li>Backface culling check (if enabled)</li>
- * <li>Flat shading calculation (if lighting is enabled)</li>
- * <li>Triangle rasterization using the static drawPolygon method</li>
- * </ul>
+ * @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
*/
/**
- * Solid-color triangle rendering with scanline rasterization.
+ * Solid-color polygon rendering with scanline rasterization.
*
- * <p>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.</p>
+ * <p>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.</p>
*
* <p>Key classes:</p>
* <ul>
- * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon} - The solid triangle shape</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon} - Unified polygon for rendering and CSG</li>
* <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.LineInterpolator} - Edge interpolation for scanlines</li>
* </ul>
*
/**
* Sets the screen and texture coordinates for this edge.
*
+ * <p>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.</p>
+ *
* @param onScreenPoint1 the first screen-space endpoint
* @param onScreenPoint2 the second screen-space endpoint
* @param texturePoint1 the texture coordinate for the first endpoint
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;
+++ /dev/null
-/*
- * 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.
- *
- * <p>This class renders triangles with UV-mapped textures. For large triangles,
- * the rendering may be sliced into smaller pieces for better perspective correction.</p>
- *
- * <p><b>Perspective-correct texture rendering:</b></p>
- * <ul>
- * <li>Small polygons are rendered without perspective correction</li>
- * <li>Larger polygons are sliced into smaller pieces for accurate perspective</li>
- * </ul>
- *
- * @see Texture
- * @see Vertex#textureCoordinate
- */
-public class TexturedPolygon extends AbstractCoordinateShape {
-
- private static final ThreadLocal<PolygonBorderInterpolator[]> 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.
- *
- * <p>This method performs:</p>
- * <ul>
- * <li>Backface culling check (if enabled)</li>
- * <li>Mouse interaction detection</li>
- * <li>Mipmap level selection based on screen coverage</li>
- * <li>Scanline rasterization with texture sampling</li>
- * </ul>
- *
- * @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);
- });
- }
-
-}
--- /dev/null
+/*
+ * 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.
+ *
+ * <p>This class renders triangles with UV-mapped textures. For large triangles,
+ * the rendering may be sliced into smaller pieces for better perspective correction.</p>
+ *
+ * <p><b>Perspective-correct texture rendering:</b></p>
+ * <ul>
+ * <li>Small triangles are rendered without perspective correction</li>
+ * <li>Larger triangles are sliced into smaller pieces for accurate perspective</li>
+ * </ul>
+ *
+ * @see Texture
+ * @see Vertex#textureCoordinate
+ */
+public class TexturedTriangle extends AbstractCoordinateShape {
+
+ private static final ThreadLocal<PolygonBorderInterpolator[]> 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.
+ *
+ * <p>This method performs:</p>
+ * <ul>
+ * <li>Backface culling check (if enabled)</li>
+ * <li>Mouse interaction detection</li>
+ * <li>Mipmap level selection based on screen coverage</li>
+ * <li>Scanline rasterization with texture sampling</li>
+ * </ul>
+ *
+ * @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);
+ });
+ }
+
+}
/**
* Textured triangle rendering with perspective-correct UV mapping.
*
- * <p>Textured polygons apply 2D textures to 3D triangles using UV coordinates.
- * Large polygons may be sliced into smaller pieces for accurate perspective correction.</p>
+ * <p>Textured triangles apply 2D textures to 3D triangles using UV coordinates.
+ * Large triangles may be sliced into smaller pieces for accurate perspective correction.</p>
*
* <p>Key classes:</p>
* <ul>
- * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedPolygon} - The textured triangle shape</li>
+ * <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.TexturedTriangle} - The textured triangle shape</li>
* <li>{@link eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.texturedpolygon.PolygonBorderInterpolator} - Edge interpolation with UVs</li>
* </ul>
*
- * @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
*/
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);
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),
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);
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),
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) {
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;
* A rectangular shape with texture mapping, composed of two textured triangles.
*
* <p>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.</p>
*
* shapeCollection.addShape(rect);
* }</pre>
*
- * @see TexturedPolygon
+ * @see TexturedTriangle
* @see Texture
* @see AbstractCompositeShape
*/
*
* <p>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.</p>
*
* @param width the width of the rectangle in world units
- final TexturedPolygon texturedPolygon1 = new TexturedPolygon(
+ final TexturedTriangle texturedPolygon1 = new TexturedTriangle(
new Vertex(topLeft, textureTopLeft),
new Vertex(topRight, textureTopRight),
new Vertex(bottomRight, textureBottomRight), texture);
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);
*/
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;
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;
/**
}
/**
- * Extracts all SolidPolygon triangles from this composite shape.
+ * Extracts all SolidPolygon instances from this composite shape.
*
* <p>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}.</p>
+ * SolidPolygon instances. Used for CSG operations where polygons
+ * are needed directly without conversion.</p>
*
- * <p><b>Example:</b></p>
- * <pre>{@code
- * SolidPolygonCube cube = new SolidPolygonCube(new Point3D(0, 0, 0), 50, Color.RED);
- * List<SolidPolygon> triangles = cube.extractSolidPolygons();
- * CSG csg = CSG.fromSolidPolygons(triangles);
- * }</pre>
- *
- * @return list of all SolidPolygon sub-shapes
+ * @return list of SolidPolygon instances from this shape hierarchy
*/
public List<SolidPolygon> extractSolidPolygons() {
final List<SolidPolygon> result = new ArrayList<>();
*
* @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) {
// 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.
+ *
+ * <p>This shape's SolidPolygon children are replaced with the union result.
+ * Non-SolidPolygon children from both shapes are preserved and combined.</p>
+ *
+ * <p><b>CSG Operation:</b> Union combines two shapes into one, keeping all
+ * geometry from both. Uses BSP tree algorithms for robust boolean operations.</p>
+ *
+ * <p><b>Child handling:</b></p>
+ * <ul>
+ * <li>SolidPolygon children from both shapes → replaced with union result</li>
+ * <li>Non-SolidPolygon children from this shape → preserved</li>
+ * <li>Non-SolidPolygon children from other shape → added to this shape</li>
+ * <li>Nested AbstractCompositeShape children → preserved unchanged (not recursively processed)</li>
+ * </ul>
+ *
+ * @param other the shape to union with
+ * @see #subtract(AbstractCompositeShape)
+ * @see #intersect(AbstractCompositeShape)
+ */
+ public void union(final AbstractCompositeShape other) {
+ final List<SolidPolygon> selfPolygons = clonePolygons(extractSolidPolygons());
+ final List<SolidPolygon> 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.
+ *
+ * <p>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.</p>
+ *
+ * <p><b>CSG Operation:</b> Subtract removes the volume of the second shape
+ * from the first shape. Useful for creating holes, cavities, and cutouts.</p>
+ *
+ * <p><b>Child handling:</b></p>
+ * <ul>
+ * <li>SolidPolygon children from this shape → replaced with difference result</li>
+ * <li>Non-SolidPolygon children from this shape → preserved</li>
+ * <li>All children from other shape → discarded (other is just a cutter)</li>
+ * <li>Nested AbstractCompositeShape children → preserved unchanged</li>
+ * </ul>
+ *
+ * @param other the shape to subtract (the cutter)
+ * @see #union(AbstractCompositeShape)
+ * @see #intersect(AbstractCompositeShape)
+ */
+ public void subtract(final AbstractCompositeShape other) {
+ final List<SolidPolygon> selfPolygons = clonePolygons(extractSolidPolygons());
+ final List<SolidPolygon> 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.
+ *
+ * <p>This shape's SolidPolygon children are replaced with the intersection result.
+ * Only the overlapping volume between the two shapes remains.</p>
+ *
+ * <p><b>CSG Operation:</b> Intersect keeps only the volume where both shapes
+ * overlap. Useful for creating shapes constrained by multiple boundaries.</p>
+ *
+ * <p><b>Child handling:</b></p>
+ * <ul>
+ * <li>SolidPolygon children from this shape → replaced with intersection result</li>
+ * <li>Non-SolidPolygon children from this shape → preserved</li>
+ * <li>All children from other shape → discarded</li>
+ * <li>Nested AbstractCompositeShape children → preserved unchanged</li>
+ * </ul>
+ *
+ * @param other the shape to intersect with
+ * @see #union(AbstractCompositeShape)
+ * @see #subtract(AbstractCompositeShape)
+ */
+ public void intersect(final AbstractCompositeShape other) {
+ final List<SolidPolygon> selfPolygons = clonePolygons(extractSolidPolygons());
+ final List<SolidPolygon> 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.
+ *
+ * <p>CSG operations modify polygons in-place via BSP tree operations.
+ * Cloning ensures the original polygon data is preserved.</p>
+ *
+ * @param polygons the polygons to clone
+ * @return a new list containing deep clones of all polygons
+ */
+ private List<SolidPolygon> clonePolygons(final List<SolidPolygon> polygons) {
+ final List<SolidPolygon> 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.
+ *
+ * <p>Preserves all non-SolidPolygon children (Lines, nested composites, etc.).
+ * Optionally carries over non-SolidPolygon children from another shape.</p>
+ *
+ * @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<SolidPolygon> newPolygons,
+ final AbstractCompositeShape other,
+ final boolean carryOtherNonPolygons) {
+ // Remove all direct SolidPolygon children from this shape
+ final Iterator<SubShape> 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;
}
/**
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());
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.
*
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));
}
/**
}
// 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
}
// 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);
}
}
// 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);
}
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);
}
* A renderable mesh composed of SolidPolygon triangles.
*
* <p>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.</p>
+ * It can be constructed from any source of triangles, such as procedural
+ * geometry generation or loaded mesh data.</p>
*
* <p><b>Usage:</b></p>
* <pre>{@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<SolidPolygon> 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));
* }</pre>
*
* @see SolidPolygon the triangle type for rendering
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);
}
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);
}
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).
*
* <p>The box is defined by two diagonally opposite corner points in 3D space.
*
* @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();
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);
}
*/
/**
- * Solid composite shapes built from SolidPolygon primitives.
+ * Solid composite shapes built from SolidTriangle primitives.
*
* <p>These shapes render as filled surfaces with optional flat shading.
* Useful for creating opaque 3D objects like boxes, spheres, and cylinders.</p>
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;
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);
}
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);
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)
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;
* to break large composite shapes into appropriately-sized sub-polygons.</p>
*
* @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 {
/**
* Result of slicing.
*/
- private final List<TexturedPolygon> result = new ArrayList<>();
+ private final List<TexturedTriangle> result = new ArrayList<>();
/**
* Creates a new slicer with the specified maximum edge length.
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);
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);
/**
* 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<TexturedPolygon> getResult() {
+ public List<TexturedTriangle> getResult() {
return result;
}
*
* @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);
}
* }</pre>
*
* @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 {