github地址:https://github.com/hua1995116/Fly-Three.js
大家好,我是秋風。繼上一篇《Three.js系列: 遊戲中的第一/三人稱視角》今天想要和大家分享的呢,是做一個海洋球池。
海洋球大家都見過吧?就是商場裡非常受小孩子們青睞的小球,自己看了也想往裡蹦躂的那種。
就想著做一個海洋球池,然後順便帶大家來學習學習 Three.js 中的物理引擎。
那麼讓我們開始吧,要實現一個海洋球池,那麼首先肯定得有「球」吧。
因此先帶大家來實現一個小球,而恰恰在 Three.js 中定義一個小球非常的簡單。因為 Three.js 給我們提供非常豐富幾何形狀 API ,大概有十幾種吧。
提供的幾何形狀恰巧有我們需要的球形, 球形的 API 叫 SphereGeometry。
SphereGeometry(radius : Float, widthSegments : Integer, heightSegments : Integer, phiStart : Float, phiLength : Float, thetaStart : Float, thetaLength : Float)
這個API 一共有 7 個引數,但是呢,我們需要用到就只有前3個引數,後面的暫時不需要管。
Radius 的意思很簡單,就是半徑,說白了就是設定小球的大小,首先我們設定小球的大小,設定為 0.5,然後其次就是 widthSegments 和 heightSegments ,這倆值越大,球的稜角就越少,看起來就越細膩,但是精細帶來的後果就是效能消耗越大,widthSegments 預設值為32,heightSegments預設值為 16 ,我們可以設定 20, 20
const sphereGeometry = new THREE.SphereGeometry(0.5, 20, 20);
這非常的簡單,雖然小球有了形狀,我們還得給小球設定上材質,材質就是類似我們現實生活中的材料,不是是隻要是球形的就叫一個東西,比如有玻璃材質的彈珠,有橡膠材質的網球等等,不同的材質會與光的反射不一樣,看起來的樣子也不一樣。在 Three.js 中我們就設定一個標準物理材質 MeshStandardMaterial
,它可以設定金屬度和粗糙度,會對光照形成反射,然後把球的顏色設定成紅色,
const sphereMaterial = new THREE.MeshStandardMaterial({
color: '#ff0000'
});
const mesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
scene.add(mesh);
然後我們將它新增到我們的場景中,emmm,看起來黑乎乎的一片。
「上帝說要有光,於是就有了光」,黑乎乎是正常的,因為在我們場景中沒有燈光,這個意思很簡單,當夜晚的時候,關了燈當然是伸手不見五指。於是我們在場景中加入兩盞燈,一個環境燈,一個直射燈,燈光在本篇文章中不是重點,所以就不會展開描述。只要記住,」天黑了,要開燈」
// 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)
嗯!現在這個球終於展現出它的樣子了。
一個靜態的還海洋球肯定沒有什麼意思,我們需要讓它動起來,因此我們需要給它新增物理引擎。有了物理引擎之後小球就會像現實生活中的樣子,有重力,在高空的時候它會做自由落地運動,不同材質的物體落地的時候會有不同的反應,網球落地會彈起再下落,鉛球落地則是靜止的。
常用的 3d 物理引擎有Physijs 、Ammo.js 、Cannon.js 和 Oimo.js 等等。這裡我們用到的則是 Cannon.js
在 Cannon.js 官網有很多關於 3d 物理的效果,詳細可以看他的官網 https://pmndrs.github.io/cannon-es/
引入 Cannon.js
import * as CANNON from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/cannon-es.js';
首先先建立一個物理的世界,並且設定重力系數 9.8
const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);
在物理世界中建立一個和我們 Three.js 中一一對應的小球,唯一不一樣的就是需要設定 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);
然後我們再修改一下我們的渲染邏輯,我們需要讓每一幀的渲染和物理世界對應。
+ 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();
但是發現我們的小球並沒有動靜,原因是我們沒有繫結物理世界中和 Three.js 小球的關係。
const tick = () => {
...
+ mesh.position.copy(body.position);
...
}
來看看現在的樣子。
小球已經有了物理的特性,在做自由落體了~ 但是由於沒有地面,小球落向了無盡的深淵,我們需要設定一個地板來讓小球落在一個平面上。
建立 Three.js 中的地面, 這裡主要用到的是 PlaneGeometry
它有4個引數
PlaneGeometry(width : Float, height : Float, widthSegments : Integer, heightSegments : Integer)
和之前類似我們只需要關注前 2 個引數,就是平面的寬和高,由於平面預設是 x-y 軸的平面,由於Three.js 預設用的是右手座標系,對應的旋轉也是右手法則,所以逆時針為正值,順時針為負值,而我們的平面需要向順時針旋轉 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);
然後繼續繫結平面的物理引擎,寫法基本和 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);
來看看效果:
但是這個效果彷彿是一個鉛球落地的效果,沒有任何回彈以及其他的效果。為了讓小球不像鉛球一樣直接落在地面上,我們需要給小球增加彈性係數。
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,
});
...
檢視效果:
海洋球池當然不能只有一個球,我們需要有很多很多球,接下來我們再來實現多個小球的情況,為了生成多個小球,我們需要寫一個隨機小球生成器。
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,
});
};
以上只是對我們之前寫的程式碼做了一個函數封裝,並且讓小球的顏色隨機,我們暴露出小球的位置以及小球的大小兩個引數。
最後我們需要修改一下更新的邏輯,因為我們需要在每一幀修改每個小球的位置資訊。
const tick = () => {
...
for (const object of objectsToUpdate) {
object.mesh.position.copy(object.body.position);
object.mesh.quaternion.copy(object.body.quaternion);
}
...
}
緊接著我們再來寫一個點選事件,點選螢幕的時候能生成 100 個海洋球。
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);
檢視下效果:
初步的效果已經實現了,由於我們的池子只有底部一個平面,沒有設定任何牆,所以小球就四處散開了。所以大家很容易地想到,我們需要建設4面牆,由於牆和底部平面有的區別就是有厚度,它不是一個單純的面,因此我們需要用到新的形狀 —— BoxGeometry
, 它一共也有7個引數,但是我們也只需要關注前3個,對應的就是長寬高。
BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer)
現在我們來建立一堵 長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)
現在它長成了這個樣子:
接著我們」依葫蘆畫瓢「完成剩下3面牆:
Untitled
然後我們也給我們的牆新增上物理引擎,讓小球觸控到的時候,彷彿是真的碰到了牆,而不是穿透牆。
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
檢視效果
收穫滿滿一盆海洋球
大功告成!
來總結一下我們本期學習的內容,一共用到 SphereGeometry、PlaneGeometry、 BoxGeometry,然後學習了 Three.js 幾何體 與 物理引擎 cannon.js 繫結,讓小球擁有物理的特性。
主要得步驟為
好了,以上就是本章的全部內容了,下一個篇章再見。
github地址:https://github.com/hua1995116/Fly-Three.js
系列連載首發地址: