Skip to content

Commit f98ba88

Browse files
committed
✨ Add threejs journey notes 3 draft.
1 parent 4561aac commit f98ba88

1 file changed

Lines changed: 353 additions & 0 deletions

File tree

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
---
2+
draft: false
3+
title: Three.js Journey Notes 3 - Advanced Techniques
4+
date: 2025-03-11
5+
categories: Learning
6+
comments: true
7+
ShowToc: true
8+
isCJKLanguage: false
9+
---
10+
11+
课程链接:[three.js journey](https://threejs-journey.com/)
12+
13+
[Notes 1 - Basics](../threejs-journey-notes-1-basics)
14+
[Notes 2 - Classic Techniques](../threejs-journey-notes-2-classic-techniques)
15+
16+
## Physics
17+
18+
We need to add physics library.
19+
The idea is that we add a physics world which is purely theoretical. We cannot see it.
20+
So when we create a Three.js mesh, we also create a version of that mesh inside physics world, like projected one in physics world.
21+
So on each frame, physics world update itself, we take the coordinates of the projected physics object and then apply them to the corresponding Three.js mesh.
22+
23+
> First, you must decide if you need a 3D library or a 2D library. While you might think it has to be a 3D library because Three.js is all about 3D, you might be wrong. 2D libraries are usually much more performant, and if you can sum up your experience physics up to 2D collisions, you better use a 2D library.
24+
25+
3D Libraries:
26+
- Ammo.js: https://github.com/kripken/ammo.js/
27+
- physics engine written in C++, a little heavy
28+
- still updated by community
29+
- most used library
30+
- Cannon.js: https://github.com/schteppe/cannon.js
31+
- lighter than Ammo.js
32+
- more comfortable to implement
33+
- not updated, but has maintained fork
34+
- Oimo.js: https://github.com/lo-th/Oimo.js
35+
- lighter than Ammo.js
36+
- easier to implement
37+
- not updated
38+
- Rapier: https://github.com/dimforge/rapier
39+
- similar to Cannon.js
40+
- good performance
41+
- still update and maintained
42+
2D Libraries:
43+
- Matter.js: https://github.com/liabru/matter-js
44+
- still kind of updated
45+
- p2.js: https://github.com/schteppe/p2.js
46+
- not updated
47+
- https://github.com/piqnt/planck.js
48+
- still updated
49+
- https://github.com/kripken/box2d.js/
50+
- not updated
51+
- Rapier: https://github.com/dimforge/rapier
52+
- same library for 3D
53+
54+
solutions that try to combine Three.js with libraries like [Physijs](https://chandlerprall.github.io/Physijs/), it uses Ammo.js and supports workers natively.
55+
56+
### Base example
57+
code example for the following will use Cannon.js.
58+
59+
```js
60+
import * as CANNON from 'cannon-es';
61+
62+
/**
63+
* Physics world
64+
*/
65+
const world = new CANNON.World(); // empty space, like Scene in Threejs
66+
// add gravity, Vec3, just like Three.js Vector3
67+
world.gravity.set(0, -9.82, 0); // y axis, go down, which is G
68+
69+
// Materials, bouncing and friction behaviour
70+
const concreteMaterial = new CANNON.Material('concrete');
71+
const plasticMaterial = new CANNON.Material('plastic');
72+
73+
74+
// create a Body (which are objects in the physics world that will fall and collide with other bodies)
75+
// Sphere
76+
const sphereShape = new CANNON.Sphere(0.5); // 0.5 is same size as the buffer test sphere
77+
const sphereBody = new CANNON.Body({
78+
mass: 1,
79+
position: new CANNON.Vec3(0, 3, 0), // higher than the floor
80+
shape: sphereShape,
81+
material: plasticMaterial,
82+
});
83+
// pushing the sphere to origin
84+
sphereBody.applyLocalForce(new CANNON.Vec3(150, 0, 0), new CANNON.Vec3(0, 0, 0))
85+
world.addBody(sphereBody); // like add mesh to scene
86+
87+
// Floor phsics, add floor to stop the sphere from falling through
88+
const floorShape = new CANNON.Plane();
89+
const floorBody = new CANNON.Body();
90+
floorBody.material = concreteMaterial;
91+
floorBody.mass = 0; // default mass is 0, it means static, won't move
92+
floorBody.addShape(floorShape);
93+
// since threejs floor is rotated, need to rotate the cannon floor, which is harder than in threejs
94+
floorBody.quaternion.setFromAxisAngle(
95+
new CANNON.Vec3(-1, 0, 0),
96+
Math.PI * 0.5
97+
);
98+
world.addBody(floorBody);
99+
```
100+
101+
To make physics world update on frame, we need to update animate `tick()` function:
102+
103+
```js
104+
const tick = () =>
105+
{
106+
// ...
107+
108+
// Update physics
109+
world.step(1 / 60, deltaTime, 3)
110+
}
111+
```
112+
113+
At last, we need to update our Three.js sphere by using `sphereBody` coordinates:
114+
115+
```js
116+
const sphere = new THREE.Mesh(
117+
new THREE.SphereGeometry(0.5, 32, 32),
118+
new THREE.MeshStandardMaterial({
119+
metalness: 0.3,
120+
roughness: 0.4,
121+
envMap: environmentMapTexture,
122+
envMapIntensity: 0.5,
123+
})
124+
);
125+
sphere.castShadow = true;
126+
sphere.position.y = 0.5;
127+
scene.add(sphere);
128+
129+
// update position from cannon to threejs, you will see the sphere fall down
130+
sphere.position.copy(sphereBody.position) // which just do below copy x, y, z
131+
sphere.position.x = sphereBody.position.x;
132+
sphere.position.y = sphereBody.position.y;
133+
sphere.position.z = sphereBody.position.z;
134+
```
135+
136+
### Contact material
137+
138+
to make ball bounce, we need to add change material.
139+
140+
A material in physics world is just a reference. So name it with reasonable name.
141+
142+
Concate material is the combination of two materials with defined friction coefficient (how much does it rub) and the restitution coefficient (how much does it bounce)—both have default values of 0.3.
143+
144+
```js
145+
const concretePlasticContactMaterial = new CANNON.ContactMaterial(
146+
concreteMaterial,
147+
plasticMaterial,
148+
{
149+
friction: 0.1,
150+
restitution: 0.7
151+
}
152+
)
153+
world.addContactMaterial(concretePlasticContactMaterial)
154+
```
155+
156+
Then the ball will bounce.
157+
158+
### Apply forces
159+
160+
many ways to apply forces to a [Body](http://schteppe.github.io/cannon.js/docs/classes/Body.html):
161+
162+
- [applyForce](http://schteppe.github.io/cannon.js/docs/classes/Body.html#method_applyForce) to apply a force to the [Body](http://schteppe.github.io/cannon.js/docs/classes/Body.html) from a specified point in space (not necessarily on the [Body](http://schteppe.github.io/cannon.js/docs/classes/Body.html)'s surface) like the wind that pushes everything a little all the time, a small but sudden push on a domino or a greater sudden force to make an angry bird jump toward the enemy castle.
163+
- [applyImpulse](http://schteppe.github.io/cannon.js/docs/classes/Body.html#method_applyImpulse) is like [applyForce](http://schteppe.github.io/cannon.js/docs/classes/Body.html#method_applyForce) but instead of adding to the force that will result in velocity changes, it applies directly to the velocity.
164+
- [applyLocalForce](http://schteppe.github.io/cannon.js/docs/classes/Body.html#method_applyLocalForce) is the same as [applyForce](http://schteppe.github.io/cannon.js/docs/classes/Body.html#method_applyForce) but the coordinates are local to the [Body](http://schteppe.github.io/cannon.js/docs/classes/Body.html) (meaning that `0, 0, 0` would be the center of the [Body](http://schteppe.github.io/cannon.js/docs/classes/Body.html)).
165+
- [applyLocalImpulse](http://schteppe.github.io/cannon.js/docs/classes/Body.html#method_applyLocalImpulse) is the same as [applyImpulse](http://schteppe.github.io/cannon.js/docs/classes/Body.html#method_applyImpulse) but the coordinates are local to the [Body](http://schteppe.github.io/cannon.js/docs/classes/Body.html).
166+
167+
Because using "force" methods will result in velocity changes, let's not use "impulse" methods
168+
169+
### Broadphase
170+
171+
Testing collision between objects are costly in terms of performance.
172+
The broadphase doing a rough sorting of Bodies before testing them.
173+
174+
There are 3 broadphase algorithms available in Cannon.js:
175+
176+
- [NaiveBroadphase](http://schteppe.github.io/cannon.js/docs/classes/NaiveBroadphase.html): Tests every [Bodies](http://schteppe.github.io/cannon.js/docs/classes/Body.html) against every other [Bodies](http://schteppe.github.io/cannon.js/docs/classes/Body.html)
177+
- [GridBroadphase](http://schteppe.github.io/cannon.js/docs/classes/GridBroadphase.html): Quadrilles the world and only tests [Bodies](http://schteppe.github.io/cannon.js/docs/classes/Body.html) against other [Bodies](http://schteppe.github.io/cannon.js/docs/classes/Body.html) in the same grid box or the neighbors' grid boxes.
178+
- [SAPBroadphase](http://schteppe.github.io/cannon.js/docs/classes/SAPBroadphase.html) (Sweep and prune broadphase): Tests [Bodies](http://schteppe.github.io/cannon.js/docs/classes/Body.html) on arbitrary axes during multiples steps.
179+
- can eventually generate bugs where a collision doesn't occur, but it's rare, and it involves doing things like moving [Bodies](http://schteppe.github.io/cannon.js/docs/classes/Body.html) very fast.
180+
181+
```js
182+
// default is NaiveBoradphase, SAPBoradphase is recommended
183+
world.broadphase = new CANNON.SAPBroadphase(world)
184+
```
185+
186+
### Sleep
187+
188+
set this to be true for far and not moving objects, so no need to be tested.
189+
190+
```js
191+
world.allowSleep = true
192+
```
193+
194+
### Events on Body
195+
196+
 `'collide'``'sleep'` or `'wakeup'`.
197+
198+
for example, play a hit sound when collide
199+
200+
```js
201+
/**
202+
* Sounds
203+
*/
204+
const hitSound = new Audio('/sounds/hit.mp3')
205+
206+
const playHitSound = () =>
207+
{
208+
hitSound.play()
209+
}
210+
211+
const createBox = (width, height, depth, position) =>
212+
{
213+
// ...
214+
body.addEventListener('collide', playHitSound)
215+
// ...
216+
}
217+
```
218+
219+
## Imported Models
220+
some of popular 3D model formats:
221+
- OBJ
222+
- FBX
223+
- STL
224+
- PLY
225+
- COLLADA
226+
- 3DS
227+
- GLTF
228+
- stands for GL Transmission Format, made by the Khronos Group (the guys behind OpenGL, WebGL, Vulkan, Collada and with many members like AMD / ATI, Nvidia, Apple, id Software, Google, Nintendo, etc.)
229+
- become very popular these past few years
230+
- supports very different sets of data
231+
- supports various file formats like json, binary, embed textures
232+
- has become the standard when it comes to real-time
233+
234+
Find some pre-made models on: [GitHub - KhronosGroup/glTF-Sample-Assets: An assortment of assets that demonstrate features and capabilities of the glTF format](https://github.com/KhronosGroup/glTF-Sample-Assets)
235+
236+
Each model folder contain different GLTF formats, these 4 are most important:
237+
- glTF
238+
- kind of default format, it's a JSON, can open it in editor
239+
- contains various information like cameras, lights, scenes, materials, objects transformations,
240+
- but neither the geometries nor the textures
241+
- `Duck0.bin` is the file contain geometries and all information associated with the vertices like UV coordinates, normals, vertex colors, etc.
242+
- `DuckCM.png` is simply the texture of the duck.
243+
- we only need to load `Duck.gltf`
244+
- glTF-Binary
245+
- composed of only one file, binary file
246+
- a little lighter and more comfortable to load
247+
- but won't be able to alter its data
248+
- glTF-Draco
249+
- like glTF default one, but the buffer data is compressed using the [Draco algorithm](https://github.com/google/draco)
250+
- its `.bin` file size is much lighter
251+
- glTF-Embedded
252+
- like glTF-Binary, with only one file, but actually it's a JSON which you can edit
253+
254+
Choosing which format is based on how you want to handle the assets.
255+
256+
>If you want to be able to alter the textures or the coordinates of the lights after exporting, you better go for the **glTF-default**. It also presents the advantage of loading the different files separately, resulting in a load speed improvement.
257+
>If you want only one file per model and don't care about modifying the assets, you better go for **glTF-Binary**.
258+
>In both cases, you must decide if you want to use the **Draco** compression or not, but we will cover this part later.
259+
260+
### Base Example
261+
262+
```javascript
263+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
264+
265+
/**
266+
* Models
267+
*/
268+
const gltfLoader = new GLTFLoader()
269+
270+
gltfLoader.load(
271+
'/models/Duck/glTF/Duck.gltf',
272+
// success loaded callback
273+
(gltf) =>
274+
{
275+
console.log('success')
276+
console.log(gltf)
277+
scene.add(gltf.scene.children[0]) // duck
278+
// if there are multiple parts in this model, we need to add them all like
279+
//for(const child of gltf.scene.children)
280+
//{
281+
// scene.add(child)
282+
//}
283+
// but we cannot use for loop, The problem is that when we add a child from one scene to the other, it gets automatically removed from the first scene. That means that the first scene now has fewer children in it.
284+
// can use while loop
285+
while(gltf.scene.children.length)
286+
{
287+
scene.add(gltf.scene.children[0])
288+
}
289+
},
290+
// progress callback, can be omitted
291+
(progress) =>
292+
{
293+
console.log('progress')
294+
console.log(progress)
295+
},
296+
// error callback, can be omitted
297+
(error) =>
298+
{
299+
console.log('error')
300+
console.log(error)
301+
}
302+
)
303+
```
304+
305+
### Draco compression example
306+
307+
Draco need a decoder, which is located in `/node_modules/three/examples/jsm/libs/`, so need to copy whole folder to be next to model folder
308+
309+
```javascript
310+
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
311+
312+
const dracoLoader = new DRACOLoader()
313+
314+
dracoLoader.setDecoderPath('/draco/')
315+
gltfLoader.setDRACOLoader(dracoLoader)
316+
```
317+
318+
### Animated Model
319+
320+
Some models already contain animation. To activate it, we need [AnimationMixer](https://threejs.org/docs/#api/en/animation/AnimationMixer), it's like a player associated with an object that can contain one or many [AnimationClips](https://threejs.org/docs/#api/en/animation/AnimationClip).
321+
322+
```javascript
323+
let mixer = null
324+
325+
gltfLoader.load(
326+
'/models/Fox/glTF/Fox.gltf',
327+
(gltf) =>
328+
{
329+
gltf.scene.scale.set(0.03, 0.03, 0.03)
330+
scene.add(gltf.scene)
331+
332+
mixer = new THREE.AnimationMixer(gltf.scene)
333+
const action = mixer.clipAction(gltf.animations[0])
334+
action.play()
335+
}
336+
)
337+
```
338+
339+
and update the mixer in tick function to update it
340+
341+
```javascript
342+
const tick = () =>
343+
{
344+
// ...
345+
346+
if(mixer)
347+
{
348+
mixer.update(deltaTime)
349+
}
350+
351+
// ...
352+
}
353+
```

0 commit comments

Comments
 (0)