001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.geometry.examples.tutorials.teapot; 018 019import java.io.IOException; 020import java.nio.file.Files; 021import java.nio.file.Path; 022import java.nio.file.Paths; 023import java.util.Arrays; 024import java.util.HashMap; 025import java.util.LinkedHashMap; 026import java.util.Map; 027import java.util.function.DoubleFunction; 028import java.util.function.UnaryOperator; 029 030import org.apache.commons.geometry.euclidean.threed.AffineTransformMatrix3D; 031import org.apache.commons.geometry.euclidean.threed.Bounds3D; 032import org.apache.commons.geometry.euclidean.threed.Plane; 033import org.apache.commons.geometry.euclidean.threed.PlaneConvexSubset; 034import org.apache.commons.geometry.euclidean.threed.Planes; 035import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D; 036import org.apache.commons.geometry.euclidean.threed.Vector3D; 037import org.apache.commons.geometry.euclidean.threed.mesh.SimpleTriangleMesh; 038import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh; 039import org.apache.commons.geometry.euclidean.threed.rotation.QuaternionRotation; 040import org.apache.commons.geometry.euclidean.threed.shape.Sphere; 041import org.apache.commons.geometry.euclidean.twod.Vector2D; 042import org.apache.commons.geometry.io.euclidean.threed.IO3D; 043import org.apache.commons.geometry.io.euclidean.threed.obj.ObjWriter; 044import org.apache.commons.numbers.angle.Angle; 045import org.apache.commons.numbers.core.Precision; 046 047/** Class used to construct a simple 3D teapot shape using the 048 * {@code commons-geometry-euclidean} module. 049 */ 050public class TeapotBuilder { 051 052 /** Name used to identify the teapot body geometry. */ 053 private static final String BODY_NAME = "body"; 054 055 /** Name used to identify the teapot lid geometry. */ 056 private static final String LID_NAME = "lid"; 057 058 /** Name used to identify the teapot handle geometry. */ 059 private static final String HANDLE_NAME = "handle"; 060 061 /** Name used to identify the teapot spout geometry. */ 062 private static final String SPOUT_NAME = "spout"; 063 064 /** Precision context used during region construction. */ 065 private final Precision.DoubleEquivalence precision; 066 067 /** Construct a new build instance. 068 * @param precision precision context to use during region construction 069 */ 070 public TeapotBuilder(final Precision.DoubleEquivalence precision) { 071 this.precision = precision; 072 } 073 074 /** Build a teapot as a {@link RegionBSPTree3D}. 075 * @return teapot as a BSP tree 076 */ 077 public RegionBSPTree3D buildTeapot() { 078 return buildTeapot(null); 079 } 080 081 /** Build a teapot as a {@link RegionBSPTree3D}. 082 * @param debugOutputs if not null, important geometries used during the construction of the 083 * teapot will be placed in this map, keyed by part name 084 * @return teapot as a BSP tree 085 */ 086 public RegionBSPTree3D buildTeapot(final Map<String, RegionBSPTree3D> debugOutputs) { 087 // build the parts 088 final RegionBSPTree3D body = buildBody(1); 089 final RegionBSPTree3D lid = buildLid(body); 090 final RegionBSPTree3D handle = buildHandle(); 091 final RegionBSPTree3D spout = buildSpout(1); 092 093 // combine into the final region 094 final RegionBSPTree3D teapot = RegionBSPTree3D.empty(); 095 teapot.union(body, lid); 096 teapot.union(handle); 097 teapot.union(spout); 098 099 // 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}