]> gitweb.ps.run Git - cloth_sim/blob - js/cloth.js
restructure
[cloth_sim] / js / cloth.js
1 const DAMPING = 0.03;\r
2 const DRAG = 1 - DAMPING;\r
3 const MASS = 0.1;\r
4 const GRAVITY = new THREE.Vector3(0, -9.81 * MASS, 0);\r
5 const K = 1;\r
6 const MAX_STRETCH = 1.5;\r
7 \r
8 const options = {\r
9   wind: true,\r
10 };\r
11 \r
12 class Spring {\r
13   constructor(p1, p2, restDist) {\r
14     this.p1 = p1;\r
15     this.p2 = p2;\r
16     this.restDist = restDist;\r
17   }\r
18 \r
19   satisfy() {\r
20     /** calculate current spring length */\r
21     const diff = this.p2.position.clone().sub(this.p1.position);\r
22     const currentDist = diff.length();\r
23     if (currentDist == 0) return;\r
24     if (currentDist <= this.restDist) return;\r
25     //const correction = diff.multiplyScalar(1 - (this.restDist / currentDist));\r
26 \r
27     /** calculate necessary correction length and direction */\r
28     const correction = diff.multiplyScalar((currentDist - this.restDist) / currentDist);\r
29     correction.multiplyScalar(K);\r
30     const correctionHalf = correction.multiplyScalar(0.5);\r
31 \r
32     let p1movable = this.p1.movable && this.p1.movableTmp;\r
33     let p2movable = this.p2.movable && this.p2.movableTmp;\r
34 \r
35     /** apply correction if masses aren't fixed */\r
36     /** divide correction if both are movable */\r
37     if (p1movable && p2movable) {\r
38       this.p1.position.add(correctionHalf);\r
39       this.p2.position.sub(correctionHalf);\r
40     } else if (! p1movable && p2movable) {\r
41       this.p2.position.sub(correction);\r
42     } else if (p1movable && ! p2movable) {\r
43       this.p1.position.add(correction);\r
44     }\r
45   }\r
46 }\r
47 \r
48 class Mass {\r
49   movableTmp = true;\r
50   movable = true;\r
51 \r
52   constructor(x, y, z, mass) {\r
53     this.position = new THREE.Vector3(x, y, z);\r
54     this.previous = new THREE.Vector3(x, y, z);\r
55     this.acceleration = new THREE.Vector3(0, 0, 0);\r
56     this.mass = mass;\r
57   }\r
58   addForce(force) {\r
59     this.acceleration.add(\r
60       force.clone().multiplyScalar(1/this.mass)\r
61     );\r
62   }\r
63   verlet(dt) {\r
64     // verlet algorithm\r
65     // next position = 2 * current Position - previous position + acceleration * (passed time)^2\r
66     // acceleration (dv/dt) = F(net)\r
67     /** calculate velocity */\r
68     const nextPosition = this.position.clone().sub(this.previous);\r
69     /** apply drag */\r
70     nextPosition.multiplyScalar(DRAG);\r
71     /** add to current position and add acceleration */\r
72     nextPosition.add(this.position);\r
73     nextPosition.add(this.acceleration.multiplyScalar(dt*dt));\r
74 \r
75     if (this.movable && this.movableTmp) {\r
76       this.previous = this.position;\r
77       this.position = nextPosition;\r
78     }\r
79 \r
80     /** reset for next frame */\r
81     this.acceleration.set(0, 0, 0);\r
82   }\r
83 }\r
84 \r
85 class Cloth {\r
86   constructor(width, height, numPointsWidth, numPointsHeight) {\r
87     this.width = width;\r
88     this.height = height;\r
89     this.numPointsWidth = numPointsWidth;\r
90     this.numPointsHeight = numPointsHeight;\r
91     this.windFactor = new THREE.Vector3(3, 2, 2);\r
92 \r
93     /**\r
94      * distance between two vertices horizontally/vertically\r
95      * divide by the number of points minus one\r
96      * because there are (n - 1) lines between n vertices\r
97      */\r
98     let stepWidth = width / (numPointsWidth - 1);\r
99     let stepHeight = height / (numPointsHeight - 1);\r
100 \r
101     /**\r
102      * iterate over the number of vertices in x/y axis\r
103      * and add a new Particle to "masses"\r
104      */\r
105     this.masses = [];\r
106     for (let y = 0; y < numPointsHeight; y++) {\r
107       for (let x = 0; x < numPointsWidth; x++) {\r
108         this.masses.push(\r
109           new Mass(\r
110             (x - ((numPointsWidth-1)/2)) * stepWidth,\r
111             height - (y + ((numPointsHeight-1)/2)) * stepHeight,\r
112             0,\r
113             MASS)\r
114         );\r
115       }\r
116     }\r
117 \r
118     /** attach cloth to flag pole */\r
119     const n = 3;\r
120     for (let i = 0; i < numPointsHeight; i++)\r
121       this.masses[this.getVertexIndex(0, i)].movable = false;\r
122 \r
123     const REST_DIST_X = width / (numPointsWidth-1);\r
124     const REST_DIST_Y = height / (numPointsHeight-1);\r
125 \r
126     /**\r
127      * generate springs (constraints)\r
128      */\r
129     this.springs = [];\r
130     for (let y = 0; y < numPointsHeight; y++) {\r
131       for (let x = 0; x < numPointsWidth; x++) {\r
132         if (x < numPointsWidth-1) {\r
133           this.springs.push(new Spring(\r
134             this.masses[this.getVertexIndex(x, y)],\r
135             this.masses[this.getVertexIndex(x+1, y)],\r
136             REST_DIST_X\r
137           ));\r
138         }\r
139         if (y < numPointsHeight-1) {\r
140           this.springs.push(new Spring(\r
141             this.masses[this.getVertexIndex(x, y)],\r
142             this.masses[this.getVertexIndex(x, y+1)],\r
143             REST_DIST_Y\r
144           ));\r
145         }\r
146       }\r
147     }\r
148   }\r
149   generateGeometry() {\r
150     const geometry = new THREE.BufferGeometry();\r
151 \r
152     const vertices = [];\r
153     const indices = [];\r
154     const uvs = [];\r
155 \r
156     /** create one vertex and one uv coordinate per mass */\r
157     for (let i in this.masses) {\r
158       let particle = this.masses[i];\r
159       vertices.push(\r
160         particle.position.x,\r
161         particle.position.y,\r
162         particle.position.z);\r
163       uvs.push(\r
164         this.getX(i) / (this.numPointsWidth-1),\r
165         1 - (this.getY(i) / (this.numPointsHeight-1))\r
166       );\r
167     }\r
168 \r
169     /**\r
170      * generate faces based on 4 vertices\r
171      * and 6 springs each\r
172      */\r
173     for (let y = 0; y < this.numPointsHeight - 1; y++) {\r
174       for (let x = 0; x < this.numPointsWidth - 1; x++) {\r
175         indices.push(\r
176           this.getVertexIndex(x, y),\r
177           this.getVertexIndex(x+1, y),\r
178           this.getVertexIndex(x+1, y+1)\r
179         );\r
180         indices.push(\r
181           this.getVertexIndex(x, y),\r
182           this.getVertexIndex(x+1, y+1),\r
183           this.getVertexIndex(x, y+1)\r
184         );\r
185       }\r
186     }\r
187 \r
188     /** set up geometry */\r
189     geometry.setIndex(indices);\r
190     geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));\r
191     geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));\r
192     geometry.computeBoundingSphere();\r
193     geometry.computeVertexNormals();\r
194 \r
195     return geometry;\r
196   }\r
197   updateGeometry(geometry) {\r
198     /** update vertex positions in place */\r
199     const positions = geometry.attributes.position.array;\r
200     for (let i in this.masses) {\r
201       let p = this.masses[i];\r
202       positions[i*3+0] = p.position.x;\r
203       positions[i*3+1] = p.position.y;\r
204       positions[i*3+2] = p.position.z;\r
205     }\r
206     /** update internally and recalculate bounding volume */\r
207     geometry.attributes.position.needsUpdate = true;\r
208     geometry.computeBoundingSphere();\r
209     geometry.computeVertexNormals();\r
210   }\r
211   simulate(dt) {\r
212     let now = performance.now();\r
213     for (let mass of this.masses) {\r
214       /** accumulate acceleration:\r
215        *  - wind\r
216        *  - gravity\r
217        */\r
218       let vertex = mass.position;\r
219       let fWind = new THREE.Vector3(\r
220         this.windFactor.x * (Math.sin(vertex.x * vertex.y * now)+1),\r
221         this.windFactor.y * Math.cos(vertex.z * now),\r
222         this.windFactor.z * Math.sin(Math.cos(5 * vertex.x * vertex.y * vertex.z))\r
223       );\r
224       // normalize then multiply?\r
225       if (options.wind)\r
226         mass.addForce(fWind);\r
227       // calculate wind with normal?\r
228 \r
229       mass.addForce(GRAVITY);\r
230 \r
231       /** integrate motion */\r
232       mass.verlet(dt);\r
233     }\r
234 \r
235     /** run satisfy step */\r
236     for (let constraint of this.springs) {\r
237       constraint.satisfy();\r
238     }\r
239 \r
240     /** prevent self-intersections */\r
241     this.intersect();\r
242   }\r
243 \r
244   intersect() {\r
245     for (let i in this.masses) {\r
246       for (let j in this.masses) {  \r
247         let p1 = this.masses[i];\r
248         let p2 = this.masses[j];\r
249 \r
250         p1.movableTmp = true;\r
251         p2.movableTmp = true;\r
252 \r
253         /** skip if i == j or if masses are adjacent */\r
254         if (i == j || (Math.abs(this.getX(i) - this.getX(j)) == 1 && Math.abs(this.getY(i) - this.getY(j)) == 1))\r
255           continue;\r
256 \r
257         /** calculate distance of points  */\r
258         let dist = p1.position.distanceTo(p2.position);\r
259         /** calculate minimal resting distance (largest distance that should not be fallen below) */\r
260         let collisionDistance = Math.min(this.width / this.numPointsWidth, this.height / this.numPointsHeight);\r
261         // collisionDistance /= 2;\r
262         /** calculate "sphere intersection" */\r
263         if (dist < collisionDistance) {\r
264           // p1.movableTmp = false;\r
265           // p2.movableTmp = false;\r
266 \r
267           /** vectors from p1 to p2 and the other way round */\r
268           let diffP2P1 = p1.position.clone().sub(p2.position).normalize();\r
269           diffP2P1.multiplyScalar((collisionDistance - dist) * 1.001 / 2);\r
270           let diffP1P2 = diffP2P1.clone().multiplyScalar(-1);\r
271 \r
272           // let v1 = p1.position.clone().sub(p1.previous).normalize();\r
273           // let v2 = p2.position.clone().sub(p2.previous).normalize();\r
274 \r
275           // let factor1 = (Math.PI - Math.acos(v1.dot(diffP2P1))) / Math.PI * 2;\r
276           // let factor2 = (Math.PI - Math.acos(v2.dot(diffP1P2))) / Math.PI * 2;\r
277 \r
278           /** move masses apart */\r
279           if (p1.movable)\r
280             p1.position.add(diffP2P1);\r
281             //p1.position.add(diffP2P1.multiplyScalar(factor1));\r
282           if (p2.movable)\r
283             p2.position.add(diffP1P2);\r
284             //p2.position.add(diffP1P2.multiplyScalar(factor2));\r
285         }\r
286       }\r
287     }\r
288   }\r
289   blow(camPos, intersects) {\r
290     let face = intersects[0].face;\r
291     /** vector from cam to intersection (wind) */\r
292     let dir = intersects[0].point.clone().sub(camPos).multiplyScalar(50);\r
293     /** apply to all vertices of affected face */\r
294     this.masses[face.a].addForce(dir);\r
295     this.masses[face.b].addForce(dir);\r
296     this.masses[face.c].addForce(dir);\r
297   }\r
298   drag(mousePosWorld, index) {\r
299     /** calculate vector from vertex to cursor */\r
300     let dir = mousePosWorld.clone().sub(this.masses[index].position).multiplyScalar(200);\r
301     /** apply to grabbed vertex */\r
302     this.masses[index].addForce(dir);\r
303   }\r
304 \r
305   /**\r
306    * helper function to calculate index of vertex\r
307    * in "vertices" array based on its x and y positions\r
308    * in the mesh\r
309    * @param {number} x - x index of vertex\r
310    * @param {number} y - y index of vertex\r
311    */\r
312   getVertexIndex(x, y) {\r
313     return y * this.numPointsWidth + x;\r
314   }\r
315   getX(i) { return i % this.numPointsWidth; }\r
316   getY(i) { return Math.floor(i / this.numPointsWidth); }\r
317 }