第十四节:物理引擎集成:Cannon.js入门
引言
物理引擎为3D世界注入真实感,让物体遵循重力、碰撞和动量等物理规律。Cannon.js是Three.js生态中最强大的物理引擎之一,本文将深入解析其核心机制,并通过Vue3实现物理沙盒系统,让你掌握动态仿真的核心技术。
1. 物理引擎基础
1.1 核心概念
1.2 工作流程
2. Cannon.js核心API
2.1 初始化物理世界
<script setup>
import * as CANNON from 'cannon';
import * as THREE from 'three';
// 创建物理世界
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0); // 设置重力
world.broadphase = new CANNON.NaiveBroadphase(); // 碰撞检测算法
world.solver.iterations = 10; // 求解器迭代次数
</script>
2.2 刚体与形状
// 创建刚体
const body = new CANNON.Body({
mass: 5, // 质量 (0 = 静态)
position: new CANNON.Vec3(0, 10, 0),
shape: new CANNON.Box(new CANNON.Vec3(1, 1, 1)) // 盒子形状
});
// 添加物理世界
world.addBody(body);
// 更多形状:
const sphereShape = new CANNON.Sphere(0.5); // 球体
const cylinderShape = new CANNON.Cylinder(0.5, 0.5, 2, 16); // 圆柱
const planeShape = new CANNON.Plane(); // 平面
2.3 材质与碰撞
// 创建材质
const concrete = new CANNON.Material('concrete');
const rubber = new CANNON.Material('rubber');
// 定义接触材质
const concreteRubberContact = new CANNON.ContactMaterial(
concrete,
rubber,
{
friction: 0.8, // 摩擦系数
restitution: 0.5 // 弹性系数
}
);
world.addContactMaterial(concreteRubberContact);
3. Three.js集成
3.1 物理-视觉同步
<script setup>
import { ref, onMounted } from 'vue';
const physicsObjects = ref([]);
// 添加物理对象
function addPhysicsObject(threeMesh, cannonBody) {
physicsObjects.value.push({
mesh: threeMesh,
body: cannonBody
});
scene.add(threeMesh);
world.addBody(cannonBody);
}
// 同步位置
function syncPhysics() {
physicsObjects.value.forEach(obj => {
obj.mesh.position.copy(obj.body.position);
obj.mesh.quaternion.copy(obj.body.quaternion);
});
}
// 物理更新循环
function physicsStep() {
world.step(1/60); // 60FPS
syncPhysics();
requestAnimationFrame(physicsStep);
}
onMounted(physicsStep);
</script>
3.2 碰撞事件处理
// 碰撞事件监听
body.addEventListener('collide', (e) => {
const impactStrength = e.contact.getImpactVelocityAlongNormal();
// 播放声音
if (impactStrength > 2) {
playCollisionSound(impactStrength);
}
// 视觉反馈
gsap.to(mesh.material.color, {
r: 1, g: 0, b: 0,
duration: 0.3,
yoyo: true,
repeat: 1
});
});
3.3 射线检测
const raycaster = new CANNON.Ray();
const rayResult = new CANNON.RaycastResult();
function castRay(from, to) {
raycaster.origin.copy(from);
raycaster.direction.copy(to).sub(from).normalize();
raycaster.collisionMask = 1; // 碰撞组
rayResult.reset();
world.raycastClosest(raycaster, rayResult);
if (rayResult.hasHit) {
return {
hitPoint: rayResult.hitPointWorld,
body: rayResult.body
};
}
return null;
}
4. 约束与关节
4.1 铰链关节
// 创建两个刚体
const bodyA = new CANNON.Body({ mass: 0 }); // 固定基座
const bodyB = new CANNON.Body({ mass: 1 }); // 可动部分
// 创建铰链约束
const hinge = new CANNON.HingeConstraint(
bodyA,
bodyB,
{
pivotA: new CANNON.Vec3(0, 0, 0), // 基座连接点
pivotB: new CANNON.Vec3(0, -1, 0), // 可动部分连接点
axisA: new CANNON.Vec3(0, 0, 1), // 基座旋转轴
axisB: new CANNON.Vec3(0, 0, 1) // 可动部分旋转轴
}
);
// 添加约束
world.addConstraint(hinge);
// 添加马达驱动
hinge.enableMotor();
hinge.setMotorSpeed(Math.PI); // 1转/秒
hinge.setMotorMaxForce(100);
4.2 弹簧约束
const spring = new CANNON.Spring(
bodyA, // 物体A
bodyB, // 物体B
{
localAnchorA: new CANNON.Vec3(0,0,0),
localAnchorB: new CANNON.Vec3(0,0,0),
restLength: 5, // 自然长度
stiffness: 50, // 刚度
damping: 5 // 阻尼
}
);
// 手动更新(需每帧调用)
function updateSprings() {
spring.applyForce();
}
4.3 点对点约束
const p2p = new CANNON.PointToPointConstraint(
bodyA,
new CANNON.Vec3(0,1,0), // 物体A连接点
bodyB,
new CANNON.Vec3(0,-1,0) // 物体B连接点
);
// 设置最大力
p2p.maxForce = 100;
world.addConstraint(p2p);
5. 车辆控制器
5.1 车辆组装
function createCar() {
const carBody = new CANNON.Body({ mass: 1000 });
carBody.addShape(new CANNON.Box(new CANNON.Vec3(1.5, 0.5, 3)));
const wheelBodies = [];
const wheelPositions = [
new CANNON.Vec3(-1, -0.5, 1.5), // 前左
new CANNON.Vec3(1, -0.5, 1.5), // 前右
new CANNON.Vec3(-1, -0.5, -1.5), // 后左
new CANNON.Vec3(1, -0.5, -1.5) // 后右
];
wheelPositions.forEach(pos => {
const wheelBody = new CANNON.Body({ mass: 20 });
wheelBody.addShape(new CANNON.Sphere(0.4));
wheelBody.position.copy(pos);
// 车轮约束
const constraint = new CANNON.PointToPointConstraint(
carBody,
pos.clone(),
wheelBody,
new CANNON.Vec3(0,0,0)
);
wheelBodies.push(wheelBody);
world.addBody(wheelBody);
world.addConstraint(constraint);
});
return { carBody, wheelBodies };
}
5.2 车辆控制
<script setup>
import { onMounted, onUnmounted } from 'vue';
const car = ref(null);
const steering = ref(0);
const engineForce = ref(0);
function initCar() {
car.value = createCar();
scene.add(createCarVisual(car.value));
}
// 键盘控制
function onKeyDown(event) {
switch(event.key) {
case 'ArrowUp':
engineForce.value = 500;
break;
case 'ArrowDown':
engineForce.value = -300;
break;
case 'ArrowLeft':
steering.value = Math.PI/8;
break;
case 'ArrowRight':
steering.value = -Math.PI/8;
break;
}
}
// 更新车辆
function updateCar() {
if (!car.value) return;
// 应用引擎力
car.value.wheelBodies[0].applyLocalForce(
new CANNON.Vec3(engineForce.value, 0, 0),
new CANNON.Vec3(0,0,0)
);
// 转向
car.value.wheelBodies[0].quaternion.setFromAxisAngle(
new CANNON.Vec3(0,1,0),
steering.value
);
// 重置控制
engineForce.value = 0;
steering.value = 0;
}
onMounted(() => {
window.addEventListener('keydown', onKeyDown);
world.addEventListener('postStep', updateCar);
});
onUnmounted(() => {
window.removeEventListener('keydown', onKeyDown);
world.removeEventListener('postStep', updateCar);
});
</script>
6. 角色控制器
6.1 角色物理体
function createCharacter() {
const body = new CANNON.Body({
mass: 70, // 70kg
position: new CANNON.Vec3(0, 5, 0),
fixedRotation: true // 防止摔倒
});
// 胶囊体形状
const capsule = new CANNON.Capsule(0.3, 1.6);
body.addShape(capsule);
// 角色状态
body.velocity = new CANNON.Vec3();
body.canJump = true;
return body;
}
6.2 角色移动控制
function updateCharacter(deltaTime) {
if (!characterBody) return;
// 键盘输入
const direction = new CANNON.Vec3();
if (keys.ArrowUp) direction.z = -1;
if (keys.ArrowDown) direction.z = 1;
if (keys.ArrowLeft) direction.x = -1;
if (keys.ArrowRight) direction.x = 1;
// 标准化方向
if (direction.length() > 0) {
direction.normalize();
// 根据相机方向旋转移动向量
const cameraQuat = new THREE.Quaternion();
camera.getWorldQuaternion(cameraQuat);
const rotatedDirection = direction.applyQuaternion(cameraQuat);
// 应用移动力
characterBody.velocity.x = rotatedDirection.x * 5;
characterBody.velocity.z = rotatedDirection.z * 5;
} else {
// 摩擦力停止
characterBody.velocity.x *= 0.9;
characterBody.velocity.z *= 0.9;
}
// 跳跃
if (keys.Space && characterBody.canJump) {
characterBody.velocity.y = 8;
characterBody.canJump = false;
}
// 检测地面
const rayStart = characterBody.position.clone();
rayStart.y -= 1.8; // 脚部位置
const rayEnd = rayStart.clone();
rayEnd.y -= 0.1;
const result = castRay(rayStart, rayEnd);
characterBody.canJump = result?.body?.material?.name === 'ground';
}
6.3 相机跟随
function updateCamera() {
const charPos = characterBody.position;
// 第三人称相机
const offset = new THREE.Vector3(0, 2, 5);
offset.applyQuaternion(camera.quaternion);
camera.position.copy(charPos).add(offset);
camera.lookAt(charPos.x, charPos.y + 1.6, charPos.z);
}
7. Vue3物理沙盒系统
7.1 项目结构
src/
├── components/
│ ├── PhysicsSandbox.vue // 主沙盒
│ ├── ObjectSpawner.vue // 物体生成器
│ ├── ConstraintCreator.vue // 约束工具
│ └── PhysicsDebugger.vue // 物理调试
└── App.vue
7.2 物理沙盒主组件
<!-- PhysicsSandbox.vue -->
<template>
<div class="physics-sandbox">
<canvas ref="canvasRef"></canvas>
<div class="controls">
<ObjectSpawner @spawn="spawnObject" />
<ConstraintCreator
:objects="sceneObjects"
@create-constraint="createConstraint"
/>
<PhysicsDebugger :world="world" />
</div>
<div class="stats">
<div>FPS: {{ stats.fps }}</div>
<div>物体数: {{ sceneObjects.length }}</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import * as THREE from 'three';
import * as CANNON from 'cannon';
import { PhysicsDebugger } from './PhysicsDebugger.vue';
// 物理世界
const world = ref(new CANNON.World());
world.value.gravity.set(0, -9.82, 0);
// 场景对象
const sceneObjects = reactive([]);
const physicsObjects = ref([]);
// 初始化场景
onMounted(() => {
initScene();
initGround();
startPhysicsLoop();
});
// 生成物体
function spawnObject(type) {
const position = new THREE.Vector3(
Math.random() * 10 - 5,
10,
Math.random() * 10 - 5
);
let threeObj, cannonBody;
switch(type) {
case 'box':
threeObj = new THREE.Mesh(
new THREE.BoxGeometry(1,1,1),
new THREE.MeshStandardMaterial({ color: 0xff0000 })
);
cannonBody = new CANNON.Body({
mass: 1,
shape: new CANNON.Box(new CANNON.Vec3(0.5,0.5,0.5))
});
break;
case 'sphere':
threeObj = new THREE.Mesh(
new THREE.SphereGeometry(0.5),
new THREE.MeshStandardMaterial({ color: 0x00ff00 })
);
cannonBody = new CANNON.Body({
mass: 1,
shape: new CANNON.Sphere(0.5)
});
break;
// 其他类型...
}
threeObj.position.copy(position);
cannonBody.position.copy(position);
scene.add(threeObj);
world.value.addBody(cannonBody);
sceneObjects.push(threeObj);
physicsObjects.value.push({
mesh: threeObj,
body: cannonBody
});
}
// 创建约束
function createConstraint({ objectA, objectB, type }) {
const bodyA = physicsObjects.value.find(
o => o.mesh === objectA
).body;
const bodyB = physicsObjects.value.find(
o => o.mesh === objectB
).body;
let constraint;
switch(type) {
case 'hinge':
constraint = new CANNON.HingeConstraint(
bodyA,
bodyB,
// 参数...
);
break;
case 'spring':
constraint = new CANNON.Spring(
bodyA,
bodyB,
// 参数...
);
break;
}
world.value.addConstraint(constraint);
}
</script>
7.3 物理调试器
<!-- PhysicsDebugger.vue -->
<template>
<div class="physics-debugger">
<button @click="toggleDebug">调试模式: {{ debugMode ? '开启' : '关闭' }}</button>
<div v-if="debugMode" class="debug-options">
<label>
<input type="checkbox" v-model="showColliders">
显示碰撞体
</label>
<label>
<input type="checkbox" v-model="showContacts">
显示接触点
</label>
</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
import { CannonDebugger } from 'cannon-es-debugger';
const props = defineProps(['world']);
const debugMode = ref(false);
const showColliders = ref(true);
const showContacts = ref(false);
let debugRenderer = null;
// 切换调试模式
function toggleDebug() {
debugMode.value = !debugMode.value;
}
// 初始化调试渲染器
watch(debugMode, (enabled) => {
if (enabled && !debugRenderer) {
debugRenderer = new CannonDebugger(scene, props.world, {
color: 0x00ff00,
scale: 1.0
});
}
});
// 更新调试选项
watch([showColliders, showContacts], () => {
if (debugRenderer) {
debugRenderer.update(); // 需要自定义实现
}
});
</script>
7.4 物体生成器
<!-- ObjectSpawner.vue -->
<template>
<div class="object-spawner">
<button
v-for="type in objectTypes"
:key="type"
@click="spawn(type)"
>
{{ type }}
</button>
</div>
</template>
<script setup>
const objectTypes = [
'box', 'sphere', 'cylinder',
'car', 'character', 'chain'
];
const emit = defineEmits(['spawn']);
function spawn(type) {
emit('spawn', type);
}
</script>
7.5 约束创建器
<!-- ConstraintCreator.vue -->
<template>
<div class="constraint-creator">
<select v-model="selectedType">
<option v-for="type in constraintTypes" :value="type">
{{ type }}
</option>
</select>
<div v-if="selectedObjectA && selectedObjectB">
已选择: {{ selectedObjectA.name }} 和 {{ selectedObjectB.name }}
<button @click="create">创建约束</button>
</div>
<div v-else>
<p>选择第一个物体</p>
<div class="object-list">
<div
v-for="obj in objects"
:key="obj.uuid"
:class="{ selected: obj === selectedObjectA }"
@click="selectObjectA(obj)"
>
{{ obj.name || obj.type }}
</div>
</div>
<div v-if="selectedObjectA">
<p>选择第二个物体</p>
<div class="object-list">
<div
v-for="obj in objects"
:key="obj.uuid"
:class="{ selected: obj === selectedObjectB }"
@click="selectObjectB(obj)"
>
{{ obj.name || obj.type }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps(['objects']);
const emit = defineEmits(['create-constraint']);
const constraintTypes = ['hinge', 'point', 'spring', 'slider'];
const selectedType = ref('hinge');
const selectedObjectA = ref(null);
const selectedObjectB = ref(null);
function selectObjectA(obj) {
selectedObjectA.value = obj;
}
function selectObjectB(obj) {
selectedObjectB.value = obj;
}
function create() {
if (selectedObjectA.value && selectedObjectB.value) {
emit('create-constraint', {
type: selectedType.value,
objectA: selectedObjectA.value,
objectB: selectedObjectB.value
});
resetSelection();
}
}
function resetSelection() {
selectedObjectA.value = null;
selectedObjectB.value = null;
}
</script>
8. 性能优化
8.1 碰撞检测优化
// 使用SAPBroadphase
world.broadphase = new CANNON.SAPBroadphase(world);
world.broadphase.useBoundingBoxes = true;
// 设置碰撞组
const STATIC_GROUP = 1;
const DYNAMIC_GROUP = 2;
const CHARACTER_GROUP = 4;
groundBody.collisionFilterGroup = STATIC_GROUP;
groundBody.collisionFilterMask = DYNAMIC_GROUP | CHARACTER_GROUP;
characterBody.collisionFilterGroup = CHARACTER_GROUP;
characterBody.collisionFilterMask = STATIC_GROUP | DYNAMIC_GROUP;
8.2 休眠系统
// 启用休眠
world.allowSleep = true;
// 设置休眠参数
body.sleepSpeedLimit = 0.1; // 速度阈值
body.sleepTimeLimit = 1.0; // 休眠时间
// 手动唤醒
body.wakeUp();
8.3 时间缩放
// 慢动作效果
world.timeScale = 0.5;
// 暂停物理
world.pause();
// 恢复物理
world.resume();
8.4 多线程计算
// 在Web Worker中运行物理
const physicsWorker = new Worker('physics-worker.js');
function initPhysicsWorker() {
physicsWorker.postMessage({
type: 'init',
gravity: [0, -9.82, 0]
});
}
function updatePhysics() {
physicsWorker.postMessage({
type: 'step',
dt: 1/60,
objects: getObjectStates()
});
physicsWorker.onmessage = (e) => {
applyObjectStates(e.data);
};
}
// physics-worker.js
self.onmessage = (e) => {
if (e.data.type === 'init') {
world = new CANNON.World();
world.gravity.set(...e.data.gravity);
} else if (e.data.type === 'step') {
// 更新物体状态
e.data.objects.forEach(obj => {
const body = getBodyById(obj.id);
body.position.copy(obj.position);
body.velocity.copy(obj.velocity);
});
// 物理步进
world.step(e.data.dt);
// 返回新状态
const states = world.bodies.map(body => ({
id: body.id,
position: body.position,
quaternion: body.quaternion
}));
self.postMessage(states);
}
};
9. 常见问题解答
Q1:物体穿透怎么办?
- 增加求解器迭代次数:
world.solver.iterations = 20;
- 减少时间步长:
// 使用固定时间步长 const fixedTimeStep = 1/120; world.step(fixedTimeStep);
- 使用连续碰撞检测:
body.collisionResponse = true; body.ccdSpeedThreshold = 1.0; body.ccdIterations = 10;
Q2:如何优化大量物体的性能?
- 使用SAPBroadphase
- 启用休眠系统
- 简化碰撞形状
- 静态物体设为mass=0
Q3:角色控制器卡顿问题?
- 使用固定时间步长:
const fixedTimeStep = 1/60; let accumulatedTime = 0; function animate(time) { accumulatedTime += deltaTime; while (accumulatedTime >= fixedTimeStep) { world.step(fixedTimeStep); accumulatedTime -= fixedTimeStep; } }
- 减少角色碰撞形状复杂度
- 限制物理更新频率
10. 总结
通过本文,你已掌握:
- 物理引擎核心概念与Cannon.js架构
- Three.js与Cannon.js集成技术
- 刚体、约束与关节系统
- 车辆与角色控制器实现
- 碰撞检测与物理事件处理
- Vue3物理沙盒系统开发
- 物理性能优化策略
核心价值:Cannon.js为Three.js应用带来真实物理行为,结合Vue3的响应式系统,实现交互式物理仿真环境,为游戏、模拟和可视化应用提供坚实基础。
下一篇预告
第十五篇:第一阶段总结:你的第一个3D网页
你将学习:
- 综合应用前14篇知识
- 产品级3D展示页开发
- 响应式设计适配多端
- 性能优化最佳实践
- 部署与SEO优化
- Vue3+Three.js项目架构
准备好将所学知识融会贯通了吗?让我们打造一个令人惊叹的3D产品展示页!