├── .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 |
--------------------------------------------------------------------------------