]> gitweb.ps.run Git - cloth_sim/blob - Scripts/cloth.js
fix verlet integration, add wind
[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)));         // oben\r
160         newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x, y + 1)));         // links\r
161         newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x + 1, y + 1)));     // oben links  -> unten rechts diagonal\r
162         newFace.springs.push(new Spring(vertices, getVertexIndex(x + 1, y), getVertexIndex(x, y + 1)));     // oben rechts -> unten links diagonal\r
163         newFace.springs.push(new Spring(vertices, getVertexIndex(x + 1, y), getVertexIndex(x + 1, y + 1))); // rechts\r
164         newFace.springs.push(new Spring(vertices, getVertexIndex(x, y + 1), getVertexIndex(x + 1, y + 1))); // unten\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     this.geometry.computeFaceNormals();\r
251   }\r
252 \r
253   /**\r
254    * generate a debug mesh for visualizing\r
255    * vertices and springs of the cloth\r
256    * and add it to scene for rendering\r
257    * @param {Scene} scene - Scene to add Debug Mesh to\r
258    */\r
259   createDebugMesh(scene) {\r
260     /**\r
261      * helper function to generate a single line\r
262      * between two Vertices with a given color\r
263      * @param {Vector3} from \r
264      * @param {Vector3} to \r
265      * @param {number} color \r
266      */\r
267     function addLine(from, to, color) {\r
268       let geometry = new THREE.Geometry();\r
269       geometry.vertices.push(from);\r
270       geometry.vertices.push(to);\r
271       let material = new THREE.LineBasicMaterial({ color: color, linewidth: 10 });\r
272       let line = new THREE.Line(geometry, material);\r
273       line.renderOrder = 1;\r
274       scene.add(line);\r
275     }\r
276     /**\r
277      * helper function to generate a small sphere\r
278      * at a given Vertex Position with color\r
279      * @param {Vector3} point \r
280      * @param {number} color \r
281      */\r
282     function addPoint(point, color) {\r
283       const geometry = new THREE.SphereGeometry(0.05, 32, 32);\r
284       const material = new THREE.MeshBasicMaterial({ color: color });\r
285       const sphere = new THREE.Mesh(geometry, material);\r
286       sphere.position.set(point.x, point.y, point.z);\r
287       scene.add(sphere);\r
288     }\r
289 \r
290     let lineColor = 0x000000;\r
291     let pointColor = 0xff00000;\r
292 \r
293     /**\r
294      * generate one line for each of the 6 springs\r
295      * and one point for each of the 4 vertices\r
296      * for all of the faces\r
297      */\r
298     for (let i in this.faces) {\r
299       let face = this.faces[i];\r
300       addLine(this.geometry.vertices[face.a], this.geometry.vertices[face.b], lineColor);\r
301       addLine(this.geometry.vertices[face.a], this.geometry.vertices[face.c], lineColor);\r
302       addLine(this.geometry.vertices[face.a], this.geometry.vertices[face.d], lineColor);\r
303       addLine(this.geometry.vertices[face.b], this.geometry.vertices[face.c], lineColor);\r
304       addLine(this.geometry.vertices[face.b], this.geometry.vertices[face.d], lineColor);\r
305       addLine(this.geometry.vertices[face.c], this.geometry.vertices[face.d], lineColor);\r
306 \r
307       addPoint(this.geometry.vertices[face.a], pointColor);\r
308       addPoint(this.geometry.vertices[face.b], pointColor);\r
309       addPoint(this.geometry.vertices[face.c], pointColor);\r
310       addPoint(this.geometry.vertices[face.d], pointColor);\r
311     }\r
312   }\r
313 \r
314   previousPositions = [];\r
315   time = 0;\r
316   /**\r
317    * \r
318    * @param {number} dt time in seconds since last frame\r
319    */\r
320   simulate(dt) {\r
321     for (let i in this.geometry.vertices) {\r
322       let acceleration = this.getAcceleration(i, dt);\r
323 \r
324       //acceleration.clampLength(0, 10);\r
325 \r
326       if (Math.abs(acceleration.length()) <= 10e-4) {\r
327         acceleration.set(0, 0, 0);\r
328       }\r
329  \r
330       let currentPosition = this.verlet(this.geometry.vertices[i].clone(), this.previousPositions[i].clone(), acceleration, dt);\r
331       //let currentPosition = this.euler(this.geometry.vertices[i], acceleration, dt);\r
332      \r
333       this.previousPositions[i].copy(this.geometry.vertices[i]);\r
334       this.geometry.vertices[i].copy(currentPosition);\r
335     }\r
336     //console.log(this.getAcceleration(1, dt));\r
337     \r
338     this.time += dt;\r
339 \r
340     for (let face of this.faces) {\r
341       for (let spring of face.springs) {\r
342         spring.update(this.geometry.vertices);\r
343       }\r
344     }\r
345 \r
346     /**\r
347      * let THREE JS compute bounding sphere around generated mesh\r
348      * needed for View Frustum Culling internally\r
349      */\r
350 \r
351     this.geometry.verticesNeedUpdate = true;\r
352     this.geometry.elementsNeedUpdate = true;\r
353     this.geometry.computeBoundingSphere();\r
354     this.geometry.computeFaceNormals();\r
355 \r
356   }\r
357 \r
358 \r
359 \r
360 /**\r
361  * Equation of motion for each vertex which represents the acceleration \r
362  * @param {number} vertexIndex The index of the current vertex whose acceleration should be calculated\r
363  *  @param {number} dt The time passed since last frame\r
364  */\r
365 getAcceleration(vertexIndex, dt) {\r
366   if (this.vertexRigidness[vertexIndex])\r
367     return new THREE.Vector3(0, 0, 0);\r
368 \r
369   let vertex = this.geometry.vertices[vertexIndex];\r
370 \r
371   // Mass of vertex\r
372   let M = this.vertexWeights[vertexIndex];\r
373   // constant gravity\r
374   let g = new THREE.Vector3(0, -9.8, 0);\r
375   // stiffness\r
376   let k = 1000;\r
377 \r
378   // Wind vector\r
379   let fWind = new THREE.Vector3(\r
380     Math.sin(vertex.x * vertex.y * this.time),\r
381     Math.cos(vertex.z * this.time),\r
382     Math.sin(Math.cos(5 * vertex.x * vertex.y * vertex.z))\r
383   );\r
384 \r
385   /**\r
386    * constant determined by the properties of the surrounding fluids (air)\r
387    * achievement of cloth effects through try out\r
388    * */\r
389   let a = 0.1;\r
390   \r
391   let velocity = new THREE.Vector3(\r
392     (this.previousPositions[vertexIndex].x - vertex.x) / dt,\r
393     (this.previousPositions[vertexIndex].y - vertex.y) / dt,\r
394     (this.previousPositions[vertexIndex].z - vertex.z) / dt\r
395   );\r
396 \r
397   //console.log(velocity, vertex, this.previousPositions[vertexIndex]);\r
398 \r
399   let fAirResistance = velocity.multiply(velocity).multiplyScalar(-a);\r
400   \r
401   let springSum = new THREE.Vector3(0, 0, 0);\r
402 \r
403   // Get the bounding springs and add them to the needed springs\r
404   // TODO: optimize\r
405 \r
406   const numPointsX = 10;\r
407   const numPointsY = 10;\r
408   const numFacesX = numPointsX - 1;\r
409   const numFacesY = numPointsY - 1;\r
410 \r
411   function getFaceIndex(x, y) {\r
412     return y * numFacesX + x;\r
413   }\r
414 \r
415   let indexX = vertexIndex % numPointsX;\r
416   let indexY = Math.floor(vertexIndex / numPointsX);\r
417 \r
418   let springs = [];\r
419 \r
420   // 0  oben\r
421   // 1  links\r
422   // 2  oben links  -> unten rechts diagonal\r
423   // 3  oben rechts -> unten links diagonal\r
424   // 4  rechts\r
425   // 5  unten\r
426 \r
427   let ul = indexX > 0 && indexY < numPointsY - 1;\r
428   let ur = indexX < numPointsX - 1 && indexY < numPointsY - 1;\r
429   let ol = indexX > 0 && indexY > 0;\r
430   let or = indexX < numPointsX - 1 && indexY > 0;\r
431 \r
432   if (ul) {\r
433     let faceUL = this.faces[getFaceIndex(indexX - 1, indexY)];\r
434     springs.push(faceUL.springs[3]);\r
435     if (!ol)\r
436       springs.push(faceUL.springs[0]);\r
437     springs.push(faceUL.springs[4]);\r
438   }\r
439   if (ur) {\r
440     let faceUR = this.faces[getFaceIndex(indexX, indexY)];\r
441     springs.push(faceUR.springs[2]);\r
442     if (!or)\r
443       springs.push(faceUR.springs[0]);\r
444     if (!ul)\r
445       springs.push(faceUR.springs[1]);\r
446   }\r
447   if (ol) {\r
448     let faceOL = this.faces[getFaceIndex(indexX - 1, indexY - 1)];\r
449     springs.push(faceOL.springs[2]);\r
450     springs.push(faceOL.springs[4]);\r
451     springs.push(faceOL.springs[5]);\r
452   }\r
453   if (or) {\r
454     let faceOR = this.faces[getFaceIndex(indexX , indexY - 1)];\r
455     springs.push(faceOR.springs[3]);\r
456     if (!ol)\r
457       springs.push(faceOR.springs[1]);\r
458     springs.push(faceOR.springs[5]);\r
459   }\r
460 \r
461   for (let spring of springs) {\r
462     let springDirection = spring.getDirection(this.geometry.vertices);\r
463 \r
464     if (spring.index1 == vertexIndex)\r
465       springDirection.multiplyScalar(-1);\r
466 \r
467     springSum.add(springDirection.multiplyScalar(k * (spring.restLength - spring.currentLength)));\r
468   }\r
469   \r
470   let result = new THREE.Vector3(1, 1, 1);\r
471 \r
472   let temp = result.multiplyScalar(M).multiply(g).add(fWind).add(fAirResistance).clone();\r
473   result.sub(springSum);\r
474   document.getElementById("Output").innerText = "SpringSum: " + Math.floor(springSum.y) + "\nTemp: " + Math.floor(temp.y);\r
475 \r
476   return result;\r
477 }\r
478 \r
479 /**\r
480  * The Verlet algorithm as an integrator \r
481  * to get the next position of a vertex  \r
482  * @param {Vector3} currentPosition \r
483  * @param {Vector3} previousPosition \r
484  * @param {Vector3} acceleration \r
485  * @param {number} passedTime The delta time since last frame\r
486  */\r
487 verlet(currentPosition, previousPosition, acceleration, passedTime) {\r
488   // verlet algorithm\r
489   // next position = 2 * current Position - previous position + acceleration * (passed time)^2\r
490   // acceleration (dv/dt) = F(net)\r
491   // Dependency for one vertex: gravity, fluids/air, springs\r
492   const DRAG = 0.97;\r
493   let nextPosition = new THREE.Vector3(\r
494     (currentPosition.x - previousPosition.x) * DRAG + currentPosition.x + acceleration.x * (passedTime * passedTime),\r
495     (currentPosition.y - previousPosition.y) * DRAG + currentPosition.y + acceleration.y * (passedTime * passedTime),\r
496     (currentPosition.z - previousPosition.z) * DRAG + currentPosition.z + acceleration.z * (passedTime * passedTime),\r
497   );\r
498 \r
499   // let nextPosition = new THREE.Vector3(\r
500   //   (2 * currentPosition.x) - previousPosition.x + acceleration.x * (passedTime * passedTime),\r
501   //   (2 * currentPosition.y) - previousPosition.y + acceleration.y * (passedTime * passedTime),\r
502   //   (2 * currentPosition.z) - previousPosition.z + acceleration.z * (passedTime * passedTime),\r
503   // );\r
504 \r
505   return nextPosition;\r
506 }\r
507 \r
508 euler(currentPosition, acceleration, passedTime) {\r
509   let nextPosition = new THREE.Vector3(\r
510     currentPosition.x + acceleration.x * passedTime,\r
511     currentPosition.y + acceleration.y * passedTime,\r
512     currentPosition.z + acceleration.z * passedTime,\r
513   );\r
514 \r
515   return nextPosition;\r
516 }\r
517 \r
518 wind(intersects) {\r
519   let intersect = intersects[0];\r
520   this.geometry.vertices[intersect.face.a].z -= 0.05;\r
521   this.geometry.vertices[intersect.face.b].z -= 0.05;\r
522   this.geometry.vertices[intersect.face.c].z -= 0.05;\r
523 }\r
524 \r
525 }\r
526 \r