abouttreesummaryrefslogcommitdiff
path: root/js/cloth.js
diff options
context:
space:
mode:
authorpatrick-scho2025-04-17 22:22:14 +0200
committerpatrick-scho2025-04-17 22:22:14 +0200
commit959c350fb8cddf7d1b31907fcc1f7f99dad52f3e (patch)
treece2f5e05082aa0536bad3d87dce424a3d3b2db93 /js/cloth.js
parentf97f4ee25759ffaa6a4d4709f45fc8b7b5b24973 (diff)
downloadcloth_sim-959c350fb8cddf7d1b31907fcc1f7f99dad52f3e.tar.gz
cloth_sim-959c350fb8cddf7d1b31907fcc1f7f99dad52f3e.zip
restructureHEADmain
Diffstat (limited to 'js/cloth.js')
-rw-r--r--js/cloth.js317
1 files changed, 317 insertions, 0 deletions
diff --git a/js/cloth.js b/js/cloth.js
new file mode 100644
index 0000000..bd13951
--- /dev/null
+++ b/js/cloth.js
@@ -0,0 +1,317 @@
+const DAMPING = 0.03;
+const DRAG = 1 - DAMPING;
+const MASS = 0.1;
+const GRAVITY = new THREE.Vector3(0, -9.81 * MASS, 0);
+const K = 1;
+const MAX_STRETCH = 1.5;
+
+const options = {
+ wind: true,
+};
+
+class Spring {
+ constructor(p1, p2, restDist) {
+ this.p1 = p1;
+ this.p2 = p2;
+ this.restDist = restDist;
+ }
+
+ satisfy() {
+ /** calculate current spring length */
+ const diff = this.p2.position.clone().sub(this.p1.position);
+ const currentDist = diff.length();
+ if (currentDist == 0) return;
+ if (currentDist <= this.restDist) return;
+ //const correction = diff.multiplyScalar(1 - (this.restDist / currentDist));
+
+ /** calculate necessary correction length and direction */
+ const correction = diff.multiplyScalar((currentDist - this.restDist) / currentDist);
+ correction.multiplyScalar(K);
+ const correctionHalf = correction.multiplyScalar(0.5);
+
+ let p1movable = this.p1.movable && this.p1.movableTmp;
+ let p2movable = this.p2.movable && this.p2.movableTmp;
+
+ /** apply correction if masses aren't fixed */
+ /** divide correction if both are movable */
+ if (p1movable && p2movable) {
+ this.p1.position.add(correctionHalf);
+ this.p2.position.sub(correctionHalf);
+ } else if (! p1movable && p2movable) {
+ this.p2.position.sub(correction);
+ } else if (p1movable && ! p2movable) {
+ this.p1.position.add(correction);
+ }
+ }
+}
+
+class Mass {
+ movableTmp = true;
+ movable = true;
+
+ constructor(x, y, z, mass) {
+ this.position = new THREE.Vector3(x, y, z);
+ this.previous = new THREE.Vector3(x, y, z);
+ this.acceleration = new THREE.Vector3(0, 0, 0);
+ this.mass = mass;
+ }
+ addForce(force) {
+ this.acceleration.add(
+ force.clone().multiplyScalar(1/this.mass)
+ );
+ }
+ verlet(dt) {
+ // verlet algorithm
+ // next position = 2 * current Position - previous position + acceleration * (passed time)^2
+ // acceleration (dv/dt) = F(net)
+ /** calculate velocity */
+ const nextPosition = this.position.clone().sub(this.previous);
+ /** apply drag */
+ nextPosition.multiplyScalar(DRAG);
+ /** add to current position and add acceleration */
+ nextPosition.add(this.position);
+ nextPosition.add(this.acceleration.multiplyScalar(dt*dt));
+
+ if (this.movable && this.movableTmp) {
+ this.previous = this.position;
+ this.position = nextPosition;
+ }
+
+ /** reset for next frame */
+ this.acceleration.set(0, 0, 0);
+ }
+}
+
+class Cloth {
+ constructor(width, height, numPointsWidth, numPointsHeight) {
+ this.width = width;
+ this.height = height;
+ this.numPointsWidth = numPointsWidth;
+ this.numPointsHeight = numPointsHeight;
+ this.windFactor = new THREE.Vector3(3, 2, 2);
+
+ /**
+ * 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 Particle to "masses"
+ */
+ this.masses = [];
+ for (let y = 0; y < numPointsHeight; y++) {
+ for (let x = 0; x < numPointsWidth; x++) {
+ this.masses.push(
+ new Mass(
+ (x - ((numPointsWidth-1)/2)) * stepWidth,
+ height - (y + ((numPointsHeight-1)/2)) * stepHeight,
+ 0,
+ MASS)
+ );
+ }
+ }
+
+ /** attach cloth to flag pole */
+ const n = 3;
+ for (let i = 0; i < numPointsHeight; i++)
+ this.masses[this.getVertexIndex(0, i)].movable = false;
+
+ const REST_DIST_X = width / (numPointsWidth-1);
+ const REST_DIST_Y = height / (numPointsHeight-1);
+
+ /**
+ * generate springs (constraints)
+ */
+ this.springs = [];
+ for (let y = 0; y < numPointsHeight; y++) {
+ for (let x = 0; x < numPointsWidth; x++) {
+ if (x < numPointsWidth-1) {
+ this.springs.push(new Spring(
+ this.masses[this.getVertexIndex(x, y)],
+ this.masses[this.getVertexIndex(x+1, y)],
+ REST_DIST_X
+ ));
+ }
+ if (y < numPointsHeight-1) {
+ this.springs.push(new Spring(
+ this.masses[this.getVertexIndex(x, y)],
+ this.masses[this.getVertexIndex(x, y+1)],
+ REST_DIST_Y
+ ));
+ }
+ }
+ }
+ }
+ generateGeometry() {
+ const geometry = new THREE.BufferGeometry();
+
+ const vertices = [];
+ const indices = [];
+ const uvs = [];
+
+ /** create one vertex and one uv coordinate per mass */
+ for (let i in this.masses) {
+ let particle = this.masses[i];
+ vertices.push(
+ particle.position.x,
+ particle.position.y,
+ particle.position.z);
+ uvs.push(
+ this.getX(i) / (this.numPointsWidth-1),
+ 1 - (this.getY(i) / (this.numPointsHeight-1))
+ );
+ }
+
+ /**
+ * generate faces based on 4 vertices
+ * and 6 springs each
+ */
+ for (let y = 0; y < this.numPointsHeight - 1; y++) {
+ for (let x = 0; x < this.numPointsWidth - 1; x++) {
+ indices.push(
+ this.getVertexIndex(x, y),
+ this.getVertexIndex(x+1, y),
+ this.getVertexIndex(x+1, y+1)
+ );
+ indices.push(
+ this.getVertexIndex(x, y),
+ this.getVertexIndex(x+1, y+1),
+ this.getVertexIndex(x, y+1)
+ );
+ }
+ }
+
+ /** set up geometry */
+ geometry.setIndex(indices);
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
+ geometry.computeBoundingSphere();
+ geometry.computeVertexNormals();
+
+ return geometry;
+ }
+ updateGeometry(geometry) {
+ /** update vertex positions in place */
+ const positions = geometry.attributes.position.array;
+ for (let i in this.masses) {
+ let p = this.masses[i];
+ positions[i*3+0] = p.position.x;
+ positions[i*3+1] = p.position.y;
+ positions[i*3+2] = p.position.z;
+ }
+ /** update internally and recalculate bounding volume */
+ geometry.attributes.position.needsUpdate = true;
+ geometry.computeBoundingSphere();
+ geometry.computeVertexNormals();
+ }
+ simulate(dt) {
+ let now = performance.now();
+ for (let mass of this.masses) {
+ /** accumulate acceleration:
+ * - wind
+ * - gravity
+ */
+ let vertex = mass.position;
+ let fWind = new THREE.Vector3(
+ this.windFactor.x * (Math.sin(vertex.x * vertex.y * now)+1),
+ this.windFactor.y * Math.cos(vertex.z * now),
+ this.windFactor.z * Math.sin(Math.cos(5 * vertex.x * vertex.y * vertex.z))
+ );
+ // normalize then multiply?
+ if (options.wind)
+ mass.addForce(fWind);
+ // calculate wind with normal?
+
+ mass.addForce(GRAVITY);
+
+ /** integrate motion */
+ mass.verlet(dt);
+ }
+
+ /** run satisfy step */
+ for (let constraint of this.springs) {
+ constraint.satisfy();
+ }
+
+ /** prevent self-intersections */
+ this.intersect();
+ }
+
+ intersect() {
+ for (let i in this.masses) {
+ for (let j in this.masses) {
+ let p1 = this.masses[i];
+ let p2 = this.masses[j];
+
+ p1.movableTmp = true;
+ p2.movableTmp = true;
+
+ /** skip if i == j or if masses are adjacent */
+ if (i == j || (Math.abs(this.getX(i) - this.getX(j)) == 1 && Math.abs(this.getY(i) - this.getY(j)) == 1))
+ continue;
+
+ /** calculate distance of points */
+ let dist = p1.position.distanceTo(p2.position);
+ /** calculate minimal resting distance (largest distance that should not be fallen below) */
+ let collisionDistance = Math.min(this.width / this.numPointsWidth, this.height / this.numPointsHeight);
+ // collisionDistance /= 2;
+ /** calculate "sphere intersection" */
+ if (dist < collisionDistance) {
+ // p1.movableTmp = false;
+ // p2.movableTmp = false;
+
+ /** vectors from p1 to p2 and the other way round */
+ let diffP2P1 = p1.position.clone().sub(p2.position).normalize();
+ diffP2P1.multiplyScalar((collisionDistance - dist) * 1.001 / 2);
+ let diffP1P2 = diffP2P1.clone().multiplyScalar(-1);
+
+ // let v1 = p1.position.clone().sub(p1.previous).normalize();
+ // let v2 = p2.position.clone().sub(p2.previous).normalize();
+
+ // let factor1 = (Math.PI - Math.acos(v1.dot(diffP2P1))) / Math.PI * 2;
+ // let factor2 = (Math.PI - Math.acos(v2.dot(diffP1P2))) / Math.PI * 2;
+
+ /** move masses apart */
+ if (p1.movable)
+ p1.position.add(diffP2P1);
+ //p1.position.add(diffP2P1.multiplyScalar(factor1));
+ if (p2.movable)
+ p2.position.add(diffP1P2);
+ //p2.position.add(diffP1P2.multiplyScalar(factor2));
+ }
+ }
+ }
+ }
+ blow(camPos, intersects) {
+ let face = intersects[0].face;
+ /** vector from cam to intersection (wind) */
+ let dir = intersects[0].point.clone().sub(camPos).multiplyScalar(50);
+ /** apply to all vertices of affected face */
+ this.masses[face.a].addForce(dir);
+ this.masses[face.b].addForce(dir);
+ this.masses[face.c].addForce(dir);
+ }
+ drag(mousePosWorld, index) {
+ /** calculate vector from vertex to cursor */
+ let dir = mousePosWorld.clone().sub(this.masses[index].position).multiplyScalar(200);
+ /** apply to grabbed vertex */
+ this.masses[index].addForce(dir);
+ }
+
+ /**
+ * 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
+ */
+ getVertexIndex(x, y) {
+ return y * this.numPointsWidth + x;
+ }
+ getX(i) { return i % this.numPointsWidth; }
+ getY(i) { return Math.floor(i / this.numPointsWidth); }
+} \ No newline at end of file