├── .gitignore ├── CREDITS.md ├── LICENSE ├── README.md ├── index.html ├── index.js ├── package-lock.json ├── package.json ├── src ├── FEMPhysics │ ├── FEMPhysics.js │ ├── grid.js │ ├── math.js │ ├── softbodyGeometry.js │ ├── softbodyInstance.js │ ├── structuredArray.js │ └── tetVisualizer.js ├── app.js ├── assets │ ├── autumn_field_puresky_1k.hdr │ ├── rock_0005_ao_1k.jpg │ ├── rock_0005_color_1k.jpg │ ├── rock_0005_normal_opengl_1k.png │ └── rock_0005_roughness_1k.jpg ├── collisionGeometry.js ├── conf.js ├── geometry │ ├── cube.js │ ├── icosphere.js │ ├── loadModel.js │ ├── skull.js │ ├── skull2.js │ ├── skull3.js │ ├── textures │ │ ├── fabrics_0066_ao_1k.jpg │ │ ├── fabrics_0066_color_1k.jpg │ │ ├── fabrics_0066_normal_opengl_1k.png │ │ ├── fabrics_0066_roughness_1k.jpg │ │ ├── skullColor.jpg │ │ ├── skullNormal.png │ │ ├── skullRoughness.jpg │ │ ├── virus_normal.png │ │ └── virus_roughness.jpg │ ├── torus.js │ └── virus.js ├── info.js └── lights.js └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | ## Credits 2 | 3 | The softbody simulation is based on the WebGL implementation in [TetSim](https://github.com/zalo/TetSim) by [Johnathon Selstad](https://github.com/zalo). I reimplemented it in three.js TSL and added collision detection. 4 | 5 | [Skull model](https://sketchfab.com/3d-models/skull-b78e4e6b29b2430293edd9c99d88a64e) by [DJMaesen](https://sketchfab.com/bumstrum). 6 | 7 | [Virus model](https://sketchfab.com/3d-models/corona-virus-2e7ffcc5d8df41bfa6f7ee666237757c) by [Refref1990](https://sketchfab.com/refref1990). 8 | 9 | [HDRi background](https://polyhaven.com/a/autumn_field_puresky) by Jarod Guest and Sergej Majboroda on [Polyhaven.com](https://polyhaven.com). 10 | 11 | [Rope texture](https://www.texturecan.com/details/424/) by [texturecan.com](https://texturecan.com). 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Holtsetio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Realtime softbody simulation in the Browser with WebGPU 2 | 3 | This is a realtime simulation of softbodies with collisions, running in the browser using the three.js WebGPURenderer. 4 | 5 | See it running live [here](https://holtsetio.com/lab/softbodies/)! 6 | 7 | [![softbodies](https://github.com/user-attachments/assets/843f8955-d45b-4702-9e9b-77ebb99a0075)](https://holtsetio.com/lab/softbodies/) 8 | 9 | ## Credits 10 | 11 | The softbody simulation is based on the WebGL implementation in [TetSim](https://github.com/zalo/TetSim) by [Johnathon Selstad](https://github.com/zalo). I reimplemented it in three.js TSL and added collision detection. 12 | 13 | Full list of credits for the assets can be found [here](CREDITS.md) 14 | 15 | ## How to run 16 | ``` 17 | npm install 18 | npm run dev 19 | ``` 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Softbodies - ThreeJS WebGPU Experiment - holtsetio.com 9 | 62 | 63 | 64 | 65 |
66 |
67 |
68 |
69 |
70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three/webgpu" 2 | import App from "./src/app"; 3 | THREE.ColorManagement.enabled = true 4 | 5 | const updateLoadingProgressBar = async (frac, delay=0) => { 6 | return new Promise(resolve => { 7 | const progress = document.getElementById("progress") 8 | // 200px is the width of the progress bar defined in index.html 9 | progress.style.width = `${frac * 200}px` 10 | if (delay === 0) { 11 | resolve(); 12 | } else { 13 | setTimeout(resolve, delay) 14 | } 15 | }) 16 | } 17 | 18 | const createRenderer = () => { 19 | const renderer = new THREE.WebGPURenderer({ 20 | //forceWebGL: true, 21 | //antialias: true, 22 | }); 23 | renderer.setPixelRatio(window.devicePixelRatio); 24 | renderer.setSize(window.innerWidth, window.innerHeight); 25 | renderer.outputColorSpace = THREE.SRGBColorSpace; 26 | return renderer; 27 | } 28 | 29 | const error = (msg) => { 30 | const progressBar = document.getElementById("progress-bar"); 31 | progressBar.style.opacity = 0; 32 | const error = document.getElementById("error"); 33 | error.style.visibility = "visible"; 34 | error.innerText = "Error: " + msg; 35 | const veil = document.getElementById("veil"); 36 | error.style.pointerEvents = "auto"; 37 | }; 38 | 39 | 40 | const run = async ()=>{ 41 | if (!navigator.gpu) { 42 | error("Your device does not support WebGPU."); 43 | return; 44 | } 45 | 46 | const renderer = createRenderer(); 47 | 48 | if (!renderer.backend.isWebGPUBackend) { 49 | error("Couldn't initialize WebGPU. Make sure WebGPU is supported by your Browser!"); 50 | return; 51 | } 52 | 53 | const container = document.getElementById("container"); 54 | container.appendChild(renderer.domElement); 55 | 56 | const app = new App(renderer); 57 | await app.init(updateLoadingProgressBar); 58 | 59 | const resize = () => { 60 | renderer.setSize(window.innerWidth, window.innerHeight); 61 | app.resize(window.innerWidth, window.innerHeight); 62 | } 63 | window.addEventListener("resize", resize); 64 | resize(); 65 | 66 | const veil = document.getElementById("veil"); 67 | veil.style.opacity = 0; 68 | const progressBar = document.getElementById("progress-bar"); 69 | progressBar.style.opacity = 0; 70 | const clock = new THREE.Clock(); 71 | const animate = async ()=>{ 72 | const delta = clock.getDelta(); 73 | const elapsed = clock.getElapsedTime(); 74 | await app.update(delta, elapsed); 75 | requestAnimationFrame(animate); 76 | }; 77 | requestAnimationFrame(animate); 78 | }; 79 | 80 | run().catch(error => { 81 | console.error(error); 82 | }); -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "softbodies", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "softbodies", 9 | "version": "0.0.1", 10 | "dependencies": { 11 | "@tweakpane/plugin-essentials": "^0.2.1", 12 | "is-mobile": "^5.0.0", 13 | "three": "^0.176.0", 14 | "tweakpane": "^4.0.5", 15 | "tweakpane-plugin-infodump": "^4.0.2" 16 | }, 17 | "devDependencies": { 18 | "@babel/generator": "^7.26.9", 19 | "@babel/parser": "^7.26.9", 20 | "@babel/traverse": "^7.26.9", 21 | "@babel/types": "^7.26.9", 22 | "vite": "^6.3.1", 23 | "vite-plugin-tsl-operator": "^1.2.3" 24 | } 25 | }, 26 | "node_modules/@babel/code-frame": { 27 | "version": "7.26.2", 28 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", 29 | "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", 30 | "dev": true, 31 | "dependencies": { 32 | "@babel/helper-validator-identifier": "^7.25.9", 33 | "js-tokens": "^4.0.0", 34 | "picocolors": "^1.0.0" 35 | }, 36 | "engines": { 37 | "node": ">=6.9.0" 38 | } 39 | }, 40 | "node_modules/@babel/generator": { 41 | "version": "7.27.0", 42 | "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", 43 | "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", 44 | "dev": true, 45 | "dependencies": { 46 | "@babel/parser": "^7.27.0", 47 | "@babel/types": "^7.27.0", 48 | "@jridgewell/gen-mapping": "^0.3.5", 49 | "@jridgewell/trace-mapping": "^0.3.25", 50 | "jsesc": "^3.0.2" 51 | }, 52 | "engines": { 53 | "node": ">=6.9.0" 54 | } 55 | }, 56 | "node_modules/@babel/helper-string-parser": { 57 | "version": "7.25.9", 58 | "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", 59 | "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", 60 | "dev": true, 61 | "engines": { 62 | "node": ">=6.9.0" 63 | } 64 | }, 65 | "node_modules/@babel/helper-validator-identifier": { 66 | "version": "7.25.9", 67 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", 68 | "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", 69 | "dev": true, 70 | "engines": { 71 | "node": ">=6.9.0" 72 | } 73 | }, 74 | "node_modules/@babel/parser": { 75 | "version": "7.27.0", 76 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", 77 | "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", 78 | "dev": true, 79 | "dependencies": { 80 | "@babel/types": "^7.27.0" 81 | }, 82 | "bin": { 83 | "parser": "bin/babel-parser.js" 84 | }, 85 | "engines": { 86 | "node": ">=6.0.0" 87 | } 88 | }, 89 | "node_modules/@babel/template": { 90 | "version": "7.27.0", 91 | "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", 92 | "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", 93 | "dev": true, 94 | "dependencies": { 95 | "@babel/code-frame": "^7.26.2", 96 | "@babel/parser": "^7.27.0", 97 | "@babel/types": "^7.27.0" 98 | }, 99 | "engines": { 100 | "node": ">=6.9.0" 101 | } 102 | }, 103 | "node_modules/@babel/traverse": { 104 | "version": "7.27.0", 105 | "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", 106 | "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", 107 | "dev": true, 108 | "dependencies": { 109 | "@babel/code-frame": "^7.26.2", 110 | "@babel/generator": "^7.27.0", 111 | "@babel/parser": "^7.27.0", 112 | "@babel/template": "^7.27.0", 113 | "@babel/types": "^7.27.0", 114 | "debug": "^4.3.1", 115 | "globals": "^11.1.0" 116 | }, 117 | "engines": { 118 | "node": ">=6.9.0" 119 | } 120 | }, 121 | "node_modules/@babel/types": { 122 | "version": "7.27.0", 123 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", 124 | "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", 125 | "dev": true, 126 | "dependencies": { 127 | "@babel/helper-string-parser": "^7.25.9", 128 | "@babel/helper-validator-identifier": "^7.25.9" 129 | }, 130 | "engines": { 131 | "node": ">=6.9.0" 132 | } 133 | }, 134 | "node_modules/@esbuild/aix-ppc64": { 135 | "version": "0.25.2", 136 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", 137 | "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", 138 | "cpu": [ 139 | "ppc64" 140 | ], 141 | "dev": true, 142 | "optional": true, 143 | "os": [ 144 | "aix" 145 | ], 146 | "engines": { 147 | "node": ">=18" 148 | } 149 | }, 150 | "node_modules/@esbuild/android-arm": { 151 | "version": "0.25.2", 152 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", 153 | "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", 154 | "cpu": [ 155 | "arm" 156 | ], 157 | "dev": true, 158 | "optional": true, 159 | "os": [ 160 | "android" 161 | ], 162 | "engines": { 163 | "node": ">=18" 164 | } 165 | }, 166 | "node_modules/@esbuild/android-arm64": { 167 | "version": "0.25.2", 168 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", 169 | "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", 170 | "cpu": [ 171 | "arm64" 172 | ], 173 | "dev": true, 174 | "optional": true, 175 | "os": [ 176 | "android" 177 | ], 178 | "engines": { 179 | "node": ">=18" 180 | } 181 | }, 182 | "node_modules/@esbuild/android-x64": { 183 | "version": "0.25.2", 184 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", 185 | "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", 186 | "cpu": [ 187 | "x64" 188 | ], 189 | "dev": true, 190 | "optional": true, 191 | "os": [ 192 | "android" 193 | ], 194 | "engines": { 195 | "node": ">=18" 196 | } 197 | }, 198 | "node_modules/@esbuild/darwin-arm64": { 199 | "version": "0.25.2", 200 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", 201 | "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", 202 | "cpu": [ 203 | "arm64" 204 | ], 205 | "dev": true, 206 | "optional": true, 207 | "os": [ 208 | "darwin" 209 | ], 210 | "engines": { 211 | "node": ">=18" 212 | } 213 | }, 214 | "node_modules/@esbuild/darwin-x64": { 215 | "version": "0.25.2", 216 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", 217 | "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", 218 | "cpu": [ 219 | "x64" 220 | ], 221 | "dev": true, 222 | "optional": true, 223 | "os": [ 224 | "darwin" 225 | ], 226 | "engines": { 227 | "node": ">=18" 228 | } 229 | }, 230 | "node_modules/@esbuild/freebsd-arm64": { 231 | "version": "0.25.2", 232 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", 233 | "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", 234 | "cpu": [ 235 | "arm64" 236 | ], 237 | "dev": true, 238 | "optional": true, 239 | "os": [ 240 | "freebsd" 241 | ], 242 | "engines": { 243 | "node": ">=18" 244 | } 245 | }, 246 | "node_modules/@esbuild/freebsd-x64": { 247 | "version": "0.25.2", 248 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", 249 | "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", 250 | "cpu": [ 251 | "x64" 252 | ], 253 | "dev": true, 254 | "optional": true, 255 | "os": [ 256 | "freebsd" 257 | ], 258 | "engines": { 259 | "node": ">=18" 260 | } 261 | }, 262 | "node_modules/@esbuild/linux-arm": { 263 | "version": "0.25.2", 264 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", 265 | "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", 266 | "cpu": [ 267 | "arm" 268 | ], 269 | "dev": true, 270 | "optional": true, 271 | "os": [ 272 | "linux" 273 | ], 274 | "engines": { 275 | "node": ">=18" 276 | } 277 | }, 278 | "node_modules/@esbuild/linux-arm64": { 279 | "version": "0.25.2", 280 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", 281 | "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", 282 | "cpu": [ 283 | "arm64" 284 | ], 285 | "dev": true, 286 | "optional": true, 287 | "os": [ 288 | "linux" 289 | ], 290 | "engines": { 291 | "node": ">=18" 292 | } 293 | }, 294 | "node_modules/@esbuild/linux-ia32": { 295 | "version": "0.25.2", 296 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", 297 | "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", 298 | "cpu": [ 299 | "ia32" 300 | ], 301 | "dev": true, 302 | "optional": true, 303 | "os": [ 304 | "linux" 305 | ], 306 | "engines": { 307 | "node": ">=18" 308 | } 309 | }, 310 | "node_modules/@esbuild/linux-loong64": { 311 | "version": "0.25.2", 312 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", 313 | "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", 314 | "cpu": [ 315 | "loong64" 316 | ], 317 | "dev": true, 318 | "optional": true, 319 | "os": [ 320 | "linux" 321 | ], 322 | "engines": { 323 | "node": ">=18" 324 | } 325 | }, 326 | "node_modules/@esbuild/linux-mips64el": { 327 | "version": "0.25.2", 328 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", 329 | "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", 330 | "cpu": [ 331 | "mips64el" 332 | ], 333 | "dev": true, 334 | "optional": true, 335 | "os": [ 336 | "linux" 337 | ], 338 | "engines": { 339 | "node": ">=18" 340 | } 341 | }, 342 | "node_modules/@esbuild/linux-ppc64": { 343 | "version": "0.25.2", 344 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", 345 | "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", 346 | "cpu": [ 347 | "ppc64" 348 | ], 349 | "dev": true, 350 | "optional": true, 351 | "os": [ 352 | "linux" 353 | ], 354 | "engines": { 355 | "node": ">=18" 356 | } 357 | }, 358 | "node_modules/@esbuild/linux-riscv64": { 359 | "version": "0.25.2", 360 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", 361 | "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", 362 | "cpu": [ 363 | "riscv64" 364 | ], 365 | "dev": true, 366 | "optional": true, 367 | "os": [ 368 | "linux" 369 | ], 370 | "engines": { 371 | "node": ">=18" 372 | } 373 | }, 374 | "node_modules/@esbuild/linux-s390x": { 375 | "version": "0.25.2", 376 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", 377 | "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", 378 | "cpu": [ 379 | "s390x" 380 | ], 381 | "dev": true, 382 | "optional": true, 383 | "os": [ 384 | "linux" 385 | ], 386 | "engines": { 387 | "node": ">=18" 388 | } 389 | }, 390 | "node_modules/@esbuild/linux-x64": { 391 | "version": "0.25.2", 392 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", 393 | "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", 394 | "cpu": [ 395 | "x64" 396 | ], 397 | "dev": true, 398 | "optional": true, 399 | "os": [ 400 | "linux" 401 | ], 402 | "engines": { 403 | "node": ">=18" 404 | } 405 | }, 406 | "node_modules/@esbuild/netbsd-arm64": { 407 | "version": "0.25.2", 408 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", 409 | "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", 410 | "cpu": [ 411 | "arm64" 412 | ], 413 | "dev": true, 414 | "optional": true, 415 | "os": [ 416 | "netbsd" 417 | ], 418 | "engines": { 419 | "node": ">=18" 420 | } 421 | }, 422 | "node_modules/@esbuild/netbsd-x64": { 423 | "version": "0.25.2", 424 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", 425 | "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", 426 | "cpu": [ 427 | "x64" 428 | ], 429 | "dev": true, 430 | "optional": true, 431 | "os": [ 432 | "netbsd" 433 | ], 434 | "engines": { 435 | "node": ">=18" 436 | } 437 | }, 438 | "node_modules/@esbuild/openbsd-arm64": { 439 | "version": "0.25.2", 440 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", 441 | "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", 442 | "cpu": [ 443 | "arm64" 444 | ], 445 | "dev": true, 446 | "optional": true, 447 | "os": [ 448 | "openbsd" 449 | ], 450 | "engines": { 451 | "node": ">=18" 452 | } 453 | }, 454 | "node_modules/@esbuild/openbsd-x64": { 455 | "version": "0.25.2", 456 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", 457 | "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", 458 | "cpu": [ 459 | "x64" 460 | ], 461 | "dev": true, 462 | "optional": true, 463 | "os": [ 464 | "openbsd" 465 | ], 466 | "engines": { 467 | "node": ">=18" 468 | } 469 | }, 470 | "node_modules/@esbuild/sunos-x64": { 471 | "version": "0.25.2", 472 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", 473 | "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", 474 | "cpu": [ 475 | "x64" 476 | ], 477 | "dev": true, 478 | "optional": true, 479 | "os": [ 480 | "sunos" 481 | ], 482 | "engines": { 483 | "node": ">=18" 484 | } 485 | }, 486 | "node_modules/@esbuild/win32-arm64": { 487 | "version": "0.25.2", 488 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", 489 | "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", 490 | "cpu": [ 491 | "arm64" 492 | ], 493 | "dev": true, 494 | "optional": true, 495 | "os": [ 496 | "win32" 497 | ], 498 | "engines": { 499 | "node": ">=18" 500 | } 501 | }, 502 | "node_modules/@esbuild/win32-ia32": { 503 | "version": "0.25.2", 504 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", 505 | "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", 506 | "cpu": [ 507 | "ia32" 508 | ], 509 | "dev": true, 510 | "optional": true, 511 | "os": [ 512 | "win32" 513 | ], 514 | "engines": { 515 | "node": ">=18" 516 | } 517 | }, 518 | "node_modules/@esbuild/win32-x64": { 519 | "version": "0.25.2", 520 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", 521 | "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", 522 | "cpu": [ 523 | "x64" 524 | ], 525 | "dev": true, 526 | "optional": true, 527 | "os": [ 528 | "win32" 529 | ], 530 | "engines": { 531 | "node": ">=18" 532 | } 533 | }, 534 | "node_modules/@jridgewell/gen-mapping": { 535 | "version": "0.3.8", 536 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", 537 | "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", 538 | "dev": true, 539 | "dependencies": { 540 | "@jridgewell/set-array": "^1.2.1", 541 | "@jridgewell/sourcemap-codec": "^1.4.10", 542 | "@jridgewell/trace-mapping": "^0.3.24" 543 | }, 544 | "engines": { 545 | "node": ">=6.0.0" 546 | } 547 | }, 548 | "node_modules/@jridgewell/resolve-uri": { 549 | "version": "3.1.2", 550 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 551 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 552 | "dev": true, 553 | "engines": { 554 | "node": ">=6.0.0" 555 | } 556 | }, 557 | "node_modules/@jridgewell/set-array": { 558 | "version": "1.2.1", 559 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", 560 | "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", 561 | "dev": true, 562 | "engines": { 563 | "node": ">=6.0.0" 564 | } 565 | }, 566 | "node_modules/@jridgewell/sourcemap-codec": { 567 | "version": "1.5.0", 568 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 569 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 570 | "dev": true 571 | }, 572 | "node_modules/@jridgewell/trace-mapping": { 573 | "version": "0.3.25", 574 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", 575 | "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", 576 | "dev": true, 577 | "dependencies": { 578 | "@jridgewell/resolve-uri": "^3.1.0", 579 | "@jridgewell/sourcemap-codec": "^1.4.14" 580 | } 581 | }, 582 | "node_modules/@rollup/rollup-android-arm-eabi": { 583 | "version": "4.40.0", 584 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", 585 | "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", 586 | "cpu": [ 587 | "arm" 588 | ], 589 | "dev": true, 590 | "optional": true, 591 | "os": [ 592 | "android" 593 | ] 594 | }, 595 | "node_modules/@rollup/rollup-android-arm64": { 596 | "version": "4.40.0", 597 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", 598 | "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", 599 | "cpu": [ 600 | "arm64" 601 | ], 602 | "dev": true, 603 | "optional": true, 604 | "os": [ 605 | "android" 606 | ] 607 | }, 608 | "node_modules/@rollup/rollup-darwin-arm64": { 609 | "version": "4.40.0", 610 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", 611 | "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", 612 | "cpu": [ 613 | "arm64" 614 | ], 615 | "dev": true, 616 | "optional": true, 617 | "os": [ 618 | "darwin" 619 | ] 620 | }, 621 | "node_modules/@rollup/rollup-darwin-x64": { 622 | "version": "4.40.0", 623 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", 624 | "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", 625 | "cpu": [ 626 | "x64" 627 | ], 628 | "dev": true, 629 | "optional": true, 630 | "os": [ 631 | "darwin" 632 | ] 633 | }, 634 | "node_modules/@rollup/rollup-freebsd-arm64": { 635 | "version": "4.40.0", 636 | "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", 637 | "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", 638 | "cpu": [ 639 | "arm64" 640 | ], 641 | "dev": true, 642 | "optional": true, 643 | "os": [ 644 | "freebsd" 645 | ] 646 | }, 647 | "node_modules/@rollup/rollup-freebsd-x64": { 648 | "version": "4.40.0", 649 | "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", 650 | "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", 651 | "cpu": [ 652 | "x64" 653 | ], 654 | "dev": true, 655 | "optional": true, 656 | "os": [ 657 | "freebsd" 658 | ] 659 | }, 660 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 661 | "version": "4.40.0", 662 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", 663 | "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", 664 | "cpu": [ 665 | "arm" 666 | ], 667 | "dev": true, 668 | "optional": true, 669 | "os": [ 670 | "linux" 671 | ] 672 | }, 673 | "node_modules/@rollup/rollup-linux-arm-musleabihf": { 674 | "version": "4.40.0", 675 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", 676 | "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", 677 | "cpu": [ 678 | "arm" 679 | ], 680 | "dev": true, 681 | "optional": true, 682 | "os": [ 683 | "linux" 684 | ] 685 | }, 686 | "node_modules/@rollup/rollup-linux-arm64-gnu": { 687 | "version": "4.40.0", 688 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", 689 | "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", 690 | "cpu": [ 691 | "arm64" 692 | ], 693 | "dev": true, 694 | "optional": true, 695 | "os": [ 696 | "linux" 697 | ] 698 | }, 699 | "node_modules/@rollup/rollup-linux-arm64-musl": { 700 | "version": "4.40.0", 701 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", 702 | "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", 703 | "cpu": [ 704 | "arm64" 705 | ], 706 | "dev": true, 707 | "optional": true, 708 | "os": [ 709 | "linux" 710 | ] 711 | }, 712 | "node_modules/@rollup/rollup-linux-loongarch64-gnu": { 713 | "version": "4.40.0", 714 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", 715 | "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", 716 | "cpu": [ 717 | "loong64" 718 | ], 719 | "dev": true, 720 | "optional": true, 721 | "os": [ 722 | "linux" 723 | ] 724 | }, 725 | "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 726 | "version": "4.40.0", 727 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", 728 | "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", 729 | "cpu": [ 730 | "ppc64" 731 | ], 732 | "dev": true, 733 | "optional": true, 734 | "os": [ 735 | "linux" 736 | ] 737 | }, 738 | "node_modules/@rollup/rollup-linux-riscv64-gnu": { 739 | "version": "4.40.0", 740 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", 741 | "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", 742 | "cpu": [ 743 | "riscv64" 744 | ], 745 | "dev": true, 746 | "optional": true, 747 | "os": [ 748 | "linux" 749 | ] 750 | }, 751 | "node_modules/@rollup/rollup-linux-riscv64-musl": { 752 | "version": "4.40.0", 753 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", 754 | "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", 755 | "cpu": [ 756 | "riscv64" 757 | ], 758 | "dev": true, 759 | "optional": true, 760 | "os": [ 761 | "linux" 762 | ] 763 | }, 764 | "node_modules/@rollup/rollup-linux-s390x-gnu": { 765 | "version": "4.40.0", 766 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", 767 | "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", 768 | "cpu": [ 769 | "s390x" 770 | ], 771 | "dev": true, 772 | "optional": true, 773 | "os": [ 774 | "linux" 775 | ] 776 | }, 777 | "node_modules/@rollup/rollup-linux-x64-gnu": { 778 | "version": "4.40.0", 779 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", 780 | "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", 781 | "cpu": [ 782 | "x64" 783 | ], 784 | "dev": true, 785 | "optional": true, 786 | "os": [ 787 | "linux" 788 | ] 789 | }, 790 | "node_modules/@rollup/rollup-linux-x64-musl": { 791 | "version": "4.40.0", 792 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", 793 | "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", 794 | "cpu": [ 795 | "x64" 796 | ], 797 | "dev": true, 798 | "optional": true, 799 | "os": [ 800 | "linux" 801 | ] 802 | }, 803 | "node_modules/@rollup/rollup-win32-arm64-msvc": { 804 | "version": "4.40.0", 805 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", 806 | "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", 807 | "cpu": [ 808 | "arm64" 809 | ], 810 | "dev": true, 811 | "optional": true, 812 | "os": [ 813 | "win32" 814 | ] 815 | }, 816 | "node_modules/@rollup/rollup-win32-ia32-msvc": { 817 | "version": "4.40.0", 818 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", 819 | "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", 820 | "cpu": [ 821 | "ia32" 822 | ], 823 | "dev": true, 824 | "optional": true, 825 | "os": [ 826 | "win32" 827 | ] 828 | }, 829 | "node_modules/@rollup/rollup-win32-x64-msvc": { 830 | "version": "4.40.0", 831 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", 832 | "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", 833 | "cpu": [ 834 | "x64" 835 | ], 836 | "dev": true, 837 | "optional": true, 838 | "os": [ 839 | "win32" 840 | ] 841 | }, 842 | "node_modules/@tweakpane/plugin-essentials": { 843 | "version": "0.2.1", 844 | "resolved": "https://registry.npmjs.org/@tweakpane/plugin-essentials/-/plugin-essentials-0.2.1.tgz", 845 | "integrity": "sha512-VbFU1/uD+CJNFQdfLXUOLjeG5HyUZH97Ox9CxmyVetg1hqjVun3C83HAGFULyhKzl8tSgii8jr304r8QpdHwzQ==", 846 | "peerDependencies": { 847 | "tweakpane": "^4.0.0" 848 | } 849 | }, 850 | "node_modules/@types/estree": { 851 | "version": "1.0.7", 852 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", 853 | "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", 854 | "dev": true 855 | }, 856 | "node_modules/debug": { 857 | "version": "4.4.0", 858 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 859 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 860 | "dev": true, 861 | "dependencies": { 862 | "ms": "^2.1.3" 863 | }, 864 | "engines": { 865 | "node": ">=6.0" 866 | }, 867 | "peerDependenciesMeta": { 868 | "supports-color": { 869 | "optional": true 870 | } 871 | } 872 | }, 873 | "node_modules/esbuild": { 874 | "version": "0.25.2", 875 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", 876 | "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", 877 | "dev": true, 878 | "hasInstallScript": true, 879 | "bin": { 880 | "esbuild": "bin/esbuild" 881 | }, 882 | "engines": { 883 | "node": ">=18" 884 | }, 885 | "optionalDependencies": { 886 | "@esbuild/aix-ppc64": "0.25.2", 887 | "@esbuild/android-arm": "0.25.2", 888 | "@esbuild/android-arm64": "0.25.2", 889 | "@esbuild/android-x64": "0.25.2", 890 | "@esbuild/darwin-arm64": "0.25.2", 891 | "@esbuild/darwin-x64": "0.25.2", 892 | "@esbuild/freebsd-arm64": "0.25.2", 893 | "@esbuild/freebsd-x64": "0.25.2", 894 | "@esbuild/linux-arm": "0.25.2", 895 | "@esbuild/linux-arm64": "0.25.2", 896 | "@esbuild/linux-ia32": "0.25.2", 897 | "@esbuild/linux-loong64": "0.25.2", 898 | "@esbuild/linux-mips64el": "0.25.2", 899 | "@esbuild/linux-ppc64": "0.25.2", 900 | "@esbuild/linux-riscv64": "0.25.2", 901 | "@esbuild/linux-s390x": "0.25.2", 902 | "@esbuild/linux-x64": "0.25.2", 903 | "@esbuild/netbsd-arm64": "0.25.2", 904 | "@esbuild/netbsd-x64": "0.25.2", 905 | "@esbuild/openbsd-arm64": "0.25.2", 906 | "@esbuild/openbsd-x64": "0.25.2", 907 | "@esbuild/sunos-x64": "0.25.2", 908 | "@esbuild/win32-arm64": "0.25.2", 909 | "@esbuild/win32-ia32": "0.25.2", 910 | "@esbuild/win32-x64": "0.25.2" 911 | } 912 | }, 913 | "node_modules/fdir": { 914 | "version": "6.4.4", 915 | "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", 916 | "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", 917 | "dev": true, 918 | "peerDependencies": { 919 | "picomatch": "^3 || ^4" 920 | }, 921 | "peerDependenciesMeta": { 922 | "picomatch": { 923 | "optional": true 924 | } 925 | } 926 | }, 927 | "node_modules/fsevents": { 928 | "version": "2.3.3", 929 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 930 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 931 | "dev": true, 932 | "hasInstallScript": true, 933 | "optional": true, 934 | "os": [ 935 | "darwin" 936 | ], 937 | "engines": { 938 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 939 | } 940 | }, 941 | "node_modules/globals": { 942 | "version": "11.12.0", 943 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", 944 | "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", 945 | "dev": true, 946 | "engines": { 947 | "node": ">=4" 948 | } 949 | }, 950 | "node_modules/is-mobile": { 951 | "version": "5.0.0", 952 | "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", 953 | "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==" 954 | }, 955 | "node_modules/js-tokens": { 956 | "version": "4.0.0", 957 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 958 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 959 | "dev": true 960 | }, 961 | "node_modules/jsesc": { 962 | "version": "3.1.0", 963 | "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", 964 | "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", 965 | "dev": true, 966 | "bin": { 967 | "jsesc": "bin/jsesc" 968 | }, 969 | "engines": { 970 | "node": ">=6" 971 | } 972 | }, 973 | "node_modules/ms": { 974 | "version": "2.1.3", 975 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 976 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 977 | "dev": true 978 | }, 979 | "node_modules/nanoid": { 980 | "version": "3.3.11", 981 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 982 | "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 983 | "dev": true, 984 | "funding": [ 985 | { 986 | "type": "github", 987 | "url": "https://github.com/sponsors/ai" 988 | } 989 | ], 990 | "bin": { 991 | "nanoid": "bin/nanoid.cjs" 992 | }, 993 | "engines": { 994 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 995 | } 996 | }, 997 | "node_modules/picocolors": { 998 | "version": "1.1.1", 999 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 1000 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 1001 | "dev": true 1002 | }, 1003 | "node_modules/picomatch": { 1004 | "version": "4.0.2", 1005 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", 1006 | "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", 1007 | "dev": true, 1008 | "engines": { 1009 | "node": ">=12" 1010 | }, 1011 | "funding": { 1012 | "url": "https://github.com/sponsors/jonschlinkert" 1013 | } 1014 | }, 1015 | "node_modules/postcss": { 1016 | "version": "8.5.3", 1017 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", 1018 | "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", 1019 | "dev": true, 1020 | "funding": [ 1021 | { 1022 | "type": "opencollective", 1023 | "url": "https://opencollective.com/postcss/" 1024 | }, 1025 | { 1026 | "type": "tidelift", 1027 | "url": "https://tidelift.com/funding/github/npm/postcss" 1028 | }, 1029 | { 1030 | "type": "github", 1031 | "url": "https://github.com/sponsors/ai" 1032 | } 1033 | ], 1034 | "dependencies": { 1035 | "nanoid": "^3.3.8", 1036 | "picocolors": "^1.1.1", 1037 | "source-map-js": "^1.2.1" 1038 | }, 1039 | "engines": { 1040 | "node": "^10 || ^12 || >=14" 1041 | } 1042 | }, 1043 | "node_modules/rollup": { 1044 | "version": "4.40.0", 1045 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", 1046 | "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", 1047 | "dev": true, 1048 | "dependencies": { 1049 | "@types/estree": "1.0.7" 1050 | }, 1051 | "bin": { 1052 | "rollup": "dist/bin/rollup" 1053 | }, 1054 | "engines": { 1055 | "node": ">=18.0.0", 1056 | "npm": ">=8.0.0" 1057 | }, 1058 | "optionalDependencies": { 1059 | "@rollup/rollup-android-arm-eabi": "4.40.0", 1060 | "@rollup/rollup-android-arm64": "4.40.0", 1061 | "@rollup/rollup-darwin-arm64": "4.40.0", 1062 | "@rollup/rollup-darwin-x64": "4.40.0", 1063 | "@rollup/rollup-freebsd-arm64": "4.40.0", 1064 | "@rollup/rollup-freebsd-x64": "4.40.0", 1065 | "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", 1066 | "@rollup/rollup-linux-arm-musleabihf": "4.40.0", 1067 | "@rollup/rollup-linux-arm64-gnu": "4.40.0", 1068 | "@rollup/rollup-linux-arm64-musl": "4.40.0", 1069 | "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", 1070 | "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", 1071 | "@rollup/rollup-linux-riscv64-gnu": "4.40.0", 1072 | "@rollup/rollup-linux-riscv64-musl": "4.40.0", 1073 | "@rollup/rollup-linux-s390x-gnu": "4.40.0", 1074 | "@rollup/rollup-linux-x64-gnu": "4.40.0", 1075 | "@rollup/rollup-linux-x64-musl": "4.40.0", 1076 | "@rollup/rollup-win32-arm64-msvc": "4.40.0", 1077 | "@rollup/rollup-win32-ia32-msvc": "4.40.0", 1078 | "@rollup/rollup-win32-x64-msvc": "4.40.0", 1079 | "fsevents": "~2.3.2" 1080 | } 1081 | }, 1082 | "node_modules/slimdown-js": { 1083 | "version": "1.0.0", 1084 | "resolved": "https://registry.npmjs.org/slimdown-js/-/slimdown-js-1.0.0.tgz", 1085 | "integrity": "sha512-5bl05N92l1MQCnXZ7db09g6rZjwJ8d7ue3gWlAw1mYAcm6jrIY/wQeJNqRZvVc1Fr7MQ+3xw4oQmtp5DWUsVrw==" 1086 | }, 1087 | "node_modules/source-map-js": { 1088 | "version": "1.2.1", 1089 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 1090 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 1091 | "dev": true, 1092 | "engines": { 1093 | "node": ">=0.10.0" 1094 | } 1095 | }, 1096 | "node_modules/three": { 1097 | "version": "0.176.0", 1098 | "resolved": "https://registry.npmjs.org/three/-/three-0.176.0.tgz", 1099 | "integrity": "sha512-PWRKYWQo23ojf9oZSlRGH8K09q7nRSWx6LY/HF/UUrMdYgN9i1e2OwJYHoQjwc6HF/4lvvYLC5YC1X8UJL2ZpA==" 1100 | }, 1101 | "node_modules/tinyglobby": { 1102 | "version": "0.2.12", 1103 | "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", 1104 | "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", 1105 | "dev": true, 1106 | "dependencies": { 1107 | "fdir": "^6.4.3", 1108 | "picomatch": "^4.0.2" 1109 | }, 1110 | "engines": { 1111 | "node": ">=12.0.0" 1112 | }, 1113 | "funding": { 1114 | "url": "https://github.com/sponsors/SuperchupuDev" 1115 | } 1116 | }, 1117 | "node_modules/tweakpane": { 1118 | "version": "4.0.5", 1119 | "resolved": "https://registry.npmjs.org/tweakpane/-/tweakpane-4.0.5.tgz", 1120 | "integrity": "sha512-rxEXdSI+ArlG1RyO6FghC4ZUX8JkEfz8F3v1JuteXSV0pEtHJzyo07fcDG+NsJfN5L39kSbCYbB9cBGHyuI/tQ==", 1121 | "funding": { 1122 | "url": "https://github.com/sponsors/cocopon" 1123 | } 1124 | }, 1125 | "node_modules/tweakpane-plugin-infodump": { 1126 | "version": "4.0.2", 1127 | "resolved": "https://registry.npmjs.org/tweakpane-plugin-infodump/-/tweakpane-plugin-infodump-4.0.2.tgz", 1128 | "integrity": "sha512-zJOFZ1JVMNgsOdY+1+pGDgNYAWY/Zdmr1rRFVwFfVqqmDMYmhDriCnU7zWKP6SI5cTE+7iFjJrg2tAjfiRZiLw==", 1129 | "dependencies": { 1130 | "slimdown-js": "^1.0.0" 1131 | }, 1132 | "peerDependencies": { 1133 | "tweakpane": "^4.0.0" 1134 | } 1135 | }, 1136 | "node_modules/vite": { 1137 | "version": "6.3.2", 1138 | "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.2.tgz", 1139 | "integrity": "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==", 1140 | "dev": true, 1141 | "dependencies": { 1142 | "esbuild": "^0.25.0", 1143 | "fdir": "^6.4.3", 1144 | "picomatch": "^4.0.2", 1145 | "postcss": "^8.5.3", 1146 | "rollup": "^4.34.9", 1147 | "tinyglobby": "^0.2.12" 1148 | }, 1149 | "bin": { 1150 | "vite": "bin/vite.js" 1151 | }, 1152 | "engines": { 1153 | "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 1154 | }, 1155 | "funding": { 1156 | "url": "https://github.com/vitejs/vite?sponsor=1" 1157 | }, 1158 | "optionalDependencies": { 1159 | "fsevents": "~2.3.3" 1160 | }, 1161 | "peerDependencies": { 1162 | "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 1163 | "jiti": ">=1.21.0", 1164 | "less": "*", 1165 | "lightningcss": "^1.21.0", 1166 | "sass": "*", 1167 | "sass-embedded": "*", 1168 | "stylus": "*", 1169 | "sugarss": "*", 1170 | "terser": "^5.16.0", 1171 | "tsx": "^4.8.1", 1172 | "yaml": "^2.4.2" 1173 | }, 1174 | "peerDependenciesMeta": { 1175 | "@types/node": { 1176 | "optional": true 1177 | }, 1178 | "jiti": { 1179 | "optional": true 1180 | }, 1181 | "less": { 1182 | "optional": true 1183 | }, 1184 | "lightningcss": { 1185 | "optional": true 1186 | }, 1187 | "sass": { 1188 | "optional": true 1189 | }, 1190 | "sass-embedded": { 1191 | "optional": true 1192 | }, 1193 | "stylus": { 1194 | "optional": true 1195 | }, 1196 | "sugarss": { 1197 | "optional": true 1198 | }, 1199 | "terser": { 1200 | "optional": true 1201 | }, 1202 | "tsx": { 1203 | "optional": true 1204 | }, 1205 | "yaml": { 1206 | "optional": true 1207 | } 1208 | } 1209 | }, 1210 | "node_modules/vite-plugin-tsl-operator": { 1211 | "version": "1.2.3", 1212 | "resolved": "https://registry.npmjs.org/vite-plugin-tsl-operator/-/vite-plugin-tsl-operator-1.2.3.tgz", 1213 | "integrity": "sha512-ss5SiUmcxtzbvBHAt6vZggBiueoyq2h9iHQcwJorgArs550N1cRY+Vb5/R4vlLh0OzyMN4d0uuiInu44/0HIYg==", 1214 | "dev": true 1215 | } 1216 | } 1217 | } 1218 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "softbodies", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "vite build --mode=production", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "@babel/generator": "^7.26.9", 13 | "@babel/parser": "^7.26.9", 14 | "@babel/traverse": "^7.26.9", 15 | "@babel/types": "^7.26.9", 16 | "vite": "^6.3.1", 17 | "vite-plugin-tsl-operator": "^1.2.3" 18 | }, 19 | "dependencies": { 20 | "@tweakpane/plugin-essentials": "^0.2.1", 21 | "is-mobile": "^5.0.0", 22 | "three": "^0.176.0", 23 | "tweakpane": "^4.0.5", 24 | "tweakpane-plugin-infodump": "^4.0.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/FEMPhysics/FEMPhysics.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three/webgpu"; 2 | import { 3 | Fn, 4 | instancedArray, 5 | instanceIndex, 6 | float, 7 | uint, 8 | vec3, 9 | vec2, 10 | sin, 11 | vec4, 12 | cross, 13 | mul, 14 | mat3, 15 | int, 16 | dot, 17 | abs, 18 | div, 19 | length, 20 | If, 21 | Loop, 22 | Break, 23 | normalize, Return, uniform, select, time, mix, min, uniformArray, ivec3, atomicAdd, atomicStore, atomicFunc, uvec3, struct 24 | } from "three/tsl"; 25 | import {SoftbodyGeometry} from "./softbodyGeometry.js"; 26 | import {conf} from "../conf"; 27 | import {StructuredArray} from "./structuredArray.js"; 28 | import { murmurHash13, rotateByQuat, quat_conj, quat_mult, extractRotation } from "./math.js"; 29 | import {Grid} from "./grid.js"; 30 | 31 | export class FEMPhysics { 32 | vertices = []; 33 | 34 | tets = []; 35 | 36 | geometries = []; 37 | 38 | objects = []; 39 | 40 | objectData = []; 41 | 42 | vertexCount = 0; 43 | 44 | tetCount = 0; 45 | 46 | density = 1000; 47 | 48 | kernels = {}; 49 | 50 | uniforms = {}; 51 | 52 | time = 0; 53 | 54 | frameNum = 0; 55 | 56 | timeSinceLastStep = 0; 57 | 58 | colliders = []; 59 | 60 | objectCount = 0; 61 | 62 | constructor(renderer) { 63 | this.renderer = renderer; 64 | this.object = new THREE.Object3D(); 65 | } 66 | 67 | addVertex(objectId,x,y,z) { 68 | const id = this.vertexCount; 69 | const vertex = new THREE.Vector3(x,y,z); 70 | vertex.id = id; 71 | vertex.objectId = objectId; 72 | vertex.influencers = []; 73 | this.vertices.push(vertex); 74 | 75 | const objectDataElement = this.objectData[objectId]; 76 | const distance = vertex.length(); 77 | if (distance < objectDataElement.centerVertexDistance) { 78 | objectDataElement.centerVertexDistance = distance; 79 | objectDataElement.centerVertex = vertex; 80 | } 81 | 82 | objectDataElement.vertexCount++; 83 | this.vertexCount++; 84 | return vertex; 85 | } 86 | 87 | addTet(objectId,v0,v1,v2,v3) { 88 | const id = this.tetCount; 89 | const tet = {id,v0,v1,v2,v3,objectId}; 90 | this.tets.push(tet); 91 | v0.influencers.push(id * 4 + 0); 92 | v1.influencers.push(id * 4 + 1); 93 | v2.influencers.push(id * 4 + 2); 94 | v3.influencers.push(id * 4 + 3); 95 | this.objectData[objectId].tetCount++; 96 | this.tetCount++; 97 | return tet; 98 | } 99 | 100 | _addObject(object) { 101 | const id = this.objects.length; 102 | this.objects.push(object); 103 | 104 | const params = { 105 | id, 106 | centerVertexDistance: 1e9, 107 | centerVertex: null, 108 | tetStart: this.tetCount, 109 | tetCount: 0, 110 | vertexStart: this.vertexCount, 111 | vertexCount: 0, 112 | position: new THREE.Vector3(), 113 | }; 114 | 115 | this.objectData.push(params); 116 | return params; 117 | } 118 | 119 | addGeometry(model, materialClass = THREE.MeshPhysicalNodeMaterial) { 120 | const geometry = new SoftbodyGeometry(this, model, materialClass); 121 | this.geometries.push(geometry); 122 | return geometry; 123 | } 124 | 125 | addInstance(geometry) { 126 | return geometry.addInstance(); 127 | } 128 | 129 | addCollider(collider) { 130 | this.colliders.push(collider); 131 | } 132 | 133 | getPosition(objectId) { 134 | return this.objectData[objectId].position; 135 | } 136 | 137 | async bake() { 138 | console.log(this.vertexCount + " vertices"); 139 | console.log(this.tetCount + " tetrahedrons"); 140 | 141 | // ################ 142 | // CREATE BUFFERS 143 | // ################ 144 | 145 | const tetStruct = { 146 | restVolume: "float", 147 | radius: "float", 148 | objectId: "uint", 149 | nextTet: "int", 150 | quat: "vec4", 151 | initialPosition: "vec3", 152 | centroid: "vec3", 153 | vertexIds: "uvec4", 154 | }; 155 | const tetBuffer = new StructuredArray(tetStruct, this.tetCount, "tets"); 156 | this.tetBuffer = tetBuffer; 157 | 158 | const restposeStruct = { 159 | position: "vec3", 160 | restVolume: "float", 161 | }; 162 | const restPosesBuffer = new StructuredArray(restposeStruct, this.tetCount * 4, "restPoses"); 163 | this.restPosesBuffer = restPosesBuffer; 164 | 165 | let maxRadius = 0; 166 | this.tets.forEach((tet,index) => { 167 | const { v0, v1, v2, v3 } = tet; 168 | const center = v0.clone().add(v1).add(v2).add(v3).multiplyScalar(0.25); 169 | const a = v1.clone().sub(v0); 170 | const b = v2.clone().sub(v0); 171 | const c = v3.clone().sub(v0); 172 | const V = Math.abs(a.cross(b).dot(c)) / 6; 173 | const radius = (Math.pow((3/4) * V / Math.PI, 1/3)); 174 | maxRadius = Math.max(maxRadius, radius); 175 | const vs = [v0, v1, v2, v3]; 176 | vs.forEach((vertex,subindex) => { 177 | restPosesBuffer.set(index*4 + subindex, "position", vertex); 178 | restPosesBuffer.set(index*4 + subindex, "restVolume", V); 179 | }); 180 | 181 | tetBuffer.set(index, "initialPosition", center); 182 | tetBuffer.set(index, "vertexIds", [v0.id,v1.id,v2.id,v3.id]); 183 | tetBuffer.set(index, "restVolume", V); 184 | tetBuffer.set(index, "quat", [0,0,0,1]); 185 | tetBuffer.set(index, "objectId", tet.objectId); 186 | tetBuffer.set(index, "radius", radius); 187 | }); 188 | console.log("maxRadius", maxRadius); 189 | 190 | 191 | const vertexStruct = { 192 | objectId: "uint", 193 | influencerPtr: "uint", 194 | influencerCount: "uint", 195 | initialPosition: "vec3", 196 | position: "vec3", 197 | prevPosition: "vec3", 198 | }; 199 | const vertexBuffer = new StructuredArray(vertexStruct, this.vertexCount, "vertices"); 200 | this.vertexBuffer = vertexBuffer; 201 | 202 | const influencerArray = new Uint32Array(this.tetCount * 4); 203 | let influencerPtr = 0; 204 | this.vertices.forEach((vertex, index) => { 205 | vertexBuffer.set(index, "initialPosition", vertex); 206 | //vertexBuffer.set(index, "position", vertex); 207 | vertexBuffer.set(index, "prevPosition", vertex); 208 | vertexBuffer.set(index, "influencerPtr", influencerPtr); 209 | vertexBuffer.set(index, "influencerCount", vertex.influencers.length); 210 | vertexBuffer.set(index, "objectId", vertex.objectId); 211 | 212 | vertex.influencers.forEach(influencer => { 213 | influencerArray[influencerPtr] = influencer; 214 | influencerPtr++; 215 | }); 216 | }); 217 | 218 | const objectStruct = { 219 | size: "float", 220 | centerVertex: "uint", 221 | } 222 | const objectBuffer = new StructuredArray(objectStruct, this.objects.length, "objects"); 223 | this.objectBuffer = objectBuffer; 224 | this.objectData.forEach((objectData, index) => { 225 | objectBuffer.set(index, "size", 0.0); 226 | objectBuffer.set(index, "centerVertex", objectData.centerVertex.id); 227 | }); 228 | 229 | 230 | this.influencerBuffer = instancedArray(influencerArray, 'uint'); 231 | 232 | const gridCellSize = maxRadius * 2; //0.36 233 | this.grid = new Grid(gridCellSize, "basic"); 234 | 235 | 236 | // ################# 237 | // CREATE UNIFORMS 238 | // ################# 239 | 240 | this.uniforms.vertexCount = uniform(this.vertexCount, "int"); 241 | this.uniforms.tetCount = uniform(this.tetCount, "int"); 242 | this.uniforms.time = uniform(0, "float"); 243 | this.uniforms.dt = uniform(1, "float"); 244 | this.uniforms.gravity = uniform(new THREE.Vector3(0,-9.81*2,0), "vec3"); 245 | //this.uniforms.scales = uniformArray(new Array(this.objectData.length).fill(0), "float"); 246 | this.uniforms.rotationRefinementSteps = uniform(2, "int"); 247 | //conf.settings.addBinding(this.uniforms.rotationRefinementSteps, "value", { min: 1, max: 9, step: 1 }); 248 | 249 | 250 | // ################ 251 | // CREATE KERNELS 252 | // ################ 253 | 254 | //console.time("clearHashMap"); 255 | await this.grid.clearBuffer(this.renderer) //call once to compile 256 | //console.timeEnd("clearHashMap"); 257 | 258 | 259 | this.kernels.solveElemPass = Fn(() => { 260 | this.grid.setAtomic(true); 261 | If(instanceIndex.greaterThanEqual(this.uniforms.tetCount), () => { 262 | Return(); 263 | }); 264 | const objectId = tetBuffer.get(instanceIndex, "objectId"); 265 | const size = objectBuffer.get(objectId, "size"); 266 | If(size.lessThan(0.0001), () => { 267 | Return(); 268 | }); 269 | 270 | // Gather this tetrahedron's 4 vertex positions 271 | const vertexIds = tetBuffer.get(instanceIndex, "vertexIds").toVar(); 272 | const pos0 = vertexBuffer.get(vertexIds.x, "position").toVar(); 273 | const pos1 = vertexBuffer.get(vertexIds.y, "position").toVar(); 274 | const pos2 = vertexBuffer.get(vertexIds.z, "position").toVar(); 275 | const pos3 = vertexBuffer.get(vertexIds.w, "position").toVar(); 276 | 277 | // The Reference Rest Pose Positions 278 | // These are the same as the resting pose, but they're already pre-rotated 279 | // to a good approximation of the current pose 280 | const ref0 = restPosesBuffer.get(instanceIndex.mul(4), "position").toVar(); 281 | const ref1 = restPosesBuffer.get(instanceIndex.mul(4).add(1), "position").toVar(); 282 | const ref2 = restPosesBuffer.get(instanceIndex.mul(4).add(2), "position").toVar(); 283 | const ref3 = restPosesBuffer.get(instanceIndex.mul(4).add(3), "position").toVar(); 284 | 285 | // Get the centroids 286 | const curCentroid = pos0.add(pos1).add(pos2).add(pos3).mul(0.25).toVar(); 287 | const lastRestCentroid = ref0.add(ref1).add(ref2).add(ref3).mul(0.25).toVar(); 288 | 289 | // Center the Deformed Tetrahedron 290 | pos0.subAssign(curCentroid); 291 | pos1.subAssign(curCentroid); 292 | pos2.subAssign(curCentroid); 293 | pos3.subAssign(curCentroid); 294 | 295 | // Center the Undeformed Tetrahedron 296 | ref0.subAssign(lastRestCentroid); 297 | ref1.subAssign(lastRestCentroid); 298 | ref2.subAssign(lastRestCentroid); 299 | ref3.subAssign(lastRestCentroid); 300 | 301 | // Find the rotational offset between the two and rotate the undeformed tetrahedron by it 302 | const covariance = mat3(0,0,0,0,0,0,0,0,0).toVar(); 303 | covariance.element(0).xyz.addAssign(ref0.xxx.mul(pos0)); 304 | covariance.element(1).xyz.addAssign(ref0.yyy.mul(pos0)); 305 | covariance.element(2).xyz.addAssign(ref0.zzz.mul(pos0)); 306 | covariance.element(0).xyz.addAssign(ref1.xxx.mul(pos1)); 307 | covariance.element(1).xyz.addAssign(ref1.yyy.mul(pos1)); 308 | covariance.element(2).xyz.addAssign(ref1.zzz.mul(pos1)); 309 | covariance.element(0).xyz.addAssign(ref2.xxx.mul(pos2)); 310 | covariance.element(1).xyz.addAssign(ref2.yyy.mul(pos2)); 311 | covariance.element(2).xyz.addAssign(ref2.zzz.mul(pos2)); 312 | covariance.element(0).xyz.addAssign(ref3.xxx.mul(pos3)); 313 | covariance.element(1).xyz.addAssign(ref3.yyy.mul(pos3)); 314 | covariance.element(2).xyz.addAssign(ref3.zzz.mul(pos3)); 315 | const rotation = extractRotation(covariance, vec4(0.0, 0.0, 0.0, 1.0), this.uniforms.rotationRefinementSteps); 316 | 317 | // Write out the undeformed tetrahedron 318 | const prevQuat = tetBuffer.get(instanceIndex, "quat").toVar(); 319 | const newQuat = normalize(quat_mult(rotation, prevQuat)); // Keep track of the current Quaternion for normals 320 | tetBuffer.get(instanceIndex, "quat").assign(newQuat); 321 | 322 | const relativeQuat = normalize(quat_mult(newQuat, quat_conj(prevQuat))); 323 | 324 | // Rotate the undeformed tetrahedron by the deformed's rotationf 325 | ref0.assign(rotateByQuat(ref0, relativeQuat).add(curCentroid)); 326 | ref1.assign(rotateByQuat(ref1, relativeQuat).add(curCentroid)); 327 | ref2.assign(rotateByQuat(ref2, relativeQuat).add(curCentroid)); 328 | ref3.assign(rotateByQuat(ref3, relativeQuat).add(curCentroid)); 329 | 330 | tetBuffer.get(instanceIndex, "centroid").assign(curCentroid); 331 | restPosesBuffer.get(instanceIndex.mul(4), "position").assign(ref0); 332 | restPosesBuffer.get(instanceIndex.mul(4).add(1), "position").assign(ref1); 333 | restPosesBuffer.get(instanceIndex.mul(4).add(2), "position").assign(ref2); 334 | restPosesBuffer.get(instanceIndex.mul(4).add(3), "position").assign(ref3); 335 | 336 | const gridElement = this.grid.getElement(curCentroid); 337 | tetBuffer.get(instanceIndex, "nextTet").assign(atomicFunc("atomicExchange", gridElement, instanceIndex)); 338 | 339 | })().compute(this.tetCount); 340 | //console.time("solveElemPass"); 341 | await this.renderer.computeAsync(this.kernels.solveElemPass); //call once to compile 342 | //console.timeEnd("solveElemPass"); 343 | 344 | 345 | this.kernels.solveCollisions = Fn(() => { 346 | this.grid.setAtomic(false); 347 | If(instanceIndex.greaterThanEqual(this.uniforms.tetCount), () => { 348 | Return(); 349 | }); 350 | const objectId = tetBuffer.get(instanceIndex, "objectId").toVar(); 351 | const size = objectBuffer.get(objectId, "size"); 352 | If(size.lessThan(0.0001), () => { 353 | Return(); 354 | }); 355 | 356 | const centroid = tetBuffer.get(instanceIndex, "centroid").toVar("centroid"); 357 | const position = centroid.toVar("pos"); 358 | const radius = tetBuffer.get(instanceIndex, "radius").toVar(); 359 | const initialPosition = tetBuffer.get(instanceIndex, "initialPosition").toVar(); 360 | 361 | const cellIndex = ivec3(position.div(this.grid.cellsize).floor()).sub(1).toConst("cellIndex"); 362 | const diff = vec3(0).toVar(); 363 | const totalForce = float(0).toVar(); 364 | 365 | //If(uint(1).greaterThan(uint(0)), () => { Return(); }); 366 | Loop({ start: 0, end: 3, type: 'int', name: 'gx', condition: '<' }, ({gx}) => { 367 | Loop({ start: 0, end: 3, type: 'int', name: 'gy', condition: '<' }, ({gy}) => { 368 | Loop({ start: 0, end: 3, type: 'int', name: 'gz', condition: '<' }, ({gz}) => { 369 | const cellX = cellIndex.add(ivec3(gx,gy,gz)).toConst(); 370 | const tetPtr = this.grid.getElementFromIndex(cellX).toVar('tetPtr'); 371 | Loop(tetPtr.notEqual(int(-1)), () => { 372 | const checkCollision = uint(1).toVar(); 373 | const objectId2 = tetBuffer.get(tetPtr, "objectId"); 374 | If(objectId.equal(objectId2), () => { 375 | const initialPosition2 = tetBuffer.get(tetPtr, "initialPosition") 376 | const delta = initialPosition2.sub(initialPosition).toVar(); 377 | const distSquared = dot(delta,delta); 378 | checkCollision.assign(select(distSquared.greaterThan(1.5*1.5), uint(1), uint(0))); 379 | }); 380 | 381 | If(checkCollision.equal(uint(1)), () => { 382 | const centroid_2 = tetBuffer.get(tetPtr, "centroid").toVar("centroid2"); 383 | const radius2 = tetBuffer.get(tetPtr, "radius").toVar(); 384 | 385 | const minDist = radius.add(radius2); 386 | const dist = centroid.distance(centroid_2); 387 | const dir = centroid.sub(centroid_2).div(dist); 388 | const force = minDist.sub(dist).max(0); 389 | totalForce.addAssign(force.div(minDist)); 390 | diff.addAssign(dir.mul(force).mul(0.5)); 391 | }); 392 | tetPtr.assign(tetBuffer.get(tetPtr, "nextTet")); 393 | }) 394 | }); 395 | }); 396 | }); 397 | If(totalForce.greaterThan(0.0), () => { 398 | //diff.divAssign(totalForce); 399 | restPosesBuffer.get(instanceIndex.mul(4), "position").addAssign(diff); 400 | restPosesBuffer.get(instanceIndex.mul(4).add(1), "position").addAssign(diff); 401 | restPosesBuffer.get(instanceIndex.mul(4).add(2), "position").addAssign(diff); 402 | restPosesBuffer.get(instanceIndex.mul(4).add(3), "position").addAssign(diff); 403 | }); 404 | })().compute(this.tetCount); 405 | //console.time("solveCollisions"); 406 | await this.renderer.computeAsync(this.kernels.solveCollisions); //call once to compile 407 | //console.timeEnd("solveCollisions"); 408 | 409 | 410 | this.kernels.applyElemPass = Fn(()=>{ 411 | If(instanceIndex.greaterThanEqual(this.uniforms.vertexCount), () => { 412 | Return(); 413 | }); 414 | const objectId = vertexBuffer.get(instanceIndex, "objectId"); 415 | const size = objectBuffer.get(objectId, "size"); 416 | If(size.lessThan(0.0001), () => { 417 | Return(); 418 | }); 419 | 420 | const prevPosition = vertexBuffer.get(instanceIndex, "prevPosition").toVar(); 421 | const ptrStart = vertexBuffer.get(instanceIndex, "influencerPtr").toVar(); 422 | const ptrEnd = ptrStart.add(vertexBuffer.get(instanceIndex, "influencerCount")).toVar(); 423 | const position = vec3().toVar(); 424 | const weight = float().toVar(); 425 | Loop({ start: ptrStart, end: ptrEnd, type: 'uint', condition: '<' }, ({ i })=>{ 426 | const restPositionPtr = this.influencerBuffer.element(i); 427 | const restPosition = restPosesBuffer.get(restPositionPtr, "position"); 428 | const restVolume = restPosesBuffer.get(restPositionPtr, "restVolume"); 429 | position.addAssign(restPosition.mul(restVolume)); 430 | weight.addAssign(restVolume); 431 | }); 432 | position.divAssign(weight); 433 | //const currentPosition = this.positionBuffer.element(instanceIndex).toVar(); 434 | 435 | vertexBuffer.get(instanceIndex, "prevPosition").assign(position); 436 | 437 | const { dt, gravity } = this.uniforms; 438 | const gravity2 = position.normalize().mul(-9.81).mul(1); 439 | const velocity = position.sub(prevPosition).div(dt).add(gravity2.mul(dt)).mul(0.999); 440 | position.addAssign(velocity.mul(dt)); 441 | 442 | const F = prevPosition.sub(position); 443 | const frictionDir = vec3(0).toVar(); 444 | this.colliders.forEach((collider) => { 445 | const colliderResult = collider(position); 446 | const diff = colliderResult.w.min(0).negate().toVar(); 447 | position.addAssign(diff.mul(colliderResult.xyz)); 448 | frictionDir.addAssign(colliderResult.xyz.abs().oneMinus().mul(diff.sign())); 449 | }); 450 | position.xyz.addAssign(F.mul(frictionDir).mul(min(1.0, dt.mul(100)))); 451 | 452 | vertexBuffer.get(instanceIndex, "position").assign(position); 453 | })().compute(this.vertexCount); 454 | //console.time("applyElemPass"); 455 | await this.renderer.computeAsync(this.kernels.applyElemPass); //call once to compile 456 | //console.timeEnd("applyElemPass"); 457 | 458 | 459 | // ###################### 460 | // CREATE RESET KERNELS 461 | // ###################### 462 | 463 | this.uniforms.resetVertexStart = uniform(0, "uint"); 464 | this.uniforms.resetVertexCount = uniform(0, "uint"); 465 | this.uniforms.resetVelocity = uniform(new THREE.Vector3()); 466 | this.uniforms.resetMatrix = uniform(new THREE.Matrix4()); 467 | this.uniforms.resetQuat = uniform(new THREE.Vector4()); 468 | this.kernels.resetVertices = Fn(()=>{ 469 | If(instanceIndex.greaterThanEqual(this.uniforms.resetVertexCount), () => { 470 | Return(); 471 | }); 472 | const vertexId = this.uniforms.resetVertexStart.add(instanceIndex).toVar(); 473 | 474 | If(instanceIndex.equal(uint(0)), () => { 475 | const objectId = vertexBuffer.get(vertexId, "objectId").toVar(); 476 | objectBuffer.get(objectId, "size").assign(1.0); 477 | }); 478 | 479 | 480 | const initialPosition = this.uniforms.resetMatrix.mul(vec4(vertexBuffer.get(vertexId, "initialPosition").xyz, 1)).xyz.toVar(); 481 | vertexBuffer.get(vertexId, "position").assign(initialPosition); 482 | vertexBuffer.get(vertexId, "prevPosition").assign(initialPosition.sub(this.uniforms.resetVelocity)); 483 | })().compute(this.vertexCount); 484 | //console.time("resetVertices"); 485 | await this.renderer.computeAsync(this.kernels.resetVertices); //call once to compile 486 | //console.timeEnd("resetVertices"); 487 | 488 | 489 | this.uniforms.resetTetStart = uniform(0, "uint"); 490 | this.uniforms.resetTetCount = uniform(0, "uint"); 491 | this.kernels.resetTets = Fn(() => { 492 | If(instanceIndex.greaterThanEqual(this.uniforms.resetTetCount), () => { 493 | Return(); 494 | }); 495 | const tetId = this.uniforms.resetTetStart.add(instanceIndex).toVar(); 496 | const volume = tetBuffer.get(tetId, "restVolume").toVar(); 497 | 498 | // Gather this tetrahedron's 4 vertex positions 499 | const vertexIds = tetBuffer.get(tetId, "vertexIds"); 500 | const pos0 = this.uniforms.resetMatrix.mul(vec4(vertexBuffer.get(vertexIds.x, "initialPosition").xyz, 1)).xyz.toVar(); 501 | const pos1 = this.uniforms.resetMatrix.mul(vec4(vertexBuffer.get(vertexIds.y, "initialPosition").xyz, 1)).xyz.toVar(); 502 | const pos2 = this.uniforms.resetMatrix.mul(vec4(vertexBuffer.get(vertexIds.z, "initialPosition").xyz, 1)).xyz.toVar(); 503 | const pos3 = this.uniforms.resetMatrix.mul(vec4(vertexBuffer.get(vertexIds.w, "initialPosition").xyz, 1)).xyz.toVar(); 504 | 505 | restPosesBuffer.get(tetId.mul(4), "position").assign(pos0); 506 | restPosesBuffer.get(tetId.mul(4).add(1), "position").assign(pos1); 507 | restPosesBuffer.get(tetId.mul(4).add(2), "position").assign(pos2); 508 | restPosesBuffer.get(tetId.mul(4).add(3), "position").assign(pos3); 509 | tetBuffer.get(tetId, "quat").assign(this.uniforms.resetQuat); 510 | })().compute(this.tetCount); 511 | //console.time("resetTets"); 512 | await this.renderer.computeAsync(this.kernels.resetTets); //call once to compile 513 | //console.timeEnd("resetTets"); 514 | 515 | this.uniforms.objectStart = uniform(0, "uint"); 516 | this.kernels.resetObjects = Fn(() => { 517 | const objectId = this.uniforms.objectStart.add(instanceIndex).toVar(); 518 | objectBuffer.get(objectId, "size").assign(0.0); 519 | })().compute(this.objects.length); 520 | 521 | 522 | // ################################## 523 | // CREATE MOUSE INTERACTION KERNELS 524 | // ################################## 525 | 526 | this.uniforms.mouseRayOrigin = uniform(new THREE.Vector3()); 527 | this.uniforms.mouseRayDirection = uniform(new THREE.Vector3()); 528 | this.kernels.applyMouseEvent = Fn(()=>{ 529 | If(instanceIndex.greaterThanEqual(this.uniforms.vertexCount), () => { 530 | Return(); 531 | }); 532 | 533 | const objectId = vertexBuffer.get(instanceIndex, "objectId"); 534 | const size = objectBuffer.get(objectId, "size"); 535 | If(size.lessThan(0.0001), () => { 536 | Return(); 537 | }); 538 | 539 | const { mouseRayOrigin, mouseRayDirection } = this.uniforms; 540 | const position = vertexBuffer.get(instanceIndex, "position").toVar(); 541 | const prevPosition = vertexBuffer.get(instanceIndex, "prevPosition"); 542 | 543 | const dist = cross(mouseRayDirection, position.sub(mouseRayOrigin)).length() 544 | const force = dist.mul(0.3).oneMinus().max(0.0).pow(0.5); 545 | prevPosition.addAssign(vec3(0,-0.25,0).mul(force)); 546 | })().compute(this.vertexCount); 547 | //console.time("applyMouseEvent"); 548 | await this.renderer.computeAsync(this.kernels.applyMouseEvent); //call once to compile 549 | //console.timeEnd("applyMouseEvent"); 550 | 551 | 552 | // ############################# 553 | // CREATE POSITION READ KERNEL 554 | // ############################# 555 | 556 | const centerVertexArray = new Uint32Array(this.objectData.map(d => d.centerVertex.id)); 557 | this.centerVertexBuffer = instancedArray(centerVertexArray, 'uint'); 558 | this.positionReadbackBuffer = instancedArray(new Float32Array(this.objects.length*3), 'vec3'); 559 | this.kernels.readPositions = Fn(()=>{ 560 | const centerVertex = this.centerVertexBuffer.element(instanceIndex); 561 | const position = vertexBuffer.get(centerVertex, "position"); 562 | this.positionReadbackBuffer.element(instanceIndex).assign(position); 563 | })().compute(this.objects.length); 564 | await this.renderer.computeAsync(this.kernels.readPositions); //call once to compile 565 | 566 | 567 | // #################### 568 | // BAKE OTHER OBJECTS 569 | // #################### 570 | 571 | const geometryPromises = this.geometries.map(geom => geom.bake(this)); 572 | await Promise.all(geometryPromises); 573 | } 574 | 575 | async readPositions() { 576 | await this.renderer.computeAsync(this.kernels.readPositions); 577 | const positions = new Float32Array(await this.renderer.getArrayBufferAsync(this.positionReadbackBuffer.value)); 578 | this.objectData.forEach((o, index) => { 579 | const x = positions[index*4+0]; 580 | const y = positions[index*4+1]; 581 | const z = positions[index*4+2]; 582 | o.position.set(x,y,z); 583 | }); 584 | } 585 | 586 | async resetObject(id, position, quaternion, scale, velocity = new THREE.Vector3()) { 587 | this.objectData[id].position.copy(position); 588 | 589 | this.uniforms.resetMatrix.value.compose(position, quaternion, scale); 590 | this.uniforms.resetQuat.value.copy(quaternion); 591 | this.uniforms.resetVertexStart.value = this.objectData[id].vertexStart; 592 | this.uniforms.resetVertexCount.value = this.objectData[id].vertexCount; 593 | this.uniforms.resetTetStart.value = this.objectData[id].tetStart; 594 | this.uniforms.resetTetCount.value = this.objectData[id].tetCount; 595 | //this.uniforms.resetOffset.value.copy(position); 596 | this.uniforms.resetVelocity.value.copy(velocity); 597 | //this.uniforms.resetScale.value = scale; 598 | this.kernels.resetVertices.count = this.objectData[id].vertexCount; 599 | this.kernels.resetTets.count = this.objectData[id].tetCount; 600 | this.kernels.resetVertices.updateDispatchCount(); 601 | this.kernels.resetTets.updateDispatchCount(); 602 | await this.renderer.computeAsync(this.kernels.resetVertices); 603 | await this.renderer.computeAsync(this.kernels.resetTets); 604 | } 605 | 606 | async onPointerDown(origin, direction) { 607 | this.uniforms.mouseRayOrigin.value.copy(origin); 608 | this.uniforms.mouseRayDirection.value.copy(direction); 609 | await this.renderer.computeAsync(this.kernels.applyMouseEvent); 610 | } 611 | 612 | async update(interval, elapsed) { 613 | const { stepsPerSecond, bodies } = conf; 614 | this.frameNum++; 615 | 616 | if (bodies !== this.objectCount) { 617 | this.objectCount = bodies; 618 | for (let i=this.objectCount; i geom.updateCount()); 623 | 624 | const lastObject = this.objectData[this.objectCount - 1]; 625 | const tetCount = lastObject.tetStart + lastObject.tetCount; 626 | const vertexCount = lastObject.vertexStart + lastObject.vertexCount; 627 | 628 | this.uniforms.tetCount.value = tetCount; 629 | this.uniforms.vertexCount.value = vertexCount; 630 | 631 | this.kernels.solveElemPass.count = tetCount; 632 | this.kernels.solveCollisions.count = tetCount; 633 | this.kernels.applyElemPass.count = vertexCount; 634 | this.kernels.applyMouseEvent.count = vertexCount; 635 | 636 | this.kernels.solveElemPass.updateDispatchCount(); 637 | this.kernels.solveCollisions.updateDispatchCount(); 638 | this.kernels.applyElemPass.updateDispatchCount(); 639 | this.kernels.applyMouseEvent.updateDispatchCount(); 640 | 641 | this.uniforms.objectStart.value = this.objectCount; 642 | await this.renderer.computeAsync(this.kernels.resetObjects); 643 | } 644 | 645 | if (this.frameNum % 50 === 0) { 646 | //this.readPositions().then(() => {}); // no await to prevent blocking! 647 | } 648 | 649 | 650 | const timePerStep = 1 / stepsPerSecond; 651 | 652 | interval = Math.max(Math.min(interval, 1/60), 0.0001); 653 | this.uniforms.dt.value = timePerStep; 654 | 655 | this.timeSinceLastStep += interval; 656 | 657 | for (let i=0; i= timePerStep) { 663 | this.time += timePerStep; 664 | this.timeSinceLastStep -= timePerStep; 665 | this.uniforms.time.value = this.time; 666 | await this.grid.clearBuffer(this.renderer); 667 | await this.renderer.computeAsync(this.kernels.solveElemPass); 668 | await this.renderer.computeAsync(this.kernels.solveCollisions); 669 | await this.renderer.computeAsync(this.kernels.applyElemPass); 670 | } 671 | 672 | 673 | /*if (this.frameNum > 1) { 674 | const hashMap = new Int32Array(await this.renderer.getArrayBufferAsync(this.grid.buffer.value)); 675 | const res = hashMap.filter(i => i >= 0); 676 | console.log((res.length / hashMap.length) * 100 + "% filled"); 677 | }*/ 678 | } 679 | 680 | dispose() { 681 | Object.keys(this.kernels).forEach(key => { 682 | this.kernels[key].dispose(); 683 | }) 684 | this.geometries.forEach(geom => geom.dispose); 685 | } 686 | } -------------------------------------------------------------------------------- /src/FEMPhysics/grid.js: -------------------------------------------------------------------------------- 1 | import {atomicFunc, Fn, instancedArray, instanceIndex, ivec3, uvec3, uint, int, float} from "three/tsl"; 2 | import {murmurHash13} from "./math.js"; 3 | 4 | export class Grid { 5 | cellsize = 1; 6 | type = ""; 7 | buffer = null; 8 | 9 | constructor(cellsize, type = "basic") { 10 | this.cellsize = cellsize; 11 | this.type = type; 12 | 13 | if (type === "basic") { 14 | this.gridsize1d = 80; 15 | this.gridsize = this.gridsize1d * this.gridsize1d * this.gridsize1d; 16 | } else if (type === "hash") { 17 | this.gridsize = 1048573; // biggest prime below 2^20 18 | } else { 19 | console.error("Unrecognized grid type"); 20 | } 21 | 22 | this.buffer = instancedArray(this.gridsize, "int").toAtomic(); 23 | 24 | this.clearKernel = Fn(() => { 25 | this.buffer.setAtomic(false); 26 | this.buffer.element(instanceIndex).assign(int(-1)); 27 | })().compute(this.gridsize); 28 | 29 | } 30 | 31 | async clearBuffer(renderer) { 32 | await renderer.computeAsync(this.clearKernel); 33 | } 34 | 35 | getElementFromIndex(ipos) { 36 | if (this.type === "basic") { 37 | const upos = uvec3(ipos.add(1073741823)).mod(this.gridsize1d).toVar(); 38 | const hash = upos.x.mul(this.gridsize1d*this.gridsize1d).add(upos.y.mul(this.gridsize1d)).add(upos.z).mod(this.gridsize).toVar("hash"); 39 | return this.buffer.element(hash); 40 | } else if (this.type === "hash") { 41 | const hash = murmurHash13(ipos).mod(uint(this.gridsize)).toVar("hash"); 42 | return this.buffer.element(hash); 43 | } 44 | } 45 | 46 | getElement(pos) { 47 | const ipos = ivec3(pos.div(this.cellsize).floor()); 48 | return this.getElementFromIndex(ipos); 49 | } 50 | 51 | setAtomic(value) { 52 | this.buffer.setAtomic(value); 53 | } 54 | } -------------------------------------------------------------------------------- /src/FEMPhysics/math.js: -------------------------------------------------------------------------------- 1 | import { 2 | abs, Break, 3 | cross, 4 | div, dot, 5 | float, 6 | Fn, If, 7 | int, length, 8 | Loop, 9 | mat3, 10 | mul, 11 | normalize, 12 | sin, 13 | uint, 14 | uvec3, 15 | vec2, 16 | vec3, 17 | vec4 18 | } from "three/tsl"; 19 | 20 | export const murmurHash13 = /*#__PURE__*/ Fn( ( [ src_immutable ] ) => { 21 | const src = uvec3( src_immutable.add(1073741823) ).toVar(); // int to uint 22 | const M = uint( int( 0x5bd1e995 ) ); 23 | const h = uint( uint( 1190494759 ) ).toVar(); 24 | src.mulAssign( M ); 25 | src.bitXorAssign( src.shiftRight( uvec3( 24 ) ) ); 26 | src.mulAssign( M ); 27 | h.mulAssign( M ); 28 | h.bitXorAssign( src.x ); 29 | h.mulAssign( M ); 30 | h.bitXorAssign( src.y ); 31 | h.mulAssign( M ); 32 | h.bitXorAssign( src.z ); 33 | h.bitXorAssign( h.shiftRight( uint( 13 ) ) ); 34 | h.mulAssign( M ); 35 | h.bitXorAssign( h.shiftRight( uint( 15 ) ) ); 36 | return h; 37 | } ).setLayout( { 38 | name: 'murmurHash13', 39 | type: 'uint', 40 | inputs: [ 41 | { name: 'src', type: 'ivec3' } 42 | ] 43 | } ); 44 | 45 | export const rotationToQuaternion = /*#__PURE__*/ Fn( ( [ axis_immutable, angle_immutable ] ) => { 46 | 47 | const angle = float( angle_immutable ).toVar(); 48 | const axis = vec3( axis_immutable ).toVar(); 49 | const half_angle = float( angle.mul( 0.5 ) ).toVar(); 50 | const s = vec2( sin( vec2( half_angle, half_angle.add( Math.PI * 0.5 ) ) ) ).toVar(); 51 | 52 | return vec4( axis.mul( s.x ), s.y ); 53 | 54 | } ).setLayout( { 55 | name: 'rotationToQuaternion', 56 | type: 'vec4', 57 | inputs: [ 58 | { name: 'axis', type: 'vec3' }, 59 | { name: 'angle', type: 'float' } 60 | ] 61 | } ); 62 | 63 | export const rotateByQuat = /*#__PURE__*/ Fn( ( [ pos_immutable, quat_immutable ] ) => { 64 | 65 | const quat = vec4( quat_immutable ).toVar(); 66 | const pos = vec3( pos_immutable ).toVar(); 67 | 68 | return pos.add( mul( 2.0, cross( quat.xyz, cross( quat.xyz, pos ).add( quat.w.mul( pos ) ) ) ) ); 69 | 70 | } ).setLayout( { 71 | name: 'rotateByQuat', 72 | type: 'vec3', 73 | inputs: [ 74 | { name: 'pos', type: 'vec3' }, 75 | { name: 'quat', type: 'vec4' } 76 | ] 77 | } ); 78 | 79 | export const quat_conj = /*#__PURE__*/ Fn( ( [ q_immutable ] ) => { 80 | 81 | const q = vec4( q_immutable ).toVar(); 82 | 83 | return normalize( vec4( q.x.negate(), q.y.negate(), q.z.negate(), q.w ) ); 84 | 85 | } ).setLayout( { 86 | name: 'quat_conj', 87 | type: 'vec4', 88 | inputs: [ 89 | { name: 'q', type: 'vec4' } 90 | ] 91 | } ); 92 | 93 | export const quat_mult = /*#__PURE__*/ Fn( ( [ q1_immutable, q2_immutable ] ) => { 94 | 95 | const q2 = vec4( q2_immutable ).toVar(); 96 | const q1 = vec4( q1_immutable ).toVar(); 97 | const qr = vec4().toVar(); 98 | qr.x.assign( q1.w.mul( q2.x ).add( q1.x.mul( q2.w ) ).add( q1.y.mul( q2.z ).sub( q1.z.mul( q2.y ) ) ) ); 99 | qr.y.assign( q1.w.mul( q2.y ).sub( q1.x.mul( q2.z ) ).add( q1.y.mul( q2.w ) ).add( q1.z.mul( q2.x ) ) ); 100 | qr.z.assign( q1.w.mul( q2.z ).add( q1.x.mul( q2.y ).sub( q1.y.mul( q2.x ) ) ).add( q1.z.mul( q2.w ) ) ); 101 | qr.w.assign( q1.w.mul( q2.w ).sub( q1.x.mul( q2.x ) ).sub( q1.y.mul( q2.y ) ).sub( q1.z.mul( q2.z ) ) ); 102 | 103 | return qr; 104 | 105 | } ).setLayout( { 106 | name: 'quat_mult', 107 | type: 'vec4', 108 | inputs: [ 109 | { name: 'q1', type: 'vec4' }, 110 | { name: 'q2', type: 'vec4' } 111 | ] 112 | } ); 113 | 114 | export const extractRotation = /*#__PURE__*/ Fn( ( [ A_immutable, q_immutable, steps = 3 ] ) => { 115 | 116 | const q = vec4( q_immutable ).toVar(); 117 | const A = mat3( A_immutable ).toVar(); 118 | 119 | Loop( { start: int( 0 ), end: steps, name: 'iter' }, ( { iter } ) => { 120 | 121 | const X = vec3( rotateByQuat( vec3( 1.0, 0.0, 0.0 ), q ) ).toVar(); 122 | const Y = vec3( rotateByQuat( vec3( 0.0, 1.0, 0.0 ), q ) ).toVar(); 123 | const Z = vec3( rotateByQuat( vec3( 0.0, 0.0, 1.0 ), q ) ).toVar(); 124 | const omega = vec3( cross( X, A.element( int( 0 ) ) ).add( cross( Y, A.element( int( 1 ) ) ) ).add( cross( Z, A.element( int( 2 ) ) ) ).mul( div( 1.0, abs( dot( X, A.element( int( 0 ) ) ).add( dot( Y, A.element( int( 1 ) ) ) ).add( dot( Z, A.element( int( 2 ) ) ) ).add( 0.000000001 ) ) ) ) ).toVar(); 125 | const w = float( length( omega ) ).toVar(); 126 | 127 | If( w.lessThan( 0.000000001 ), () => { 128 | Break(); 129 | } ); 130 | 131 | q.assign( quat_mult( rotationToQuaternion( omega.div( w ), w ), q ) ); 132 | 133 | } ); 134 | 135 | return q; 136 | 137 | } ).setLayout( { 138 | name: 'extractRotation', 139 | type: 'vec4', 140 | inputs: [ 141 | { name: 'A', type: 'mat3' }, 142 | { name: 'q', type: 'vec4' }, 143 | { name: 'steps', type: 'int' } 144 | ] 145 | } ); 146 | 147 | -------------------------------------------------------------------------------- /src/FEMPhysics/softbodyGeometry.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three/webgpu"; 2 | import { 3 | attribute, 4 | cross, 5 | float, 6 | Fn, 7 | mul, 8 | transformNormalToView, 9 | varying, 10 | vec3, 11 | vec4 12 | } from "three/tsl"; 13 | import {rotateByQuat} from "./math.js"; 14 | import {SoftbodyInstance} from "./softbodyInstance.js"; 15 | 16 | export class SoftbodyGeometry { 17 | physics = null; 18 | model = 0; 19 | instances = []; 20 | 21 | constructor(physics, model, materialClass) { 22 | this.physics = physics; 23 | this.model = model; 24 | 25 | this.createMaterial(materialClass); 26 | } 27 | 28 | createGeometry() { 29 | const { attachedTets, baryCoords, positions, normals, uvs, indices, tetVerts, tetIds } = this.model; 30 | const instanceCount = this.instances.length; 31 | if (instanceCount === 0) { return; } 32 | 33 | const vertexCount = attachedTets.length; 34 | const positionArray = new Float32Array(positions); 35 | const normalArray = new Float32Array(normals); 36 | const uvArray = new Float32Array(uvs); 37 | const tetIdArray = new Uint32Array(vertexCount); 38 | const vertexIdArray = new Uint32Array(vertexCount * 4); 39 | const tetBaryCoordsArray = new Float32Array(baryCoords); 40 | 41 | const instanceDataArray = new Uint32Array(instanceCount * 3); // x: objectId, y: tetOffset, z: vertexOffset 42 | 43 | for (let i=0; i i.id < totalCount && i.spawned).length; 92 | this.geometry.instanceCount = count; 93 | } 94 | 95 | addInstance() { 96 | const instance = new SoftbodyInstance(this.physics, this); 97 | this.instances.push(instance); 98 | return instance; 99 | } 100 | 101 | async bake() { 102 | this.createGeometry(); 103 | } 104 | 105 | createMaterial(materialClass) { 106 | const material = new materialClass(); 107 | 108 | const vNormal = varying(vec3(0), "v_normalView"); 109 | const vDistance = varying(float(0), "v_distance"); 110 | material.positionNode = Fn(() => { 111 | const objectId = attribute("instanceData").x; 112 | const tetOffset = attribute("instanceData").y; 113 | const vertexOffset = attribute("instanceData").z; 114 | 115 | const tetId = attribute("tetId").add(tetOffset); 116 | 117 | const vertexIds = attribute("vertexIds").add(vertexOffset).toVar(); 118 | const baryCoords = attribute("tetBaryCoords"); 119 | const v0 = this.physics.vertexBuffer.get(vertexIds.x, "position").xyz.toVar(); 120 | const v1 = this.physics.vertexBuffer.get(vertexIds.y, "position").xyz.toVar(); 121 | const v2 = this.physics.vertexBuffer.get(vertexIds.z, "position").xyz.toVar(); 122 | const v3 = this.physics.vertexBuffer.get(vertexIds.w, "position").xyz.toVar(); 123 | const quat = this.physics.tetBuffer.get(tetId, "quat"); 124 | 125 | const normal = rotateByQuat(attribute("normal"), quat); 126 | vNormal.assign(transformNormalToView(normal)); 127 | vDistance.assign(attribute("position").length()); 128 | 129 | const a = v1.sub(v0).mul(baryCoords.x); 130 | const b = v2.sub(v0).mul(baryCoords.y); 131 | const c = v3.sub(v0).mul(baryCoords.z); 132 | const position = a.add(b).add(c).add(v0).toVar(); 133 | 134 | /*const positionInitial = attribute("position"); 135 | const scale = this.physics.uniforms.scales.element(objectId).toVar(); 136 | 137 | position.subAssign(positionInitial.mul(scale.oneMinus()));*/ 138 | return position; 139 | })(); 140 | 141 | this.material = material; 142 | } 143 | 144 | dispose() { 145 | this.geometry.dispose(); 146 | this.material.dispose(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/FEMPhysics/softbodyInstance.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three/webgpu"; 2 | 3 | export class SoftbodyInstance { 4 | physics = null; 5 | vertices = []; 6 | tets = []; 7 | age = 0; 8 | spawned = false; 9 | outOfSight = false; 10 | 11 | constructor(physics, geometry) { 12 | this.physics = physics; 13 | this.geometry = geometry; 14 | 15 | const params = this.physics._addObject(this); 16 | 17 | this.id = params.id; 18 | this.tetOffset = params.tetStart; 19 | this.vertexOffset = params.vertexStart; 20 | 21 | this.createTetrahedralGeometry(); 22 | } 23 | 24 | createTetrahedralGeometry() { 25 | const { tetVerts, tetIds } = this.geometry.model; 26 | for (let i=0; i < tetVerts.length; i += 3) { 27 | const x = tetVerts[i]*3; 28 | const y = tetVerts[i+1]*3; 29 | const z = tetVerts[i+2]*3; 30 | const vertex = this.physics.addVertex(this.id,x,y,z); 31 | this.vertices.push(vertex); 32 | } 33 | for (let i=0; i < tetIds.length; i += 4) { 34 | const a = this.vertices[tetIds[i]]; 35 | const b = this.vertices[tetIds[i+1]]; 36 | const c = this.vertices[tetIds[i+2]]; 37 | const d = this.vertices[tetIds[i+3]]; 38 | this.tets.push(this.physics.addTet(this.id,a,b,c,d)); 39 | } 40 | } 41 | 42 | async reset(position) { 43 | const scale = new THREE.Vector3(1,1,1); 44 | const quaternion = new THREE.Quaternion().random(); 45 | //if (!this.geometry.material.iridescence) { quaternion.random(); } 46 | 47 | //const velocity = new THREE.Vector3(0,-0.005,0.03); 48 | const velocity = new THREE.Vector3(0,0, 0); 49 | await this.physics.resetObject(this.id, position, quaternion, scale, velocity); 50 | this.age = 0; 51 | //this.object.visible = true; 52 | this.spawned = true; 53 | this.outOfSight = false; 54 | this.geometry.updateCount(); 55 | } 56 | 57 | async update(interval) { 58 | this.age += interval; 59 | const position = this.physics.getPosition(this.id); 60 | this.outOfSight = (!this.spawned || position.z > 70); 61 | } 62 | }; -------------------------------------------------------------------------------- /src/FEMPhysics/structuredArray.js: -------------------------------------------------------------------------------- 1 | import {struct, instancedArray} from "three/tsl"; 2 | 3 | const TYPES = { 4 | int: { size: 1, alignment: 1, isFloat: false }, 5 | uint: { size: 1, alignment: 1, isFloat: false }, 6 | float: { size: 1, alignment: 1, isFloat: true }, 7 | 8 | vec2: { size: 2, alignment: 2, isFloat: true }, 9 | ivec2: { size: 2, alignment: 2, isFloat: false }, 10 | uvec2: { size: 2, alignment: 2, isFloat: false }, 11 | 12 | vec3: { size: 3, alignment: 4, isFloat: true }, 13 | ivec3: { size: 3, alignment: 4, isFloat: false }, 14 | uvec3: { size: 3, alignment: 4, isFloat: false }, 15 | 16 | vec4: { size: 4, alignment: 4, isFloat: true }, 17 | ivec4: { size: 4, alignment: 4, isFloat: false }, 18 | uvec4: { size: 4, alignment: 4, isFloat: false }, 19 | 20 | mat2: { size: 4, alignment: 2, isFloat: true }, 21 | mat3: { size: 12, alignment: 4, isFloat: true }, 22 | mat4: { size: 16, alignment: 4, isFloat: true }, 23 | }; 24 | 25 | export class StructuredArray { 26 | structNode = null; 27 | buffer = null; 28 | layout = null; 29 | structSize = 0; 30 | 31 | constructor(layout, length, label) { 32 | this.layout = this._parse(layout); 33 | this.length = length; 34 | this.structNode = struct(this.layout); 35 | this.floatArray = new Float32Array(this.structSize * this.length); 36 | this.intArray = new Int32Array(this.floatArray.buffer); 37 | this.buffer = instancedArray(this.floatArray, this.structNode).label(label); 38 | } 39 | 40 | set(index, element, value) { 41 | const member = this.layout[element]; 42 | if (!member) { 43 | return console.error("Unknown element '" + element + "'"); 44 | } 45 | const offset = index * this.structSize + member.offset; 46 | const array = member.isFloat ? this.floatArray : this.intArray; 47 | 48 | if (member.size === 1) { 49 | if (typeof value !== 'number') { 50 | return console.error("Expected a Number value for element '" + element + "'"); 51 | } 52 | array[offset] = value; 53 | } 54 | if (member.size > 1) { 55 | if (typeof value === 'object' && !Array.isArray(value)) { 56 | const obj = value; 57 | value = [obj.x, obj.y || 0, obj.z || 0, obj.w || 0]; 58 | } 59 | if (!Array.isArray(value) || value.length < member.size) { 60 | return console.error("Expected an array of length " + member.size + " for element '" + element + "'"); 61 | } 62 | for (let i = 0; i < member.size; i++) { 63 | array[offset + i] = value[i]; 64 | } 65 | } 66 | } 67 | 68 | get(index, element) { 69 | return this.buffer.element(index).get(element); 70 | } 71 | 72 | _parse(layout) { 73 | let offset = 0; 74 | const parsedLayout = {}; 75 | 76 | const keys = Object.keys(layout); 77 | for (let i = 0; i < keys.length; i++) { 78 | const key = keys[i]; 79 | let member = layout[key]; 80 | if (typeof member === 'string' || member instanceof String) { 81 | member = { type: member }; 82 | } 83 | const type = member.type; 84 | if (!TYPES[type]) { 85 | return console.error("Unknown type '" + type + "'"); 86 | } 87 | const { size, alignment, isFloat } = TYPES[type]; 88 | member.size = size; 89 | member.isFloat = isFloat; 90 | 91 | const rest = offset % alignment; 92 | if (rest !== 0) { 93 | offset += (alignment - rest); 94 | } 95 | member.offset = offset; 96 | offset += size; 97 | 98 | parsedLayout[key] = member; 99 | } 100 | 101 | const rest = offset % 4; 102 | if (rest !== 0) { 103 | offset += (4 - rest); 104 | } 105 | 106 | this.structSize = offset; 107 | return parsedLayout; 108 | } 109 | }; -------------------------------------------------------------------------------- /src/FEMPhysics/tetVisualizer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three/webgpu"; 2 | import {Fn, instancedArray, instanceIndex, attribute} from "three/tsl"; 3 | 4 | export class TetVisualizer { 5 | physics = null; 6 | constructor(physics) { 7 | this.physics = physics; 8 | this.vertexMaterial = new THREE.SpriteNodeMaterial(); 9 | this.vertexMaterial.positionNode = Fn(() => { 10 | return this.physics.vertexBuffer.get(instanceIndex, "position"); 11 | })(); 12 | this.vertexObject = new THREE.Mesh(new THREE.PlaneGeometry(0.01, 0.01), this.vertexMaterial); 13 | this.vertexObject.count = this.physics.vertexCount; 14 | this.vertexObject.frustumCulled = false; 15 | 16 | const tetPositionBuffer = new THREE.BufferAttribute(new Float32Array(new Array(36).fill(0)), 3, false); 17 | const tetIndexBuffer = new THREE.BufferAttribute(new Int32Array([0,1,0,2,0,3,1,2,1,3,2,3]), 1, false); 18 | 19 | this.tetMaterial = new THREE.LineBasicNodeMaterial({ color: 0x000000 }); 20 | this.tetMaterial.positionNode = Fn( () => { 21 | const vertices = this.physics.tetBuffer.get(instanceIndex, "vertexIds"); 22 | const tetIndex = attribute('vertexIndex'); 23 | const vertexId = vertices.element(tetIndex); 24 | return this.physics.vertexBuffer.get(vertexId, "position"); 25 | } )(); 26 | 27 | const tetGeometry = new THREE.InstancedBufferGeometry(); 28 | tetGeometry.setAttribute("position", tetPositionBuffer); 29 | tetGeometry.setAttribute("vertexIndex", tetIndexBuffer); 30 | tetGeometry.instanceCount = this.physics.tetCount; 31 | 32 | this.tetObject = new THREE.Line(tetGeometry, this.tetMaterial); 33 | this.tetObject.frustumCulled = false; 34 | 35 | this.object = new THREE.Object3D(); 36 | this.object.add(this.vertexObject); 37 | this.object.add(this.tetObject); 38 | } 39 | 40 | dispose() { 41 | this.vertexMaterial.dispose(); 42 | this.vertexObject.geometry.dispose(); 43 | this.tetMaterial.dispose(); 44 | this.tetObject.geometry.dispose(); 45 | } 46 | } -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three/webgpu"; 2 | import { OrbitControls } from "three/examples/jsm/controls/OrbitControls" 3 | import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'; 4 | 5 | import {Lights} from "./lights"; 6 | 7 | import hdri from "./assets/autumn_field_puresky_1k.hdr"; 8 | 9 | import { 10 | dot, float, 11 | Fn, 12 | mix, 13 | normalView, 14 | normalWorld, 15 | pmremTexture, 16 | smoothstep, texture, uv, normalMap, 17 | varying, vec2, 18 | vec3, instanceIndex, 19 | mx_hsvtorgb 20 | } from "three/tsl"; 21 | import {FEMPhysics} from "./FEMPhysics/FEMPhysics"; 22 | import {TetVisualizer} from "./FEMPhysics/tetVisualizer"; 23 | import CollisionGeometry from "./collisionGeometry"; 24 | 25 | import virus from './geometry/virus'; 26 | import skull from './geometry/skull3'; 27 | import icosphere from './geometry/icosphere'; 28 | 29 | import normalMapFileVirus from './geometry/textures/virus_normal.png'; 30 | import roughnessMapFileVirus from './geometry/textures/virus_roughness.jpg'; 31 | import colorMapFileSkull from './geometry/textures/skullColor.jpg'; 32 | import normalMapFileSkull from './geometry/textures/skullNormal.png'; 33 | import roughnessMapFileSkull from './geometry/textures/skullRoughness.jpg'; 34 | 35 | import normalMapFileRope from './geometry/textures/fabrics_0066_normal_opengl_1k.png'; 36 | import roughnessMapFileRope from './geometry/textures/fabrics_0066_roughness_1k.jpg'; 37 | import colorMapFileRope from './geometry/textures/fabrics_0066_color_1k.jpg'; 38 | import aoMapFileRope from './geometry/textures/fabrics_0066_ao_1k.jpg'; 39 | 40 | /*import earthColorFile from './geometry/textures/2k_earth_daymap.jpg'; 41 | import earthNormalFile from './geometry/textures/2k_earth_normal_map.png'; 42 | import earthSpecularFile from './geometry/textures/2k_earth_specular_map.png';*/ 43 | 44 | import {conf} from "./conf"; 45 | import {Info} from "./info"; 46 | import {generateTube} from "./geometry/loadModel"; 47 | 48 | const rope = generateTube(25); 49 | const longRope = generateTube(500); 50 | 51 | const loadHdr = async (file) => { 52 | const texture = await new Promise(resolve => { 53 | new RGBELoader().load(file, result => { resolve(result); }); 54 | }); 55 | return texture; 56 | } 57 | const textureLoader = new THREE.TextureLoader(); 58 | 59 | class App { 60 | renderer = null; 61 | 62 | camera = null; 63 | 64 | scene = null; 65 | 66 | controls = null; 67 | 68 | lights = null; 69 | 70 | stats = null; 71 | 72 | physics = null; 73 | 74 | softbodies = []; 75 | 76 | softbodyCount = 10; 77 | 78 | lastSoftbody = 0; 79 | 80 | wireframe = false; 81 | 82 | lastPositions = []; 83 | 84 | textures = { 85 | ropeNormal: normalMapFileRope, 86 | ropeColor: colorMapFileRope, 87 | ropeRoughness: roughnessMapFileRope, 88 | ropeAo: aoMapFileRope, 89 | virusNormal: normalMapFileVirus, 90 | virusRoughness: roughnessMapFileVirus, 91 | skullColor: colorMapFileSkull, 92 | skullRoughness: roughnessMapFileSkull, 93 | skullNormal: normalMapFileSkull, 94 | }; 95 | 96 | constructor(renderer) { 97 | this.renderer = renderer; 98 | } 99 | 100 | async init(progressCallback) { 101 | conf.init(); 102 | this.info = new Info(); 103 | 104 | const texturePromises = Object.keys(this.textures).map(key => { 105 | const file = this.textures[key]; 106 | return new Promise(resolve => { 107 | textureLoader.load(file, texture => { 108 | texture.wrapS = THREE.RepeatWrapping; 109 | texture.wrapT = THREE.RepeatWrapping; 110 | this.textures[key] = texture; 111 | resolve(); 112 | }); 113 | }); 114 | }); 115 | await Promise.all(texturePromises); 116 | await progressCallback(0.2) 117 | this.textures.hdri = await loadHdr(hdri); 118 | await progressCallback(0.3) 119 | 120 | this.sceneName = conf.scene; 121 | await this.setupScene(progressCallback); 122 | 123 | this.raycaster = new THREE.Raycaster(); 124 | this.renderer.domElement.addEventListener("pointerdown", (event) => { this.onPointerDown(event); }); 125 | 126 | await progressCallback(1.0, 100); 127 | } 128 | 129 | async setupScene(progressCallback) { 130 | this.softbodyCount = conf.maxBodies; 131 | this.wireframe = false; 132 | 133 | this.camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.01, 120); 134 | this.camera.position.set(40,0,24); 135 | this.camera.lookAt(0,0,0); 136 | this.camera.updateProjectionMatrix() 137 | 138 | this.scene = new THREE.Scene(); 139 | 140 | this.controls = new OrbitControls(this.camera, this.renderer.domElement); 141 | this.controls.enableDamping = true; 142 | this.controls.enablePan = false; 143 | this.controls.minDistance = 20; 144 | this.controls.maxDistance = 100; 145 | this.controls.minPolarAngle = 0.2 * Math.PI; 146 | this.controls.maxPolarAngle = 0.8 * Math.PI; 147 | 148 | this.scene.backgroundNode = pmremTexture(this.textures.hdri, normalWorld); 149 | this.scene.environmentNode = pmremTexture(this.textures.hdri, normalWorld).mul(0.5); 150 | 151 | this.renderer.toneMapping = THREE.ACESFilmicToneMapping; 152 | this.renderer.toneMappingExposure = 0.5; 153 | this.renderer.shadowMap.enabled = true; 154 | this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; 155 | 156 | this.lights = new Lights(); 157 | this.scene.add(this.lights.object); 158 | 159 | this.physics = new FEMPhysics(this.renderer); 160 | this.scene.add(this.physics.object); 161 | 162 | 163 | const ropeGeometry = this.physics.addGeometry(rope) 164 | const longRopeGeometry = this.physics.addGeometry(longRope) 165 | const virusGeometry = this.physics.addGeometry(virus); 166 | const skullGeometry = this.physics.addGeometry(skull); 167 | const sphereGeometry = this.physics.addGeometry(icosphere); 168 | await progressCallback(0.5) 169 | 170 | { 171 | [ropeGeometry, longRopeGeometry].forEach((geometry) => { 172 | geometry.material.normalMap = this.textures.ropeNormal; 173 | geometry.material.roughnessMap = this.textures.ropeRoughness; 174 | geometry.material.aoMap = this.textures.ropeAo; 175 | geometry.material.map = this.textures.ropeColor; 176 | geometry.material.normalScale = new THREE.Vector2(3,3); 177 | }); 178 | } 179 | { 180 | /*const mapFiles = [earthColorFile, earthNormalFile, earthSpecularFile]; 181 | const [ colorMap, normalMapTexture, specularMap ] = await Promise.all(mapFiles.map(f => loadTexture(f))); 182 | sphereGeometry.material.normalNode = normalMap(texture(normalMapTexture), vec2(3,3)); 183 | sphereGeometry.material.roughnessNode = texture(specularMap).oneMinus(); 184 | sphereGeometry.material.colorNode = texture(colorMap);*/ 185 | sphereGeometry.material.metalness = 0.39; 186 | sphereGeometry.material.roughness = 0.45; 187 | sphereGeometry.material.color = new THREE.Color(0,0.8,1); 188 | } 189 | { 190 | virusGeometry.material.normalMap = this.textures.virusNormal; 191 | virusGeometry.material.roughnessMap = this.textures.virusRoughness; 192 | virusGeometry.material.metalness = 0.4; 193 | virusGeometry.material.iridescence = 1.0; 194 | virusGeometry.material.color = 0xFFAAFF; 195 | virusGeometry.material.normalScale = new THREE.Vector2(3,3); 196 | 197 | let color, colorHighlight; 198 | 199 | if (this.sceneName === "viruses") { 200 | color = mx_hsvtorgb(vec3(float(instanceIndex).mul(0.07), 0.99, 0.25 )); 201 | colorHighlight = mx_hsvtorgb(vec3(float(instanceIndex).mul(0.07).add(0.1), 0.99, 0.9 )); 202 | } else { 203 | color = vec3(0.25,0,0.25); 204 | colorHighlight = vec3(1,0,0.5); 205 | } 206 | 207 | virusGeometry.material.colorNode = color; 208 | 209 | const vDistance = varying(float(0), "v_distance"); 210 | virusGeometry.material.emissiveNode = Fn(() => { 211 | const dp = dot(vec3(0,0,1), normalView).max(0).pow(4); 212 | const of = mix(0.0, 1.0, smoothstep(1.3,1.6, vDistance)); 213 | return dp.mul(of).mul(colorHighlight); 214 | })(); 215 | } 216 | { 217 | skullGeometry.material.map = this.textures.skullColor; 218 | skullGeometry.material.normalMap = this.textures.skullNormal; 219 | skullGeometry.material.roughnessMap = this.textures.skullRoughness; 220 | skullGeometry.material.metalness = 1.0; 221 | skullGeometry.material.iridescence = 1.0; 222 | } 223 | let geometries = []; 224 | switch (this.sceneName) { 225 | case "mixed": 226 | geometries = [virusGeometry, skullGeometry, sphereGeometry, ropeGeometry, ropeGeometry, ropeGeometry, ropeGeometry, ropeGeometry, ropeGeometry, ropeGeometry]; 227 | break; 228 | case "spheres": 229 | geometries = [sphereGeometry]; 230 | break; 231 | case "skulls": 232 | geometries = [skullGeometry]; 233 | break; 234 | case "viruses": 235 | geometries = [virusGeometry]; 236 | break; 237 | case "ropes": 238 | geometries = [ropeGeometry]; 239 | break; 240 | case "longropes": 241 | geometries = [longRopeGeometry]; 242 | break; 243 | } 244 | 245 | this.softbodies = []; 246 | for (let i=0; i { 301 | return p.distanceTo(position) < 8; 302 | }) 303 | if (!nearby) { 304 | found = true; 305 | this.lastPositions.push(position); 306 | if (this.lastPositions.length > 20) { 307 | this.lastPositions.shift(); 308 | } 309 | } 310 | } 311 | return position; 312 | } 313 | 314 | async update(delta, elapsed) { 315 | conf.begin(); 316 | 317 | const { wireframe, bodies, scene } = conf; 318 | 319 | if (this.sceneName !== scene) { 320 | this.clear(); 321 | this.sceneName = scene; 322 | await this.setupScene(() => {}); 323 | } 324 | 325 | if (wireframe !== this.wireframe) { 326 | this.wireframe = wireframe; 327 | this.physics.object.visible = !wireframe; 328 | this.tetVisualizer.object.visible = wireframe; 329 | } 330 | 331 | this.controls.update(delta); 332 | 333 | this.lastSoftbody += delta; 334 | if (this.lastSoftbody > 0.15) { 335 | const nextSoftbody = this.softbodies.find((sb, index) => (index < bodies && sb.outOfSight)); 336 | if (nextSoftbody) { 337 | this.lastSoftbody = Math.random() * -0.0; 338 | const position = this.getRandomPosition(); 339 | await nextSoftbody.reset(position); 340 | } 341 | } 342 | this.lights.update(elapsed); 343 | await this.physics.update(delta, elapsed); 344 | 345 | await this.renderer.renderAsync(this.scene, this.camera); 346 | 347 | conf.end(); 348 | } 349 | } 350 | export default App; 351 | -------------------------------------------------------------------------------- /src/assets/autumn_field_puresky_1k.hdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtsetio/softbodies/8b3289ac8551a5cbe9f47982305b0867593e4926/src/assets/autumn_field_puresky_1k.hdr -------------------------------------------------------------------------------- /src/assets/rock_0005_ao_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtsetio/softbodies/8b3289ac8551a5cbe9f47982305b0867593e4926/src/assets/rock_0005_ao_1k.jpg -------------------------------------------------------------------------------- /src/assets/rock_0005_color_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtsetio/softbodies/8b3289ac8551a5cbe9f47982305b0867593e4926/src/assets/rock_0005_color_1k.jpg -------------------------------------------------------------------------------- /src/assets/rock_0005_normal_opengl_1k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtsetio/softbodies/8b3289ac8551a5cbe9f47982305b0867593e4926/src/assets/rock_0005_normal_opengl_1k.png -------------------------------------------------------------------------------- /src/assets/rock_0005_roughness_1k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/holtsetio/softbodies/8b3289ac8551a5cbe9f47982305b0867593e4926/src/assets/rock_0005_roughness_1k.jpg -------------------------------------------------------------------------------- /src/collisionGeometry.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three/webgpu"; 2 | import { 3 | vec3, 4 | vec4, 5 | Fn, smoothstep, positionView, positionWorld, 6 | } from "three/tsl"; 7 | 8 | class CollisionGeometry { 9 | constructor(physics) { 10 | this.physics = physics; 11 | this.object = new THREE.Object3D(); 12 | } 13 | 14 | async createGeometry() { 15 | const collider = (positionImmutable) => { 16 | const position = vec3(positionImmutable).toVar(); 17 | //position.addAssign(vec3(0,-20,10)); 18 | const normal = position.normalize(); 19 | const length = position.length(); 20 | const dist = length.sub(5); //float(25).sub(length).mul(step(100, length).oneMinus()); 21 | return vec4( normal, dist ); 22 | }; 23 | this.physics.addCollider(collider); 24 | const ball = new THREE.Mesh(new THREE.IcosahedronGeometry(5, 3), new THREE.MeshStandardNodeMaterial( 25 | {color: new THREE.Color(1,1,1), metalness: 0.1, roughness:0.8} 26 | )); 27 | ball.castShadow = true; 28 | ball.receiveShadow = true; 29 | this.object.add(ball); 30 | } 31 | 32 | update(delta, elapsed) { 33 | 34 | } 35 | 36 | dispose() { 37 | 38 | } 39 | } 40 | export default CollisionGeometry; 41 | -------------------------------------------------------------------------------- /src/conf.js: -------------------------------------------------------------------------------- 1 | import {Pane} from 'tweakpane'; 2 | import * as EssentialsPlugin from '@tweakpane/plugin-essentials'; 3 | import mobile from "is-mobile"; 4 | 5 | const isMobile = mobile(); 6 | 7 | class Conf { 8 | gui = null; 9 | 10 | wireframe = false; 11 | 12 | stepsPerSecond = 180; 13 | 14 | bodies = (isMobile ? 30 : 100); 15 | 16 | maxBodies = 300; 17 | 18 | scene = 'mixed'; 19 | 20 | constructor() { 21 | 22 | } 23 | 24 | init() { 25 | const gui = new Pane() 26 | gui.registerPlugin(EssentialsPlugin); 27 | 28 | const stats = gui.addFolder({ 29 | title: "stats", 30 | expanded: false, 31 | }); 32 | this.fpsGraph = stats.addBlade({ 33 | view: 'fpsgraph', 34 | label: 'fps', 35 | rows: 2, 36 | }); 37 | 38 | const settings = gui.addFolder({ 39 | title: "settings", 40 | expanded: false, 41 | }); 42 | 43 | const scenes = { 44 | mixed: { min: 10, max: 300, default: 100, text: "mixed" }, 45 | spheres: { min: 10, max: 200, default: 50, text: "only spheres" }, 46 | skulls: { min: 10, max: 200, default: 50, text: "only skulls" }, 47 | viruses: { min: 10, max: 100, default: 30, text: "only viruses" }, 48 | ropes: { min: 30, max: 500, default: 100, text: "only ropes" }, 49 | longropes: { min: 3, max: 100, default: 10, text: "looooong ropes" }, 50 | }; 51 | 52 | settings.addBlade({ 53 | view: 'list', 54 | label: 'scene', 55 | options: Object.keys(scenes).map(key => ({ ...scenes[key], value: key })), 56 | value: 'mixed', 57 | }).on('change', (ev) => { 58 | const params = scenes[ev.value]; 59 | this.bodies = Math.round(params.default * (isMobile ? 0.3 : 1.0)); 60 | this.maxBodies = params.max; 61 | this.bodiesBinding.min = params.min; 62 | this.bodiesBinding.max = params.max; 63 | this.scene = ev.value; 64 | gui.refresh(); 65 | }); 66 | 67 | this.bodiesBinding = settings.addBinding(this, "bodies", { min: 20, max: this.maxBodies, step: 10 }); 68 | settings.addBinding(this, "stepsPerSecond", { min: 120, max: 300, step: 60 }); 69 | //settings.addBinding(this, "wireframe"); 70 | 71 | this.settings = settings; 72 | this.gui = gui; 73 | } 74 | 75 | update() { 76 | } 77 | 78 | begin() { 79 | this.fpsGraph.begin(); 80 | } 81 | end() { 82 | this.fpsGraph.end(); 83 | } 84 | } 85 | export const conf = new Conf(); -------------------------------------------------------------------------------- /src/geometry/icosphere.js: -------------------------------------------------------------------------------- 1 | const model = { 2 | tetVerts: [-0.9827,0.1663,-0.0702,-0.9876,-0.0505,0.1425,-0.982,-0.0749,-0.1711,-0.9721,-0.2171,-0.0767,-0.9562,0.2281,0.1797,-0.9291,-0.2861,0.2313,-0.9202,0.1211,0.3706,-0.9177,-0.391,0.0558,-0.9381,0.1386,-0.315,-0.92,-0.0365,-0.3885,-0.9203,0.3848,-0.0546,-0.8797,0.4375,0.182,-0.8995,-0.1119,0.4209,-0.8819,-0.2868,-0.372,-0.8664,-0.4826,-0.1217,-0.8421,-0.3704,0.3903,-0.815,-0.5482,0.1843,-0.8365,0.3527,0.4173,-0.8349,0.4423,-0.326,-0.8265,0.0518,-0.5594,-0.815,0.5564,-0.1563,-0.8005,0.0276,0.5975,-0.7829,0.2178,0.5819,-0.8166,0.2999,-0.4919,-0.7881,-0.0976,-0.0842,-0.7958,-0.1896,-0.5739,-0.7863,0.0808,-0.1051,-0.777,0.0999,0.1545,-0.7773,-0.1399,0.1087,-0.77,-0.5151,-0.374,-0.7337,0.3111,0.0311,-0.7442,-0.2709,-0.0915,-0.8063,0.5898,0.0293,-0.7302,-0.3919,0.5591,-0.7246,0.1387,-0.6741,-0.7717,0.5675,0.2845,-0.7128,-0.7005,0.0113,-0.736,-0.1208,-0.286,-0.75,-0.1826,0.6355,-0.7399,-0.3952,-0.5435,-0.6945,0.0968,0.3835,-0.6915,0.3039,0.2567,-0.7128,0.309,-0.191,-0.7029,-0.0729,-0.707,-0.6986,0.5622,-0.4407,-0.7132,0.1281,-0.3325,-0.7023,-0.1564,0.3434,-0.6839,-0.6743,-0.2757,-0.694,-0.3522,0.1727,-0.685,0.491,0.5368,-0.6434,-0.7264,0.2379,-0.7041,-0.5686,0.4232,-0.6631,-0.3279,-0.3,-0.6646,0.4236,-0.1326,-0.6343,0.7595,0.1383,-0.6572,0.7465,-0.0972,-0.6576,-0.4449,-0.0721,-0.634,-0.0856,-0.4772,-0.6207,0.1656,-0.4741,-0.6212,0.3486,-0.7007,-0.5985,0.685,0.4141,-0.5987,-0.3535,0.3911,-0.6044,0.3754,-0.3651,-0.5929,-0.0054,0.5336,-0.5889,-0.5275,0.1055,-0.6215,-0.2203,-0.4483,-0.6029,0.2121,0.7681,-0.5827,0.3085,0.449,-0.5497,0.5566,-0.1582,-0.601,-0.3525,-0.7165,-0.5827,0.5141,0.1804,-0.5568,-0.218,0.5291,-0.6132,-0.0107,0.7892,-0.5699,-0.5375,-0.6199,-0.5723,-0.409,0.7093,-0.5203,-0.1843,-0.8329,-0.5926,0.5333,0.0161,-0.5619,0.0503,-0.8251,-0.543,-0.5773,-0.0882,-0.5814,0.7361,-0.3443,-0.585,-0.6592,-0.4701,-0.528,-0.8338,0.1577,-0.5397,0.1789,0.5588,-0.5577,-0.8173,-0.1383,-0.5031,0.8032,0.3164,-0.528,-0.5312,-0.2738,-0.4964,0.1133,-0.6138,-0.5161,-0.6669,0.5357,-0.5174,-0.3319,-0.5076,-0.5141,-0.544,0.2754,-0.5069,0.505,0.3505,-0.4642,0.8773,-0.1149,-0.5049,-0.1391,-0.6011,-0.5157,0.3229,-0.5153,-0.4819,0.6598,-0.5752,-0.4852,0.4249,0.763,-0.4498,-0.4982,0.4301,-0.4758,0.2792,-0.8329,-0.464,-0.4844,-0.4311,-0.4544,0.5066,-0.4141,-0.4357,-0.8183,0.3735,-0.4825,-0.7883,-0.3797,-0.4566,0.0703,0.65,-0.4663,-0.1914,0.8631,-0.4565,0.6538,0.0324,-0.3959,0.6209,0.6752,-0.4272,-0.4887,0.7598,-0.4375,0.8851,0.1535,-0.4363,-0.6661,0.0419,-0.4165,-0.6543,-0.1856,-0.438,-0.1404,0.6522,-0.4334,-0.3751,0.5536,-0.4173,0.6475,-0.205,-0.4,0.2472,0.6472,-0.3992,0.461,0.5123,-0.4165,-0.6795,-0.6023,-0.3789,0.1151,0.9171,-0.356,0.0711,-0.9306,-0.3611,-0.7257,0.5844,-0.3492,-0.081,-0.7127,-0.3632,-0.6063,-0.3747,-0.3605,0.7912,0.4926,-0.2592,0.9622,0.0761,-0.3797,-0.9224,-0.0687,-0.352,-0.3128,-0.6433,-0.3124,0.6829,0.2698,-0.3176,0.2794,-0.677,-0.3044,0.1083,-0.7288,-0.3495,0.4961,-0.794,-0.3322,-0.3352,-0.8813,-0.3186,0.5842,0.4391,-0.3309,-0.6925,0.2203,-0.3217,-0.1392,-0.9358,-0.3672,0.8395,-0.3991,-0.3368,-0.3735,0.8633,-0.3153,-0.8053,-0.5014,-0.315,-0.8892,-0.3296,-0.332,-0.5404,-0.7721,-0.3086,0.4416,-0.5887,-0.3037,-0.506,-0.536,-0.3035,0.3124,0.8991,-0.2955,0.7026,-0.646,-0.2866,-0.7394,-0.1054,-0.2681,-0.6385,0.3975,-0.2966,0.6317,-0.3858,-0.2452,0.9543,-0.1654,-0.213,-0.1183,0.9693,-0.2777,0.1344,0.7345,-0.2576,0.3558,0.6653,-0.286,-0.2441,0.7029,-0.2862,0.737,-0.1162,-0.2567,-0.5201,0.5486,-0.2226,0.761,0.0822,-0.2611,-0.9493,0.1702,-0.1763,-0.7735,0.6078,-0.23,-0.3854,0.6592,-0.2355,0.5288,0.8144,-0.2079,0.5638,-0.5241,-0.2049,0.2074,-0.9559,-0.2442,-0.7087,-0.271,-0.2066,-0.5949,0.7757,-0.1836,-0.8893,0.4165,-0.1832,0.3654,-0.9117,-0.2084,-0.7673,0.0602,-0.2069,0.7209,-0.2728,-0.2066,-0.0575,0.7678,-0.1316,-0.2433,-0.7476,-0.1734,-0.5957,-0.5027,-0.1708,-0.9837,-0.0375,-0.166,-0.3946,0.9028,-0.1634,-0.4122,-0.6626,-0.2205,0.9148,0.3363,-0.1427,0.4082,-0.6701,-0.1456,0.669,-0.7278,-0.1328,0.7099,0.6904,-0.1325,0.8349,-0.5328,-0.1141,0.2191,-0.7584,-0.1698,-0.6991,-0.6939,-0.1368,-0.0091,-0.7847,-0.0733,-0.4847,-0.8708,-0.1193,0.4303,0.8942,-0.1382,0.4997,0.6055,-0.1371,-0.9554,-0.2584,-0.0979,-0.7456,0.265,-0.0979,0.7456,0.265,-0.1023,0.1797,0.9775,-0.095,0.6482,0.4558,-0.0834,0.0334,-0.9953,-0.082,0.9384,-0.3332,-0.057,-0.7146,-0.3497,-0.0725,0.9945,-0.066,-0.0657,-0.4393,0.6618,-0.0585,0.8257,0.5599,-0.0513,0.5193,-0.8523,-0.0711,-0.7845,-0.129,-0.0274,0.7876,-0.1206,-0.0595,0.9878,0.1381,-0.0838,-0.2673,0.7472,-0.0707,0.1003,0.7879,-0.0437,-0.8408,-0.5379,-0.0647,-0.2773,-0.9579,-0.0081,0.7473,-0.2755,-0.0643,-0.5813,0.5415,-0.0452,0.6456,-0.4668,-0.048,0.3024,0.7368,0.0199,0.9031,0.4269,0.019,-0.4505,-0.6583,0.0183,-0.6877,0.7249,-0.0343,-0.6855,0.406,0.0022,-0.9714,0.2332,-0.0206,-0.2842,0.9578,0.0078,0.5071,-0.6149,0.0195,-0.7929,0.0769,0.0143,0.7925,0.0807,0.0309,0.2965,-0.9536,0.0253,-0.6759,-0.7358,0.0455,-0.1039,0.7895,0.067,0.7657,-0.6387,0.035,-0.5222,0.8512,0.0465,-0.8049,0.5905,0.0473,-0.0777,-0.7923,0.0256,0.5908,0.8057,0.0424,-0.0437,0.9974,0.1593,0.1023,-0.9811,0.0711,-0.2909,-0.7397,0.0471,0.1212,-0.7866,0.1073,0.1067,0.7837,0.1113,-0.943,-0.3111,0.0895,-0.0899,-0.9911,0.07,0.5458,0.5767,0.0691,-0.9972,-0.0103,0.1551,-0.6959,-0.3563,0.0963,0.6636,0.4324,0.0405,0.3451,-0.7178,0.1062,0.965,0.2361,0.1076,-0.7759,-0.1484,0.1285,0.4113,0.9019,0.1104,-0.8933,0.4336,0.1126,-0.5011,0.6098,0.0656,-0.6073,-0.5139,0.1565,-0.7213,0.3024,0.134,0.6897,-0.3768,0.1393,0.9576,-0.2489,0.1248,-0.3415,0.7096,0.1258,-0.6204,0.4847,0.1877,0.7664,0.6131,0.2008,0.1995,0.9588,0.1733,0.6058,-0.7755,0.1111,0.7397,0.2746,0.1801,-0.7402,-0.6468,0.1691,0.3427,0.7001,0.1568,0.9869,-0.0084,0.192,0.4013,-0.8953,0.1995,0.8787,-0.4316,0.2497,0.8761,0.4105,0.1997,-0.5374,-0.8183,0.2488,0.7204,-0.2365,0.2067,0.7671,-0.069,0.1961,-0.3418,-0.9187,0.2815,0.1568,0.7292,0.2445,0.6329,0.4206,0.2186,-0.7695,0,0.2348,-0.2695,0.9329,0.2834,-0.8187,-0.4978,0.1647,0.5742,-0.5286,0.2157,-0.6444,0.7324,0.2514,-0.2045,-0.7287,0.2477,0.236,-0.7206,0.2467,-0.947,0.2032,0.2396,0.4279,-0.6296,0.2546,0.7459,0.1227,0.2546,-0.4476,-0.6088,0.2599,0.7626,-0.591,0.2914,-0.7235,-0.1642,0.2664,-0.0329,0.7508,0.23,0.0287,-0.7631,0.2772,0.2379,-0.9303,0.3061,-0.2111,0.7068,0.2933,-0.7103,0.2141,0.2814,-0.9534,-0.0994,0.2917,-0.7687,0.5679,0.339,-0.4121,0.5921,0.2974,-0.5657,-0.4812,0.3166,-0.5954,0.4244,0.3201,0.4733,0.5578,0.3151,-0.4859,0.8146,0.3258,0.6708,0.2836,0.2729,0.592,0.7572,0.35,0.5776,-0.4228,0.3777,0.3835,0.8423,0.3992,0.6207,-0.6734,0.3772,0.9224,0.0786,0.3771,-0.0158,0.9251,0.3619,-0.6514,-0.6654,0.3929,0.689,-0.0991,0.4176,-0.5809,0.6981,0.4029,-0.0754,-0.6835,0.4336,0.5504,0.3796,0.3987,-0.8665,-0.2977,0.3984,0.2701,0.6367,0.4062,-0.2918,-0.6204,0.4269,0.8349,-0.3454,0.4212,0.1275,-0.6674,0.4486,0.7306,0.5127,0.4157,-0.8253,0.38,0.4462,0.4202,-0.7892,0.4298,0.3441,-0.5759,0.3673,-0.0431,-0.928,0.4587,0.8466,0.2668,0.4248,0.8959,-0.1288,0.477,-0.3375,0.8105,0.4367,-0.6623,0.0782,0.4417,0.2015,-0.8736,0.4009,-0.4576,-0.7925,0.4607,-0.8771,0.1315,0.4584,0.0324,0.6524,0.4623,0.5954,-0.2597,0.4921,-0.6019,-0.1767,0.4833,-0.8699,-0.0886,0.5032,-0.2287,0.5745,0.4844,-0.4311,0.464,0.4961,0.5892,0.208,0.4742,0.6397,0.0577,0.4925,-0.4035,-0.4789,0.4855,-0.5304,-0.3462,0.5133,-0.5408,0.2824,0.4984,-0.2829,-0.8185,0.4992,0.3824,0.4898,0.5072,0.5188,0.6871,0.476,0.1368,0.8678,0.5606,0.021,-0.568,0.5222,-0.7414,-0.4194,0.5577,0.7018,-0.4413,0.5358,-0.6288,-0.562,0.5496,-0.1195,-0.826,0.5618,0.2368,0.5179,0.5753,0.3063,0.7572,0.6292,0.5868,0.5079,0.5515,-0.1947,-0.5421,0.5819,0.3984,-0.373,0.5942,0.5622,-0.5737,0.5711,-0.5489,0.0925,0.5639,0.8228,0.0621,0.594,0.4359,0.3036,0.5715,-0.6228,0.5331,0.5838,0.2215,-0.497,0.5926,0.5209,-0.1147,0.6044,-0.0566,0.5177,0.6013,-0.3965,0.6924,0.6279,-0.7758,0.0453,0.5955,0.78,-0.1875,0.6294,-0.4723,-0.6167,0.5773,0.0804,-0.8115,0.6422,-0.2516,0.4014,0.6403,0.4703,0.0764,0.6233,-0.3163,-0.3845,0.6665,0.4057,-0.624,0.6541,-0.7041,0.274,0.6641,-0.4421,0.0233,0.6611,-0.7267,-0.1821,0.6243,-0.4379,-0.2339,0.6554,-0.3744,0.2603,0.6404,0.2332,-0.7306,0.6714,0.2442,0.3547,0.6454,0.6947,0.3153,0.697,0.007,-0.3885,0.7297,0.665,0.1534,0.6907,0.3853,0.6109,0.7066,0.6165,-0.3449,0.6937,0.0183,0.3935,0.7013,-0.3676,-0.0955,0.692,0.2425,-0.3137,0.6893,-0.2293,-0.6863,0.7167,0.6908,-0.0875,0.7113,0.3451,-0.1016,0.7183,-0.6046,-0.3423,0.7325,0.1141,0.6703,0.7327,0.2924,0.1123,0.7327,-0.1948,-0.2513,0.7479,-0.1125,0.2545,0.7647,0.1212,0.1941,0.7331,-0.2754,0.1475,0.7636,0.1486,-0.1756,0.7711,-0.2131,0,0.7646,-0.0318,-0.6426,0.7783,0.4952,0.3848,0.7498,-0.1871,0.6347,0.7879,-0.0523,-0.1104,0.7484,-0.3652,0.5522,0.79,0.1072,-0.0103,0.7897,-0.0632,0.0928,0.7941,0.4066,-0.4496,0.7819,-0.6203,0.0464,0.8044,0.2002,-0.5583,0.8167,-0.4541,0.3545,0.8224,0.5221,-0.2237,0.847,-0.4015,-0.3458,0.8535,0.5159,0.0637,0.8353,-0.5143,0.19,0.8573,0.2629,0.4418,0.8615,-0.135,0.4889,0.8304,-0.2299,-0.507,0.8497,-0.5076,-0.1368,0.8912,-0.2457,0.3796,0.909,-0.0374,-0.4128,0.9061,0.3387,-0.2506,0.9037,0.1894,-0.3825,0.9214,0.0421,0.3847,0.9279,0.3695,-0.0253,0.9183,0.3272,0.2203,0.9301,-0.3642,0.0227,0.9576,-0.2115,0.1933,0.96,-0.1819,-0.2093,0.9747,0.0681,-0.2101,0.9821,0.1829,-0.0241,0.9817,0.0488,0.1811,0.9937,-0.1032,0.0011,0.3392,-0.6465,-0.3205,0.6192,-0.1304,0.773,0.4684,0.6902,0.3305], 3 | tetIds: [164,175,201,203,34,43,57,19,269,233,252,267,400,369,376,407,319,347,386,353,288,253,256,301,268,240,304,278,316,294,350,301,14,56,7,16,346,294,322,350,200,220,224,228,59,34,86,58,253,241,217,272,38,46,33,15,97,126,86,127,302,312,306,362,285,277,262,310,161,131,183,153,44,62,93,99,326,338,372,334,368,332,340,339,382,408,374,389,234,291,270,251,114,90,49,60,159,182,189,194,306,356,340,339,322,286,321,308,228,225,220,275,202,208,219,154,322,286,308,270,85,101,120,109,228,275,220,266,94,128,138,59,389,415,380,390,391,340,356,339,278,304,325,283,416,415,414,389,167,135,159,120,38,103,110,72,263,227,298,231,387,413,389,416,263,293,282,331,313,333,351,271,205,234,184,171,369,379,400,353,415,409,380,407,378,404,365,413,383,412,416,410,320,325,361,394,163,123,81,108,36,16,50,64,212,261,278,268,160,154,202,207,385,363,399,367,307,258,326,266,246,329,292,259,322,270,308,291,316,301,256,294,411,390,415,416,250,259,289,246,96,151,87,143,298,331,317,359,58,23,34,19,25,65,57,37,87,51,89,50,167,139,115,120,78,64,36,108,253,242,241,256,84,60,90,130,276,267,305,252,79,99,144,112,120,115,135,101,167,189,135,199,170,137,167,139,408,382,414,389,7,48,31,28,167,199,135,177,29,85,56,47,19,57,25,43,165,146,149,197,59,86,34,77,86,57,43,92,377,363,409,343,256,294,309,257,272,217,264,241,79,133,112,144,145,133,188,164,78,36,83,108,15,46,48,5,40,1,12,6,91,133,112,79,110,103,106,149,160,169,218,191,67,49,90,17,270,248,286,254,174,105,130,181,19,45,9,37,70,35,11,41,33,61,51,15,22,21,6,40,369,335,376,363,320,361,353,394,376,336,367,335,307,326,296,266,284,250,287,229,240,237,304,280,240,283,280,304,283,325,320,344,328,327,367,336,226,292,274,259,236,246,250,289,236,287,289,250,165,216,146,197,156,181,221,180,121,84,171,125,121,60,130,105,286,303,308,254,229,250,287,221,288,332,272,301,363,399,380,409,419,303,286,297,378,396,412,383,316,301,332,288,251,257,291,270,257,270,294,291,257,309,291,294,202,244,238,207,315,274,292,259,335,315,336,299,254,270,234,308,106,155,134,160,376,347,369,386,339,340,306,288,90,17,49,35,409,363,385,343,299,327,336,335,377,380,409,363,284,287,250,289,278,261,311,314,397,377,354,374,366,343,354,397,407,399,380,369,378,413,365,387,419,308,286,303,413,406,382,405,321,322,308,366,291,309,342,294,397,374,408,377,385,399,363,409,407,376,399,369,367,363,399,376,397,395,408,374,391,339,368,340,406,391,371,405,369,399,380,363,364,385,337,343,395,371,374,339,385,343,363,367,389,380,408,377,376,363,335,367,369,363,376,399,377,354,343,397,407,380,399,409,408,405,374,395,288,253,301,272,316,332,301,350,346,294,350,316,346,316,350,368,294,309,342,350,164,188,201,175,395,368,339,346,346,339,316,368,322,342,294,291,383,416,387,390,338,401,384,365,242,309,257,256,234,308,270,291,251,257,242,309,251,309,291,257,301,294,309,256,288,290,332,340,301,350,309,294,253,288,241,272,301,256,309,242,301,242,253,256,413,365,382,406,201,203,175,241,20,53,18,44,276,223,275,225,302,345,362,306,413,414,405,382,306,362,305,356,406,404,365,393,306,356,305,340,306,362,312,305,231,282,249,239,155,134,169,149,193,128,172,173,269,290,306,305,380,409,415,414,214,176,162,158,339,332,288,316,269,252,247,305,397,354,346,374,307,302,352,312,334,330,384,352,334,384,338,372,276,312,267,275,397,409,343,377,305,306,267,312,302,267,312,275,141,128,173,138,338,384,330,365,228,223,275,307,365,378,355,401,266,326,313,258,200,187,178,220,288,241,272,264,288,272,332,290,306,305,290,340,408,414,380,389,395,339,391,371,408,414,409,380,288,256,316,301,288,241,256,253,408,382,405,414,151,154,143,202,338,351,323,300,368,350,332,316,368,316,332,339,373,346,368,395,322,270,291,294,322,308,342,291,322,342,308,366,322,350,294,342,326,372,338,351,405,371,382,374,322,321,354,366,389,414,380,415,339,332,340,288,391,368,339,395,413,404,365,406,397,385,343,409,397,377,408,409,338,355,401,365,391,395,371,405,378,401,412,396,380,409,408,377,338,330,384,334,326,351,338,300,408,374,389,377,397,374,346,395,356,345,393,391,405,408,374,382,395,346,339,374,387,365,382,413,370,360,396,375,370,396,378,383,413,389,414,382,378,401,404,412,387,382,389,413,387,412,413,416,387,390,416,389,326,300,338,296,416,389,414,413,387,413,412,378,412,404,378,413,370,402,396,383,355,396,351,401,416,390,415,389,338,401,372,384,313,351,300,271,383,402,412,410,200,228,187,220,263,249,231,282,298,231,227,273,381,410,383,411,402,396,383,412,307,296,275,266,317,375,324,360,355,401,378,396,320,353,388,394,383,416,412,387,383,387,412,378,317,358,359,370,15,16,48,51,353,403,400,388,365,401,404,378,263,249,282,293,258,313,266,255,228,223,225,275,331,282,333,293,365,401,384,404,231,263,282,417,307,352,302,296,189,227,231,235,338,401,355,351,338,351,372,401,326,300,313,351,338,351,355,323,296,330,334,352,365,330,345,393,228,266,220,224,224,228,266,258,307,276,275,312,307,296,302,275,296,338,334,330,326,334,296,338,326,334,307,296,266,313,326,300,266,296,300,326,170,166,206,179,266,300,271,313,266,313,271,255,224,271,255,266,170,177,179,206,228,266,307,275,307,228,258,266,307,223,275,276,293,255,249,271,293,313,255,271,279,273,235,261,271,323,351,300,271,282,239,249,293,282,333,271,293,249,282,271,293,271,333,313,271,282,333,323,271,323,333,351,263,331,282,417,227,199,231,263,159,182,136,189,279,261,318,273,349,341,357,392,317,273,318,311,279,273,298,227,263,231,298,417,261,273,311,318,209,212,240,268,212,183,153,209,163,131,153,183,279,318,298,273,298,273,317,417,298,417,231,273,298,417,331,263,358,359,341,317,375,317,324,331,331,324,417,317,359,317,375,331,357,392,341,398,333,323,324,375,361,394,398,381,278,325,304,311,311,314,318,349,295,281,310,348,310,418,319,277,353,403,394,361,418,319,386,348,149,134,169,146,403,353,379,361,357,325,398,341,411,416,410,383,381,398,410,411,411,383,381,390,411,416,383,390,392,410,358,398,392,358,410,402,403,400,379,353,320,344,388,348,418,347,319,315,320,388,319,348,361,398,358,381,379,380,369,407,347,335,376,369,353,386,388,400,347,369,386,353,357,325,304,344,353,388,394,403,394,381,403,411,361,394,381,403,394,411,398,381,379,407,415,380,379,380,415,390,411,379,403,407,348,388,319,386,319,386,388,353,283,281,295,344,344,388,394,320,357,394,398,325,341,398,358,361,344,394,325,320,202,207,154,219,292,315,277,274,344,304,280,283,310,277,292,418,281,320,344,283,418,347,386,319,197,146,210,216,418,277,292,315,262,310,277,292,160,118,151,154,278,304,283,240,268,261,279,230,78,83,47,109,319,388,320,353,151,118,143,154,310,319,418,348,149,103,134,146,222,246,226,185,295,281,348,344,33,111,61,71,398,341,325,361,344,304,283,325,100,131,153,81,78,36,47,83,265,295,283,281,265,281,283,238,265,295,281,285,191,169,218,197,230,212,168,209,191,197,155,169,265,244,219,207,344,283,280,295,161,237,209,183,278,314,311,304,280,283,265,295,265,219,244,280,398,361,325,394,268,261,278,314,268,279,261,314,311,317,341,359,163,108,142,123,240,244,237,280,240,283,244,280,268,278,304,314,268,237,304,240,314,318,261,311,314,261,318,279,311,357,304,314,357,394,325,344,311,341,325,357,311,357,349,341,311,304,357,325,311,357,314,349,161,237,183,208,161,183,209,153,183,212,240,209,183,208,237,240,197,169,218,210,108,83,81,36,96,111,87,151,160,207,191,218,142,123,136,182,38,46,15,12,265,283,244,238,100,89,87,143,38,103,74,110,197,218,243,210,160,202,154,151,210,243,216,262,197,169,146,149,161,208,219,237,161,154,219,208,161,153,100,131,222,216,262,210,197,210,243,216,143,96,89,87,143,100,131,161,116,110,72,102,243,218,238,265,15,48,16,5,33,111,71,74,185,216,198,226,197,210,146,169,222,262,216,292,216,292,277,274,110,149,106,111,143,131,183,161,216,274,226,292,116,146,165,185,147,102,116,113,143,183,208,161,160,191,202,151,143,154,161,208,155,149,169,197,143,154,118,161,15,61,51,48,143,100,161,118,74,111,87,51,46,15,61,33,7,5,16,48,202,208,154,143,243,218,262,210,160,207,202,191,191,238,207,202,285,243,218,262,160,191,151,155,160,134,169,155,160,155,169,191,106,155,151,111,106,151,160,118,106,151,155,160,87,151,106,118,87,151,111,106,209,237,240,183,152,190,195,145,87,118,143,151,87,100,143,118,37,2,24,26,15,46,61,48,25,65,92,57,110,165,103,149,78,109,47,85,9,2,37,26,74,110,111,71,46,1,5,12,8,0,2,26,40,1,6,27,14,56,29,52,87,89,100,50,51,48,89,16,51,16,89,50,61,96,111,51,51,89,96,87,33,111,74,51,51,87,96,111,15,5,12,46,38,110,71,63,77,43,92,86,116,165,103,110,38,12,21,63,40,12,63,21,72,63,38,21,85,120,80,98,159,109,136,142,206,177,179,215,209,268,240,237,108,83,123,81,215,199,239,177,29,98,85,80,7,48,56,31,167,115,139,137,28,26,24,1,168,123,163,142,108,131,50,81,215,239,249,271,215,239,199,249,168,123,153,163,108,131,81,163,163,153,131,81,142,123,109,136,163,153,81,123,212,278,240,268,109,136,123,83,108,64,50,89,108,50,36,81,108,89,50,131,108,64,36,50,142,123,108,109,123,108,109,83,206,167,177,239,212,153,183,163,159,142,136,182,168,163,194,142,168,182,142,194,230,212,261,235,230,235,279,227,182,194,159,142,182,142,123,168,101,83,109,47,182,168,230,194,182,227,194,230,135,136,189,159,189,235,194,227,189,194,182,227,189,182,199,227,215,255,271,249,135,199,189,136,189,182,136,199,101,109,159,120,101,83,136,109,77,119,132,117,29,47,80,85,25,43,57,92,85,47,80,101,124,75,69,92,31,37,3,24,8,26,2,9,200,166,132,129,40,63,12,46,104,76,55,68,13,65,25,37,85,101,80,120,124,75,92,119,206,239,215,271,98,139,115,73,92,69,43,75,167,115,177,135,170,166,179,129,73,88,69,39,200,224,258,228,170,137,177,167,179,224,258,200,170,167,177,206,170,137,129,179,73,139,115,137,29,52,39,13,69,39,88,65,13,37,2,3,29,80,73,98,97,77,127,86,167,115,135,120,98,115,80,73,98,80,115,120,98,115,139,120,13,9,37,25,120,80,115,101,167,177,115,137,48,64,16,56,77,75,92,43,170,179,177,137,167,159,135,189,101,159,136,135,101,136,159,109,120,135,159,101,52,29,39,98,56,36,47,78,31,3,37,13,124,129,69,75,206,255,271,215,206,215,179,255,206,271,255,224,206,224,255,179,255,258,224,266,255,224,258,179,25,37,57,19,206,224,179,166,85,109,47,101,124,69,73,88,124,137,73,69,124,139,137,170,124,69,129,137,124,137,129,170,124,129,166,170,52,65,88,39,78,108,83,109,14,16,36,56,31,37,52,13,56,16,36,64,56,64,36,78,56,47,85,78,28,26,1,27,98,39,52,88,13,39,25,65,3,28,5,7,52,65,39,13,52,37,65,13,7,16,56,48,3,24,37,2,14,31,7,56,14,13,3,31,3,2,1,24,14,31,3,7,3,5,28,1,30,11,70,32,25,65,69,92,25,69,43,92,25,39,69,65,29,56,85,52,13,2,37,9,29,85,98,52,14,52,31,56,14,13,52,29,14,52,13,31,14,36,47,56,14,56,47,29,59,77,97,86,0,30,10,4,44,68,62,99,86,43,57,34,59,94,44,93,19,9,25,37,253,241,175,217,117,119,127,77,58,34,57,19,0,30,4,27,23,93,59,44,58,19,57,45,68,53,32,20,86,59,58,93,164,175,133,188,8,42,18,10,203,217,241,264,201,242,241,188,201,188,195,242,201,256,242,195,201,242,256,241,188,242,241,253,203,264,211,217,222,146,216,210,23,62,93,44,86,34,57,58,77,34,43,86,187,117,178,158,117,178,132,187,129,166,119,124,252,214,233,193,97,127,117,158,158,178,225,176,214,223,276,225,176,126,172,162,187,228,223,225,214,158,225,176,187,225,178,220,187,225,220,228,158,162,126,176,158,126,127,176,158,97,126,162,94,138,128,141,59,126,93,86,59,126,86,97,144,99,94,157,195,188,190,242,173,211,157,172,144,157,94,141,164,144,133,175,91,112,133,150,144,141,94,133,175,144,157,203,213,251,234,270,203,157,173,211,203,217,211,173,175,173,157,141,175,157,173,203,175,157,144,141,175,133,141,144,175,173,217,203,164,133,144,112,201,241,175,188,175,217,241,203,164,195,201,188,164,203,144,175,79,68,99,112,188,175,253,241,79,133,144,94,195,256,242,257,195,242,251,257,195,251,242,190,91,55,79,112,91,104,55,112,150,112,133,164,79,68,112,55,79,99,94,144,79,94,99,44,32,55,68,20,145,188,195,164,145,190,195,188,251,213,196,190,171,184,125,152,32,53,68,76,54,55,76,32,32,55,76,68,30,53,10,32,40,41,27,6,30,76,53,32,40,21,6,12,40,12,1,46,82,22,67,40,22,21,40,63,67,17,22,49,90,60,35,49,30,32,10,11,10,53,20,32,17,41,6,4,17,40,22,6,17,6,41,40,67,40,17,41,90,17,35,41,70,35,32,11,8,18,42,23,30,70,11,41,70,76,30,32,11,4,30,41,11,41,17,4,11,17,41,35,130,60,90,114,67,40,22,17,90,35,70,41,54,70,60,35,67,41,17,90,82,40,63,22,66,63,21,22,66,102,63,82,181,148,156,105,196,190,213,152,66,72,102,116,204,221,250,229,114,90,67,49,140,66,113,116,246,292,226,259,145,150,91,133,213,195,152,190,84,70,104,125,213,248,234,184,171,196,234,184,174,121,186,130,171,184,152,196,145,150,133,164,54,70,104,84,171,125,184,186,171,186,205,192,84,90,70,125,84,130,90,125,245,232,254,260,192,229,174,186,121,125,130,84,121,60,84,130,205,234,248,184,254,270,308,286,171,107,125,84,121,186,130,125,174,156,105,181,66,72,21,63,185,236,246,204,185,198,216,165,180,204,148,181,204,250,246,226,246,259,226,250,116,102,147,110,66,63,102,72,185,216,146,165,204,180,148,140,116,72,110,103,146,116,165,103,140,95,156,148,140,148,156,180,140,116,147,185,364,297,321,343,366,321,354,343,321,364,308,419,364,419,321,297,333,324,323,282,333,324,282,331,331,417,324,282,265,244,283,280,265,244,207,238,303,284,287,245,303,297,284,260,341,359,392,349,392,359,358,402,341,311,359,349,303,297,260,286,303,286,260,254,310,281,285,277,277,319,315,418,310,319,281,277,245,229,232,260,192,229,186,232,174,186,229,181,2,24,26,1,0,1,26,27,0,2,26,1,0,26,30,27,279,235,230,261,279,227,235,273,247,233,252,269,247,211,233,269,370,360,375,317,359,370,375,317,77,119,92,75,77,86,119,127,77,119,86,92,69,92,88,124,69,88,92,65,234,248,270,254,234,270,248,213,247,211,264,217,247,264,211,269,328,289,299,336,328,284,303,297,328,284,297,327,328,336,299,327,328,284,327,299,145,195,150,164,107,125,152,171,122,152,171,107,145,152,150,195,122,190,152,145,122,196,152,190,122,152,196,171,180,204,236,185,180,204,221,236,180,204,185,140,94,138,157,99,94,157,138,141,94,138,93,59,214,176,225,233,214,158,187,225,252,233,247,193,162,176,233,172,214,176,233,162,38,46,63,71,46,71,33,61,38,63,46,12,196,152,213,184,196,184,213,234,215,239,206,177,173,141,138,157,128,97,126,59,128,138,126,172,128,138,59,126,128,138,172,173,173,138,172,157,146,103,165,149,110,165,147,116,222,216,226,292,222,226,216,185,222,216,146,185,331,317,417,298,185,165,147,198,185,165,116,147,295,310,281,285,281,310,348,319,348,281,319,320,344,320,281,348,0,30,26,8,10,30,0,8,10,30,42,53,10,4,30,11,91,152,122,107,91,122,152,145,150,145,91,152,107,104,125,84,91,150,152,104,20,10,18,53,199,239,167,189,199,231,239,189,199,167,239,177,289,329,299,336,289,284,299,250,289,250,299,259,289,259,299,329,299,315,329,259,289,287,328,284,289,328,299,284,262,277,216,292,262,216,277,243,251,213,257,270,251,195,213,190,251,196,213,234,251,257,213,195,318,317,359,298,318,273,317,298,318,317,311,359,318,311,349,359,105,148,114,181,105,114,60,130,105,181,114,130,105,60,114,49,391,345,371,339,395,371,405,374,193,211,247,173,173,211,247,217,211,233,193,247,403,379,381,361,403,381,379,411,379,407,369,400,403,407,379,400,38,46,71,33,121,186,171,192,121,171,186,125,121,174,105,130,132,178,119,166,132,200,187,178,129,124,119,75,132,75,129,119,132,119,178,117,132,119,77,75,367,335,327,363,367,327,335,336,124,139,73,137,367,327,343,363,367,327,328,337,254,232,248,260,254,260,248,286,254,234,248,205,205,245,232,254,396,378,360,370,396,375,402,370,396,351,375,355,396,375,360,355,396,355,360,378,66,67,22,49,66,67,82,22,66,22,82,63,66,113,102,82,288,290,306,269,288,306,290,340,107,104,152,125,107,152,104,91,362,345,330,393,362,330,345,302,352,330,362,302,370,358,359,402,370,402,359,375,94,93,99,44,59,138,93,126,94,99,93,138,400,386,376,369,400,353,386,369,393,330,362,384,362,330,352,384,393,330,384,365,419,308,321,286,132,129,166,119,419,364,308,303,419,286,321,297,297,303,364,419,23,59,93,58,23,93,62,58,23,58,34,59,73,124,88,98,73,39,29,98,73,88,39,98,285,265,243,281,285,243,262,277,285,243,277,281,243,265,238,281,74,111,106,87,74,110,71,38,74,106,111,110,33,111,51,61,1,27,0,4,1,27,40,46,1,28,27,46,329,259,315,292,225,233,267,214,276,275,267,225,276,225,267,214,346,322,373,350,373,350,322,342,373,322,366,342,114,49,95,105,117,127,97,77,117,127,119,178,117,127,178,158,178,127,176,158,23,62,45,58,23,42,62,18,38,74,33,71,55,68,112,104,20,79,68,44,20,68,53,44,20,55,68,79,44,79,68,99,290,305,269,247,290,272,264,288,290,264,269,288,66,113,116,102,230,261,212,268,230,212,209,268,6,27,1,4,4,6,27,41,4,27,30,41,98,73,124,139,227,231,235,273,200,166,178,132,200,224,166,179,200,166,220,178,200,166,224,220,200,166,129,179,150,112,104,91,54,104,107,84,392,358,341,398,392,358,359,341,187,214,225,223,187,178,225,158,140,113,66,95,140,113,95,148,140,113,147,116,140,147,113,148,18,62,23,44,18,62,53,42,44,53,18,62,44,53,62,68,18,42,53,10,376,347,315,335,376,347,386,418,376,315,347,418,205,186,171,184,205,192,186,232,205,186,184,232,221,181,229,204,221,181,174,229,221,181,204,180,411,379,390,381,411,379,407,415,411,379,415,390,365,393,404,384,393,345,356,362,193,233,162,214,193,172,211,173,193,172,233,211,307,352,296,334,307,275,302,312,3,28,7,31,3,1,28,24,3,24,28,31,406,371,382,405,406,382,371,365,199,249,231,263,199,249,239,231,199,227,231,189,329,315,418,292,376,315,329,336,245,229,221,174,245,287,229,284,245,229,260,284,221,204,250,236,221,236,250,287,337,297,343,327,337,367,343,385,337,328,297,327,337,327,343,367,185,140,204,147,185,204,198,147,204,148,147,140,54,32,70,35,54,104,70,76,54,76,70,32,410,358,383,402,402,370,358,383,410,381,358,398,410,358,381,383,100,89,131,50,100,81,50,131,100,131,89,143,174,186,121,192,174,186,181,130,315,418,376,329,376,315,336,335,373,346,366,322,366,354,346,397,366,354,322,346,366,385,343,397,95,114,105,148,105,148,156,95,95,114,148,113,95,67,66,49,375,324,333,331,323,375,333,351,355,360,324,375,351,323,375,355,375,323,324,355,303,284,328,287,106,134,149,103,106,110,74,103,134,149,155,106,106,155,111,149,72,63,110,38,72,110,63,102,23,45,62,42,373,397,346,395,343,366,364,385,364,366,343,321,321,308,364,366,343,337,364,297,297,337,364,303,95,67,114,82,95,67,49,114,95,114,113,82,95,82,113,66,95,82,66,67,208,244,237,240,219,244,280,237,219,207,244,202,219,202,244,208,219,244,237,208,8,26,42,30,8,42,45,23,8,42,26,45,8,30,42,10,245,232,229,192,245,229,174,192,373,368,346,350,5,48,28,46,5,28,48,7,5,46,28,1,373,366,346,397,156,221,181,174,156,148,181,180,205,232,245,192,205,232,184,248,205,232,248,254,54,55,104,76,19,57,45,37,9,45,26,37,9,45,8,26,371,393,391,406,371,393,406,365,246,226,185,204,185,204,226,198,246,236,250,204,356,345,306,362,356,345,339,306,356,339,345,391,214,233,267,252,214,252,267,276,162,172,233,193,303,337,328,297,193,172,128,162,128,162,126,97,128,172,126,162,8,9,19,45,19,23,45,58,19,45,23,8,222,226,246,292,352,330,302,296,352,302,362,312,230,235,194,212,230,235,227,194,51,89,48,61,51,89,61,96,16,48,89,64,16,89,50,64,97,126,127,158,247,264,290,272,247,264,272,217,269,290,247,264,299,329,315,336,289,259,329,246,168,194,212,230,168,153,212,163,168,209,212,153,168,212,194,163,54,91,107,104,54,91,104,55,218,243,285,265,218,238,265,207,218,197,243,191,218,238,207,191,218,243,238,191,60,70,54,84,60,35,70,90,60,90,70,84,245,221,229,287,393,371,345,365,393,345,371,391,245,284,260,303,303,245,254,260,276,305,267,312,305,269,267,306,305,267,269,252,312,267,302,306], 4 | attachedTets: [760,1072,1235,1242,778,770,784,992,774,791,765,114,115,1042,1171,1137,1146,834,1189,835,891,930,885,810,1183,1245,888,804,985,1249,757,176,116,178,232,146,90,86,722,757,177,178,181,146,744,690,697,164,732,736,12,1101,962,898,1053,90,727,211,1210,187,753,32,1211,779,1020,676,1215,62,681,754,934,691,587,928,677,861,1062,1032,89,1041,1015,1209,943,1119,1144,1121,108,938,148,150,1114,149,373,3,890,892,798,829,532,408,479,529,456,699,473,1155,448,497,451,453,515,661,608,615,285,490,662,606,521,646,675,555,414,537,78,702,1043,200,1100,596,977,704,1075,641,588,273,604,602,630,898,1055,180,228,1040,1250,193,152,230,1218,252,245,1010,1198,1098,333,438,353,376,388,387,377,384,286,1087,105,422,409,406,426,417,16,1238,495,437,428,536,391,52,402,1060,423,485,1233,17,487,547,413,437,341,353,335,402,571,452,553,344,330,578,622,288,538,597,576,535,954,348,291,320,634,1004,1150,42,215,280,297,19,259,270,954,849,849,277,266,241,362,277,47,47,261,383,58,261,1096,459,394,1139,1216,55,110,519,507,500,98,1028,1049,617,616,584,639,609,667,205,308,317,204,213,1207,45,220,144,220,243,163,163,1188,1188,1040], 5 | baryCoords: [0.0696,-0.0243,0.0825,-0.0247,0.7585,0.197,0.5845,0.1143,-0.0751,-0.0449,0.25,0.2494,0.9583,0.2286,-0.1682,0.2657,0.16,0.6248,0.7456,-0.0408,0.1328,-0.0684,0.5578,0.4396,-0.0545,0.42,0.5288,-0.0614,0.5986,0.1571,0.6258,0.1493,0.2737,0.341,0.3147,-0.0612,0.3881,-0.0804,0.48,0.3735,0.5648,0.134,0.0604,-0.0866,0.8462,-0.0698,0.5092,0.0811,-0.0389,0.4514,0.0269,0.5044,0.004,-0.0458,0.2468,-0.0485,0.583,-0.0304,0.4476,0.472,-0.0417,0.5797,0.1545,0.6528,-0.0545,-0.0189,-0.0523,0.4782,0.0587,-0.0563,0.1653,0.3521,-0.0601,0.2801,0.4265,0.5765,-0.0655,0.1635,0.136,-0.0378,0.5932,0.3948,0.4995,-0.0511,0.0073,-0.0387,0.5362,0.8006,0.0126,-0.0399,-0.0253,0.1003,0.07,0.219,-0.089,0.3058,0.264,0.3381,0.4617,0.3142,0.4956,-0.0513,0.5941,0.2233,0.221,0.3526,0.7086,-0.035,0.1761,-0.053,0.1192,0.1388,0.0636,-0.035,0.3052,0.3906,0.37,-0.0253,0.1003,0.07,-0.0661,0.6266,0.4125,0.3142,0.4956,-0.0513,0.2175,0.6138,-0.0382,0.3526,0.7086,-0.035,1.0035,-0.0021,-0.0096,0.3948,0.1238,0.533,-0.0882,-0.0447,0.8596,0.1477,0.7259,0.1728,-0.0953,-0.0329,0.7152,0.0341,0.6829,-0.0376,-0.0116,0.0795,0.9208,-0.0914,0.0715,0.8184,-0.0508,0.7136,0.1962,-0.0349,0.1371,0.8003,0.942,-0.0141,0.0814,-0.0361,-0.0093,0.9917,-0.0155,0.9,0.1395,0.6315,-0.0152,-0.0447,-0.06,0.317,0.3576,-0.0566,-0.2532,0.4594,0.1456,-0.0098,0.9958,0.1794,-0.0943,0.5713,0.1517,-0.0612,0.5602,0.7486,-0.0345,0.229,-0.0552,-0.0129,0.4494,0.3823,0.0809,-0.0697,-0.0653,0.4525,0.3277,-0.0085,0.7827,0.2417,0.269,0.1429,-0.0374,0.1657,-0.0269,0.0721,0.6574,0.2367,-0.0504,-0.0712,0.0363,0.2732,-0.0313,0.1406,0.6305,0.0286,-0.034,0.7676,-0.049,0.1577,0.2909,0.6566,-0.0506,0.0565,0.3535,-0.0839,0.049,-0.0432,0.1114,0.1179,0.1833,0.3908,0.4841,-0.0399,0.5342,0.5381,0.6869,0.2639,0.1539,0.0658,0.207,-0.042,0.7992,0.2742,-0.0318,0.7649,-0.0389,0.3591,0.4524,-0.0594,0.0424,0.0248,-0.0321,0.3556,-0.036,0.2161,0.1018,0.0474,-0.0183,0.0759,0.3209,-0.0552,0.2233,-0.0498,0.2688,0.1038,-0.0481,0.1937,0.6395,0.1578,0.6011,-0.0567,-0.041,1.0086,0.1651,-0.0856,0.3531,0.3397,-0.0765,0.4806,0.3322,-0.0465,0.4994,0.492,0.2028,-0.0359,0.8292,0.0016,-0.0103,0.9569,-0.0402,0.6645,0.2412,0.2427,-0.0698,0.5275,0.624,-0.0736,0.2763,0.1269,0.8742,-0.172,0.498,0.2402,-0.0944,0.0882,-0.0477,0.3129,-0.0547,0.5593,0.5502,0.5125,-0.0462,0.2453,-0.0208,0.2737,0.7843,0.0582,0.6334,-0.0402,-0.0584,-0.021,0.9218,-0.0618,0.2453,0.605,0.1978,0.6161,0.2386,-0.2351,0.248,-0.0203,-0.0707,0.3908,0.1574,0.2775,-0.0581,0.4062,0.1363,-0.0252,0.0096,0.2137,0.3359,-0.0486,-0.04,0.2967,-0.1197,-0.0438,-0.0476,0.5782,0.5913,-0.0516,0.2387,0.1745,0.5395,-0.0697,-0.0623,0.5527,0.1405,0.2701,0.4728,0.3067,0.396,0.2464,0.417,0.4471,0.1464,0.4737,-0.0314,0.4221,0.5623,-0.0191,0.38,-0.0285,-0.0509,0.3094,0.134,-0.0149,0.3023,0.734,-0.037,0.0583,0.7589,0.4782,0.1958,0.3795,-0.0544,0.1433,0.3457,-0.067,0.265,0.5501,0.298,-0.062,0.2366,0.2456,0.6826,0.1326,-0.0649,0.2977,0.2537,0.4896,0.2387,-0.0566,0.2122,0.2517,0.6009,-0.06,0.2877,0.5185,0.1403,0.6766,0.2356,-0.032,0.2863,0.0163,0.2137,-0.0639,0.3902,0.5723,0.6453,-0.1595,0.4596,0.5633,-0.052,0.4388,-0.0362,0.1386,0.711,-0.0487,-0.0111,0.2407,0.4604,0.3635,-0.0642,0.3619,0.4562,-0.0449,0.3847,0.4409,-0.0288,0.8535,0.2963,-0.0496,0.4071,0.11,0.0686,-0.0425,0.361,-0.0658,0.3175,0.4594,0.4612,-0.0255,0.5893,0.1605,0.5213,0.383,-0.0424,0.317,0.0811,0.0538,0.1867,0.7961,0.2418,0.4835,0.3174,-0.0597,0.2918,0.1037,0.1254,0.4557,-0.0564,0.0812,0.3751,-0.0807,0.0392,0.7043,0.2874,0.6993,-0.0272,0.2287,0.3078,0.6255,0.0939,-0.0455,0.7773,0.0149,0.1826,0.8375,0.0212,-0.0654,0.4123,-0.0029,-0.0481,0.648,0.0887,-0.0563,0.5652,0.3183,0.1753,-0.0369,0.7367,0.476,-0.0355,0.24,-0.0502,0.1967,0.6005,-0.0391,0.5018,0.4034,-0.0773,0.4424,0.2289,-0.0601,-0.0998,0.6989,-0.016,0.0107,0.0254,0.1057,-0.0134,0.9689,0.1387,0.2066,0.6858,0.0921,-0.0481,0.4213,-0.0406,0.1771,0.7394,-0.0137,0.9478,-0.0359,0.1833,0.0118,-0.0383,0.593,0.3868,0.0507,0.2538,-0.039,-0.1288,-0.0022,-0.0228,0.8755,0.7049,0.2808,0.0592,0.721,0.3077,-0.0402,-0.0533,0.2322,0.6809,-0.0601,-0.0998,0.6989,0.6204,0.4878,-0.0295,0.2418,0.4835,0.3174,-0.0622,0.2176,0.3129,-0.0406,0.1771,0.7394,0.325,-0.079,0.2987,0.3976,0.1649,0.5031,0.3242,0.1391,0.6027,-0.0471,0.1257,0.4251,0.23,-0.0782,0.3404,0.301,0.2215,0.5606,-0.0359,0.4429,0.2922,0.4794,-0.0375,0.1528,0.6865,-0.0414,-0.1217,-0.0297,0.5045,0.5707,0.3643,-0.0446,0.2408,0.5934,0.2611,0.1959,-0.034,0.3133,0.68,-0.0513,0.0117,0.6196,-0.0572,0.265,0.4082,0.2352,0.2788,-0.0507,-0.0405,0.4128,0.0582,0.4153,0.1533,0.4839,0.5507,0.0277,0.4881,0.0354,0.6177,-0.0496,0.4242,0.3709,0.265,0.0827,-0.0705,0.9303,0.4083,0.1958,0.4679,-0.0412,0.496,0.0894,0.725,0.198,0.1212,0.0845,0.3716,0.581,-0.034,0.3133,0.68,0.0689,-0.0333,0.1715,0.0689,-0.0333,0.1715,-0.1984,-0.0594,0.3772,0.2157,0.0013,-0.216,0.6914,-0.0357,0.022,0.5242,0.3721,0.1628,-0.1984,-0.0594,0.3772,0.049,0.5822,0.4239,0.049,0.5822,0.4239,-0.0033,0.6159,0.4377,-0.0363,0.2555,0.0753,-0.035,0.6902,0.3419,-0.0033,0.6159,0.4377,-0.0434,0.8223,0.0279,-0.0895,0.0695,0.9153,-0.082,0.6758,-0.0016,0.5251,0.2399,0.3208,-0.0867,0.1833,0.4357,0.7512,0.1087,-0.0424,0.7456,-0.0341,0.0952,-0.0674,0.1786,0.648,-0.0047,-0.0186,0.3088,-0.0654,0.4179,0.2251,-0.0056,0.8918,-0.2037,-0.0516,0.0922,0.2138,0.0032,0.7117,-0.0282,0.1321,0.1793,0.7237,0.2939,0.6632,0.0796,0.5142,0.0034,0.5273,0.171,0.4672,0.4164,0.2034,0.3396,-0.0669,0.5711,0.1572,-0.0431,-0.0539,0.2348,0.5327,0.0643,0.639,-0.056,0.1743,0.7113,-0.066,-0.0396,0.0183,0.7293,0.4152,-0.059,0.3652,-0.0166,-0.0361,0.7484,0.5849,-0.0724,0.2456,0.8955,0.1206,-0.1494,0.1292,-0.056,0.4637,0.8955,0.1206,-0.1494,-0.0473,0.0772,0.487,0.1559,-0.0536,0.3753,0.1559,-0.0536,0.3753,0.0574,-0.0504,0.4782,0.0574,-0.0504,0.4782,0.4388,-0.0362,0.1386], 6 | normals: [-0.6511,0.7501,0.1159,-0.4636,0.866,0.1875,-0.5257,0.8507,0,-0.5844,0.7408,0.3313,-0.765,0.5955,0.2453,-0.6849,0.5541,0.4732,-0.8408,0.397,0.368,-0.7408,0.3313,0.5844,-0.866,0.1875,0.4636,-0.7501,0.1159,0.6511,-0.8507,0,0.5257,-0.368,0.8408,0.397,-0.4732,0.6849,0.5541,-0.5541,0.4732,0.6849,-0.5955,0.2453,0.765,-0.2453,0.765,0.5955,-0.3313,0.5844,0.7408,-0.397,0.368,0.8408,-0.1159,0.6511,0.7501,-0.1875,0.4636,0.866,0,0.5257,0.8507,-0.3477,0.9376,0,-0.2531,0.9455,0.2047,-0.1308,0.8965,0.4233,0,0.7891,0.6142,0.1159,0.6511,0.7501,-0.1227,0.9924,0,0,0.9773,0.2116,0.1308,0.8965,0.4233,0.2453,0.765,0.5955,0.1227,0.9924,0,0.2531,0.9455,0.2047,0.368,0.8408,0.397,0.3477,0.9376,0,0.4636,0.866,0.1875,0.5257,0.8507,0,-0.4636,0.866,-0.1875,-0.2531,0.9455,-0.2047,0,0.9773,-0.2116,0.1227,0.9924,0,0.2531,0.9455,-0.2047,0.3477,0.9376,0,0.4636,0.866,-0.1875,0.5257,0.8507,0,-0.368,0.8408,-0.397,-0.1308,0.8965,-0.4233,0.1308,0.8965,-0.4233,0.368,0.8408,-0.397,-0.2453,0.765,-0.5955,0,0.7891,-0.6142,0.2453,0.765,-0.5955,-0.1159,0.6511,-0.7501,0.1159,0.6511,-0.7501,0,0.5257,-0.8507,-0.6511,0.7501,-0.1159,-0.5844,0.7408,-0.3313,-0.4732,0.6849,-0.5541,-0.3313,0.5844,-0.7408,-0.1875,0.4636,-0.866,-0.765,0.5955,-0.2453,-0.6849,0.5541,-0.4732,-0.5541,0.4732,-0.6849,-0.397,0.368,-0.8408,-0.8408,0.397,-0.368,-0.7408,0.3313,-0.5844,-0.5955,0.2453,-0.765,-0.866,0.1875,-0.4636,-0.7501,0.1159,-0.6511,-0.8507,0,-0.5257,-0.7891,0.6142,0,-0.8965,0.4233,-0.1308,-0.9455,0.2047,-0.2531,-0.9376,0,-0.3477,-0.8965,0.4233,0.1308,-0.9773,0.2116,0,-0.9924,0,-0.1227,-0.9455,0.2047,0.2531,-0.9924,0,0.1227,-0.9376,0,0.3477,0.6511,0.7501,0.1159,0.5844,0.7408,0.3313,0.4732,0.6849,0.5541,0.3313,0.5844,0.7408,0.1875,0.4636,0.866,0.765,0.5955,0.2453,0.6849,0.5541,0.4732,0.5541,0.4732,0.6849,0.397,0.368,0.8408,0.8408,0.397,0.368,0.7408,0.3313,0.5844,0.5955,0.2453,0.765,0.866,0.1875,0.4636,0.7501,0.1159,0.6511,0.8507,0,0.5257,0,0.3477,0.9376,-0.2047,0.2531,0.9455,-0.4233,0.1308,0.8965,-0.6142,0,0.7891,-0.7501,-0.1159,0.6511,0,0.1227,0.9924,-0.2116,0,0.9773,-0.4233,-0.1308,0.8965,-0.5955,-0.2453,0.765,0,-0.1227,0.9924,-0.2047,-0.2531,0.9455,-0.397,-0.368,0.8408,0,-0.3477,0.9376,-0.1875,-0.4636,0.866,0,-0.5257,0.8507,-0.866,-0.1875,0.4636,-0.9455,-0.2047,0.2531,-0.9773,-0.2116,0,-0.9455,-0.2047,-0.2531,-0.866,-0.1875,-0.4636,-0.8408,-0.397,0.368,-0.8965,-0.4233,0.1308,-0.8965,-0.4233,-0.1308,-0.8408,-0.397,-0.368,-0.765,-0.5955,0.2453,-0.7891,-0.6142,0,-0.765,-0.5955,-0.2453,-0.6511,-0.7501,0.1159,-0.6511,-0.7501,-0.1159,-0.5257,-0.8507,0,-0.7501,-0.1159,-0.6511,-0.6142,0,-0.7891,-0.4233,0.1308,-0.8965,-0.2047,0.2531,-0.9455,0,0.3477,-0.9376,-0.5955,-0.2453,-0.765,-0.4233,-0.1308,-0.8965,-0.2116,0,-0.9773,0,0.1227,-0.9924,-0.397,-0.368,-0.8408,-0.2047,-0.2531,-0.9455,0,-0.1227,-0.9924,-0.1875,-0.4636,-0.866,0,-0.3477,-0.9376,0,-0.5257,-0.8507,0.1875,0.4636,-0.866,0.3313,0.5844,-0.7408,0.4732,0.6849,-0.5541,0.5844,0.7408,-0.3313,0.6511,0.7501,-0.1159,0.397,0.368,-0.8408,0.5541,0.4732,-0.6849,0.6849,0.5541,-0.4732,0.765,0.5955,-0.2453,0.5955,0.2453,-0.765,0.7408,0.3313,-0.5844,0.8408,0.397,-0.368,0.7501,0.1159,-0.6511,0.866,0.1875,-0.4636,0.8507,0,-0.5257,0.6511,-0.7501,0.1159,0.4636,-0.866,0.1875,0.5257,-0.8507,0,0.5844,-0.7408,0.3313,0.765,-0.5955,0.2453,0.6849,-0.5541,0.4732,0.8408,-0.397,0.368,0.7408,-0.3313,0.5844,0.866,-0.1875,0.4636,0.7501,-0.1159,0.6511,0.368,-0.8408,0.397,0.4732,-0.6849,0.5541,0.5541,-0.4732,0.6849,0.5955,-0.2453,0.765,0.2453,-0.765,0.5955,0.3313,-0.5844,0.7408,0.397,-0.368,0.8408,0.1159,-0.6511,0.7501,0.1875,-0.4636,0.866,0.3477,-0.9376,0,0.2531,-0.9455,0.2047,0.1308,-0.8965,0.4233,0,-0.7891,0.6142,-0.1159,-0.6511,0.7501,0.1227,-0.9924,0,0,-0.9773,0.2116,-0.1308,-0.8965,0.4233,-0.2453,-0.765,0.5955,-0.1227,-0.9924,0,-0.2531,-0.9455,0.2047,-0.368,-0.8408,0.397,-0.3477,-0.9376,0,-0.4636,-0.866,0.1875,0.3477,-0.9376,0,0.4636,-0.866,-0.1875,0.5257,-0.8507,0,0.2531,-0.9455,-0.2047,0.1227,-0.9924,0,0,-0.9773,-0.2116,-0.2531,-0.9455,-0.2047,-0.4636,-0.866,-0.1875,0.368,-0.8408,-0.397,0.1308,-0.8965,-0.4233,-0.1308,-0.8965,-0.4233,-0.368,-0.8408,-0.397,0.2453,-0.765,-0.5955,0,-0.7891,-0.6142,-0.2453,-0.765,-0.5955,0.1159,-0.6511,-0.7501,-0.1159,-0.6511,-0.7501,0.6511,-0.7501,-0.1159,0.5844,-0.7408,-0.3313,0.4732,-0.6849,-0.5541,0.3313,-0.5844,-0.7408,0.1875,-0.4636,-0.866,0.765,-0.5955,-0.2453,0.6849,-0.5541,-0.4732,0.5541,-0.4732,-0.6849,0.397,-0.368,-0.8408,0.8408,-0.397,-0.368,0.7408,-0.3313,-0.5844,0.5955,-0.2453,-0.765,0.866,-0.1875,-0.4636,0.7501,-0.1159,-0.6511,0.6511,-0.7501,-0.1159,0.7891,-0.6142,0,0.7891,-0.6142,0,0.8965,-0.4233,-0.1308,0.9455,-0.2047,-0.2531,0.9376,0,-0.3477,0.8965,-0.4233,0.1308,0.8965,-0.4233,-0.1308,0.9773,-0.2116,0,0.9773,-0.2116,0,0.9924,0,-0.1227,0.9455,-0.2047,0.2531,0.9924,0,0.1227,0.9924,0,-0.1227,0.9376,0,0.3477,0.2047,-0.2531,0.9455,0.4233,-0.1308,0.8965,0.6142,0,0.7891,0.2116,0,0.9773,0.4233,0.1308,0.8965,0.2047,0.2531,0.9455,-0.5844,-0.7408,0.3313,-0.4732,-0.6849,0.5541,-0.3313,-0.5844,0.7408,-0.6849,-0.5541,0.4732,-0.5541,-0.4732,0.6849,-0.7408,-0.3313,0.5844,-0.3313,-0.5844,-0.7408,-0.4732,-0.6849,-0.5541,-0.5844,-0.7408,-0.3313,-0.5541,-0.4732,-0.6849,-0.6849,-0.5541,-0.4732,-0.7408,-0.3313,-0.5844,0.6142,0,-0.7891,0.4233,-0.1308,-0.8965,0.2047,-0.2531,-0.9455,0.4233,0.1308,-0.8965,0.2116,0,-0.9773,0.2047,0.2531,-0.9455,0.9455,0.2047,0.2531,0.9773,0.2116,0,0.9455,0.2047,-0.2531,0.9773,0.2116,0,0.8965,0.4233,0.1308,0.8965,0.4233,-0.1308,0.8965,0.4233,-0.1308,0.7891,0.6142,0,0.7891,0.6142,0,0.6511,0.7501,-0.1159], 7 | uvs: [0.528,0.77,0.5612,0.8333,0.5,0.8238,0.5821,0.7655,0.5494,0.703,0.5962,0.6869,0.5657,0.6299,0.6063,0.6075,0.5782,0.56,0.6138,0.537,0.5881,0.5,0.631,0.8179,0.6375,0.7401,0.6417,0.6569,0.6447,0.5789,0.6878,0.7773,0.6831,0.6987,0.6798,0.62,0.7256,0.7257,0.7161,0.6534,0.75,0.6762,0.5,0.887,0.6083,0.8945,0.7023,0.8539,0.75,0.7895,0.7744,0.7257,0.5,0.9609,0.75,0.9321,0.7977,0.8539,0.8122,0.7773,1,0.9609,0.8917,0.8945,0.869,0.8179,1,0.887,0.9388,0.8333,1,0.8238,0.4388,0.8333,0.3917,0.8945,0.25,0.9321,0,0.9609,0.1083,0.8945,0,0.887,0.0612,0.8333,0,0.8238,0.369,0.8179,0.2977,0.8539,0.2023,0.8539,0.131,0.8179,0.3122,0.7773,0.25,0.7895,0.1878,0.7773,0.2744,0.7257,0.2256,0.7257,0.25,0.6762,0.472,0.77,0.4179,0.7655,0.3625,0.7401,0.3169,0.6987,0.2839,0.6534,0.4506,0.703,0.4038,0.6869,0.3583,0.6569,0.3202,0.62,0.4343,0.6299,0.3937,0.6075,0.3553,0.5789,0.4218,0.56,0.3862,0.537,0.4119,0.5,0.5,0.7105,0.4769,0.6391,0.4584,0.5656,0.4435,0.5,0.5231,0.6391,0.5,0.5679,0.4804,0.5,0.5416,0.5656,0.5196,0.5,0.5565,0.5,0.972,0.77,0.9179,0.7655,0.8625,0.7401,0.8169,0.6987,0.7839,0.6534,0.9506,0.703,0.9038,0.6869,0.8583,0.6569,0.8202,0.62,0.9343,0.6299,0.8937,0.6075,0.8553,0.5789,0.9218,0.56,0.8862,0.537,0.9119,0.5,0.75,0.613,0.7161,0.5814,0.6798,0.5418,0.6447,0.5,0.6138,0.463,0.75,0.5391,0.7161,0.5,0.6798,0.4582,0.6447,0.4211,0.75,0.4609,0.7161,0.4186,0.6798,0.38,0.75,0.387,0.7161,0.3466,0.75,0.3238,0.5782,0.44,0.5416,0.4344,0.5,0.4321,0.4584,0.4344,0.4218,0.44,0.5657,0.3701,0.5231,0.3609,0.4769,0.3609,0.4343,0.3701,0.5494,0.297,0.5,0.2895,0.4506,0.297,0.528,0.23,0.472,0.23,0.5,0.1762,0.3862,0.463,0.3553,0.5,0.3202,0.5418,0.2839,0.5814,0.25,0.613,0.3553,0.4211,0.3202,0.4582,0.2839,0.5,0.25,0.5391,0.3202,0.38,0.2839,0.4186,0.25,0.4609,0.2839,0.3466,0.25,0.387,0.25,0.3238,0.2161,0.6534,0.1831,0.6987,0.1375,0.7401,0.0821,0.7655,0.028,0.77,0.1798,0.62,0.1417,0.6569,0.0962,0.6869,0.0494,0.703,0.1447,0.5789,0.1063,0.6075,0.0657,0.6299,0.1138,0.537,0.0782,0.56,0.0881,0.5,0.972,0.23,0.9388,0.1667,1,0.1762,0.9179,0.2345,0.9506,0.297,0.9038,0.3131,0.9343,0.3701,0.8937,0.3925,0.9218,0.44,0.8862,0.463,0.869,0.1821,0.8625,0.2599,0.8583,0.3431,0.8553,0.4211,0.8122,0.2227,0.8169,0.3013,0.8202,0.38,0.7744,0.2743,0.7839,0.3466,1,0.113,0.8917,0.1055,0.7977,0.1461,0.75,0.2105,0.7256,0.2743,1,0.0391,0.75,0.0679,0.7023,0.1461,0.6878,0.2227,0.5,0.0391,0.6083,0.1055,0.631,0.1821,0.5,0.113,0.5612,0.1667,0,0.113,0.0612,0.1667,0,0.1762,0.1083,0.1055,0,0.0391,0.25,0.0679,0.3917,0.1055,0.4388,0.1667,0.131,0.1821,0.2023,0.1461,0.2977,0.1461,0.369,0.1821,0.1878,0.2227,0.25,0.2105,0.3122,0.2227,0.2256,0.2743,0.2744,0.2743,0.028,0.23,0.0821,0.2345,0.1375,0.2599,0.1831,0.3013,0.2161,0.3466,0.0494,0.297,0.0962,0.3131,0.1417,0.3431,0.1798,0.38,0.0657,0.3701,0.1063,0.3925,0.1447,0.4211,0.0782,0.44,0.1138,0.463,1.028,0.23,1,0.2895,0,0.2895,0.0231,0.3609,0.0416,0.4344,0.0565,0.5,0.9769,0.3609,1.0231,0.3609,1,0.4321,0,0.4321,0.0196,0.5,0.9584,0.4344,0.9804,0.5,1.0196,0.5,0.9435,0.5,0.7839,0.4186,0.8202,0.4582,0.8553,0.5,0.7839,0.5,0.8202,0.5418,0.7839,0.5814,0.5821,0.2345,0.6375,0.2599,0.6831,0.3013,0.5962,0.3131,0.6417,0.3431,0.6063,0.3925,0.3169,0.3013,0.3625,0.2599,0.4179,0.2345,0.3583,0.3431,0.4038,0.3131,0.3937,0.3925,0.1447,0.5,0.1798,0.4582,0.2161,0.4186,0.1798,0.5418,0.2161,0.5,0.2161,0.5814,0.9584,0.5656,1,0.5679,0.0416,0.5656,0,0.5679,0.9769,0.6391,1.0231,0.6391,0.0231,0.6391,1,0.7105,0,0.7105,1.028,0.77], 8 | positions: [-0.6511,0.7501,0.1159,-0.4636,0.866,0.1875,-0.5257,0.8507,0,-0.5844,0.7408,0.3313,-0.765,0.5955,0.2453,-0.6849,0.5541,0.4732,-0.8408,0.397,0.368,-0.7408,0.3313,0.5844,-0.866,0.1875,0.4636,-0.7501,0.1159,0.6511,-0.8507,0,0.5257,-0.368,0.8408,0.397,-0.4732,0.6849,0.5541,-0.5541,0.4732,0.6849,-0.5955,0.2453,0.765,-0.2453,0.765,0.5955,-0.3313,0.5844,0.7408,-0.397,0.368,0.8408,-0.1159,0.6511,0.7501,-0.1875,0.4636,0.866,0,0.5257,0.8507,-0.3477,0.9376,0,-0.2531,0.9455,0.2047,-0.1308,0.8965,0.4233,0,0.7891,0.6142,0.1159,0.6511,0.7501,-0.1227,0.9924,0,0,0.9773,0.2116,0.1308,0.8965,0.4233,0.2453,0.765,0.5955,0.1227,0.9924,0,0.2531,0.9455,0.2047,0.368,0.8408,0.397,0.3477,0.9376,0,0.4636,0.866,0.1875,0.5257,0.8507,0,-0.4636,0.866,-0.1875,-0.2531,0.9455,-0.2047,0,0.9773,-0.2116,0.1227,0.9924,0,0.2531,0.9455,-0.2047,0.3477,0.9376,0,0.4636,0.866,-0.1875,0.5257,0.8507,0,-0.368,0.8408,-0.397,-0.1308,0.8965,-0.4233,0.1308,0.8965,-0.4233,0.368,0.8408,-0.397,-0.2453,0.765,-0.5955,0,0.7891,-0.6142,0.2453,0.765,-0.5955,-0.1159,0.6511,-0.7501,0.1159,0.6511,-0.7501,0,0.5257,-0.8507,-0.6511,0.7501,-0.1159,-0.5844,0.7408,-0.3313,-0.4732,0.6849,-0.5541,-0.3313,0.5844,-0.7408,-0.1875,0.4636,-0.866,-0.765,0.5955,-0.2453,-0.6849,0.5541,-0.4732,-0.5541,0.4732,-0.6849,-0.397,0.368,-0.8408,-0.8408,0.397,-0.368,-0.7408,0.3313,-0.5844,-0.5955,0.2453,-0.765,-0.866,0.1875,-0.4636,-0.7501,0.1159,-0.6511,-0.8507,0,-0.5257,-0.7891,0.6142,0,-0.8965,0.4233,-0.1308,-0.9455,0.2047,-0.2531,-0.9376,0,-0.3477,-0.8965,0.4233,0.1308,-0.9773,0.2116,0,-0.9924,0,-0.1227,-0.9455,0.2047,0.2531,-0.9924,0,0.1227,-0.9376,0,0.3477,0.6511,0.7501,0.1159,0.5844,0.7408,0.3313,0.4732,0.6849,0.5541,0.3313,0.5844,0.7408,0.1875,0.4636,0.866,0.765,0.5955,0.2453,0.6849,0.5541,0.4732,0.5541,0.4732,0.6849,0.397,0.368,0.8408,0.8408,0.397,0.368,0.7408,0.3313,0.5844,0.5955,0.2453,0.765,0.866,0.1875,0.4636,0.7501,0.1159,0.6511,0.8507,0,0.5257,0,0.3477,0.9376,-0.2047,0.2531,0.9455,-0.4233,0.1308,0.8965,-0.6142,0,0.7891,-0.7501,-0.1159,0.6511,0,0.1227,0.9924,-0.2116,0,0.9773,-0.4233,-0.1308,0.8965,-0.5955,-0.2453,0.765,0,-0.1227,0.9924,-0.2047,-0.2531,0.9455,-0.397,-0.368,0.8408,0,-0.3477,0.9376,-0.1875,-0.4636,0.866,0,-0.5257,0.8507,-0.866,-0.1875,0.4636,-0.9455,-0.2047,0.2531,-0.9773,-0.2116,0,-0.9455,-0.2047,-0.2531,-0.866,-0.1875,-0.4636,-0.8408,-0.397,0.368,-0.8965,-0.4233,0.1308,-0.8965,-0.4233,-0.1308,-0.8408,-0.397,-0.368,-0.765,-0.5955,0.2453,-0.7891,-0.6142,0,-0.765,-0.5955,-0.2453,-0.6511,-0.7501,0.1159,-0.6511,-0.7501,-0.1159,-0.5257,-0.8507,0,-0.7501,-0.1159,-0.6511,-0.6142,0,-0.7891,-0.4233,0.1308,-0.8965,-0.2047,0.2531,-0.9455,0,0.3477,-0.9376,-0.5955,-0.2453,-0.765,-0.4233,-0.1308,-0.8965,-0.2116,0,-0.9773,0,0.1227,-0.9924,-0.397,-0.368,-0.8408,-0.2047,-0.2531,-0.9455,0,-0.1227,-0.9924,-0.1875,-0.4636,-0.866,0,-0.3477,-0.9376,0,-0.5257,-0.8507,0.1875,0.4636,-0.866,0.3313,0.5844,-0.7408,0.4732,0.6849,-0.5541,0.5844,0.7408,-0.3313,0.6511,0.7501,-0.1159,0.397,0.368,-0.8408,0.5541,0.4732,-0.6849,0.6849,0.5541,-0.4732,0.765,0.5955,-0.2453,0.5955,0.2453,-0.765,0.7408,0.3313,-0.5844,0.8408,0.397,-0.368,0.7501,0.1159,-0.6511,0.866,0.1875,-0.4636,0.8507,0,-0.5257,0.6511,-0.7501,0.1159,0.4636,-0.866,0.1875,0.5257,-0.8507,0,0.5844,-0.7408,0.3313,0.765,-0.5955,0.2453,0.6849,-0.5541,0.4732,0.8408,-0.397,0.368,0.7408,-0.3313,0.5844,0.866,-0.1875,0.4636,0.7501,-0.1159,0.6511,0.368,-0.8408,0.397,0.4732,-0.6849,0.5541,0.5541,-0.4732,0.6849,0.5955,-0.2453,0.765,0.2453,-0.765,0.5955,0.3313,-0.5844,0.7408,0.397,-0.368,0.8408,0.1159,-0.6511,0.7501,0.1875,-0.4636,0.866,0.3477,-0.9376,0,0.2531,-0.9455,0.2047,0.1308,-0.8965,0.4233,0,-0.7891,0.6142,-0.1159,-0.6511,0.7501,0.1227,-0.9924,0,0,-0.9773,0.2116,-0.1308,-0.8965,0.4233,-0.2453,-0.765,0.5955,-0.1227,-0.9924,0,-0.2531,-0.9455,0.2047,-0.368,-0.8408,0.397,-0.3477,-0.9376,0,-0.4636,-0.866,0.1875,0.3477,-0.9376,0,0.4636,-0.866,-0.1875,0.5257,-0.8507,0,0.2531,-0.9455,-0.2047,0.1227,-0.9924,0,0,-0.9773,-0.2116,-0.2531,-0.9455,-0.2047,-0.4636,-0.866,-0.1875,0.368,-0.8408,-0.397,0.1308,-0.8965,-0.4233,-0.1308,-0.8965,-0.4233,-0.368,-0.8408,-0.397,0.2453,-0.765,-0.5955,0,-0.7891,-0.6142,-0.2453,-0.765,-0.5955,0.1159,-0.6511,-0.7501,-0.1159,-0.6511,-0.7501,0.6511,-0.7501,-0.1159,0.5844,-0.7408,-0.3313,0.4732,-0.6849,-0.5541,0.3313,-0.5844,-0.7408,0.1875,-0.4636,-0.866,0.765,-0.5955,-0.2453,0.6849,-0.5541,-0.4732,0.5541,-0.4732,-0.6849,0.397,-0.368,-0.8408,0.8408,-0.397,-0.368,0.7408,-0.3313,-0.5844,0.5955,-0.2453,-0.765,0.866,-0.1875,-0.4636,0.7501,-0.1159,-0.6511,0.6511,-0.7501,-0.1159,0.7891,-0.6142,0,0.7891,-0.6142,0,0.8965,-0.4233,-0.1308,0.9455,-0.2047,-0.2531,0.9376,0,-0.3477,0.8965,-0.4233,0.1308,0.8965,-0.4233,-0.1308,0.9773,-0.2116,0,0.9773,-0.2116,0,0.9924,0,-0.1227,0.9455,-0.2047,0.2531,0.9924,0,0.1227,0.9924,0,-0.1227,0.9376,0,0.3477,0.2047,-0.2531,0.9455,0.4233,-0.1308,0.8965,0.6142,0,0.7891,0.2116,0,0.9773,0.4233,0.1308,0.8965,0.2047,0.2531,0.9455,-0.5844,-0.7408,0.3313,-0.4732,-0.6849,0.5541,-0.3313,-0.5844,0.7408,-0.6849,-0.5541,0.4732,-0.5541,-0.4732,0.6849,-0.7408,-0.3313,0.5844,-0.3313,-0.5844,-0.7408,-0.4732,-0.6849,-0.5541,-0.5844,-0.7408,-0.3313,-0.5541,-0.4732,-0.6849,-0.6849,-0.5541,-0.4732,-0.7408,-0.3313,-0.5844,0.6142,0,-0.7891,0.4233,-0.1308,-0.8965,0.2047,-0.2531,-0.9455,0.4233,0.1308,-0.8965,0.2116,0,-0.9773,0.2047,0.2531,-0.9455,0.9455,0.2047,0.2531,0.9773,0.2116,0,0.9455,0.2047,-0.2531,0.9773,0.2116,0,0.8965,0.4233,0.1308,0.8965,0.4233,-0.1308,0.8965,0.4233,-0.1308,0.7891,0.6142,0,0.7891,0.6142,0,0.6511,0.7501,-0.1159], 9 | indices: [0,1,2,0,3,1,4,3,0,4,5,3,6,5,4,6,7,5,8,7,6,8,9,7,10,9,8,3,11,1,3,12,11,5,12,3,5,13,12,7,13,5,7,14,13,9,14,7,12,15,11,12,16,15,13,16,12,13,17,16,14,17,13,16,18,15,16,19,18,17,19,16,19,20,18,1,21,2,1,22,21,11,22,1,11,23,22,15,23,11,15,24,23,18,24,15,18,25,24,20,25,18,22,26,21,22,27,26,23,27,22,23,28,27,24,28,23,24,29,28,25,29,24,27,30,26,27,31,30,28,31,27,28,32,31,29,32,28,31,33,30,31,34,33,32,34,31,34,35,33,21,36,2,21,37,36,26,37,21,26,38,37,39,38,26,39,40,38,41,40,39,41,42,40,43,42,41,37,44,36,37,45,44,38,45,37,38,46,45,40,46,38,40,47,46,42,47,40,45,48,44,45,49,48,46,49,45,46,50,49,47,50,46,49,51,48,49,52,51,50,52,49,52,53,51,36,54,2,36,55,54,44,55,36,44,56,55,48,56,44,48,57,56,51,57,48,51,58,57,53,58,51,55,59,54,55,60,59,56,60,55,56,61,60,57,61,56,57,62,61,58,62,57,60,63,59,60,64,63,61,64,60,61,65,64,62,65,61,64,66,63,64,67,66,65,67,64,67,68,66,54,0,2,54,69,0,59,69,54,59,70,69,63,70,59,63,71,70,66,71,63,66,72,71,68,72,66,69,4,0,69,73,4,70,73,69,70,74,73,71,74,70,71,75,74,72,75,71,73,6,4,73,76,6,74,76,73,74,77,76,75,77,74,76,8,6,76,78,8,77,78,76,78,10,8,34,79,35,34,80,79,32,80,34,32,81,80,29,81,32,29,82,81,25,82,29,25,83,82,20,83,25,80,84,79,80,85,84,81,85,80,81,86,85,82,86,81,82,87,86,83,87,82,85,88,84,85,89,88,86,89,85,86,90,89,87,90,86,89,91,88,89,92,91,90,92,89,92,93,91,19,94,20,19,95,94,17,95,19,17,96,95,14,96,17,14,97,96,9,97,14,9,98,97,10,98,9,95,99,94,95,100,99,96,100,95,96,101,100,97,101,96,97,102,101,98,102,97,100,103,99,100,104,103,101,104,100,101,105,104,102,105,101,104,106,103,104,107,106,105,107,104,107,108,106,78,109,10,78,110,109,77,110,78,77,111,110,75,111,77,75,112,111,72,112,75,72,113,112,68,113,72,110,114,109,110,115,114,111,115,110,111,116,115,112,116,111,112,117,116,113,117,112,115,118,114,115,119,118,116,119,115,116,120,119,117,120,116,119,121,118,119,122,121,120,122,119,122,123,121,67,124,68,67,125,124,65,125,67,65,126,125,62,126,65,62,127,126,58,127,62,58,128,127,53,128,58,125,129,124,125,130,129,126,130,125,126,131,130,127,131,126,127,132,131,128,132,127,130,133,129,130,134,133,131,134,130,131,135,134,132,135,131,134,136,133,134,137,136,135,137,134,137,138,136,52,139,53,52,140,139,50,140,52,50,141,140,47,141,50,47,142,141,42,142,47,42,143,142,43,143,42,140,144,139,140,145,144,141,145,140,141,146,145,142,146,141,142,147,146,143,147,142,145,148,144,145,149,148,146,149,145,146,150,149,147,150,146,149,151,148,149,152,151,150,152,149,152,153,151,154,155,156,154,157,155,158,157,154,158,159,157,160,159,158,160,161,159,162,161,160,162,163,161,93,163,162,157,164,155,157,165,164,159,165,157,159,166,165,161,166,159,161,167,166,163,167,161,165,168,164,165,169,168,166,169,165,166,170,169,167,170,166,169,171,168,169,172,171,170,172,169,172,108,171,155,173,156,155,174,173,164,174,155,164,175,174,168,175,164,168,176,175,171,176,168,171,177,176,108,177,171,174,178,173,174,179,178,175,179,174,175,180,179,176,180,175,176,181,180,177,181,176,179,182,178,179,183,182,180,183,179,180,184,183,181,184,180,183,185,182,183,186,185,184,186,183,186,123,185,187,188,189,187,190,188,191,190,187,191,192,190,182,192,191,182,193,192,185,193,182,185,194,193,123,194,185,190,195,188,190,196,195,192,196,190,192,197,196,193,197,192,193,198,197,194,198,193,196,199,195,196,200,199,197,200,196,197,201,200,198,201,197,200,202,199,200,203,202,201,203,200,203,138,202,188,204,189,188,205,204,195,205,188,195,206,205,199,206,195,199,207,206,202,207,199,202,208,207,138,208,202,205,209,204,205,210,209,206,210,205,206,211,210,207,211,206,207,212,211,208,212,207,210,213,209,210,214,213,211,214,210,211,215,214,212,215,211,214,216,213,214,217,216,215,217,214,217,153,216,218,154,156,218,219,154,209,220,204,209,221,220,213,221,209,213,222,221,216,222,213,216,223,222,153,223,216,219,158,154,219,224,158,225,224,219,225,226,224,222,227,221,222,228,227,223,228,222,224,160,158,224,229,160,226,229,224,226,230,229,231,230,226,229,162,160,229,232,162,230,232,229,232,93,162,172,106,108,172,233,106,170,233,172,170,234,233,167,234,170,167,235,234,163,235,167,163,92,235,93,92,163,233,103,106,233,236,103,234,236,233,234,237,236,235,237,234,235,90,237,92,90,235,236,99,103,236,238,99,237,238,236,237,87,238,90,87,237,238,94,99,238,83,94,87,83,238,83,20,94,186,121,123,186,239,121,184,239,186,184,240,239,181,240,184,181,241,240,177,241,181,177,107,241,108,107,177,239,118,121,239,242,118,240,242,239,240,243,242,241,243,240,241,105,243,107,105,241,242,114,118,242,244,114,243,244,242,243,102,244,105,102,243,244,109,114,244,98,109,102,98,244,98,10,109,203,136,138,203,245,136,201,245,203,201,246,245,198,246,201,198,247,246,194,247,198,194,122,247,123,122,194,245,133,136,245,248,133,246,248,245,246,249,248,247,249,246,247,120,249,122,120,247,248,129,133,248,250,129,249,250,248,249,117,250,120,117,249,250,124,129,250,113,124,117,113,250,113,68,124,217,151,153,217,251,151,215,251,217,215,252,251,212,252,215,212,253,252,208,253,212,208,137,253,138,137,208,251,148,151,251,254,148,252,254,251,252,255,254,253,255,252,253,135,255,137,135,253,254,144,148,254,256,144,255,256,254,255,132,256,135,132,255,256,139,144,256,128,139,132,128,256,128,53,139,232,91,93,232,257,91,230,257,232,230,258,257,231,258,230,228,259,260,223,259,228,223,152,259,153,152,223,257,88,91,257,261,88,258,261,257,258,262,261,259,263,260,259,150,263,152,150,259,261,84,88,261,264,84,262,264,261,263,147,265,150,147,263,264,79,84,264,266,79,147,143,265,266,35,79], 10 | }; 11 | export default model; -------------------------------------------------------------------------------- /src/geometry/loadModel.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three/webgpu"; 2 | import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'; 3 | import {OBJLoader} from "three/examples/jsm/loaders/OBJLoader"; 4 | 5 | const processTetGeometry = (tetVertsRaw, tetIdsRaw) => { 6 | let vertices = []; 7 | let tets = []; 8 | const addVertex = (x,y,z) => { 9 | const id = vertices.length; 10 | const vertex = new THREE.Vector3(Number(x),Number(y),Number(z)); 11 | vertex.id = id; 12 | vertex.tetCount = 0; 13 | vertices.push(vertex); 14 | } 15 | 16 | const addTet = (v0,v1,v2,v3) => { 17 | const id = tets.length; 18 | const center = v0.clone().add(v1).add(v2).add(v3).divideScalar(4); 19 | const tet = {id,v0,v1,v2,v3,center}; 20 | tets.push(tet); 21 | } 22 | 23 | 24 | for (let i=0; i < tetVertsRaw.length; i += 3) { 25 | const x = tetVertsRaw[i]; 26 | const y = tetVertsRaw[i+1]; 27 | const z = tetVertsRaw[i+2]; 28 | addVertex(x,y,z); 29 | } 30 | 31 | for (let i=0; i < tetIdsRaw.length; i += 4) { 32 | const a = vertices[tetIdsRaw[i]-1]; 33 | const b = vertices[tetIdsRaw[i+1]-1]; 34 | const c = vertices[tetIdsRaw[i+2]-1]; 35 | const d = vertices[tetIdsRaw[i+3]-1]; 36 | a.tetCount++; 37 | b.tetCount++; 38 | c.tetCount++; 39 | d.tetCount++; 40 | addTet(a,b,c,d); 41 | } 42 | 43 | vertices = vertices.filter(v => v.tetCount > 0); 44 | vertices.forEach((v, index) => { v.id = index; }); 45 | const tetVerts = vertices.map(v => [v.x,v.y,v.z]).flat(); 46 | const tetIds = tets.map(t => [t.v0.id,t.v1.id,t.v2.id,t.v3.id]).flat(); 47 | 48 | return { tetVerts, tetIds, vertices, tets }; 49 | } 50 | 51 | const processMsh = (msh) => { 52 | const vertexRegex = /\$Nodes\n\d+\n(.+)\$EndNodes/gms; 53 | const vertexMatch = vertexRegex.exec(msh); 54 | const vertexRepRegex = /\d+\s(.+)\n/gm; 55 | const tetVertsRaw = vertexMatch['1'].replace(vertexRepRegex, '$1 ').trim().split(' ').map(v => Number(v)).map(v => Math.round(v * 10000) / 10000); 56 | 57 | const tetRegex = /\$Elements\n\d+\n(.+)\$EndElements/gms; 58 | const tetMatch = tetRegex.exec(msh); 59 | const tetRepRegex = /.+(\s\d+\s\d+\s\d+\s\d+)\s\n/gm; 60 | const tetIdsRaw = tetMatch['1'].replace(tetRepRegex, '$1').trim().split(' ').map(v => Number(v)); 61 | return processTetGeometry(tetVertsRaw, tetIdsRaw); 62 | } 63 | 64 | const processGeometry = (geometry, tets) => { 65 | const positionAttribute = geometry.getAttribute("position"); 66 | const vertexCount = positionAttribute.count; 67 | const positionArray = positionAttribute.array; 68 | const normalArray = geometry.getAttribute("normal").array; 69 | const uvArray = geometry.getAttribute("uv").array; 70 | const indexArray = geometry.index.array; 71 | 72 | const tetIdArray = new Uint32Array(vertexCount); 73 | //const vertexIdArray = new Uint32Array(vertexCount*4); 74 | const tetBaryCoordsArray = new Float32Array(vertexCount * 3); 75 | 76 | const findClosestTet = (vertex) => { 77 | let minDist = 1e9; 78 | let closestTet = null; 79 | for (let i=0; i { 91 | const { v0,v1,v2,v3 } = tet; 92 | const a = v1.clone().sub(v0); 93 | const b = v2.clone().sub(v0); 94 | const c = v3.clone().sub(v0); 95 | const matrix = new THREE.Matrix3(a.x,b.x,c.x,a.y,b.y,c.y,a.z,b.z,c.z); 96 | return matrix.clone().invert(); 97 | } 98 | 99 | for (let i=0; iMath.round(v*10000)/10000), 126 | positions: [...positionArray].map(v=>Math.round(v*10000)/10000), 127 | normals: [...normalArray].map(v=>Math.round(v*10000)/10000), 128 | uvs: [...uvArray].map(v=>Math.round(v*10000)/10000), 129 | indices: [...indexArray], 130 | }; 131 | return processedModel; 132 | }; 133 | 134 | const processObj = (obj, tets) => { 135 | const objectRaw = new OBJLoader().parse(obj); 136 | const geometry = BufferGeometryUtils.mergeVertices(objectRaw.children[0].geometry); 137 | return processGeometry(geometry, tets); 138 | } 139 | 140 | const print = (model) => { 141 | let str = "const model = { \n"; 142 | Object.keys(model).forEach((key) => { 143 | str += " " + key + ": ["; 144 | str += model[key].join(",") 145 | str += "],\n" 146 | }); 147 | str += "};\n"; 148 | str += "export default model;" 149 | console.log(str); 150 | } 151 | 152 | export const loadModel = (msh, obj) => { 153 | const { tetVerts, tetIds, vertices, tets } = processMsh(msh); 154 | const { attachedTets, baryCoords, normals, uvs, positions, indices} = processObj(obj, tets); 155 | const model = { tetVerts, tetIds, attachedTets, baryCoords, normals, uvs, positions, indices }; 156 | print(model); 157 | return model; 158 | }; 159 | 160 | export const loadModelWithGeo = (msh, geo) => { 161 | const { tetVerts, tetIds, vertices, tets } = processMsh(msh); 162 | const { attachedTets, baryCoords, normals, uvs, positions, indices} = processGeometry(geo, tets); 163 | const model = { tetVerts, tetIds, attachedTets, baryCoords, normals, uvs, positions, indices }; 164 | print(model); 165 | return model; 166 | }; 167 | 168 | export const generateTube = (segments) => { 169 | const radius = 0.125; 170 | //const segments = 250; 171 | const capsuleRadius = Math.sqrt(4/Math.PI) * radius; 172 | const length = radius * segments * 2; 173 | 174 | const tetVertsRaw = []; 175 | const tetIdsRaw = []; 176 | 177 | const rr = radius; 178 | for (let x=0; x<=segments; x++) { 179 | const px = x * (length/segments) - length * 0.5; 180 | tetVertsRaw.push(rr, -px, -rr); 181 | tetVertsRaw.push(rr, -px, rr); 182 | tetVertsRaw.push(-rr, -px, rr); 183 | tetVertsRaw.push(-rr, -px, -rr); 184 | } 185 | tetVertsRaw.push( 0, (length*0.5 + capsuleRadius), 0); 186 | const bottomVert = tetVertsRaw.length / 3; 187 | tetVertsRaw.push( 0, -(length*0.5 + capsuleRadius), 0); 188 | const topVert = tetVertsRaw.length / 3; 189 | 190 | for (let x=0; x x*4+n; 192 | tetIdsRaw.push(v(1), v(4), v(8), v(7)); 193 | tetIdsRaw.push(v(1), v(8), v(5), v(7)); 194 | tetIdsRaw.push(v(1), v(5), v(6), v(7)); 195 | tetIdsRaw.push(v(1), v(6), v(2), v(7)); 196 | tetIdsRaw.push(v(1), v(2), v(3), v(7)); 197 | tetIdsRaw.push(v(1), v(3), v(4), v(7)); 198 | } 199 | tetIdsRaw.push(bottomVert, 1,2,3); 200 | tetIdsRaw.push(bottomVert, 1,3,4); 201 | tetIdsRaw.push(topVert, segments*4+1,segments*4+2,segments*4+3); 202 | tetIdsRaw.push(topVert, segments*4+1,segments*4+3,segments*4+4); 203 | 204 | //const geometry = new THREE.CylinderGeometry( radius, radius, length, 8, segments ); 205 | const geometry = new THREE.CapsuleGeometry(capsuleRadius, length, 4, 8, segments ); 206 | //geometry.rotateZ(Math.PI/2); 207 | const { tetVerts, tetIds, vertices, tets } = processTetGeometry(tetVertsRaw, tetIdsRaw); 208 | const { attachedTets, baryCoords, normals, uvs, positions, indices} = processGeometry(geometry, tets); 209 | 210 | for (let i=1; i Other experiments](https://holtsetio.com)", 32 | markdown: true, 33 | }) 34 | } 35 | } -------------------------------------------------------------------------------- /src/lights.js: -------------------------------------------------------------------------------- 1 | import * as THREE from "three/webgpu"; 2 | 3 | export class Lights { 4 | constructor() { 5 | this.object = new THREE.Object3D(); 6 | const light = new THREE.SpotLight(0xffffff, 5, 0, Math.PI * 0.075, 1, 0); 7 | const lightTarget = new THREE.Object3D(); 8 | light.position.set(100, 70, 74); 9 | lightTarget.position.set(0,0,0); 10 | light.target = lightTarget; 11 | 12 | this.object.add(light); 13 | this.object.add(lightTarget); 14 | //this.object.add(new THREE.SpotLightHelper(light, new THREE.Color(0,0,0))); 15 | 16 | light.castShadow = true; // default false 17 | light.shadow.mapSize.width = 512*2*2; // default 18 | light.shadow.mapSize.height = 512*2*2; // default 19 | light.shadow.bias = -0.000005; 20 | light.shadow.camera.near = 0.5; // default 21 | light.shadow.camera.far = 150; 22 | 23 | this.light = light; 24 | } 25 | 26 | update(elapsed) { 27 | 28 | } 29 | 30 | dispose() { 31 | this.light.shadow.dispose(); 32 | } 33 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import tslOperatorPlugin from 'vite-plugin-tsl-operator' 3 | 4 | export default defineConfig({ 5 | base: './', 6 | assetsInclude: ['**/*.hdr'], 7 | server: { 8 | port: 1234, 9 | }, 10 | plugins: [ 11 | tslOperatorPlugin({logs:false}) 12 | //.. other plugins 13 | ] 14 | }); --------------------------------------------------------------------------------