From 428d163d0b3e9d09ba42ed27862921ca1ef39cfa Mon Sep 17 00:00:00 2001 From: Svjatoslav Agejenko Date: Sat, 28 Mar 2026 14:47:12 +0200 Subject: [PATCH] feat(csg): add constructive solid geometry with BSP tree boolean operations Add a complete CSG system that enables boolean operations (union, subtract, intersect) on 3D solids using BSP trees. The implementation includes: - CSG class with union/subtract/intersect/inverse operations - CSGNode BSP tree for spatial partitioning and polygon clipping - CSGPlane for polygon classification and splitting - CSGPolygon N-gon type for BSP operations - PolygonType enum for vertex classification Integration with existing shapes via: - extractSolidPolygons() method on AbstractCompositeShape - SolidPolygonMesh composite for rendering CSG results - CSG.fromCompositeShape() factory for converting existing primitives Also includes supporting refactorings: rename coordinates field to vertices for consistency, simplified constructors for arrow shapes with auto-calculated tip dimensions, and Point3D/Vertex enhancements for vector math. --- TODO.org | 14 +- doc/Axis.png | Bin 0 -> 74804 bytes .../java/eu/svjatoslav/sixth/e3d/csg/CSG.java | 368 ++++++++++++++++++ .../eu/svjatoslav/sixth/e3d/csg/CSGNode.java | 359 +++++++++++++++++ .../eu/svjatoslav/sixth/e3d/csg/CSGPlane.java | 239 ++++++++++++ .../svjatoslav/sixth/e3d/csg/CSGPolygon.java | 91 +++++ .../svjatoslav/sixth/e3d/csg/PolygonType.java | 56 +++ .../sixth/e3d/geometry/Point3D.java | 111 ++++++ .../eu/svjatoslav/sixth/e3d/math/Vertex.java | 75 +++- .../shapes/AbstractCoordinateShape.java | 22 +- .../raster/shapes/basic/Billboard.java | 6 +- .../raster/shapes/basic/line/Line.java | 72 ++-- .../basic/solidpolygon/SolidPolygon.java | 49 ++- .../texturedpolygon/TexturedPolygon.java | 50 ++- .../composite/ForwardOrientedTextBlock.java | 15 +- .../base/AbstractCompositeShape.java | 31 ++ .../composite/solid/SolidPolygonArrow.java | 68 ++-- .../composite/solid/SolidPolygonMesh.java | 65 ++++ .../composite/textcanvas/CanvasCharacter.java | 34 +- .../composite/wireframe/WireframeArrow.java | 79 +++- .../e3d/renderer/raster/slicer/Slicer.java | 24 +- 21 files changed, 1647 insertions(+), 181 deletions(-) create mode 100644 doc/Axis.png create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/csg/CSG.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPolygon.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/csg/PolygonType.java create mode 100644 src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java diff --git a/TODO.org b/TODO.org index 2327fa0..c904218 100644 --- a/TODO.org +++ b/TODO.org @@ -16,6 +16,8 @@ sources. Make dedicated tutorial about shading algorithm with screenshot and what are available parameters. +** Document boolean operations + * Add 3D mouse support :PROPERTIES: :CUSTOM_ID: add-3d-mouse-support @@ -29,6 +31,7 @@ what are available parameters. :PROPERTIES: :CUSTOM_ID: add-more-math-formula-examples :END: + ** Allow manual thread count specification in performance test demo :PROPERTIES: :CUSTOM_ID: allow-manual-thread-count-specification @@ -80,6 +83,7 @@ the sweet spot. :PROPERTIES: :CUSTOM_ID: add-polygon-reduction-lod :END: + ** Add object fading based on view distance :PROPERTIES: :CUSTOM_ID: add-object-fading-view-distance @@ -90,6 +94,7 @@ Goal: make it easier to distinguish nearby objects from distant ones. :PROPERTIES: :CUSTOM_ID: add-csg-support :END: + ** Add shadow casting :PROPERTIES: :CUSTOM_ID: add-shadow-casting @@ -118,10 +123,12 @@ shadows. image resolution if needed to maintain desired FPS. ** Explore possibility for implementing better perspective correct textured polygons -** Add arrow shape: cone + cylinder + ** Add X, Y, Z axis indicators Will use different colored arrows + text label +** Add collision detection (physics engine) + * Add clickable vertexes :PROPERTIES: :CUSTOM_ID: add-clickable-vertexes @@ -148,8 +155,3 @@ Add formula textbox display on top of 3D graph. http://blog.rogach.org/2015/08/how-to-create-your-own-simple-3d-render.html + Improve triangulation. Read: https://ianthehenry.com/posts/delaunay/ - -** Fix camera rotation for voxel raytracer -:PROPERTIES: -:CUSTOM_ID: fix-camera-rotation-voxel-raytracer -:END: diff --git a/doc/Axis.png b/doc/Axis.png new file mode 100644 index 0000000000000000000000000000000000000000..d028e6169a6d0c9868b473eeefdeb6dfdd211c16 GIT binary patch literal 74804 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 WP+WWc@{f27fQ4Wt6<13m>AWBpd0aGC0^c% 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 Hd&K=8S+WHD literal 0 HcmV?d00001 diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSG.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSG.java new file mode 100644 index 0000000..2d26a98 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSG.java @@ -0,0 +1,368 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.csg; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +import eu.svjatoslav.sixth.e3d.math.Vertex; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonMesh; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a solid for Constructive Solid Geometry (CSG) operations. + * + *

CSG allows combining 3D shapes using boolean operations:

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

Usage example:

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

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

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

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

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

The result contains all points that are in either solid.

+ * + *

Algorithm:

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

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

+ * + *

Algorithm:

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

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

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

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

+ * + *

Algorithm:

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

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

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

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

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

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

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

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

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

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

+ * + * @param polygon the polygon to triangulate (may have 3+ vertices) + * @param color the color to apply to all resulting triangles + * @param triangles the list to add the resulting SolidPolygon triangles to + */ + private void triangulatePolygon(final CSGPolygon polygon, final Color color, + final List triangles) { + final int vertexCount = polygon.vertices.size(); + + // Skip degenerate polygons (less than 3 vertices cannot form a triangle) + if (vertexCount < 3) { + return; + } + + // Use the first vertex as the "pivot" of the fan + final Point3D v0 = polygon.vertices.get(0).coordinate; + + // Create triangles by connecting v0 to each consecutive pair of vertices + // For a polygon with vertices [v0, v1, v2, v3], we create: + // - Triangle 1: v0, v1, v2 (i=1) + // - Triangle 2: v0, v2, v3 (i=2) + for (int i = 1; i < vertexCount - 1; i++) { + final Point3D v1 = polygon.vertices.get(i).coordinate; + final Point3D v2 = polygon.vertices.get(i + 1).coordinate; + + // Clone the points to avoid sharing references with the original polygon + final SolidPolygon triangle = new SolidPolygon( + new Point3D(v0), + new Point3D(v1), + new Point3D(v2), + color + ); + + triangles.add(triangle); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java new file mode 100644 index 0000000..0766122 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGNode.java @@ -0,0 +1,359 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.csg; + +import java.util.ArrayList; +import java.util.List; + +/** + * A node in a Binary Space Partitioning (BSP) tree used for CSG operations. + * + *

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

+ * + *

BSP Tree Structure:

+ *
+ *                 [Node: plane P]
+ *                /               \
+ *        [Front subtree]     [Back subtree]
+ *     (same side as P's     (opposite side
+ *        normal)             of P's normal)
+ * 
+ * + *

Key Properties:

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

CSG Algorithm Overview:

+ *

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

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

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

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

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

+ * + *

Null for leaf nodes (empty subtrees).

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

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

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

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

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

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

+ */ + public CSGNode() { + } + + /** + * Creates a BSP tree from a list of polygons. + * + *

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

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

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

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

This operation is fundamental to CSG subtraction and intersection:

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

After inversion:

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

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

+ */ + public void invert() { + // Flip all polygons at this node + for (final CSGPolygon polygon : polygons) { + polygon.flip(); + } + + // Flip the partitioning plane + if (plane != null) { + plane.flip(); + } + + // Recursively invert child subtrees + if (front != null) { + front.invert(); + } + if (back != null) { + 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. + * + *

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

+ * + *

Algorithm:

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

This is used during CSG operations to remove overlapping geometry.

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

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

+ * + *

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

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

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

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

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

+ * + *

Algorithm:

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

Calling Conventions:

+ *
    + *
  • Can be called multiple times to add more polygons to an existing tree
  • + *
  • Empty polygon list is a no-op
  • + *
  • Creates child nodes as needed
  • + *
+ * + * @param polygonList the polygons to add to this BSP tree + */ + public void build(final List polygonList) { + // Base case: no polygons to add + 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(); + } + + // Classify each polygon relative to this node's plane + final List frontList = new ArrayList<>(); + final List backList = new ArrayList<>(); + + for (final CSGPolygon polygon : polygonList) { + // Split the polygon and distribute to appropriate lists: + // - coplanarFront/coplanarBack → this node's polygons list + // - front → frontList (for front subtree) + // - back → backList (for back subtree) + 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(); + } + back.build(backList); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java new file mode 100644 index 0000000..473608b --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPlane.java @@ -0,0 +1,239 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; + +/** + * Represents an infinite plane in 3D space using the Hesse normal form. + * + *

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

+ * + *

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

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

Planes are used to:

+ *
    + *
  • Define the surface orientation of {@link CSGPolygon} faces
  • + *
  • Split polygons that cross BSP partition boundaries
  • + *
  • Determine which side of a BSP node a polygon lies on
  • + *
+ * + * @see CSGPolygon polygons that reference their containing plane + * @see CSGNode BSP tree nodes that use planes for spatial partitioning + */ +public class CSGPlane { + + /** + * Epsilon value used for floating-point comparisons. + * + *

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

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

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

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

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

+ */ + public double w; + + /** + * 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 + */ + public CSGPlane(final Point3D normal, final double w) { + this.normal = normal; + this.w = w; + } + + /** + * Creates a plane from three non-collinear points. + * + *

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

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

The normal vector is cloned to avoid shared references.

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

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

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

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

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

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

+ * + *

Classification types:

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

Destination lists:

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

Spanning polygon handling:

+ *

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

+ *
    + *
  1. Vertices on the front side become a new polygon (added to front list)
  2. + *
  3. Vertices on the back side become a new polygon (added to back list)
  4. + *
  5. Intersection points are computed and added to both polygons
  6. + *
+ * + * @param polygon the polygon to classify and potentially split + * @param coplanarFront list to receive coplanar polygons with same-facing normals + * @param coplanarBack list to receive coplanar polygons with opposite-facing normals + * @param front list to receive polygons in the front half-space + * @param back list to receive polygons in the back half-space + */ + public void splitPolygon(final CSGPolygon polygon, + final List coplanarFront, + final List coplanarBack, + final List front, + final List back) { + + PolygonType polygonType = PolygonType.COPLANAR; + final PolygonType[] types = new PolygonType[polygon.vertices.size()]; + + for (int i = 0; i < polygon.vertices.size(); i++) { + final Vertex v = polygon.vertices.get(i); + final double t = normal.dot(v.coordinate) - w; + final PolygonType type = (t < -EPSILON) ? PolygonType.BACK + : (t > EPSILON) ? PolygonType.FRONT : PolygonType.COPLANAR; + polygonType = polygonType.combine(type); + types[i] = type; + } + + switch (polygonType) { + case COPLANAR: + ((normal.dot(polygon.plane.normal) > 0) ? coplanarFront : coplanarBack).add(polygon); + break; + + case FRONT: + front.add(polygon); + break; + + case BACK: + back.add(polygon); + break; + + case SPANNING: + final List f = new ArrayList<>(); + final List b = new ArrayList<>(); + + for (int i = 0; i < polygon.vertices.size(); i++) { + final int j = (i + 1) % polygon.vertices.size(); + 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); + } + if (ti.isBack()) { + b.add(ti == PolygonType.COPLANAR ? vi.clone() : vi); + } + 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 Vertex v = vi.interpolate(vj, t); + f.add(v); + b.add(v.clone()); + } + } + + if (f.size() >= 3) { + final CSGPolygon frontPoly = new CSGPolygon(f, polygon.color); + front.add(frontPoly); + } + if (b.size() >= 3) { + final CSGPolygon backPoly = new CSGPolygon(b, polygon.color); + back.add(backPoly); + } + break; + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPolygon.java new file mode 100644 index 0000000..9ba8ceb --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/csg/CSGPolygon.java @@ -0,0 +1,91 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.csg; + +import eu.svjatoslav.sixth.e3d.math.Vertex; +import eu.svjatoslav.sixth.e3d.renderer.raster.Color; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * An N-gon polygon used for CSG BSP tree operations. + * + *

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

+ * + *

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

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

Clones all vertices and preserves the color.

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

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

+ */ + public void flip() { + Collections.reverse(vertices); + for (final Vertex v : vertices) { + v.flip(); + } + plane.flip(); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/csg/PolygonType.java b/src/main/java/eu/svjatoslav/sixth/e3d/csg/PolygonType.java new file mode 100644 index 0000000..c603a59 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/csg/PolygonType.java @@ -0,0 +1,56 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.csg; + +/** + * Classification of a polygon's position relative to a plane. + * Used in CSG operations to determine how polygons should be split. + */ +enum PolygonType { + /** Polygon lies on the plane. */ + COPLANAR, + /** Polygon is entirely in front of the plane. */ + FRONT, + /** Polygon is entirely behind the plane. */ + BACK, + /** Polygon straddles the plane (vertices on both sides). */ + SPANNING; + + /** + * Combines this type with another to compute the aggregate classification. + * When vertices are on both sides of a plane, the result is SPANNING. + * + * @param other the other polygon type to combine with + * @return the combined classification + */ + PolygonType combine(final PolygonType other) { + if (this == other || other == COPLANAR) { + return this; + } + if (this == COPLANAR) { + return other; + } + // FRONT + BACK = SPANNING + return SPANNING; + } + + /** + * Checks if this type represents a vertex in front of the plane. + * + * @return true if FRONT or COPLANAR (treated as front for classification) + */ + boolean isFront() { + return this == FRONT || this == COPLANAR; + } + + /** + * Checks if this type represents a vertex behind the plane. + * + * @return true if BACK or COPLANAR (treated as back for classification) + */ + boolean isBack() { + return this == BACK || this == COPLANAR; + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java index 7cfd8e0..c4e6b52 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/geometry/Point3D.java @@ -432,4 +432,115 @@ public class Point3D implements Cloneable { return this; } + // ========== Non-mutating vector operations (return new Point3D) ========== + + /** + * Computes the dot product of this vector with another. + * + * @param other the other vector + * @return the dot product (scalar) + */ + public double dot(final Point3D other) { + return x * other.x + y * other.y + z * other.z; + } + + /** + * Computes the cross product of this vector with another. + * Returns a new vector perpendicular to both input vectors. + * + * @param other the other vector + * @return a new Point3D representing the cross product + */ + public Point3D cross(final Point3D other) { + return new Point3D( + y * other.z - z * other.y, + z * other.x - x * other.z, + x * other.y - y * other.x + ); + } + + /** + * Returns a new point that is the sum of this point and another. + * Neither point is modified. + * + * @param other the point to add + * @return a new Point3D representing the sum + */ + public Point3D plus(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. + * + * @param other the point to subtract + * @return a new Point3D representing the difference + */ + public Point3D minus(final Point3D other) { + return new Point3D(x - other.x, y - other.y, z - other.z); + } + + /** + * Returns a new point with negated coordinates. + * This point is not modified. + * + * @return a new Point3D with negated coordinates + */ + public Point3D negated() { + return new Point3D(-x, -y, -z); + } + + /** + * Returns a new unit vector (normalized) in the same direction. + * This point is not modified. + * + * @return a new Point3D with unit length + */ + public Point3D unit() { + final double len = getVectorLength(); + if (len == 0) { + return new Point3D(0, 0, 0); + } + return new Point3D(x / len, y / len, z / len); + } + + /** + * Returns a new point that is a linear interpolation between this point and another. + * When t=0, returns this point. When t=1, returns the other point. + * + * @param other the other point + * @param t the interpolation parameter (0 to 1) + * @return a new Point3D representing the interpolated position + */ + public Point3D lerp(final Point3D other, final double t) { + return new Point3D( + x + (other.x - x) * t, + y + (other.y - y) * t, + z + (other.z - z) * t + ); + } + + /** + * Returns a new point with coordinates multiplied by a factor. + * This point is not modified. Unlike {@link #scaleUp}, this returns a new instance. + * + * @param factor the scaling factor + * @return a new scaled Point3D + */ + public Point3D times(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. + * + * @param factor the divisor + * @return a new scaled Point3D + */ + public Point3D dividedBy(final double factor) { + return new Point3D(x / factor, y / factor, z / factor); + } + } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java index 20cea76..ba63d6c 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/math/Vertex.java @@ -58,7 +58,14 @@ public class Vertex { /** * Texture coordinate for UV mapping (optional). */ - public Point2D textureCoordinate; // TODO: is this proper term ? + public Point2D textureCoordinate; + + /** + * Normal vector for this vertex (optional). + * Used by CSG operations for smooth interpolation during polygon splitting. + * Null for non-CSG usage; existing rendering code ignores this field. + */ + public Point3D normal; /** @@ -76,20 +83,20 @@ public class Vertex { /** * Creates a vertex at the specified position with no texture coordinate. * - * @param location the 3D position of this vertex + * @param coordinate the 3D position of this vertex */ - public Vertex(final Point3D location) { - this(location, null); + public Vertex(final Point3D coordinate) { + this(coordinate, null); } /** * Creates a vertex at the specified position with an optional texture coordinate. * - * @param location the 3D position of this vertex + * @param coordinate the 3D position of this vertex * @param textureCoordinate the UV texture coordinate, or {@code null} for none */ - public Vertex(final Point3D location, Point2D textureCoordinate) { - coordinate = location; + public Vertex(final Point3D coordinate, final Point2D textureCoordinate) { + this.coordinate = coordinate; transformedCoordinate = new Point3D(); onScreenCoordinate = new Point2D(); this.textureCoordinate = textureCoordinate; @@ -120,4 +127,58 @@ public class Vertex { onScreenCoordinate.y = ((transformedCoordinate.y / transformedCoordinate.z) * renderContext.projectionScale); onScreenCoordinate.add(renderContext.centerCoordinate); } + + // ========== CSG support methods ========== + + /** + * Creates a deep copy of this vertex. + * Clones the coordinate, normal (if present), and texture coordinate (if present). + * The transformedCoordinate and onScreenCoordinate are not cloned (they are computed per-frame). + * + * @return a new Vertex with cloned data + */ + public Vertex clone() { + final Vertex result = new Vertex(new Point3D(coordinate), + textureCoordinate != null ? new Point2D(textureCoordinate) : null); + if (normal != null) { + result.normal = new Point3D(normal); + } + return result; + } + + /** + * Flips the orientation of this vertex by negating the normal vector. + * Called when the orientation of a polygon is flipped during CSG operations. + * If normal is null, this method does nothing. + */ + public void flip() { + if (normal != null) { + normal = normal.negated(); + } + } + + /** + * Creates a new vertex between this vertex and another by linearly interpolating + * all properties using parameter t. + * + *

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

+ * + * @param other the other vertex to interpolate towards + * @param t the interpolation parameter (0 = this vertex, 1 = other vertex) + * @return a new Vertex representing the interpolated position + */ + public Vertex interpolate(final Vertex other, final double t) { + final Vertex result = new Vertex( + coordinate.lerp(other.coordinate, t), + (textureCoordinate != null && other.textureCoordinate != null) + ? new Point2D( + textureCoordinate.x + (other.textureCoordinate.x - textureCoordinate.x) * t, + textureCoordinate.y + (other.textureCoordinate.y - textureCoordinate.y) * t) + : null + ); + if (normal != null && other.normal != null) { + result.normal = normal.lerp(other.normal, t); + } + return result; + } } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java index a75d798..3112fde 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/AbstractCoordinateShape.java @@ -63,7 +63,7 @@ public abstract class AbstractCoordinateShape extends AbstractShape { * Each vertex contains both the original world-space coordinate and * a transformed screen-space coordinate computed during {@link #transform}. */ - public final Vertex[] coordinates; + public final Vertex[] vertices; /** * Average Z-depth of this shape in screen space after transformation. @@ -76,12 +76,12 @@ public abstract class AbstractCoordinateShape extends AbstractShape { * Creates a shape with the specified number of vertices, each initialized * to the origin (0, 0, 0). * - * @param pointsCount the number of vertices in this shape + * @param vertexCount the number of vertices in this shape */ - public AbstractCoordinateShape(final int pointsCount) { - coordinates = new Vertex[pointsCount]; - for (int i = 0; i < pointsCount; i++) - coordinates[i] = new Vertex(); + public AbstractCoordinateShape(final int vertexCount) { + vertices = new Vertex[vertexCount]; + for (int i = 0; i < vertexCount; i++) + vertices[i] = new Vertex(); shapeId = lastShapeId.getAndIncrement(); } @@ -89,10 +89,10 @@ public abstract class AbstractCoordinateShape extends AbstractShape { /** * Creates a shape from the given vertices. * - * @param vertexes the vertices defining this shape's geometry + * @param vertices the vertices defining this shape's geometry */ - public AbstractCoordinateShape(final Vertex... vertexes) { - coordinates = vertexes; + public AbstractCoordinateShape(final Vertex... vertices) { + this.vertices = vertices; shapeId = lastShapeId.getAndIncrement(); } @@ -132,7 +132,7 @@ public abstract class AbstractCoordinateShape extends AbstractShape { double accumulatedZ = 0; boolean paint = true; - for (final Vertex geometryPoint : coordinates) { + for (final Vertex geometryPoint : vertices) { geometryPoint.calculateLocationRelativeToViewer(transforms, renderingContext); accumulatedZ += geometryPoint.transformedCoordinate.z; @@ -142,7 +142,7 @@ public abstract class AbstractCoordinateShape extends AbstractShape { } if (paint) { - onScreenZ = accumulatedZ / coordinates.length; + onScreenZ = accumulatedZ / vertices.length; aggregator.queueShapeForRendering(this); } } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java index ec38e5b..974e285 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/Billboard.java @@ -77,7 +77,7 @@ public class Billboard extends AbstractCoordinateShape { public void paint(final RenderingContext targetRenderingArea) { // distance from camera/viewer to center of the texture - final double z = coordinates[0].transformedCoordinate.z; + final double z = vertices[0].transformedCoordinate.z; // compute forward oriented texture visible distance from center final double visibleHorizontalDistanceFromCenter = (targetRenderingArea.width @@ -92,7 +92,7 @@ public class Billboard extends AbstractCoordinateShape { final TextureBitmap textureBitmap = texture.getZoomedBitmap(zoom); - final Point2D onScreenCoordinate = coordinates[0].onScreenCoordinate; + final Point2D onScreenCoordinate = vertices[0].onScreenCoordinate; // compute Y final int onScreenUncappedYStart = (int) (onScreenCoordinate.y - visibleVerticalDistanceFromCenter); @@ -219,7 +219,7 @@ public class Billboard extends AbstractCoordinateShape { * @return the center position in world coordinates */ public Point3D getLocation() { - return coordinates[0].coordinate; + return vertices[0].coordinate; } } diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java index 72c38fa..4b53da1 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/line/Line.java @@ -22,9 +22,9 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape; *

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

* Note: The width is scaled by the LINE_WIDTH_MULTIPLIER and adjusted based on * the distance from the viewer (z-coordinate) to maintain a consistent visual size. @@ -52,8 +52,8 @@ public class Line extends AbstractCoordinateShape { * @param parentLine the line to copy */ public Line(final Line parentLine) { - this(parentLine.coordinates[0].coordinate.clone(), - parentLine.coordinates[1].coordinate.clone(), + this(parentLine.vertices[0].coordinate.clone(), + parentLine.vertices[1].coordinate.clone(), new Color(parentLine.color), parentLine.width); } @@ -164,8 +164,8 @@ public class Line extends AbstractCoordinateShape { private void drawSinglePixelHorizontalLine(final RenderingContext buffer, final int alpha) { - final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate; - final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate; + final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate; + final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate; int xStart = (int) onScreenPoint1.x; int xEnd = (int) onScreenPoint2.x; @@ -202,21 +202,21 @@ public class Line extends AbstractCoordinateShape { final int y = yBase + ((relativeX * lineHeight) / lineWidth); if ((y >= buffer.renderMinY) && (y < buffer.renderMaxY)) { - if ((y >= 0) && (y < buffer.height)) { - int offset = (y * buffer.width) + x; + if ((y >= 0) && (y < buffer.height)) { + int offset = (y * buffer.width) + x; - final int dest = pixels[offset]; - final int destR = (dest >> 16) & 0xff; - final int destG = (dest >> 8) & 0xff; - final int destB = dest & 0xff; + final int dest = pixels[offset]; + final int destR = (dest >> 16) & 0xff; + final int destG = (dest >> 8) & 0xff; + final int destB = dest & 0xff; - final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8; - final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8; - final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8; + final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8; + final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8; + final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8; - pixels[offset] = (newR << 16) | (newG << 8) | newB; - } + pixels[offset] = (newR << 16) | (newG << 8) | newB; } + } } } @@ -232,8 +232,8 @@ public class Line extends AbstractCoordinateShape { private void drawSinglePixelVerticalLine(final RenderingContext buffer, final int alpha) { - final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate; - final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate; + final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate; + final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate; int yStart = (int) onScreenPoint1.y; int yEnd = (int) onScreenPoint2.y; @@ -267,25 +267,25 @@ public class Line extends AbstractCoordinateShape { final int y = yStart + relativeY; if ((y >= buffer.renderMinY) && (y < buffer.renderMaxY)) { - if ((y >= 0) && (y < buffer.height)) { + if ((y >= 0) && (y < buffer.height)) { - final int x = xBase + ((relativeY * lineWidth) / lineHeight); - if ((x >= 0) && (x < buffer.width)) { - int offset = (y * buffer.width) + x; + final int x = xBase + ((relativeY * lineWidth) / lineHeight); + if ((x >= 0) && (x < buffer.width)) { + int offset = (y * buffer.width) + x; - final int dest = pixels[offset]; - final int destR = (dest >> 16) & 0xff; - final int destG = (dest >> 8) & 0xff; - final int destB = dest & 0xff; + final int dest = pixels[offset]; + final int destR = (dest >> 16) & 0xff; + final int destG = (dest >> 8) & 0xff; + final int destB = dest & 0xff; - final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8; - final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8; - final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8; + final int newR = ((destR * backgroundAlpha) + redWithAlpha) >> 8; + final int newG = ((destG * backgroundAlpha) + greenWithAlpha) >> 8; + final int newB = ((destB * backgroundAlpha) + blueWithAlpha) >> 8; - pixels[offset] = (newR << 16) | (newG << 8) | newB; - } + pixels[offset] = (newR << 16) | (newG << 8) | newB; } } + } } } @@ -320,16 +320,16 @@ public class Line extends AbstractCoordinateShape { @Override public void paint(final RenderingContext buffer) { - final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate; - final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate; + final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate; + final Point2D onScreenPoint2 = vertices[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) - / coordinates[0].transformedCoordinate.z; + / vertices[0].transformedCoordinate.z; final double point2radius = (buffer.width * LINE_WIDTH_MULTIPLIER * width) - / coordinates[1].transformedCoordinate.z; + / vertices[1].transformedCoordinate.z; if ((point1radius < MINIMUM_WIDTH_THRESHOLD) || (point2radius < MINIMUM_WIDTH_THRESHOLD)) { diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java index fb729c6..9d03c8f 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/solidpolygon/SolidPolygon.java @@ -10,7 +10,6 @@ import eu.svjatoslav.sixth.e3d.gui.RenderingContext; import eu.svjatoslav.sixth.e3d.gui.humaninput.MouseInteractionController; import eu.svjatoslav.sixth.e3d.math.Vertex; import eu.svjatoslav.sixth.e3d.renderer.raster.Color; -import eu.svjatoslav.sixth.e3d.renderer.raster.lighting.LightingManager; import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.AbstractCoordinateShape; import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon; @@ -136,12 +135,12 @@ public class SolidPolygon extends AbstractCoordinateShape { *

  • Scanline rasterization with alpha blending
  • * * - * @param context the rendering context - * @param onScreenPoint1 the first vertex in screen coordinates - * @param onScreenPoint2 the second vertex in screen coordinates - * @param onScreenPoint3 the third vertex in screen coordinates - * @param mouseInteractionController optional controller for mouse events, or null - * @param color the fill color + * @param context the rendering context + * @param onScreenPoint1 the first vertex in screen coordinates + * @param onScreenPoint2 the second vertex in screen coordinates + * @param onScreenPoint3 the third vertex in screen coordinates + * @param 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, @@ -207,9 +206,21 @@ public class SolidPolygon extends AbstractCoordinateShape { LineInterpolator b = polygonBoundary2; LineInterpolator c = polygonBoundary3; LineInterpolator 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; } + 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)) { @@ -287,9 +298,9 @@ public class SolidPolygon extends AbstractCoordinateShape { * @param result the point to store the normal vector in */ private void calculateNormal(final Point3D result) { - final Point3D v1 = coordinates[0].coordinate; - final Point3D v2 = coordinates[1].coordinate; - final Point3D v3 = coordinates[2].coordinate; + final Point3D v1 = vertices[0].coordinate; + final Point3D v2 = vertices[1].coordinate; + final Point3D v3 = vertices[2].coordinate; final double ax = v2.x - v1.x; final double ay = v2.y - v1.y; @@ -321,9 +332,9 @@ public class SolidPolygon extends AbstractCoordinateShape { * @param result the point to store the center in */ private void calculateCenter(final Point3D result) { - final Point3D v1 = coordinates[0].coordinate; - final Point3D v2 = coordinates[1].coordinate; - final Point3D v3 = coordinates[2].coordinate; + final Point3D v1 = vertices[0].coordinate; + final Point3D v2 = vertices[1].coordinate; + final Point3D v3 = vertices[2].coordinate; result.x = (v1.x + v2.x + v3.x) / 3.0; result.y = (v1.y + v2.y + v3.y) / 3.0; @@ -345,9 +356,9 @@ public class SolidPolygon extends AbstractCoordinateShape { @Override public void paint(final RenderingContext renderBuffer) { - final Point2D onScreenPoint1 = coordinates[0].onScreenCoordinate; - final Point2D onScreenPoint2 = coordinates[1].onScreenCoordinate; - final Point2D onScreenPoint3 = coordinates[2].onScreenCoordinate; + final Point2D onScreenPoint1 = vertices[0].onScreenCoordinate; + final Point2D onScreenPoint2 = vertices[1].onScreenCoordinate; + final Point2D onScreenPoint3 = vertices[2].onScreenCoordinate; if (backfaceCulling) { final double signedArea = (onScreenPoint2.x - onScreenPoint1.x) diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java index 477c30b..b5ba635 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/basic/texturedpolygon/TexturedPolygon.java @@ -11,7 +11,7 @@ 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.Color; +import java.awt.*; import static eu.svjatoslav.sixth.e3d.geometry.Polygon.pointWithinPolygon; @@ -66,9 +66,9 @@ public class TexturedPolygon extends AbstractCoordinateShape { */ private void computeTotalTextureDistance() { // compute total texture distance - totalTextureDistance = coordinates[0].textureCoordinate.getDistanceTo(coordinates[1].textureCoordinate); - totalTextureDistance += coordinates[0].textureCoordinate.getDistanceTo(coordinates[2].textureCoordinate); - totalTextureDistance += coordinates[1].textureCoordinate.getDistanceTo(coordinates[2].textureCoordinate); + totalTextureDistance = vertices[0].textureCoordinate.getDistanceTo(vertices[1].textureCoordinate); + totalTextureDistance += vertices[0].textureCoordinate.getDistanceTo(vertices[2].textureCoordinate); + totalTextureDistance += vertices[1].textureCoordinate.getDistanceTo(vertices[2].textureCoordinate); } /** @@ -201,9 +201,9 @@ public class TexturedPolygon extends AbstractCoordinateShape { @Override public void paint(final RenderingContext renderBuffer) { - final Point2D projectedPoint1 = coordinates[0].onScreenCoordinate; - final Point2D projectedPoint2 = coordinates[1].onScreenCoordinate; - final Point2D projectedPoint3 = coordinates[2].onScreenCoordinate; + 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) @@ -276,23 +276,35 @@ public class TexturedPolygon extends AbstractCoordinateShape { final PolygonBorderInterpolator polygonBorder3 = interp[2]; polygonBorder1.setPoints(projectedPoint1, projectedPoint2, - coordinates[0].textureCoordinate, - coordinates[1].textureCoordinate); + vertices[0].textureCoordinate, + vertices[1].textureCoordinate); polygonBorder2.setPoints(projectedPoint1, projectedPoint3, - coordinates[0].textureCoordinate, - coordinates[2].textureCoordinate); + vertices[0].textureCoordinate, + vertices[2].textureCoordinate); polygonBorder3.setPoints(projectedPoint2, projectedPoint3, - coordinates[1].textureCoordinate, - coordinates[2].textureCoordinate); + 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; } + 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)) { @@ -331,9 +343,9 @@ public class TexturedPolygon extends AbstractCoordinateShape { */ private void showBorders(final RenderingContext renderBuffer) { - final Point2D projectedPoint1 = coordinates[0].onScreenCoordinate; - final Point2D projectedPoint2 = coordinates[1].onScreenCoordinate; - final Point2D projectedPoint3 = coordinates[2].onScreenCoordinate; + 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; diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java index f394a29..20c2901 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/ForwardOrientedTextBlock.java @@ -40,11 +40,11 @@ public class ForwardOrientedTextBlock extends Billboard { /** * Creates a new forward-oriented text block at the given 3D position. * - * @param point the 3D position where the text label is placed - * @param scale the scale factor controlling the rendered size of the text + * @param point the 3D position where the text label is placed + * @param scale the scale factor controlling the rendered size of the text * @param maxUpscaleFactor the maximum mipmap upscale factor for the backing texture - * @param text the text string to render - * @param textColor the color of the rendered text + * @param text the text string to render + * @param textColor the color of the rendered text */ public ForwardOrientedTextBlock(final Point3D point, final double scale, final int maxUpscaleFactor, final String text, @@ -60,9 +60,9 @@ public class ForwardOrientedTextBlock extends Billboard { * defined in {@link TextCanvas}. Each character is drawn individually at the appropriate * horizontal offset using {@link TextCanvas#FONT}.

    * - * @param text the text string to render into the texture + * @param text the text string to render into the texture * @param maxUpscaleFactor the maximum mipmap upscale factor for the texture - * @param textColor the color of the rendered text + * @param textColor the color of the rendered text * @return a new {@link Texture} containing the rendered text */ public static Texture getTexture(final String text, @@ -73,9 +73,10 @@ public class ForwardOrientedTextBlock extends Billboard { * TextCanvas.FONT_CHAR_WIDTH_TEXTURE_PIXELS, TextCanvas.FONT_CHAR_HEIGHT_TEXTURE_PIXELS, maxUpscaleFactor); + // Put blue background to test if texture has correct size // texture.graphics.setColor(Color.BLUE); // texture.graphics.fillRect(0, 0, texture.primaryBitmap.width, - // texture.primaryBitmap.width); + // texture.primaryBitmap.width); texture.graphics.setFont(TextCanvas.FONT); texture.graphics.setColor(textColor.toAwtColor()); diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java index f456590..d9bfe23 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/base/AbstractCompositeShape.java @@ -183,6 +183,37 @@ public class AbstractCompositeShape extends AbstractShape { return originalSubShapes; } + /** + * Extracts all SolidPolygon triangles from this composite shape. + * + *

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

    + * + *

    Example:

    + *
    {@code
    +     * SolidPolygonCube cube = new SolidPolygonCube(new Point3D(0, 0, 0), 50, Color.RED);
    +     * List triangles = cube.extractSolidPolygons();
    +     * CSG csg = CSG.fromSolidPolygons(triangles);
    +     * }
    + * + * @return list of all SolidPolygon sub-shapes + */ + public List extractSolidPolygons() { + final List result = new ArrayList<>(); + for (final SubShape subShape : originalSubShapes) { + final AbstractShape shape = subShape.getShape(); + if (shape instanceof SolidPolygon) { + result.add((SolidPolygon) shape); + } else if (shape instanceof AbstractCompositeShape) { + result.addAll(((AbstractCompositeShape) shape).extractSolidPolygons()); + } + } + return result; + } + /** * Returns the view-space tracker that monitors the distance * and angle between the camera and this shape for level-of-detail adjustments. diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java index 5fd74ab..d8a9236 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonArrow.java @@ -7,7 +7,6 @@ package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid; import eu.svjatoslav.sixth.e3d.geometry.Point3D; import eu.svjatoslav.sixth.e3d.math.Matrix3x3; import eu.svjatoslav.sixth.e3d.math.Quaternion; -import eu.svjatoslav.sixth.e3d.math.Transform; 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; @@ -48,26 +47,37 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCom public class SolidPolygonArrow extends AbstractCompositeShape { /** - * Constructs a 3D arrow pointing from start to end. * - *

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

    + * Number of segments for arrow smoothness. + */ + private static final int SEGMENTS = 12; + + /** + * Arrow tip radius as a fraction of body radius (2.5x). + */ + private static final double TIP_RADIUS_FACTOR = 2.5; + + /** + * Arrow tip length as a fraction of body radius (5.0x). + */ + private static final double TIP_LENGTH_FACTOR = 5.0; + + /** + * Constructs a 3D arrow pointing from start to end with sensible defaults. + * + *

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

    * - * @param startPoint the origin point of the arrow (where the body starts) - * @param endPoint the destination point of the arrow (where the tip points to) - * @param bodyRadius the radius of the cylindrical body - * @param tipRadius the radius of the cone base at the tip - * @param tipLength the length of the conical tip - * @param segments the number of segments for cylinder and cone smoothness. - * Higher values create smoother arrows. Minimum is 3. - * @param color the fill color (RGBA; alpha controls transparency) + * @param startPoint the origin point of the arrow (where the body starts) + * @param endPoint the destination point of the arrow (where the tip points to) + * @param bodyRadius the radius of the cylindrical body; tip dimensions are + * calculated automatically from this value + * @param color the fill color (RGBA; alpha controls transparency) */ public SolidPolygonArrow(final Point3D startPoint, final Point3D endPoint, - final double bodyRadius, final double tipRadius, - final double tipLength, final int segments, - final Color color) { + final double bodyRadius, final Color color) { super(); // Calculate direction and distance @@ -93,13 +103,13 @@ public class SolidPolygonArrow extends AbstractCompositeShape { final Matrix3x3 rotMatrix = rotation.toMatrix(); // Calculate body length (distance minus tip) - final double bodyLength = Math.max(0, distance - tipLength); + final double bodyLength = Math.max(0, distance - bodyRadius * TIP_LENGTH_FACTOR); // Build the arrow components if (bodyLength > 0) { - addCylinderBody(startPoint, bodyRadius, bodyLength, segments, color, rotMatrix, nx, ny, nz); + addCylinderBody(startPoint, bodyRadius, bodyLength, SEGMENTS, color, rotMatrix, nx, ny, nz); } - addConeTip(endPoint, tipRadius, tipLength, segments, color, rotMatrix, nx, ny, nz); + addConeTip(endPoint, bodyRadius * TIP_RADIUS_FACTOR, bodyRadius * TIP_LENGTH_FACTOR, SEGMENTS, color, rotMatrix, nx, ny, nz); setBackfaceCulling(true); } @@ -248,15 +258,15 @@ public class SolidPolygonArrow extends AbstractCompositeShape { *

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

    * - * @param endPoint the position of the arrow tip (cone apex) - * @param radius the radius of the cone base - * @param length the length of the cone - * @param segments the number of segments around the circumference - * @param color the fill color - * @param rotMatrix the rotation matrix to apply - * @param dirX direction X component - * @param dirY direction Y component - * @param dirZ direction Z component + * @param endPoint the position of the arrow tip (cone apex) + * @param radius the radius of the cone base + * @param length the length of the cone + * @param segments the number of segments around the circumference + * @param color the fill color + * @param rotMatrix the rotation matrix to apply + * @param dirX direction X component + * @param dirY direction Y component + * @param dirZ direction Z component */ private void addConeTip(final Point3D endPoint, final double radius, final double length, final int segments, diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java new file mode 100644 index 0000000..3014ef7 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/solid/SolidPolygonMesh.java @@ -0,0 +1,65 @@ +/* + * Sixth 3D engine. Author: Svjatoslav Agejenko. + * This project is released under Creative Commons Zero (CC0) license. + */ +package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid; + +import eu.svjatoslav.sixth.e3d.geometry.Point3D; +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 java.util.List; + +/** + * A renderable mesh composed of SolidPolygon triangles. + * + *

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

    + * + *

    Usage:

    + *
    {@code
    + * // From CSG result
    + * CSG result = cubeCSG.subtract(sphereCSG);
    + * SolidPolygonMesh mesh = result.toMesh(new Point3D(0, 0, 0));
    + * mesh.setShadingEnabled(true);
    + * mesh.setBackfaceCulling(true);
    + * shapes.addShape(mesh);
    + *
    + * // From list of triangles
    + * List triangles = ...;
    + * SolidPolygonMesh mesh = new SolidPolygonMesh(triangles, new Point3D(0, 0, 0));
    + * }
    + * + * @see SolidPolygon the triangle type for rendering + */ +public class SolidPolygonMesh extends AbstractCompositeShape { + + private int triangleCount; + + /** + * Creates a mesh from a list of SolidPolygon triangles. + * + * @param triangles the triangles to include in the mesh + * @param location the position in 3D space + */ + public SolidPolygonMesh(final List triangles, final Point3D location) { + super(location); + this.triangleCount = 0; + + for (final SolidPolygon triangle : triangles) { + addShape(triangle); + triangleCount++; + } + } + + /** + * Returns the number of triangles in this mesh. + * + * @return the triangle count + */ + public int getTriangleCount() { + return triangleCount; + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java index a103d7c..59fdb2b 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/textcanvas/CanvasCharacter.java @@ -8,7 +8,6 @@ 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.shapes.AbstractCoordinateShape; -import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon; import java.awt.*; @@ -47,10 +46,10 @@ public class CanvasCharacter extends AbstractCoordinateShape { /** * Creates a canvas character at the specified location with given colors. * - * @param centerLocation the center position in 3D space - * @param character the character to render - * @param foregroundColor the foreground (text) color - * @param backgroundColor the background color + * @param centerLocation the center position in 3D space + * @param character the character to render + * @param foregroundColor the foreground (text) color + * @param backgroundColor the background color */ public CanvasCharacter(final Point3D centerLocation, final char character, final eu.svjatoslav.sixth.e3d.renderer.raster.Color foregroundColor, @@ -64,25 +63,25 @@ public class CanvasCharacter extends AbstractCoordinateShape { this.backgroundColor = backgroundColor; - coordinates[0].coordinate = centerLocation; + vertices[0].coordinate = centerLocation; final double halfWidth = FONT_CHAR_WIDTH / 2d; final double halfHeight = FONT_CHAR_HEIGHT / 2d; // upper left - coordinates[1].coordinate = centerLocation.clone().translateX(-halfWidth) + vertices[1].coordinate = centerLocation.clone().translateX(-halfWidth) .translateY(-halfHeight); // upper right - coordinates[2].coordinate = centerLocation.clone().translateX(halfWidth) + vertices[2].coordinate = centerLocation.clone().translateX(halfWidth) .translateY(-halfHeight); // lower right - coordinates[3].coordinate = centerLocation.clone().translateX(halfWidth) + vertices[3].coordinate = centerLocation.clone().translateX(halfWidth) .translateY(halfHeight); // lower left - coordinates[4].coordinate = centerLocation.clone().translateX(-halfWidth) + vertices[4].coordinate = centerLocation.clone().translateX(-halfWidth) .translateY(halfHeight); } @@ -144,6 +143,7 @@ public class CanvasCharacter extends AbstractCoordinateShape { /** * Paints the character on the screen. + * * @param renderingContext the rendering context */ @Override @@ -151,16 +151,16 @@ public class CanvasCharacter extends AbstractCoordinateShape { // Draw background rectangle first. It is composed of two triangles. drawPolygon(renderingContext, - coordinates[1].onScreenCoordinate, - coordinates[2].onScreenCoordinate, - coordinates[3].onScreenCoordinate, + vertices[1].onScreenCoordinate, + vertices[2].onScreenCoordinate, + vertices[3].onScreenCoordinate, mouseInteractionController, backgroundColor); drawPolygon(renderingContext, - coordinates[1].onScreenCoordinate, - coordinates[3].onScreenCoordinate, - coordinates[4].onScreenCoordinate, + vertices[1].onScreenCoordinate, + vertices[3].onScreenCoordinate, + vertices[4].onScreenCoordinate, mouseInteractionController, backgroundColor); @@ -170,7 +170,7 @@ public class CanvasCharacter extends AbstractCoordinateShape { if (desiredFontSize >= MAX_FONT_SIZE) return; - final Point2D onScreenLocation = coordinates[0].onScreenCoordinate; + final Point2D onScreenLocation = vertices[0].onScreenCoordinate; // screen borders check if (onScreenLocation.x < 0) diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeArrow.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeArrow.java index 96900ae..69ec6cf 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeArrow.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeArrow.java @@ -42,27 +42,64 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCom */ public class WireframeArrow extends AbstractCompositeShape { - /** - * Constructs a 3D wireframe arrow pointing from start to end. - * - *

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

    - * - * @param startPoint the origin point of the arrow (where the body starts) - * @param endPoint the destination point of the arrow (where the tip points to) - * @param bodyRadius the radius of the cylindrical body - * @param tipRadius the radius of the cone base at the tip - * @param tipLength the length of the conical tip - * @param segments the number of segments for cylinder and cone smoothness. - * Higher values create smoother arrows. Minimum is 3. - * @param appearance the line appearance (color, width) used for all lines - */ - public WireframeArrow(final Point3D startPoint, final Point3D endPoint, - final double bodyRadius, final double tipRadius, - final double tipLength, final int segments, - final LineAppearance appearance) { +/** + * Default number of segments for arrow smoothness. + */ +private static final int DEFAULT_SEGMENTS = 12; + +/** + * Default tip radius as a fraction of body radius (2.5x). + */ +private static final double TIP_RADIUS_FACTOR = 2.5; + +/** + * Default tip length as a fraction of body radius (5.0x). + */ +private static final double TIP_LENGTH_FACTOR = 5.0; + +/** + * Constructs a 3D wireframe arrow pointing from start to end with sensible defaults. + * + *

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

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

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

    + * + * @param startPoint the origin point of the arrow (where the body starts) + * @param endPoint the destination point of the arrow (where the tip points to) + * @param bodyRadius the radius of the cylindrical body + * @param tipRadius the radius of the cone base at the tip + * @param tipLength the length of the conical tip + * @param segments the number of segments for cylinder and cone smoothness. + * Higher values create smoother arrows. Minimum is 3. + * @param appearance the line appearance (color, width) used for all lines + */ +public WireframeArrow(final Point3D startPoint, final Point3D endPoint, + final double bodyRadius, final double tipRadius, + final double tipLength, final int segments, + final LineAppearance appearance) { super(); // Calculate direction and distance diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java index 20b80e9..c023f3f 100644 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/slicer/Slicer.java @@ -77,9 +77,21 @@ public class Slicer { BorderLine b = line2; BorderLine c = line3; BorderLine t; - if (a.getLength() > b.getLength()) { t = a; a = b; b = t; } - if (b.getLength() > c.getLength()) { t = b; b = c; c = t; } - if (a.getLength() > b.getLength()) { t = a; a = b; b = t; } + if (a.getLength() > b.getLength()) { + t = a; + a = b; + b = t; + } + if (b.getLength() > c.getLength()) { + t = b; + b = c; + c = t; + } + if (a.getLength() > b.getLength()) { + t = a; + a = b; + b = t; + } final BorderLine longestLine = c; @@ -132,9 +144,9 @@ public class Slicer { public void slice(final TexturedPolygon originalPolygon) { considerSlicing( - originalPolygon.coordinates[0], - originalPolygon.coordinates[1], - originalPolygon.coordinates[2], + originalPolygon.vertices[0], + originalPolygon.vertices[1], + originalPolygon.vertices[2], originalPolygon); } -- 2.20.1