256 lines
7.2 KiB
JavaScript
256 lines
7.2 KiB
JavaScript
// A point of the current Catmull-Clark surface.
|
|
class Point {
|
|
constructor(x = 0.0, y = 0.0, z = 0.0) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
}
|
|
|
|
add(other) {
|
|
return new Point(this.x + other.x, this.y + other.y, this.z + other.z);
|
|
}
|
|
|
|
multiply(factor) {
|
|
return new Point(this.x * factor, this.y * factor, this.z * factor);
|
|
}
|
|
|
|
divide(factor) {
|
|
return this.multiply(1.0 / factor);
|
|
}
|
|
|
|
lessThan(other) {
|
|
return (this.x < other.x) ||
|
|
(this.x === other.x && this.y < other.y) ||
|
|
(this.x === other.x && this.y === other.y && this.z < other.z);
|
|
}
|
|
|
|
equals(other) {
|
|
return this.x === other.x && this.y === other.y && this.z === other.z;
|
|
}
|
|
|
|
format(value) {
|
|
const formattedValue = value.toFixed(3);
|
|
return (value >= 0) ? " " + formattedValue : formattedValue;
|
|
}
|
|
|
|
toString() {
|
|
return "(" + this.format(this.x) + ", " + this.format(this.y) + ", " + this.format(this.z) + ")";
|
|
}
|
|
}
|
|
|
|
// Return the centroid point of the given array of points.
|
|
function centroid(points) {
|
|
let sum = new Point();
|
|
for (const point of points) {
|
|
sum = sum.add(point);
|
|
}
|
|
return sum.divide(points.length);
|
|
}
|
|
|
|
// An edge of the current Catmull-Clark surface.
|
|
class Edge {
|
|
constructor(aBegin, aEnd) {
|
|
if (aEnd.lessThan(aBegin)) {
|
|
[aBegin, aEnd] = [aEnd, aBegin];
|
|
}
|
|
this.begin = aBegin;
|
|
this.end = aEnd;
|
|
this.mid_edge = centroid([this.begin, this.end]);
|
|
this.hole_edge = false;
|
|
}
|
|
|
|
contains(point) {
|
|
return point.equals(this.begin) || point.equals(this.end);
|
|
}
|
|
|
|
lessThan(other) {
|
|
return this.begin.lessThan(other.begin) ||
|
|
(this.begin.equals(other.begin) && this.end.lessThan(other.end));
|
|
}
|
|
|
|
equals(other) {
|
|
return this.contains(other.begin) && this.contains(other.end);
|
|
}
|
|
}
|
|
|
|
// A face of the current Catmull-Clark surface.
|
|
class Face {
|
|
constructor(aVertices) {
|
|
this.vertices = aVertices;
|
|
this.face_point = centroid(this.vertices);
|
|
this.edges = [];
|
|
|
|
for (let i = 0; i < this.vertices.length - 1; ++i) {
|
|
this.edges.push(new Edge(this.vertices[i], this.vertices[i + 1]));
|
|
}
|
|
this.edges.push(new Edge(this.vertices[this.vertices.length - 1], this.vertices[0]));
|
|
}
|
|
|
|
contains(vertex) {
|
|
if (vertex instanceof Point) {
|
|
return this.vertices.some(v => v.equals(vertex));
|
|
} else if (vertex instanceof Edge) {
|
|
return this.contains(vertex.begin) && this.contains(vertex.end);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
toString() {
|
|
let result = "Face: ";
|
|
for (let i = 0; i < this.vertices.length - 1; ++i) {
|
|
result += this.vertices[i].toString() + ", ";
|
|
}
|
|
return result + this.vertices[this.vertices.length - 1].toString();
|
|
}
|
|
}
|
|
|
|
// Return a map containing, for each vertex,
|
|
// the new vertex created by the current iteration of the Catmull-Clark surface subdivision algorithm.
|
|
function next_vertices(edges, faces) {
|
|
const next_vertices_map = new Map();
|
|
const vertices = new Set();
|
|
|
|
// Collect all unique vertices
|
|
for (const face of faces) {
|
|
for (const vertex of face.vertices) {
|
|
vertices.add(vertex);
|
|
}
|
|
}
|
|
|
|
// Convert Set to Array for iteration
|
|
const verticesArray = Array.from(vertices);
|
|
|
|
for (const vertex of verticesArray) {
|
|
const faces_for_vertex = faces.filter(face => face.contains(vertex));
|
|
const edges_for_vertex = edges.filter(edge => edge.contains(vertex));
|
|
|
|
if (faces_for_vertex.length !== edges_for_vertex.length) {
|
|
const mid_edge_of_hole_edges = edges_for_vertex
|
|
.filter(edge => edge.hole_edge)
|
|
.map(edge => edge.mid_edge);
|
|
|
|
mid_edge_of_hole_edges.push(vertex);
|
|
next_vertices_map.set(vertex, centroid(mid_edge_of_hole_edges));
|
|
} else {
|
|
const face_count = faces_for_vertex.length;
|
|
const multiple_1 = (face_count - 3) / face_count;
|
|
const multiple_2 = 1.0 / face_count;
|
|
const multiple_3 = 2.0 / face_count;
|
|
|
|
const next_vertex_1 = vertex.multiply(multiple_1);
|
|
const face_points = faces_for_vertex.map(face => face.face_point);
|
|
const next_vertex_2 = centroid(face_points).multiply(multiple_2);
|
|
const mid_edges = edges_for_vertex.map(edge => edge.mid_edge);
|
|
const next_vertex_3 = centroid(mid_edges).multiply(multiple_3);
|
|
const next_vertex_4 = next_vertex_1.add(next_vertex_2);
|
|
|
|
next_vertices_map.set(vertex, next_vertex_4.add(next_vertex_3));
|
|
}
|
|
}
|
|
|
|
return next_vertices_map;
|
|
}
|
|
|
|
// The Catmull-Clarke surface subdivision algorithm.
|
|
function catmull_clark_surface_subdivision(faces) {
|
|
// Determine, for each edge, whether or not it is an edge of a hole,
|
|
// and set its edge point accordingly.
|
|
for (const face of faces) {
|
|
for (const edge of face.edges) {
|
|
const face_points_for_edge = [];
|
|
for (const search_face of faces) {
|
|
if (search_face.contains(edge)) {
|
|
face_points_for_edge.push(search_face.face_point);
|
|
}
|
|
}
|
|
|
|
if (face_points_for_edge.length === 2) {
|
|
edge.hole_edge = false;
|
|
edge.edge_point = centroid([edge.mid_edge, centroid(face_points_for_edge)]);
|
|
} else {
|
|
edge.hole_edge = true;
|
|
edge.edge_point = edge.mid_edge;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect all edges
|
|
const edges = [];
|
|
for (const face of faces) {
|
|
for (const edge of face.edges) {
|
|
edges.push(edge);
|
|
}
|
|
}
|
|
|
|
const next_vertex_map = next_vertices(edges, faces);
|
|
const next_faces = [];
|
|
|
|
for (const face of faces) {
|
|
if (face.vertices.length >= 3) { // A face with 2 or fewer points does not contribute to the surface
|
|
const face_point = face.face_point;
|
|
for (let i = 0; i < face.vertices.length; ++i) {
|
|
const prevIndex = (i - 1 + face.vertices.length) % face.vertices.length;
|
|
next_faces.push(new Face([
|
|
next_vertex_map.get(face.vertices[i]),
|
|
face.edges[i].edge_point,
|
|
face_point,
|
|
face.edges[prevIndex].edge_point
|
|
]));
|
|
}
|
|
}
|
|
}
|
|
|
|
return next_faces;
|
|
}
|
|
|
|
// Display the current Catmull-Clark surface on the console.
|
|
function displaySurface(faces) {
|
|
console.log("Surface {");
|
|
for (const face of faces) {
|
|
console.log(face.toString());
|
|
}
|
|
console.log("}");
|
|
console.log();
|
|
}
|
|
|
|
// Main function
|
|
function main() {
|
|
const faces = [
|
|
new Face([
|
|
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([
|
|
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([
|
|
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([
|
|
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([
|
|
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([
|
|
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);
|
|
const iterations = 1;
|
|
let result = faces;
|
|
for (let i = 0; i < iterations; ++i) {
|
|
result = catmull_clark_surface_subdivision(result);
|
|
}
|
|
displaySurface(result);
|
|
}
|
|
|
|
// Run the main function
|
|
main();
|