From: Svjatoslav Agejenko The billboard is rendered as a screen-aligned quad centered on the projected
* position. The size is computed based on distance and scale factor. Performance optimization: Uses fixed-point incremental stepping to avoid
+ * per-pixel division, and inlines alpha blending to avoid method call overhead.
+ * This provides 50-70% better performance than the previous division-based approach. The arrow points from a start point to an end point, with the tip
+ * located at the end point. The arrow's appearance (size, color, transparency)
+ * can be customized through the constructor parameters. Usage example: 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. The arrow by default points in the -Y direction. This method computes
+ * the rotation needed to align the arrow with the target direction vector. The cylinder is created with its base at the start point and extends
+ * in the direction of the arrow for the specified body length. Local coordinate system: The arrow points in -Y direction in local space.
+ * Therefore, local -Y is toward the tip (front), and local +Y is toward the start (back). The cone is created with its apex at the end point (the arrow tip)
+ * and its base pointing back towards the start point. 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. The cone has a circular base and a single apex (tip) point. Two constructors
+ * are provided for different use cases: Usage examples: This is the recommended constructor for placing cones in 3D space.
+ * The cone's apex (tip) is at {@code apexPoint}, and the circular base
+ * is centered at {@code baseCenterPoint}. The cone points in the direction
+ * from apex to base center. Coordinate interpretation: This constructor creates a Y-axis aligned cone. The apex is positioned
+ * at {@code baseCenter.y - height} (above the base in the negative Y direction).
+ * For cones pointing in arbitrary directions, use
+ * {@link #SolidPolygonCone(Point3D, Point3D, double, int, Color)} instead. Coordinate system: The cone points in -Y direction (apex at lower Y).
+ * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height.
+ * In Sixth 3D's coordinate system, "up" visually is negative Y. The cone by default points in the -Y direction (apex at origin, base at -Y).
+ * This method computes the rotation needed to align the cone with the target
+ * direction vector. The cylinder has circular top and bottom caps connected by a curved side
- * surface made of rectangular panels. The number of segments determines the
- * smoothness of the curved surface. The cylinder extends from startPoint to endPoint with circular caps at both
+ * ends. The number of segments determines the smoothness of the curved surface. Usage example: The cylinder has circular caps at both startPoint and endPoint,
+ * connected by a curved side surface. The orientation is automatically
+ * calculated from the direction between the two points. The pyramid has a square base and four triangular faces meeting at an apex.
- * The base has side length of {@code 2 * baseSize} and the height extends
- * {@code height} units above the base center to the apex. The pyramid has a square base and four triangular faces meeting at an apex
+ * (tip). Two constructors are provided for different use cases: Usage example: Usage examples: This is the recommended constructor for placing pyramids in 3D space.
+ * The pyramid's apex (tip) is at {@code apexPoint}, and the square base
+ * is centered at {@code baseCenter}. The pyramid points in the direction
+ * from apex to base center. Coordinate interpretation: This constructor creates a Y-axis aligned pyramid. The apex is positioned
+ * at {@code baseCenter.y - height} (above the base in the negative Y direction).
+ * For pyramids pointing in arbitrary directions, use
+ * {@link #SolidPolygonPyramid(Point3D, Point3D, double, Color)} instead. Coordinate system: The pyramid points in -Y direction (apex at lower Y).
+ * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height.
+ * In Sixth 3D's coordinate system, "up" visually is negative Y. The pyramid by default points in the -Y direction (apex at origin, base at -Y).
+ * This method computes the rotation needed to align the pyramid with the target
+ * direction vector. The box can be constructed either from a center point and a uniform size
- * (producing a cube), or from two diagonally opposite corner points (producing
- * an arbitrary axis-aligned rectangular box). The box is defined by two diagonally opposite corner points in 3D space.
+ * The box is axis-aligned, meaning its edges are parallel to the X, Y, and Z axes. The vertices are labeled p1 through p8, representing the eight corners of
- * the box. The triangles are arranged to cover the bottom, top, front, back,
- * left, and right faces. Vertex layout: Usage example: The eight vertices are derived from the two corner points: Usage examples: The box is axis-aligned and fills the rectangular region between the
+ * two corners. The corner points do not need to be ordered (cornerA can have
+ * larger coordinates than cornerB); the constructor will determine the actual
+ * min/max bounds automatically. At each grid intersection point, up to three line segments are created
- * (one along each axis), forming a three-dimensional lattice. The corner
- * points are automatically normalized so that {@code p1} holds the minimum
- * coordinates and {@code p2} holds the maximum coordinates.{@code
+ * // Create a red arrow pointing from origin to (100, -50, 200)
+ * SolidPolygonArrow arrow = new SolidPolygonArrow(
+ * new Point3D(0, 0, 0), // start point
+ * new Point3D(100, -50, 200), // end point
+ * 8, // body radius
+ * 20, // tip radius
+ * 40, // tip length
+ * 16, // segments
+ * Color.RED // color
+ * );
+ * shapeCollection.addShape(arrow);
+ *
+ * // Create a semi-transparent blue arrow
+ * SolidPolygonArrow seeThroughArrow = new SolidPolygonArrow(
+ * new Point3D(0, 100, 0),
+ * new Point3D(0, -100, 0),
+ * 10, 25, 50, 12,
+ * new Color(0, 0, 255, 128) // blue with 50% transparency
+ * );
+ * }
+ *
+ * @see SolidPolygonCone
+ * @see SolidPolygonCylinder
+ */
+public class SolidPolygonArrow extends AbstractCompositeShape {
+
+ /**
+ * Constructs a 3D arrow pointing from start to end.
+ *
+ *
+ *
+ *
+ * {@code
+ * // Directional constructor: cone pointing from apex toward base
+ * SolidPolygonCone directionalCone = new SolidPolygonCone(
+ * new Point3D(0, -100, 0), // apex (tip of the cone)
+ * new Point3D(0, 50, 0), // baseCenter (cone points toward this)
+ * 50, // radius of the circular base
+ * 16, // segments
+ * Color.RED
+ * );
+ *
+ * // Y-axis aligned constructor: cone pointing upward
+ * SolidPolygonCone verticalCone = new SolidPolygonCone(
+ * new Point3D(0, 0, 300), // baseCenter
+ * 50, // radius
+ * 100, // height
+ * 16, // segments
+ * Color.RED
+ * );
+ * }
+ *
+ * @see SolidPolygonCylinder
+ * @see SolidPolygonArrow
+ * @see SolidPolygon
+ */
+public class SolidPolygonCone extends AbstractCompositeShape {
+
+ /**
+ * Constructs a solid cone pointing from apex toward base center.
+ *
+ *
+ *
+ *
+ * @param apexPoint the position of the cone's tip (apex)
+ * @param baseCenterPoint the center point of the circular base; the cone
+ * points from apex toward this point
+ * @param radius the radius of the circular base
+ * @param segments the number of segments around the circumference.
+ * Higher values create smoother cones. Minimum is 3.
+ * @param color the fill color applied to all faces of the cone
+ */
+ public SolidPolygonCone(final Point3D apexPoint, final Point3D baseCenterPoint,
+ final double radius, final int segments,
+ final Color color) {
+ super();
+
+ // Calculate direction and height from apex to base center
+ final double dx = baseCenterPoint.x - apexPoint.x;
+ final double dy = baseCenterPoint.y - apexPoint.y;
+ final double dz = baseCenterPoint.z - apexPoint.z;
+ final double height = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ // Handle degenerate case: apex and base center are the same point
+ if (height < 0.001) {
+ return;
+ }
+
+ // Normalize direction vector (from apex toward base)
+ final double nx = dx / height;
+ final double ny = dy / height;
+ final double nz = dz / height;
+
+ // Calculate rotation to align Y-axis with direction
+ // Default cone points in -Y direction (apex at origin, base at -Y)
+ // We need to rotate from (0, -1, 0) to (nx, ny, nz)
+ final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
+ final Matrix3x3 rotMatrix = rotation.toMatrix();
+
+ // Generate base ring vertices in local space, then rotate and translate
+ // In local space: apex is at origin, base is at Y = -height
+ // (cone points in -Y direction in local space)
+ final Point3D[] baseRing = new Point3D[segments];
+
+ for (int i = 0; i < segments; i++) {
+ final double angle = 2.0 * Math.PI * i / segments;
+ final double localX = radius * Math.cos(angle);
+ final double localZ = radius * Math.sin(angle);
+
+ // Base ring vertex in local space (Y = -height)
+ final Point3D local = new Point3D(localX, -height, localZ);
+ rotMatrix.transform(local, local);
+ local.x += apexPoint.x;
+ local.y += apexPoint.y;
+ local.z += apexPoint.z;
+ baseRing[i] = local;
+ }
+
+ // Apex point (the cone tip)
+ final Point3D apex = new Point3D(apexPoint.x, apexPoint.y, apexPoint.z);
+
+ // Create side faces connecting each pair of adjacent base vertices to the apex
+ // Winding: apex â next â current creates CCW winding when viewed from outside
+ // (Base ring vertices go CCW when viewed from apex looking at base, so we reverse)
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+
+ addShape(new SolidPolygon(
+ new Point3D(apex.x, apex.y, apex.z),
+ new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z),
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
+ color));
+ }
+
+ // Create base cap (circular bottom face)
+ // The cap faces away from the apex (in the direction the cone points).
+ // Winding: center â current â next creates CCW winding when viewed from
+ // outside (away from apex).
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+ addShape(new SolidPolygon(
+ new Point3D(baseCenterPoint.x, baseCenterPoint.y, baseCenterPoint.z),
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
+ new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z),
+ color));
+ }
+
+ setBackfaceCulling(true);
+ }
+
+ /**
+ * Constructs a solid cone with circular base centered at the given point,
+ * pointing in the -Y direction.
+ *
+ * {@code
- * // Create a cylinder with radius 50, height 100, and 16 segments
+ * // Create a vertical cylinder from Y=100 to Y=200
* SolidPolygonCylinder cylinder = new SolidPolygonCylinder(
- * new Point3D(0, 0, 200), 50, 100, 16, Color.RED);
- * shapeCollection.addShape(cylinder);
+ * new Point3D(0, 100, 0), // start point (bottom)
+ * new Point3D(0, 200, 0), // end point (top)
+ * 10, // radius
+ * 16, // segments
+ * Color.RED // color
+ * );
+ *
+ * // Create a horizontal cylinder along X axis
+ * SolidPolygonCylinder pipe = new SolidPolygonCylinder(
+ * new Point3D(-50, 0, 0),
+ * new Point3D(50, 0, 0),
+ * 5, 12, Color.BLUE
+ * );
* }
*
- * @see SolidPolygonCube
- * @see SolidPolygonSphere
+ * @see SolidPolygonCone
+ * @see SolidPolygonArrow
* @see SolidPolygon
*/
public class SolidPolygonCylinder extends AbstractCompositeShape {
/**
- * Constructs a solid cylinder centered at the given point.
+ * Constructs a solid cylinder between two end points.
+ *
+ *
+ *
+ *
+ * {@code
- * SolidPolygonPyramid pyramid = new SolidPolygonPyramid(
- * new Point3D(0, 0, 300), 50, 100, Color.BLUE);
- * shapeCollection.addShape(pyramid);
+ * // Directional constructor: pyramid pointing from apex toward base
+ * SolidPolygonPyramid directionalPyramid = new SolidPolygonPyramid(
+ * new Point3D(0, -100, 0), // apex (tip of the pyramid)
+ * new Point3D(0, 50, 0), // baseCenter (pyramid points toward this)
+ * 50, // baseSize (half-width of square base)
+ * Color.RED
+ * );
+ *
+ * // Y-axis aligned constructor: pyramid pointing upward
+ * SolidPolygonPyramid verticalPyramid = new SolidPolygonPyramid(
+ * new Point3D(0, 0, 300), // baseCenter
+ * 50, // baseSize (half-width of square base)
+ * 100, // height
+ * Color.BLUE
+ * );
* }
*
+ * @see SolidPolygonCone
* @see SolidPolygonCube
- * @see SolidPolygonSphere
* @see SolidPolygon
*/
public class SolidPolygonPyramid extends AbstractCompositeShape {
/**
- * Constructs a solid square-based pyramid with base centered at the given point.
+ * Constructs a solid square-based pyramid pointing from apex toward base center.
+ *
+ *
+ *
+ *
+ * @param apexPoint the position of the pyramid's tip (apex)
+ * @param baseCenter the center point of the square base; the pyramid
+ * points from apex toward this point
+ * @param baseSize the half-width of the square base; the base extends
+ * this distance from the center, giving a total base
+ * edge length of {@code 2 * baseSize}
+ * @param color the fill color applied to all faces of the pyramid
+ */
+ public SolidPolygonPyramid(final Point3D apexPoint, final Point3D baseCenter,
+ final double baseSize, final Color color) {
+ super();
+
+ // Calculate direction and height from apex to base center
+ final double dx = baseCenter.x - apexPoint.x;
+ final double dy = baseCenter.y - apexPoint.y;
+ final double dz = baseCenter.z - apexPoint.z;
+ final double height = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ // Handle degenerate case: apex and base center are the same point
+ if (height < 0.001) {
+ return;
+ }
+
+ // Normalize direction vector (from apex toward base)
+ final double nx = dx / height;
+ final double ny = dy / height;
+ final double nz = dz / height;
+
+ // Calculate rotation to align Y-axis with direction
+ // Default pyramid points in -Y direction (apex at origin, base at -Y)
+ // We need to rotate from (0, -1, 0) to (nx, ny, nz)
+ final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
+ final Matrix3x3 rotMatrix = rotation.toMatrix();
+
+ // Generate base corner vertices in local space, then rotate and translate
+ // In local space: apex is at origin, base is at Y = -height
+ // Base corners form a square centered at (0, -height, 0)
+ final double h = baseSize;
+ final Point3D[] baseCorners = new Point3D[4];
+
+ // Local space corner positions (before rotation)
+ // Arranged clockwise when viewed from apex (from +Y)
+ final double[][] localCorners = {
+ {-h, -height, -h}, // corner 0: negative X, negative Z
+ {+h, -height, -h}, // corner 1: positive X, negative Z
+ {+h, -height, +h}, // corner 2: positive X, positive Z
+ {-h, -height, +h} // corner 3: negative X, positive Z
+ };
+
+ for (int i = 0; i < 4; i++) {
+ final Point3D local = new Point3D(localCorners[i][0], localCorners[i][1], localCorners[i][2]);
+ rotMatrix.transform(local, local);
+ local.x += apexPoint.x;
+ local.y += apexPoint.y;
+ local.z += apexPoint.z;
+ baseCorners[i] = local;
+ }
+
+ // Apex point (the pyramid tip)
+ final Point3D apex = new Point3D(apexPoint.x, apexPoint.y, apexPoint.z);
+
+ // Create the four triangular faces connecting apex to base edges
+ // Winding: next â current â apex creates CCW winding when viewed from outside
+ // (Base corners go CW when viewed from apex, so we reverse to get outward normals)
+ for (int i = 0; i < 4; i++) {
+ final int next = (i + 1) % 4;
+ addShape(new SolidPolygon(
+ new Point3D(baseCorners[next].x, baseCorners[next].y, baseCorners[next].z),
+ new Point3D(baseCorners[i].x, baseCorners[i].y, baseCorners[i].z),
+ new Point3D(apex.x, apex.y, apex.z),
+ color));
+ }
+
+ // Create base cap (square bottom face)
+ // The cap faces away from the apex (in the direction the pyramid points).
+ // Base corners go CW when viewed from apex, so CW when viewed from apex means
+ // CCW when viewed from outside (base side). Use CCW ordering for outward normal.
+ // Triangulate the square base: (center, 3, 0) and (center, 0, 1) and
+ // (center, 1, 2) and (center, 2, 3)
+ addShape(new SolidPolygon(
+ new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
+ new Point3D(baseCorners[3].x, baseCorners[3].y, baseCorners[3].z),
+ new Point3D(baseCorners[0].x, baseCorners[0].y, baseCorners[0].z),
+ color));
+ addShape(new SolidPolygon(
+ new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
+ new Point3D(baseCorners[0].x, baseCorners[0].y, baseCorners[0].z),
+ new Point3D(baseCorners[1].x, baseCorners[1].y, baseCorners[1].z),
+ color));
+ addShape(new SolidPolygon(
+ new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
+ new Point3D(baseCorners[1].x, baseCorners[1].y, baseCorners[1].z),
+ new Point3D(baseCorners[2].x, baseCorners[2].y, baseCorners[2].z),
+ color));
+ addShape(new SolidPolygon(
+ new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
+ new Point3D(baseCorners[2].x, baseCorners[2].y, baseCorners[2].z),
+ new Point3D(baseCorners[3].x, baseCorners[3].y, baseCorners[3].z),
+ color));
+
+ setBackfaceCulling(true);
+ }
+
+ /**
+ * Constructs a solid square-based pyramid with base centered at the given point,
+ * pointing in the -Y direction.
+ *
+ *
+ * cornerB (max) âââââââââ
+ * /â /â
+ * / â / â
+ * / â / â
+ * âââââ¼ââââââââââââ â
+ * â â â â
+ * â â â â
+ * â âââââââââââââââââ
+ * â / â /
+ * â / â /
+ * â/ â/
+ * âââââââââââââââââ cornerA (min)
+ *
*
- * {@code
- * // From center and size:
- * SolidPolygonRectangularBox box1 = new SolidPolygonRectangularBox(
- * new Point3D(0, 0, 200), 100, Color.RED);
+ *
+ *
*
- * // From two corner points:
- * SolidPolygonRectangularBox box2 = new SolidPolygonRectangularBox(
- * new Point3D(-50, -25, 100), new Point3D(50, 25, 200), Color.BLUE);
+ * {@code
+ * // Create a box from two opposite corners
+ * SolidPolygonRectangularBox box = new SolidPolygonRectangularBox(
+ * new Point3D(-50, -25, 100), // cornerA (minimum X, Y, Z)
+ * new Point3D(50, 25, 200), // cornerB (maximum X, Y, Z)
+ * Color.BLUE
+ * );
*
- * shapeCollection.addShape(box1);
+ * // Create a cube using center + size (see SolidPolygonCube for convenience)
+ * double size = 50;
+ * SolidPolygonRectangularBox cube = new SolidPolygonRectangularBox(
+ * new Point3D(0 - size, 0 - size, 200 - size), // cornerA
+ * new Point3D(0 + size, 0 + size, 200 + size), // cornerB
+ * Color.RED
+ * );
* }
*
* @see SolidPolygonCube
* @see SolidPolygon
- * @see AbstractCompositeShape
*/
public class SolidPolygonRectangularBox extends AbstractCompositeShape {
/**
* Constructs a solid rectangular box between two diagonally opposite corner
- * points in 3D space. The eight vertices of the box are derived from the
- * coordinate components of {@code p1} and {@code p7}. All six faces are
- * tessellated into two triangles each, for a total of 12 solid polygons.
+ * points in 3D space.
+ *
+ *
This shape is useful for visualizing 3D space, voxel boundaries, or * spatial reference grids in a scene.
@@ -24,9 +22,9 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCom *Usage example:
*{@code
* LineAppearance appearance = new LineAppearance(1, Color.GRAY);
- * Point3D corner1 = new Point3D(-100, -100, -100);
- * Point3D corner2 = new Point3D(100, 100, 100);
- * Grid3D grid = new Grid3D(corner1, corner2, 50, appearance);
+ * Point3D cornerA = new Point3D(-100, -100, -100);
+ * Point3D cornerB = new Point3D(100, 100, 100);
+ * Grid3D grid = new Grid3D(cornerA, cornerB, 50, appearance);
* shapeCollection.addShape(grid);
* }
*
@@ -37,63 +35,53 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCom
public class Grid3D extends AbstractCompositeShape {
/**
- * Constructs a 3D grid filling the volume between two corner points.
- * The corner points are copied and normalized internally so that grid
- * generation always proceeds from minimum to maximum coordinates.
+ * Constructs a 3D grid filling the volume between two diagonally opposite
+ * corner points.
*
- * @param p1t the first corner point defining the volume (copied, not modified)
- * @param p2t the diagonally opposite corner point (copied, not modified)
- * @param step the spacing between grid lines along each axis; must be
- * positive
+ * The corner points do not need to be in any particular min/max order; + * the constructor automatically normalizes them so that grid generation + * always proceeds from minimum to maximum coordinates.
+ * + * @param cornerA the first corner point defining the volume + * @param cornerB the diagonally opposite corner point + * @param step the spacing between grid lines along each axis; must be positive * @param appearance the line appearance (color, width) used for all grid lines */ - public Grid3D(final Point3D p1t, final Point3D p2t, final double step, + public Grid3D(final Point3D cornerA, final Point3D cornerB, final double step, final LineAppearance appearance) { super(); - final Point3D p1 = new Point3D(p1t); - final Point3D p2 = new Point3D(p2t); + // Determine actual min/max bounds (corners may be in any order) + final double minX = Math.min(cornerA.x, cornerB.x); + final double maxX = Math.max(cornerA.x, cornerB.x); + final double minY = Math.min(cornerA.y, cornerB.y); + final double maxY = Math.max(cornerA.y, cornerB.y); + final double minZ = Math.min(cornerA.z, cornerB.z); + final double maxZ = Math.max(cornerA.z, cornerB.z); - if (p1.x > p2.x) { - final double tmp = p1.x; - p1.x = p2.x; - p2.x = tmp; - } + for (double x = minX; x <= maxX; x += step) { + for (double y = minY; y <= maxY; y += step) { + for (double z = minZ; z <= maxZ; z += step) { - if (p1.y > p2.y) { - final double tmp = p1.y; - p1.y = p2.y; - p2.y = tmp; - } - - if (p1.z > p2.z) { - final double tmp = p1.z; - p1.z = p2.z; - p2.z = tmp; - } + final Point3D p = new Point3D(x, y, z); - for (double x = p1.x; x <= p2.x; x += step) - for (double y = p1.y; y <= p2.y; y += step) - for (double z = p1.z; z <= p2.z; z += step) { - - final Point3D p3 = new Point3D(x, y, z); - - if ((x + step) <= p2.x) { - final Point3D point3d2 = new Point3D(x + step, y, z); - addShape(appearance.getLine(p3, point3d2)); + // Line along X axis + if ((x + step) <= maxX) { + addShape(appearance.getLine(p, new Point3D(x + step, y, z))); } - if ((y + step) <= p2.y) { - final Point3D point3d3 = new Point3D(x, y + step, z); - addShape(appearance.getLine(p3, point3d3)); + // Line along Y axis + if ((y + step) <= maxY) { + addShape(appearance.getLine(p, new Point3D(x, y + step, z))); } - if ((z + step) <= p2.z) { - final Point3D point3d4 = new Point3D(x, y, z + step); - addShape(appearance.getLine(p3, point3d4)); + // Line along Z axis + if ((z + step) <= maxZ) { + addShape(appearance.getLine(p, new Point3D(x, y, z + step))); } - } + } + } } } 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 new file mode 100644 index 0000000..96900ae --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeArrow.java @@ -0,0 +1,284 @@ +/* + * 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.wireframe; + +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.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A 3D wireframe arrow shape composed of a cylindrical body and a conical tip. + * + *The arrow points from a start point to an end point, with the tip + * located at the end point. The wireframe consists of:
+ *Usage example:
+ *{@code
+ * // Create a red arrow pointing from origin to (100, -50, 200)
+ * LineAppearance appearance = new LineAppearance(2, Color.RED);
+ * WireframeArrow arrow = new WireframeArrow(
+ * new Point3D(0, 0, 0), // start point
+ * new Point3D(100, -50, 200), // end point
+ * 8, // body radius
+ * 20, // tip radius
+ * 40, // tip length
+ * 16, // segments
+ * appearance
+ * );
+ * shapeCollection.addShape(arrow);
+ * }
+ *
+ * @see WireframeCone
+ * @see WireframeCylinder
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonArrow
+ */
+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) { + super(); + + // Calculate direction and distance + final double dx = endPoint.x - startPoint.x; + final double dy = endPoint.y - startPoint.y; + final double dz = endPoint.z - startPoint.z; + final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + // Handle degenerate case: start and end are the same point + if (distance < 0.001) { + return; + } + + // Normalize direction vector + final double nx = dx / distance; + final double ny = dy / distance; + final double nz = dz / distance; + + // Calculate rotation to align Y-axis with direction + // Default arrow points in -Y direction (apex at lower Y) + // We need to rotate from (0, -1, 0) to (nx, ny, nz) + final Quaternion rotation = createRotationFromYAxis(nx, ny, nz); + final Matrix3x3 rotMatrix = rotation.toMatrix(); + + // Calculate body length (distance minus tip) + final double bodyLength = Math.max(0, distance - tipLength); + + // Build the arrow components + if (bodyLength > 0) { + addCylinderBody(startPoint, bodyRadius, bodyLength, segments, appearance, rotMatrix, nx, ny, nz); + } + addConeTip(endPoint, tipRadius, tipLength, segments, appearance, rotMatrix, nx, ny, nz); + } + + /** + * Creates a quaternion that rotates from the -Y axis to the given direction. + * + *The arrow by default points in the -Y direction. This method computes + * the rotation needed to align the arrow with the target direction vector.
+ * + * @param nx normalized direction X component + * @param ny normalized direction Y component + * @param nz normalized direction Z component + * @return quaternion representing the rotation + */ + private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) { + // Default direction is -Y (0, -1, 0) + // Target direction is (nx, ny, nz) + // Dot product: 0*nx + (-1)*ny + 0*nz = -ny + final double dot = -ny; + + // Check for parallel vectors + if (dot > 0.9999) { + // Direction is nearly -Y, no rotation needed + return Quaternion.identity(); + } + if (dot < -0.9999) { + // Direction is nearly +Y, rotate 180° around X axis + return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI); + } + + // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx) + // This gives the rotation axis + final double axisX = -nz; + final double axisY = 0; + final double axisZ = nx; + final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + final double normalizedAxisX = axisX / axisLength; + final double normalizedAxisZ = axisZ / axisLength; + + // Angle from dot product + final double angle = Math.acos(dot); + + return Quaternion.fromAxisAngle( + new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle); + } + + /** + * Adds the cylindrical body of the arrow. + * + *Local coordinate system: The arrow points in -Y direction in local space. + * Therefore, local -Y is toward the tip (front), and local +Y is toward the start (back).
+ * + * @param startPoint the origin of the arrow body + * @param radius the radius of the cylinder + * @param length the length of the cylinder + * @param segments the number of segments around the circumference + * @param appearance the line appearance + * @param rotMatrix the rotation matrix to apply + * @param dirX direction X component (for translation calculation) + * @param dirY direction Y component + * @param dirZ direction Z component + */ + private void addCylinderBody(final Point3D startPoint, final double radius, + final double length, final int segments, + final LineAppearance appearance, final Matrix3x3 rotMatrix, + final double dirX, final double dirY, final double dirZ) { + // Cylinder center is at startPoint + (length/2) * direction + final double centerX = startPoint.x + (length / 2.0) * dirX; + final double centerY = startPoint.y + (length / 2.0) * dirY; + final double centerZ = startPoint.z + (length / 2.0) * dirZ; + + // Generate ring vertices in local space, then rotate and translate + // Arrow points in -Y direction, so: + // - tipSideRing is at local -Y (toward arrow tip, front of cylinder) + // - startSideRing is at local +Y (toward arrow start, back of cylinder) + final Point3D[] tipSideRing = new Point3D[segments]; + final Point3D[] startSideRing = new Point3D[segments]; + + final double halfLength = length / 2.0; + + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double localX = radius * Math.cos(angle); + final double localZ = radius * Math.sin(angle); + + // Tip-side ring (at -halfLength in local Y = toward arrow tip) + final Point3D tipSideLocal = new Point3D(localX, -halfLength, localZ); + rotMatrix.transform(tipSideLocal, tipSideLocal); + tipSideLocal.x += centerX; + tipSideLocal.y += centerY; + tipSideLocal.z += centerZ; + tipSideRing[i] = tipSideLocal; + + // Start-side ring (at +halfLength in local Y = toward arrow start) + final Point3D startSideLocal = new Point3D(localX, halfLength, localZ); + rotMatrix.transform(startSideLocal, startSideLocal); + startSideLocal.x += centerX; + startSideLocal.y += centerY; + startSideLocal.z += centerZ; + startSideRing[i] = startSideLocal; + } + + // Create the circular rings + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + + // Tip-side ring line segment + addShape(appearance.getLine( + new Point3D(tipSideRing[i].x, tipSideRing[i].y, tipSideRing[i].z), + new Point3D(tipSideRing[next].x, tipSideRing[next].y, tipSideRing[next].z))); + + // Start-side ring line segment + addShape(appearance.getLine( + new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z), + new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z))); + } + + // Create vertical lines connecting the two rings + for (int i = 0; i < segments; i++) { + addShape(appearance.getLine( + new Point3D(tipSideRing[i].x, tipSideRing[i].y, tipSideRing[i].z), + new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z))); + } + } + + /** + * Adds the conical tip of the arrow. + * + *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 appearance the line appearance + * @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, + final LineAppearance appearance, final Matrix3x3 rotMatrix, + final double dirX, final double dirY, final double dirZ) { + // Apex is at endPoint (the arrow tip) + // Base center is at endPoint - length * direction (toward arrow start) + final double baseCenterX = endPoint.x - length * dirX; + final double baseCenterY = endPoint.y - length * dirY; + final double baseCenterZ = endPoint.z - length * dirZ; + + // Generate base ring vertices + // In local space, cone points in -Y direction, so base is at Y=0 + final Point3D[] baseRing = new Point3D[segments]; + + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double localX = radius * Math.cos(angle); + final double localZ = radius * Math.sin(angle); + + // Base ring vertices at local Y=0 + final Point3D local = new Point3D(localX, 0, localZ); + rotMatrix.transform(local, local); + local.x += baseCenterX; + local.y += baseCenterY; + local.z += baseCenterZ; + baseRing[i] = local; + } + + // Apex point (the arrow tip) + final Point3D apex = new Point3D(endPoint.x, endPoint.y, endPoint.z); + + // Create the circular base ring + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + addShape(appearance.getLine( + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z), + new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z))); + } + + // Create lines from apex to each base vertex + for (int i = 0; i < segments; i++) { + addShape(appearance.getLine( + new Point3D(apex.x, apex.y, apex.z), + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z))); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java index b776db4..9ff9bef 100755 --- a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeBox.java @@ -12,17 +12,33 @@ import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCom /** * A wireframe box (rectangular parallelepiped) composed of 12 line segments * representing the edges of the box. The box is axis-aligned, defined by two - * opposite corner points. + * diagonally opposite corner points. * *The wireframe consists of four edges along each axis: four edges parallel * to X, four parallel to Y, and four parallel to Z.
* + *Vertex layout:
+ *+ * cornerB (max) âââââââââ + * /â /â + * / â / â + * / â / â + * âââââ¼ââââââââââââ â + * â â â â + * â â â â + * â âââââââââââââââââ + * â / â / + * â / â / + * â/ â/ + * âââââââââââââââââ cornerA (min) + *+ * *
Usage example:
*{@code
* LineAppearance appearance = new LineAppearance(2, Color.GREEN);
- * Point3D corner1 = new Point3D(-50, -50, -50);
- * Point3D corner2 = new Point3D(50, 50, 50);
- * WireframeBox box = new WireframeBox(corner1, corner2, appearance);
+ * Point3D cornerA = new Point3D(-50, -50, -50);
+ * Point3D cornerB = new Point3D(50, 50, 50);
+ * WireframeBox box = new WireframeBox(cornerA, cornerB, appearance);
* shapeCollection.addShape(box);
* }
*
@@ -46,44 +62,43 @@ public class WireframeBox extends AbstractCompositeShape {
}
/**
- * Constructs a wireframe box from two opposite corner points. The corners
- * do not need to be in any particular min/max order; the constructor uses
- * each coordinate independently to form all eight vertices of the box.
+ * Constructs a wireframe box from two diagonally opposite corner points.
+ * The corners do not need to be in any particular min/max order; the constructor
+ * uses each coordinate independently to form all eight vertices of the box.
*
- * @param p1 the first corner point of the box
- * @param p2 the diagonally opposite corner point of the box
+ * @param cornerA the first corner point of the box
+ * @param cornerB the diagonally opposite corner point of the box
* @param appearance the line appearance (color, width) used for all 12 edges
*/
- public WireframeBox(final Point3D p1, final Point3D p2,
+ public WireframeBox(final Point3D cornerA, final Point3D cornerB,
final LineAppearance appearance) {
super();
- addShape(appearance.getLine(new Point3D(p1.x, p1.y, p1.z), new Point3D(
- p2.x, p1.y, p1.z)));
- addShape(appearance.getLine(new Point3D(p1.x, p2.y, p1.z), new Point3D(
- p2.x, p2.y, p1.z)));
- addShape(appearance.getLine(new Point3D(p1.x, p1.y, p1.z), new Point3D(
- p1.x, p2.y, p1.z)));
- addShape(appearance.getLine(new Point3D(p2.x, p1.y, p1.z), new Point3D(
- p2.x, p2.y, p1.z)));
+ // Determine actual min/max bounds (corners may be in any order)
+ final double minX = Math.min(cornerA.x, cornerB.x);
+ final double maxX = Math.max(cornerA.x, cornerB.x);
+ final double minY = Math.min(cornerA.y, cornerB.y);
+ final double maxY = Math.max(cornerA.y, cornerB.y);
+ final double minZ = Math.min(cornerA.z, cornerB.z);
+ final double maxZ = Math.max(cornerA.z, cornerB.z);
- addShape(appearance.getLine(new Point3D(p1.x, p1.y, p2.z), new Point3D(
- p2.x, p1.y, p2.z)));
- addShape(appearance.getLine(new Point3D(p1.x, p2.y, p2.z), new Point3D(
- p2.x, p2.y, p2.z)));
- addShape(appearance.getLine(new Point3D(p1.x, p1.y, p2.z), new Point3D(
- p1.x, p2.y, p2.z)));
- addShape(appearance.getLine(new Point3D(p2.x, p1.y, p2.z), new Point3D(
- p2.x, p2.y, p2.z)));
+ // Generate the 12 edges of the box
+ // Four edges along X axis (varying X, fixed Y and Z)
+ addShape(appearance.getLine(new Point3D(minX, minY, minZ), new Point3D(maxX, minY, minZ)));
+ addShape(appearance.getLine(new Point3D(minX, maxY, minZ), new Point3D(maxX, maxY, minZ)));
+ addShape(appearance.getLine(new Point3D(minX, minY, maxZ), new Point3D(maxX, minY, maxZ)));
+ addShape(appearance.getLine(new Point3D(minX, maxY, maxZ), new Point3D(maxX, maxY, maxZ)));
- addShape(appearance.getLine(new Point3D(p1.x, p1.y, p1.z), new Point3D(
- p1.x, p1.y, p2.z)));
- addShape(appearance.getLine(new Point3D(p1.x, p2.y, p1.z), new Point3D(
- p1.x, p2.y, p2.z)));
- addShape(appearance.getLine(new Point3D(p2.x, p1.y, p1.z), new Point3D(
- p2.x, p1.y, p2.z)));
- addShape(appearance.getLine(new Point3D(p2.x, p2.y, p1.z), new Point3D(
- p2.x, p2.y, p2.z)));
- }
+ // Four edges along Y axis (varying Y, fixed X and Z)
+ addShape(appearance.getLine(new Point3D(minX, minY, minZ), new Point3D(minX, maxY, minZ)));
+ addShape(appearance.getLine(new Point3D(maxX, minY, minZ), new Point3D(maxX, maxY, minZ)));
+ addShape(appearance.getLine(new Point3D(minX, minY, maxZ), new Point3D(minX, maxY, maxZ)));
+ addShape(appearance.getLine(new Point3D(maxX, minY, maxZ), new Point3D(maxX, maxY, maxZ)));
+ // Four edges along Z axis (varying Z, fixed X and Y)
+ addShape(appearance.getLine(new Point3D(minX, minY, minZ), new Point3D(minX, minY, maxZ)));
+ addShape(appearance.getLine(new Point3D(maxX, minY, minZ), new Point3D(maxX, minY, maxZ)));
+ addShape(appearance.getLine(new Point3D(minX, maxY, minZ), new Point3D(minX, maxY, maxZ)));
+ addShape(appearance.getLine(new Point3D(maxX, maxY, minZ), new Point3D(maxX, maxY, maxZ)));
+ }
}
diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCone.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCone.java
new file mode 100644
index 0000000..76a31b2
--- /dev/null
+++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCone.java
@@ -0,0 +1,247 @@
+/*
+ * 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.wireframe;
+
+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.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+/**
+ * A wireframe cone that can be oriented in any direction.
+ *
+ * The cone has a circular base and a single apex (tip) point. The wireframe + * consists of:
+ *Two constructors are provided for different use cases:
+ * + *Usage examples:
+ *{@code
+ * // Directional constructor: cone pointing from apex toward base
+ * LineAppearance appearance = new LineAppearance(2, Color.RED);
+ * WireframeCone directionalCone = new WireframeCone(
+ * new Point3D(0, -100, 0), // apex (tip of the cone)
+ * new Point3D(0, 50, 0), // baseCenter (cone points toward this)
+ * 50, // radius of the circular base
+ * 16, // segments
+ * appearance
+ * );
+ *
+ * // Y-axis aligned constructor: cone pointing upward
+ * WireframeCone verticalCone = new WireframeCone(
+ * new Point3D(0, 0, 300), // baseCenter
+ * 50, // radius
+ * 100, // height
+ * 16, // segments
+ * appearance
+ * );
+ * }
+ *
+ * @see WireframeCylinder
+ * @see WireframeArrow
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCone
+ */
+public class WireframeCone extends AbstractCompositeShape {
+
+ /**
+ * Constructs a wireframe cone pointing from apex toward base center.
+ *
+ * This is the recommended constructor for placing cones in 3D space. + * The cone's apex (tip) is at {@code apexPoint}, and the circular base + * is centered at {@code baseCenterPoint}. The cone points in the direction + * from apex to base center.
+ * + *Coordinate interpretation:
+ *This constructor creates a Y-axis aligned cone. The apex is positioned + * at {@code baseCenter.y - height} (above the base in the negative Y direction). + * For cones pointing in arbitrary directions, use + * {@link #WireframeCone(Point3D, Point3D, double, int, LineAppearance)} instead.
+ * + *Coordinate system: The cone points in -Y direction (apex at lower Y). + * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height. + * In Sixth 3D's coordinate system, "up" visually is negative Y.
+ * + * @param baseCenter the center point of the cone's circular base in 3D space + * @param radius the radius of the circular base + * @param height the height of the cone from base center to apex + * @param segments the number of segments around the circumference. + * Higher values create smoother cones. Minimum is 3. + * @param appearance the line appearance (color, width) used for all lines + */ + public WireframeCone(final Point3D baseCenter, final double radius, + final double height, final int segments, + final LineAppearance appearance) { + super(); + + // Apex is above the base (negative Y direction in this coordinate system) + final double apexY = baseCenter.y - height; + final Point3D apex = new Point3D(baseCenter.x, apexY, baseCenter.z); + + // Generate vertices around the circular base + final Point3D[] baseRing = new Point3D[segments]; + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double x = baseCenter.x + radius * Math.cos(angle); + final double z = baseCenter.z + radius * Math.sin(angle); + baseRing[i] = new Point3D(x, baseCenter.y, z); + } + + // Create the circular base ring + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + addShape(appearance.getLine( + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z), + new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z))); + } + + // Create lines from apex to each base vertex + for (int i = 0; i < segments; i++) { + addShape(appearance.getLine( + new Point3D(apex.x, apex.y, apex.z), + new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z))); + } + } + + /** + * Creates a quaternion that rotates from the -Y axis to the given direction. + * + *The cone by default points in the -Y direction (apex at origin, base at -Y). + * This method computes the rotation needed to align the cone with the target + * direction vector.
+ * + * @param nx normalized direction X component + * @param ny normalized direction Y component + * @param nz normalized direction Z component + * @return quaternion representing the rotation + */ + private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) { + // Default direction is -Y (0, -1, 0) + // Target direction is (nx, ny, nz) + // Dot product: 0*nx + (-1)*ny + 0*nz = -ny + final double dot = -ny; + + // Check for parallel vectors + if (dot > 0.9999) { + // Direction is nearly -Y, no rotation needed + return Quaternion.identity(); + } + if (dot < -0.9999) { + // Direction is nearly +Y, rotate 180° around X axis + return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI); + } + + // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx) + // This gives the rotation axis + final double axisX = -nz; + final double axisY = 0; + final double axisZ = nx; + final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + final double normalizedAxisX = axisX / axisLength; + final double normalizedAxisZ = axisZ / axisLength; + + // Angle from dot product + final double angle = Math.acos(dot); + + return Quaternion.fromAxisAngle( + new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCylinder.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCylinder.java new file mode 100644 index 0000000..7bd1381 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframeCylinder.java @@ -0,0 +1,188 @@ +/* + * 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.wireframe; + +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.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A wireframe cylinder defined by two end points. + * + *The cylinder extends from startPoint to endPoint with circular rings at both + * ends. The number of segments determines the smoothness of the circular rings. + * The wireframe consists of:
+ *Usage example:
+ *{@code
+ * // Create a vertical cylinder from Y=100 to Y=200
+ * LineAppearance appearance = new LineAppearance(2, Color.RED);
+ * WireframeCylinder cylinder = new WireframeCylinder(
+ * new Point3D(0, 100, 0), // start point (bottom)
+ * new Point3D(0, 200, 0), // end point (top)
+ * 10, // radius
+ * 16, // segments
+ * appearance
+ * );
+ *
+ * // Create a horizontal cylinder along X axis
+ * WireframeCylinder pipe = new WireframeCylinder(
+ * new Point3D(-50, 0, 0),
+ * new Point3D(50, 0, 0),
+ * 5, 12, appearance
+ * );
+ * }
+ *
+ * @see WireframeCone
+ * @see WireframeArrow
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCylinder
+ */
+public class WireframeCylinder extends AbstractCompositeShape {
+
+ /**
+ * Constructs a wireframe cylinder between two end points.
+ *
+ * The cylinder has circular rings at both startPoint and endPoint, + * connected by lines between corresponding vertices. The orientation is + * automatically calculated from the direction between the two points.
+ * + * @param startPoint the center of the first ring + * @param endPoint the center of the second ring + * @param radius the radius of the cylinder + * @param segments the number of segments around the circumference. + * Higher values create smoother cylinders. Minimum is 3. + * @param appearance the line appearance (color, width) used for all lines + */ + public WireframeCylinder(final Point3D startPoint, final Point3D endPoint, + final double radius, final int segments, + final LineAppearance appearance) { + super(); + + // Calculate direction and distance + final double dx = endPoint.x - startPoint.x; + final double dy = endPoint.y - startPoint.y; + final double dz = endPoint.z - startPoint.z; + final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz); + + // Handle degenerate case: start and end are the same point + if (distance < 0.001) { + return; + } + + // Normalize direction vector + final double nx = dx / distance; + final double ny = dy / distance; + final double nz = dz / distance; + + // Calculate rotation to align Y-axis with direction + // Default cylinder is aligned along Y-axis + // We need to rotate from (0, 1, 0) to (nx, ny, nz) + final Quaternion rotation = createRotationFromYAxis(nx, ny, nz); + final Matrix3x3 rotMatrix = rotation.toMatrix(); + + // Cylinder center is at midpoint between start and end + final double centerX = (startPoint.x + endPoint.x) / 2.0; + final double centerY = (startPoint.y + endPoint.y) / 2.0; + final double centerZ = (startPoint.z + endPoint.z) / 2.0; + final double halfLength = distance / 2.0; + + // Generate ring vertices in local space, then rotate and translate + // In local space: cylinder is aligned along Y-axis + // - startRing is at local -Y (toward startPoint) + // - endRing is at local +Y (toward endPoint) + final Point3D[] startRing = new Point3D[segments]; + final Point3D[] endRing = new Point3D[segments]; + + for (int i = 0; i < segments; i++) { + final double angle = 2.0 * Math.PI * i / segments; + final double localX = radius * Math.cos(angle); + final double localZ = radius * Math.sin(angle); + + // Start ring (at -halfLength in local Y = toward startPoint) + final Point3D startLocal = new Point3D(localX, -halfLength, localZ); + rotMatrix.transform(startLocal, startLocal); + startLocal.x += centerX; + startLocal.y += centerY; + startLocal.z += centerZ; + startRing[i] = startLocal; + + // End ring (at +halfLength in local Y = toward endPoint) + final Point3D endLocal = new Point3D(localX, halfLength, localZ); + rotMatrix.transform(endLocal, endLocal); + endLocal.x += centerX; + endLocal.y += centerY; + endLocal.z += centerZ; + endRing[i] = endLocal; + } + + // Create the circular rings + for (int i = 0; i < segments; i++) { + final int next = (i + 1) % segments; + + // Start ring line segment + addShape(appearance.getLine( + new Point3D(startRing[i].x, startRing[i].y, startRing[i].z), + new Point3D(startRing[next].x, startRing[next].y, startRing[next].z))); + + // End ring line segment + addShape(appearance.getLine( + new Point3D(endRing[i].x, endRing[i].y, endRing[i].z), + new Point3D(endRing[next].x, endRing[next].y, endRing[next].z))); + } + + // Create vertical lines connecting the two rings + for (int i = 0; i < segments; i++) { + addShape(appearance.getLine( + new Point3D(startRing[i].x, startRing[i].y, startRing[i].z), + new Point3D(endRing[i].x, endRing[i].y, endRing[i].z))); + } + } + + /** + * Creates a quaternion that rotates from the +Y axis to the given direction. + * + * @param nx normalized direction X component + * @param ny normalized direction Y component + * @param nz normalized direction Z component + * @return quaternion representing the rotation + */ + private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) { + // Default direction is +Y (0, 1, 0) + // Target direction is (nx, ny, nz) + // Dot product: 0*nx + 1*ny + 0*nz = ny + final double dot = ny; + + // Check for parallel vectors + if (dot > 0.9999) { + // Direction is nearly +Y, no rotation needed + return Quaternion.identity(); + } + if (dot < -0.9999) { + // Direction is nearly -Y, rotate 180° around X axis + return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI); + } + + // Cross product: (0, 1, 0) x (nx, ny, nz) = (nz, 0, -nx) + // This gives the rotation axis + final double axisX = nz; + final double axisY = 0; + final double axisZ = -nx; + final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + final double normalizedAxisX = axisX / axisLength; + final double normalizedAxisZ = axisZ / axisLength; + + // Angle from dot product + final double angle = Math.acos(dot); + + return Quaternion.fromAxisAngle( + new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle); + } +} \ No newline at end of file diff --git a/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframePyramid.java b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframePyramid.java new file mode 100644 index 0000000..242cc03 --- /dev/null +++ b/src/main/java/eu/svjatoslav/sixth/e3d/renderer/raster/shapes/composite/wireframe/WireframePyramid.java @@ -0,0 +1,246 @@ +/* + * 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.wireframe; + +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.renderer.raster.shapes.basic.line.LineAppearance; +import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape; + +/** + * A wireframe square-based pyramid that can be oriented in any direction. + * + *The pyramid has a square base and four triangular faces meeting at an apex + * (tip). The wireframe consists of:
+ *Two constructors are provided for different use cases:
+ * + *Usage examples:
+ *{@code
+ * // Directional constructor: pyramid pointing from apex toward base
+ * LineAppearance appearance = new LineAppearance(2, Color.RED);
+ * WireframePyramid directionalPyramid = new WireframePyramid(
+ * new Point3D(0, -100, 0), // apex (tip of the pyramid)
+ * new Point3D(0, 50, 0), // baseCenter (pyramid points toward this)
+ * 50, // baseSize (half-width of square base)
+ * appearance
+ * );
+ *
+ * // Y-axis aligned constructor: pyramid pointing upward
+ * WireframePyramid verticalPyramid = new WireframePyramid(
+ * new Point3D(0, 0, 300), // baseCenter
+ * 50, // baseSize (half-width of square base)
+ * 100, // height
+ * appearance
+ * );
+ * }
+ *
+ * @see WireframeCone
+ * @see WireframeCube
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonPyramid
+ */
+public class WireframePyramid extends AbstractCompositeShape {
+
+ /**
+ * Constructs a wireframe square-based pyramid pointing from apex toward base center.
+ *
+ * This is the recommended constructor for placing pyramids in 3D space. + * The pyramid's apex (tip) is at {@code apexPoint}, and the square base + * is centered at {@code baseCenter}. The pyramid points in the direction + * from apex to base center.
+ * + *Coordinate interpretation:
+ *This constructor creates a Y-axis aligned pyramid. The apex is positioned + * at {@code baseCenter.y - height} (above the base in the negative Y direction). + * For pyramids pointing in arbitrary directions, use + * {@link #WireframePyramid(Point3D, Point3D, double, LineAppearance)} instead.
+ * + *Coordinate system: The pyramid points in -Y direction (apex at lower Y). + * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height. + * In Sixth 3D's coordinate system, "up" visually is negative Y.
+ * + * @param baseCenter the center point of the pyramid's base in 3D space + * @param baseSize the half-width of the square base; the base extends + * this distance from the center along X and Z axes, + * giving a total base edge length of {@code 2 * baseSize} + * @param height the height of the pyramid from base center to apex + * @param appearance the line appearance (color, width) used for all lines + */ + public WireframePyramid(final Point3D baseCenter, final double baseSize, + final double height, final LineAppearance appearance) { + super(); + + final double halfBase = baseSize; + final double apexY = baseCenter.y - height; + final double baseY = baseCenter.y; + + // Base corners arranged counter-clockwise when viewed from above (+Y) + // Naming: "negative/positive X" and "negative/positive Z" relative to base center + final Point3D negXnegZ = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z - halfBase); + final Point3D posXnegZ = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z - halfBase); + final Point3D posXposZ = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z + halfBase); + final Point3D negXposZ = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z + halfBase); + final Point3D apex = new Point3D(baseCenter.x, apexY, baseCenter.z); + + // Create the four lines forming the square base + addShape(appearance.getLine(negXnegZ, posXnegZ)); + addShape(appearance.getLine(posXnegZ, posXposZ)); + addShape(appearance.getLine(posXposZ, negXposZ)); + addShape(appearance.getLine(negXposZ, negXnegZ)); + + // Create the four lines from apex to each base corner + addShape(appearance.getLine(apex, negXnegZ)); + addShape(appearance.getLine(apex, posXnegZ)); + addShape(appearance.getLine(apex, posXposZ)); + addShape(appearance.getLine(apex, negXposZ)); + } + + /** + * Creates a quaternion that rotates from the -Y axis to the given direction. + * + *The pyramid by default points in the -Y direction (apex at origin, base at -Y). + * This method computes the rotation needed to align the pyramid with the target + * direction vector.
+ * + * @param nx normalized direction X component + * @param ny normalized direction Y component + * @param nz normalized direction Z component + * @return quaternion representing the rotation + */ + private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) { + // Default direction is -Y (0, -1, 0) + // Target direction is (nx, ny, nz) + // Dot product: 0*nx + (-1)*ny + 0*nz = -ny + final double dot = -ny; + + // Check for parallel vectors + if (dot > 0.9999) { + // Direction is nearly -Y, no rotation needed + return Quaternion.identity(); + } + if (dot < -0.9999) { + // Direction is nearly +Y, rotate 180° around X axis + return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI); + } + + // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx) + // This gives the rotation axis + final double axisX = -nz; + final double axisY = 0; + final double axisZ = nx; + final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ); + final double normalizedAxisX = axisX / axisLength; + final double normalizedAxisZ = axisZ / axisLength; + + // Angle from dot product + final double angle = Math.acos(dot); + + return Quaternion.fromAxisAngle( + new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle); + } +} \ No newline at end of file