更新于 

Cannon Tutorial 物理引擎

cannon.js引入

1
npm install cannon-es

模块引入:

1
import * as CANNON from 'cannon-es'

World容器与Body实体

CANNON.World

cannon.js必须要创建一个World实例,
相当于ThreeJs中必须存在一个Scene实例一样,
World实例用于管理所有物体、约束、碰撞等物理元素

1
2
3
4
const world = new CANNON.World({
// 重力
gravity:new CANNON.Vec3(0,-9.89,0)
})
CANNON.Body

CANNON.Body表示某个物体所对应的物理属性,
CANNON.Body和Three.Mesh组合在一起,构成一个具有物理属性的物体

  • CANNON 里
  • Mesh 表
1
2
3
4
5
const planeBody = new CANNON.Body({
mass: 1,
shape:new CANNON.Plane()
})
world.addBody(planeBody)

将Body添加到world中之后,
需要根据body每帧计算的值更新对应mesh的属性值:

1
2
3
4
5
6
7
const stepTime = 1/60
function animate(){
world.step(stepTime) // 推进wolrd
planeMesh.position.copy(planeBody.position)
planeMesh.quaternion.copy(planeBody.quaternion)
renderer.render(scene, camera)
}

根据这段代码,plane是一个质量为1的无限延长的平面,
在向下的重力作用下,plane将会下坠:

物体碰撞

水平平面

我们将平面水平放置(沿x轴旋转90度),作为静态物体,
并且将平面的尺寸设置为30*30:

1
2
3
4
5
const planeBody = new CANNON.Body({
shape: new CANNON.Box(new CANNON.Vec3(15,15,0.5)),
type:CANNON.BODY_TYPES.STATIC
})
groundBody.quaternion.setFromEuler(-Math.PI/2,0,0)

创建一个方形物体:
(注意创建方形物体时,vec3的属性表示距物体中心点的位置)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const boxGeo = new THREE.BoxGeometry(3, 3, 3)
const boxMat = new THREE.MeshBasicMaterial({
color:0x00ff00,
wireframe:true
})
const boxMesh = new THREE.Mesh(boxGeo, boxMat)
scene.add(boxMesh)
///////////
const boxBody = new CANNON.Body({
shape: new CANNON.Box(new CANNON.Vec3(1.5, 1.5, 1, 5)),
mass: 1,
position:new CANNON.Vec3(1,20,0)
})
world.addBody(boxBody)
/////////
function animate(){
//...
boxMesh.position.copy(boxBody.position)
boxMesh.quaternion.copy(boxBody.quaternion)
//...
}

再创建一个球体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const sphereGeo = new THREE.SphereGeometry(3)
const sphereMat = new THREE.MeshBasicMaterial({
color: 0xff00ff,
wireframe:true,
})
const sphereMesh = new THREE.Mesh(sphereGeo, sphereMat)
scene.add(sphereMesh)
////////////
const sphereBody = new CANNON.Body({
shape: new CANNON.Sphere(3),
mass: 1,
position:new CANNON.Vec3(0,15,0)
})
world.addBody(sphereBody)
///////////
function animate(){
// ...
sphereMesh.position.copy(sphereBody.position)
sphereMesh.quaternion.copy(sphereBody.quaternion)
// ...
}
运动属性

线性阻尼值的范围在0-1之间,
值越大代表摩擦力越大:

1
sphereBody.linearDamping = 0.3

自旋角速度,自旋阻尼:

1
2
boxBody.angularVelocity.set(10,0,0,0)
boxBody.angularDamping = 0.3

顺便一提,自旋角整得太大,方块会被创飞

物理材质

材质

不同物理材质的物体接触时也会有不同的运动轨迹,
也就是给每种物理材质赋予独有的一组运动属性,
下面分别定义了平面、立方体、球体的材质

1
2
3
const planePhysMat = new CANNON.Material()
const boxPhysMat = new CANNON.Material()
const spherePhysMat = new CANNON.Material()

将材质和对应的Body联系在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const planeBody = new CANNON.Body({
shape: new CANNON.Box(new CANNON.Vec3(15,15,0.5)),
type: CANNON.BODY_TYPES.STATIC,
material:planePhysMat
})
const boxBody = new CANNON.Body({
shape: new CANNON.Box(new CANNON.Vec3(1.5, 1.5, 1, 5)),
mass: 1,
position: new CANNON.Vec3(1, 20, 0),
material:boxPhysMat
})
const sphereBody = new CANNON.Body({
shape: new CANNON.Sphere(3),
mass: 1,
position: new CANNON.Vec3(0, 15, 0),
material:spherePhysMat
})
相互作用

不同材质间相互作用,才能赋予运动以独特的物理属性,
因此需要将不同的材质联系起来,
下面的代码将立方体与平面的摩擦力设置为0,
将球体与平面的弹跳系数设置为0.9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const planeBoxContactMat = new CANNON.ContactMaterial(
planePhysMat,
boxPhysMat,
{
friction:0
}
)
const planeSphereContactMat = new CANNON.ContactMaterial(
planePhysMat,
spherePhysMat,
{
restitution:0.9 // 回弹系数
}
)
world.addContactMaterial(planeBoxContactMat)
world.addContactMaterial(planeSphereContactMat)

GLTF模型的碰撞

这个部分踩坑无数,bug叠bug,难绷

要使物体间产生碰撞效果,首先要为碰撞物创建碰撞网格(Body),
如何对GLTF模型创建碰撞网格,
需要用到 CANNON.Trimesh

我想要实现的效果是:一把钥匙模型从半空掉落到地板上

使用CANNON.Trimesh,需要用到vertex和index两个数组,
这两个数据源从gltf模型上都能取到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
loader.load('assets/key.glb',gltf=>{
key = gltf.scene.getObjectByName('Object_2') // 获取物体
key.scale.set(0.005,0.005,0.005) // 物体原模型过大,需要缩小
scene.add(key)
const geo = key.geometry
const keyShape = new CANNON.Trimesh(
geo.attributes.position.array, // vertex
geo.index.array // index
)
keyBody = new CANNON.Body({
mass:1,
position:new CANNON.Vec3(0,3,0),
shape:keyShape // 加载shape
})
keyBody.quaternion.setFromEuler(Math.PI/2,0,0)
world.addBody(keyBody)
})

上面的代码里,将position和index数据直接取了出来,
放入Trimesh中构建实体,
但是还是不行,钥匙直接穿过平面掉落。

chatGPT告诉我,之所以会出这个问题,
是由于使用scale对模型进行缩小,
但物体实际的position坐标依旧没有改变
我用Trimesh返回的顶点坐标信息创建了一个网格物体,
发现它说的没错,顶点坐标依旧保持gltf模型刚被引入的尺寸:

因此需要对原本的vertex进行手动缩放

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
loader.load('assets/key.glb',gltf=>{
gltf.scene.traverse(obj=>{
if(obj.isMesh){
obj.castShadow = true
}
})
key = gltf.scene.getObjectByName('Object_2')
// scale缩放 也可以在计算好scaledVertices之后,
// 使用setAttribute对物体顶点进行更新
key.scale.set(0.005,0.005,0.005)
scene.add(key)
const geo = key.geometry
let vertices = geo.attributes.position.array
// 手动缩放
// for (let i = 0; i < vertices.length; i += 3) {
// scaledVertices.push(
// vertices[i] * 0.005,
// vertices[i+1] * 0.005,
// vertices[i+2] * 0.005,
// )
// }
let scaledVertices = vertices.map(i=>i*0.005)
const keyShape = new CANNON.Trimesh(scaledVertices, geo.index.array)
keyBody = new CANNON.Body({
mass:1,
position:new CANNON.Vec3(0,3,0),
shape:keyShape
})
keyBody.quaternion.setFromEuler(Math.PI/2,0,0)
world.addBody(keyBody)
})

手动缩放后,Shape和物体原本网格应该保持一致,
试了一下,模型还是无限向下掉(shiiiiiiiit)
这时我隐约觉得可能不是我用于构建TriMesh的数据有问题,
于是我引用了一个普通盒子模型的vertex和index,
发现Trimesh果然还是往下掉,和顶点、索引数据无关。
上网搜了一下,发现原来这是CANNON.Trimesh的一个官方bug

CANNON.Trimesh构建的不规则Shape只能和Plane/Sphere类型的Shape进行碰撞

好吧……于是我把碰撞平面的Shape类型从Box改成了Plane,
终于成功嘞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const planeGeo = new THREE.PlaneGeometry(12,12)
const planeMat = new THREE.MeshPhysicalMaterial({
color:0xffffff,
roughness:0,
})
const planeMesh = new THREE.Mesh(planeGeo, planeMat)
planeMesh.rotation.x = Math.PI/2
planeMesh.receiveShadow = true
scene.add(planeMesh)

const planeBody = new CANNON.Body({
shape:new CANNON.Plane(),
type:CANNON.BODY_SLEEP_STATES
})
planeBody.quaternion.setFromEuler(-Math.PI/2, 0,0)
world.addBody(planeBody)