Skip to content

CannonJS 物理引擎

引用

js
import * as CANNON from "cannon-es";

模拟乒乓球下落反弹

一、碰撞体Body

通过CANNON.Body类,可以创建一个用于物体物理模拟计算,比如用 Body 表示一个球、一个箱子、一张桌子,你可以计算 Body 的位置、速度等物理量

  1. 创建

    js
    const body = new CANNON.Body();
  2. 设置Body的物理属性

    js
    const sphereMaterial = new CANNON.Material()
    const body = new CANNON.Body({
    mass: 0.3,// 碰撞体质量0.3kg
    material: sphereMaterial,//碰撞体材质
    // position: new CANNON.Vec3(0, 100, 0),
    shape: new CANNON.Sphere(1);, //1m半径球体
    });
    body.position.y = 100; // 碰撞体的三维空间中位置

二、创建物理世界

  1. 创建并定义物理属性

    js
    const world = new CANNON.World();
    // 设置物理世界重力加速度
    world.gravity.set(0, -9.8, 0); //单位:m/s²
  2. .addBody()把物体添加到物理世界

    js
    const world = new CANNON.World();
    world.addBody(body);
  3. world.step()物理世界更新计算

    js
    //固定的时间步长1/60秒
    const fixedTimeStep = 1 / 60;
    function render() {
      world.step(fixedTimeStep);
      requestAnimationFrame(render);
    }

三、创建物理地面

js
// 物理地面
const groundMaterial = new CANNON.Material();
const groundBody = new CANNON.Body({
  mass: 0, // 质量为0,始终保持静止,不会受到力碰撞或加速度影响
  shape: new CANNON.Plane(),
  material: groundMaterial,
});
// 改变平面默认的方向,法线默认沿着z轴,旋转到平面向上朝着y方向
//旋转规律类似threejs 平面
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0);
world.addBody(groundBody);

四、设置碰撞物体反弹系数

restitution 的范围一般是 0~1, 值越大弹性越大

js
const contactMaterial = new CANNON.ContactMaterial(groundMaterial, sphereMaterial, {
  restitution: 0.7, //反弹恢复系数
});
// 把关联的材质添加到物理世界中
world.addContactMaterial(contactMaterial);

五、构建 three 网格模型

js
// 网格小球
const geometry = new THREE.SphereGeometry(1.5);
const material = new THREE.MeshLambertMaterial({
  color: 0xffff00,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.y = 100;

// 网格地面
const planeGeometry = new THREE.PlaneGeometry(200, 200);
const texture = new THREE.TextureLoader().load("./瓷砖.jpg");
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(3, 3);
const planeMaterial = new THREE.MeshLambertMaterial({
  color: 0x777777,
  map: texture,
});
const planeMesh = new THREE.Mesh(planeGeometry, planeMaterial);
planeMesh.rotateX(-Math.PI / 2);

六、同步网格小球和物理小球位置属性

js
const clock = new THREE.Clock();
// 渲染循环
function render() {
  world.step(1 / 60); //更新物理计算
  mesh.position.copy(body.position); // 网格小球与物理小球位置同步
  renderer.render(scene, camera);
  requestAnimationFrame(render);
}

七、控制小球重复下落

点击按钮,body 回到下落的初始位置

js
let addBool = false; //标记body是否已经添加到world中
document.getElementById("test").addEventListener("click", function () {
  body.position.y = 1; //点击按钮,body回到下落的初始位置
  if (!addBool) {
    world.addBody(body); //添加到物理世界,才开始下落
    addBool = true;
  }
});

碰撞事件声音

碰撞体Body的碰撞事件collide

js
body.addEventListener("collide", (event) => {
  console.log("碰撞信息", event.contact);
});

碰撞事件的接触属性.contact

当两个碰撞体 Body 碰撞的时候,会生成碰撞信息,碰撞事件.contact包含内容是 body 之间的碰撞信息

  • contact.bi: 监控碰撞事件的 Body
  • contact.bj: 与 contact.bi 发生的碰撞的 Body

contact.getImpactVelocityAlongNormal()

两个物体在碰撞法线方向上对的相对速度

js
const contact = event.contact;
//获得沿法线的冲击速度
contact.getImpactVelocityAlongNormal();

实现

  1. 加载音频代码

    js
    const audio = new Audio("./碰撞声.wav");
    document.getElementById("audio").addEventListener("click", function () {
      audio.volume = 0; //按钮开启声音时候,设置静音
      audio.play();
    });
  2. 设置碰撞事件触发音频播放

    js
    body.addEventListener("collide", () => {
      audio.volume = 1;
      audio.play();
    });
  3. 可以用碰撞相对速度表示碰撞程度,通过碰撞相对速度,控制音量大小

js
body.addEventListener("collide", (event) => {
  const contact = event.contact;
  const ImpactV = contact.getImpactVelocityAlongNormal();
  // 碰撞越狠,声音越大
  //4.5比ImpactV最大值吕略微大一点,这样音量范围0~1
  audio.volume = ImpactV / 4.5;
  audio.play();
});

长方体 Box 碰撞体(箱子下落)

  1. 创建物理长方体

    三维向量的三个参数分别是表示长方体 xyz 方向的一半尺寸

    js
    const boxMaterial = new CANNON.Material();
    const body = new CANNON.Body({
      mass: 0.3,
      material: boxMaterial, //碰撞体材质
      // x、y、z三个方向的尺寸(长宽高),分别为1.0、0.4、0.6
      shape: new CANNON.Box(new CANNON.Vec3(0.5, 0.2, 0.3)),
    });
    body.position.y = 5;
  2. 创建网格长方体

    js
    // 网格长方体
    const geometry = new THREE.BoxGeometry(1.0, 0.4, 0.6);
    const material = new THREE.MeshLambertMaterial({
      map: new THREE.TextureLoader().load("./箱子.jpg"),
    });
    const mesh = new THREE.Mesh(geometry, material);
    mesh.position.y = 5;
  3. 设置弹性系数

    js
    new CANNON.ContactMaterial(groundMaterial, boxMaterial, {
      restitution: 0.2, //箱子,反弹恢复系数低一点
    });
  4. 其余物理地面、物理世界等设置同乒乓球反弹

旋转长方体

箱子换个姿态角度下落,观察下落效果差异

  • 网格箱子旋转
js
// 设置箱子下落的初始姿态角度
mesh.rotation.set(Math.PI / 3, Math.PI / 3, Math.PI / 3);
  • 箱子碰撞体 body 旋转
js
body.quaternion.setFromEuler(Math.PI / 3, Math.PI / 3, Math.PI / 3);
  • 同步箱子 Mesh 和 Body 姿态角度.quaternion

渲染循环中不仅要同步位置.position,也要同步两者的姿态角度.quaternion(四元数)

js
// 渲染循环
function render() {
  world.step(1 / 60);
  // 网格mesh与boyd位置同步
  mesh.position.copy(body.position);
  //同步姿态角度
  mesh.quaternion.copy(body.quaternion);
  renderer.render(scene, camera);
  requestAnimationFrame(render);
}
render();

加载外部 gltf 模型

思路基本一样, 模型尺寸通过包围盒Box3()获取, 再赋值给物理模型

js
const box3 = new THREE.Box3();
box3.expandByObject(mesh); //计算模型包围盒
const size = new THREE.Vector3();
box3.getSize(size); //包围盒计算箱子的尺寸

物理箱子:

js
// 物理箱子
const body = new CANNON.Body({
  //...
  shape: new CANNON.Box(new CANNON.Vec3(size.x / 2, size.y / 2, size.z / 2)),
});

凸多面体ConvexPolyhedron

ConvexPolyhedron可以用来表示不同外形的凸多面体

  1. 加载 gltf 模型凸多面体

    js
    const gltf = await loader.loadAsync("../凸多面体.glb");
    const mesh = gltf.scene.getObjectByName("多面体"); //获取凸多面体网格模型
    mesh.position.y = 5;
    console.log("mesh.geometry", mesh.geometry);
  2. 获取模型三角形顶点数据

    js
    const vertices = []; //所有三角形顶点位置数据
    const faces = []; //所有三角形面的索引值
    const pos = mesh.geometry.attributes.position;
    for (let i = 0; i < pos.count; i++) {
      const x = pos.getX(i);
      const y = pos.getY(i);
      const z = pos.getZ(i);
      vertices.push(new CANNON.Vec3(x, y, z));
    }
    const index = mesh.geometry.index.array;
    for (let i = 0; i < index.length; i += 3) {
      const a = index[i];
      const b = index[i + 1];
      const c = index[i + 2];
      faces.push([a, b, c]);
    }
  3. 创建凸多面体并赋予顶点数据

    js
    // CannonJS的凸多面体ConvexPolyhedron
    const shape = new CANNON.ConvexPolyhedron({ vertices, faces });
    // 物理凸多面体
    const body = new CANNON.Body({
      shape: shape,
    });