Catmull–Clark subdivision surface: Difference between revisions

m
Rewritten code to deal with holes in the surface.
m (Minor edit.)
m (Rewritten code to deal with holes in the surface.)
Line 917:
<syntaxhighlight lang="java">
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
 
public final class CatmullClarkSurfaceSubdivision {
 
public static void main(String[] args) {
List<PointFace> pointsfaces = List.of( new Face(List.of( new Point(-1.0, 1.0, 1.0),
new Point(-1.0, -1.0, 1.0),
new Point( 1.0, -1.0, 1.0),
new Point( 1.0, 1.0, 1.0) )),
new Point( 1.0, -1.0, -1.0),
new Face(List.of( new Point( 1.0, 1.0, - 1.0),
new Point(- 1.0, -1.0, - 1.0),
new Point(- 1.0, -1.0, -1.0) );,
new Point( 1.0, 1.0, -1.0) )),
new Face(List.of( new Point( 1.0, 1.0, -1.0),
new Point( 1.0, -1.0, -1.0),
new Point(-1.0, -1.0, -1.0),
new Point(-1.0, 1.0, -1.0) )),
new Face(List.of( new Point(-1.0, 1.0, -1.0),
new Point(-1.0, 1.0, 1.0),
new Point( 1.0, 1.0, 1.0),
new Point( 1.0, 1.0, -1.0) )),
new Face(List.of( new Point(-1.0, 1.0, -1.0),
new Point(-1.0, -1.0, -1.0),
new Point(-1.0, -1.0, 1.0),
new Point(-1.0, 1.0, 1.0) )),
new Face(List.of( new Point(-1.0, -1.0, -1.0),
new Point(-1.0, -1.0, 1.0),
new Point( 1.0, -1.0, 1.0),
new Point( 1.0, -1.0, -1.0) )) );
displaySurface(faces);
List<List<Integer>> faces = List.of( List.of( 0, 1, 2, 3 ),
List.of( 3, 2, 4, 5 ),
List.of( 5, 4, 6, 7 ),
List.of( 7, 0, 3, 5 ),
List.of( 7, 6, 1, 0 ),
List.of( 6, 1, 2, 4 ) );
ListPair result = new ListPair(points, faces);
final int iterations = 1;
for ( int i = 0; i < iterations; i++ ) {
result faces = catmullClarkSurfaceSubdivision(result.points, result.faces);
}
displaySurface(faces);
}
for ( Point point : result.points ) {
System.out.println(point);
// The Catmull-Clarke surface subdivision algorithm.
}
private static List<Face> catmullClarkSurfaceSubdivision(List<Face> faces) {
System.out.println();
// Determine, for each edge, whether or not it is an edge of a hole, and set its edge point accordingly.
List<Edge> edges = faces.stream().map( face -> face.edges.stream() ).flatMap(Function.identity()).toList();
for ( List<Integer> face : result.faces ) {
for ( Edge edge : edges ) {
System.out.println(face);
List<Point> facePointsForEdge =
faces.stream().filter( face -> face.contains(edge) ).map( face -> face.facePoint ).toList();
if ( facePointsForEdge.size() == 2 ) {
edge.holeEdge = false;
edge.edgePoint = centroid(List.of( edge.midEdge, centroid(facePointsForEdge) ));
} else {
edge.holeEdge = true;
edge.edgePoint = edge.midEdge;
}
}
}
private static ListPair catmullClarkSurfaceSubdivision(List<Point> points, List<List<Integer>> faces) {
List<Point> facePoints = facePoints(points, faces);
List<Edge> edges = edges(points, faces);
List<Point> edgePoints = edgePoints(points, facePoints, edges);
List<Point> averageFacePoints = averageFacePoints(points, faces, facePoints);
List<Point> averageEdgeCentres = averageEdgeCentres(points, edges);
List<Integer> facesCount = facesCount(points, faces);
List<Point> newPoints = newPoints(points, facesCount, averageFacePoints, averageEdgeCentres);
Map<Point, Point> nextVertices = nextVertices(edges, faces);
List<Integer> facePointIndexes = new ArrayList<Integer>();
int facePointIndex = newPoints.size();
for ( Point facePoint : facePoints ) {
newPoints.add(facePoint);
facePointIndexes.addLast(facePointIndex);
facePointIndex += 1;
}
Map<Long, Integer> edgePointIndexes = new HashMap<Long, Integer>();
for ( int edgeIndex = 0; edgeIndex < edges.size(); edgeIndex++ ) {
final int pointOne = edges.get(edgeIndex).pointOne;
final int pointTwo = edges.get(edgeIndex).pointTwo;
Point edgePoint = edgePoints.get(edgeIndex);
newPoints.add(edgePoint);
edgePointIndexes.put(szudzikHash( new IndexPair(pointOne, pointTwo) ), facePointIndex);
facePointIndex += 1;
}
List<List<Integer>> newFaces = new ArrayList<List<Integer>>();
for ( List<Integer> face : faces ) { // The face can contain any number of points
if ( face.size() >= 3 ) { // A face with 2 or less points does not contribute to the surface
final int fullFace = facePointIndexes.get(faces.indexOf(face));
IndexPair pair = order( new IndexPair(face.getLast(), face.getFirst()) );
int edgePoint = edgePointIndexes.get(szudzikHash(pair));
for ( int i = 0; i < face.size(); i++ ) {
IndexPair previousPair = order( new IndexPair(face.get(i), face.get(( i + 1 ) % face.size())) );
final int previousEdgePoint = edgePointIndexes.get(szudzikHash(previousPair));
newFaces.addLast(List.of( face.get(i), previousEdgePoint, fullFace, edgePoint ));
edgePoint = previousEdgePoint;
}
}
}
List<Face> nextFaces = new ArrayList<Face>();
return new ListPair(newPoints, newFaces);
for ( Face face : faces ) { // The face may contain any number of points
}
if ( face.vertices.size() >= 3 ) { // A face with 2 or fewer points does not contribute to the surface
Point facePoint = face.facePoint;
// Return the new points created by the current iteration of the Catmull-Clark surface subdivision algorithm.
for ( int i = 0; i < face.vertices.size(); i++ ) {
private static List<Point> newPoints(List<Point> points, List<Integer> facesCount,
nextFaces.addLast( new Face(List.of(
List<Point> averageFacePoints, List<Point> averageEdgeCentres) {
nextVertices.get(face.vertices.get(i)),
List<Point> newPoints = new ArrayList<Point>();
face.edges.get(i).edgePoint,
for ( int pointIndex = 0; pointIndex < points.size(); pointIndex++ ) {
facePoint,
final int faceCount = facesCount.get(pointIndex);
face.edges.get(Math.floorMod(i - 1, face.vertices.size())).edgePoint )) );
final double multipleOne = (double) ( faceCount - 3 ) / faceCount;
}
final double multipleTwo = 1.0 / faceCount;
}
final double multipleThree = 2.0 / faceCount;
}
return nextFaces;
Point currentPoint = points.get(pointIndex);
Point pointOne = currentPoint.scalarMultiply(multipleOne);
Point averageFacePoint = averageFacePoints.get(pointIndex);
Point pointTwo = averageFacePoint.scalarMultiply(multipleTwo);
Point averageEdgeCentre = averageEdgeCentres.get(pointIndex);
Point pointThree = averageEdgeCentre.scalarMultiply(multipleThree);
Point pointFour = pointOne.add(pointTwo);
newPoints.addLast(pointFour.add(pointThree));
}
 
return newPoints;
}
// Return a list containing a value for each point, which is the number of faces containing that point.
private static List<Integer> facesCount(List<Point> points, List<List<Integer>> faces) {
List<Integer> pointsFaces = Stream.generate( () -> 0 ).limit(points.size()).collect(Collectors.toList());
for ( List<Integer> face : faces ) {
for ( int pointIndex : face ) {
pointsFaces.set(pointIndex, pointsFaces.get(pointIndex) + 1);
}
}
return pointsFaces;
}
// Return a list containing a value for each point,
// which is the average of the centres of the edges containing that point.
private static List<Point> averageEdgeCentres(List<Point> points, List<Edge> edges) {
List<PointWithEdgeCount> pointExtras = Stream.generate( () -> new PointWithEdgeCount(Point.ZERO, 0) )
.limit(points.size()).collect(Collectors.toList());
for ( Edge edge : edges ) {
Point centrePoint = edge.centrePoint;
for ( int pointIndex : List.of( edge.pointOne, edge.pointTwo ) ) {
Point point = pointExtras.get(pointIndex).point;
pointExtras.get(pointIndex).point = point.add(centrePoint);
pointExtras.get(pointIndex).edgeCount += 1;
}
}
List<Point> averageEdgeCentres = new ArrayList<Point>();
for ( PointWithEdgeCount pointExtra : pointExtras ) {
averageEdgeCentres.addLast(pointExtra.point.scalarDivide(pointExtra.edgeCount));
};
 
return averageEdgeCentres;
}
// Return a listmap containing a value, for each pointvertex,
// whichthe isnew thevertex averagecreated ofby the facecurrent pointsiteration forof the facesCatmull-Clark containingsurface thatsubdivision pointalgorithm.
private static ListMap<Point, Point> averageFacePointsnextVertices(List<PointEdge> pointsedges, List<List<Integer>Face> faces,) {
Map<Point, Point> nextVertices = new HashMap<Point, Point>();
List<Point> facePoints) {
List<Point> vertices =
List<PointWithEdgeCount> pointExtras = Stream.generate( () -> new PointWithEdgeCount(Point.ZERO, 0) )
faces.stream().map( face -> face.vertices.stream() ).flatMap(Function.identity()).distinct().toList();
.limit(points.size()).collect(Collectors.toList());
for ( List<Integer> face : faces ) {
Point facePoint = facePoints.get(faces.indexOf(face));
for ( int pointIndex : face ) {
Point pointExtra = pointExtras.get(pointIndex).point;
pointExtras.get(pointIndex).point = pointExtra.add(facePoint);
pointExtras.get(pointIndex).edgeCount += 1;
}
}
for ( Point vertex : vertices ) {
List<Point> averageFacePoints = new ArrayList<Point>();
List<Face> facesForVertex = faces.stream().filter( face -> face.contains(vertex) ).toList();
for ( PointWithEdgeCount pointExtra : pointExtras ) {
List<Edge> edgesForVertex = edges.stream().filter( edge -> edge.contains(vertex) ).distinct().toList();
averageFacePoints.addLast(pointExtra.point.scalarDivide(pointExtra.edgeCount));
}
if ( facesForVertex.size() != edgesForVertex.size() ) {
 
List<Point> midEdgeOfHoleEdges = edgesForVertex.stream().filter( edge -> edge.holeEdge )
return averageFacePoints;
.map( edge -> edge.midEdge ).collect(Collectors.toList());
midEdgeOfHoleEdges.add(vertex);
nextVertices.put(vertex, centroid(midEdgeOfHoleEdges));
} else {
final int faceCount = facesForVertex.size();
final double multipleOne = (double) ( faceCount - 3 ) / faceCount;
final double multipleTwo = 1.0 / faceCount;
final double multipleThree = 2.0 / faceCount;
Point nextVertexOne = vertex.multiply(multipleOne);
List<Point> facePoints = facesForVertex.stream().map( face -> face.facePoint ).toList();
Point nextVertexTwo = centroid(facePoints).multiply(multipleTwo);
List<Point> midEdges = edgesForVertex.stream().map( edge -> edge.midEdge ).toList();
Point nextVertexThree = centroid(midEdges).multiply(multipleThree);
Point nextVertexFour = nextVertexOne.add(nextVertexTwo);
nextVertices.put(vertex, nextVertexFour.add(nextVertexThree));
}
}
return nextVertices;
}
// Return athe list of edge points, where an edgecentroid point isof the averagegiven of the centrelist of an edgepoints.
private static Point centroid(List<Point> points) {
// and the centre of the line segment joining the face points of its two adjacent faces.
return points.stream().reduce(Point.ZERO, (left, right) -> left.add(right) ).divide(points.size());
private static List<Point> edgePoints(List<Point> points, List<Point> facePoints, List<Edge> edges) {
List<Point> edgePoints = new ArrayList<Point>();
for ( Edge edge : edges ) {
Point edgeCentre = edge.centrePoint;
Point facePoint1 = facePoints.get(edge.faceOne);
Point facePoint2 = ( edge.faceTwo == FACE_NOT_SET ) ? facePoint1 : facePoints.get(edge.faceTwo);
Point centreFacePoint = facePoint1.centrePoint(facePoint2);
edgePoints.addLast(edgeCentre.centrePoint(centreFacePoint));
}
 
return edgePoints;
}
// Display the current Catmull-Clark surface on the console.
// Return a list of edges.
private static List<Edge>void edgesdisplaySurface(List<Point> points, List<List<Integer>Face> faces) {
System.out.println("Surface {");
List<Edge> partialEdges = new ArrayList<Edge>();
faces.stream().forEach(System.out::println);
for ( List<Integer> face : faces ) {
System.out.println("}" + System.lineSeparator());
for ( int pointIndex = 0; pointIndex < face.size(); pointIndex++ ) {
final int pointIndexOne = face.get(pointIndex);
final int pointIndexTwo = face.get((pointIndex + 1) % face.size());
IndexPair pair = order( new IndexPair(pointIndexOne, pointIndexTwo) );
partialEdges.addLast( new Edge(pair.first, pair.second, faces.indexOf(face), 0, Point.ZERO) );
}
}
Collections.sort(partialEdges);
List<Edge> mergedEdges = new ArrayList<Edge>();
int edgeIndex = 0;
while ( edgeIndex < partialEdges.size() ) {
Edge edgeOne = partialEdges.get(edgeIndex);
if ( edgeIndex < partialEdges.size() - 1 ) {
Edge edgeTwo = partialEdges.get(edgeIndex + 1);
if ( edgeOne.pointOne == edgeTwo.pointOne && edgeOne.pointTwo == edgeTwo.pointTwo ) {
mergedEdges.addLast( new Edge(
edgeOne.pointOne, edgeOne.pointTwo, edgeOne.faceOne, edgeTwo.faceOne, Point.ZERO) );
edgeIndex += 2;
} else {
mergedEdges.addLast( new Edge(
edgeOne.pointOne, edgeOne.pointTwo, edgeOne.faceOne, FACE_NOT_SET, Point.ZERO) );
edgeIndex += 1;
}
} else {
mergedEdges.add( new Edge(
edgeOne.pointOne, edgeOne.pointTwo, edgeOne.faceOne, FACE_NOT_SET, Point.ZERO) );
edgeIndex += 1;
}
}
List<Edge> edges = new ArrayList<Edge>();
for ( Edge mergedEdge : mergedEdges ) {
Point pointOne = points.get(mergedEdge.pointOne);
Point pointTwo = points.get(mergedEdge.pointTwo);
Point centrePoint = pointOne.centrePoint(pointTwo);
edges.addLast( new Edge(
mergedEdge.pointOne, mergedEdge.pointTwo, mergedEdge.faceOne, mergedEdge.faceTwo, centrePoint) );
}
return edges;
}
// Return a list of face points, where a face point is the average of all the points of a face.
private static List<Point> facePoints(List<Point> points, List<List<Integer>> faces) {
List<Point> facePoints = new ArrayList<Point>();
for ( List<Integer> face : faces ) {
Point facePoint = Point.ZERO;
for ( int pointIndex : face ) {
Point point = points.get(pointIndex);
facePoint = facePoint.add(point);
}
facePoints.addLast(facePoint.scalarDivide(face.size()));
}
 
return facePoints;
}
// A point of the current Catmull-Clark surface.
// Return an IndexPair with its values sorted into ascending order.
private static IndexPairfinal order(IndexPairclass pair)Point implements Comparable<Point> {
if ( pair.first < pair.second ) {
return pair;
}
return new IndexPair(pair.second, pair.first);
}
// Return a hashed value of the two numbers in the given IndexPair.
private static long szudzikHash(IndexPair pair) {
return ( pair.first >= pair.second ) ?
( pair.first * pair.first ) + pair.first + pair.second : ( pair.second * pair.second ) + pair.first;
}
// A three dimensional point.
private static class Point {
public Point(double aX, double aY, double aZ) {
x = aX; y = aY; z = aZ;
}
@Override
public int compareTo(Point other) {
if ( x == other.x ) {
if ( y == other.y ) {
return Double.compare(z, other.z);
}
return Double.compare(y, other.y);
}
return Double.compare(x, other.x);
}
@Override
public boolean equals(Object other) {
return switch ( other ) {
case Point point -> x == point.x && y == point.y && z == point.z;
default -> false;
};
}
@Override
public int hashCode() {
return Objects.hash(x, y, z);
}
Line 1,190 ⟶ 1,081:
}
public Point scalarMultiplymultiply(double factor) {
return new Point(x * factor, y * factor, z * factor);
}
public Point scalarDividedivide(double factor) {
return scalarMultiplymultiply(1.0 / factor);
}
public Point centrePoint(Point other) {
return add(other).scalarDivide(2.0);
}
Line 1,209 ⟶ 1,096:
private String format(double value) {
return ( value >= 0 ) ? String.format(" %.5f3f", value) : String.format("%.5f3f", value);
}
private final double x, y, z;
}
// An edge of the current Catmull-Clark surface.
// An edge consists of two end points, together with it's one or two adjacent faces
private static final class Edge {
// and the centre point of that edge.
private static class Edge implements Comparable<Edge> {
public Edge(int aPointOne, int aPointTwo, int aFaceOne, intPoint aFaceTwoaBegin, Point aCentrePointaEnd) {
if ( aBegin.compareTo(aEnd) <= 0 ) {
pointOne = aPointOne; pointTwo = aPointTwo; faceOne = aFaceOne; faceTwo = aFaceTwo;
begin = aBegin; end = aEnd;
centrePoint = aCentrePoint;
} else {
begin = aEnd; end = aBegin;
}
midEdge = centroid(List.of( begin, end ));
}
 
@Override
public boolean equals(Object other) {
return switch ( other ) {
case Edge edge -> begin.equals(edge.begin) && end.equals(edge.end);
default -> false;
};
}
@Override
public int compareTohashCode(Edge other) {
return Objects.hash(begin, end);
if ( pointOne == other.pointOne ) {
if ( pointTwo == other.pointTwo ) {
return Integer.compare(faceOne, other.faceOne);
}
return Integer.compare(pointTwo, other.pointTwo);
}
return Integer.compare(pointOne, other.pointOne);
}
private int pointOne, pointTwo, faceOne, faceTwo;
private Point centrePoint;
}
// A point together with the number of edges containing that point.
private static class PointWithEdgeCount {
public PointWithEdgeCountboolean contains(Point aPoint, int aEdgeCountpoint) {
return point.equals(begin) || point.equals(end);
point = aPoint; edgeCount = aEdgeCount;
}
public boolean holeEdge;
private Point point;
public Point edgePoint;
private int edgeCount;
public final Point begin, end, midEdge;
}
// A face of the current Catmull-Clark surface.
private static record IndexPair(int first, int second) {}
private static recordfinal ListPair(List<Point>class points, List<List<Integer>> faces)Face {}
public Face(List<Point> aVertices) {
private static final int FACE_NOT_SET = -1;
vertices = new ArrayList<Point>(aVertices);
facePoint = centroid(vertices);
edges = new ArrayList<Edge>();
for ( int i = 0; i < vertices.size() - 1; i++ ) {
edges.addLast( new Edge(vertices.get(i), vertices.get(i + 1)) );
}
edges.addLast( new Edge(vertices.getLast(), vertices.getFirst()) );;
}
public boolean contains(Point vertex) {
return vertices.contains(vertex);
}
public boolean contains(Edge edge) {
return contains(edge.begin) && contains(edge.end);
}
public String toString() {
return "Face: " + vertices.stream().map( point -> point.toString() ).collect(Collectors.joining("; "));
}
public final List<Point> vertices;
public final Point facePoint;
public final List<Edge> edges;
}
 
}
Line 1,264 ⟶ 1,176:
{{ out }}
<pre>
Surface {
(-0.55556, 0.55556, 0.55556)
Face: (-1.000, 1.000, 1.000); (-1.000, -1.000, 1.000); ( 1.000, -1.000, 1.000); ( 1.000, 1.000, 1.000)
(-0.55556, -0.55556, 0.55556)
Face: ( 1.000, 1.000, 1.000); ( 1.000, -1.000, 1.000); ( 1.000, -1.000, -1.000); ( 1.000, 1.000, -1.000)
( 0.55556, -0.55556, 0.55556)
Face: ( 1.000, 1.000, -1.000); ( 1.000, -1.000, -1.000); (-1.000, -1.000, -1.000); (-1.000, 1.000, -1.000)
( 0.55556, 0.55556, 0.55556)
Face: (-1.000, 1.000, -1.000); (-1.000, 1.000, 1.000); ( 1.000, 1.000, 1.000); ( 1.000, 1.000, -1.000)
( 0.55556, -0.55556, -0.55556)
Face: (-1.000, 1.000, -1.000); (-1.000, -1.000, -1.000); (-1.000, -1.000, 1.000); (-1.000, 1.000, 1.000)
( 0.55556, 0.55556, -0.55556)
Face: (-1.000, -1.000, -1.000); (-1.000, -1.000, 1.000); ( 1.000, -1.000, 1.000); ( 1.000, -1.000, -1.000)
(-0.55556, -0.55556, -0.55556)
}
(-0.55556, 0.55556, -0.55556)
( 0.00000, 0.00000, 1.00000)
( 1.00000, 0.00000, 0.00000)
( 0.00000, 0.00000, -1.00000)
( 0.00000, 1.00000, 0.00000)
(-1.00000, 0.00000, 0.00000)
( 0.00000, -1.00000, 0.00000)
(-0.75000, 0.00000, 0.75000)
( 0.00000, 0.75000, 0.75000)
(-0.75000, 0.75000, 0.00000)
( 0.00000, -0.75000, 0.75000)
(-0.75000, -0.75000, 0.00000)
( 0.75000, 0.00000, 0.75000)
( 0.75000, -0.75000, 0.00000)
( 0.75000, 0.75000, 0.00000)
( 0.75000, 0.00000, -0.75000)
( 0.00000, -0.75000, -0.75000)
( 0.00000, 0.75000, -0.75000)
(-0.75000, 0.00000, -0.75000)
 
Surface {
[0, 14, 8, 15]
Face: (-0.556, 0.556, 0.556); (-0.750, 0.000, 0.750); ( 0.000, 0.000, 1.000); ( 0.000, 0.750, 0.750)
[1, 17, 8, 14]
Face: (-0.556, -0.556, 0.556); ( 0.000, -0.750, 0.750); ( 0.000, 0.000, 1.000); (-0.750, 0.000, 0.750)
[2, 19, 8, 17]
Face: ( 0.556, -0.556, 0.556); ( 0.750, 0.000, 0.750); ( 0.000, 0.000, 1.000); ( 0.000, -0.750, 0.750)
[3, 15, 8, 19]
Face: ( 0.556, 0.556, 0.556); ( 0.000, 0.750, 0.750); ( 0.000, 0.000, 1.000); ( 0.750, 0.000, 0.750)
[3, 19, 9, 21]
Face: ( 0.556, 0.556, 0.556); ( 0.750, 0.000, 0.750); ( 1.000, 0.000, 0.000); ( 0.750, 0.750, 0.000)
[2, 20, 9, 19]
Face: ( 0.556, -0.556, 0.556); ( 0.750, -0.750, 0.000); ( 1.000, 0.000, 0.000); ( 0.750, 0.000, 0.750)
[4, 22, 9, 20]
Face: ( 0.556, -0.556, -0.556); ( 0.750, 0.000, -0.750); ( 1.000, 0.000, 0.000); ( 0.750, -0.750, 0.000)
[5, 21, 9, 22]
Face: ( 0.556, 0.556, -0.556); ( 0.750, 0.750, 0.000); ( 1.000, 0.000, 0.000); ( 0.750, 0.000, -0.750)
[5, 22, 10, 24]
Face: ( 0.556, 0.556, -0.556); ( 0.750, 0.000, -0.750); ( 0.000, 0.000, -1.000); ( 0.000, 0.750, -0.750)
[4, 23, 10, 22]
Face: ( 0.556, -0.556, -0.556); ( 0.000, -0.750, -0.750); ( 0.000, 0.000, -1.000); ( 0.750, 0.000, -0.750)
[6, 25, 10, 23]
Face: (-0.556, -0.556, -0.556); (-0.750, 0.000, -0.750); ( 0.000, 0.000, -1.000); ( 0.000, -0.750, -0.750)
[7, 24, 10, 25]
Face: (-0.556, 0.556, -0.556); ( 0.000, 0.750, -0.750); ( 0.000, 0.000, -1.000); (-0.750, 0.000, -0.750)
[7, 16, 11, 24]
Face: (-0.556, 0.556, -0.556); (-0.750, 0.750, 0.000); ( 0.000, 1.000, 0.000); ( 0.000, 0.750, -0.750)
[0, 15, 11, 16]
Face: (-0.556, 0.556, 0.556); ( 0.000, 0.750, 0.750); ( 0.000, 1.000, 0.000); (-0.750, 0.750, 0.000)
[3, 21, 11, 15]
Face: ( 0.556, 0.556, 0.556); ( 0.750, 0.750, 0.000); ( 0.000, 1.000, 0.000); ( 0.000, 0.750, 0.750)
[5, 24, 11, 21]
Face: ( 0.556, 0.556, -0.556); ( 0.000, 0.750, -0.750); ( 0.000, 1.000, 0.000); ( 0.750, 0.750, 0.000)
[7, 25, 12, 16]
Face: (-0.556, 0.556, -0.556); (-0.750, 0.000, -0.750); (-1.000, 0.000, 0.000); (-0.750, 0.750, 0.000)
[6, 18, 12, 25]
Face: (-0.556, -0.556, -0.556); (-0.750, -0.750, 0.000); (-1.000, 0.000, 0.000); (-0.750, 0.000, -0.750)
[1, 14, 12, 18]
Face: (-0.556, -0.556, 0.556); (-0.750, 0.000, 0.750); (-1.000, 0.000, 0.000); (-0.750, -0.750, 0.000)
[0, 16, 12, 14]
Face: (-0.556, 0.556, 0.556); (-0.750, 0.750, 0.000); (-1.000, 0.000, 0.000); (-0.750, 0.000, 0.750)
[6, 18, 13, 23]
Face: (-0.556, -0.556, -0.556); (-0.750, -0.750, 0.000); ( 0.000, -1.000, 0.000); ( 0.000, -0.750, -0.750)
[1, 17, 13, 18]
Face: (-0.556, -0.556, 0.556); ( 0.000, -0.750, 0.750); ( 0.000, -1.000, 0.000); (-0.750, -0.750, 0.000)
[2, 20, 13, 17]
Face: ( 0.556, -0.556, 0.556); ( 0.750, -0.750, 0.000); ( 0.000, -1.000, 0.000); ( 0.000, -0.750, 0.750)
[4, 23, 13, 20]
Face: ( 0.556, -0.556, -0.556); ( 0.000, -0.750, -0.750); ( 0.000, -1.000, 0.000); ( 0.750, -0.750, 0.000)
}
</pre>
 
908

edits