【可视化学习】11-Three.js物理引擎
发表于:2023-05-27 |

Three.js物理引擎

本篇文章将围绕物理引擎,让物体有更加真实的现实效果的感觉,主要使用的物理引擎为CANNON

小球弹跳

基础代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
300
);

// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);
// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);

function render() {
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

render();

// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");

// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();

// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});

添加一个球和平面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建球
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial();
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(sphere);

// 创建地面
const floor = new THREE.Mesh(
new THREE.PlaneBufferGeometry(20, 20),
new THREE.MeshStandardMaterial()
);
// 设置地面位置,默认为垂直贴边,调整位置,旋转-90度
floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);

效果图

打光

1
2
3
4
5
6
7
//添加环境光和平行光
// 环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
// 平行光
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
scene.add(dirLight);

效果图

添加阴影

1
2
3
4
5
6
7
8
// 平行光投射
dirLight.castShadow = true;
// 地面接收影子
floor.receiveShadow = true;
// 小球发射光
sphere.castShadow = true;
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;

效果图

物理世界

要实现一个小球掉落效果,需要在物理世界添加一个物理小球,并且和renderer中的绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 导入connon引擎
import * as CANNON from "cannon-es";
// 创建物理世界
const world = new CANNON.World();
// 设置物理世界重力,y方向9.8
world.gravity.set(0, -9.8, 0);
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);

//设置物体材质
const sphereWorldMaterial = new CANNON.Material();

// 创建物理世界的物体
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: sphereWorldMaterial,
});

// 将物体添加至物理世界
world.addBody(sphereBody);
// 设置时钟
const clock = new THREE.Clock();
function render() {
// 两次渲染间隔
let deltaTime = clock.getDelta();
// 更新物理引擎里世界的物体(1/120,120帧率)
world.step(1 / 120, deltaTime);
// 将物理世界的位置信息给renderer小球
sphere.position.copy(sphereBody.position);
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

效果图

添加物理地面

我们刚才创建了一个物理小球,但是这个小球会落到我们的地面下面去,那是因为我们的地面并不是物理的才导致的,接下来我们写一个物理的地面

1
2
3
4
5
6
7
8
9
10
11
// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置(第一个参数:沿着某个向量旋转,第二个参数:旋转的角度)
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);

效果图

添加掉落音效

这里的音效可以找爱给网的音效,里面有非常多好用的素材(这里自己大家自己尝试,博客里面音效不是很好放出来)

1
2
3
4
5
6
7
8
9
// 创建击打声音
const hitSound = new Audio("assets/metalHit.mp3");
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
const impactStrength = e.contact.getImpactVelocityAlongNormal();
hitSound.play();
}
sphereBody.addEventListener("collide", HitEvent);

让小球多次回弹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//设置小球物体材质
const sphereWorldMaterial = new CANNON.Material("sphere");
// 创建物理世界的物体
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: sphereWorldMaterial,
});

// 设置地面物体的材质
const floorMaterial = new CANNON.Material("floor");
floorBody.material = floorMaterial;
// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
sphereMaterial,
floorMaterial,
{
// 摩擦力
friction: 0.1,
// 弹性
restitution: 0.7,
}
);

// 讲材料的关联设置添加的物理世界
world.addContactMaterial(defaultContactMaterial);

效果图

小球回弹添加音效

刚才我们将小球变得可以回弹了,但是他的声音依旧只有一个,我们可以根据小球的强度来播放音效(这里一样不放了)
碰撞强度大于2,就将之前的音效清零,重新播放

1
2
3
4
5
6
7
8
9
10
11
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength);
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.play();
}
}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";

// 目标:使用cannon引擎
console.log(CANNON);

// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
300
);

// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);

// 创建球和平面
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial();
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.castShadow = true;
scene.add(sphere);

const floor = new THREE.Mesh(
new THREE.PlaneBufferGeometry(20, 20),
new THREE.MeshStandardMaterial()
);

floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);

// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0);
// 创建物理小球形状
const sphereShape = new CANNON.Sphere(1);

//设置物体材质
const sphereWorldMaterial = new CANNON.Material("sphere");

// 创建物理世界的物体
const sphereBody = new CANNON.Body({
shape: sphereShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: sphereWorldMaterial,
});

// 将物体添加至物理世界
world.addBody(sphereBody);

// 创建击打声音
const hitSound = new Audio("assets/metalHit.mp3");
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength);
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.play();
}
}
sphereBody.addEventListener("collide", HitEvent);

// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
const floorMaterial = new CANNON.Material("floor");
floorBody.material = floorMaterial;
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);

// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
sphereMaterial,
floorMaterial,
{
// 摩擦力
friction: 0.1,
// 弹性
restitution: 0.7,
}
);

// 讲材料的关联设置添加的物理世界
world.addContactMaterial(defaultContactMaterial);

//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true;
scene.add(dirLight);

// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;

// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);

// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();

function render() {
// let time = clock.getElapsedTime();
let deltaTime = clock.getDelta();
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime);

sphere.position.copy(sphereBody.position);

renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

render();

// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");

// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();

// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});

立方体碰撞

基础代码和上面是一样的,这里就不贴了

创建立方体

将小球换成立方体即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// renderer
const cubeGeometry = new THREE.BoxBufferGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshStandardMaterial();
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.castShadow = true;
scene.add(cube);
// 物理
//设置物体材质
const cubeWorldMaterial = new CANNON.Material("cube");
// 创建物理cube形状
const cubeShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));
// 创建物理世界的物体
const cubeBody = new CANNON.Body({
shape: cubeShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: cubeWorldMaterial,
});
// 将物体添加至物理世界
world.addBody(cubeBody);

// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
cubeWorldMaterial,
floorMaterial,
{
// 摩擦力
friction: 0.1,
// 弹性
restitution: 0.7,
}
);

通过点击创建立方体

先封装一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const cubeArr = [];
//设置物体材质
const cubeWorldMaterial = new CANNON.Material("cube");

function createCube() {
// 创建立方体和平面
const cubeGeometry = new THREE.BoxBufferGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshStandardMaterial();
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.castShadow = true;
scene.add(cube);
// 创建物理cube形状
const cubeShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));

// 创建物理世界的物体
const cubeBody = new CANNON.Body({
shape: cubeShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: cubeWorldMaterial,
});
cubeBody.applyLocalForce(
new CANNON.Vec3(300, 0, 0), //添加的力的大小和方向
new CANNON.Vec3(0, 0, 0) //施加的力所在的位置
);

// 将物体添加至物理世界
world.addBody(cubeBody);
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength);
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.volume = impactStrength / 12;
hitSound.play();
}
}
cubeBody.addEventListener("collide", HitEvent);
cubeArr.push({
mesh: cube,
body: cubeBody,
});
}

点击监听

1
window.addEventListener("click", createCube);

渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function render() {
// let time = clock.getElapsedTime();
let deltaTime = clock.getDelta();
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime);

// cube.position.copy(cubeBody.position);
cubeArr.forEach((item) => {
item.mesh.position.copy(item.body.position);
// 设置渲染的物体跟随物理的物体旋转
item.mesh.quaternion.copy(item.body.quaternion);
});

renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

添加外部力

1
2
3
4
cubeBody.applyLocalForce(
new CANNON.Vec3(300, 0, 0), //添加的力的大小和方向
new CANNON.Vec3(0, 0, 0) //施加的力所在的位置
);

声音的强弱

1
hitSound.volume = impactStrength / 12;

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 导入动画库
import gsap from "gsap";
// 导入dat.gui
import * as dat from "dat.gui";
// 导入connon引擎
import * as CANNON from "cannon-es";

// 目标:设置cube跟着旋转
console.log(CANNON);

// const gui = new dat.GUI();
// 1、创建场景
const scene = new THREE.Scene();

// 2、创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
300
);

// 设置相机位置
camera.position.set(0, 0, 18);
scene.add(camera);

const cubeArr = [];
//设置物体材质
const cubeWorldMaterial = new CANNON.Material("cube");

function createCube() {
// 创建立方体和平面
const cubeGeometry = new THREE.BoxBufferGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshStandardMaterial();
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.castShadow = true;
scene.add(cube);
// 创建物理cube形状
const cubeShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5));

// 创建物理世界的物体
const cubeBody = new CANNON.Body({
shape: cubeShape,
position: new CANNON.Vec3(0, 0, 0),
// 小球质量
mass: 1,
// 物体材质
material: cubeWorldMaterial,
});
cubeBody.applyLocalForce(
new CANNON.Vec3(300, 0, 0), //添加的力的大小和方向
new CANNON.Vec3(0, 0, 0) //施加的力所在的位置
);

// 将物体添加至物理世界
world.addBody(cubeBody);
// 添加监听碰撞事件
function HitEvent(e) {
// 获取碰撞的强度
// console.log("hit", e);
const impactStrength = e.contact.getImpactVelocityAlongNormal();
console.log(impactStrength);
if (impactStrength > 2) {
// 重新从零开始播放
hitSound.currentTime = 0;
hitSound.volume = impactStrength / 12;
hitSound.play();
}
}
cubeBody.addEventListener("collide", HitEvent);
cubeArr.push({
mesh: cube,
body: cubeBody,
});
}

window.addEventListener("click", createCube);

const floor = new THREE.Mesh(
new THREE.PlaneBufferGeometry(20, 20),
new THREE.MeshStandardMaterial()
);

floor.position.set(0, -5, 0);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);

// 创建物理世界
// const world = new CANNON.World({ gravity: 9.8 });
const world = new CANNON.World();
world.gravity.set(0, -9.8, 0);

// 创建击打声音
const hitSound = new Audio("assets/metalHit.mp3");

// 物理世界创建地面
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
const floorMaterial = new CANNON.Material("floor");
floorBody.material = floorMaterial;
// 当质量为0的时候,可以使得物体保持不动
floorBody.mass = 0;
floorBody.addShape(floorShape);
// 地面位置
floorBody.position.set(0, -5, 0);
// 旋转地面的位置
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);

// 设置2种材质碰撞的参数
const defaultContactMaterial = new CANNON.ContactMaterial(
cubeWorldMaterial,
floorMaterial,
{
// 摩擦力
friction: 0.1,
// 弹性
restitution: 0.7,
}
);

// 讲材料的关联设置添加的物理世界
world.addContactMaterial(defaultContactMaterial);

// 设置世界碰撞的默认材料,如果材料没有设置,都用这个
world.defaultContactMaterial = defaultContactMaterial;

//添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.castShadow = true;
scene.add(dirLight);

// 初始化渲染器
// 渲染器透明
const renderer = new THREE.WebGLRenderer({ alpha: true });
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 开启场景中的阴影贴图
renderer.shadowMap.enabled = true;

// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);

// // 使用渲染器,通过相机将场景渲染进来
// renderer.render(scene, camera);

// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
// 设置控制器阻尼,让控制器更有真实效果,必须在动画循环里调用.update()。
controls.enableDamping = true;

// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置时钟
const clock = new THREE.Clock();

function render() {
// let time = clock.getElapsedTime();
let deltaTime = clock.getDelta();
// 更新物理引擎里世界的物体
world.step(1 / 120, deltaTime);

// cube.position.copy(cubeBody.position);
cubeArr.forEach((item) => {
item.mesh.position.copy(item.body.position);
// 设置渲染的物体跟随物理的物体旋转
item.mesh.quaternion.copy(item.body.quaternion);
});

renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}

render();

// 监听画面变化,更新渲染画面
window.addEventListener("resize", () => {
// console.log("画面变化了");

// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();

// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});

效果图

上一篇:
【可视化学习】12-WEBGL与GPU渲染原理
下一篇:
【可视化学习】10-Three.js全屏滚动