上一课程我们了解了three.js怎么集成cannon.js 和 ammo.js两种物理引擎。本节课程将学习如何利用它们做碰撞检测和响应。
关于安装集成教程请看上一课。
javascript
import * as THREE from 'three';
import * as CANNON from 'cannon';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
class PhysicsScene {
constructor() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(this.renderer.domElement);
// 初始化物理世界
this.world = new CANNON.World();
this.world.gravity.set(0, -9.82, 0); // 设置重力
this.world.broadphase = new CANNON.NaiveBroadphase();
// 创建物理物体和Three.js物体的映射
this.objects = [];
this.init();
this.animate();
}
init() {
// 添加光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
this.scene.add(light);
this.scene.add(new THREE.AmbientLight(0x404040));
// 添加轨道控制器
new OrbitControls(this.camera, this.renderer.domElement);
this.camera.position.set(0, 5, 10);
// 创建地面
this.createGround();
// 创建动态物体
this.createDynamicObjects();
// 添加鼠标交互
this.setupMouseInteraction();
}
createGround() {
// Three.js 地面
const groundGeometry = new THREE.PlaneGeometry(10, 10);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
groundMesh.rotation.x = -Math.PI / 2;
groundMesh.receiveShadow = true;
this.scene.add(groundMesh);
// Cannon.js 物理地面
const groundShape = new CANNON.Plane();
const groundBody = new CANNON.Body({ mass: 0 }); // 质量为0表示静态物体
groundBody.addShape(groundShape);
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
this.world.addBody(groundBody);
this.objects.push({
mesh: groundMesh,
body: groundBody
});
}
createDynamicObjects() {
const colors = [0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff];
for (let i = 0; i < 5; i++) {
// 随机位置
const x = (Math.random() - 0.5) * 4;
const y = 5 + i * 1.5;
const z = (Math.random() - 0.5) * 4;
// Three.js 球体
const geometry = new THREE.SphereGeometry(0.5, 32, 32);
const material = new THREE.MeshStandardMaterial({
color: colors[i % colors.length]
});
const mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.position.set(x, y, z);
this.scene.add(mesh);
// Cannon.js 物理球体
const shape = new CANNON.Sphere(0.5);
const body = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(x, y, z)
});
body.addShape(shape);
body.linearDamping = 0.1; // 线性阻尼
body.angularDamping = 0.1; // 角阻尼
// 添加碰撞事件监听
body.addEventListener('collide', (event) => {
this.handleCollision(event, mesh);
});
this.world.addBody(body);
this.objects.push({
mesh: mesh,
body: body
});
}
}
handleCollision(event, mesh) {
// 获取碰撞强度和接触点
const contact = event.contact;
const impactStrength = contact.getImpactVelocityAlongNormal();
if (Math.abs(impactStrength) > 1) {
// 根据碰撞强度改变颜色
mesh.material.color.setHex(0xffffff);
setTimeout(() => {
mesh.material.color.setHex(mesh.userData.originalColor || 0xff0000);
}, 300);
}
}
setupMouseInteraction() {
// 射线检测,用于鼠标拾取
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('click', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, this.camera);
const intersects = raycaster.intersectObjects(
this.objects.map(obj => obj.mesh).filter(mesh => mesh.geometry.type === 'SphereGeometry')
);
if (intersects.length > 0) {
const mesh = intersects[0].object;
const obj = this.objects.find(o => o.mesh === mesh);
if (obj) {
// 给物体施加一个向上的力
obj.body.applyImpulse(
new CANNON.Vec3(0, 5, 0),
obj.body.position
);
}
}
});
}
animate() {
requestAnimationFrame(() => this.animate());
// 更新物理世界
this.world.step(1/60); // 60 FPS
// 同步Three.js物体和物理物体的位置/旋转
this.objects.forEach(obj => {
if (obj.body.mass > 0) { // 只更新动态物体
obj.mesh.position.copy(obj.body.position);
obj.mesh.quaternion.copy(obj.body.quaternion);
}
});
this.renderer.render(this.scene, this.camera);
}
}
new PhysicsScene();
Ammo.js 需要先构建或使用预构建版本:
bash
# 使用预构建版本
# 从 https://github.com/kripken/ammo.js 下载 ammo.js 文件
javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
class AmmoPhysicsScene {
constructor() {
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.shadowMap.enabled = true;
document.body.appendChild(this.renderer.domElement);
this.objects = [];
this.initAmmo();
}
async initAmmo() {
// 加载Ammo.js
this.Ammo = await Ammo();
// 初始化物理世界
this.initPhysics();
this.initScene();
this.animate();
}
initPhysics() {
// 碰撞配置
const collisionConfiguration = new this.Ammo.btDefaultCollisionConfiguration();
const dispatcher = new this.Ammo.btCollisionDispatcher(collisionConfiguration);
const overlappingPairCache = new this.Ammo.btDbvtBroadphase();
const solver = new this.Ammo.btSequentialImpulseConstraintSolver();
this.physicsWorld = new this.Ammo.btDiscreteDynamicsWorld(
dispatcher,
overlappingPairCache,
solver,
collisionConfiguration
);
// 设置重力
this.physicsWorld.setGravity(new this.Ammo.btVector3(0, -9.8, 0));
}
initScene() {
// 添加光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
light.castShadow = true;
this.scene.add(light);
this.scene.add(new THREE.AmbientLight(0x404040));
// 控制器
new OrbitControls(this.camera, this.renderer.domElement);
this.camera.position.set(0, 5, 10);
// 创建地面
this.createGround();
// 创建盒子
this.createBoxes();
// 鼠标交互
this.setupMouseEvents();
}
createGround() {
// Three.js 地面
const groundGeometry = new THREE.PlaneGeometry(20, 20);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x888888,
side: THREE.DoubleSide
});
const groundMesh = new THREE.Mesh(groundGeometry, groundMaterial);
groundMesh.rotation.x = Math.PI / 2;
groundMesh.receiveShadow = true;
this.scene.add(groundMesh);
// Ammo.js 物理地面
const groundShape = new this.Ammo.btStaticPlaneShape(
new this.Ammo.btVector3(0, 1, 0), 1
);
const groundTransform = new this.Ammo.btTransform();
groundTransform.setIdentity();
groundTransform.setOrigin(new this.Ammo.btVector3(0, -1, 0));
const groundMotionState = new this.Ammo.btDefaultMotionState(groundTransform);
const groundRigidBodyInfo = new this.Ammo.btRigidBodyConstructionInfo(
0, groundMotionState, groundShape, new this.Ammo.btVector3(0, 0, 0)
);
const groundBody = new this.Ammo.btRigidBody(groundRigidBodyInfo);
this.physicsWorld.addRigidBody(groundBody);
this.objects.push({
mesh: groundMesh,
body: groundBody
});
}
createBoxes() {
for (let i = 0; i < 5; i++) {
const size = 0.5 + Math.random() * 0.5;
// Three.js 盒子
const geometry = new THREE.BoxGeometry(size, size, size);
const material = new THREE.MeshStandardMaterial({
color: Math.random() * 0xffffff
});
const mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.position.set(
(Math.random() - 0.5) * 8,
2 + i * 2,
(Math.random() - 0.5) * 8
);
this.scene.add(mesh);
// Ammo.js 物理盒子
const boxShape = new this.Ammo.btBoxShape(
new this.Ammo.btVector3(size/2, size/2, size/2)
);
const startTransform = new this.Ammo.btTransform();
startTransform.setIdentity();
const pos = mesh.position;
startTransform.setOrigin(new this.Ammo.btVector3(pos.x, pos.y, pos.z));
const mass = 1;
const localInertia = new this.Ammo.btVector3(0, 0, 0);
boxShape.calculateLocalInertia(mass, localInertia);
const motionState = new this.Ammo.btDefaultMotionState(startTransform);
const rbInfo = new this.Ammo.btRigidBodyConstructionInfo(
mass, motionState, boxShape, localInertia
);
const body = new this.Ammo.btRigidBody(rbInfo);
// 设置反弹系数
body.setRestitution(0.7);
// 设置摩擦力
body.setFriction(0.5);
this.physicsWorld.addRigidBody(body);
this.objects.push({
mesh: mesh,
body: body,
size: size
});
}
}
setupMouseEvents() {
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('click', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, this.camera);
const intersects = raycaster.intersectObjects(
this.objects.map(obj => obj.mesh).filter(mesh => mesh.geometry.type === 'BoxGeometry')
);
if (intersects.length > 0) {
const mesh = intersects[0].object;
const obj = this.objects.find(o => o.mesh === mesh);
if (obj) {
// 施加冲量
const impulse = new this.Ammo.btVector3(0, 8, 0);
const relPos = new this.Ammo.btVector3(0, 0, 0);
obj.body.applyImpulse(impulse, relPos);
}
}
});
}
animate() {
requestAnimationFrame(() => this.animate());
// 更新物理世界
this.physicsWorld.stepSimulation(1/60, 10);
// 同步位置和旋转
this.objects.forEach(obj => {
if (obj.body.getMass() > 0) {
const motionState = obj.body.getMotionState();
if (motionState) {
const transform = new this.Ammo.btTransform();
motionState.getWorldTransform(transform);
const pos = transform.getOrigin();
const quat = transform.getRotation();
obj.mesh.position.set(pos.x(), pos.y(), pos.z());
obj.mesh.quaternion.set(quat.x(), quat.y(), quat.z(), quat.w());
}
}
});
this.renderer.render(this.scene, this.camera);
}
}
new AmmoPhysicsScene();
纯JavaScript实现,易于集成
API简单直观,学习曲线平缓
体积较小(约200KB)
适合中小型项目
基于Bullet Physics,功能强大
支持复杂的碰撞形状和约束
性能优秀(C++编译到WebAssembly)
适合需要高级物理特性的项目
javascript
// 1. 使用简化的碰撞形状
const simpleShape = new CANNON.Sphere(radius); // 替代复杂网格
// 2. 控制物理更新频率
let physicsTime = 0;
const physicsStep = 1 / 60; // 固定时间步长
function animate(time) {
requestAnimationFrame(animate);
// 累计时间,固定步长更新
physicsTime += time;
while (physicsTime >= physicsStep) {
world.step(physicsStep);
physicsTime -= physicsStep;
}
// 插值渲染
interpolateObjects();
}
// 3. 使用刚体组
const compoundBody = new CANNON.Body({ mass: 1 });
compoundBody.addShape(shape1);
compoundBody.addShape(shape2);
javascript
// 1. 触发器区域(无物理响应的碰撞检测)
const sensorShape = new CANNON.Box(new CANNON.Vec3(1, 1, 1));
const sensorBody = new CANNON.Body({
mass: 0,
isTrigger: true // 设为触发器
});
sensorBody.addEventListener('collide', (e) => {
console.log('物体进入触发器区域');
});
// 2. 约束(关节)
const hingeConstraint = new CANNON.HingeConstraint(
bodyA, bodyB,
{ pivotA: new CANNON.Vec3(0, 0, 0) }
);
world.addConstraint(hingeConstraint);
// 3. 连续碰撞检测(防止穿透)
body.collisionResponse = true; // 启用碰撞响应
body.ccdSpeedThreshold = 0.1;
body.ccdIterations = 10;
选择建议:
对于简单的物理需求,推荐使用 Cannon.js
对于复杂的物理模拟(车辆、布料、柔体等),推荐使用 Ammo.js
考虑团队熟悉程度和项目复杂度做出选择