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:

+ * + * + *

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); }