├── .github └── FUNDING.yml ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public ├── .gitkeep └── suzanne.glb ├── src ├── Debug.js ├── index.js ├── loaders │ ├── GLTFLoader.js │ ├── TextureLoader.js │ └── index.js ├── materials │ ├── SampleShaderMaterial │ │ ├── fragment.glsl │ │ ├── index.js │ │ └── vertex.glsl │ └── TSLSampleMaterial.js ├── physics │ ├── Body.js │ ├── Box.js │ ├── Floor.js │ └── Simulation.js ├── style.css └── webgpu.js ├── vite.config.js └── webgpu.html /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: fra_michelini 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: fra_michelini 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Francesco Michelini 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 | # ThreeJS starter 2 | 3 | This is a general template for ThreeJS applications. It uses [ViteJS](https://vitejs.dev/) to create the bundle and [Tweakpane](https://github.com/cocopon/tweakpane) for live updates. 4 | 5 | # Before we start 6 | This has been developed with NodeJS `16.11.0`; it should work with other versions too, but in case something doesn't work I recommend to switch to version `16.11.0` with [nvm](https://github.com/nvm-sh/nvm). 7 | 8 | ## Setup 9 | ```shell 10 | $ yarn install 11 | ``` 12 | 13 | ## Develop 14 | 15 | Run 16 | 17 | ```shell 18 | $ yarn dev 19 | ``` 20 | 21 | then open a new browser window and navigate to `http://localhost:1234` 22 | 23 | ## Debug panel (Tweakpane + Stats.js) 24 | The template uses dynamic imports to include the code to run the debug and performance panels. To display them, simply append `debug` to the URL's hash, i.e. `http://localhost:1234#debug`, or set the `debug` option to `true` in the app config object in `/src/index.js`. 25 | 26 | ## Physics (cannon-es) 27 | Since `v1.5.0`, the template features a basic physics setup with [cannon-es](https://github.com/pmndrs/cannon-es) that can be enabled simply by appending `physics` to the URL's hash, i.e. `http://localhost:1234#physics`, or setting the `physics` option to `true` in the app config object in `/src/index.js`. 28 | 29 | ## Build 30 | 31 | ```shell 32 | $ yarn build 33 | ``` 34 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ThreeJS Starter 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threejs-starter", 3 | "private": false, 4 | "version": "1.6.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "@types/three": "^0.167.2", 13 | "vite": "^5.4.2", 14 | "vite-plugin-glsl": "^1.3.0" 15 | }, 16 | "dependencies": { 17 | "cannon-es": "^0.20.0", 18 | "cannon-es-debugger": "^1.0.0", 19 | "stats.js": "^0.17.0", 20 | "three": "^0.167.1", 21 | "tweakpane": "^4.0.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | cannon-es: 12 | specifier: ^0.20.0 13 | version: 0.20.0 14 | cannon-es-debugger: 15 | specifier: ^1.0.0 16 | version: 1.0.0(cannon-es@0.20.0)(three@0.167.1) 17 | stats.js: 18 | specifier: ^0.17.0 19 | version: 0.17.0 20 | three: 21 | specifier: ^0.167.1 22 | version: 0.167.1 23 | tweakpane: 24 | specifier: ^4.0.4 25 | version: 4.0.4 26 | devDependencies: 27 | '@types/three': 28 | specifier: ^0.167.2 29 | version: 0.167.2 30 | vite: 31 | specifier: ^5.4.2 32 | version: 5.4.2 33 | vite-plugin-glsl: 34 | specifier: ^1.3.0 35 | version: 1.3.0(rollup@4.21.0)(vite@5.4.2) 36 | 37 | packages: 38 | 39 | '@esbuild/aix-ppc64@0.21.5': 40 | resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} 41 | engines: {node: '>=12'} 42 | cpu: [ppc64] 43 | os: [aix] 44 | 45 | '@esbuild/android-arm64@0.21.5': 46 | resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} 47 | engines: {node: '>=12'} 48 | cpu: [arm64] 49 | os: [android] 50 | 51 | '@esbuild/android-arm@0.21.5': 52 | resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} 53 | engines: {node: '>=12'} 54 | cpu: [arm] 55 | os: [android] 56 | 57 | '@esbuild/android-x64@0.21.5': 58 | resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} 59 | engines: {node: '>=12'} 60 | cpu: [x64] 61 | os: [android] 62 | 63 | '@esbuild/darwin-arm64@0.21.5': 64 | resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} 65 | engines: {node: '>=12'} 66 | cpu: [arm64] 67 | os: [darwin] 68 | 69 | '@esbuild/darwin-x64@0.21.5': 70 | resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} 71 | engines: {node: '>=12'} 72 | cpu: [x64] 73 | os: [darwin] 74 | 75 | '@esbuild/freebsd-arm64@0.21.5': 76 | resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} 77 | engines: {node: '>=12'} 78 | cpu: [arm64] 79 | os: [freebsd] 80 | 81 | '@esbuild/freebsd-x64@0.21.5': 82 | resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} 83 | engines: {node: '>=12'} 84 | cpu: [x64] 85 | os: [freebsd] 86 | 87 | '@esbuild/linux-arm64@0.21.5': 88 | resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} 89 | engines: {node: '>=12'} 90 | cpu: [arm64] 91 | os: [linux] 92 | 93 | '@esbuild/linux-arm@0.21.5': 94 | resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} 95 | engines: {node: '>=12'} 96 | cpu: [arm] 97 | os: [linux] 98 | 99 | '@esbuild/linux-ia32@0.21.5': 100 | resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} 101 | engines: {node: '>=12'} 102 | cpu: [ia32] 103 | os: [linux] 104 | 105 | '@esbuild/linux-loong64@0.21.5': 106 | resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} 107 | engines: {node: '>=12'} 108 | cpu: [loong64] 109 | os: [linux] 110 | 111 | '@esbuild/linux-mips64el@0.21.5': 112 | resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} 113 | engines: {node: '>=12'} 114 | cpu: [mips64el] 115 | os: [linux] 116 | 117 | '@esbuild/linux-ppc64@0.21.5': 118 | resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} 119 | engines: {node: '>=12'} 120 | cpu: [ppc64] 121 | os: [linux] 122 | 123 | '@esbuild/linux-riscv64@0.21.5': 124 | resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} 125 | engines: {node: '>=12'} 126 | cpu: [riscv64] 127 | os: [linux] 128 | 129 | '@esbuild/linux-s390x@0.21.5': 130 | resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} 131 | engines: {node: '>=12'} 132 | cpu: [s390x] 133 | os: [linux] 134 | 135 | '@esbuild/linux-x64@0.21.5': 136 | resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} 137 | engines: {node: '>=12'} 138 | cpu: [x64] 139 | os: [linux] 140 | 141 | '@esbuild/netbsd-x64@0.21.5': 142 | resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} 143 | engines: {node: '>=12'} 144 | cpu: [x64] 145 | os: [netbsd] 146 | 147 | '@esbuild/openbsd-x64@0.21.5': 148 | resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} 149 | engines: {node: '>=12'} 150 | cpu: [x64] 151 | os: [openbsd] 152 | 153 | '@esbuild/sunos-x64@0.21.5': 154 | resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} 155 | engines: {node: '>=12'} 156 | cpu: [x64] 157 | os: [sunos] 158 | 159 | '@esbuild/win32-arm64@0.21.5': 160 | resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} 161 | engines: {node: '>=12'} 162 | cpu: [arm64] 163 | os: [win32] 164 | 165 | '@esbuild/win32-ia32@0.21.5': 166 | resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} 167 | engines: {node: '>=12'} 168 | cpu: [ia32] 169 | os: [win32] 170 | 171 | '@esbuild/win32-x64@0.21.5': 172 | resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} 173 | engines: {node: '>=12'} 174 | cpu: [x64] 175 | os: [win32] 176 | 177 | '@rollup/pluginutils@5.1.0': 178 | resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} 179 | engines: {node: '>=14.0.0'} 180 | peerDependencies: 181 | rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 182 | peerDependenciesMeta: 183 | rollup: 184 | optional: true 185 | 186 | '@rollup/rollup-android-arm-eabi@4.21.0': 187 | resolution: {integrity: sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==} 188 | cpu: [arm] 189 | os: [android] 190 | 191 | '@rollup/rollup-android-arm64@4.21.0': 192 | resolution: {integrity: sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==} 193 | cpu: [arm64] 194 | os: [android] 195 | 196 | '@rollup/rollup-darwin-arm64@4.21.0': 197 | resolution: {integrity: sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==} 198 | cpu: [arm64] 199 | os: [darwin] 200 | 201 | '@rollup/rollup-darwin-x64@4.21.0': 202 | resolution: {integrity: sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==} 203 | cpu: [x64] 204 | os: [darwin] 205 | 206 | '@rollup/rollup-linux-arm-gnueabihf@4.21.0': 207 | resolution: {integrity: sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==} 208 | cpu: [arm] 209 | os: [linux] 210 | 211 | '@rollup/rollup-linux-arm-musleabihf@4.21.0': 212 | resolution: {integrity: sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==} 213 | cpu: [arm] 214 | os: [linux] 215 | 216 | '@rollup/rollup-linux-arm64-gnu@4.21.0': 217 | resolution: {integrity: sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==} 218 | cpu: [arm64] 219 | os: [linux] 220 | 221 | '@rollup/rollup-linux-arm64-musl@4.21.0': 222 | resolution: {integrity: sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==} 223 | cpu: [arm64] 224 | os: [linux] 225 | 226 | '@rollup/rollup-linux-powerpc64le-gnu@4.21.0': 227 | resolution: {integrity: sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==} 228 | cpu: [ppc64] 229 | os: [linux] 230 | 231 | '@rollup/rollup-linux-riscv64-gnu@4.21.0': 232 | resolution: {integrity: sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==} 233 | cpu: [riscv64] 234 | os: [linux] 235 | 236 | '@rollup/rollup-linux-s390x-gnu@4.21.0': 237 | resolution: {integrity: sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==} 238 | cpu: [s390x] 239 | os: [linux] 240 | 241 | '@rollup/rollup-linux-x64-gnu@4.21.0': 242 | resolution: {integrity: sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==} 243 | cpu: [x64] 244 | os: [linux] 245 | 246 | '@rollup/rollup-linux-x64-musl@4.21.0': 247 | resolution: {integrity: sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==} 248 | cpu: [x64] 249 | os: [linux] 250 | 251 | '@rollup/rollup-win32-arm64-msvc@4.21.0': 252 | resolution: {integrity: sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==} 253 | cpu: [arm64] 254 | os: [win32] 255 | 256 | '@rollup/rollup-win32-ia32-msvc@4.21.0': 257 | resolution: {integrity: sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==} 258 | cpu: [ia32] 259 | os: [win32] 260 | 261 | '@rollup/rollup-win32-x64-msvc@4.21.0': 262 | resolution: {integrity: sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==} 263 | cpu: [x64] 264 | os: [win32] 265 | 266 | '@tweenjs/tween.js@23.1.3': 267 | resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} 268 | 269 | '@types/estree@1.0.5': 270 | resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 271 | 272 | '@types/stats.js@0.17.3': 273 | resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==} 274 | 275 | '@types/three@0.167.2': 276 | resolution: {integrity: sha512-onxnIUNYpXcZJ5DTiIsxfnr4F9kAWkkxAUWx5yqzz/u0a4IygCLCjMuOl2DEeCxyJdJ2nOJZvKpu48sBMqfmkQ==} 277 | 278 | '@types/webxr@0.5.19': 279 | resolution: {integrity: sha512-4hxA+NwohSgImdTSlPXEqDqqFktNgmTXQ05ff1uWam05tNGroCMp4G+4XVl6qWm1p7GQ/9oD41kAYsSssF6Mzw==} 280 | 281 | cannon-es-debugger@1.0.0: 282 | resolution: {integrity: sha512-sE9lDOBAYFKlh+0w+cvWKwUhJef8HYnUSVPWPL0jD15MAuVRQKno4QYZSGxgOoJkMR3mQqxL4bxys2b3RSWH8g==} 283 | peerDependencies: 284 | cannon-es: 0.x 285 | three: 0.x 286 | typescript: '>=3.8' 287 | peerDependenciesMeta: 288 | typescript: 289 | optional: true 290 | 291 | cannon-es@0.20.0: 292 | resolution: {integrity: sha512-eZhWTZIkFOnMAJOgfXJa9+b3kVlvG+FX4mdkpePev/w/rP5V8NRquGyEozcjPfEoXUlb+p7d9SUcmDSn14prOA==} 293 | 294 | esbuild@0.21.5: 295 | resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} 296 | engines: {node: '>=12'} 297 | hasBin: true 298 | 299 | estree-walker@2.0.2: 300 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 301 | 302 | fflate@0.8.2: 303 | resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} 304 | 305 | fsevents@2.3.3: 306 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 307 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 308 | os: [darwin] 309 | 310 | meshoptimizer@0.18.1: 311 | resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} 312 | 313 | nanoid@3.3.7: 314 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 315 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 316 | hasBin: true 317 | 318 | picocolors@1.0.1: 319 | resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} 320 | 321 | picomatch@2.3.1: 322 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 323 | engines: {node: '>=8.6'} 324 | 325 | postcss@8.4.41: 326 | resolution: {integrity: sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==} 327 | engines: {node: ^10 || ^12 || >=14} 328 | 329 | rollup@4.21.0: 330 | resolution: {integrity: sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==} 331 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 332 | hasBin: true 333 | 334 | source-map-js@1.2.0: 335 | resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} 336 | engines: {node: '>=0.10.0'} 337 | 338 | stats.js@0.17.0: 339 | resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} 340 | 341 | three@0.167.1: 342 | resolution: {integrity: sha512-gYTLJA/UQip6J/tJvl91YYqlZF47+D/kxiWrbTon35ZHlXEN0VOo+Qke2walF1/x92v55H6enomymg4Dak52kw==} 343 | 344 | tweakpane@4.0.4: 345 | resolution: {integrity: sha512-RkWD54zDlEbnN01wQPk0ANHGbdCvlJx/E8A1QxhTfCbX+ROWos1Ws2MnhOm39aUGMOh+36TjUwpDmLfmwTr1Fg==} 346 | 347 | vite-plugin-glsl@1.3.0: 348 | resolution: {integrity: sha512-SzEoLet9Bp5VSozjrhUiSc3xX1+u7rCTjXAsq4qWM3u8UjilI76A9ucX/T+CRGQCe25j50GSY+9mKSGUVPET1w==} 349 | engines: {node: '>= 16.15.1', npm: '>= 8.11.0'} 350 | peerDependencies: 351 | vite: ^3.0.0 || ^4.0.0 || ^5.0.0 352 | 353 | vite@5.4.2: 354 | resolution: {integrity: sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==} 355 | engines: {node: ^18.0.0 || >=20.0.0} 356 | hasBin: true 357 | peerDependencies: 358 | '@types/node': ^18.0.0 || >=20.0.0 359 | less: '*' 360 | lightningcss: ^1.21.0 361 | sass: '*' 362 | sass-embedded: '*' 363 | stylus: '*' 364 | sugarss: '*' 365 | terser: ^5.4.0 366 | peerDependenciesMeta: 367 | '@types/node': 368 | optional: true 369 | less: 370 | optional: true 371 | lightningcss: 372 | optional: true 373 | sass: 374 | optional: true 375 | sass-embedded: 376 | optional: true 377 | stylus: 378 | optional: true 379 | sugarss: 380 | optional: true 381 | terser: 382 | optional: true 383 | 384 | snapshots: 385 | 386 | '@esbuild/aix-ppc64@0.21.5': 387 | optional: true 388 | 389 | '@esbuild/android-arm64@0.21.5': 390 | optional: true 391 | 392 | '@esbuild/android-arm@0.21.5': 393 | optional: true 394 | 395 | '@esbuild/android-x64@0.21.5': 396 | optional: true 397 | 398 | '@esbuild/darwin-arm64@0.21.5': 399 | optional: true 400 | 401 | '@esbuild/darwin-x64@0.21.5': 402 | optional: true 403 | 404 | '@esbuild/freebsd-arm64@0.21.5': 405 | optional: true 406 | 407 | '@esbuild/freebsd-x64@0.21.5': 408 | optional: true 409 | 410 | '@esbuild/linux-arm64@0.21.5': 411 | optional: true 412 | 413 | '@esbuild/linux-arm@0.21.5': 414 | optional: true 415 | 416 | '@esbuild/linux-ia32@0.21.5': 417 | optional: true 418 | 419 | '@esbuild/linux-loong64@0.21.5': 420 | optional: true 421 | 422 | '@esbuild/linux-mips64el@0.21.5': 423 | optional: true 424 | 425 | '@esbuild/linux-ppc64@0.21.5': 426 | optional: true 427 | 428 | '@esbuild/linux-riscv64@0.21.5': 429 | optional: true 430 | 431 | '@esbuild/linux-s390x@0.21.5': 432 | optional: true 433 | 434 | '@esbuild/linux-x64@0.21.5': 435 | optional: true 436 | 437 | '@esbuild/netbsd-x64@0.21.5': 438 | optional: true 439 | 440 | '@esbuild/openbsd-x64@0.21.5': 441 | optional: true 442 | 443 | '@esbuild/sunos-x64@0.21.5': 444 | optional: true 445 | 446 | '@esbuild/win32-arm64@0.21.5': 447 | optional: true 448 | 449 | '@esbuild/win32-ia32@0.21.5': 450 | optional: true 451 | 452 | '@esbuild/win32-x64@0.21.5': 453 | optional: true 454 | 455 | '@rollup/pluginutils@5.1.0(rollup@4.21.0)': 456 | dependencies: 457 | '@types/estree': 1.0.5 458 | estree-walker: 2.0.2 459 | picomatch: 2.3.1 460 | optionalDependencies: 461 | rollup: 4.21.0 462 | 463 | '@rollup/rollup-android-arm-eabi@4.21.0': 464 | optional: true 465 | 466 | '@rollup/rollup-android-arm64@4.21.0': 467 | optional: true 468 | 469 | '@rollup/rollup-darwin-arm64@4.21.0': 470 | optional: true 471 | 472 | '@rollup/rollup-darwin-x64@4.21.0': 473 | optional: true 474 | 475 | '@rollup/rollup-linux-arm-gnueabihf@4.21.0': 476 | optional: true 477 | 478 | '@rollup/rollup-linux-arm-musleabihf@4.21.0': 479 | optional: true 480 | 481 | '@rollup/rollup-linux-arm64-gnu@4.21.0': 482 | optional: true 483 | 484 | '@rollup/rollup-linux-arm64-musl@4.21.0': 485 | optional: true 486 | 487 | '@rollup/rollup-linux-powerpc64le-gnu@4.21.0': 488 | optional: true 489 | 490 | '@rollup/rollup-linux-riscv64-gnu@4.21.0': 491 | optional: true 492 | 493 | '@rollup/rollup-linux-s390x-gnu@4.21.0': 494 | optional: true 495 | 496 | '@rollup/rollup-linux-x64-gnu@4.21.0': 497 | optional: true 498 | 499 | '@rollup/rollup-linux-x64-musl@4.21.0': 500 | optional: true 501 | 502 | '@rollup/rollup-win32-arm64-msvc@4.21.0': 503 | optional: true 504 | 505 | '@rollup/rollup-win32-ia32-msvc@4.21.0': 506 | optional: true 507 | 508 | '@rollup/rollup-win32-x64-msvc@4.21.0': 509 | optional: true 510 | 511 | '@tweenjs/tween.js@23.1.3': {} 512 | 513 | '@types/estree@1.0.5': {} 514 | 515 | '@types/stats.js@0.17.3': {} 516 | 517 | '@types/three@0.167.2': 518 | dependencies: 519 | '@tweenjs/tween.js': 23.1.3 520 | '@types/stats.js': 0.17.3 521 | '@types/webxr': 0.5.19 522 | fflate: 0.8.2 523 | meshoptimizer: 0.18.1 524 | 525 | '@types/webxr@0.5.19': {} 526 | 527 | cannon-es-debugger@1.0.0(cannon-es@0.20.0)(three@0.167.1): 528 | dependencies: 529 | cannon-es: 0.20.0 530 | three: 0.167.1 531 | 532 | cannon-es@0.20.0: {} 533 | 534 | esbuild@0.21.5: 535 | optionalDependencies: 536 | '@esbuild/aix-ppc64': 0.21.5 537 | '@esbuild/android-arm': 0.21.5 538 | '@esbuild/android-arm64': 0.21.5 539 | '@esbuild/android-x64': 0.21.5 540 | '@esbuild/darwin-arm64': 0.21.5 541 | '@esbuild/darwin-x64': 0.21.5 542 | '@esbuild/freebsd-arm64': 0.21.5 543 | '@esbuild/freebsd-x64': 0.21.5 544 | '@esbuild/linux-arm': 0.21.5 545 | '@esbuild/linux-arm64': 0.21.5 546 | '@esbuild/linux-ia32': 0.21.5 547 | '@esbuild/linux-loong64': 0.21.5 548 | '@esbuild/linux-mips64el': 0.21.5 549 | '@esbuild/linux-ppc64': 0.21.5 550 | '@esbuild/linux-riscv64': 0.21.5 551 | '@esbuild/linux-s390x': 0.21.5 552 | '@esbuild/linux-x64': 0.21.5 553 | '@esbuild/netbsd-x64': 0.21.5 554 | '@esbuild/openbsd-x64': 0.21.5 555 | '@esbuild/sunos-x64': 0.21.5 556 | '@esbuild/win32-arm64': 0.21.5 557 | '@esbuild/win32-ia32': 0.21.5 558 | '@esbuild/win32-x64': 0.21.5 559 | 560 | estree-walker@2.0.2: {} 561 | 562 | fflate@0.8.2: {} 563 | 564 | fsevents@2.3.3: 565 | optional: true 566 | 567 | meshoptimizer@0.18.1: {} 568 | 569 | nanoid@3.3.7: {} 570 | 571 | picocolors@1.0.1: {} 572 | 573 | picomatch@2.3.1: {} 574 | 575 | postcss@8.4.41: 576 | dependencies: 577 | nanoid: 3.3.7 578 | picocolors: 1.0.1 579 | source-map-js: 1.2.0 580 | 581 | rollup@4.21.0: 582 | dependencies: 583 | '@types/estree': 1.0.5 584 | optionalDependencies: 585 | '@rollup/rollup-android-arm-eabi': 4.21.0 586 | '@rollup/rollup-android-arm64': 4.21.0 587 | '@rollup/rollup-darwin-arm64': 4.21.0 588 | '@rollup/rollup-darwin-x64': 4.21.0 589 | '@rollup/rollup-linux-arm-gnueabihf': 4.21.0 590 | '@rollup/rollup-linux-arm-musleabihf': 4.21.0 591 | '@rollup/rollup-linux-arm64-gnu': 4.21.0 592 | '@rollup/rollup-linux-arm64-musl': 4.21.0 593 | '@rollup/rollup-linux-powerpc64le-gnu': 4.21.0 594 | '@rollup/rollup-linux-riscv64-gnu': 4.21.0 595 | '@rollup/rollup-linux-s390x-gnu': 4.21.0 596 | '@rollup/rollup-linux-x64-gnu': 4.21.0 597 | '@rollup/rollup-linux-x64-musl': 4.21.0 598 | '@rollup/rollup-win32-arm64-msvc': 4.21.0 599 | '@rollup/rollup-win32-ia32-msvc': 4.21.0 600 | '@rollup/rollup-win32-x64-msvc': 4.21.0 601 | fsevents: 2.3.3 602 | 603 | source-map-js@1.2.0: {} 604 | 605 | stats.js@0.17.0: {} 606 | 607 | three@0.167.1: {} 608 | 609 | tweakpane@4.0.4: {} 610 | 611 | vite-plugin-glsl@1.3.0(rollup@4.21.0)(vite@5.4.2): 612 | dependencies: 613 | '@rollup/pluginutils': 5.1.0(rollup@4.21.0) 614 | vite: 5.4.2 615 | transitivePeerDependencies: 616 | - rollup 617 | 618 | vite@5.4.2: 619 | dependencies: 620 | esbuild: 0.21.5 621 | postcss: 8.4.41 622 | rollup: 4.21.0 623 | optionalDependencies: 624 | fsevents: 2.3.3 625 | -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kekkorider/threejs-starter/46be9b8f3698f0b21fd3f74e44daf2d9eb4fd523/public/.gitkeep -------------------------------------------------------------------------------- /public/suzanne.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kekkorider/threejs-starter/46be9b8f3698f0b21fd3f74e44daf2d9eb4fd523/public/suzanne.glb -------------------------------------------------------------------------------- /src/Debug.js: -------------------------------------------------------------------------------- 1 | import { Pane } from 'tweakpane' 2 | import { Color, Vector4 } from 'three' 3 | 4 | export class Debug { 5 | constructor(app) { 6 | this.app = app 7 | 8 | this.#createPanel() 9 | this.#createSceneConfig() 10 | this.#createPhysicsConfig() 11 | this.#createBoxConfig() 12 | this.#createShadedBoxConfig() 13 | this.#createLightConfig() 14 | } 15 | 16 | refresh() { 17 | this.pane.refresh() 18 | } 19 | 20 | #createPanel() { 21 | this.pane = new Pane({ 22 | container: document.querySelector('#debug') 23 | }) 24 | } 25 | 26 | #createSceneConfig() { 27 | const folder = this.pane.addFolder({ title: 'Scene' }) 28 | 29 | const params = { 30 | background: { r: 18, g: 18, b: 18 } 31 | } 32 | 33 | folder.addBinding(params, 'background', { label: 'Background Color' }).on('change', e => { 34 | this.app.renderer.setClearColor(new Color(e.value.r / 255, e.value.g / 255, e.value.b / 255)) 35 | }) 36 | } 37 | 38 | #createPhysicsConfig() { 39 | if (!this.app.hasPhysics) return 40 | 41 | const folder = this.pane.addFolder({ title: 'Physics' }) 42 | 43 | folder.addButton({ title: 'Toggle Debug' }).on('click', () => { 44 | window.dispatchEvent(new CustomEvent('togglePhysicsDebug')) 45 | }) 46 | } 47 | 48 | #createBoxConfig() { 49 | const folder = this.pane.addFolder({ title: 'Box' }) 50 | const mesh = this.app.box 51 | 52 | this.#createColorControl(mesh.material, folder) 53 | 54 | folder.addBinding(mesh.material, 'metalness', { label: 'Metallic', min: 0, max: 1 }) 55 | folder.addBinding(mesh.material, 'roughness', { label: 'Roughness', min: 0, max: 1 }) 56 | } 57 | 58 | #createShadedBoxConfig() { 59 | const folder = this.pane.addFolder({ title: 'Shaded Box' }) 60 | const mesh = this.app.shadedBox 61 | 62 | folder.addBinding(mesh.scale, 'x', { label: 'Width', min: 0.1, max: 4 }) 63 | folder.addBinding(mesh.scale, 'y', { label: 'Height', min: 0.1, max: 4 }) 64 | folder.addBinding(mesh.scale, 'z', { label: 'Depth', min: 0.1, max: 4 }) 65 | } 66 | 67 | #createLightConfig() { 68 | const folder = this.pane.addFolder({ title: 'Light' }) 69 | 70 | this.#createColorControl(this.app.pointLight, folder) 71 | 72 | folder.addBinding(this.app.pointLight, 'intensity', { label: 'Intensity', min: 0, max: 1000 }) 73 | } 74 | 75 | /** 76 | * Adds a color control for the given object to the given folder. 77 | * 78 | * @param {*} obj Any THREE object with a color property 79 | * @param {*} folder The folder to add the control to 80 | */ 81 | #createColorControl(obj, folder) { 82 | const baseColor255 = obj.color.clone().multiplyScalar(255) 83 | const params = { color: { r: baseColor255.r, g: baseColor255.g, b: baseColor255.b } } 84 | 85 | folder.addBinding(params, 'color', { label: 'Color' }).on('change', e => { 86 | obj.color.setRGB(e.value.r, e.value.g, e.value.b).multiplyScalar(1 / 255) 87 | }) 88 | } 89 | 90 | /** 91 | * Adds a color control for a custom uniform to the given object in the given folder. 92 | * 93 | * @param {THREE.Mesh} obj A `THREE.Mesh` object 94 | * @param {*} folder The folder to add the control to 95 | * @param {String} uniformName The name of the uniform to control 96 | * @param {String} label The label to use for the control 97 | */ 98 | #createColorUniformControl(obj, folder, uniformName, label = 'Color') { 99 | const preMultVector = new Vector4(255, 255, 255, 1) 100 | const postMultVector = new Vector4(1 / 255, 1 / 255, 1 / 255, 1) 101 | 102 | const baseColor255 = obj.material.uniforms[uniformName].value.clone().multiply(preMultVector) 103 | const params = { color: { r: baseColor255.x, g: baseColor255.y, b: baseColor255.z, a: baseColor255.w } } 104 | 105 | folder.addInput(params, 'color', { label, view: 'color', color: { alpha: true } }).on('change', e => { 106 | obj.material.uniforms[uniformName].value.set(e.value.r, e.value.g, e.value.b, e.value.a).multiply(postMultVector) 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, 3 | WebGLRenderer, 4 | PerspectiveCamera, 5 | BoxGeometry, 6 | MeshStandardMaterial, 7 | Mesh, 8 | PointLight, 9 | Clock, 10 | Vector2, 11 | PlaneGeometry, 12 | MeshBasicMaterial 13 | } from 'three' 14 | 15 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' 16 | 17 | import { SampleShaderMaterial } from './materials/SampleShaderMaterial' 18 | import { gltfLoader } from './loaders' 19 | 20 | class App { 21 | #resizeCallback = () => this.#onResize() 22 | 23 | constructor(container, opts = { physics: false, debug: false }) { 24 | this.container = document.querySelector(container) 25 | this.screen = new Vector2(this.container.clientWidth, this.container.clientHeight) 26 | 27 | this.hasPhysics = opts.physics 28 | this.hasDebug = opts.debug 29 | } 30 | 31 | async init() { 32 | this.#createScene() 33 | this.#createCamera() 34 | this.#createRenderer() 35 | 36 | if (this.hasPhysics) { 37 | const { Simulation } = await import('./physics/Simulation') 38 | this.simulation = new Simulation(this) 39 | 40 | const { PhysicsBox } = await import('./physics/Box') 41 | const { PhysicsFloor } = await import('./physics/Floor') 42 | 43 | Object.assign(this, { PhysicsBox, PhysicsFloor }) 44 | } 45 | 46 | this.#createBox() 47 | this.#createShadedBox() 48 | this.#createLight() 49 | this.#createFloor() 50 | this.#createClock() 51 | this.#addListeners() 52 | this.#createControls() 53 | 54 | await this.#loadModel() 55 | 56 | if (this.hasDebug) { 57 | const { Debug } = await import('./Debug.js') 58 | new Debug(this) 59 | 60 | const { default: Stats } = await import('stats.js') 61 | this.stats = new Stats() 62 | document.body.appendChild(this.stats.dom) 63 | } 64 | 65 | this.renderer.setAnimationLoop(() => { 66 | this.stats?.begin() 67 | 68 | this.#update() 69 | this.#render() 70 | 71 | this.stats?.end() 72 | }) 73 | 74 | console.log(this) 75 | } 76 | 77 | destroy() { 78 | this.renderer.dispose() 79 | this.#removeListeners() 80 | } 81 | 82 | #update() { 83 | const elapsed = this.clock.getElapsedTime() 84 | 85 | this.box.rotation.y = elapsed*0.72 86 | this.box.rotation.z = elapsed*0.6 87 | 88 | this.shadedBox.rotation.y = elapsed 89 | this.shadedBox.rotation.z = elapsed*0.6 90 | 91 | this.simulation?.update() 92 | } 93 | 94 | #render() { 95 | this.renderer.render(this.scene, this.camera) 96 | } 97 | 98 | #createScene() { 99 | this.scene = new Scene() 100 | } 101 | 102 | #createCamera() { 103 | this.camera = new PerspectiveCamera(75, this.screen.x / this.screen.y, 0.1, 100) 104 | this.camera.position.set(-0.7, 0.8, 3) 105 | } 106 | 107 | #createRenderer() { 108 | const params = { 109 | alpha: true, 110 | antialias: window.devicePixelRatio === 1 111 | } 112 | 113 | this.renderer = new WebGLRenderer({ ...params }) 114 | 115 | this.container.appendChild(this.renderer.domElement) 116 | 117 | this.renderer.setSize(this.screen.x, this.screen.y) 118 | this.renderer.setPixelRatio(Math.min(1.5, window.devicePixelRatio)) 119 | this.renderer.setClearColor(0x121212) 120 | this.renderer.physicallyCorrectLights = true 121 | } 122 | 123 | #createLight() { 124 | this.pointLight = new PointLight(0xff0055, 500, 100, 2) 125 | this.pointLight.position.set(0, 10, 13) 126 | this.scene.add(this.pointLight) 127 | } 128 | 129 | /** 130 | * Create a box with a PBR material 131 | */ 132 | #createBox() { 133 | const geometry = new BoxGeometry(1, 1, 1, 1, 1, 1) 134 | 135 | const material = new MeshStandardMaterial({ 136 | color: 0xffffff, 137 | metalness: 0.7, 138 | roughness: 0.35 139 | }) 140 | 141 | this.box = new Mesh(geometry, material) 142 | this.box.position.x = -1.5 143 | this.box.rotation.set( 144 | Math.random() * Math.PI, 145 | Math.random() * Math.PI, 146 | Math.random() * Math.PI 147 | ) 148 | 149 | this.scene.add(this.box) 150 | 151 | if (!this.hasPhysics) return 152 | 153 | const body = new this.PhysicsBox(this.box, this.scene) 154 | this.simulation.addItem(body) 155 | } 156 | 157 | /** 158 | * Create a box with a custom ShaderMaterial 159 | */ 160 | #createShadedBox() { 161 | const geometry = new BoxGeometry(1, 1, 1, 1, 1, 1) 162 | 163 | this.shadedBox = new Mesh(geometry, SampleShaderMaterial) 164 | this.shadedBox.position.x = 1.5 165 | 166 | this.scene.add(this.shadedBox) 167 | } 168 | 169 | #createFloor() { 170 | if (!this.hasPhysics) return 171 | 172 | const geometry = new PlaneGeometry(20, 20, 1, 1) 173 | const material = new MeshBasicMaterial({ color: 0x424242 }) 174 | 175 | this.floor = new Mesh(geometry, material) 176 | this.floor.rotateX(-Math.PI*0.5) 177 | this.floor.position.set(0, -2, 0) 178 | 179 | this.scene.add(this.floor) 180 | 181 | const body = new this.PhysicsFloor(this.floor, this.scene) 182 | this.simulation.addItem(body) 183 | } 184 | 185 | /** 186 | * Load a 3D model and append it to the scene 187 | */ 188 | async #loadModel() { 189 | const gltf = await gltfLoader.load('/suzanne.glb') 190 | 191 | const mesh = gltf.scene.children[0] 192 | mesh.position.z = 1.5 193 | 194 | mesh.material = SampleShaderMaterial 195 | 196 | this.scene.add(mesh) 197 | } 198 | 199 | #createControls() { 200 | this.controls = new OrbitControls(this.camera, this.renderer.domElement) 201 | } 202 | 203 | #createClock() { 204 | this.clock = new Clock() 205 | } 206 | 207 | #addListeners() { 208 | window.addEventListener('resize', this.#resizeCallback, { passive: true }) 209 | } 210 | 211 | #removeListeners() { 212 | window.removeEventListener('resize', this.#resizeCallback, { passive: true }) 213 | } 214 | 215 | #onResize() { 216 | this.screen.set(this.container.clientWidth, this.container.clientHeight) 217 | 218 | this.camera.aspect = this.screen.x / this.screen.y 219 | this.camera.updateProjectionMatrix() 220 | 221 | this.renderer.setSize(this.screen.x, this.screen.y) 222 | } 223 | } 224 | 225 | window._APP_ = new App('#app', { 226 | physics: window.location.hash.includes('physics'), 227 | debug: window.location.hash.includes('debug') 228 | }) 229 | 230 | window._APP_.init() 231 | -------------------------------------------------------------------------------- /src/loaders/GLTFLoader.js: -------------------------------------------------------------------------------- 1 | import { GLTFLoader as ThreeGLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' 2 | 3 | export class GLTFLoader { 4 | constructor(manager) { 5 | this.loader = new ThreeGLTFLoader(manager) 6 | } 7 | 8 | /** 9 | * Load a single model or an array of models. 10 | * 11 | * @param {String|String[]} resources Single URL or array of URLs of the model(s) to load. 12 | * @returns Object|Object[] 13 | */ 14 | async load(resources) { 15 | if (Array.isArray(resources)) { 16 | const promises = resources.map(url => this.#loadModel(url)) 17 | return await Promise.all(promises) 18 | } else { 19 | return await this.#loadModel(resources) 20 | } 21 | } 22 | 23 | /** 24 | * Load a single model. 25 | * 26 | * @param {String} url The URL of the model to load 27 | * @returns Promise 28 | */ 29 | #loadModel(url) { 30 | return new Promise(resolve => { 31 | this.loader.load(url, model => { 32 | resolve(model) 33 | }) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/loaders/TextureLoader.js: -------------------------------------------------------------------------------- 1 | import { TextureLoader as ThreeTextureLoader } from 'three' 2 | 3 | export class TextureLoader { 4 | constructor(manager) { 5 | this.loader = new ThreeTextureLoader(manager) 6 | } 7 | 8 | /** 9 | * Load a single texture or an array of textures. 10 | * 11 | * @param {String|String[]} resources Single URL or array of URLs of the texture(s) to load. 12 | * @returns Texture|Texture[] 13 | */ 14 | async load(resources) { 15 | if (Array.isArray(resources)) { 16 | const promises = resources.map(url => this.#loadTexture(url)) 17 | return await Promise.all(promises) 18 | } else { 19 | return await this.#loadTexture(resources) 20 | } 21 | } 22 | 23 | /** 24 | * Load a single texture. 25 | * 26 | * @param {String} url The URL of the texture to load 27 | * @returns Promise 28 | */ 29 | #loadTexture(url) { 30 | return new Promise(resolve => { 31 | this.loader.load(url, texture => { 32 | resolve(texture) 33 | }) 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/loaders/index.js: -------------------------------------------------------------------------------- 1 | import { LoadingManager } from 'three' 2 | import { TextureLoader } from './TextureLoader' 3 | import { GLTFLoader } from './GLTFLoader' 4 | 5 | /** 6 | * Loading manager 7 | */ 8 | const loadingManager = new LoadingManager() 9 | 10 | loadingManager.onProgress = (url, loaded, total) => { 11 | // In case the progress count is not correct, see this: 12 | // https://discourse.threejs.org/t/gltf-file-loaded-twice-when-loading-is-initiated-in-loadingmanager-inside-onprogress-callback/27799/2 13 | console.log(`Loaded ${loaded} resources out of ${total} -> ${url}`) 14 | } 15 | 16 | /** 17 | * Texture Loader 18 | */ 19 | export const textureLoader = new TextureLoader(loadingManager) 20 | 21 | /** 22 | * GLTF Models 23 | */ 24 | export const gltfLoader = new GLTFLoader(loadingManager) 25 | -------------------------------------------------------------------------------- /src/materials/SampleShaderMaterial/fragment.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() { 4 | vec3 color = vec3(vUv, 0.5); 5 | 6 | gl_FragColor = vec4(color, 1.0); 7 | } 8 | -------------------------------------------------------------------------------- /src/materials/SampleShaderMaterial/index.js: -------------------------------------------------------------------------------- 1 | import { ShaderMaterial } from 'three' 2 | 3 | import vertexShader from './vertex.glsl' 4 | import fragmentShader from './fragment.glsl' 5 | 6 | export const SampleShaderMaterial = new ShaderMaterial({ 7 | vertexShader, 8 | fragmentShader, 9 | transparent: false 10 | }) 11 | -------------------------------------------------------------------------------- /src/materials/SampleShaderMaterial/vertex.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() { 4 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 5 | 6 | vUv = uv; 7 | } 8 | -------------------------------------------------------------------------------- /src/materials/TSLSampleMaterial.js: -------------------------------------------------------------------------------- 1 | import { MeshBasicNodeMaterial, vec3, vec4, timerLocal, sin, cos, mix, color, smoothstep, tslFn, positionLocal, remap, normalLocal } from 'three/tsl' 2 | 3 | const time = timerLocal() 4 | 5 | const material = new MeshBasicNodeMaterial() 6 | 7 | const calculatePos = tslFn(() => { 8 | const x1 = remap(cos(positionLocal.x.add(time).mul(1.2)), -1, 1, 0, 1) 9 | const x2 = remap(sin(positionLocal.x.add(time.mul(0.5)).mul(5.5)), -1, 1, 0, 1) 10 | const x = x1.add(x2) 11 | 12 | const y1 = remap(sin(positionLocal.y.sub(time.mul(0.03)).mul(7)), -1, 1, 0, 1) 13 | const y2 = remap(sin(positionLocal.y.sub(time.mul(0.52)).mul(3.6)), -1, 1, 0, 1) 14 | const y = y1.add(y2) 15 | 16 | const z1 = remap(sin(positionLocal.z.sub(time.mul(0.73)).mul(3)), -1, 1, 0, 1) 17 | const z2 = remap(sin(positionLocal.z.add(time.mul(0.23)).mul(7.3)), -1, 1, 0, 1) 18 | const z = z1.add(z2) 19 | 20 | return vec3(x, y, z) 21 | }) 22 | 23 | material.colorNode = tslFn(() => { 24 | const pos = calculatePos() 25 | 26 | const dist = smoothstep(0.6, 1.8, pos.length()) 27 | 28 | const colorA = color('#F08700') 29 | const colorB = color('#00A6A6') 30 | 31 | const col = mix(colorA, colorB, dist) 32 | 33 | return vec4(col, 1) 34 | })() 35 | 36 | material.positionNode = tslFn(() => { 37 | const pos = calculatePos() 38 | 39 | return positionLocal.add(normalLocal.mul(pos.mul(0.25))) 40 | })() 41 | 42 | export const TSLSampleMaterial = material 43 | -------------------------------------------------------------------------------- /src/physics/Body.js: -------------------------------------------------------------------------------- 1 | export class PhysicsBody { 2 | constructor(mesh, scene) { 3 | this.scene = scene 4 | this.mesh = mesh 5 | 6 | this.body = null 7 | } 8 | 9 | update() { 10 | this.mesh.position.copy(this.body.position) 11 | this.mesh.quaternion.copy(this.body.quaternion) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/physics/Box.js: -------------------------------------------------------------------------------- 1 | import { Box, Body, Vec3 } from 'cannon-es' 2 | import { PhysicsBody } from './Body' 3 | 4 | export class PhysicsBox extends PhysicsBody { 5 | constructor(mesh, scene) { 6 | super(mesh, scene) 7 | 8 | this.#addBody() 9 | } 10 | 11 | #addBody() { 12 | const { position, quaternion } = this.mesh 13 | const { width, height, depth } = this.mesh.geometry.parameters 14 | const halfExtents = new Vec3(width / 2, height / 2, depth / 2) 15 | 16 | this.body = new Body({ 17 | mass: 1, 18 | position, 19 | quaternion, 20 | shape: new Box(halfExtents) 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/physics/Floor.js: -------------------------------------------------------------------------------- 1 | import { Plane, Body } from 'cannon-es' 2 | import { PhysicsBody } from './Body' 3 | 4 | export class PhysicsFloor extends PhysicsBody { 5 | constructor(mesh, scene) { 6 | super(mesh, scene) 7 | 8 | this.#addBody() 9 | } 10 | 11 | #addBody() { 12 | const { quaternion, position } = this.mesh 13 | 14 | this.body = new Body({ 15 | mass: 0, 16 | quaternion, 17 | position, 18 | shape: new Plane() 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/physics/Simulation.js: -------------------------------------------------------------------------------- 1 | import { World, Vec3, SAPBroadphase } from 'cannon-es' 2 | 3 | export class Simulation { 4 | constructor(app) { 5 | this.app = app 6 | this.items = [] 7 | 8 | this.init() 9 | } 10 | 11 | async init() { 12 | this.world = new World({ 13 | gravity: new Vec3(0, -9.82, 0), 14 | broadphase: new SAPBroadphase() 15 | }) 16 | 17 | if (this.app.hasDebug) { 18 | const { default: Debugger } = await import('cannon-es-debugger') 19 | 20 | this.debugger = new Debugger(this.app.scene, this.world, { 21 | color: 0x005500, 22 | onInit: (body, mesh) => { 23 | window.addEventListener('togglePhysicsDebug', () => { 24 | mesh.visible = !mesh.visible 25 | }) 26 | } 27 | }) 28 | } 29 | } 30 | 31 | addItem(item) { 32 | this.items.push(item) 33 | this.world.addBody(item.body) 34 | } 35 | 36 | removeItem(item) { 37 | setTimeout(() => { 38 | this.items = this.items.filter((b) => b !== item) 39 | this.world.removeBody(item.body) 40 | }, 0) 41 | } 42 | 43 | update() { 44 | this.world.fixedStep() 45 | 46 | for (const item of this.items) { 47 | item.update() 48 | } 49 | 50 | this.debugger?.update() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | height: 100vh; 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | #app { 9 | height: 100%; 10 | width: 100%; 11 | } 12 | 13 | #app canvas { 14 | outline: none; 15 | } 16 | 17 | #debug { 18 | position: fixed; 19 | right: 10px; 20 | top: 10px; 21 | width: 350px; 22 | z-index: 10; 23 | } 24 | -------------------------------------------------------------------------------- /src/webgpu.js: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, 3 | PerspectiveCamera, 4 | Mesh, 5 | Clock, 6 | Vector2, 7 | IcosahedronGeometry 8 | } from 'three' 9 | 10 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' 11 | import { WebGPURenderer } from 'three/webgpu' 12 | 13 | import { TSLSampleMaterial } from './materials/TSLSampleMaterial' 14 | 15 | class App { 16 | #resizeCallback = () => this.#onResize() 17 | 18 | constructor(container, opts = { debug: false }) { 19 | this.container = document.querySelector(container) 20 | this.screen = new Vector2(this.container.clientWidth, this.container.clientHeight) 21 | 22 | this.hasDebug = opts.debug 23 | } 24 | 25 | async init() { 26 | this.#createScene() 27 | this.#createCamera() 28 | this.#createRenderer() 29 | 30 | this.#createMesh() 31 | this.#createClock() 32 | this.#addListeners() 33 | this.#createControls() 34 | 35 | if (this.hasDebug) { 36 | const { Debug } = await import('./Debug.js') 37 | new Debug(this) 38 | 39 | const { default: Stats } = await import('stats.js') 40 | this.stats = new Stats() 41 | document.body.appendChild(this.stats.dom) 42 | } 43 | 44 | this.renderer.setAnimationLoop(() => { 45 | this.stats?.begin() 46 | 47 | this.#update() 48 | this.#render() 49 | 50 | this.stats?.end() 51 | }) 52 | 53 | console.log(this) 54 | } 55 | 56 | destroy() { 57 | this.renderer.dispose() 58 | this.#removeListeners() 59 | } 60 | 61 | #update() { 62 | // const elapsed = this.clock.getElapsedTime() 63 | 64 | // this.mesh.rotation.y = elapsed*0.22 65 | // this.mesh.rotation.z = elapsed*0.15 66 | } 67 | 68 | #render() { 69 | this.renderer.render(this.scene, this.camera) 70 | } 71 | 72 | #createScene() { 73 | this.scene = new Scene() 74 | } 75 | 76 | #createCamera() { 77 | this.camera = new PerspectiveCamera(75, this.screen.x / this.screen.y, 0.1, 100) 78 | this.camera.position.set(0, 0.8, 3) 79 | } 80 | 81 | #createRenderer() { 82 | const params = { 83 | alpha: true, 84 | antialias: window.devicePixelRatio === 1 85 | } 86 | 87 | this.renderer = new WebGPURenderer({ ...params }) 88 | 89 | this.container.appendChild(this.renderer.domElement) 90 | 91 | this.renderer.setSize(this.screen.x, this.screen.y) 92 | this.renderer.setPixelRatio(Math.min(1.5, window.devicePixelRatio)) 93 | this.renderer.setClearColor(0x121212) 94 | this.renderer.physicallyCorrectLights = true 95 | } 96 | 97 | /** 98 | * Create a box with a PBR material 99 | */ 100 | #createMesh() { 101 | const geometry = new IcosahedronGeometry(1, 7) 102 | 103 | this.mesh = new Mesh(geometry, TSLSampleMaterial) 104 | 105 | this.scene.add(this.mesh) 106 | } 107 | 108 | #createControls() { 109 | this.controls = new OrbitControls(this.camera, this.renderer.domElement) 110 | } 111 | 112 | #createClock() { 113 | this.clock = new Clock() 114 | } 115 | 116 | #addListeners() { 117 | window.addEventListener('resize', this.#resizeCallback, { passive: true }) 118 | } 119 | 120 | #removeListeners() { 121 | window.removeEventListener('resize', this.#resizeCallback, { passive: true }) 122 | } 123 | 124 | #onResize() { 125 | this.screen.set(this.container.clientWidth, this.container.clientHeight) 126 | 127 | this.camera.aspect = this.screen.x / this.screen.y 128 | this.camera.updateProjectionMatrix() 129 | 130 | this.renderer.setSize(this.screen.x, this.screen.y) 131 | } 132 | } 133 | 134 | window._APP_ = new App('#app', { 135 | debug: window.location.hash.includes('debug') 136 | }) 137 | 138 | window._APP_.init() 139 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import glsl from 'vite-plugin-glsl' 2 | import { resolve } from 'path' 3 | import { defineConfig } from 'vite' 4 | 5 | export default defineConfig({ 6 | server: { 7 | port: 1234 8 | }, 9 | plugins: [ 10 | glsl() 11 | ], 12 | optimizeDeps: { 13 | esbuildOptions: { 14 | target: 'esnext' 15 | } 16 | }, 17 | build: { 18 | target: 'esnext', 19 | rollupOptions: { 20 | input: { 21 | main: resolve(__dirname, 'index.html'), 22 | webgpu: resolve(__dirname, 'webgpu.html'), 23 | } 24 | } 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /webgpu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ThreeJS Starter 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------