/** * 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.copy(a); let v2 = new THREE.Vector3(); v2.copy(b); 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} 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; } getDirection(vertices) { let direction = new THREE.Vector3(); direction.copy(vertices[this.index1]); direction.sub(vertices[this.index2]); direction.divideScalar(vectorLength(vertices[this.index1], vertices[this.index2])); return direction; } update(vertices) { let length = vectorLength(vertices[this.index1], vertices[this.index2]); 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 = []; vertexRigidness = []; fixedPoints = []; externalForces = []; windForce = 50; /** * 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 = []; this.numPointsWidth = numPointsWidth; this.numPointsHeight = numPointsHeight; /** * 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 - (numPointsWidth/2)) * 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))); // oben newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x, y + 1))); // links newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x + 1, y + 1))); // oben links -> unten rechts diagonal newFace.springs.push(new Spring(vertices, getVertexIndex(x + 1, y), getVertexIndex(x, y + 1))); // oben rechts -> unten links diagonal newFace.springs.push(new Spring(vertices, getVertexIndex(x + 1, y), getVertexIndex(x + 1, y + 1))); // rechts newFace.springs.push(new Spring(vertices, getVertexIndex(x, y + 1), getVertexIndex(x + 1, y + 1))); // unten faces.push(newFace); } } /** * call createExplicit * with generated vertices and faces */ this.createExplicit(vertices, faces); /** * hand cloth from left and right upper corners */ //this.vertexRigidness[0] = true; //this.vertexRigidness[numPointsWidth * (numPointsHeight - 1)] = true; this.fixedPoints.push(getVertexIndex(8, 10)); this.fixedPoints.push(getVertexIndex(12, 9)); } /** * 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} vertices * @param {Array} faces */ createExplicit(vertices, faces) { /** * Copy vertices and initialize vertex weights to 0 */ for (let i in vertices) { this.geometry.vertices.push(vertices[i].clone()); this.previousPositions.push(vertices[i].clone()); // this.geometry.vertices.push(vertices[i]); // this.previousPositions.push(vertices[i]); this.vertexWeights.push(0); this.vertexRigidness.push(false); this.externalForces.push(new THREE.Vector3(0,0,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(); this.geometry.computeFaceNormals(); this.geometry.computeVertexNormals(); } /** * 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); } } previousPositions = []; time = 0; /** * * @param {number} dt time in seconds since last frame */ simulate(dt) { for (let i in this.geometry.vertices) { let acceleration = this.getAcceleration(i, dt); //acceleration.clampLength(0, 10); if (Math.abs(acceleration.length()) <= 10e-4) { acceleration.set(0, 0, 0); } let currentPosition = this.verlet(this.geometry.vertices[i].clone(), this.previousPositions[i].clone(), acceleration, dt); //let currentPosition = this.euler(this.geometry.vertices[i], acceleration, dt); this.previousPositions[i].copy(this.geometry.vertices[i]); this.geometry.vertices[i].copy(currentPosition); } this.checkIntersect(); this.time += dt; for (let face of this.faces) { for (let spring of face.springs) { spring.update(this.geometry.vertices); } } /** * let THREE JS compute bounding sphere around generated mesh * needed for View Frustum Culling internally */ this.geometry.verticesNeedUpdate = true; this.geometry.elementsNeedUpdate = true; this.geometry.computeBoundingSphere(); this.geometry.computeFaceNormals(); this.geometry.computeVertexNormals(); } checkIntersect() { let npw = this.numPointsWidth; function getX(i, ) { return i % npw; } function getY(i) { return Math.floor(i / npw); } for (let i in this.geometry.vertices) { for (let j in this.geometry.vertices) { this.vertexRigidness[i] = false; this.vertexRigidness[j] = false; if (i == j || (Math.abs(getX(i) - getX(j)) == 1 && Math.abs(getY(i) - getY(j)) == 1)) continue; let posI = this.geometry.vertices[i]; let posJ = this.geometry.vertices[j]; let dist = posI.distanceTo(posJ); const collisionDistance = 0.5; if (dist < collisionDistance) { this.vertexRigidness[i] = true; this.vertexRigidness[j] = true; let diff = this.geometry.vertices[i].clone().sub(this.geometry.vertices[j]).normalize().multiplyScalar((collisionDistance - dist) * 1.001 / 2); this.geometry.vertices[i].add(diff); this.geometry.vertices[j].sub(diff); console.log(this.geometry.vertices[i].distanceTo(this.geometry.vertices[j])); } } } } /** * Equation of motion for each vertex which represents the acceleration * @param {number} vertexIndex The index of the current vertex whose acceleration should be calculated * @param {number} dt The time passed since last frame */ getAcceleration(vertexIndex, dt) { if (this.fixedPoints.includes(parseInt(vertexIndex)) || this.vertexRigidness[vertexIndex]) { return new THREE.Vector3(0, 0, 0); } let externalForce = this.externalForces[vertexIndex]; let vertex = this.geometry.vertices[vertexIndex];//.add(externalForce); // Mass of vertex let M = this.vertexWeights[vertexIndex]; // constant gravity let g = new THREE.Vector3(0, -9.8, 0); // stiffness let k = 500; // Wind vector let fWind = new THREE.Vector3( Math.sin(vertex.x * vertex.y * this.time), Math.cos(vertex.z * this.time), Math.sin(Math.cos(5 * vertex.x * vertex.y * vertex.z)) ); /** * constant determined by the properties of the surrounding fluids (air) * achievement of cloth effects through try out * */ let a = 0.1; let velocity = new THREE.Vector3( (this.previousPositions[vertexIndex].x - vertex.x) / dt, (this.previousPositions[vertexIndex].y - vertex.y) / dt, (this.previousPositions[vertexIndex].z - vertex.z) / dt ); //console.log(velocity, vertex, this.previousPositions[vertexIndex]); let fAirResistance = velocity.multiply(velocity).multiplyScalar(-a); let springSum = new THREE.Vector3(0, 0, 0); // Get the bounding springs and add them to the needed springs // TODO: optimize const numPointsX = this.numPointsWidth; const numPointsY = this.numPointsHeight; const numFacesX = numPointsX - 1; const numFacesY = numPointsY - 1; function getFaceIndex(x, y) { return y * numFacesX + x; } let indexX = vertexIndex % numPointsX; let indexY = Math.floor(vertexIndex / numPointsX); let springs = []; // 0 oben // 1 links // 2 oben links -> unten rechts diagonal // 3 oben rechts -> unten links diagonal // 4 rechts // 5 unten let ul = indexX > 0 && indexY < numPointsY - 1; let ur = indexX < numPointsX - 1 && indexY < numPointsY - 1; let ol = indexX > 0 && indexY > 0; let or = indexX < numPointsX - 1 && indexY > 0; if (ul) { let faceUL = this.faces[getFaceIndex(indexX - 1, indexY)]; springs.push(faceUL.springs[3]); if (!ol) springs.push(faceUL.springs[0]); springs.push(faceUL.springs[4]); } if (ur) { let faceUR = this.faces[getFaceIndex(indexX, indexY)]; springs.push(faceUR.springs[2]); if (!or) springs.push(faceUR.springs[0]); if (!ul) springs.push(faceUR.springs[1]); } if (ol) { let faceOL = this.faces[getFaceIndex(indexX - 1, indexY - 1)]; springs.push(faceOL.springs[2]); springs.push(faceOL.springs[4]); springs.push(faceOL.springs[5]); } if (or) { let faceOR = this.faces[getFaceIndex(indexX , indexY - 1)]; springs.push(faceOR.springs[3]); if (!ol) springs.push(faceOR.springs[1]); springs.push(faceOR.springs[5]); } for (let spring of springs) { let springDirection = spring.getDirection(this.geometry.vertices); if (spring.index1 == vertexIndex) springDirection.multiplyScalar(-1); springSum.add(springDirection.multiplyScalar(k * (spring.restLength - spring.currentLength))); } let result = new THREE.Vector3(1, 1, 1); result.multiplyScalar(M).multiply(g).add(fWind).add(externalForce).add(fAirResistance).sub(springSum); document.getElementById("Output").innerText = "SpringSum: " + Math.floor(springSum.y); let threshold = 1; let forceReduktion = 0.8; if(Math.abs(externalForce.z) > threshold){ externalForce.z *= forceReduktion; } else { externalForce.z = 0; } if(Math.abs(externalForce.y) > threshold){ externalForce.y *= forceReduktion; } else { externalForce.y = 0; } if(Math.abs(externalForce.x) > threshold){ externalForce.x *= forceReduktion; } else { externalForce.x = 0; } return result; } /** * The Verlet algorithm as an integrator * to get the next position of a vertex * @param {Vector3} currentPosition * @param {Vector3} previousPosition * @param {Vector3} acceleration * @param {number} passedTime The delta time since last frame */ verlet(currentPosition, previousPosition, acceleration, passedTime) { // verlet algorithm // next position = 2 * current Position - previous position + acceleration * (passed time)^2 // acceleration (dv/dt) = F(net) // Dependency for one vertex: gravity, fluids/air, springs const DRAG = 0.96; let nextPosition = new THREE.Vector3( (currentPosition.x - previousPosition.x) * DRAG + currentPosition.x + acceleration.x * (passedTime * passedTime), (currentPosition.y - previousPosition.y) * DRAG + currentPosition.y + acceleration.y * (passedTime * passedTime), (currentPosition.z - previousPosition.z) * DRAG + currentPosition.z + acceleration.z * (passedTime * passedTime), ); // let nextPosition = new THREE.Vector3( // (2 * currentPosition.x) - previousPosition.x + acceleration.x * (passedTime * passedTime), // (2 * currentPosition.y) - previousPosition.y + acceleration.y * (passedTime * passedTime), // (2 * currentPosition.z) - previousPosition.z + acceleration.z * (passedTime * passedTime), // ); return nextPosition; } euler(currentPosition, acceleration, passedTime) { let nextPosition = new THREE.Vector3( currentPosition.x + acceleration.x * passedTime, currentPosition.y + acceleration.y * passedTime, currentPosition.z + acceleration.z * passedTime, ); return nextPosition; } wind(intersects) { let intersect = intersects[0]; this.externalForces[intersect.face.a].z -= this.windForce; this.externalForces[intersect.face.b].z -= this.windForce; this.externalForces[intersect.face.c].z -= this.windForce; } mousePressed = false; mouseMoved = false; intersects; mousePress(intersects){ this.mousePressed = true; this.intersects = intersects; } mouseMove(mousePos){ this.mouseMoved = true; if(this.mousePressed){ let intersect = this.intersects[0]; this.externalForces[intersect.face.a].add(mousePos.clone().sub(this.geometry.vertices[intersect.face.a]).multiplyScalar(90)); /* this.geometry.vertices[intersect.face.a].x = mousePos.x; this.geometry.vertices[intersect.face.a].y = mousePos.y; this.geometry.vertices[intersect.face.a].z = mousePos.z; */ } } mouseRelease(){ this.mousePressed = false; } }