不是吧,在網(wǎng)頁(yè)上造個(gè)海洋球池?
大家好,我是魚皮,今天想要和大家分享一篇使用Three.js 造一個(gè)海洋球池的文章,內(nèi)容還是很有趣的,希望小伙伴們能喜歡。
海洋球大家都見(jiàn)過(guò)吧?就是商場(chǎng)里非常受小孩子們青睞的小球,自己看了也想往里蹦跶的那種。

圖源于網(wǎng)絡(luò)
我就想著做一個(gè)海洋球池,然后順便帶大家來(lái)學(xué)習(xí)學(xué)習(xí) Three.js 中的物理引擎。
那么讓我們開(kāi)始吧,要實(shí)現(xiàn)一個(gè)海洋球池,那么首先肯定得有“球”吧。
因此先帶大家來(lái)實(shí)現(xiàn)一個(gè)小球,而恰恰在 Three.js 中定義一個(gè)小球非常的簡(jiǎn)單。因?yàn)?Three.js 給我們提供非常豐富幾何形狀 API ,大概有十幾種吧。

提供的幾何形狀恰巧有我們需要的球形, 球形的 API 叫 SphereGeometry。
SphereGeometry(radius : Float, widthSegments : Integer, heightSegments : Integer, phiStart : Float, phiLength : Float, thetaStart : Float, thetaLength : Float)
這個(gè)API 一共有 7 個(gè)參數(shù),但是呢,我們需要用到就只有前3個(gè)參數(shù),后面的暫時(shí)不需要管。
Radius 的意思很簡(jiǎn)單,就是半徑,說(shuō)白了就是設(shè)置小球的大小,首先我們?cè)O(shè)置小球的大小,設(shè)置為 0.5,然后其次就是 widthSegments 和 heightSegments ,這倆值越大,球的棱角就越少,看起來(lái)就越細(xì)膩,但是精細(xì)帶來(lái)的后果就是性能消耗越大,widthSegments 默認(rèn)值為32,heightSegments默認(rèn)值為 16 ,我們可以設(shè)置 20, 20
const sphereGeometry = new THREE.SphereGeometry(0.5, 20, 20);
這非常的簡(jiǎn)單,雖然小球有了形狀,我們還得給小球設(shè)置上材質(zhì),材質(zhì)就是類似我們現(xiàn)實(shí)生活中的材料,不是是只要是球形的就叫一個(gè)東西,比如有玻璃材質(zhì)的彈珠,有橡膠材質(zhì)的網(wǎng)球等等,不同的材質(zhì)會(huì)與光的反射不一樣,看起來(lái)的樣子也不一樣。在 Three.js 中我們就設(shè)置一個(gè)標(biāo)準(zhǔn)物理材質(zhì) MeshStandardMaterial ,它可以設(shè)置金屬度和粗糙度,會(huì)對(duì)光照形成反射,然后把球的顏色設(shè)置成紅色,
const sphereMaterial = new THREE.MeshStandardMaterial({
color: '#ff0000'
});
const mesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(mesh);
然后我們將它添加到我們的場(chǎng)景中,emmm,看起來(lái)黑乎乎的一片。

“上帝說(shuō)要有光,于是就有了光”,黑乎乎是正常的,因?yàn)樵谖覀儓?chǎng)景中沒(méi)有燈光,這個(gè)意思很簡(jiǎn)單,當(dāng)夜晚的時(shí)候,關(guān)了燈當(dāng)然是伸手不見(jiàn)五指。于是我們?cè)趫?chǎng)景中加入兩盞燈,一個(gè)環(huán)境燈,一個(gè)直射燈,燈光在本篇文章中不是重點(diǎn),所以就不會(huì)展開(kāi)描述。只要記住,”天黑了,要開(kāi)燈”
// Ambient light
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
// Directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5)
directionalLight.position.set(2, 2, -1)
scene.add(directionalLight)
嗯!現(xiàn)在這個(gè)球終于展現(xiàn)出它的樣子了。

一個(gè)靜態(tài)的還海洋球肯定沒(méi)有什么意思,我們需要讓它動(dòng)起來(lái),因此我們需要給它添加物理引擎。有了物理引擎之后小球就會(huì)像現(xiàn)實(shí)生活中的樣子,有重力,在高空的時(shí)候它會(huì)做自由落地運(yùn)動(dòng),不同材質(zhì)的物體落地的時(shí)候會(huì)有不同的反應(yīng),網(wǎng)球落地會(huì)彈起再下落,鉛球落地則是靜止的。
常用的 3d 物理引擎有Physijs 、Ammo.js 、Cannon.js 和 Oimo.js 等等。這里我們用到的則是 Cannon.js
在 Cannon.js 官網(wǎng)有很多關(guān)于 3d 物理的效果,詳細(xì)可以看他的官網(wǎng) https://pmndrs.github.io/cannon-es/

引入 Cannon.js
import * as CANNON from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/cannon-es.js';
首先先創(chuàng)建一個(gè)物理的世界,并且設(shè)置重力系數(shù) 9.8
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
在物理世界中創(chuàng)建一個(gè)和我們 Three.js 中一一對(duì)應(yīng)的小球,唯一不一樣的就是需要設(shè)置 mass,就是小球的重量。
const shape = new CANNON.Sphere(0.5);
const body = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(0, 3, 0),
shape: shape,
});
world.addBody(body);
然后我們?cè)傩薷囊幌挛覀兊匿秩具壿?,我們需要讓每一幀的渲染和物理世界?duì)應(yīng)。
+ 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)
}
tick();
但是發(fā)現(xiàn)我們的小球并沒(méi)有動(dòng)靜,原因是我們沒(méi)有綁定物理世界中和 Three.js 小球的關(guān)系。
const tick = () => {
...
+ mesh.position.copy(body.position);
...
}
來(lái)看看現(xiàn)在的樣子。

小球已經(jīng)有了物理的特性,在做自由落體了~ 但是由于沒(méi)有地面,小球落向了無(wú)盡的深淵,我們需要設(shè)置一個(gè)地板來(lái)讓小球落在一個(gè)平面上。
創(chuàng)建 Three.js 中的地面, 這里主要用到的是 PlaneGeometry 它有4個(gè)參數(shù)
PlaneGeometry(width : Float, height : Float, widthSegments : Integer, heightSegments : Integer)
和之前類似我們只需要關(guān)注前 2 個(gè)參數(shù),就是平面的寬和高,由于平面默認(rèn)是 x-y 軸的平面,由于Three.js 默認(rèn)用的是右手坐標(biāo)系,對(duì)應(yīng)的旋轉(zhuǎn)也是右手法則,所以逆時(shí)針為正值,順時(shí)針為負(fù)值,而我們的平面需要向順時(shí)針旋轉(zhuǎn) 90°,所以是 -PI/2
const planeGeometry = new THREE.PlaneGeometry(20, 20);
const planeMaterial = new THREE.MeshStandardMaterial({
color: '#777777',
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI * 0.5;
scene.add(plane);
然后繼續(xù)綁定平面的物理引擎,寫法基本和 Three.js 差不多,只是 API 名字不一樣
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
floorBody.mass = 0;
floorBody.addShape(floorShape);
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(-1, 0, 0), Math.PI * 0.5);
world.addBody(floorBody);
來(lái)看看效果:

但是這個(gè)效果仿佛是一個(gè)鉛球落地的效果,沒(méi)有任何回彈以及其他的效果。為了讓小球不像鉛球一樣直接落在地面上,我們需要給小球增加彈性系數(shù)。
const defaultMaterial = new CANNON.Material("default");
const defaultContactMaterial = new CANNON.ContactMaterial(
defaultMaterial,
defaultMaterial,
{
restitution: 0.4,
}
);
world.addContactMaterial(defaultContactMaterial);
world.defaultContactMaterial = defaultContactMaterial;
...
const body = new CANNON.Body({
mass: 1,
position: new CANNON.Vec3(0, 3, 0),
shape: shape,
+ material: defaultMaterial,
});
...
查看效果:

海洋球池當(dāng)然不能只有一個(gè)球,我們需要有很多很多球,接下來(lái)我們?cè)賮?lái)實(shí)現(xiàn)多個(gè)小球的情況,為了生成多個(gè)小球,我們需要寫一個(gè)隨機(jī)小球生成器。
const objectsToUpdate = [];
const createSphere = (radius, position) => {
const sphereMaterial = new THREE.MeshStandardMaterial({
metalness: 0.3,
roughness: 0.4,
color: Math.random() * 0xffffff
});
const mesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
mesh.scale.set(radius, radius, radius);
mesh.castShadow = true;
mesh.position.copy(position);
scene.add(mesh);
const shape = new CANNON.Sphere(radius * 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,
});
};
以上只是對(duì)我們之前寫的代碼做了一個(gè)函數(shù)封裝,并且讓小球的顏色隨機(jī),我們暴露出小球的位置以及小球的大小兩個(gè)參數(shù)。
最后我們需要修改一下更新的邏輯,因?yàn)槲覀冃枰诿恳粠薷拿總€(gè)小球的位置信息。
const tick = () => {
...
for (const object of objectsToUpdate) {
object.mesh.position.copy(object.body.position);
object.mesh.quaternion.copy(object.body.quaternion);
}
...
}
緊接著我們?cè)賮?lái)寫一個(gè)點(diǎn)擊事件,點(diǎn)擊屏幕的時(shí)候能生成 100 個(gè)海洋球。
window.addEventListener('click', () => {
for (let i = 0; i < 100; i++) {
createSphere(1, {
x: (Math.random() - 0.5) * 10,
y: 10,
z: (Math.random() - 0.5) * 10,
});
}
}, false);
查看下效果:

初步的效果已經(jīng)實(shí)現(xiàn)了,由于我們的池子只有底部一個(gè)平面,沒(méi)有設(shè)置任何墻,所以小球就四處散開(kāi)了。所以大家很容易地想到,我們需要建設(shè)4面墻,由于墻和底部平面有的區(qū)別就是有厚度,它不是一個(gè)單純的面,因此我們需要用到新的形狀 —— BoxGeometry , 它一共也有7個(gè)參數(shù),但是我們也只需要關(guān)注前3個(gè),對(duì)應(yīng)的就是長(zhǎng)寬高。
BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer)
現(xiàn)在我們來(lái)建立一堵 長(zhǎng)20, 寬 5, 厚度為 0.1 墻。
const box = new THREE.BoxGeometry(20, 5, 0.1);
const boxMaterial = new THREE.MeshStandardMaterial({
color: '#777777',
metalness: 0.3,
roughness: 0.4,
});
const box = new THREE.Mesh(box, boxMaterial);
box.position.set(0, 2.5, -10);
scene.add(box)
現(xiàn)在它長(zhǎng)成了這個(gè)樣子:

接著我們”依葫蘆畫瓢“完成剩下3面墻:

然后我們也給我們的墻添加上物理引擎,讓小球觸摸到的時(shí)候,仿佛是真的碰到了墻,而不是穿透墻。
const halfExtents = new CANNON.Vec3(20, 5, 0.1)
const boxShape = new CANNON.Box(halfExtents)
const boxBody1 = new CANNON.Body({
mass: 0,
material: defaultMaterial,
shape: boxShape,
})
boxBody1.position.set(0, 2.5, -10);
world.addBody(boxBody1);
...
boxBody2
boxBody3
boxBody4
查看效果

收獲滿滿一盆海洋球

大功告成!
來(lái)總結(jié)一下我們本期學(xué)習(xí)的內(nèi)容,一共用到 SphereGeometry、PlaneGeometry、 BoxGeometry,然后學(xué)習(xí)了 Three.js 幾何體 與 物理引擎 cannon.js 綁定,讓小球擁有物理的特性。
主要得步驟為
定義小球 引入物理引擎 將 Three.js 和 物理引擎結(jié)合 生成隨機(jī)球 定義墻
以上就是本期分享了。
最后,歡迎加入 魚皮的編程知識(shí)星球(點(diǎn)擊了解詳情),和 8300 多名小伙伴們一起交流學(xué)習(xí),向魚皮和大廠同學(xué) 1 對(duì) 1 提問(wèn)、幫你制定學(xué)習(xí)計(jì)劃不迷茫、跟著魚皮直播做項(xiàng)目(往期項(xiàng)目可無(wú)限回看)、領(lǐng)取魚皮原創(chuàng)編程學(xué)習(xí)/求職資料等。

往期推薦
