276e6d4cf21189f6559011c87fa0d96288387e04
[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 cylinder defined by two end points.
16  *
17  * <p>The cylinder extends from startPoint to endPoint with circular caps at both
18  * ends. The number of segments determines the smoothness of the curved surface.</p>
19  *
20  * <p><b>Usage example:</b></p>
21  * <pre>{@code
22  * // Create a vertical cylinder from Y=100 to Y=200
23  * SolidPolygonCylinder cylinder = new SolidPolygonCylinder(
24  *     new Point3D(0, 100, 0),   // start point (bottom)
25  *     new Point3D(0, 200, 0),   // end point (top)
26  *     10,                        // radius
27  *     16,                        // segments
28  *     Color.RED                  // color
29  * );
30  *
31  * // Create a horizontal cylinder along X axis
32  * SolidPolygonCylinder pipe = new SolidPolygonCylinder(
33  *     new Point3D(-50, 0, 0),
34  *     new Point3D(50, 0, 0),
35  *     5, 12, Color.BLUE
36  * );
37  * }</pre>
38  *
39  * @see SolidPolygonCone
40  * @see SolidPolygonArrow
41  * @see SolidPolygon
42  */
43 public class SolidPolygonCylinder extends AbstractCompositeShape {
44
45     /**
46      * Constructs a solid cylinder between two end points.
47      *
48      * <p>The cylinder has circular caps at both startPoint and endPoint,
49      * connected by a curved side surface. The orientation is automatically
50      * calculated from the direction between the two points.</p>
51      *
52      * @param startPoint the center of the first cap
53      * @param endPoint   the center of the second cap
54      * @param radius     the radius of the cylinder
55      * @param segments   the number of segments around the circumference.
56      *                   Higher values create smoother cylinders. Minimum is 3.
57      * @param color      the fill color applied to all polygons
58      */
59     public SolidPolygonCylinder(final Point3D startPoint, final Point3D endPoint,
60                                 final double radius, final int segments,
61                                 final Color color) {
62         super();
63
64         // Calculate direction and distance
65         final double dx = endPoint.x - startPoint.x;
66         final double dy = endPoint.y - startPoint.y;
67         final double dz = endPoint.z - startPoint.z;
68         final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
69
70         // Handle degenerate case: start and end are the same point
71         if (distance < 0.001) {
72             return;
73         }
74
75         // Normalize direction vector
76         final double nx = dx / distance;
77         final double ny = dy / distance;
78         final double nz = dz / distance;
79
80         // Calculate rotation to align Y-axis with direction
81         // Default cylinder is aligned along Y-axis
82         // We need to rotate from (0, 1, 0) to (nx, ny, nz)
83         final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
84         final Matrix3x3 rotMatrix = rotation.toMatrix();
85
86         // Cylinder center is at midpoint between start and end
87         final double centerX = (startPoint.x + endPoint.x) / 2.0;
88         final double centerY = (startPoint.y + endPoint.y) / 2.0;
89         final double centerZ = (startPoint.z + endPoint.z) / 2.0;
90         final double halfLength = distance / 2.0;
91
92         // Generate ring vertices in local space, then rotate and translate
93         // In local space: cylinder is aligned along Y-axis
94         //   - startSideRing is at local -Y (toward startPoint)
95         //   - endSideRing is at local +Y (toward endPoint)
96         final Point3D[] startSideRing = new Point3D[segments];
97         final Point3D[] endSideRing = new Point3D[segments];
98
99         for (int i = 0; i < segments; i++) {
100             final double angle = 2.0 * Math.PI * i / segments;
101             final double localX = radius * Math.cos(angle);
102             final double localZ = radius * Math.sin(angle);
103
104             // Start-side ring (at -halfLength in local Y = toward startPoint)
105             final Point3D startLocal = new Point3D(localX, -halfLength, localZ);
106             rotMatrix.transform(startLocal, startLocal);
107             startLocal.x += centerX;
108             startLocal.y += centerY;
109             startLocal.z += centerZ;
110             startSideRing[i] = startLocal;
111
112             // End-side ring (at +halfLength in local Y = toward endPoint)
113             final Point3D endLocal = new Point3D(localX, halfLength, localZ);
114             rotMatrix.transform(endLocal, endLocal);
115             endLocal.x += centerX;
116             endLocal.y += centerY;
117             endLocal.z += centerZ;
118             endSideRing[i] = endLocal;
119         }
120
121         // Create side faces (two triangles per segment)
122         // Winding: startSide → endSide → startSide+next, then startSide+next → endSide → endSide+next
123         // This creates CCW winding when viewed from outside the cylinder
124         for (int i = 0; i < segments; i++) {
125             final int next = (i + 1) % segments;
126
127             addShape(new SolidPolygon(
128                     new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
129                     new Point3D(endSideRing[i].x, endSideRing[i].y, endSideRing[i].z),
130                     new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
131                     color));
132
133             addShape(new SolidPolygon(
134                     new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
135                     new Point3D(endSideRing[i].x, endSideRing[i].y, endSideRing[i].z),
136                     new Point3D(endSideRing[next].x, endSideRing[next].y, endSideRing[next].z),
137                     color));
138         }
139
140         // Create start cap (at startPoint, faces outward from cylinder)
141         // Winding: center → current → next creates CCW winding when viewed from outside
142         for (int i = 0; i < segments; i++) {
143             final int next = (i + 1) % segments;
144             addShape(new SolidPolygon(
145                     new Point3D(startPoint.x, startPoint.y, startPoint.z),
146                     new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
147                     new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
148                     color));
149         }
150
151         // Create end cap (at endPoint, faces outward from cylinder)
152         // Winding: center → next → current creates CCW winding when viewed from outside
153         // (opposite to start cap because end cap faces the opposite direction)
154         for (int i = 0; i < segments; i++) {
155             final int next = (i + 1) % segments;
156             addShape(new SolidPolygon(
157                     new Point3D(endPoint.x, endPoint.y, endPoint.z),
158                     new Point3D(endSideRing[next].x, endSideRing[next].y, endSideRing[next].z),
159                     new Point3D(endSideRing[i].x, endSideRing[i].y, endSideRing[i].z),
160                     color));
161         }
162
163         setBackfaceCulling(true);
164     }
165
166     /**
167      * Creates a quaternion that rotates from the +Y axis to the given direction.
168      *
169      * @param nx normalized direction X component
170      * @param ny normalized direction Y component
171      * @param nz normalized direction Z component
172      * @return quaternion representing the rotation
173      */
174     private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) {
175         // Default direction is +Y (0, 1, 0)
176         // Target direction is (nx, ny, nz)
177         // Dot product: 0*nx + 1*ny + 0*nz = ny
178         final double dot = ny;
179
180         // Check for parallel vectors
181         if (dot > 0.9999) {
182             // Direction is nearly +Y, no rotation needed
183             return Quaternion.identity();
184         }
185         if (dot < -0.9999) {
186             // Direction is nearly -Y, rotate 180° around X axis
187             return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
188         }
189
190         // Cross product: (0, 1, 0) x (nx, ny, nz) = (nz, 0, -nx)
191         // This gives the rotation axis
192         final double axisX = nz;
193         final double axisY = 0;
194         final double axisZ = -nx;
195         final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
196         final double normalizedAxisX = axisX / axisLength;
197         final double normalizedAxisZ = axisZ / axisLength;
198
199         // Angle from dot product
200         final double angle = Math.acos(dot);
201
202         return Quaternion.fromAxisAngle(
203                 new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);
204     }
205 }