/** * Convenience Function for calculating the distance between two vectors * because THREE JS Vector functions mutate variables * @param {Vector3} a - Vector A * @param {Vector3} b - Vector B */ function vectorLength(a, b) { let v1 = new THREE.Vector3(); v1.set(a.x, a.y, a.z); let v2 = new THREE.Vector3(); v2.set(b.x, b.y, b.z); return v1.sub(v2).length(); } /** * Class representing a quad face * Each face consists of two triangular mesh faces * containts four indices for determining vertices * and six springs, one between each of the vertices */ export class Face { a; b; c; d; springs = []; constructor(a, b, c, d) { this.a = a; this.b = b; this.c = c; this.d = d; } } /** * Class representing a single spring * has a current and resting length * and indices to the two connected vertices */ export class Spring { restLength; currentLength; index1; index2; /** * set vertex indices * and calculate inital length based on the * vertex positions * @param {Array of Vector3} vertices * @param {number} index1 * @param {number} index2 */ constructor(vertices, index1, index2) { this.index1 = index1; this.index2 = index2; let length = vectorLength(vertices[index1], vertices[index2]); this.restLength = length; this.currentLength = length; } } /** * Class representing a single piece of cloth * contains THREE JS geometry, * logically represented by an array of adjacent faces * and vertex weights which are accessed by the same * indices as the vertices in the Mesh */ export class Cloth { VertexWeight = 1; geometry = new THREE.Geometry(); faces = []; vertexWeights = []; /** * creates a rectangular piece of cloth * takes the size of the cloth * and the number of vertices it should be composed of * @param {number} width - width of the cloth * @param {number} height - height of the cloth * @param {number} numPointsWidth - number of vertices in horizontal direction * @param {number} numPointsHeight - number of vertices in vertical direction */ createBasic(width, height, numPointsWidth, numPointsHeight) { /** resulting vertices and faces */ let vertices = []; let faces = []; /** * distance between two vertices horizontally/vertically * divide by the number of points minus one * because there are (n - 1) lines between n vertices */ let stepWidth = width / (numPointsWidth - 1); let stepHeight = height / (numPointsHeight - 1); /** * iterate over the number of vertices in x/y axis * and add a new Vector3 to "vertices" */ for (let y = 0; y < numPointsHeight; y++) { for (let x = 0; x < numPointsWidth; x++) { vertices.push( new THREE.Vector3(x * stepWidth, height - y * stepHeight, 0) ); } } /** * helper function to calculate index of vertex * in "vertices" array based on its x and y positions * in the mesh * @param {number} x - x index of vertex * @param {number} y - y index of vertex */ function getVertexIndex(x, y) { return y * numPointsWidth + x; } /** * generate faces based on 4 vertices * and 6 springs each */ for (let y = 0; y < numPointsHeight - 1; y++) { for (let x = 0; x < numPointsWidth - 1; x++) { let newFace = new Face( getVertexIndex(x, y), getVertexIndex(x, y + 1), getVertexIndex(x + 1, y), getVertexIndex(x + 1, y + 1), ); newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x + 1, y))); newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x, y + 1))); newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x + 1, y + 1))); newFace.springs.push(new Spring(vertices, getVertexIndex(x + 1, y), getVertexIndex(x, y + 1))); newFace.springs.push(new Spring(vertices, getVertexIndex(x + 1, y), getVertexIndex(x + 1, y + 1))); newFace.springs.push(new Spring(vertices, getVertexIndex(x, y + 1), getVertexIndex(x + 1, y + 1))); faces.push(newFace); } } /** * call createExplicit * with generated vertices and faces */ this.createExplicit(vertices, faces); } /** * Generate THREE JS Geometry * (list of vertices and list of indices representing triangles) * and calculate the weight of each face and split it between * surrounding vertices * @param {Array of Vector3} vertices * @param {Array of Face} faces */ createExplicit(vertices, faces) { /** * Copy vertices and initialize vertex weights to 0 */ for (let i in vertices) { this.geometry.vertices.push(vertices[i]); this.vertexWeights.push(0); } /** * copy faces, * generate two triangles per face, * calculate weight of face as its area * and split between the 4 vertices */ for (let i in faces) { let face = faces[i]; /** copy faces to class member */ this.faces.push(face); /** generate triangles */ this.geometry.faces.push(new THREE.Face3( face.a, face.b, face.c )); this.geometry.faces.push(new THREE.Face3( face.c, face.b, face.d )); /** * calculate area of face as combined area of * its two composing triangles */ let xLength = vectorLength(this.geometry.vertices[face.b], this.geometry.vertices[face.a]); let yLength = vectorLength(this.geometry.vertices[face.c], this.geometry.vertices[face.a]); let weight = xLength * yLength / 2; xLength = vectorLength(this.geometry.vertices[face.b], this.geometry.vertices[face.d]); yLength = vectorLength(this.geometry.vertices[face.c], this.geometry.vertices[face.d]); weight += xLength * yLength / 2; /** * split weight equally between four surrounding vertices */ this.vertexWeights[face.a] += weight / 4; this.vertexWeights[face.b] += weight / 4; this.vertexWeights[face.c] += weight / 4; this.vertexWeights[face.d] += weight / 4; } /** * let THREE JS compute bounding sphere around generated mesh * needed for View Frustum Culling internally */ this.geometry.computeBoundingSphere(); } /** * generate a debug mesh for visualizing * vertices and springs of the cloth * and add it to scene for rendering * @param {Scene} scene - Scene to add Debug Mesh to */ createDebugMesh(scene) { /** * helper function to generate a single line * between two Vertices with a given color * @param {Vector3} from * @param {Vector3} to * @param {number} color */ function addLine(from, to, color) { let geometry = new THREE.Geometry(); geometry.vertices.push(from); geometry.vertices.push(to); let material = new THREE.LineBasicMaterial( { color: color, linewidth: 10 } ); let line = new THREE.Line(geometry, material); line.renderOrder = 1; scene.add(line); } /** * helper function to generate a small sphere * at a given Vertex Position with color * @param {Vector3} point * @param {number} color */ function addPoint(point, color) { const geometry = new THREE.SphereGeometry( 0.05, 32, 32 ); const material = new THREE.MeshBasicMaterial( { color: color } ); const sphere = new THREE.Mesh( geometry, material ); sphere.position.set(point.x, point.y, point.z); scene.add( sphere ); } let lineColor = 0x000000; let pointColor = 0xff00000; /** * generate one line for each of the 6 springs * and one point for each of the 4 vertices * for all of the faces */ for (let i in this.faces) { let face = this.faces[i]; addLine(this.geometry.vertices[face.a], this.geometry.vertices[face.b], lineColor); addLine(this.geometry.vertices[face.a], this.geometry.vertices[face.c], lineColor); addLine(this.geometry.vertices[face.a], this.geometry.vertices[face.d], lineColor); addLine(this.geometry.vertices[face.b], this.geometry.vertices[face.c], lineColor); addLine(this.geometry.vertices[face.b], this.geometry.vertices[face.d], lineColor); addLine(this.geometry.vertices[face.c], this.geometry.vertices[face.d], lineColor); addPoint(this.geometry.vertices[face.a], pointColor); addPoint(this.geometry.vertices[face.b], pointColor); addPoint(this.geometry.vertices[face.c], pointColor); addPoint(this.geometry.vertices[face.d], pointColor); } } }