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 cylinder defined by two end points.
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>
20 * <p><b>Usage example:</b></p>
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)
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),
39 * @see SolidPolygonCone
40 * @see SolidPolygonArrow
43 public class SolidPolygonCylinder extends AbstractCompositeShape {
46 * Constructs a solid cylinder between two end points.
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>
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
59 public SolidPolygonCylinder(final Point3D startPoint, final Point3D endPoint,
60 final double radius, final int segments,
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);
70 // Handle degenerate case: start and end are the same point
71 if (distance < 0.001) {
75 // Normalize direction vector
76 final double nx = dx / distance;
77 final double ny = dy / distance;
78 final double nz = dz / distance;
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();
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;
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];
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);
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;
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;
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;
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),
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),
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),
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),
163 setBackfaceCulling(true);
167 * Creates a quaternion that rotates from the +Y axis to the given direction.
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
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;
180 // Check for parallel vectors
182 // Direction is nearly +Y, no rotation needed
183 return Quaternion.identity();
186 // Direction is nearly -Y, rotate 180° around X axis
187 return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
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;
199 // Angle from dot product
200 final double angle = Math.acos(dot);
202 return Quaternion.fromAxisAngle(
203 new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);