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 }