1. 物理引擎集成:Cannon.js/Ammo.js

1.1. 物理引擎选择对比

物理引擎选择对比.png

1.2. Cannon.js 集成指南

1.2.1. 安装

bash

npm install cannon-es

1.2.2. 基础集成示例

javascript

import * as THREE from 'three';
import * as CANNON from 'cannon-es';

class PhysicsWorld {
  constructor() {
    // 创建物理世界
    this.world = new CANNON.World();
    this.world.gravity.set(0, -9.82, 0);
    this.world.broadphase = new CANNON.SAPBroadphase(this.world);

    // 存储 Three.js 和 Cannon.js 对象的映射
    this.objects = new Map();

    // 设置时间步长
    this.timeStep = 1 / 60;
  }

  // 创建立方体物理体
  createBox(mesh, mass = 1, material = null) {
    const size = mesh.geometry.parameters || 
                 new THREE.Vector3().fromArray(mesh.geometry.attributes.position.array);

    const shape = new CANNON.Box(new CANNON.Vec3(
      size.width / 2 || 1,
      size.height / 2 || 1,
      size.depth / 2 || 1
    ));

    const body = new CANNON.Body({
      mass,
      shape,
      material: material || new CANNON.Material()
    });

    body.position.copy(mesh.position);
    body.quaternion.copy(mesh.quaternion);

    this.world.addBody(body);
    this.objects.set(mesh.uuid, { mesh, body });

    return body;
  }

  // 创建球体物理体
  createSphere(mesh, mass = 1) {
    const radius = mesh.geometry.parameters.radius;
    const shape = new CANNON.Sphere(radius);

    const body = new CANNON.Body({
      mass,
      shape,
      material: new CANNON.Material()
    });

    body.position.copy(mesh.position);
    this.world.addBody(body);
    this.objects.set(mesh.uuid, { mesh, body });

    return body;
  }

  // 创建地面
  createGround(mesh) {
    const body = new CANNON.Body({
      mass: 0, // 质量为 0 表示静态物体
      shape: new CANNON.Plane()
    });

    body.quaternion.setFromEuler(-Math.PI / 2, 0, 0); // 旋转为水平面
    this.world.addBody(body);
    this.objects.set(mesh.uuid, { mesh, body });

    return body;
  }

  // 更新物理世界并同步 Three.js 对象
  update() {
    this.world.step(this.timeStep);

    // 同步物理体和 Three.js 对象
    this.objects.forEach(({ mesh, body }) => {
      if (mesh && body) {
        mesh.position.copy(body.position);
        mesh.quaternion.copy(body.quaternion);
      }
    });
  }

  // 应用力
  applyForce(meshId, force, worldPoint) {
    const obj = this.objects.get(meshId);
    if (obj && obj.body) {
      obj.body.applyForce(force, worldPoint || obj.body.position);
    }
  }
}

// 使用示例
class Game {
  constructor() {
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
    this.renderer = new THREE.WebGLRenderer();

    // 创建物理世界
    this.physicsWorld = new PhysicsWorld();

    this.init();
    this.animate();
  }

  init() {
    // 创建 Three.js 物体
    const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
    const boxMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
    const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);
    boxMesh.position.set(0, 5, 0);
    this.scene.add(boxMesh);

    // 创建对应的物理体
    this.physicsWorld.createBox(boxMesh, 1);

    // 创建地面
    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;
    this.scene.add(groundMesh);

    this.physicsWorld.createGround(groundMesh);
  }

  animate() {
    requestAnimationFrame(() => this.animate());

    // 更新物理世界
    this.physicsWorld.update();

    this.renderer.render(this.scene, this.camera);
  }
}

1.3. Ammo.js 集成指南

1.3.1. 安装与初始化

javascript

// 下载 ammo.wasm 或使用 CDN
// 下载路径[已经预构建的](https://github.com/kripken/ammo.js/blob/main/builds/ammo.js)
// 初始化 Ammo.js
async function initAmmo() {
  return new Promise((resolve) => {
    Ammo().then(resolve);
  });
}

class AmmoPhysicsWorld {
  constructor() {
    this.rigidBodies = [];
    this.transformAux1 = new Ammo.btTransform();
  }

  async init() {
    this.ammo = await initAmmo();

    // 创建物理世界
    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.world = new this.ammo.btDiscreteDynamicsWorld(
      dispatcher,
      overlappingPairCache,
      solver,
      collisionConfiguration
    );

    this.world.setGravity(new this.ammo.btVector3(0, -9.8, 0));
  }

  // 创建立方体物理体
  createRigidBody(threeObject, physicsShape, mass, pos, quat) {
    const transform = new this.ammo.btTransform();
    transform.setIdentity();
    transform.setOrigin(new this.ammo.btVector3(pos.x, pos.y, pos.z));
    transform.setRotation(new this.ammo.btQuaternion(quat.x, quat.y, quat.z, quat.w));

    const motionState = new this.ammo.btDefaultMotionState(transform);
    const localInertia = new this.ammo.btVector3(0, 0, 0);

    if (mass > 0) {
      physicsShape.calculateLocalInertia(mass, localInertia);
    }

    const rbInfo = new this.ammo.btRigidBodyConstructionInfo(
      mass,
      motionState,
      physicsShape,
      localInertia
    );

    const body = new this.ammo.btRigidBody(rbInfo);

    if (mass > 0) {
      body.setActivationState(4); // DISABLE_DEACTIVATION
    }

    this.world.addRigidBody(body);

    // 存储关联
    threeObject.userData.physicsBody = body;
    this.rigidBodies.push(threeObject);

    return body;
  }

  update(deltaTime) {
    this.world.stepSimulation(deltaTime, 10);

    // 更新所有刚体的位置和旋转
    for (let i = 0; i < this.rigidBodies.length; i++) {
      const objThree = this.rigidBodies[i];
      const objAmmo = objThree.userData.physicsBody;

      if (objAmmo) {
        const motionState = objAmmo.getMotionState();
        if (motionState) {
          motionState.getWorldTransform(this.transformAux1);
          const p = this.transformAux1.getOrigin();
          const q = this.transformAux1.getRotation();

          objThree.position.set(p.x(), p.y(), p.z());
          objThree.quaternion.set(q.x(), q.y(), q.z(), q.w());
        }
      }
    }
  }
}

1.4. 高级特性集成

1.4.1. 约束(关节)

javascript

// Cannon.js 约束示例
class PhysicsConstraints {
  constructor(world) {
    this.world = world;
  }

  createHingeConstraint(bodyA, bodyB, options) {
    const { pivotA, pivotB, axisA, axisB } = options;

    const constraint = new CANNON.HingeConstraint(
      bodyA, bodyB,
      pivotA, pivotB,
      axisA, axisB
    );

    this.world.addConstraint(constraint);
    return constraint;
  }

  createDistanceConstraint(bodyA, bodyB, distance) {
    const constraint = new CANNON.DistanceConstraint(
      bodyA, bodyB,
      distance
    );

    this.world.addConstraint(constraint);
    return constraint;
  }
}

1.4.2. 碰撞检测

javascript

class CollisionSystem {
  constructor(world) {
    this.world = world;
    this.contactMaterial = new CANNON.ContactMaterial(
      new CANNON.Material('default'),
      new CANNON.Material('ground'),
      {
        friction: 0.5,
        restitution: 0.3
      }
    );

    this.world.addContactMaterial(this.contactMaterial);

    // 监听碰撞事件
    this.world.addEventListener('beginContact', (event) => {
      this.handleCollision(event.bodyA, event.bodyB);
    });
  }

  handleCollision(bodyA, bodyB) {
    console.log('碰撞发生在:', bodyA, '和', bodyB);

    // 可以触发游戏逻辑
    if (bodyA.userData && bodyA.userData.type === 'player') {
      this.handlePlayerCollision(bodyB);
    }
  }
}

1.5. 性能优化建议

  1. 简化碰撞体:使用简单的几何体近似复杂模型

  2. 合理使用质量:静态物体质量设为0

  3. 批次更新:减少渲染和物理更新频率

  4. 休眠机制:启用自动休眠减少计算

javascript

// Cannon.js 启用休眠
world.allowSleep = true;

// Ammo.js 手动控制激活状态
body.setActivationState(4); // 禁用休眠

1.6. 实际应用建议

1.6.1. 选择建议:

1.6.2. 最佳实践:

  1. 保持物理更新频率稳定(60Hz)

  2. 分离渲染帧率和物理更新率

  3. 使用射线检测进行交互

  4. 实现对象池重用物理对象

1.7. 调试工具:

javascript

// Cannon.js 调试可视化
import CannonDebugger from 'cannon-es-debugger';

const cannonDebugger = new CannonDebugger(scene, world);
// 在渲染循环中
cannonDebugger.update();

这两种物理引擎都能为 Three.js 项目提供强大的物理能力。根据项目需求和复杂度选择合适的方案,并注意性能优化以获得最佳体验。