第十四节:物理引擎集成:Cannon.js入门

发布于:2025-08-18 ⋅ 阅读:(14) ⋅ 点赞:(0)

第十四节:物理引擎集成:Cannon.js入门

引言

物理引擎为3D世界注入真实感,让物体遵循重力、碰撞和动量等物理规律。Cannon.js是Three.js生态中最强大的物理引擎之一,本文将深入解析其核心机制,并通过Vue3实现物理沙盒系统,让你掌握动态仿真的核心技术。


在这里插入图片描述

1. 物理引擎基础
1.1 核心概念
物理世界
刚体
碰撞形状
约束
质量/位置/旋转
盒子/球体/圆柱
铰链/滑块/弹簧
1.2 工作流程
ThreeJS CannonJS Renderer 创建物理体 物理模拟 更新位置/旋转 渲染新状态 ThreeJS CannonJS Renderer

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:物体穿透怎么办?

  1. 增加求解器迭代次数:
    world.solver.iterations = 20;
    
  2. 减少时间步长:
    // 使用固定时间步长
    const fixedTimeStep = 1/120;
    world.step(fixedTimeStep);
    
  3. 使用连续碰撞检测:
    body.collisionResponse = true;
    body.ccdSpeedThreshold = 1.0;
    body.ccdIterations = 10;
    

Q2:如何优化大量物体的性能?

  1. 使用SAPBroadphase
  2. 启用休眠系统
  3. 简化碰撞形状
  4. 静态物体设为mass=0

Q3:角色控制器卡顿问题?

  1. 使用固定时间步长:
    const fixedTimeStep = 1/60;
    let accumulatedTime = 0;
    
    function animate(time) {
      accumulatedTime += deltaTime;
      while (accumulatedTime >= fixedTimeStep) {
        world.step(fixedTimeStep);
        accumulatedTime -= fixedTimeStep;
      }
    }
    
  2. 减少角色碰撞形状复杂度
  3. 限制物理更新频率

10. 总结

通过本文,你已掌握:

  1. 物理引擎核心概念与Cannon.js架构
  2. Three.js与Cannon.js集成技术
  3. 刚体、约束与关节系统
  4. 车辆与角色控制器实现
  5. 碰撞检测与物理事件处理
  6. Vue3物理沙盒系统开发
  7. 物理性能优化策略

核心价值:Cannon.js为Three.js应用带来真实物理行为,结合Vue3的响应式系统,实现交互式物理仿真环境,为游戏、模拟和可视化应用提供坚实基础。


下一篇预告

第十五篇:第一阶段总结:你的第一个3D网页
你将学习:

  • 综合应用前14篇知识
  • 产品级3D展示页开发
  • 响应式设计适配多端
  • 性能优化最佳实践
  • 部署与SEO优化
  • Vue3+Three.js项目架构

准备好将所学知识融会贯通了吗?让我们打造一个令人惊叹的3D产品展示页!


网站公告

今日签到

点亮在社区的每一天
去签到