造個海洋球池來學習物理引擎【Three.js系列】

2022-05-24 12:00:29

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 繫結,讓小球擁有物理的特性。

主要得步驟為

  • 定義小球
  • 引入物理引擎
  • 將 Three.js 和 物理引擎結合
  • 生成隨機球
  • 定義牆

好了,以上就是本章的全部內容了,下一個篇章再見。

github地址:https://github.com/hua1995116/Fly-Three.js

系列連載首發地址:

  1. Three.js系列: 造個海洋球池來學習物理引擎
  2. Three.js系列: 遊戲中的第一、三人稱視角