]> gitweb.ps.run Git - cloth_sim/blob - Scripts/cloth.js
correct units, tweak parameters
[cloth_sim] / Scripts / cloth.js
1 /**\r
2  *  Convenience Function for calculating the distance between two vectors\r
3  *  because THREE JS Vector functions mutate variables\r
4  * @param {Vector3} a - Vector A\r
5  * @param {Vector3} b - Vector B\r
6  */\r
7 function vectorLength(a, b) {\r
8   let v1 = new THREE.Vector3();\r
9   v1.copy(a);\r
10   let v2 = new THREE.Vector3();\r
11   v2.copy(b);\r
12 \r
13   return v1.sub(v2).length();\r
14 }\r
15 \r
16 /**\r
17  * Class representing a quad face\r
18  * Each face consists of two triangular mesh faces\r
19  * containts four indices for determining vertices\r
20  * and six springs, one between each of the vertices\r
21  */\r
22 export class Face {\r
23   a;\r
24   b;\r
25   c;\r
26   d;\r
27 \r
28   springs = [];\r
29 \r
30   constructor(a, b, c, d) {\r
31     this.a = a;\r
32     this.b = b;\r
33     this.c = c;\r
34     this.d = d;\r
35   }\r
36 }\r
37 \r
38 /**\r
39  * Class representing a single spring\r
40  * has a current and resting length\r
41  * and indices to the two connected vertices\r
42  */\r
43 export class Spring {\r
44   restLength;\r
45   currentLength;\r
46   index1;\r
47   index2;\r
48 \r
49 \r
50   /**\r
51    * set vertex indices\r
52    * and calculate inital length based on the\r
53    * vertex positions\r
54    * @param {Array<Vector3>} vertices \r
55    * @param {number} index1 \r
56    * @param {number} index2 \r
57    */\r
58   constructor(vertices, index1, index2) {\r
59     this.index1 = index1;\r
60     this.index2 = index2;\r
61 \r
62     let length = vectorLength(vertices[index1], vertices[index2]);\r
63     this.restLength = length;\r
64     this.currentLength = length;\r
65   }\r
66 \r
67   getDirection(vertices) {\r
68     let direction = new THREE.Vector3();\r
69     direction.copy(vertices[this.index1]);\r
70 \r
71     direction.sub(vertices[this.index2]);\r
72     direction.divideScalar(vectorLength(vertices[this.index1], vertices[this.index2]));\r
73 \r
74     return direction;\r
75   }\r
76 \r
77   update(vertices) {\r
78     let length = vectorLength(vertices[this.index1], vertices[this.index2]);\r
79     this.currentLength = length;\r
80   }\r
81 }\r
82 \r
83 /**\r
84  * Class representing a single piece of cloth\r
85  * contains THREE JS geometry,\r
86  * logically represented by an array of adjacent faces\r
87  * and vertex weights which are accessed by the same\r
88  * indices as the vertices in the Mesh\r
89  */\r
90 export class Cloth {\r
91   VertexWeight = 1;\r
92 \r
93   geometry = new THREE.Geometry();\r
94 \r
95   faces = [];\r
96 \r
97   vertexWeights = [];\r
98 \r
99   vertexRigidness = [];\r
100 \r
101   /**\r
102    * creates a rectangular piece of cloth\r
103    * takes the size of the cloth\r
104    * and the number of vertices it should be composed of\r
105    * @param {number} width - width of the cloth\r
106    * @param {number} height - height of the cloth\r
107    * @param {number} numPointsWidth - number of vertices in horizontal direction\r
108    * @param {number} numPointsHeight  - number of vertices in vertical direction\r
109    */\r
110   createBasic(width, height, numPointsWidth, numPointsHeight) {\r
111     /** resulting vertices and faces */\r
112     let vertices = [];\r
113     let faces = [];\r
114 \r
115     /**\r
116      * distance between two vertices horizontally/vertically\r
117      * divide by the number of points minus one\r
118      * because there are (n - 1) lines between n vertices\r
119      */\r
120     let stepWidth = width / (numPointsWidth - 1);\r
121     let stepHeight = height / (numPointsHeight - 1);\r
122 \r
123     /**\r
124      * iterate over the number of vertices in x/y axis\r
125      * and add a new Vector3 to "vertices"\r
126      */\r
127     for (let y = 0; y < numPointsHeight; y++) {\r
128       for (let x = 0; x < numPointsWidth; x++) {\r
129         vertices.push(\r
130           new THREE.Vector3(x * stepWidth, height - y * stepHeight, 0)\r
131         );\r
132       }\r
133     }\r
134 \r
135     /**\r
136      * helper function to calculate index of vertex\r
137      * in "vertices" array based on its x and y positions\r
138      * in the mesh\r
139      * @param {number} x - x index of vertex\r
140      * @param {number} y - y index of vertex\r
141      */\r
142     function getVertexIndex(x, y) {\r
143       return y * numPointsWidth + x;\r
144     }\r
145 \r
146     /**\r
147      * generate faces based on 4 vertices\r
148      * and 6 springs each\r
149      */\r
150     for (let y = 0; y < numPointsHeight - 1; y++) {\r
151       for (let x = 0; x < numPointsWidth - 1; x++) {\r
152         let newFace = new Face(\r
153           getVertexIndex(x, y),\r
154           getVertexIndex(x, y + 1),\r
155           getVertexIndex(x + 1, y),\r
156           getVertexIndex(x + 1, y + 1),\r
157         );\r
158 \r
159         newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x + 1, y)));\r
160         newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x, y + 1)));\r
161         newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x + 1, y + 1)));\r
162         newFace.springs.push(new Spring(vertices, getVertexIndex(x + 1, y), getVertexIndex(x, y + 1)));\r
163         newFace.springs.push(new Spring(vertices, getVertexIndex(x + 1, y), getVertexIndex(x + 1, y + 1)));\r
164         newFace.springs.push(new Spring(vertices, getVertexIndex(x, y + 1), getVertexIndex(x + 1, y + 1)));\r
165 \r
166         faces.push(newFace);\r
167       }\r
168     }\r
169 \r
170     /**\r
171      * call createExplicit\r
172      * with generated vertices and faces\r
173      */\r
174     this.createExplicit(vertices, faces);\r
175 \r
176     /**\r
177      * hand cloth from left and right upper corners\r
178      */\r
179     this.vertexRigidness[0] = true;\r
180     this.vertexRigidness[numPointsWidth-1] = true;\r
181   }\r
182 \r
183   /**\r
184    * Generate THREE JS Geometry\r
185    * (list of vertices and list of indices representing triangles)\r
186    * and calculate the weight of each face and split it between\r
187    * surrounding vertices\r
188    * @param {Array<Vector3>} vertices \r
189    * @param {Array<Face>} faces \r
190    */\r
191   createExplicit(vertices, faces) {\r
192 \r
193     /**\r
194      * Copy vertices and initialize vertex weights to 0\r
195      */\r
196     for (let i in vertices) {\r
197       this.geometry.vertices.push(vertices[i].clone());\r
198       this.previousPositions.push(vertices[i].clone());\r
199       // this.geometry.vertices.push(vertices[i]);\r
200       // this.previousPositions.push(vertices[i]);\r
201       this.vertexWeights.push(0);\r
202       this.vertexRigidness.push(false);\r
203     }\r
204     /**\r
205      * copy faces,\r
206      * generate two triangles per face,\r
207      * calculate weight of face as its area\r
208      * and split between the 4 vertices\r
209      */\r
210     for (let i in faces) {\r
211       let face = faces[i];\r
212 \r
213       /** copy faces to class member */\r
214       this.faces.push(face);\r
215 \r
216       /** generate triangles */\r
217       this.geometry.faces.push(new THREE.Face3(\r
218         face.a, face.b, face.c\r
219       ));\r
220       this.geometry.faces.push(new THREE.Face3(\r
221         face.c, face.b, face.d\r
222       ));\r
223 \r
224       /**\r
225        * calculate area of face as combined area of\r
226        * its two composing triangles\r
227        */\r
228       let xLength = vectorLength(this.geometry.vertices[face.b], this.geometry.vertices[face.a]);\r
229       let yLength = vectorLength(this.geometry.vertices[face.c], this.geometry.vertices[face.a]);\r
230       let weight = xLength * yLength / 2;\r
231 \r
232       xLength = vectorLength(this.geometry.vertices[face.b], this.geometry.vertices[face.d]);\r
233       yLength = vectorLength(this.geometry.vertices[face.c], this.geometry.vertices[face.d]);\r
234       weight += xLength * yLength / 2;\r
235 \r
236       /**\r
237        * split weight equally between four surrounding vertices\r
238        */\r
239       this.vertexWeights[face.a] += weight / 4;\r
240       this.vertexWeights[face.b] += weight / 4;\r
241       this.vertexWeights[face.c] += weight / 4;\r
242       this.vertexWeights[face.d] += weight / 4;\r
243     }\r
244 \r
245     /**\r
246      * let THREE JS compute bounding sphere around generated mesh\r
247      * needed for View Frustum Culling internally\r
248      */\r
249     this.geometry.computeBoundingSphere();\r
250   }\r
251 \r
252   /**\r
253    * generate a debug mesh for visualizing\r
254    * vertices and springs of the cloth\r
255    * and add it to scene for rendering\r
256    * @param {Scene} scene - Scene to add Debug Mesh to\r
257    */\r
258   createDebugMesh(scene) {\r
259     /**\r
260      * helper function to generate a single line\r
261      * between two Vertices with a given color\r
262      * @param {Vector3} from \r
263      * @param {Vector3} to \r
264      * @param {number} color \r
265      */\r
266     function addLine(from, to, color) {\r
267       let geometry = new THREE.Geometry();\r
268       geometry.vertices.push(from);\r
269       geometry.vertices.push(to);\r
270       let material = new THREE.LineBasicMaterial({ color: color, linewidth: 10 });\r
271       let line = new THREE.Line(geometry, material);\r
272       line.renderOrder = 1;\r
273       scene.add(line);\r
274     }\r
275     /**\r
276      * helper function to generate a small sphere\r
277      * at a given Vertex Position with color\r
278      * @param {Vector3} point \r
279      * @param {number} color \r
280      */\r
281     function addPoint(point, color) {\r
282       const geometry = new THREE.SphereGeometry(0.05, 32, 32);\r
283       const material = new THREE.MeshBasicMaterial({ color: color });\r
284       const sphere = new THREE.Mesh(geometry, material);\r
285       sphere.position.set(point.x, point.y, point.z);\r
286       scene.add(sphere);\r
287     }\r
288 \r
289     let lineColor = 0x000000;\r
290     let pointColor = 0xff00000;\r
291 \r
292     /**\r
293      * generate one line for each of the 6 springs\r
294      * and one point for each of the 4 vertices\r
295      * for all of the faces\r
296      */\r
297     for (let i in this.faces) {\r
298       let face = this.faces[i];\r
299       addLine(this.geometry.vertices[face.a], this.geometry.vertices[face.b], lineColor);\r
300       addLine(this.geometry.vertices[face.a], this.geometry.vertices[face.c], lineColor);\r
301       addLine(this.geometry.vertices[face.a], this.geometry.vertices[face.d], lineColor);\r
302       addLine(this.geometry.vertices[face.b], this.geometry.vertices[face.c], lineColor);\r
303       addLine(this.geometry.vertices[face.b], this.geometry.vertices[face.d], lineColor);\r
304       addLine(this.geometry.vertices[face.c], this.geometry.vertices[face.d], lineColor);\r
305 \r
306       addPoint(this.geometry.vertices[face.a], pointColor);\r
307       addPoint(this.geometry.vertices[face.b], pointColor);\r
308       addPoint(this.geometry.vertices[face.c], pointColor);\r
309       addPoint(this.geometry.vertices[face.d], pointColor);\r
310     }\r
311   }\r
312 \r
313   previousPositions = [];\r
314   time = 0;\r
315   /**\r
316    * \r
317    * @param {number} dt time in seconds since last frame\r
318    */\r
319   simulate(dt) {\r
320     for (let i in this.geometry.vertices) {\r
321       let acceleration = this.getAcceleration(i, dt);\r
322 \r
323       //acceleration.clampLength(0, 10);\r
324 \r
325       if (Math.abs(acceleration.length()) <= 10e-4) {\r
326         acceleration.set(0, 0, 0);\r
327       }\r
328  \r
329       let currentPosition = this.verlet(this.geometry.vertices[i].clone(), this.previousPositions[i].clone(), acceleration, dt);\r
330       //let currentPosition = this.euler(this.geometry.vertices[i], acceleration, dt);\r
331      \r
332       this.previousPositions[i].copy(this.geometry.vertices[i]);\r
333       this.geometry.vertices[i].copy(currentPosition);\r
334     }\r
335     //console.log(this.getAcceleration(1, dt));\r
336     \r
337     this.time += dt;\r
338 \r
339     for (let face of this.faces) {\r
340       for (let spring of face.springs) {\r
341         spring.update(this.geometry.vertices);\r
342       }\r
343     }\r
344 \r
345     /**\r
346      * let THREE JS compute bounding sphere around generated mesh\r
347      * needed for View Frustum Culling internally\r
348      */\r
349 \r
350     this.geometry.verticesNeedUpdate = true;\r
351     this.geometry.elementsNeedUpdate = true;\r
352     this.geometry.computeBoundingSphere();\r
353 \r
354   }\r
355 \r
356 \r
357 \r
358 /**\r
359  * Equation of motion for each vertex which represents the acceleration \r
360  * @param {number} vertexIndex The index of the current vertex whose acceleration should be calculated\r
361  *  @param {number} dt The time passed since last frame\r
362  */\r
363 getAcceleration(vertexIndex, dt) {\r
364   if (this.vertexRigidness[vertexIndex])\r
365     return new THREE.Vector3(0, 0, 0);\r
366 \r
367   let vertex = this.geometry.vertices[vertexIndex];\r
368 \r
369   // Mass of vertex\r
370   let M = this.vertexWeights[vertexIndex];\r
371   // constant gravity\r
372   let g = new THREE.Vector3(0, -9.8, 0);\r
373   // stiffness\r
374   let k = 1000;\r
375 \r
376   // Wind vector\r
377   let fWind = new THREE.Vector3(\r
378     Math.sin(vertex.x * vertex.y * this.time),\r
379     Math.cos(vertex.z * this.time),\r
380     Math.sin(Math.cos(5 * vertex.x * vertex.y * vertex.z))\r
381   );\r
382   fWind.set(0, 0, 0);\r
383 \r
384   /**\r
385    * constant determined by the properties of the surrounding fluids (air)\r
386    * achievement of cloth effects through try out\r
387    * */\r
388   let a = 0.01;\r
389 \r
390   let velocity = new THREE.Vector3(\r
391     (this.previousPositions[vertexIndex].x - vertex.x) / dt,\r
392     (this.previousPositions[vertexIndex].y - vertex.y) / dt,\r
393     (this.previousPositions[vertexIndex].z - vertex.z) / dt\r
394   );\r
395 \r
396   //console.log(velocity, vertex, this.previousPositions[vertexIndex]);\r
397 \r
398   let fAirResistance = velocity.cross(velocity).multiplyScalar(-a);\r
399 \r
400   let springSum = new THREE.Vector3(0, 0, 0);\r
401 \r
402   // Get the bounding springs and add them to the needed springs\r
403   // TODO: optimize\r
404   for (let i in this.faces) {\r
405     if (this.faces[i].a == vertexIndex || this.faces[i].b == vertexIndex || this.faces[i].c == vertexIndex || this.faces[i].d == vertexIndex) {\r
406       for (let j in this.faces[i].springs) {\r
407         if (this.faces[i].springs[j].index1 == vertexIndex || this.faces[i].springs[j].index2 == vertexIndex) {\r
408 \r
409           let spring = this.faces[i].springs[j];\r
410           let springDirection = spring.getDirection(this.geometry.vertices);\r
411 \r
412 \r
413           if (this.faces[i].springs[j].index1 == vertexIndex)\r
414             springDirection.multiplyScalar(-1);\r
415 \r
416           springSum.add(springDirection.multiplyScalar(k * (spring.restLength - spring.currentLength)));\r
417         }\r
418       }\r
419     }\r
420   }\r
421   \r
422   let result = new THREE.Vector3(1, 1, 1);\r
423 \r
424   result.multiplyScalar(M).multiply(g).add(fWind).add(fAirResistance).sub(springSum);\r
425   \r
426   return result;\r
427 }\r
428 \r
429 /**\r
430  * The Verlet algorithm as an integrator \r
431  * to get the next position of a vertex  \r
432  * @param {Vector3} currentPosition \r
433  * @param {Vector3} previousPosition \r
434  * @param {Vector3} acceleration \r
435  * @param {number} passedTime The delta time since last frame\r
436  */\r
437 verlet(currentPosition, previousPosition, acceleration, passedTime) {\r
438   // verlet algorithm\r
439   // next position = 2 * current Position - previous position + acceleration * (passed time)^2\r
440   // acceleration (dv/dt) = F(net)\r
441   // Dependency for one vertex: gravity, fluids/air, springs\r
442 \r
443   let nextPosition = new THREE.Vector3(\r
444     (2 * currentPosition.x) - previousPosition.x + acceleration.x * (passedTime * passedTime),\r
445     (2 * currentPosition.y) - previousPosition.y + acceleration.y * (passedTime * passedTime),\r
446     (2 * currentPosition.z) - previousPosition.z + acceleration.z * (passedTime * passedTime),\r
447   );\r
448 \r
449   return nextPosition;\r
450 }\r
451 \r
452 euler(currentPosition, acceleration, passedTime) {\r
453   let nextPosition = new THREE.Vector3(\r
454     currentPosition.x + acceleration.x * passedTime,\r
455     currentPosition.y + acceleration.y * passedTime,\r
456     currentPosition.z + acceleration.z * passedTime,\r
457   );\r
458 \r
459   return nextPosition;\r
460 }\r
461 \r
462 }\r
463 \r