]> gitweb.ps.run Git - cloth_sim/blob - Scripts/main.js
comment code
[cloth_sim] / Scripts / main.js
1 class Point {\r
2   constructor(x, y) {\r
3     this.x = x;\r
4     this.y = y;\r
5   }\r
6 \r
7   add(that) {\r
8     return new Point(\r
9       this.x + that.x,\r
10       this.y + that.y\r
11     );\r
12   }\r
13 \r
14   sub(that) {\r
15     return new Point(\r
16       this.x - that.x,\r
17       this.y - that.y\r
18     );\r
19   }\r
20 \r
21   dist(that) {\r
22     let a = this.x - that.x;\r
23     let b = this.y - that.y;\r
24     return Math.sqrt(a * a + b * b)\r
25   }\r
26 }\r
27 \r
28 /**\r
29  *  Convenience Function for calculating the distance between two vectors\r
30  *  because THREE JS Vector functions mutate variables\r
31  * @param {Vector3} a - Vector A\r
32  * @param {Vector3} b - Vector B\r
33  */\r
34 function vectorLength(a, b) {\r
35   let v1 = new THREE.Vector3();\r
36   v1.set(a.x, a.y, a.z);\r
37   let v2 = new THREE.Vector3();\r
38   v2.set(b.x, b.y, b.z);\r
39 \r
40   return v1.sub(v2).length();\r
41 }\r
42 \r
43 /**\r
44  * Class representing a quad face\r
45  * Each face consists of two triangular mesh faces\r
46  * containts four indices for determining vertices\r
47  * and six springs, one between each of the vertices\r
48  */\r
49 class Face {\r
50   a;\r
51   b;\r
52   c;\r
53   d;\r
54 \r
55   springs = [];\r
56 \r
57   constructor(a, b, c, d) {\r
58     this.a = a;\r
59     this.b = b;\r
60     this.c = c;\r
61     this.d = d;\r
62   }\r
63 }\r
64 \r
65 /**\r
66  * Class representing a single spring\r
67  * has a current and resting length\r
68  * and indices to the two connected vertices\r
69  */\r
70 class Spring {\r
71   restLength;\r
72   currentLength;\r
73   index1;\r
74   index2;\r
75 \r
76  \r
77   /**\r
78    * set vertex indices\r
79    * and calculate inital length based on the\r
80    * vertex positions\r
81    * @param {Array of Vector3} vertices \r
82    * @param {number} index1 \r
83    * @param {number} index2 \r
84    */\r
85   constructor(vertices, index1, index2) {\r
86     this.index1 = index1;\r
87     this.index2 = index2;\r
88 \r
89     let length = vectorLength(vertices[index1], vertices[index2]);\r
90     this.restLength = length;\r
91     this.currentLength = length;\r
92   }\r
93 }\r
94 \r
95 /**\r
96  * Class representing a single piece of cloth\r
97  * contains THREE JS geometry,\r
98  * logically represented by an array of adjacent faces\r
99  * and vertex weights which are accessed by the same\r
100  * indices as the vertices in the Mesh\r
101  */\r
102 class Cloth {\r
103   VertexWeight = 1;\r
104 \r
105   geometry = new THREE.Geometry();\r
106 \r
107   faces = [];\r
108 \r
109   vertexWeights = [];\r
110 \r
111   \r
112   /**\r
113    * creates a rectangular piece of cloth\r
114    * takes the size of the cloth\r
115    * and the number of vertices it should be composed of\r
116    * @param {number} width - width of the cloth\r
117    * @param {number} height - height of the cloth\r
118    * @param {number} numPointsWidth - number of vertices in horizontal direction\r
119    * @param {number} numPointsHeight  - number of vertices in vertical direction\r
120    */\r
121   createBasic(width, height, numPointsWidth, numPointsHeight) {\r
122     /** resulting vertices and faces */\r
123     let vertices = [];\r
124     let faces = [];\r
125 \r
126     /**\r
127      * distance between two vertices horizontally/vertically\r
128      * divide by the number of points minus one\r
129      * because there are (n - 1) lines between n vertices\r
130      */\r
131     let stepWidth = width / (numPointsWidth - 1);\r
132     let stepHeight = height / (numPointsHeight - 1);\r
133 \r
134     /**\r
135      * iterate over the number of vertices in x/y axis\r
136      * and add a new Vector3 to "vertices"\r
137      */\r
138     for (let y = 0; y < numPointsHeight; y++) {\r
139       for (let x = 0; x < numPointsWidth; x++) {\r
140         vertices.push(\r
141           new THREE.Vector3(x * stepWidth, height - y * stepHeight, 0)\r
142         );\r
143       }\r
144     }\r
145 \r
146     /**\r
147      * helper function to calculate index of vertex\r
148      * in "vertices" array based on its x and y positions\r
149      * in the mesh\r
150      * @param {number} x - x index of vertex\r
151      * @param {number} y - y index of vertex\r
152      */\r
153     function getVertexIndex(x, y) {\r
154       return y * numPointsWidth + x;\r
155     }\r
156     \r
157     /**\r
158      * generate faces based on 4 vertices\r
159      * and 6 springs each\r
160      */\r
161     for (let y = 0; y < numPointsHeight - 1; y++) {\r
162       for (let x = 0; x < numPointsWidth - 1; x++) {\r
163         let newFace = new Face(\r
164           getVertexIndex(x, y),\r
165           getVertexIndex(x, y + 1),\r
166           getVertexIndex(x + 1, y),\r
167           getVertexIndex(x + 1, y + 1),\r
168         );\r
169 \r
170         newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x + 1, y)));\r
171         newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x, y + 1)));\r
172         newFace.springs.push(new Spring(vertices, getVertexIndex(x, y), getVertexIndex(x + 1, y + 1)));\r
173         newFace.springs.push(new Spring(vertices, getVertexIndex(x + 1, y), getVertexIndex(x, y + 1)));\r
174         newFace.springs.push(new Spring(vertices, getVertexIndex(x + 1, y), getVertexIndex(x + 1, y + 1)));\r
175         newFace.springs.push(new Spring(vertices, getVertexIndex(x, y + 1), getVertexIndex(x + 1, y + 1)));\r
176         \r
177         faces.push(newFace);\r
178       }\r
179     }\r
180 \r
181     /**\r
182      * call createExplicit\r
183      * with generated vertices and faces\r
184      */\r
185     this.createExplicit(vertices, faces);\r
186   }\r
187 \r
188   /**\r
189    * Generate THREE JS Geometry\r
190    * (list of vertices and list of indices representing triangles)\r
191    * and calculate the weight of each face and split it between\r
192    * surrounding vertices\r
193    * @param {Array of Vector3} vertices \r
194    * @param {Array of Face} faces \r
195    */\r
196   createExplicit(vertices, faces) {\r
197     /**\r
198      * Copy vertices and initialize vertex weights to 0\r
199      */\r
200     for (let i in vertices) {\r
201       this.geometry.vertices.push(vertices[i]);\r
202       this.vertexWeights.push(0);\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 \r
314 /**\r
315  * setup THREE JS Scene, Camera and Renderer\r
316  */\r
317 function setup_scene() {\r
318   const scene = new THREE.Scene();\r
319   const camera = new THREE.PerspectiveCamera(75, window.innerWidth / (window.innerHeight - canvasSpace), 0.1, 1000);\r
320   const renderer = new THREE.WebGLRenderer();\r
321   /** size canvas to leave some space for UI */\r
322   renderer.setSize(window.innerWidth, window.innerHeight - canvasSpace);\r
323   /** embed canvas in HTML */\r
324   document.getElementById("threejscontainer").appendChild(renderer.domElement);\r
325 \r
326   /** add global light */\r
327   const directionalLight = new THREE.DirectionalLight(0xffffff, 1);\r
328   scene.add(directionalLight);\r
329 \r
330   /** position camera */\r
331   camera.position.y = 5;\r
332   camera.position.z = 10;\r
333 \r
334   return [scene, camera];\r
335 }\r
336 \r
337 function init() {\r
338   let mousePos = new Point();\r
339 \r
340   /**\r
341    * Space left empty under canvas\r
342    * for UI elements\r
343    */\r
344   const canvasSpace = 200;\r
345 \r
346   /** Setup scene */\r
347   let [scene, camera] = setup_scene();\r
348   \r
349   /** setup cloth and generate debug mesh */\r
350   let cloth = new Cloth();\r
351   cloth.createBasic(10, 10, 5, 5);\r
352   cloth.createDebugMesh(scene);\r
353 \r
354   const material = new THREE.MeshBasicMaterial({ color: 0x0000ff });\r
355   const mesh = new THREE.Mesh(cloth.geometry, material);\r
356   scene.add(mesh);\r
357 \r
358   /**\r
359    * function called every frame\r
360    * @param {number} dt - time passed since last frame\r
361    */\r
362   function animate(dt) {\r
363     requestAnimationFrame(animate);\r
364     renderer.render(scene, camera);\r
365   }\r
366 \r
367   /** add callback for window resize */\r
368   let canvas = document.getElementsByTagName("canvas")[0];\r
369   let resize = function () {\r
370     w = window.innerWidth;\r
371     h = window.innerHeight - 200;\r
372     canvas.width = w;\r
373     canvas.height = h;\r
374   }\r
375   window.onresize = resize;\r
376   resize();\r
377 \r
378   /**\r
379    * if canvas has been successfully initialized\r
380    * start rendering\r
381    */\r
382   if (canvas.getContext) {\r
383     animate(performance.now());\r
384   }\r
385 \r
386   /** add mouse move callback */\r
387   canvas.onmousemove = (evt) => {\r
388     mousePos.x = evt.clientX;\r
389     mousePos.y = evt.clientY;\r
390   };\r
391 }