View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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  /** Class used to construct a simple 3D teapot shape using the
48   * {@code commons-geometry-euclidean} module.
49   */
50  public class TeapotBuilder {
51  
52      /** Name used to identify the teapot body geometry. */
53      private static final String BODY_NAME = "body";
54  
55      /** Name used to identify the teapot lid geometry. */
56      private static final String LID_NAME = "lid";
57  
58      /** Name used to identify the teapot handle geometry. */
59      private static final String HANDLE_NAME = "handle";
60  
61      /** Name used to identify the teapot spout geometry. */
62      private static final String SPOUT_NAME = "spout";
63  
64      /** Precision context used during region construction. */
65      private final Precision.DoubleEquivalence precision;
66  
67      /** Construct a new build instance.
68       * @param precision precision context to use during region construction
69       */
70      public TeapotBuilder(final Precision.DoubleEquivalence precision) {
71          this.precision = precision;
72      }
73  
74      /** Build a teapot as a {@link RegionBSPTree3D}.
75       * @return teapot as a BSP tree
76       */
77      public RegionBSPTree3D buildTeapot() {
78          return buildTeapot(null);
79      }
80  
81      /** Build a teapot as a {@link RegionBSPTree3D}.
82       * @param debugOutputs if not null, important geometries used during the construction of the
83       *      teapot will be placed in this map, keyed by part name
84       * @return teapot as a BSP tree
85       */
86      public RegionBSPTree3D buildTeapot(final Map<String, RegionBSPTree3D> debugOutputs) {
87          // build the parts
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          // combine into the final region
94          final RegionBSPTree3D teapot = RegionBSPTree3D.empty();
95          teapot.union(body, lid);
96          teapot.union(handle);
97          teapot.union(spout);
98  
99          // subtract scaled-down versions of the body and spout to
100         // create the hollow interior
101         teapot.difference(buildBody(0.9));
102         teapot.difference(buildSpout(0.8));
103 
104         // add debug outputs if needed
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     /** Build a teapot separated into its component parts. The keys of the returned map are
116      * the component names and the values are the regions.
117      * @return map of teapot component names to regions
118      */
119     public Map<String, RegionBSPTree3D> buildSeparatedTeapot() {
120         // construct the single-piece teapot
121         final RegionBSPTree3D teapot = buildTeapot();
122 
123         // create a region to extract the lid
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         // extract the lid
138         final RegionBSPTree3D lid = RegionBSPTree3D.empty();
139         lid.intersection(teapot, extractor);
140 
141         // remove the lid from the body
142         final RegionBSPTree3D body = RegionBSPTree3D.empty();
143         body.difference(teapot, extractor);
144 
145         // build the output
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     /** Build the teapot body.
154      * @param initialRadius radius of the sphere used as the first step in body construction
155      * @return teapot body
156      */
157     private RegionBSPTree3D buildBody(final double initialRadius) {
158         // construct a BSP tree sphere approximation
159         final Sphere sphere = Sphere.from(Vector3D.ZERO, initialRadius, precision);
160         final RegionBSPTree3D body = sphere.toTree(4);
161 
162         // squash it a little bit along the z-axis
163         final AffineTransformMatrix3D t = AffineTransformMatrix3D.createScale(1, 1, 0.75);
164         body.transform(t);
165 
166         // cut off part of the bottom to make it flat
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     /** Build the lid of the teapot.
178      * @param body region representing the teapot lid
179      * @return teapot lid
180      */
181     private RegionBSPTree3D buildLid(final RegionBSPTree3D body) {
182         // make a copy of the body so that we match its curve exactly
183         final RegionBSPTree3D lid = body.copy();
184 
185         // translate the lid to be above the body
186         final AffineTransformMatrix3D t = AffineTransformMatrix3D.createTranslation(0, 0, 0.03);
187         lid.transform(t);
188 
189         // intersect the translated body with a cylinder
190         final TriangleMesh cylinder =
191                 buildUnitCylinderMesh(1, 20, AffineTransformMatrix3D.createScale(0.5, 0.5, 10));
192         lid.intersection(cylinder.toTree());
193 
194         // add a small squashed sphere on top; use the bounds of the top in order to place
195         // the sphere at the correct position
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         // make the small sphere a part of the top
205         lid.union(sphereTree);
206 
207         return lid;
208     }
209 
210     /** Build the handle of the teapot.
211      * @return teapot handle
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     /** Build the teapot spout.
252      * @param initialRadius radius of the cylinder used as the first step in spout
253      *      construction
254      * @return teapot spout
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     /** Construct a triangle mesh approximating a cylinder of radius one and length one oriented with its
280      * based on the origin and extending along the positive z-axis. The vertices may be transformed by
281      * {@code vertexTransform} during construction, meaning that the resulting mesh may no longer represent
282      * a cylinder.
283      * @param segments number of vertical segments used in the cylinder
284      * @param circleVertexCount number of vertices used to approximate the outside circle
285      * @param vertexTransform function used to transform each computed vertex from its position on the unit
286      *      cylinder to its position in the returned mesh
287      * @return triangle mesh
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         // add the cylinder vertices
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         // add the bottom faces using a triangle fan, making sure
317         // that the triangles are oriented so that the face normal
318         // points down
319         for (int i = 1; i < circleVertexCount - 1; ++i) {
320             builder.addFace(0, i + 1, i);
321         }
322 
323         // add the side faces
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         // add the top faces using a triangle fan
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     /** Entry point for command-line execution of the {@link TeapotBuilder} class. Two positional
354      * arguments are supported:
355      * <ol>
356      *  <li><em>outputFile</em> - File to write the constructed teapot to.</li>
357      *  <li><em>debugDir</em> - (Optional) Directory to write geometry files for important
358      *      components used in the construction of the teapot.</li>
359      * </ol>
360      * @param args argument array
361      * @throws IOException if an I/O error occurs
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             // write additional files to the debug dir
381             final Path debugDir = Paths.get(args[1]);
382             Files.createDirectories(debugDir);
383 
384             // build and write teapot components
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             // write debug outputs
395             for (Map.Entry<String, RegionBSPTree3D> entry : debugOutputs.entrySet()) {
396                 IO3D.write(entry.getValue(), debugDir.resolve(entry.getKey() + ".stl"));
397             }
398         }
399     }
400 }