更新于 

CANNON-Constraint 约束力

Lock Constraint

说实话,听完Wael Yasmina讲的之后,
我也没弄明白到底什么是Lock Constraint,
视频下方相关的一些文章都是全英文论文(捂脸),
实际操作之后,
constraint似乎像是一种锁链关系,
将两个离散的物体作为一个整体计算受力情况。

Cannon专门提供了一个类用于实现Constraint,
就是LockConstraint
实例化时必填2个参数:

  • 参数1:当前Body
  • 参数2:当前Body具有锁定关系的上一个Body

将一连串物体使用LockConstrain锁在一起后,
它们将作为一个整体受力,
但同时整体的各个部分相互之间也有力的作用。

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

const world = new CANNON.World({
gravity: new CANNON.Vec3(0, -9.81, 0)
})

const size = 0.5
const space = size * 0.1
const N = 10
const shape = new CANNON.Box(new CANNON.Vec3(size, size, size))
const mass = 1

const meshes = []
const bodies = []

const geo = new THREE.BoxGeometry(size * 2, size * 2, size * 2)
const mat = new THREE.MeshBasicMaterial({ color: 0xffaa00 })

for (let i = 0; i < N; i++) {
const boxBody = new CANNON.Body({
shape,
mass,
position: new CANNON.Vec3((i - N / 2) * (size+space) * 2, 0, 0)
// position: new CANNON.Vec3(-(N-i-N/2), 0, 0)
})
world.addBody(boxBody)
bodies.push(boxBody)
const boxMesh = new THREE.Mesh(geo, mat)
scene.add(boxMesh)
meshes.push(boxMesh)

if (i > 0) {
const lockConstraint = new CANNON.LockConstraint(boxBody, bodies[i - 1])
world.addConstraint(lockConstraint)
}

}

const leftFlatMesh = new THREE.Mesh(geo, mat)
scene.add(leftFlatMesh)
meshes.push(leftFlatMesh)
const leftFlatBody = new CANNON.Body({
shape,
type: CANNON.BODY_TYPES.STATIC,
position:new CANNON.Vec3(-N*(size+space),-3,0)
})
world.addBody(leftFlatBody)
bodies.push(leftFlatBody)

const rightFlatMesh = new THREE.Mesh(geo, mat)
scene.add(rightFlatMesh)
meshes.push(rightFlatMesh)
const rightFlatBody = new CANNON.Body({
shape,
type: CANNON.BODY_TYPES.STATIC,
position:new CANNON.Vec3((N-1 - N / 2) * (size+space) * 2,-3,0)
})
world.addBody(rightFlatBody)
bodies.push(rightFlatBody)

function animate() {
world.step(1 / 60)
for (let i = 0; i < meshes.length; i++) {
meshes[i].position.copy(bodies[i].position)
meshes[i].quaternion.copy(bodies[i].quaternion)
}
renderer.render(scene, camera)
}

renderer.setAnimationLoop(animate)

Distance Constraint

Constrains two bodies to be at a constant distance from each others center of mass.

DistanceConstrain的功能是将两个body保持固定距离约束在一起。

实例化CANNON.DistanceConstrain需要三个参数:

  • bodyA
  • bodyB
  • distance

为了制作一个由15×15个小球组成的矩形网格,
首先需要创建15×15个body,
接着对这些body进行关联。
使用矩阵行列索引哈希表存储数据,在关联时会方便很多。

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
const row = 15
const col = 15
const size = 0.2
const space = 0.05
const ballMeshes = []
const ballBodies = []
const ballGeo = new THREE.SphereGeometry(size)
const ballMat = new THREE.MeshBasicMaterial({ color: 0xffaa00 })
const particles = {}
for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
const ballMesh = new THREE.Mesh(ballGeo, ballMat)
ballMeshes.push(ballMesh)
const x = (i - row / 2) * (size * 2 + space) + size
const z = (j - col / 2) * (size * 2 + space) + size
scene.add(ballMesh)
const body = new CANNON.Body({
shape: new CANNON.Sphere(size),
position: new CANNON.Vec3(x, 5, z),
mass: 1
})
world.addBody(body)
ballBodies.push(body)
particles[`${i} ${j}`] = body
}
}

function connect(i1, j1, i2, j2) {
const distanceConstrain = new CANNON.DistanceConstraint(
particles[`${i1} ${j1}`],
particles[`${i2} ${j2}`],
space*10
)
world.addConstraint(distanceConstrain)
}

for (let i = 0; i < row; i++) {
for (let j = 0; j < col; j++) {
if (i < row - 1)
connect(i, j, i + 1, j)
if (j < col - 1)
connect(i, j, i, j + 1)
}
}

Cloth Simulate 模拟布料

和上一部分 Distance Constraint 的原理相似,
一块方形的布料可以看做一块矩阵,
矩阵的相邻元素之间存在Distance Constrain关系。

这里组成矩阵的body使用CANNON.Particle类。

第一步,创建布料实体,按照布料矩阵的行列顶点数配置segments:

1
2
3
4
5
6
7
8
const clothGeo = new THREE.PlaneGeometry(1, 1, row, col)
const clothMaterial = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide,
// 为布料添加纹理
map:new THREE.TextureLoader().load('/assets/jean-philippe-delberghe-75xPHEQBmvA-unsplash.jpg')
})
const clothMesh = new THREE.Mesh(clothGeo, clothMaterial)
scene.add(clothMesh)

第二步,创建body矩阵,
注意:n×m分段的geometry上实际上有(n+1)*(m+1)个顶点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (let i = 0; i < row + 1; i++) {
const vec = []
for (let j = 0; j < col + 1; j++) {
const body = new CANNON.Body({
shape,
mass:j==col?0:mass,
position: new CANNON.Vec3((i - row / 2) * dist, (j - col / 2) * dist, 0),
velocity:new CANNON.Vec3(0,-0.1*(col-j))
})
world.addBody(body)
vec.push(body)
}
bodies.push(vec)
}

第三步,为body矩阵的每个元素条件constraint条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createConstraint(i1, j1, i2, j2) {
const constraint = new CANNON.DistanceConstraint(
bodies[i1][j1],
bodies[i2][j2],
dist
)
world.addConstraint(constraint)
}

for (let i = 0; i <= row; i++) {
for (let j = 0; j <= col; j++) {
if (i < row)
createConstraint(i, j, i + 1, j)
if (j < col)
createConstraint(i, j, i, j + 1)
}
}

第四步,创建布料同步函数,在loop函数中会逐帧调用
每次刷新布料geometry中attr类型变量position的对应顶点

1
2
3
4
5
6
7
8
9
10
11
function updatePosition() {
for (let i = 0; i <= row; i++) {
for (let j = 0; j <= col; j++) {
const index = i * (row + 1) + j
const position = clothGeo.attributes.position
const bodyPosition = bodies[i][j].position
position.setXYZ(index, bodyPosition.x, bodyPosition.y, bodyPosition.z)
position.needsUpdate = true
}
}
}

第五步,创建球形

1
2
3
4
5
6
7
8
9
const sphereGeo = new THREE.SphereGeometry(radius)
const sphereMat = new THREE.MeshPhysicalMaterial({})
const sphere = new THREE.Mesh(sphereGeo, sphereMat)
scene.add(sphere)
const sphereBody = new CANNON.Body({
shape: new CANNON.Sphere(radius*1.3),
type: CANNON.BODY_TYPES.STATIC
})
world.addBody(sphereBody)

第六步、启动动画帧

1
2
3
4
5
6
7
8
9
10
11
12
const timeStep = 1 / 60
function animate(time) {
world.step(timeStep)
updatePosition()
sphereBody.position.set(
radius * Math.sin(time / 1000),
0,
radius * Math.cos(time / 1000),
)
sphere.position.copy(sphereBody.position)
renderer.render(scene, camera);
}