From: Svjatoslav Agejenko
Date: Sat, 28 Mar 2026 12:47:12 +0000 (+0200)
Subject: feat(csg): add constructive solid geometry with BSP tree boolean operations
X-Git-Url: http://www2.svjatoslav.eu/gitweb/?a=commitdiff_plain;h=428d163d0b3e9d09ba42ed27862921ca1ef39cfa;p=sixth-3d.git
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.
---
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 0000000..d028e61
Binary files /dev/null and b/doc/Axis.png differ
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)
+ *
+ *
+ * - Clip A's polygons to keep only parts outside B
+ * - Clip B's polygons to keep only parts outside A
+ * - Invert B, clip to A, invert again (keeps B's surface inside A)
+ * - Build final tree from all remaining polygons
+ *
+ *
+ * @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
+ *
+ *
+ * - Invert A (turning solid into cavity, cavity into solid)
+ * - Clip inverted A to keep only parts inside B
+ * - Clip B to keep only parts inside inverted A
+ * - Invert B twice to get B's cavity surface
+ * - Combine and invert final result
+ *
+ *
+ * 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
+ *
+ *
+ * - Invert A (swap inside/outside)
+ * - Clip inverted-A to B, keeping parts outside B
+ * - Invert B, clip to A (captures B's interior surface)
+ * - Clip B again to ensure proper boundaries
+ * - Combine and invert final result
+ *
+ *
+ * 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:
+ *
+ * - Building BSP trees from both input solids
+ * - Clipping each tree against the other (removing overlapping geometry)
+ * - Optionally inverting trees (for subtraction and intersection)
+ * - Collecting the resulting polygons
+ *
+ *
+ * @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:
+ *
+ * - At each node, split input polygons by the node's plane
+ * - Polygons in front go to front child for further clipping
+ * - Polygons in back go to back child for further clipping
+ * - Coplanar polygons are kept (they're on the surface)
+ * - If no back child exists, back polygons are discarded (they're inside)
+ *
+ *
+ * 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:
+ *
+ * - If this node has no plane, use the first polygon's plane as the partitioning plane
+ * - 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
+ *
+ *
+ * - Recursively build front subtree with front polygons
+ * - Recursively build back subtree with back polygons
+ *
+ *
+ * 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:
+ *
+ * - Vertices on the front side become a new polygon (added to front list)
+ * - Vertices on the back side become a new polygon (added to back list)
+ * - Intersection points are computed and added to both polygons
+ *
+ *
+ * @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);
}