关于three.js:Threejs-进阶之旅物理效果碰撞和声音-💥

申明:本文波及图文和模型素材仅用于集体学习、钻研和观赏,请勿二次批改、非法流传、转载、出版、商用、及进行其余获利行为。

摘要

本文内容次要汇总如何在 Three.js 创立的 3D 世界中增加物理成果,使其更加实在。所谓物理成果指的是对象会有重力,它们能够互相碰撞,施加力之后能够挪动,而且通过铰链和滑块还能够在挪动过程中在对象上施加束缚。 通过本文的浏览,你将学习到如何应用 Cannon.jsThree.js 中创立一个 3D 物理世界,并在物理世界更新对象、分割材质、施加外力、解决多个物体中增加物体之间的碰撞成果,通过检测碰撞强烈水平来增加撞击声音等。

成果

本文最终将实现如下所示的成果,点击 DAT.GUI 中创立立方体 🟦 和球体 🟡 的按钮,对应的物体将在领有重力的三维世界中坠落,物体与高空及物体与物体之间产生碰撞时能够产生与碰撞强度匹配的撞击音频 🔊,点击重置按钮,创立的物体将被革除。

关上以下链接,在线预览成果,大屏拜访成果更佳。

  • 👁‍🗨 在线预览地址:https://dragonir.github.io/physics-cannon

本专栏系列代码托管在 Github 仓库【threejs-odessey】,后续所有目录也都将在此仓库中更新

🔗 代码仓库地址:git@github.com:dragonir/threejs-odessey.git

原理

在专栏之前的原理和示例学习中,咱们曾经能够应用光照、暗影、Raycaster 等个性生成一些简略的物理成果,然而如果须要实现像物体张力、摩擦力、拉伸、反弹等物理成果时,咱们能够应用一些业余的物理个性开源库来实现。

为了实现物理成果,咱们将在 Three.js 中创立一个物理世界,它纯正是实践性质的,咱们无奈间接看到它,然而在其中,三维物体将产生掉落、碰撞、摩擦、滑动等物理个性。具体原理是当咱们在 Three.js 中创立一个网格模型时,同时会将其增加到物理世界中,在每一帧渲染任何内容之前咱们会通知物理世界如何自行更新,而后咱们将获取物理世界中更新的位移和旋转坐标数据,将其利用到 Three.js 三维网格中。

曾经有很多性能齐备的物理个性库,咱们就没必要反复造轮子了。物理个性库能够分为 2D 库和 3D 库,尽管咱们是应用 Three.js 开发三维性能,然而有些 2D库 在三维世界中同样是实用的而且它们的性能会更好,如果咱们须要开发的物理性能是碰撞类的,则能够应用 2D 库,比方Ouigo Let’s play就是一个应用 2D 库开发的优良示例。上面是一些罕用的物理个性库。

对于 3D 物理库,次要有以下三个:

  • Ammo.js

    • 官网:http://schteppe.github.io/ammo.js-demos/
    • 仓库:https://github.com/kripken/ammo.js/
    • 文档:https://github.com/kripken/ammo.js/#readme
    • Bullet 一个应用 C++ 编写的物理引擎的 JavaScript 间接移植
    • 包比拟重量级,以后依然由社区更新保护
  • Cannon.js

    • 官网:https://schteppe.github.io/cannon.js/
    • 仓库:https://github.com/schteppe/cannon.js
    • 文档:http://schteppe.github.io/cannon.js/docs/
    • Ammo.js 更加轻量级,应用起来更难受
    • 次要由一个开发者保护,曾经多年未更新,有一个保护的 forkcannon-es
  • Oimo.js

    • 官网:https://lo-th.github.io/Oimo.js/
    • 仓库:https://github.com/lo-th/Oimo.js
    • 文档:http://lo-th.github.io/Oimo.js/docs.html
    • Ammo.js 轻量且更容易动手
    • 次要由一个开发者保护,曾经有两年没有更新

对于 2D 物理库,有很多,上面列出了比拟风行的几个:

  • Matter.js

    • 官网:https://brm.io/matter-js/
    • 仓库:https://github.com/liabru/matter-js
    • 文档:https://brm.io/matter-js/docs/
    • 次要由一个开发者保护,目前仍在更新中
  • P2.js

    • 官网:https://schteppe.github.io/p2.js/
    • 仓库:https://github.com/schteppe/p2.js
    • 文档:http://schteppe.github.io/p2.js/docs/
    • 次要由一个开发者保护,曾经有2年没有更新
  • Planck.js

    • 官网:https://piqnt.com/planck.js/
    • 仓库:https://github.com/shakiba/planck.js
    • 文档:https://github.com/shakiba/planck.js/tree/master/docs
    • 次要由一个开发者保护,目前仍在更新中
  • Box2D.js

    • 官网:http://kripken.github.io/box2d.js/demo/webgl/box2d.html
    • 仓库:https://github.com/kripken/box2d.js/
    • 文档:无
    • 次要由一个开发者保护,目前仍在更新中

本文内容及示例将应用 Cannon.js 库,因为它更容易了解和应用,对于其余库,应用原理基本上是一样的,大家感兴趣的话能够自行尝试。

Cannon.js

Cannon.js 是一个 3D 物理引擎,通过为物体赋予实在的物理属性的形式来计算静止、旋转和碰撞检测。Cannon.js 相较于其余常见的物理引擎来说,比拟轻量级而且齐全通过 JavaScript 来实现。次要有以下个性:

  • 刚体动力学
  • 离散碰撞检测
  • 接触、摩擦和复原
  • 点到点束缚、铰链束缚、锁紧安装束缚等
  • Gauss-Seidel 束缚求解器与孤岛宰割算法
  • 碰撞过滤
  • 刚体休眠
  • 实验性 SPH 流体反对
  • 各种形态和碰撞算法

Cannon-es

Cannon.js 库曾经多年没有更新了,然而另一库 Cannon-es 克隆了原仓库并致力于长期更新保护新的仓库,能够像上面这样装置并应用,Cannon-es 用法和 Cannon.js 用法是完全一致的。

  • Git 仓库:https://github.com/pmndrs/cannon-es
  • NPM 地址:https://www.npmjs.com/package/cannon-es

实现

🚩 本文示例及相干教程翻译并整顿自 three.js journey 相干课程。

开始

装置并引入

npm install cannon --save
// 或
npm install --save cannon-es
import CANNON from 'cannon';
// 或
import * as CANNON from 'cannon-es';

初始化场景是一个立体 🟩 和一个球体 🟡,为了更好察看物理个性,曾经开启了暗影成果。

咱们能够应用 WebGL 创立一个无重力的太空场景,然而为了模仿地球环境 🌏 ,就须要增加重力,在 Cannon.js 中能够通过批改 gravity 属性值来实现,它是一个 Cannon.js Vec3 值,和 Three.js 中的 Vector3 一样,它蕴含 xyz 属性且领有一个 set(...) 办法

world.gravity.set(0, -9.82, 0);

咱们应用 -9.82 作为重力的 y 值,是因为它是地球的重力系数,如果你想让物理坠落的更慢或者想创立一个火星重力环境 🪐 ,就能够把它改为其余数值。

根底

世界

首先,咱们须要创立一个 Cannon.js 世界:

const world = new CANNON.World();

对象

咱们在场景中曾经创立了一个球体,当初来在 Cannon.js 世界中创立一个球体。为了实现它,咱们首先必须创立一个刚体Body,刚体是一种简略的对象,能够坠落和其余刚体产生碰撞。创立刚提前,咱们首先须要决定刚体的形态,有很多形态可选,比方 BoxCylinderPlane 等,咱们创立一个和 Three.js 中球体雷同半径的球状刚体

const sphereShape = new CANNON.Sphere(0.5);

而后,创立一个初始化 mass 品质及 position 地位的 Body 刚体:

const sphereBody = new CANNON.Body({
  mass: 1,
  position: new CANNON.Vec3(0, 3, 0),
  shape: sphereShape
});

最初,咱们通过 addBody(...) 办法将创立的刚体增加到世界中:

world.addBody(sphereBody);

此时查看页面能够看到没有任何成果,咱们还须要更新 Cannon.js 世界和 Three.js 球体坐标。为更新物理世界world,咱们必须应用工夫步长step(...)办法。

更新

当初须要实现更新 Cannon.js 世界和 Three.js 场景。此时咱们须要应用 step(...) 办法,为了使其失效,必须提供一个固定工夫步长、自上次调用函数以来通过的工夫、以及每个函数调用可执行的最大固定步骤数作为参数。

step(dt, [timeSinceLastCalled], [maxSubSteps=10])
  • dt:固定工夫戳,要应用的固定工夫步长
  • [timeSinceLastCalled]:自上次调用函数以来通过的工夫
  • [maxSubSteps=10]:每个函数调用可执行的最大固定步骤数

🚩 对于工夫步长原理,可查看此文章

在动画函数中,咱们心愿以 60fps 运行,因而将第一个参数设置为 1/60,这个设置在更高或更低帧率的状况下都能以雷同速度运行;对于第二个参数,咱们须要计算自上一帧以来通过了多少工夫,通过将前一帧的 elapsedTime 减去以后 elapsedTime 来取得,不要间接应用 Clock 类中的 getDelta() 办法,因为无奈失去预期的后果还会弄乱外部逻辑;第三个迭代参数,能够轻易设置一个值,运行体验是否丝滑并不重要。

const clock = new THREE.Clock();
let oldElapsedTime = 0;

const tick = () => {
  const elapsedTime = clock.getElapsedTime();
  const deltaTime = elapsedTime - oldElapsedTime;
  oldElapsedTime = elapsedTime;
  //更新物理世界
  world.step(1/60,deltaTime,3)
  controls.update()
  renderer.render(scene, camera)
  window.requestAnimationFrame(tick)
}

此时查看页面,看起来依然没有变动,但实际上物理世界中的球体刚体 sphereBody 正在一直下坠,能够通过如下的打印日志 📜 能够察看到。

console.log(sphereBody.position.y);

当初咱们须要应用物理世界的 sphereBody 刚体坐标来更新 Three.js 中的球体,能够应用如下两种办法实现该性能:

// 办法一
sphere.position.x = sphereBody.position.x;
sphere.position.y = sphereBody.position.y;
sphere.position.z = sphereBody.position.z;
// 办法二
sphere.position.copy(sphereBody.position);

🚩 copy办法在 Vector2、Vector3、Euler、Quaternion 甚至 Material、Object3D、Geometry 等类中都是可用的。

此时就能看到小球 🟡 坠落的成果,然而它间接穿过了高空,因为当初仅在 Three.js 场景中增加了高空,而没有在 Cannon.js 物理世界中创立高空的刚体。

当初咱们应用立体形态 Plane 来创立高空刚体,高空不应该受到物理世界重力的影响而下沉,它应该是放弃静止不动的,咱们能够通过如下办法将 mass 设置为 0 来实现:

const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
floorBody.mass = 0;
floorBody.addShape(floorShape);
world.addBody(floorBody);

此时你会发现小球 🟡 坠落的方向变了,并不是咱们预期的后果,它应该落到高空上。因为物理世界中增加的立体是面向相机 📷 的,咱们须要像在 Three.js 中旋转立体一样对它进行旋转。在 Cannon.js 中,咱们只能应用四元数 Quaternion 来对刚体进行旋转,能够通过 setFromAxisAngle(...) 办法:

  • 第一个参数是旋转轴
  • 第二个参数是角度
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(- 1, 0, 0), Math.PI * 0.5);

当初能够看到小球 🟡 从高处着落并且停在高空上,因为高空是静止不动的,因而咱们不须要应用 Cannon.js 中的高空来更新 Three.js 中的高空。

分割材质

从上图能够察看到,小球 🟡 坠落到高空后并没有重复弹跳,咱们能够通过批改设置 Cannon.js 中的 MaterialContactMaterial增加摩擦和弹跳成果。

一个 Material 仅仅是一个类,你能够用它创立一种材质并命名后将它关联到 Body 刚体上,对于场景中所有的材质,都能够通过此办法进行创立。比方,假如世界中的所有物体都是塑料材质的,此时你只需创立一种材质即可,能够将它命名为 defaultplastic;如果场景中高空和小球是不同材质的,就须要依据它们的类型创立多种材质。上面咱们为示例中的两类物体别离创立名为混凝土 concrete 和 塑料 plastic 的材质:

const concreteMaterial = new CANNON.Material('concrete');
const plasticMaterial = new CANNON.Material('plastic');

接下来,咱们应用创立的两种材质来创立分割材质 ContactMaterial,它是两种材质的组合,蕴含对象碰撞时的属性。而后应用 addContactMaterial(...) 办法将它增加到世界中:

const concretePlasticContactMaterial = new CANNON.ContactMaterial(
  concreteMaterial,
  plasticMaterial,
  {
    friction: 0.1,
    restitution: 0.7
  }
)
world.addContactMaterial(concretePlasticContactMaterial)
ContactMaterial (material1, material2 , [options])
  • 前两个参数是材质
  • 第三个参数是碰撞属性对象,蕴含摩擦系数和恢复系数,两者的默认值均为 0.3

接着咱们将创立好的 Material 利用到 Body 上,能够在实例化主体时间接传递材质,也能够在实例化之后应用材质属性传递材质。当初能够看到小球 🟡 下落后在进行之前会返回弹跳屡次:

const sphereBody = new CANNON.Body({
  material: plasticMaterial
})
// 或者
const floorBody = new CANNON.Body()
floorBody.material = concreteMaterial

场景中个别会有多种材质 Materials 的物体,为每种两两组合创立 ContactMaterial 会费时费解,为了简化这一操作,咱们来应用一种默认材质来替换创立分割材质时的两种材质,并将它利用到所有刚体上:

const defaultMaterial = new CANNON.Material('default');
const defaultContactMaterial = new CANNON.ContactMaterial(
  defaultMaterial,
  defaultMaterial,
  {
    friction: 0.1,
    restitution: 0.7
  }
);
world.addContactMaterial(defaultContactMaterial)l
sphereBody.material = defaultMaterial;
floorBody.material = defaultMaterial;

能够察看到成果是雷同的。或者咱们间接设置世界的默认分割材质defaultContactMaterial 属性,而后移除 sphereBodyfloorBodymaterial 属性,这样世界中的所有材质就都是雷同的默认材质

world.defaultContactMaterial = defaultContactMaterial;

施加外力

对一个刚体 Body 有以下几种施加外力的办法:

  • applyForce(force, worldPoint):从空间中的一个非凡点对刚体施加力(不肯定在刚体的外表),比方就像风推动所有物体一样,或强劲但忽然的力推向多米诺骨牌,或者像强烈且忽然的力把愤恨的小鸟推向城堡一样。

    • force:力的大小 Vec3
    • worldPoint:施加力的世界点 Vec3
  • applyImpulse:相似于 applyForce,但它不是因为减少导致加速度扭转,而是间接作用于加速度。
  • applyLocalForce(force, localPoint):与 applyForce 雷同,然而坐标系是刚体的部分坐标,即 (0, 0, 0) 将是刚体的中点,从物体的外部施力。

    • force:要利用的力向量 Vec3
    • localPoint:刚体中中要施加力的部分点 Vec3
  • applyLocalImpulse:与 applyImpulse 雷同,然而坐标系是刚体的部分坐标,即从物体的外部施力。

当初咱们应用 applyLocalForce(...) 来为小球刚体 sphereBody 开始时施加一个小冲击力:

sphereBody.applyLocalForce(new CANNON.Vec3(150, 0, 0), new CANNON.Vec3(0, 0, 0));

能够看到小球 🟡 向右弹跳并滚动。

当初咱们应用 applyForce(...) 办法来施加一点风力 🌬 ,因为风是永久性的,因而在更新 World 之前,咱们须要将这种力施加到每一帧。要正确利用此力,受力点应该是小球的地位 sphereBody.position

const tick = () => {
  // ...
  sphereBody.applyForce(new CANNON.Vec3(- 0.5, 0, 0), sphereBody.position)
  world.step(1 / 60, deltaTime, 3)
  // ...
}

解决多个物体

对一个或两个物体增加物理成果比较简单,然而为很多个物体都按上述办法增加就会非常复杂,咱们须要增加一个自动化解决办法

主动处理函数

首先,移除或正文掉 Cannon.js 世界和 Three.js 中的球体,还有动画函数 tick() 中球体的设置,而后创立一个 createSphere 办法来生成小球:

const createSphere = (radius, position) => {
  // Three.js mesh
  const mesh = new THREE.Mesh(
    new THREE.SphereGeometry(radius, 20, 20),
    new THREE.MeshStandardMaterial({
      metalness: 0.4,
      roughness: 0.4,
      color: 0xfffc00
    })
  );
  mesh.castShadow = true;
  mesh.position.copy(position);
  scene.add(mesh);

  // Cannon.js body
  const shape = new CANNON.Sphere(radius);
  const body = new CANNON.Body({
    mass: 1,
    position: new CANNON.Vec3(0, 3, 0),
    shape: shape,
    material: defaultMaterial
  });
  body.position.copy(position);
  world.addBody(body);
}

接着应用如下办法来创立一个小球 🟡 ,其中 position 参数不用是 Three.js 中的 Vector3 或者 Cannon.js 中的 Vec3,只需应用 x, y ,z 即可:

createSphere(0.5, { x: 0, y: 3, z: 0 });

能够看到高空顶部的创立的小球,然而因为咱们移除了将 Cannon.js 世界中小球的 position 拷贝到 Three.js 中的办法,当初的小球临时没有物理下坠成果

应用一个对象数组

为了使批量创立的小球失去更新,咱们应用一个数组 objectsToUpdate 在创立函数中保留它们:

const objectsToUpdate = [];
const createSphere = (radius, position) => {
  // ...
  objectsToUpdate.push({
    mesh,
    body
  });
}

而后在动画办法 tick() 中批量将小球的 body.position 拷贝到 mesh.position

const tick = () => {
  // ...
  for (const object of objectsToUpdate) {
    object.mesh.position.copy(object.body.position);
  }
}

此时,批量创立的小球 🟡 也有物理成果了。

增加Dat.GUI

为了不便调试,咱们给页面按如下形式增加 Dat.GUI 调试工具,并增加一个 createSphere 来在场景中创立多个小球:

const gui = new dat.GUI();
const debugObject = {};
debugObject.createSphere = () => {
// 应用随机数创立随机大小和地位的小球
createSphere(
  Math.random() * 0.5,
  {
    x: (Math.random() - 0.5) * 3,
    y: 3,
    z: (Math.random() - 0.5) * 3
  }
)
}
gui.add(debugObject, 'createSphere');

优化

因为 Three.js 网格 Meshgeometrymaterial 都是一样的,咱们应该将其移出 createSphere 办法,因为咱们应用 radius 来创立几何体的,为了兼容之前的办法,咱们能够按如下形式将 SphereGeometry 半径设置为 1,并应用 scale 来调整几何体的大小,失去的后果和下面是统一的,然而性能得以晋升

const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial({
  metalness: 0.4,
  roughness: 0.4,
  color: 0xfffc00
});

const createSphere = (radius, position) => {
  const mesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
  mesh.castShadow = true;
  mesh.scale.set(radius, radius, radius);
  mesh.position.copy(position);
  scene.add(mesh)
// ...
}

增加立方体

当初咱们应用雷同的流程增加一个创立立方体 🟦 的办法 createBox,其中传入的参数将是 widthheightdepthposition。须要留神的是,Cannon.js 中创立BoxThree.js 创立 Box 不同,在 Three.js 中,创立几何体BoxBufferGeometry 只须要间接提供立方体的宽浅近就行,然而在Cannon.js中,它是依据立方体对角线间隔的一半来计算生成形态,因而其宽浅近必须乘以0.5

// 创立立方体
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
const boxMaterial = new THREE.MeshStandardMaterial({
  metalness: 0.4,
  roughness: 0.4,
  color: 0x0091ff
})
const createBox = (width, height, depth, position) => {
  // Three.js 网格
  const mesh = new THREE.Mesh(boxGeometry, boxMaterial);
  mesh.scale.set(width, height, depth);
  mesh.castShadow = true;
  mesh.position.copy(position);
  scene.add(mesh);
  // Cannon.js 刚体
  const shape = new CANNON.Box(new CANNON.Vec3(width * 0.5, height * 0.5, depth * 0.5))
  const body = new CANNON.Body({
    mass: 1,
    position: new CANNON.Vec3(0, 3, 0),
    shape: shape,
    material: defaultMaterial
  })
  body.position.copy(position);
  world.addBody(body);
  // 保留在更新对象数组中
  objectsToUpdate.push({ mesh, body });
}

createBox(1, 1.5, 2, { x: 0, y: 3, z: 0 });

// 增加到DAT.GUI
debugObject.createBox = () => {
  createBox(
    Math.random(),
    Math.random(),
    Math.random(),
    {
      x: (Math.random() - 0.5) * 3,
      y: 3,
      z: (Math.random() - 0.5) * 3
    }
  )
}
gui.add(debugObject, 'createBox');

先移除创立小球的办法,页面运行能够失去如下的后果:

当初能够创立随机的立方体了,然而看起来有点奇怪不太真切是不是?因为立方体掉下来后没有翻转,起因是 Three.js 中的网格没有像 Cannon.js 中的刚体一样旋转,在球体的示例中咱们没有发现是因为无论球体是否旋转都是和原来一样的,而在立方体中不一样。咱们能够通过如下将刚体的 quaternion 属性拷贝到网格的 quaternion 属性来实现,就像之前拷贝地位属性 position 一样:

const tick = () => {
  // ...
  for (const object of objectsToUpdate) {
    object.mesh.position.copy(object.body.position);
    object.mesh.quaternion.copy(object.body.quaternion);
  }
  // ...
}

当初立方体 🟦 坠落时的旋转也失常了。

性能优化

Broadphase

测试物体之间的碰撞时,一种办法是检测一个刚体与另外所有其余刚体之间的碰撞,尽管这一操作很容易实现,然而十分消耗性能。此时就须要 Broadphase,它会在测试之前对刚体进行粗略的分类,设想一下,两堆相距很远的立方体,为什么要用一堆立方体来测试另一堆立方体之间的碰撞关系能,它们相距很远,不会产生碰撞,因而就没必要测试来消耗性能。

Cannon.js 中共有 3Broadphase 算法:

  • NaiveBroadphase:测试每个刚体与其余所有刚体之间的碰撞,默认算法。
  • GridBroadphase: 应用四边形栅格笼罩 world,仅针对同一栅格或相邻栅格中的其余刚体进行碰撞测试。
  • SAPBroadphase:扫描剪枝算法,在多个步骤的任意轴上测试刚体。

NaiveBroadphase 是默认检测办法,然而举荐应用 SAPBroadphase 算法,尽管这种算法有时可能会产生检测不会产生碰撞的谬误,然而它的检测速度十分快。通过如下形式,简略设置 world.broadphase 属性即可批改碰撞检测算法:

world.broadphase = new CANNON.SAPBroadphase(world);

Sleep 💤

即便咱们应用改良的 Broadphase 碰撞检测算法,有可能所有的刚体都会被检测,即便是那些不再产生挪动的刚体。此时咱们能够应用称为 Sleep 的个性,当刚体的速度逐步变小不再产生挪动,它就会进入睡眠状态,此时就不会对它进行碰撞检测,除非应用代码让其施加一个足够的力再次静止或有其它的刚体击中它。能够通过对 world 设置 allowSleep 属性为 true 来实现:

world.allowSleep = true;

你也能够应用 sleepSpeedLimitsleepTimeLimit 属性对睡眠速度和工夫进行具体设置,然而个别不会扭转默认值。

事件

能够对刚体的事件进行监听,比方你想在物体产生碰撞时播放呻吟或者在射击游戏中检测是否命中敌人等状况下是十分有用的。你能够在刚体上监听 colidesleepwakeup 等事件。

当初,咱们来实现一下当场景中的小球 🟡 和立方体 🟦 相互之间产生碰撞时播放声音 🔊 的性能。首先在 JavaScript 中创立音频,并增加一个办法来播放它。

🚩 有些浏览器比方 Chrome 默认会静音 🔕 除非用户与页面产生交互,例如点击任意区域,所以不要放心首次加载时不播放声音的问题

const hitSound = new Audio('/sounds/hit.mp3');
const playHitSound = () => {
  // 播放工夫重置为0,解决屡次调用时声音间断问题
  hitSound.currentTime = 0
  hitSound.play()
}

而后在创立立方体办法 createBox 中调用:

const createBox = (width, height, depth, position) => {
  // ...
  body.addEventListener('collide', playHitSound);
  // ...
}

此时,当立方体 🟦 撞击到高空或互相碰撞时能够听到撞击声音 🔊,看起来仿佛是正确的,然而当增加多个立方体时,咱们会听到很多立方体之间互相撞击的声音是一样的,而事实中的声音应该是依据声音随着立方体之间的撞击水平而不同,撞击水平足够小的话就听不到声音。为了获取撞击的强度,咱们须要获取撞击信息,能够通过如下给 playHitSound 办法增加参数 collision 的形式来获取撞击信息:

const playHitSound = (collision) => {
const impactStrength = collision.contact.getImpactVelocityAlongNormal();
  // 只有撞击强度足够大时才播放撞击音频
  if (impactStrength > 1.5) {
    // 为了更加实在,能够给音量增加一些随机性
    hitSound.volume = Math.random();
    hitSound.currentTime = 0;
    hitSound.play();
  }
}

而后在创立球体的办法 createSphere 中同样调用播放撞击音频办法:

const createSphere = (radius, position) => {
  // ...
  body.addEventListener('collide', playHitSound)
  // ...
}

移除物体

当页面上增加过多物体时,咱们能够通过在 Dat.GUI 增加一个重置按钮来移除已增加的物体,通过遍历 objectsToUpdate 数组,将每个数组项对应的的 object.body 从 物理世界 world 中移除,将 object.meshThree.js 场景中移除,并革除 collide 碰撞事件的 eventListener

debugObject.reset = () => {
  for (const object of objectsToUpdate) {
    object.body.removeEventListener('collide', playHitSound);
    world.removeBody(object.body);
    scene.remove(object.mesh);
  }
}
gui.add(debugObject, 'reset');

总结

本文中次要蕴含的知识点包含:

  • Three.js 中增加物理成果基本原理
  • 罕用 3D2D 物理物理引擎汇总
  • Cannon.jsCannon-es 装置与援用
  • 物理世界创立、对象更新、分割材质、施加外力、解决多个物体
  • 碰撞事件监听、音频增加
  • 性能优化、物理世界移除物体等

想理解其余前端常识或其余未在本文中详细描述的Web 3D开发技术相干常识,可浏览我往期的文章。如果有疑难能够在评论中留言,如果感觉文章对你有帮忙,不要忘了一键三连哦 👍

附录

  • [1]. 🌴 Three.js 打造缤纷夏日3D梦中情岛
  • [2]. 🔥 Three.js 实现炫酷的赛博朋克格调3D数字地球大屏
  • [3]. 🐼 Three.js 实现2022冬奥主题3D趣味页面,含冰墩墩
  • [4]. 🦊 Three.js 实现3D凋谢世界小游戏:阿狸的多元宇宙
  • [5]. 🏆 掘金1000粉!应用Three.js实现一个创意留念页面
  • ...
  • 【Three.js 进阶之旅】系列专栏拜访 👈
  • 更多往期【3D】专栏拜访 👈
  • 更多往期【前端】专栏拜访 👈

参考

  • [1]. three.js journey
  • [2]. threejs.org

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理