1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.geometry.examples.tutorials.teapot;
18
19 import java.io.IOException;
20 import java.nio.file.Files;
21 import java.nio.file.Path;
22 import java.nio.file.Paths;
23 import java.util.Arrays;
24 import java.util.HashMap;
25 import java.util.LinkedHashMap;
26 import java.util.Map;
27 import java.util.function.DoubleFunction;
28 import java.util.function.UnaryOperator;
29
30 import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D;
31 import org.apache.commons.geometry.euclidean.threed.Bounds3D;
32 import org.apache.commons.geometry.euclidean.threed.Plane;
33 import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset;
34 import org.apache.commons.geometry.euclidean.threed.Planes;
35 import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D;
36 import org.apache.commons.geometry.euclidean.threed.Vector3D;
37 import org.apache.commons.geometry.euclidean.threed.mesh.SimpleTriangleMesh;
38 import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
39 import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation;
40 import org.apache.commons.geometry.euclidean.threed.shape.Sphere;
41 import org.apache.commons.geometry.euclidean.twod.Vector2D;
42 import org.apache.commons.geometry.io.euclidean.threed.IO3D;
43 import org.apache.commons.geometry.io.euclidean.threed.obj.ObjWriter;
44 import org.apache.commons.numbers.angle.Angle;
45 import org.apache.commons.numbers.core.Precision;
46
47
48
49
50 public class TeapotBuilder {
51
52
53 private static final String BODY_NAME = "body";
54
55
56 private static final String LID_NAME = "lid";
57
58
59 private static final String HANDLE_NAME = "handle";
60
61
62 private static final String SPOUT_NAME = "spout";
63
64
65 private final Precision.DoubleEquivalence precision;
66
67
68
69
70 public TeapotBuilder(final Precision.DoubleEquivalence precision) {
71 this.precision = precision;
72 }
73
74
75
76
77 public RegionBSPTree3D buildTeapot() {
78 return buildTeapot(null);
79 }
80
81
82
83
84
85
86 public RegionBSPTree3D buildTeapot(final Map<String, RegionBSPTree3D> debugOutputs) {
87
88 final RegionBSPTree3D body = buildBody(1);
89 final RegionBSPTree3D lid = buildLid(body);
90 final RegionBSPTree3D handle = buildHandle();
91 final RegionBSPTree3D spout = buildSpout(1);
92
93
94 final RegionBSPTree3D teapot = RegionBSPTree3D.empty();
95 teapot.union(body, lid);
96 teapot.union(handle);
97 teapot.union(spout);
98
99
100
101 teapot.difference(buildBody(0.9));
102 teapot.difference(buildSpout(0.8));
103
104
105 if (debugOutputs != null) {
106 debugOutputs.put(BODY_NAME, body);
107 debugOutputs.put(LID_NAME, lid);
108 debugOutputs.put(HANDLE_NAME, handle);
109 debugOutputs.put(SPOUT_NAME, spout);
110 }
111
112 return teapot;
113 }
114
115
116
117
118
119 public Map<String, RegionBSPTree3D> buildSeparatedTeapot() {
120
121 final RegionBSPTree3D teapot = buildTeapot();
122
123
124 final AffineTransformMatrix3D innerCylinderTransform = AffineTransformMatrix3D.createScale(0.4, 0.4, 1)
125 .translate(0, 0, 0.5);
126 final RegionBSPTree3D innerCylinder = buildUnitCylinderMesh(1, 20, innerCylinderTransform).toTree();
127
128 final AffineTransformMatrix3D outerCylinderTransform = AffineTransformMatrix3D.createScale(0.5, 0.5, 10);
129 final RegionBSPTree3D outerCylinder = buildUnitCylinderMesh(1, 20, outerCylinderTransform).toTree();
130
131 final Plane step = Planes.fromPointAndNormal(Vector3D.of(0, 0, 0.63), Vector3D.Unit.MINUS_Z, precision);
132
133 final RegionBSPTree3D extractor = RegionBSPTree3D.from(Arrays.asList(step.span()));
134 extractor.union(innerCylinder);
135 extractor.intersection(outerCylinder);
136
137
138 final RegionBSPTree3D lid = RegionBSPTree3D.empty();
139 lid.intersection(teapot, extractor);
140
141
142 final RegionBSPTree3D body = RegionBSPTree3D.empty();
143 body.difference(teapot, extractor);
144
145
146 final Map<String, RegionBSPTree3D> result = new LinkedHashMap<>();
147 result.put(LID_NAME, lid);
148 result.put(BODY_NAME, body);
149
150 return result;
151 }
152
153
154
155
156
157 private RegionBSPTree3D buildBody(final double initialRadius) {
158
159 final Sphere sphere = Sphere.from(Vector3D.ZERO, initialRadius, precision);
160 final RegionBSPTree3D body = sphere.toTree(4);
161
162
163 final AffineTransformMatrix3D t = AffineTransformMatrix3D.createScale(1, 1, 0.75);
164 body.transform(t);
165
166
167 final Plane bottomPlane = Planes.fromPointAndNormal(
168 Vector3D.of(0, 0, -0.6 * initialRadius),
169 Vector3D.Unit.PLUS_Z,
170 precision);
171 final PlaneConvexSubset bottom = bottomPlane.span();
172 body.difference(RegionBSPTree3D.from(Arrays.asList(bottom)));
173
174 return body;
175 }
176
177
178
179
180
181 private RegionBSPTree3D buildLid(final RegionBSPTree3D body) {
182
183 final RegionBSPTree3D lid = body.copy();
184
185
186 final AffineTransformMatrix3D t = AffineTransformMatrix3D.createTranslation(0, 0, 0.03);
187 lid.transform(t);
188
189
190 final TriangleMesh cylinder =
191 buildUnitCylinderMesh(1, 20, AffineTransformMatrix3D.createScale(0.5, 0.5, 10));
192 lid.intersection(cylinder.toTree());
193
194
195
196 final Sphere sphere = Sphere.from(Vector3D.of(0, 0, 0), 0.15, precision);
197 final RegionBSPTree3D sphereTree = sphere.toTree(2);
198
199 final Bounds3D lidBounds = lid.getBounds();
200 final double sphereZ = lidBounds.getMax().getZ() + 0.075;
201 sphereTree.transform(AffineTransformMatrix3D.createScale(1, 1, 0.75)
202 .translate(0, 0, sphereZ));
203
204
205 lid.union(sphereTree);
206
207 return lid;
208 }
209
210
211
212
213 private RegionBSPTree3D buildHandle() {
214 final double handleRadius = 0.1;
215 final double height = 1 - (2 * handleRadius);
216
217 final AffineTransformMatrix3D scale =
218 AffineTransformMatrix3D.createScale(handleRadius, handleRadius, height);
219
220 final QuaternionRotation startRotation =
221 QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, -Angle.PI_OVER_TWO);
222 final QuaternionRotation endRotation =
223 QuaternionRotation.fromAxisAngle(Vector3D.Unit.PLUS_Y, Angle.PI_OVER_TWO);
224 final DoubleFunction<QuaternionRotation> slerp = startRotation.slerp(endRotation);
225
226 final Vector3D curveCenter = Vector3D.of(0.5 * height, 0, 0);
227
228 final AffineTransformMatrix3D translation =
229 AffineTransformMatrix3D.createTranslation(Vector3D.of(-1.38, 0, 0));
230
231 final UnaryOperator<Vector3D> vertexTransform = v -> {
232 final double t = v.getZ();
233
234 final Vector3D scaled = scale.apply(v);
235
236 final QuaternionRotation rot = slerp.apply(t);
237 final AffineTransformMatrix3D mat = AffineTransformMatrix3D.createRotation(curveCenter, rot);
238
239 final Vector3D rotated = mat.apply(Vector3D.of(scaled.getX(), scaled.getY(), 0));
240
241 final Vector3D result = (t > 0 && t < 1) ?
242 rotated :
243 rotated.add(Vector3D.Unit.PLUS_X);
244
245 return translation.apply(result);
246 };
247
248 return buildUnitCylinderMesh(10, 14, vertexTransform).toTree();
249 }
250
251
252
253
254
255
256 private RegionBSPTree3D buildSpout(final double initialRadius) {
257 final Vector2D baseScale = Vector2D.of(0.4, 0.2).multiply(initialRadius);
258 final Vector2D topScale = baseScale.multiply(0.6);
259 final double shearZ = 0.9;
260
261 final AffineTransformMatrix3D translation =
262 AffineTransformMatrix3D.createTranslation(Vector3D.of(0.25, 0, -0.4));
263
264 final UnaryOperator<Vector3D> vertexTransform = v -> {
265 final Vector2D scale = baseScale.lerp(topScale, v.getZ());
266
267 final Vector3D tv = Vector3D.of(
268 (v.getX() * scale.getX()) + (v.getZ() * shearZ),
269 v.getY() * scale.getY(),
270 v.getZ()
271 );
272
273 return translation.apply(tv);
274 };
275
276 return buildUnitCylinderMesh(1, 14, vertexTransform).toTree();
277 }
278
279
280
281
282
283
284
285
286
287
288
289 private TriangleMesh buildUnitCylinderMesh(final int segments, final int circleVertexCount,
290 final UnaryOperator<Vector3D> vertexTransform) {
291
292 final SimpleTriangleMesh.Builder builder = SimpleTriangleMesh.builder(precision);
293
294
295 final double zDelta = 1.0 / segments;
296 double zValue;
297
298 final double azDelta = Angle.TWO_PI / circleVertexCount;
299 double az;
300
301 Vector3D vertex;
302 for (int i = 0; i <= segments; ++i) {
303 zValue = i * zDelta;
304
305 for (int v = 0; v < circleVertexCount; ++v) {
306 az = v * azDelta;
307
308 vertex = Vector3D.of(
309 Math.cos(az),
310 Math.sin(az),
311 zValue);
312 builder.addVertex(vertexTransform.apply(vertex));
313 }
314 }
315
316
317
318
319 for (int i = 1; i < circleVertexCount - 1; ++i) {
320 builder.addFace(0, i + 1, i);
321 }
322
323
324 int circleStart;
325 int v1;
326 int v2;
327 int v3;
328 int v4;
329 for (int s = 0; s < segments; ++s) {
330 circleStart = s * circleVertexCount;
331
332 for (int i = 0; i < circleVertexCount; ++i) {
333 v1 = i + circleStart;
334 v2 = ((i + 1) % circleVertexCount) + circleStart;
335 v3 = v2 + circleVertexCount;
336 v4 = v1 + circleVertexCount;
337
338 builder
339 .addFace(v1, v2, v3)
340 .addFace(v1, v3, v4);
341 }
342 }
343
344
345 final int lastCircleStart = circleVertexCount * segments;
346 for (int i = 1 + lastCircleStart; i < builder.getVertexCount() - 1; ++i) {
347 builder.addFace(lastCircleStart, i, i + 1);
348 }
349
350 return builder.build();
351 }
352
353
354
355
356
357
358
359
360
361
362
363 public static void main(final String[] args) throws IOException {
364 if (args.length < 1) {
365 throw new IllegalArgumentException("Output file argument is required");
366 }
367
368 final Path outputFile = Paths.get(args[0]);
369 final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-10);
370
371 final TeapotBuilder builder = new TeapotBuilder(precision);
372
373 final Map<String, RegionBSPTree3D> debugOutputs = new HashMap<>();
374
375 final RegionBSPTree3D teapot = builder.buildTeapot(debugOutputs);
376
377 IO3D.write(teapot, outputFile);
378
379 if (args.length > 1) {
380
381 final Path debugDir = Paths.get(args[1]);
382 Files.createDirectories(debugDir);
383
384
385 final Map<String, RegionBSPTree3D> partMap = builder.buildSeparatedTeapot();
386 try (ObjWriter writer = new ObjWriter(Files.newBufferedWriter(debugDir.resolve("separated-teapot.obj")))) {
387
388 for (Map.Entry<String, RegionBSPTree3D> entry : partMap.entrySet()) {
389 writer.writeObjectName(entry.getKey());
390 writer.writeBoundaries(entry.getValue());
391 }
392 }
393
394
395 for (Map.Entry<String, RegionBSPTree3D> entry : debugOutputs.entrySet()) {
396 IO3D.write(entry.getValue(), debugDir.resolve(entry.getKey() + ".stl"));
397 }
398 }
399 }
400 }