1. 基于Cannon.js 和 Ammo.js 的碰撞检测与响应

上一课程我们了解了three.js怎么集成cannon.js 和 ammo.js两种物理引擎。本节课程将学习如何利用它们做碰撞检测和响应。

1.1. Cannon.js 实现方案

关于安装集成教程请看上一课。

1.2. 基础实现代码

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();

1.3. 2. Ammo.js 实现方案

1.3.1. 安装准备

Ammo.js 需要先构建或使用预构建版本:

bash

# 使用预构建版本
# 从 https://github.com/kripken/ammo.js 下载 ammo.js 文件

1.3.2. 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();

1.4. 3. 两者对比和选择建议

Cannon.js 优点:

Ammo.js 优点:

性能优化建议:

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;

选择建议: