主頁 > 企業開發 > Three.js 進階之旅:物理效果-碰撞和聲音 💥

Three.js 進階之旅:物理效果-碰撞和聲音 💥

2023-02-16 08:06:53 企業開發

宣告:本文涉及圖文和模型素材僅用于個人學習、研究和欣賞,請勿二次修改、非法傳播、轉載、出版、商用、及進行其他獲利行為,

摘要

本文內容主要匯總如何在 Three.js 創建的 3D 世界中添加物理效果,使其更加真實,所謂物理效果指的是物件會有重力,它們可以相互碰撞,施加力之后可以移動,而且通過鉸鏈和滑塊還可以在移動程序中在物件上施加約束, 通過本文的閱讀,你將學習到如何使用 Cannon.jsThree.js 中創建一個 3D 物理世界,并在物理世界更新物件、聯系材質、施加外力、處理多個物體中添加物體之間的碰撞效果,通過檢測碰撞激烈程度來添加撞擊聲音等,

效果

本文最終將實作如下所示的效果,點擊 DAT.GUI 中創建立方體 ?? 和球體 ?? 的按鈕,對應的物體將在擁有重力的三維世界中墜落,物體與地面及物體與物體之間發生碰撞時可以產生與碰撞強度匹配的撞擊音頻 ??,點擊重置按鈕,創建的物體將被清除,

打開以下鏈接,在線預覽效果,大屏訪問效果更佳,

  • ????? 在線預覽地址:https://dragonir.github.io/physics-cannon

本專欄系列代碼托管在 Github 倉庫【threejs-odessey】,后續所有目錄也都將在此倉庫中更新

?? 代碼倉庫地址:[email protected]: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

本文作者:dragonir 本文地址:https://www.cnblogs.com/dragonir/p/17121487.html

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/544046.html

標籤:其他

上一篇:canvas畫板(滑鼠和觸摸)

下一篇:解決 Vue3 中路由切換到其他頁面再切換回來時 Echarts 圖表不顯示的問題

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • 使用Django Rest framework搭建Blog

    在前面的Blog例子中我們使用的是GraphQL, 雖然GraphQL的使用處于上升趨勢,但是Rest API還是使用的更廣泛一些. 所以還是決定回到傳統的rest api framework上來, Django rest framework的官網上給了一個很好用的QuickStart, 我參考Qu ......

    uj5u.com 2023-04-20 08:17:54 more
  • 記錄-new Date() 我忍你很久了!

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 大家平時在開發的時候有沒被new Date()折磨過?就是它的諸多怪異的設定讓你每每用的時候,都可能不小心踩坑。造成程式意外出錯,卻一下子找不到問題出處,那叫一個煩透了…… 下面,我就列舉它的“四宗罪”及應用思考 可惡的四宗罪 1. Sa ......

    uj5u.com 2023-04-20 08:17:47 more
  • 使用Vue.js實作文字跑馬燈效果

    實作文字跑馬燈效果,首先用到 substring()截取 和 setInterval計時器 clearInterval()清除計時器 效果如下: 實作代碼如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ......

    uj5u.com 2023-04-20 08:12:31 more
  • JavaScript 運算子

    JavaScript 運算子/運算子 在 JavaScript 中,有一些運算子可以使代碼更簡潔、易讀和高效。以下是一些常見的運算子: 1、可選鏈運算子(optional chaining operator) ?.是可選鏈運算子(optional chaining operator)。?. 可選鏈操 ......

    uj5u.com 2023-04-20 08:02:25 more
  • CSS—相對單位rem

    一、概述 rem是一個相對長度單位,它的單位長度取決于根標簽html的字體尺寸。rem即root em的意思,中文翻譯為根em。瀏覽器的文本尺寸一般默認為16px,即默認情況下: 1rem = 16px rem布局原理:根據CSS媒體查詢功能,更改根標簽的字體尺寸,實作rem單位隨螢屏尺寸的變化,如 ......

    uj5u.com 2023-04-20 08:02:21 more
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 08:01:50 more
  • 如何在 vue3 中使用 jsx/tsx?

    我們都知道,通常情況下我們使用 vue 大多都是用的 SFC(Signle File Component)單檔案組件模式,即一個組件就是一個檔案,但其實 Vue 也是支持使用 JSX 來撰寫組件的。這里不討論 SFC 和 JSX 的好壞,這個仁者見仁智者見智。本篇文章旨在帶領大家快速了解和使用 Vu ......

    uj5u.com 2023-04-20 08:01:37 more
  • 【Vue2.x原始碼系列06】計算屬性computed原理

    本章目標:計算屬性是如何實作的?計算屬性快取原理以及洋蔥模型的應用?在初始化Vue實體時,我們會給每個計算屬性都創建一個對應watcher,我們稱之為計算屬性watcher ......

    uj5u.com 2023-04-20 08:01:31 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:01:10 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:00:32 more