Sphere.java

  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.euclidean.threed.shape;

  18. import java.text.MessageFormat;
  19. import java.util.List;
  20. import java.util.stream.Collectors;
  21. import java.util.stream.Stream;

  22. import org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule;
  23. import org.apache.commons.geometry.euclidean.AbstractNSphere;
  24. import org.apache.commons.geometry.euclidean.threed.Plane;
  25. import org.apache.commons.geometry.euclidean.threed.Planes;
  26. import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D;
  27. import org.apache.commons.geometry.euclidean.threed.RegionBSPTree3D.RegionNode3D;
  28. import org.apache.commons.geometry.euclidean.threed.Vector3D;
  29. import org.apache.commons.geometry.euclidean.threed.line.Line3D;
  30. import org.apache.commons.geometry.euclidean.threed.line.LineConvexSubset3D;
  31. import org.apache.commons.geometry.euclidean.threed.line.LinecastPoint3D;
  32. import org.apache.commons.geometry.euclidean.threed.line.Linecastable3D;
  33. import org.apache.commons.geometry.euclidean.threed.mesh.SimpleTriangleMesh;
  34. import org.apache.commons.geometry.euclidean.threed.mesh.TriangleMesh;
  35. import org.apache.commons.numbers.core.Precision;

  36. /** Class representing a 3 dimensional sphere in Euclidean space.
  37.  */
  38. public final class Sphere extends AbstractNSphere<Vector3D> implements Linecastable3D {

  39.     /** Message used when requesting a sphere approximation with an invalid subdivision number. */
  40.     private static final String INVALID_SUBDIVISION_MESSAGE =
  41.         "Number of sphere approximation subdivisions must be greater than or equal to zero; was {0}";

  42.     /** Constant equal to {@code 4 * pi}. */
  43.     private static final double FOUR_PI = 4.0 * Math.PI;

  44.     /** Constant equal to {@code (4/3) * pi}. */
  45.     private static final double FOUR_THIRDS_PI = FOUR_PI / 3.0;

  46.     /** Construct a new sphere from its component parts.
  47.      * @param center the center of the sphere
  48.      * @param radius the sphere radius
  49.      * @param precision precision context used to compare floating point numbers
  50.      * @throws IllegalArgumentException if center is not finite or radius is not finite or is
  51.      *      less than or equal to zero as evaluated by the given precision context
  52.      */
  53.     private Sphere(final Vector3D center, final double radius, final Precision.DoubleEquivalence precision) {
  54.         super(center, radius, precision);
  55.     }

  56.     /** {@inheritDoc} */
  57.     @Override
  58.     public double getSize() {
  59.         final double r = getRadius();
  60.         return FOUR_THIRDS_PI * r * r * r;
  61.     }

  62.     /** {@inheritDoc} */
  63.     @Override
  64.     public double getBoundarySize() {
  65.         final double r = getRadius();
  66.         return FOUR_PI * r * r;
  67.     }

  68.     /** {@inheritDoc} */
  69.     @Override
  70.     public Vector3D project(final Vector3D pt) {
  71.         return project(pt, Vector3D.Unit.PLUS_X);
  72.     }

  73.     /** Build an approximation of this sphere using a {@link RegionBSPTree3D}. The approximation is constructed by
  74.      * taking an octahedron (8-sided polyhedron with triangular faces) inscribed in the sphere and subdividing each
  75.      * triangular face {@code subdivisions} number of times, each time projecting the newly created vertices onto the
  76.      * sphere surface. Each triangle subdivision produces 4 triangles, meaning that the total number of triangles
  77.      * inserted into tree is equal to \(8 \times 4^s\), where \(s\) is the number of subdivisions. For
  78.      * example, calling this method with {@code subdivisions} equal to {@code 3} will produce a tree having
  79.      * \(8 \times 4^3 = 512\) triangular facets inserted. See the table below for other examples. The returned BSP
  80.      * tree also contains structural cuts to reduce the overall height of the tree.
  81.      *
  82.      * <table>
  83.      *  <caption>Subdivisions to Triangle Counts</caption>
  84.      *  <thead>
  85.      *      <tr>
  86.      *          <th>Subdivisions</th>
  87.      *          <th>Triangles</th>
  88.      *      </tr>
  89.      *  </thead>
  90.      *  <tbody>
  91.      *      <tr><td>0</td><td>8</td></tr>
  92.      *      <tr><td>1</td><td>32</td></tr>
  93.      *      <tr><td>2</td><td>128</td></tr>
  94.      *      <tr><td>3</td><td>512</td></tr>
  95.      *      <tr><td>4</td><td>2048</td></tr>
  96.      *      <tr><td>5</td><td>8192</td></tr>
  97.      *  </tbody>
  98.      * </table>
  99.      *
  100.      * <p>Care must be taken when using this method with large subdivision numbers so that floating point errors
  101.      * do not interfere with the creation of the planes and triangles in the tree. For example, if the number of
  102.      * subdivisions is too high, the subdivided triangle points may become equivalent according to the sphere's
  103.      * {@link #getPrecision() precision context} and plane creation may fail. Or plane creation may succeed but
  104.      * insertion of the plane into the tree may fail for similar reasons. In general, it is best to use the lowest
  105.      * subdivision number practical for the intended purpose.</p>
  106.      * @param subdivisions the number of triangle subdivisions to use when creating the tree; the total number of
  107.      *      triangular facets inserted into the returned tree is equal to \(8 \times 4^s\), where \(s\) is the number
  108.      *      of subdivisions
  109.      * @return a BSP tree containing an approximation of the sphere
  110.      * @throws IllegalArgumentException if {@code subdivisions} is less than zero
  111.      * @throws IllegalStateException if tree creation fails for the given subdivision count
  112.      * @see #toTriangleMesh(int)
  113.      */
  114.     public RegionBSPTree3D toTree(final int subdivisions) {
  115.         if (subdivisions < 0) {
  116.             throw new IllegalArgumentException(MessageFormat.format(INVALID_SUBDIVISION_MESSAGE, subdivisions));
  117.         }
  118.         return new SphereTreeApproximationBuilder(this, subdivisions).build();
  119.     }

  120.     /** Build an approximation of this sphere using a {@link TriangleMesh}. The approximation is constructed by
  121.      * taking an octahedron (8-sided polyhedron with triangular faces) inscribed in the sphere and subdividing each
  122.      * triangular face {@code subdivisions} number of times, each time projecting the newly created vertices onto the
  123.      * sphere surface. Each triangle subdivision produces 4 triangles, meaning that the total number of triangles
  124.      * in the returned mesh is equal to \(8 \times 4^s\), where \(s\) is the number of subdivisions. For
  125.      * example, calling this method with {@code subdivisions} equal to {@code 3} will produce a mesh having
  126.      * \(8 \times 4^3 = 512\) triangular facets inserted. See the table below for other examples.
  127.      *
  128.      * <table>
  129.      *  <caption>Subdivisions to Triangle Counts</caption>
  130.      *  <thead>
  131.      *      <tr>
  132.      *          <th>Subdivisions</th>
  133.      *          <th>Triangles</th>
  134.      *      </tr>
  135.      *  </thead>
  136.      *  <tbody>
  137.      *      <tr><td>0</td><td>8</td></tr>
  138.      *      <tr><td>1</td><td>32</td></tr>
  139.      *      <tr><td>2</td><td>128</td></tr>
  140.      *      <tr><td>3</td><td>512</td></tr>
  141.      *      <tr><td>4</td><td>2048</td></tr>
  142.      *      <tr><td>5</td><td>8192</td></tr>
  143.      *  </tbody>
  144.      * </table>
  145.      *
  146.      * <p><strong>BSP Tree Conversion</strong></p>
  147.      * <p>Inserting the boundaries of a sphere mesh approximation directly into a BSP tree will invariably result
  148.      * in poor performance: since the region is convex the constructed BSP tree degenerates into a simple linked
  149.      * list of nodes. If a BSP tree is needed, users should prefer the {@link #toTree(int)} method, which creates
  150.      * balanced tree approximations directly, or the {@link RegionBSPTree3D.PartitionedRegionBuilder3D} class,
  151.      * which can be used to insert the mesh faces into a pre-partitioned tree.
  152.      * </p>
  153.      * @param subdivisions the number of triangle subdivisions to use when creating the mesh; the total number of
  154.      *      triangular faces in the returned mesh is equal to \(8 \times 4^s\), where \(s\) is the number
  155.      *      of subdivisions
  156.      * @return a triangle mesh approximation of the sphere
  157.      * @throws IllegalArgumentException if {@code subdivisions} is less than zero
  158.      * @see #toTree(int)
  159.      */
  160.     public TriangleMesh toTriangleMesh(final int subdivisions) {
  161.         if (subdivisions < 0) {
  162.             throw new IllegalArgumentException(MessageFormat.format(INVALID_SUBDIVISION_MESSAGE, subdivisions));
  163.         }
  164.         return new SphereMeshApproximationBuilder(this, subdivisions).build();
  165.     }

  166.     /** Get the intersections of the given line with this sphere. The returned list will
  167.      * contain either 0, 1, or 2 points.
  168.      * <ul>
  169.      *      <li><strong>2 points</strong> - The line is a secant line and intersects the sphere at two
  170.      *      distinct points. The points are ordered such that the first point in the list is the first point
  171.      *      encountered when traveling in the direction of the line. (In other words, the points are ordered
  172.      *      by increasing abscissa value.)
  173.      *      </li>
  174.      *      <li><strong>1 point</strong> - The line is a tangent line and only intersects the sphere at a
  175.      *      single point (as evaluated by the sphere's precision context).
  176.      *      </li>
  177.      *      <li><strong>0 points</strong> - The line does not intersect the sphere.</li>
  178.      * </ul>
  179.      * @param line line to intersect with the sphere
  180.      * @return a list of intersection points between the given line and this sphere
  181.      */
  182.     public List<Vector3D> intersections(final Line3D line) {
  183.         return intersections(line, Line3D::abscissa, Line3D::distance);
  184.     }

  185.     /** Get the first intersection point between the given line and this sphere, or null
  186.      * if no such point exists. The "first" intersection point is the first such point
  187.      * encountered when traveling in the direction of the line from infinity.
  188.      * @param line line to intersect with the sphere
  189.      * @return the first intersection point between the given line and this instance or
  190.      *      null if no such point exists
  191.      */
  192.     public Vector3D firstIntersection(final Line3D line) {
  193.         return firstIntersection(line, Line3D::abscissa, Line3D::distance);
  194.     }

  195.     /** {@inheritDoc} */
  196.     @Override
  197.     public List<LinecastPoint3D> linecast(final LineConvexSubset3D subset) {
  198.         return getLinecastStream(subset)
  199.                 .collect(Collectors.toList());
  200.     }

  201.     /** {@inheritDoc} */
  202.     @Override
  203.     public LinecastPoint3D linecastFirst(final LineConvexSubset3D subset) {
  204.         return getLinecastStream(subset)
  205.                 .findFirst()
  206.                 .orElse(null);
  207.     }

  208.     /** Get a stream containing the linecast intersection points of the given
  209.      * line subset with this instance.
  210.      * @param subset line subset to intersect against this instance
  211.      * @return a stream containing linecast intersection points
  212.      */
  213.     private Stream<LinecastPoint3D> getLinecastStream(final LineConvexSubset3D subset) {
  214.         return intersections(subset.getLine()).stream()
  215.             .filter(subset::contains)
  216.             .map(pt -> new LinecastPoint3D(pt, getCenter().directionTo(pt), subset.getLine()));
  217.     }

  218.     /** Construct a sphere from a center point and radius.
  219.      * @param center the center of the sphere
  220.      * @param radius the sphere radius
  221.      * @param precision precision context used to compare floating point numbers
  222.      * @return a sphere constructed from the given center point and radius
  223.      * @throws IllegalArgumentException if center is not finite or radius is not finite or is
  224.      *      less than or equal to zero as evaluated by the given precision context
  225.      */
  226.     public static Sphere from(final Vector3D center, final double radius, final Precision.DoubleEquivalence precision) {
  227.         return new Sphere(center, radius, precision);
  228.     }

  229.     /** Internal class used to construct hyperplane-bounded approximations of spheres as BSP trees. The class
  230.      * begins with an octahedron inscribed in the sphere and then subdivides each triangular face a specified
  231.      * number of times.
  232.      */
  233.     private static final class SphereTreeApproximationBuilder {

  234.         /** Threshold used to determine when to stop inserting structural cuts and begin adding facets. */
  235.         private static final int PARTITION_THRESHOLD = 2;

  236.         /** The sphere that an approximation is being created for. */
  237.         private final Sphere sphere;

  238.         /** The number of triangular subdivisions to use. */
  239.         private final int subdivisions;

  240.         /** Construct a new builder for creating a BSP tree approximation of the given sphere.
  241.          * @param sphere the sphere to create an approximation of
  242.          * @param subdivisions the number of triangle subdivisions to use in tree creation
  243.          */
  244.         SphereTreeApproximationBuilder(final Sphere sphere, final int subdivisions) {
  245.             this.sphere = sphere;
  246.             this.subdivisions = subdivisions;
  247.         }

  248.         /** Build the sphere approximation BSP tree.
  249.          * @return the sphere approximation BSP tree
  250.          * @throws IllegalStateException if tree creation fails for the configured subdivision count
  251.          */
  252.         RegionBSPTree3D build() {
  253.             final RegionBSPTree3D tree = RegionBSPTree3D.empty();

  254.             final Vector3D center = sphere.getCenter();
  255.             final double radius = sphere.getRadius();
  256.             final Precision.DoubleEquivalence precision = sphere.getPrecision();

  257.             // insert the primary split planes
  258.             final Plane plusXPlane = Planes.fromPointAndNormal(center, Vector3D.Unit.PLUS_X, precision);
  259.             final Plane plusYPlane = Planes.fromPointAndNormal(center, Vector3D.Unit.PLUS_Y, precision);
  260.             final Plane plusZPlane = Planes.fromPointAndNormal(center, Vector3D.Unit.PLUS_Z, precision);

  261.             tree.insert(plusXPlane.span(), RegionCutRule.INHERIT);
  262.             tree.insert(plusYPlane.span(), RegionCutRule.INHERIT);
  263.             tree.insert(plusZPlane.span(), RegionCutRule.INHERIT);

  264.             // create the vertices for the octahedron
  265.             final double cx = center.getX();
  266.             final double cy = center.getY();
  267.             final double cz = center.getZ();

  268.             final Vector3D maxX = Vector3D.of(cx + radius, cy, cz);
  269.             final Vector3D minX = Vector3D.of(cx - radius, cy, cz);

  270.             final Vector3D maxY = Vector3D.of(cx, cy + radius, cz);
  271.             final Vector3D minY = Vector3D.of(cx, cy - radius, cz);

  272.             final Vector3D maxZ = Vector3D.of(cx, cy, cz + radius);
  273.             final Vector3D minZ = Vector3D.of(cx, cy, cz - radius);

  274.             // partition and subdivide the face triangles and insert them into the tree
  275.             final RegionNode3D root = tree.getRoot();

  276.             try {
  277.                 partitionAndInsert(root.getMinus().getMinus().getMinus(), minX, minZ, minY, 0);
  278.                 partitionAndInsert(root.getMinus().getMinus().getPlus(), minX, minY, maxZ, 0);

  279.                 partitionAndInsert(root.getMinus().getPlus().getMinus(), minX, maxY, minZ, 0);
  280.                 partitionAndInsert(root.getMinus().getPlus().getPlus(), minX, maxZ, maxY, 0);

  281.                 partitionAndInsert(root.getPlus().getMinus().getMinus(), maxX, minY, minZ, 0);
  282.                 partitionAndInsert(root.getPlus().getMinus().getPlus(), maxX, maxZ, minY, 0);

  283.                 partitionAndInsert(root.getPlus().getPlus().getMinus(), maxX, minZ, maxY, 0);
  284.                 partitionAndInsert(root.getPlus().getPlus().getPlus(), maxX, maxY, maxZ, 0);
  285.             } catch (final IllegalStateException | IllegalArgumentException exc) {
  286.                 // standardize any tree construction failure as an IllegalStateException
  287.                 throw new IllegalStateException("Failed to construct sphere approximation with subdivision count " +
  288.                         subdivisions + ": " + exc.getMessage(), exc);
  289.             }

  290.             return tree;
  291.         }

  292.         /** Recursively insert structural BSP tree cuts into the given node and then insert subdivided triangles
  293.          * when a target subdivision level is reached. The structural BSP tree cuts are used to help reduce the
  294.          * overall depth of the BSP tree.
  295.          * @param node the node to insert into
  296.          * @param p1 first triangle point
  297.          * @param p2 second triangle point
  298.          * @param p3 third triangle point
  299.          * @param level current subdivision level
  300.          */
  301.         private void partitionAndInsert(final RegionNode3D node,
  302.                                         final Vector3D p1, final Vector3D p2, final Vector3D p3, final int level) {

  303.             if (subdivisions - level > PARTITION_THRESHOLD) {
  304.                 final int nextLevel = level + 1;

  305.                 final Vector3D center = sphere.getCenter();

  306.                 final Vector3D m1 = sphere.project(p1.lerp(p2, 0.5));
  307.                 final Vector3D m2 = sphere.project(p2.lerp(p3, 0.5));
  308.                 final Vector3D m3 = sphere.project(p3.lerp(p1, 0.5));

  309.                 RegionNode3D curNode = node;

  310.                 checkedCut(curNode, createPlane(m3, m2, center), RegionCutRule.INHERIT);
  311.                 partitionAndInsert(curNode.getPlus(), m3, m2, p3, nextLevel);

  312.                 curNode = curNode.getMinus();
  313.                 checkedCut(curNode, createPlane(m2, m1, center), RegionCutRule.INHERIT);
  314.                 partitionAndInsert(curNode.getPlus(), m1, p2, m2, nextLevel);

  315.                 curNode = curNode.getMinus();
  316.                 checkedCut(curNode, createPlane(m1, m3, center), RegionCutRule.INHERIT);
  317.                 partitionAndInsert(curNode.getPlus(), p1, m1, m3, nextLevel);

  318.                 partitionAndInsert(curNode.getMinus(), m1, m2, m3, nextLevel);
  319.             } else {
  320.                 insertSubdividedTriangles(node, p1, p2, p3, level);
  321.             }
  322.         }

  323.         /** Recursively insert subdivided triangles into the given node. Each triangle is inserted into the minus
  324.          * side of the previous triangle.
  325.          * @param node the node to insert into
  326.          * @param p1 first triangle point
  327.          * @param p2 second triangle point
  328.          * @param p3 third triangle point
  329.          * @param level the current subdivision level
  330.          * @return the node representing the inside of the region after insertion of all triangles
  331.          */
  332.         private RegionNode3D insertSubdividedTriangles(final RegionNode3D node,
  333.                                                        final Vector3D p1, final Vector3D p2, final Vector3D p3,
  334.                                                        final int level) {

  335.             if (level >= subdivisions) {
  336.                 // base case
  337.                 checkedCut(node, createPlane(p1, p2, p3), RegionCutRule.MINUS_INSIDE);
  338.                 return node.getMinus();
  339.             } else {
  340.                 final int nextLevel = level + 1;

  341.                 final Vector3D m1 = sphere.project(p1.lerp(p2, 0.5));
  342.                 final Vector3D m2 = sphere.project(p2.lerp(p3, 0.5));
  343.                 final Vector3D m3 = sphere.project(p3.lerp(p1, 0.5));

  344.                 RegionNode3D curNode = node;
  345.                 curNode = insertSubdividedTriangles(curNode, p1, m1, m3, nextLevel);
  346.                 curNode = insertSubdividedTriangles(curNode, m1, p2, m2, nextLevel);
  347.                 curNode = insertSubdividedTriangles(curNode, m3, m2, p3, nextLevel);
  348.                 curNode = insertSubdividedTriangles(curNode, m1, m2, m3, nextLevel);

  349.                 return curNode;
  350.             }
  351.         }

  352.         /** Create a plane from the given points, using the precision context of the sphere.
  353.          * @param p1 first point
  354.          * @param p2 second point
  355.          * @param p3 third point
  356.          * @return a plane defined by the given points
  357.          */
  358.         private Plane createPlane(final Vector3D p1, final Vector3D p2, final Vector3D p3) {
  359.             return Planes.fromPoints(p1, p2, p3, sphere.getPrecision());
  360.         }

  361.         /** Insert the cut into the given node, throwing an exception if no portion of the cutter intersects
  362.          * the node.
  363.          * @param node node to cut
  364.          * @param cutter plane to use to cut the node
  365.          * @param cutRule cut rule to apply
  366.          * @throws IllegalStateException if no portion of the cutter plane intersects the node
  367.          */
  368.         private void checkedCut(final RegionNode3D node, final Plane cutter, final RegionCutRule cutRule) {
  369.             if (!node.insertCut(cutter, cutRule)) {
  370.                 throw new IllegalStateException("Failed to cut BSP tree node with plane: " + cutter);
  371.             }
  372.         }
  373.     }

  374.     /** Internal class used to construct geodesic mesh sphere approximations. The class begins with an octahedron
  375.      * inscribed in the sphere and then subdivides each triangular face a specified number of times.
  376.      */
  377.     private static final class SphereMeshApproximationBuilder {

  378.         /** The sphere that an approximation is being created for. */
  379.         private final Sphere sphere;

  380.         /** The number of triangular subdivisions to use. */
  381.         private final int subdivisions;

  382.         /** Mesh builder object. */
  383.         private final SimpleTriangleMesh.Builder builder;

  384.         /** Construct a new builder for creating a mesh approximation of the given sphere.
  385.          * @param sphere the sphere to create an approximation of
  386.          * @param subdivisions the number of triangle subdivisions to use in mesh creation
  387.          */
  388.         SphereMeshApproximationBuilder(final Sphere sphere, final int subdivisions) {
  389.             this.sphere = sphere;
  390.             this.subdivisions = subdivisions;
  391.             this.builder = SimpleTriangleMesh.builder(sphere.getPrecision());
  392.         }

  393.         /** Build the mesh approximation of the configured sphere.
  394.          * @return the mesh approximation of the configured sphere
  395.          */
  396.         public SimpleTriangleMesh build() {
  397.             final Vector3D center = sphere.getCenter();
  398.             final double radius = sphere.getRadius();

  399.             // create the vertices for the octahedron
  400.             final double cx = center.getX();
  401.             final double cy = center.getY();
  402.             final double cz = center.getZ();

  403.             final Vector3D maxX = Vector3D.of(cx + radius, cy, cz);
  404.             final Vector3D minX = Vector3D.of(cx - radius, cy, cz);

  405.             final Vector3D maxY = Vector3D.of(cx, cy + radius, cz);
  406.             final Vector3D minY = Vector3D.of(cx, cy - radius, cz);

  407.             final Vector3D maxZ = Vector3D.of(cx, cy, cz + radius);
  408.             final Vector3D minZ = Vector3D.of(cx, cy, cz - radius);

  409.             addSubdivided(minX, minZ, minY, 0);
  410.             addSubdivided(minX, minY, maxZ, 0);

  411.             addSubdivided(minX, maxY, minZ, 0);
  412.             addSubdivided(minX, maxZ, maxY, 0);

  413.             addSubdivided(maxX, minY, minZ, 0);
  414.             addSubdivided(maxX, maxZ, minY, 0);

  415.             addSubdivided(maxX, minZ, maxY, 0);
  416.             addSubdivided(maxX, maxY, maxZ, 0);

  417.             return builder.build();
  418.         }

  419.         /** Recursively subdivide and add triangular faces between the given outer boundary points.
  420.          * @param p1 first point
  421.          * @param p2 second point
  422.          * @param p3 third point
  423.          * @param level recursion level; counts up
  424.          */
  425.         private void addSubdivided(final Vector3D p1, final Vector3D p2, final Vector3D p3, final int level) {
  426.             if (level >= subdivisions) {
  427.                 // base case
  428.                 builder.addFaceUsingVertices(p1, p2, p3);
  429.             } else {
  430.                 // subdivide
  431.                 final int nextLevel = level + 1;

  432.                 final Vector3D m1 = sphere.project(p1.lerp(p2, 0.5));
  433.                 final Vector3D m2 = sphere.project(p2.lerp(p3, 0.5));
  434.                 final Vector3D m3 = sphere.project(p3.lerp(p1, 0.5));

  435.                 addSubdivided(p1, m1, m3, nextLevel);
  436.                 addSubdivided(m1, p2, m2, nextLevel);
  437.                 addSubdivided(m3, m2, p3, nextLevel);
  438.                 addSubdivided(m1, m2, m3, nextLevel);
  439.             }
  440.         }
  441.     }
  442. }