e3c038efe951706c2706b410ddcdd566825bbb65
[sixth-3d.git] /
1 /*
2  * Sixth 3D engine. Author: Svjatoslav Agejenko.
3  * This project is released under Creative Commons Zero (CC0) license.
4  */
5 package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid;
6
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;
13
14 /**
15  * A solid square-based pyramid that can be oriented in any direction.
16  *
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>
19  *
20  * <ul>
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>
26  * </ul>
27  *
28  * <p><b>Usage examples:</b></p>
29  * <pre>{@code
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)
35  *     Color.RED
36  * );
37  *
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)
42  *     100,                       // height
43  *     Color.BLUE
44  * );
45  * }</pre>
46  *
47  * @see SolidPolygonCone
48  * @see SolidPolygonCube
49  * @see SolidPolygon
50  */
51 public class SolidPolygonPyramid extends AbstractCompositeShape {
52
53     /**
54      * Constructs a solid square-based pyramid pointing from apex toward base center.
55      *
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>
60      *
61      * <p><b>Coordinate interpretation:</b></p>
62      * <ul>
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>
69      * </ul>
70      *
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
78      */
79     public SolidPolygonPyramid(final Point3D apexPoint, final Point3D baseCenter,
80                                final double baseSize, final Color color) {
81         super();
82
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);
88
89         // Handle degenerate case: apex and base center are the same point
90         if (height < 0.001) {
91             return;
92         }
93
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;
98
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();
104
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];
110
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
118         };
119
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;
127         }
128
129         // Apex point (the pyramid tip)
130         final Point3D apex = new Point3D(apexPoint.x, apexPoint.y, apexPoint.z);
131
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),
141                     color));
142         }
143
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),
154                 color));
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),
159                 color));
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),
164                 color));
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),
169                 color));
170
171         setBackfaceCulling(true);
172     }
173
174     /**
175      * Constructs a solid square-based pyramid with base centered at the given point,
176      * pointing in the -Y direction.
177      *
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>
182      *
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>
186      *
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
193      */
194     public SolidPolygonPyramid(final Point3D baseCenter, final double baseSize,
195                                final double height, final Color color) {
196         super();
197
198         final double halfBase = baseSize;
199         final double apexY = baseCenter.y - height;
200         final double baseY = baseCenter.y;
201
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);
209
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));
216
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));
224
225         setBackfaceCulling(true);
226     }
227
228     /**
229      * Creates a quaternion that rotates from the -Y axis to the given direction.
230      *
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>
234      *
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
239      */
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;
245
246         // Check for parallel vectors
247         if (dot > 0.9999) {
248             // Direction is nearly -Y, no rotation needed
249             return Quaternion.identity();
250         }
251         if (dot < -0.9999) {
252             // Direction is nearly +Y, rotate 180° around X axis
253             return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
254         }
255
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;
264
265         // Angle from dot product
266         final double angle = Math.acos(dot);
267
268         return Quaternion.fromAxisAngle(
269                 new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);
270     }
271 }