2 * Sixth 3D engine. Author: Svjatoslav Agejenko.
3 * This project is released under Creative Commons Zero (CC0) license.
5 package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid;
7 import eu.svjatoslav.sixth.e3d.geometry.Point3D;
8 import eu.svjatoslav.sixth.e3d.math.Matrix3x3;
9 import eu.svjatoslav.sixth.e3d.math.Quaternion;
10 import eu.svjatoslav.sixth.e3d.renderer.raster.Color;
11 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.solidpolygon.SolidPolygon;
12 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
15 * A solid square-based pyramid that can be oriented in any direction.
17 * <p>The pyramid has a square base and four triangular faces meeting at an apex
18 * (tip). Two constructors are provided for different use cases:</p>
21 * <li><b>Directional (recommended):</b> Specify apex point and base center point.
22 * The pyramid points from apex toward the base center. This allows arbitrary
23 * orientation and is the most intuitive API.</li>
24 * <li><b>Y-axis aligned:</b> Specify base center, base size, and height. The pyramid
25 * points in -Y direction (apex at lower Y). Useful for simple vertical pyramids.</li>
28 * <p><b>Usage examples:</b></p>
30 * // Directional constructor: pyramid pointing from apex toward base
31 * SolidPolygonPyramid directionalPyramid = new SolidPolygonPyramid(
32 * new Point3D(0, -100, 0), // apex (tip of the pyramid)
33 * new Point3D(0, 50, 0), // baseCenter (pyramid points toward this)
34 * 50, // baseSize (half-width of square base)
38 * // Y-axis aligned constructor: pyramid pointing upward
39 * SolidPolygonPyramid verticalPyramid = new SolidPolygonPyramid(
40 * new Point3D(0, 0, 300), // baseCenter
41 * 50, // baseSize (half-width of square base)
47 * @see SolidPolygonCone
48 * @see SolidPolygonCube
51 public class SolidPolygonPyramid extends AbstractCompositeShape {
54 * Constructs a solid square-based pyramid pointing from apex toward base center.
56 * <p>This is the recommended constructor for placing pyramids in 3D space.
57 * The pyramid's apex (tip) is at {@code apexPoint}, and the square base
58 * is centered at {@code baseCenter}. The pyramid points in the direction
59 * from apex to base center.</p>
61 * <p><b>Coordinate interpretation:</b></p>
63 * <li>{@code apexPoint} - the sharp tip of the pyramid</li>
64 * <li>{@code baseCenter} - the center of the square base; the pyramid
65 * "points" in this direction from the apex</li>
66 * <li>{@code baseSize} - half the width of the square base; the base
67 * extends this distance from the center along perpendicular axes</li>
68 * <li>The distance between apex and base center determines the pyramid height</li>
71 * @param apexPoint the position of the pyramid's tip (apex)
72 * @param baseCenter the center point of the square base; the pyramid
73 * points from apex toward this point
74 * @param baseSize the half-width of the square base; the base extends
75 * this distance from the center, giving a total base
76 * edge length of {@code 2 * baseSize}
77 * @param color the fill color applied to all faces of the pyramid
79 public SolidPolygonPyramid(final Point3D apexPoint, final Point3D baseCenter,
80 final double baseSize, final Color color) {
83 // Calculate direction and height from apex to base center
84 final double dx = baseCenter.x - apexPoint.x;
85 final double dy = baseCenter.y - apexPoint.y;
86 final double dz = baseCenter.z - apexPoint.z;
87 final double height = Math.sqrt(dx * dx + dy * dy + dz * dz);
89 // Handle degenerate case: apex and base center are the same point
94 // Normalize direction vector (from apex toward base)
95 final double nx = dx / height;
96 final double ny = dy / height;
97 final double nz = dz / height;
99 // Calculate rotation to align Y-axis with direction
100 // Default pyramid points in -Y direction (apex at origin, base at -Y)
101 // We need to rotate from (0, -1, 0) to (nx, ny, nz)
102 final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
103 final Matrix3x3 rotMatrix = rotation.toMatrix();
105 // Generate base corner vertices in local space, then rotate and translate
106 // In local space: apex is at origin, base is at Y = -height
107 // Base corners form a square centered at (0, -height, 0)
108 final double h = baseSize;
109 final Point3D[] baseCorners = new Point3D[4];
111 // Local space corner positions (before rotation)
112 // Arranged clockwise when viewed from apex (from +Y)
113 final double[][] localCorners = {
114 {-h, -height, -h}, // corner 0: negative X, negative Z
115 {+h, -height, -h}, // corner 1: positive X, negative Z
116 {+h, -height, +h}, // corner 2: positive X, positive Z
117 {-h, -height, +h} // corner 3: negative X, positive Z
120 for (int i = 0; i < 4; i++) {
121 final Point3D local = new Point3D(localCorners[i][0], localCorners[i][1], localCorners[i][2]);
122 rotMatrix.transform(local, local);
123 local.x += apexPoint.x;
124 local.y += apexPoint.y;
125 local.z += apexPoint.z;
126 baseCorners[i] = local;
129 // Apex point (the pyramid tip)
130 final Point3D apex = new Point3D(apexPoint.x, apexPoint.y, apexPoint.z);
132 // Create the four triangular faces connecting apex to base edges
133 // Winding: next → current → apex creates CCW winding when viewed from outside
134 // (Base corners go CW when viewed from apex, so we reverse to get outward normals)
135 for (int i = 0; i < 4; i++) {
136 final int next = (i + 1) % 4;
137 addShape(new SolidPolygon(
138 new Point3D(baseCorners[next].x, baseCorners[next].y, baseCorners[next].z),
139 new Point3D(baseCorners[i].x, baseCorners[i].y, baseCorners[i].z),
140 new Point3D(apex.x, apex.y, apex.z),
144 // Create base cap (square bottom face)
145 // The cap faces away from the apex (in the direction the pyramid points).
146 // Base corners go CW when viewed from apex, so CW when viewed from apex means
147 // CCW when viewed from outside (base side). Use CCW ordering for outward normal.
148 // Triangulate the square base: (center, 3, 0) and (center, 0, 1) and
149 // (center, 1, 2) and (center, 2, 3)
150 addShape(new SolidPolygon(
151 new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
152 new Point3D(baseCorners[3].x, baseCorners[3].y, baseCorners[3].z),
153 new Point3D(baseCorners[0].x, baseCorners[0].y, baseCorners[0].z),
155 addShape(new SolidPolygon(
156 new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
157 new Point3D(baseCorners[0].x, baseCorners[0].y, baseCorners[0].z),
158 new Point3D(baseCorners[1].x, baseCorners[1].y, baseCorners[1].z),
160 addShape(new SolidPolygon(
161 new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
162 new Point3D(baseCorners[1].x, baseCorners[1].y, baseCorners[1].z),
163 new Point3D(baseCorners[2].x, baseCorners[2].y, baseCorners[2].z),
165 addShape(new SolidPolygon(
166 new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
167 new Point3D(baseCorners[2].x, baseCorners[2].y, baseCorners[2].z),
168 new Point3D(baseCorners[3].x, baseCorners[3].y, baseCorners[3].z),
171 setBackfaceCulling(true);
175 * Constructs a solid square-based pyramid with base centered at the given point,
176 * pointing in the -Y direction.
178 * <p>This constructor creates a Y-axis aligned pyramid. The apex is positioned
179 * at {@code baseCenter.y - height} (above the base in the negative Y direction).
180 * For pyramids pointing in arbitrary directions, use
181 * {@link #SolidPolygonPyramid(Point3D, Point3D, double, Color)} instead.</p>
183 * <p><b>Coordinate system:</b> The pyramid points in -Y direction (apex at lower Y).
184 * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height.
185 * In Sixth 3D's coordinate system, "up" visually is negative Y.</p>
187 * @param baseCenter the center point of the pyramid's base in 3D space
188 * @param baseSize the half-width of the square base; the base extends
189 * this distance from the center along X and Z axes,
190 * giving a total base edge length of {@code 2 * baseSize}
191 * @param height the height of the pyramid from base center to apex
192 * @param color the fill color applied to all faces of the pyramid
194 public SolidPolygonPyramid(final Point3D baseCenter, final double baseSize,
195 final double height, final Color color) {
198 final double halfBase = baseSize;
199 final double apexY = baseCenter.y - height;
200 final double baseY = baseCenter.y;
202 // Base corners arranged clockwise when viewed from above (+Y)
203 // Naming: "negative/positive X" and "negative/positive Z" relative to base center
204 final Point3D negXnegZ = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z - halfBase);
205 final Point3D posXnegZ = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z - halfBase);
206 final Point3D posXposZ = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z + halfBase);
207 final Point3D negXposZ = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z + halfBase);
208 final Point3D apex = new Point3D(baseCenter.x, apexY, baseCenter.z);
210 // Four triangular faces from apex to base edges
211 // Winding: apex → current → next creates CCW when viewed from outside
212 addShape(new SolidPolygon(negXnegZ, posXnegZ, apex, color));
213 addShape(new SolidPolygon(posXnegZ, posXposZ, apex, color));
214 addShape(new SolidPolygon(posXposZ, negXposZ, apex, color));
215 addShape(new SolidPolygon(negXposZ, negXnegZ, apex, color));
217 // Base cap (square bottom face)
218 // Cap faces +Y (downward, away from apex). The base is at higher Y than apex.
219 // Base corners go CW when viewed from apex (looking in +Y direction).
220 // For outward normal (+Y direction), we need CCW ordering when viewed from +Y.
221 // CCW from +Y is: 3 → 2 → 1 → 0, so triangles: (3, 2, 1) and (3, 1, 0)
222 addShape(new SolidPolygon(negXposZ, posXposZ, posXnegZ, color));
223 addShape(new SolidPolygon(negXposZ, posXnegZ, negXnegZ, color));
225 setBackfaceCulling(true);
229 * Creates a quaternion that rotates from the -Y axis to the given direction.
231 * <p>The pyramid by default points in the -Y direction (apex at origin, base at -Y).
232 * This method computes the rotation needed to align the pyramid with the target
233 * direction vector.</p>
235 * @param nx normalized direction X component
236 * @param ny normalized direction Y component
237 * @param nz normalized direction Z component
238 * @return quaternion representing the rotation
240 private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) {
241 // Default direction is -Y (0, -1, 0)
242 // Target direction is (nx, ny, nz)
243 // Dot product: 0*nx + (-1)*ny + 0*nz = -ny
244 final double dot = -ny;
246 // Check for parallel vectors
248 // Direction is nearly -Y, no rotation needed
249 return Quaternion.identity();
252 // Direction is nearly +Y, rotate 180° around X axis
253 return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
256 // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx)
257 // This gives the rotation axis
258 final double axisX = -nz;
259 final double axisY = 0;
260 final double axisZ = nx;
261 final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
262 final double normalizedAxisX = axisX / axisLength;
263 final double normalizedAxisZ = axisZ / axisLength;
265 // Angle from dot product
266 final double angle = Math.acos(dot);
268 return Quaternion.fromAxisAngle(
269 new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);