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

dragonir發表於2023-02-15
本文參與了SegmentFault 思否寫作挑戰賽,歡迎正在閱讀的你也加入。

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

摘要

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

效果

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

開啟以下連結,線上預覽效果,大屏訪問效果更佳。

本專欄系列程式碼託管在 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 物理庫,主要有以下三個:

對於 2D 物理庫,有很多,下面列出了比較流行的幾個:

本文內容及示例將使用 Cannon.js 庫,因為它更容易理解和使用,對於其他庫,使用原理基本上是一樣的,大家感興趣的話可以自行嘗試。

Cannon.js

Cannon.js 是一個 3D 物理引擎,透過為物體賦予真實的物理屬性的方式來計算運動、旋轉和碰撞檢測。Cannon.js 相較於其他常見的物理引擎來說,比較輕量級而且完全透過 JavaScript 來實現。主要有以下特性:

  • 剛體動力學
  • 離散碰撞檢測
  • 接觸、摩擦和恢復
  • 點到點約束、鉸鏈約束、鎖緊裝置約束等
  • Gauss-Seidel 約束求解器與孤島分割演算法
  • 碰撞過濾
  • 剛體休眠
  • 實驗性 SPH 流體支援
  • 各種形狀和碰撞演算法

Cannon-es

Cannon.js 庫已經多年沒有更新了,但是另一庫 Cannon-es 克隆了原倉庫並致力於長期更新維護新的倉庫,可以像下面這樣安裝並使用,Cannon-es 用法和 Cannon.js 用法是完全一致的。

實現

? 本文示例及相關教程翻譯並整理自 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開發技術相關知識,可閱讀我往期的文章。如果有疑問可以在評論中留言,如果覺得文章對你有幫助,不要忘了一鍵三連哦 ?

附錄

參考

相關文章