介紹
物理是WebGL可以添加到項(xiàng)目體驗(yàn)中最酷的功能之一。人們喜歡真實(shí)物理感的物體,看到它們碰撞、倒塌、墜落和彈跳,就像我的作品集一樣: https: //bruno-simon.com/
有很多方法可以將物理功能添加到您的項(xiàng)目中,這取決于您想要實(shí)現(xiàn)的目標(biāo)。您可以使用一些數(shù)學(xué)和解決方案(例如Raycaster)來創(chuàng)建自己的物理學(xué)
理論
這個(gè)想法很簡單。我們將創(chuàng)建一個(gè)物理世界。這個(gè)物理世界是純理論的。在這個(gè)物理世界上,東西會產(chǎn)生掉落、碰撞、摩擦、滑動(dòng)等等交互。
當(dāng)我們創(chuàng)建一個(gè) Three.js 網(wǎng)格時(shí),我們還將在物理世界中創(chuàng)建該網(wǎng)格的一個(gè)物理版本。如果我們在 Three.js 中創(chuàng)建一個(gè) Box,我們也會在物理世界中創(chuàng)建一個(gè)Box框。
然后,在每一幀上,在渲染任何東西之前,我們告訴物理世界進(jìn)行自我更新;我們獲取物理對象的坐標(biāo)(位置和旋轉(zhuǎn))并將它們應(yīng)用于相應(yīng)的 Three.js 網(wǎng)格。
就是這么簡單的原理。這里最困難的是將我們的代碼組織成一個(gè)合理的結(jié)構(gòu)。這是一個(gè)完全和原本文件路徑分開的路徑部分。每個(gè)開發(fā)人員都會有自己的習(xí)慣,這也取決于你想做什么以及你想把這個(gè)物理世界變得多復(fù)雜。
首先,我們將簡單地創(chuàng)建球體和盒子。
物理功能依賴庫
物理功能有多個(gè)可用的庫。首先,您必須決定是需要 3D 庫還是 2D 庫。雖然您可能認(rèn)為它必須是一個(gè) 3D 庫,因?yàn)?Three.js 完全是關(guān)于 3D 的,但您可能錯(cuò)了。2D 庫通常性能更高,如果您可以總結(jié) 2D 碰撞的物理經(jīng)驗(yàn),則最好使用 2D 庫。
舉一個(gè)例子是如果你想創(chuàng)建一個(gè)類似??彈球游戲。球可以在墻上碰撞和彈跳,您就可以使用 2D 庫將所有東西投影到二維平面上。您可以將球設(shè)計(jì)成物理世界中的圓圈,而墻壁是簡單的矩形。事實(shí)上,這么做您將無法通過擊球底部來使球跳過其他球。
像這樣完成的項(xiàng)目的一個(gè)很好的例子是Merci Michel的Ouigo Let’s play。他們使用了 2D 物理庫,因?yàn)槊總€(gè)碰撞和動(dòng)畫都可以在 2D 空間中表示。
3D物理
對于 3D 物理,主要有三個(gè)庫:
ammo.js
- 網(wǎng)站: http: //schteppe.github.io/ammo.js-demos/
- Git 存儲庫:https://github.com/kripken/ammo.js/
- 文檔:無文檔
- Bullet 的直接 JavaScript 端口(用 C++ 編寫的物理引擎)
- 體積大有點(diǎn)重
- 社區(qū)仍然在更新
cannon.js
- 網(wǎng)站: https: //schteppe.github.io/cannon.js/
- Git 存儲庫: https: //github.com/schteppe/cannon.js
- 文檔: http: //schteppe.github.io/cannon.js/docs/
- 比 Ammo.js 更輕
- 比 Ammo.js 更容易實(shí)現(xiàn)
- 主要由一名開發(fā)人員維護(hù)
- 多年未更新
- 有一個(gè)維護(hù)的叉子
Oimo.js
- 網(wǎng)站: https: //lo-th.github.io/Oimo.js/
- Git 存儲庫:https://github.com/lo-th/Oimo.js
- 文檔:http://lo-th.github.io/Oimo.js/docs.html
- 比 Ammo.js 更輕
- 比 Ammo.js 更容易實(shí)現(xiàn)
- 主要由一名開發(fā)人員維護(hù)
- 2年沒更新了
2D物理
對于 2D 物理,有很多庫,但這里是最流行的:
matter.js
- 網(wǎng)站: https: //brm.io/matter-js/
- Git 存儲庫: https: //github.com/liabru/matter-js
- 文檔: https: //brm.io/matter-js/docs/
- 主要由一名開發(fā)人員維護(hù)
- 還是有點(diǎn)更新
P2.js
- 網(wǎng)站: https: //schteppe.github.io/p2.js/
- Git 存儲庫: https: //github.com/schteppe/p2.js
- 文檔: http: //schteppe.github.io/p2.js/docs/
- 主要由一名開發(fā)人員維護(hù)(與 Cannon.js 相同)
- 2年沒更新了
planck.js
- 網(wǎng)站: https: //piqnt.com/planck.js/
- Git 存儲庫: https: //github.com/shakiba/planck.js
- 文檔: https: //github.com/shakiba/planck.js/tree/master/docs
- 主要由一名開發(fā)人員維護(hù)
- 現(xiàn)在還在更新
Box2D.js
- 網(wǎng)站:http://kripken.github.io/box2d.js/demo/webgl/box2d.html
- Git 存儲庫: https: //github.com/kripken/box2d.js/
- 文檔:無文檔
- 主要由一名開發(fā)人員維護(hù)(與 Ammo.js 相同)
- 現(xiàn)在還在更新
我們不會在本課中使用 2D 庫,但 2D 庫代碼與 3D 庫代碼非常相似。主要區(qū)別在于您必須更新的軸。
已經(jīng)有嘗試將 Three.js 與Physijs等庫結(jié)合起來的解決方案。盡管如此,我們不會使用這些已經(jīng)做好封裝的現(xiàn)成解決方案,我們要手動(dòng)結(jié)合物理庫來獲得更好的學(xué)習(xí)體驗(yàn)并更好地理解內(nèi)部運(yùn)行的邏輯。
雖然 Ammo.js 是最常用的庫,尤其是在 Three.js中,正如您在示例中看到的那樣,我們將選擇 Cannon.js。這個(gè)庫在我們的項(xiàng)目中實(shí)現(xiàn)起來更舒服,也更容易使用。
導(dǎo)入 Cannon.js
要將 Cannon.js
添加到我們的項(xiàng)目中,我們首先需要添加依賴項(xiàng)。
在您的終端的項(xiàng)目文件夾中,運(yùn)行此命令npm install --save cannon
。
我們現(xiàn)在可以使用經(jīng)典的 JavaScript 在我們的 JavaScript 中import
導(dǎo)入 Cannon.js :
import CANNON from 'cannon'
我們需要的一切都在CANNON
變量中可用。
設(shè)置
我們的啟動(dòng)器由平面上的一個(gè)球體組成,并且出于美學(xué)原因已經(jīng)啟用了陰影。
基礎(chǔ)
世界
首先,我們需要?jiǎng)?chuàng)建一個(gè) Cannon.js世界:
/**
* Physics
*/
const world = new CANNON.World()
現(xiàn)在我們獲得了一個(gè),感覺在沒有重力漂浮在太空中的 WebGL 體驗(yàn)感,讓我們增加重力腳踏實(shí)地。您可以使用Cannon.js Vec3 的 gravity
屬性更改重力。
**Cannon.js **Vec3就像 Three.js Vector3一樣。它也有**x**
、**y**
和**z**
屬性,還有一個(gè)**set(...)**
方法:
world.gravity.set(0, - 9.82, 0)
我們把第二個(gè)參數(shù)值 改為 - 9.82
是因?yàn)椋?code>- 9.82它是地球上的重力常數(shù),但如果您想讓物體下落得更慢或者如果您的場景發(fā)生在火星上,您可以使用其他重力值。
目的
因?yàn)槲覀兊膱鼍爸幸呀?jīng)有了一個(gè)球體,所以讓我們在 Cannon.js World中也創(chuàng)建一個(gè)球體。
為此,我們必須創(chuàng)建一個(gè)Body。Body是會掉落的并與其他物體碰撞的。
在我們創(chuàng)建一個(gè)Body之前,我們必須決定一個(gè)形狀。有許多可用的基本形狀,如Box、Cylinder、Plane等。我們將選擇一個(gè)與 Three.js 球體具有相同半徑的Sphere :
const sphereShape = new CANNON.Sphere(0.5)
然后我們可以創(chuàng)建我們的body并指定質(zhì)量和位置:
const sphereBody = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(0, 3, 0),
shape: sphereShape
})
最后,我們可以將Body 通過addBody(...)
添加到世界中:
world.addBody(sphereBody)
現(xiàn)在頁面里什么都沒有發(fā)生,因?yàn)槲覀內(nèi)匀恍枰挛覀兊?Cannon.js 世界并相應(yīng)地更新我們的 Three.js 球體。
更新 Cannon.js 世界和 Three.js 場景
要更新我們的world世界,我們必須使用step(...)
. 該方法底層的代碼很難理解,我們不會在本課中對其進(jìn)行解釋,但您可以在本文中找到更多相關(guān)信息。
要讓它工作,您必須提供一個(gè)固定的時(shí)間步長、自上一步以來經(jīng)過了多少時(shí)間,以及世界world可以應(yīng)用多少次迭代來趕上潛在的延遲。
我們不會解釋什么是時(shí)間步長,但我們希望體驗(yàn)以 60fps 的速度運(yùn)行,所以我們將使用1 / 60來表示. 別擔(dān)心,在幀率更高和更低的設(shè)備上,體驗(yàn)將以相同的速度運(yùn)行。
迭代次數(shù)由你決定,但體驗(yàn)是否流暢就沒那么重要了。
對于三角洲時(shí)間,它有點(diǎn)復(fù)雜。我們需要計(jì)算自上一幀以來經(jīng)過了多少時(shí)間。不要使用Clock類中的getDelta()
方法。你不會得到預(yù)期的結(jié)果,而且你會搞亂類的內(nèi)部邏輯。
為了獲得正確的增量時(shí)間,我們需要從前一幀elapsedTime
減去當(dāng)前幀elapsedTime
獲得:
const clock = new THREE.Clock()
let oldElapsedTime = 0
const tick = () =>
{
const elapsedTime = clock.getElapsedTime()
const deltaTime = elapsedTime - oldElapsedTime
oldElapsedTime = elapsedTime
// ...
}
我們終于可以更新我們的世界了:
const tick = () =>
{
// ...
// Update physics
world.step(1 / 60, deltaTime, 3)
}
似乎沒有任何東西在移動(dòng)。其實(shí)現(xiàn)實(shí)是我們的sphereBody
正在無限的墮入深淵,只是因?yàn)橄鄼C(jī)一直跟著物體墜落所以你難以發(fā)現(xiàn),你可以通過在更新world世界后記錄它的位置來看到:
world.step(1 / 60, deltaTime, 3)
console.log(sphereBody.position.y)
我們現(xiàn)在需要做的是使用sphereBody
坐標(biāo)更新我們的sphere
。 Three.js 有兩種方法可以做到這一點(diǎn)。您可以單獨(dú)更新每個(gè)position
屬性:
sphere.position.x = sphereBody.position.x
sphere.position.y = sphereBody.position.y
sphere.position.z = sphereBody.position.z
或者您可以使用以下方法將所有屬性作為一個(gè)復(fù)制copy(...)
:
sphere.position.copy(sphereBody.position)
copy(...)
在許多類中可用,例如Vector2、Vector3、Euler、Quaternion,甚至類如Material、Object3D、Geometry等。
你最終應(yīng)該看到你的項(xiàng)目中球體正在自由落體。問題是我們的球體似乎從地板上掉了下來。這是因?yàn)樵摰匕宕嬖谟?Three.js 場景中,但不存在于 Cannon.js 世界中。
我們可以使用Plane形狀簡單地添加一個(gè)新的Body,但我們不希望我們的地板受到重力影響而掉落。換句話說,我們希望我們的地板是靜態(tài)的。要使Body靜態(tài),請將其設(shè)置為:mass = 0
const floorShape = new CANNON.Plane()
const floorBody = new CANNON.Body()
floorBody.mass = 0
floorBody.addShape(floorShape)
world.addBody(floorBody)
如您所見,這次我們的做法大不相同。我們創(chuàng)建了一個(gè)沒有參數(shù)的Body ,然后我們設(shè)置了這些參數(shù)。結(jié)果是一樣的,我們這樣做的唯一原因是為了上課講解。一件有趣的事情是您可以創(chuàng)建一個(gè)由多個(gè)Shapes組成的Body。它對于復(fù)雜但堅(jiān)固的物體很有用。
您應(yīng)該看到球體朝一個(gè)方向(可能朝向相機(jī))跳躍。這不是預(yù)期的結(jié)果。原因是我們的地板plane默認(rèn)正對著相機(jī)。我們需要像在 Three.js 中旋轉(zhuǎn)地板一樣旋轉(zhuǎn)它讓他轉(zhuǎn)到離開相機(jī)的區(qū)域。
使用 Cannon.js 進(jìn)行旋轉(zhuǎn)比使用 Three.js 稍微困難一些,因?yàn)槟仨毷褂肣uaternion來實(shí)現(xiàn)。有多種旋轉(zhuǎn)Body的方法,但必須使用其quaternion
屬性。我們將使用setFromAxisAngle(...)
方法旋轉(zhuǎn)body.
第一個(gè)參數(shù)是一個(gè)軸。您可以將其想象成穿過身體的一根線。第二個(gè)參數(shù)是角度。這是你圍繞這條線旋轉(zhuǎn)身體的角度。
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(- 1, 0, 0), Math.PI * 0.5)
我們將軸設(shè)置為負(fù)軸(相對于相機(jī)的左側(cè))穿過身體的線,并將x
角度設(shè)置為(四分之一圓)。Math.PI * 0.5
您現(xiàn)在應(yīng)該看到球體下落然后停在地板上。
我們不需要用 Cannon.js 地板更新 Three.js 地板,因?yàn)檫@個(gè)對象不會再移動(dòng)了。
ContactMaterial 關(guān)聯(lián)材料
如您所見,球落地后基本不會彈跳。這是默認(rèn)行為,我們可以使用Material(不是 Three.js 中的 Material)和ContactMaterial來讓它變的富有彈性。
材料只是一個(gè)參考。您可以給它起一個(gè)名字并將它與一個(gè)Body相關(guān)聯(lián)。然后為場景中的每種材質(zhì)創(chuàng)建一個(gè)材質(zhì)。
如果場景中有多種材質(zhì),假設(shè)一種木料材質(zhì)用于地板,一種金屬材質(zhì)用于球。然后,您應(yīng)該創(chuàng)建各種材質(zhì)并為它們命名,例如'concrete'
和'plastic'
。
(假設(shè)你世界里的一切都是塑料材質(zhì)制成的。在這種情況下,您只需創(chuàng)建一種材料并將其命名為'default
’即可。)
你可以給他們互相關(guān)聯(lián)到'ground'
和'ball'
中。盡管如此,如果您想對墻壁和立方體等其他對象使用相同的材質(zhì),都名為'ground'
即可.
在創(chuàng)建球體和地板之前,創(chuàng)建這兩個(gè)材質(zhì):
const concreteMaterial = new CANNON.Material('concrete')
const plasticMaterial = new CANNON.Material('plastic')
現(xiàn)在我們有了Material,我們必須創(chuàng)建一個(gè)ContactMaterial。它是兩種材質(zhì)的組合,用來關(guān)聯(lián)兩種材料并模擬??碰撞發(fā)生,包含對象發(fā)生碰撞時(shí)的屬性。
前兩個(gè)參數(shù)是Materials。第三個(gè)參數(shù)是一個(gè)**{}**
包含兩個(gè)重要屬性的對象:**friction**
系數(shù)(摩擦系數(shù))和**restitution**
系數(shù)(彈跳系數(shù))——兩者的默認(rèn)值為0.3.
創(chuàng)建后,使用以下方法addContactMaterial(...)
將ContactMaterial添加到世界:
const concretePlasticContactMaterial = new CANNON.ContactMaterial(
concreteMaterial,
plasticMaterial,
{
friction: 0.1,
restitution: 0.7
}
)
world.addContactMaterial(concretePlasticContactMaterial)
混凝土和塑料材質(zhì)之間沒有太大的摩擦力,但是如果你讓一個(gè)橡膠球落在混凝土地板上,你會看到它會反彈的很高。
我們現(xiàn)在可以在身體上使用我們的材質(zhì)。您可以在實(shí)例化Body時(shí)或在material
屬性之后直接傳遞材質(zhì)。我們做這兩件事:
const sphereBody = new CANNON.Body({
// ...
material: plasticMaterial
})
// ...
const floorBody = new CANNON.Body()
floorBody.material = concreteMaterial
在停止之前,您應(yīng)該看到球反彈了很多次。我們看不到friction
動(dòng)作,因?yàn)槲覀兊那蛲耆P直地落在我們的地板上,而且球大部分時(shí)間都在空中。
擁有不同的材料并為每種組合創(chuàng)建一個(gè)接觸材料可能會令人費(fèi)解。為了簡化一切,讓我們將兩種材質(zhì)替換為默認(rèn)材質(zhì),并將其用于每個(gè)Bodies:
const defaultMaterial = new CANNON.Material('default')
const defaultContactMaterial = new CANNON.ContactMaterial(
defaultMaterial,
defaultMaterial,
{
friction: 0.1,
restitution: 0.7
}
)
world.addContactMaterial(defaultContactMaterial)
// ...
const sphereBody = new CANNON.Body({
// ...
material: defaultMaterial
})
// ...
floorBody.material = defaultMaterial
我們應(yīng)該得到相同的結(jié)果。
我們可以更進(jìn)一步,將我們的材質(zhì)設(shè)置為World的默認(rèn)材質(zhì)。為此,只需將defaultContactMaterial
分配給world.defaultContactMaterial
屬性:
world.defaultContactMaterial = defaultContactMaterial
我們現(xiàn)在可以刪除或注釋floorBody
的sphereBody
材料分配。
施力
有很多方法可以對Body施加力:
- applyForce從空間中的指定點(diǎn)(不一定在Body的表面)向Body施加一個(gè)力,就像風(fēng)一直將所有東西推一點(diǎn)點(diǎn),可以是多米諾骨牌穿導(dǎo)的推力,可以是更大爆發(fā)力讓憤怒的小鳥飛向敵人的城堡。
- applyImpulse與applyForce類似,但它不是自增導(dǎo)致速度變化的力,而是直接應(yīng)用于速度。
- applyLocalForce與applyForce相同,但坐標(biāo)是Body的內(nèi)部中心坐標(biāo)(意味著它將是Body0, 0, 0的中心)。
- applyLocalImpulse與applyImpulse相同,但坐標(biāo)是Body的內(nèi)部中心坐標(biāo)。
因?yàn)橛谩傲Α钡姆绞綍斐伤俣鹊淖兓?,我們還是不要用“沖量”的方式
讓我們在開始時(shí)applyLocalForce(...)
對我們的sphereBody
施加一個(gè)小的力推動(dòng)它:
sphereBody.applyLocalForce(new CANNON.Vec3(150, 0, 0), new CANNON.Vec3(0, 0, 0))
您可以看到球向右彈跳并滾動(dòng)。
現(xiàn)在讓我們使用applyForce(...)
施加一些風(fēng)的感覺。因?yàn)轱L(fēng)是永久性的,所以我們應(yīng)該在更新World之前將此力應(yīng)用于每一幀。要正確施加此力,重點(diǎn)應(yīng)該是**sphereBody.position**
:
const tick = () =>
{
// ...
// Update physics
sphereBody.applyForce(new CANNON.Vec3(- 0.5, 0, 0), sphereBody.position)
world.step(1 / 60, deltaTime, 3)
// ...
}
處理多個(gè)對象
處理一兩個(gè)對象很容易,但管理幾十個(gè)對象可能會很麻煩。我們需要稍微優(yōu)化一下讓多個(gè)對象也能同時(shí)管理。
首先,刪除或注釋sphere
、 sphereShape
和 sphereBody
的相關(guān)代碼。
自動(dòng)化功能
首先,讓我們改進(jìn)我們創(chuàng)建球體的方式,該函數(shù)將同時(shí)包括 Three.js 和 Cannon.js 創(chuàng)建球體的方法。
作為此函數(shù)的參數(shù),我們向函數(shù)傳遞radius
和position
兩個(gè)值,但你也可以再隨意添加其他參數(shù),例如mass
、material
、subdivisions
等。
/**
* Utils
*/
const createSphere = (radius, position) =>
{
}
現(xiàn)在我們可以創(chuàng)建:
Three.js網(wǎng)格:
const createSphere = (radius, position) =>
{
// Three.js mesh
const mesh = new THREE.Mesh(
new THREE.SphereGeometry(radius, 20, 20),
new THREE.MeshStandardMaterial({
metalness: 0.3,
roughness: 0.4,
envMap: environmentMapTexture,
envMapIntensity: 0.5
})
)
mesh.castShadow = true
mesh.position.copy(position)
scene.add(mesh)
}
和 Cannon.js主體:
const createSphere = (radius, position) =>
{
// ...
// 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)
}
我們可以刪除之前創(chuàng)建的球體并調(diào)用createSphere(...)
(在創(chuàng)建 Cannon.js 世界和 Three.js 場景之后)。不要忘記刪除tick()
函數(shù)中的球體更新代碼:
createSphere(0.5, { x: 0, y: 3, z: 0 })
如您所見,位置不必是 Three.js Vector3或 Cannon.js Vec3 兩個(gè)標(biāo)準(zhǔn)中心點(diǎn) ,我們可以簡單地使用具有x``,y
和z
屬性的對象(對我們來說很幸運(yùn))。
您應(yīng)該看到球體漂浮在地板上方,但不幸的是,它不再移動(dòng)了。這是完全正常的,因?yàn)槲覀儎偛抛⑨尰蛘邉h除了將 Cannon.js Body 的position
屬性應(yīng)用于 Three.js Mesh 的 position
屬性的代碼。
使用對象數(shù)組
為了處理這個(gè)問題,我們將創(chuàng)建一個(gè)包含所有需要更新的對象的數(shù)組。然后我們將對象內(nèi)新創(chuàng)建的Mesh和Body添加到該數(shù)組:
const objectsToUpdate = []
const createSphere = (radius, position) =>
{
// ...
// Save in objects to update
objectsToUpdate.push({
mesh: mesh,
body: body
})
}
您可以這樣優(yōu)化,重寫最后一部分(JavaScript 中變量名相同時(shí)無需指定屬性):
objectsToUpdate.push({ mesh, body })
我們現(xiàn)在可以在tick()
函數(shù)內(nèi)循環(huán)遍歷該數(shù)組(在我們更新世界之后)并將每個(gè)數(shù)組body.position
數(shù)值復(fù)制到mesh.position
屬性里:
const tick = () =>
{
// ...
world.step(1 / 60, deltaTime, 3)
for(const object of objectsToUpdate)
{
object.mesh.position.copy(object.body.position)
}
}
球體應(yīng)該再次開始下降。
添加到 Dat.GUI
現(xiàn)在我們可以向我們的 Dat.GUI 添加一個(gè)按鈕createSphere
。問題是使用該gui.add(...)
方法時(shí)第一個(gè)參數(shù)應(yīng)該是一個(gè)對象,第二個(gè)參數(shù)應(yīng)該是一個(gè)屬性名。不幸的是,我們的createSphere
是個(gè)函數(shù),不是一個(gè)對象,而且還需要向它傳遞參數(shù)。這種情況經(jīng)常會發(fā)生。一個(gè)不錯(cuò)的解決方案是我們再創(chuàng)建一個(gè)對象,其唯一目的是將那些丟失的功能作為屬性:
const gui = new dat.GUI()
const debugObject = {}
然后在需要時(shí)向其添加函數(shù)(在createSphere
創(chuàng)建函數(shù)之后):
debugObject.createSphere = () =>
{
createSphere(0.5, { x: 0, y: 3, z: 0 })
}
最后,我們可以將這個(gè)新createSphere
屬性添加到 Dat.GUI
:
gui.add(debugObject, 'createSphere')
如果您單擊新創(chuàng)建的createSphere
按鈕,您應(yīng)該會看到球體相互重疊。這是由于球體在完全相同的位置彈出。讓我們添加一些隨機(jī)性即可防止球體重疊了:
debugObject.createSphere = () =>
{
createSphere(
Math.random() * 0.5,
{
x: (Math.random() - 0.5) * 3,
y: 3,
z: (Math.random() - 0.5) * 3
}
)
}
像下雨了一樣!
為了盡量不要燒毀你的電腦;此代碼需要優(yōu)化。
優(yōu)化
因?yàn)門hree.js Mesh的幾何體和材質(zhì)是一樣的,我們應(yīng)該把它們從createSphere
函數(shù)中提取出來。問題是我們正在根據(jù)半徑radius
來創(chuàng)建我們的幾何體。一個(gè)簡單的解決方案是將SphereGeometry的半徑radius
固定為1
然后縮放Mesh:
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20)
const sphereMaterial = new THREE.MeshStandardMaterial({
metalness: 0.3,
roughness: 0.4,
envMap: environmentMapTexture,
envMapIntensity: 0.5
})
const createSphere = (radius, position) =>
{
// Three.js mesh
const mesh = new THREE.Mesh(sphereGeometry, sphereMaterial)
mesh.castShadow = true
mesh.scale.set(radius, radius, radius)
mesh.position.copy(position)
scene.add(mesh)
// ...
}
這樣材質(zhì)都是相同的,你應(yīng)該得到和之前相同的結(jié)果并且大大優(yōu)化了性能。
添加立方體
現(xiàn)在我們的球體運(yùn)行良好,讓我們用立方體也進(jìn)行一次相同的實(shí)現(xiàn)過程。
要?jiǎng)?chuàng)建一個(gè)立方體,我們必須使用一個(gè)BoxGeometry和一個(gè)Box形狀。當(dāng)心; 參數(shù)不一樣。BoxGeometry需要一個(gè)width
、一個(gè)height
和一個(gè)depth
。與此同時(shí),一個(gè)Box形狀需要一個(gè)halfExtents
. 它由Vec3 表示,該 Vec3對應(yīng)于從框的中心開始并連接該框角之一的段:
// Create box
const boxGeometry = new THREE.BoxGeometry(1, 1, 1)
const boxMaterial = new THREE.MeshStandardMaterial({
metalness: 0.3,
roughness: 0.4,
envMap: environmentMapTexture,
envMapIntensity: 0.5
})
const createBox = (width, height, depth, position) =>
{
// Three.js mesh
const mesh = new THREE.Mesh(boxGeometry, boxMaterial)
mesh.scale.set(width, height, depth)
mesh.castShadow = true
mesh.position.copy(position)https://www.yuque.com/channel1/wvnr6v/dtdmh6s06vgyxn6p/edit#kKBqS
scene.add(mesh)
// Cannon.js body
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)
// Save in objects
objectsToUpdate.push({ mesh, body })
}
createBox(1, 1.5, 2, { x: 0, y: 3, z: 0 })
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')
不要忘記刪除第一個(gè)createSphere(...)
調(diào)用,否則您將同時(shí)在同一位置創(chuàng)建球體和長方體,這可能會變得混亂。
如果你點(diǎn)擊Dat.GUI 的createBox
按鈕, 您應(yīng)該會看到一個(gè)盒子掉落并突然彈跳平移向地板。它看起來不太正常。
我們忘記了一件重要的事情:我們的網(wǎng)格沒有旋轉(zhuǎn)。這里發(fā)生的事情應(yīng)該是盒子在地板上彈跳起來并倒向一邊。但我們所能看到的只是盒子跳起來并一直立著移動(dòng)了起來(很怪異),因?yàn)?Three.js網(wǎng)格不像 Cannon.js主體那樣可以進(jìn)行旋轉(zhuǎn),所以立方體就一直立著運(yùn)動(dòng)了。
我們之前沒有看到這個(gè)問題,因?yàn)槲覀兪褂玫氖乔蝮w,無論我們是否旋轉(zhuǎn)它們,它們落地后的物理表現(xiàn)看起來都一樣(其實(shí)他們都不會旋轉(zhuǎn),這不正確)。
我們可以通過將Body quaternion
復(fù)制到Mesh quaternion
來解決這個(gè)問題,就像我們復(fù)制position
時(shí)一樣:
const tick = () =>
{
// ...
for(const object of objectsToUpdate)
{
object.mesh.position.copy(object.body.position)
object.mesh.quaternion.copy(object.body.quaternion)
}
// ...
}
箱子現(xiàn)在應(yīng)該會在落地后倒下了!您可以根據(jù)需要?jiǎng)?chuàng)建球體和盒子。一如既往,盡量不要燒毀你的電腦顯卡。
性能 Performance
廣相 broadphase
在測試對象之間的碰撞時(shí),一種天真的方法是測試每個(gè)Body與其他每個(gè)Body 的對比。雖然這很容易做到,但在性能方面代價(jià)高昂。
這就是 broadphase 出現(xiàn)的地方。broadphase在測試之前對身體進(jìn)行粗略的分類。想象一下,兩堆箱子彼此遠(yuǎn)離。你為什么要用一堆的盒子和另一堆的盒子進(jìn)行測試?它們相距太遠(yuǎn),不會發(fā)生碰撞。
Cannon.js 中有 3 種 broadphase 算法可用:
- NaiveBroadphase : 測試每一個(gè)身體對抗每一個(gè)其他身體
- GridBroadphase : Quadrilles the world 并且僅在同一個(gè)網(wǎng)格框或鄰居的網(wǎng)格框中針對其他主體測試主體。
- SAPBroadphase(broadphase 掃描和修剪 ):在多個(gè)步驟中測試任意軸上的主體。
broadphase 默認(rèn)的算法 是NaiveBroadphase,我建議你切換到SAPBroadphase。使用這個(gè) broadphase 可能會產(chǎn)生物體不發(fā)生碰撞的錯(cuò)誤行為,但這種情況很少見,并且它涉及到做一些事情,比如非常快速地移動(dòng)物體時(shí)導(dǎo)致不發(fā)生碰撞。
要切換到SAPBroadphase,只需在屬性中對其進(jìn)行實(shí)例化world.broadphase
,并使用相同的世界作為參數(shù):
world.broadphase = new CANNON.SAPBroadphase(world)
休眠 Sleep
就算我們使用改進(jìn)的 broadphase 算法,我們所有的身體還是都會被物理測試?yán)速M(fèi)了性能。那些不再移動(dòng)的身體,我們可以使用sleep
稱為睡眠的功能。
當(dāng)Body速度變得非常慢時(shí)(在您看不到它移動(dòng)的點(diǎn)),Body可能會休眠并且不會被測試,除非通過代碼對其施加足夠的力或者如果另一個(gè)Body擊中它。
要激活此功能,只需將[World](http://schteppe.github.io/cannon.js/docs/classes/World.html).allowSleep
屬性設(shè)置為true :
world.allowSleep = true
您還可以使用sleepSpeedLimit
和sleepTimeLimit
屬性控制Body入睡的范圍,但我們這節(jié)課不會更改這些。
事件
您可以在Body上收聽事件。如果你想做一些事情,比如在物體碰撞時(shí)播放聲音,或者如果你想知道子彈發(fā)射是否碰到了敵人,這會很有用。
您可以收聽Body上的事件,例如'colide'
,'sleep'
或'wakeup'
。
當(dāng)我們的球體和盒子與任何物體發(fā)生碰撞時(shí),讓我們播放撞擊聲。首先,在原生 JavaScript 中創(chuàng)建聲音并創(chuàng)建一個(gè)播放聲音的函數(shù)。
某些瀏覽器(如 Chrome)會阻止播放聲音,除非用戶與頁面進(jìn)行了交互(例如單擊任何地方),因此如果您沒有聽到第一個(gè)聲音,請不要擔(dān)心。
/**
* Sounds
*/
const hitSound = new Audio('/sounds/hit.mp3')
const playHitSound = () =>
{
hitSound.play()
}
只是播放聲音有點(diǎn)牽強(qiáng),但我們稍后會為該功能添加更多內(nèi)容。
現(xiàn)在,讓我們來聽聽'collide'
關(guān)于Bodies的事件。我們將只關(guān)注createBox
函數(shù),并在完成后將其添加到createSphere
函數(shù)中。
現(xiàn)在,監(jiān)聽碰撞事件并使用該playHitSound
函數(shù)作為回調(diào):
const createBox = (width, height, depth, position) =>
{
// ...
body.addEventListener('collide', playHitSound)
// ...
}
當(dāng)立方體接觸地面或立方體碰撞時(shí),您應(yīng)該會聽到撞擊聲。如果您使用的是 Chrome,請不要忘記在框落地之前點(diǎn)擊頁面,因?yàn)槿绻形窗l(fā)生用戶交互,Chrome 會拒絕播放聲音。
聲音似乎還不錯(cuò)。不幸的是,當(dāng)我們添加多個(gè)框時(shí),事情變得非常奇怪,那個(gè)聲音像是犯病了一樣一直噠噠噠噠。
第一個(gè)問題是,當(dāng)我們在調(diào)用hitSound.play()
播放聲音時(shí),沒有任何反應(yīng),因?yàn)樗呀?jīng)在播放了。我們可以通過將聲音currentTime
重置為屬性來解決這個(gè)0問題:
const playHitSound = () =>
{
hitSound.currentTime = 0
hitSound.play()
}
雖然這在物體掉落開始時(shí)比較好,但即使一個(gè)立方體輕微接觸另一個(gè)立方體,我們也會聽到太多的撞擊聲。我們需要知道影響力有多強(qiáng),如果不夠強(qiáng),我們就什么都不播放才行。
要獲得沖擊強(qiáng)度,我們首先需要獲得有關(guān)碰撞的信息。我們可以通過向'collide'
回調(diào)(這是我們的playHitSound函數(shù))添加一個(gè)參數(shù)來做到這一點(diǎn):
const playHitSound = (collision) =>
{
console.log(collision)
// ...
}
該collision
變量現(xiàn)在包含大量碰撞信息??梢酝ㄟ^調(diào)用屬性getImpactVelocityAlongNormal()
上的方法來找到?jīng)_擊強(qiáng)度contact
:
const playHitSound = (collision) =>
{
console.log(collision.contact.getImpactVelocityAlongNormal())
// ...
}
如果您查看日志,您應(yīng)該會看到一個(gè)數(shù)字。沖擊力越強(qiáng),數(shù)值越高。
我們測試impactStrength
該值并僅在足夠強(qiáng)的情況下播放聲音:
const playHitSound = (collision) =>
{
const impactStrength = collision.contact.getImpactVelocityAlongNormal()
if(impactStrength > 1.5)
{
hitSound.currentTime = 0
hitSound.play()
}
}
為了更加真實(shí),我們可以為音量添加一些隨機(jī)性:
const playHitSound = (collision) =>
{
const impactStrength = collision.contact.getImpactVelocityAlongNormal()
if(impactStrength > 1.5)
{
hitSound.volume = Math.random()
hitSound.currentTime = 0
hitSound.play()
}
}
如果我們想更加完善這個(gè)功能,我們可以有多個(gè)略有不同的擊打聲音。為了防止同時(shí)播放太多聲音,我們可以添加一個(gè)非常短的延遲,使聲音在播放一次后無法再次播放。
我們不會在本課中做這些,但請隨意嘗試。
讓我們將createBox
函數(shù)中使用的代碼復(fù)制到createSphere
函數(shù)中:
const createSphere = (radius, position) =>
{
// ...
body.addEventListener('collide', playHitSound)
// ...
}
移除物體 Remove things
讓我們添加一個(gè)reset
按鈕。
創(chuàng)建一個(gè)reset
函數(shù)并將其添加到您的 Dat.GUI 中,就像我們對createBox
和 createSphere
所做的那樣:
// Reset
debugObject.reset = () =>
{
console.log('reset')
}
gui.add(debugObject, 'reset')
現(xiàn)在,讓我們循環(huán)遍歷objectsToUpdate
數(shù)組中的每個(gè)對象。然后從object.body
中刪除world
和從 object.mesh
中刪除 scene
。另外,不要忘記像在本機(jī) JavaScript 中那樣刪除 eventListener
:
debugObject.reset = () =>
{
for(const object of objectsToUpdate)
{
// Remove body
object.body.removeEventListener('collide', playHitSound)
world.removeBody(object.body)
// Remove mesh
scene.remove(object.mesh)
}
}
我們還需要清空objectsToUpdate
數(shù)組。在 JS 中有許多清空數(shù)組的神奇方法,其中之一是用splice
方法將其內(nèi)容替換為空:
debugObject.reset = () =>
{
// ...
objectsToUpdate.splice(0, objectsToUpdate.length)
}
就是這樣。您可以單擊reset
按鈕刪除所有內(nèi)容。
使用 Cannon.js 走得更遠(yuǎn)
雖然我們介紹了基礎(chǔ)知識并且您已經(jīng)可以做很多事情,但這里有一些需要改進(jìn)的地方。
約束條件
顧名思義,約束可以在兩個(gè)主體之間啟用約束。我們不會在本課中介紹這些內(nèi)容,但這是約束列表:
- HingeConstraint:就像門鉸鏈一樣。
- DistanceConstraint:強(qiáng)制物體彼此保持一定距離。
- LockConstraint:合并實(shí)體,就像它們是一件一樣。
- PointToPointConstraint:將主體粘附到特定點(diǎn)。
類、方法、屬性和事件
有許多類,每個(gè)類都有不同的方法、屬性和事件。嘗試至少瀏覽一次所有這些以了解它們的存在。它可能會為您在未來的項(xiàng)目中節(jié)省一些時(shí)間。
例子
文檔并不完美。如果您花一些時(shí)間在演示和研究中以了解如何開發(fā),將會有所幫助。許多人可能遇到了您可能遇到的問題。不要猶豫,依靠社區(qū)。
多線程workers
運(yùn)行物理模擬需要時(shí)間。執(zhí)行這項(xiàng)工作的計(jì)算機(jī)組件是 CPU。當(dāng)你運(yùn)行 Three.js、Cannon.js、你的代碼邏輯等時(shí),一切都由你 CPU 中的同一個(gè)線程完成。如果有太多事情要做(例如物理模擬中的對象太多),該線程會很快過載,從而導(dǎo)致幀速率下降。
正確的解決方案是使用多線程。Workers 允許您將一部分代碼放在不同的線程中以分散負(fù)載。然后您可以從該代碼發(fā)送和接收數(shù)據(jù)。它可以顯著提高性能。
問題是代碼必須明顯分開防止競爭資源。您可以在頁面源代碼中找到一個(gè)很好的簡單示例。
Cannon-es
正如我們之前所說,Cannon.js 多年未更新。幸運(yùn)的是,有些人 fork 了存儲庫并開始進(jìn)行更新。多虧了他們,我們才能訪問更好且維護(hù)得更好的 Cannon.js 版本:
- Git 存儲庫: https: //github.com/pmndrs/cannon-es
- NPM 頁面:https://www.npmjs.com/package/cannon-es
要使用此版本而不是原始版本,請?jiān)陧?xiàng)目文件夾中打開終端(或關(guān)閉服務(wù)器),刪除之前的 cannon.js 依賴項(xiàng)npm uninstall --save cannon
。
至于cannon-es
,您可以安裝最新版本并npm install --save cannon-es
更改您在代碼中導(dǎo)入 Cannon.js 的方式:
import * as CANNON from 'cannon-es'
一切都應(yīng)該像以前一樣工作。您可以在Git 存儲庫頁面上查看版本改動(dòng)。
最新版本應(yīng)該可以作為直接替代品,但如果出現(xiàn)錯(cuò)誤,您可以使用更具體的版本,如(0.20測試過的)通過運(yùn)行npm install --save cannon-es@0.20
.
Ammo.js
我們使用 Cannon.js 是因?yàn)樵搸煲子趯?shí)施和理解。它最大的競爭對手之一是 Ammo.js。雖然在您的項(xiàng)目中更難使用和實(shí)施,但您可能會對以下功能感興趣:
- 它是 Bullet 的一個(gè)移植版,Bullet 是一個(gè)眾所周知且運(yùn)行良好的物理引擎,用 C++ 編寫。
- 它具有 WebAssembly (wasm) 支持。WebAssembly 是大多數(shù)最新瀏覽器都支持的低級語言。因?yàn)樗堑图墑e的,所以它具有更好的性能。
- 它更受歡迎,您可以找到更多 Three.js 的示例。
- 它支持更多功能。
如果您需要最佳性能或在您的項(xiàng)目中具有特定功能,您可能應(yīng)該選擇 Ammo.js 而不是 Cannon.js。
Physijs
Physijs 簡化了 Three.js 項(xiàng)目中物理的實(shí)現(xiàn)。它使用 Ammo.js 并原生支持 workers。
- 網(wǎng)站: https: //chandlerprall.github.io/Physijs/
- Git 存儲庫:https://github.com/chandlerprall/Physijs
- 文檔: https: //github.com/chandlerprall/Physijs/wiki
您無需創(chuàng)建 Three.js 對象和物理對象,而是同時(shí)創(chuàng)建兩者即可:文章來源:http://www.zghlxwxcb.cn/news/detail-699266.html
box = new Physijs.BoxMesh(
new THREE.CubeGeometry(5, 5, 5),
new THREE.MeshBasicMaterial({ color: 0x888888 })
)
scene.add(box)
Physijs 會處理剩下的事情。
雖然它很吸引人,尤其是對于初學(xué)者來說,但當(dāng)您嘗試做該庫不支持的事情時(shí),事情就會變得復(fù)雜。查找錯(cuò)誤的來源也可能很麻煩,因?yàn)榉庋b過頭了。
就像 Ammo.js 一樣,花點(diǎn)時(shí)間想想用哪個(gè)物理庫是您項(xiàng)目的最佳解決方案。文章來源地址http://www.zghlxwxcb.cn/news/detail-699266.html
到了這里,關(guān)于【Three.js】第二十一章 Physics 物理的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!