一隻腳踏入 Three.js

前言

正所謂:無折騰,不前端。不搞 WebGL,和鹹魚有啥區別!javascript

用官方的說法:Three.js - Javascript 3D library。css

咱們今天就來一塊兒熟悉一下 Three.js 的設計理念與思想。html

笛卡爾右手座標系

在作 3D,咱們首先得要了解其基本準則:三維座標系。前端

咱們都知道在 CSS3 的三維空間中是左手座標系。(若是不瞭解的能夠閱讀我以前寫的一篇文章《CSS3 之 3D 變換》java

可是在 Three.js 中,咱們的空間是基於右手笛卡爾座標系的而展示的。以下:git

瞭解了座標系以後,咱們就能在這片三維空間中建立咱們想要的場景了。github

建立場景

想要使用三維空間,首先就必須開闢一個三維空間這一容器。而開闢一個三維空間只須要實例化 THREE.Scene 這一對象就能夠了。canvas

var scene = new THREE.Scene();
複製代碼

場景是你能夠放置物體、相機和燈光的三維空間,如同宇宙通常,沒有邊界,也沒有光亮,有的是無盡的黑暗。設計模式

一個場景中的組件能夠的大體分爲三類:攝像機、光源、對象。數組

咱們在瞭解 Thee.js 中的組件以前,先看一張照片:

這是一張拍攝商品的工做室照片。這張照片就基本能夠說明咱們 Three.js 的 3D 設計模式:咱們在有了一個空間以後,咱們須要將咱們是拍攝對象放進去。有了對象以後咱們還須要設置至少一個光源,這樣咱們才能看到咱們的拍攝對象。最後,咱們呈如今客戶眼前的是一系列由相機拍攝出的照片連續播放產生的動畫,相機的參數、位置和角度直接影響着咱們所拍到的圖片。

拍攝對象

在使用拍攝對象以前咱們先說明一下用 Three.js 建立拍攝對象的設計模式:

首先 Three.js 將任何拍攝對象解構爲一個個小三角形。不管是二維圖形仍是三維圖形,均可以用三角形做爲結構最小單位。而結構出來的就是咱們拍攝對象的一個網格。

以下呈現的是二維平面的網格結構:

以下展現的是三維球體網格結構:

能夠看到在 Three.js 中三角形是最小分割單位。這就是網格結構。

固然有網格結構仍是不夠的。就像人體同樣,由於網格結構就像是骨架,在其外表還須要材質。材質就是物體的皮膚,決定着幾何體的外表。

幾何體模型(Geometry)

在 Three.js 中,爲咱們預設了不少幾何體的網格結構:

  • 二維:

    • THREE.PlaneGeometry(平面)

      這個幾何體在前文已經展現過了。

    • THREE.CircleGeometry(圓)

    • THREE.RingGeometry(環)

  • 三維

    • THREE.BoxGeometry(長方體)

    • THREE.SphereGeometry(球體)

      這個幾何體在前文已經展現過了。

    • THREE.CylinderGeometry(圓柱體)

    • THREE.Torus(圓環)

以上所舉的只是內置幾何體的一部分。咱們在使用這些集合體的時候,咱們只須要實例化相應幾何體對象便可。

具體咱們以實例化一個正方體爲例:

var geometry = new THREE.BoxGeometry(4, 4, 4);
複製代碼

這裏咱們先聲明而且實例化了一個 BoxGeometry(長方體)對象。在建立對象的時候咱們分別設置了長、寬、高各爲 4。

這樣一個正方體就建立好了。可是有了這麼一個網格框架是遠遠不夠的。下一步就是給他添加材質。

材質(Material)

在材質組件中,Three.js 也爲咱們預設了幾種材質對象,咱們這裏簡單的介紹兩種最經常使用的:

  1. MeshBasicMaterial

    這一材質,是 Three.js 的基礎材質。用於給幾何體網格賦予一種簡單的顏色或是顯示幾何體的網格結構。(即使在沒有光源的狀況下也能夠顯示。)

  2. MeshLambertMaterial

    這是一種考慮光照影響的材質。用於建立暗淡的,不光亮的物體。

值得注意的是,在同一個網格結構中咱們能夠多種材質進行疊加。

這裏咱們前後使用 MeshBasicMaterial 和 MeshLambertMaterial 爲咱們前文所創造的正方體準備兩個不一樣的材質:

var geometryMeshBasicMaterial = new THREE.MeshBasicMaterial({
  color: 0xff0000,
  wireframe: true
});
var geometryMeshLambertMaterial = new THREE.MeshLambertMaterial({
  color: 0x242424
});
複製代碼

其中 wireframe 屬性當設置爲 true 的時候,將會將材質渲染爲線寬。相框能夠變相的理解爲網格線。好比說一個正方體的線框以下:

網格(Mesh)

當咱們擁有了幾何體網格模型和材質以後咱們就須要將二者結合起來建立咱們正在的拍攝對象。

這裏咱們介紹兩個不一樣的拍攝對象構造方法:

  • new THREE.Mesh(geometry, material)
  • THREE.SceneUtils.createMultiMaterialObject(geometry,[materials...])

這兩種都是建立拍攝對象的方法,且第一個參數都是幾何體模型(Geometry),惟一不一樣在於第二個參數。前者只能用一種材質建立拍攝對象,後者可使用多種材質進行建立(傳入一個包含多種材質的數組)。

這裏咱們將建立一個多材質拍攝對象。

var cube = THREE.SceneUtils.createMultiMaterialObject(geometry, [
  geometryMeshBasicMaterial,
  geometryMeshLambertMaterial
]);
複製代碼

如今咱們已經有一個拍攝對象了,這時候咱們須要將咱們的對象添加到場景中,就像咱們在拍攝商品同樣,得要把咱們的商品放在拍攝空間之中。

在 Three.js 中,向場景中添加對象能夠直接經過場景對象調用 add 方法實現。具體實現以下:

scene.add(cube);
複製代碼

咱們向 add()方法內傳入咱們要添加的對象,能夠是一個,也能夠多個,用逗號隔開。

光源

和現實生活中的邏輯是同樣的,物體自己是不會發光的。若是沒有太陽這一光源,地球將陷入無盡的黑暗,啥也瞅不着。因此咱們也要向咱們的場景中添加光源對象。

在 Three.js 中,光源分爲了好幾種,接下來將簡單的介紹其中用的比較多的幾種。

  1. THREE.AmbientLight

    這是一種基本光源,該光源將會疊加到場景現有物體的顏色上。

    該光源沒有特定的來源方向,且不會產生陰影。

    咱們常常在使用了其餘光源的同時使用它,是爲了弱化陰影或給場景添加一些額外的顏色。

  2. THREE.SpotLight

    這種光源有聚光的效果,相似檯燈、手電筒、舞臺聚光燈。

    這種光源能夠投射陰影。

  3. THREE.DirectionalLight

    這種光源也稱爲無限光,相似太陽光。

    這種光源發出的光線能夠看做是平行的。

    這種光源也可投射陰影。

在咱們的例子中,咱們將用 SpotLight 來建立咱們的燈光。

首先咱們要和以前常見拍攝對象同樣,先實例化一個 SpotLight 對象,而且以一個十六進制的顏色值做爲傳參,做爲咱們燈光的顏色。

var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(0, 20, 20);
spotLight.intensity = 5;
scene.add(spotLight);
複製代碼

在擁有光源對象以後,咱們將調用 position.set()方法設置在三維空間中的位置。

intensity 屬性用於設置光源照射的強度,默認值爲 1。

最後咱們也得將光源放進咱們的場景空間之中。這樣咱們的場景就有了一個 SpotLight 光源了。

攝像機

在 THREE.js 中有兩種相機:

  • THREE.PerspectiveCamera(透視相機)

    符合近大遠小的常理。用接近真實世界的視角來渲染場景。

  • THREE.OrthographicCamera(正交相機)

    提供了一個僞三維效果。

能夠看的出來:透視相機更貼近咱們現實生活中人眼所觀察到的世界,而正交相機渲染的結果和對象相距相機距離的遠近沒有影響。

這裏我將着重介紹一下 PerspectiveCamera:

咱們先來看一張圖:

對於一個透視相機來講,咱們須要設定如下幾個參數:

  • fov(視場)是豎直方向上的張角(是角度制而非弧度制)
  • aspect(長寬比)是照相機水平方向和豎直方向長度的比值
  • near(近面距離)相機到視景體最近的距離
  • far(遠面距離)相機到視景體最遠的距離
  • zoom(變焦)

這裏咱們也將建立一個咱們本身的透視相機。

var camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.x = 5;
camera.position.y = 10;
camera.position.z = 10;
camera.lookAt(cube.position);
複製代碼

首先咱們在實例化透視相機對象的時候,向其內部傳遞了幾個參數:豎直方向上的張角爲 75 度,長寬比與窗口相同,相機到視景體最近、最遠的距離分別爲 0.1 和 1000。

最後咱們讓相機經過調用 lookAt()方法,看向咱們以前建立的拍攝對象 cube 的位置上。(默認狀態下,相機將指向三維座標系的原點。)

渲染器(Renderer)

在有了以上的這些對象以後,咱們離成功之差區區幾步了。

在看到這一部分的標題的時候,你可能會問:什麼是渲染器?

通俗地說:咱們用相機拍到的是底片,還不是真正的相片。若是你還對老式相機有所印象,這一點將不難理解。

當咱們拿着一臺老式相機(還須要膠捲的那種)咱們每拍一張都將獲得一張底片。咱們想要拿到正真的相片還須要帶着底片,前往照相館去洗出來。這時候老闆會問你你要洗多大的相片,而後依據你的需求洗出你想要的相片。

能夠說這就是渲染器的做用——洗相片。還記得咱們以前在設置相機的參數的時候,咱們並無設定相機的寬高,而是隻指定了相機的長寬比。這就像咱們的底片同樣,雖然小,可是卻顯示了咱們相片的基本長寬比。

咱們建立渲染器的方法和建立 THREE 中的其餘對象同樣,都須要先將對象實例化。

Three.js 爲咱們提供了好幾種不一樣的渲染器這裏咱們將使用 THREE.WebGLRenderer 渲染器做爲例子。

var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
renderer.render(scene, camera);
複製代碼
  • 咱們經過調用 setSize() 方法設置渲染的長寬。
  • 渲染器 renderer 的 domElement 元素,表示渲染器中的畫布,全部的渲染都是畫在 domElement 上的,因此這裏的 appendChild 表示將這個 domElement 掛接在 body 下面,這樣渲染的結果就可以在頁面中顯示了。
  • render()方法中傳遞咱們的場景和相機,至關於傳遞了一張由相機拍攝場景獲得的一張底片,它將將圖像渲染到咱們的畫布中。

這時候你將獲得一個以下形狀:

這裏咱們爲了方便觀察,添加了座標系對象。

與通常對象同樣,咱們經過實例化該對象,並向其內傳遞一個軸長參數,最後添加進咱們的場景之中。

var axes = new THREE.AxisHelper(7);
scene.add(axes);
複製代碼

這裏咱們的座標系軸長設置爲 7。

這時候你會發現這張圖片仍是靜態的,3D 的特性尚未徹底發揮出來。

動畫(Animation)

在講解動畫以前咱們須要科普幾個知識點,實際上扯遠了一點,不過會有助於咱們去理解動畫的渲染,提升性能。

理解 Event Loop

異步執行的運行機制以下:

  1. 全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。
  2. 主線程以外,還存在一個「任務隊列」(task queue)。只要知足異步任務的執行條件,就在「任務隊列」之中放置一個事件
  3. 一旦「執行棧」中的全部同步任務執行完畢,系統就會讀取「任務隊列」,看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。

主線程不斷重複上面的第三步。主線程從「任務隊列」中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲 Event Loop(事件循環)。只要主線程空了,就會去讀取「任務隊列」,這就是 JavaScript 的運行機制。這個過程會循環反覆。

動畫原理

動畫其實是由一些列的圖片在必定時間內,以必定的頻率播放而產生的錯覺。

眼睛的一個重要特性是視覺惰性,即光象一旦在視網膜上造成,視覺將會對這個光象的感受維持一個有限的時間,這種生理現象叫作視覺暫留性。對於中等亮度的光刺激,視覺暫留時間約爲 0.1 至 0.4 秒。

爲了讓動畫連貫的、平滑的方式進行過渡,通常咱們以 60 幀每秒甚至更高的速率渲染動畫。

爲何不用 setInterval() 實現動畫?

  • setInterval()的執行時間並非肯定的。在 Javascript 中, setInterval()任務被放進了異步隊列中,只有當主線程上的任務執行完之後,纔會去檢查該隊列裏的任務是否須要開始執行,所以 setInterval()的實際執行時間通常要比其設定的時間晚一些。
  • setInterval()只能設置一個固定的時間間隔,這個時間不必定和屏幕的刷新時間相同。

以上兩種狀況都會致使 setInterval()的執行步調和屏幕的刷新步調不一致,從而引發丟幀現象。 那爲何步調不一致就會引發丟幀呢?

首先要明白,setInterval()的執行只是在內存中對圖像屬性進行改變,這個變化必需要等到屏幕下次刷新時纔會被更新到屏幕上。若是二者的步調不一致,就可能會致使中間某一幀的操做被跨越過去,而直接更新下一幀的圖像。假設屏幕每隔 16.7ms 刷新一次(60 幀),而 setInterval()每隔 10ms 設置圖像向左移動 1px, 就會出現以下繪製過程:

  • 第 0ms: 屏幕未刷新,等待中,setInterval()也未執行,等待中;
  • 第 10ms: 屏幕未刷新,等待中,setInterval()開始執行並設置圖像屬性 left=1px;
  • 第 16.7ms: 屏幕開始刷新,屏幕上的圖像向左移動了1px, setInterval()未執行,繼續等待中;
  • 第 20ms: 屏幕未刷新,等待中,setInterval()開始執行並設置 left=2px;
  • 第 30ms: 屏幕未刷新,等待中,setInterval()開始執行並設置 left=3px;
  • 第 33.4ms:屏幕開始刷新,屏幕上的圖像向左移動了3px, setInterval()未執行,繼續等待中;

從上面的繪製過程當中能夠看出,屏幕沒有更新 left=2px 的那一幀畫面,圖像直接從 1px 的位置跳到了 3px 的的位置,這就是丟幀現象,這種現象就會引發動畫卡頓。

requestAnimationFrame()

requestAnimationFrame()的優點

與 setInterval()相比,requestAnimationFrame()最大的優點是**由系統來決定回調函數的執行時機。**具體一點講,若是屏幕刷新率是 60 幀,那麼回調函數就每 16.7ms 被執行一次,若是刷新率是 75Hz,那麼這個時間間隔就變成了 1000/75=13.3ms,換句話說就是,requestAnimationFrame()的步伐跟着系統的刷新步伐走。它能保證回調函數在屏幕每一次的刷新間隔中只被執行一次,這樣就不會引發丟幀現象,也不會致使動畫出現卡頓的問題。

除此以外,requestAnimationFrame()還有如下兩個優點:

  • CPU 節能:使用 setInterval()實現的動畫,當頁面被隱藏或最小化時,setInterval()仍然在後臺執行動畫任務,因爲此時頁面處於不可見或不可用狀態,刷新動畫是沒有意義的,徹底是浪費 CPU 資源。而 requestAnimationFrame()則徹底不一樣,當頁面處理未激活的狀態下,該頁面的屏幕刷新任務也會被系統暫停,所以跟着系統步伐走的 requestAnimationFrame()也會中止渲染,當頁面被激活時,動畫就從上次停留的地方繼續執行,有效節省了 CPU 開銷。

  • 函數節流:在高頻率事件(resize,scroll 等)中,爲了防止在一個刷新間隔內發生屢次函數執行,使用 requestAnimationFrame()可保證每一個刷新間隔內,函數只被執行一次,這樣既能保證流暢性,也能更好的節省函數執行的開銷。一個刷新間隔內函數執行屢次時沒有意義的,由於顯示器每 16.7ms 刷新一次,屢次繪製並不會在屏幕上體現出來。

requestAnimationFrame()的工做原理:

先來看看 Chrome 源碼:

int Document::requestAnimationFrame(PassRefPtr<RequestAnimationFrameCallback> callback)
{
  if (!m_scriptedAnimationController) {
    m_scriptedAnimationController = ScriptedAnimationController::create(this);
    // We need to make sure that we don't start up the animation controller on a background tab, for example.
      if (!page())
        m_scriptedAnimationController->suspend();
  }

  return m_scriptedAnimationController->registerCallback(callback);
}
複製代碼

仔細看看就以爲底層實現意外地簡單,生成一個 ScriptedAnimationController 的實例用於存放註冊事件,而後註冊這個 callback。

requestAnimationFrame 的實現原理就很明顯了:

  • 註冊回調函數
  • 瀏覽器按必定幀率更新時會觸發 觸發全部註冊過的 callback

這裏的工做機制能夠理解爲全部權的轉移,把觸發幀更新的時間全部權交給瀏覽器內核,與瀏覽器的更新保持同步。這樣作既能夠避免瀏覽器更新與動畫幀更新的不一樣步,又能夠給予瀏覽器足夠大的優化空間。

用 requestAnimationFrame()建立動畫

咱們須要建立一個循環渲染函數,而且進行調用:

// a render loop
function render() {
  requestAnimationFrame(render);

  // Update Properties

  // render the scene
  renderer.render(scene, camera);
}
複製代碼

咱們在函數體內部進行相應的屬性更新並渲染,而且讓瀏覽器來控制動畫幀的更新。

製做動畫

這裏咱們將經過 requestAnimationFrame() 來建立咱們的動畫效果。讓瀏覽器來控制動畫幀的更新最大的提升咱們的性能。

var animate = function() {
  requestAnimationFrame(animate);
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  renderer.render(scene, camera);
};
animate();
複製代碼

咱們在 animate()方法中,經過 requestAnimationFrame(animate)來使瀏覽器在每次更新頁面的時候調用 animate 方法。且每調用一次,正方體的屬性就做出相應的改變:每一次調用都比上一次 X 軸、Y 軸各旋轉 0.01 弧度,而且將其渲染到畫布上。

這樣咱們的動畫就產生了:

THREE.Color 對象

這裏我在補充說明一下 Three.js 內置的顏色對象。

一般狀況下,咱們可使用十六進制的字符串("#000000")或十六進制值(0x000000)來建立指定顏色對象。咱們也能夠用 RGB 顏色值來建立(0.2, 0.3, 0.4),但值得注意的是其每一個值的範圍爲 0 到 1。

例如:

var color = new THREE.Color(0x000000);
複製代碼

在建立顏色對象以後,咱們能夠對用其自身的一些方法,這裏就不詳細介紹了:

函數名 描述
set(value) 將當前顏色設置爲指定的十六進制值。這個值能夠是字符串、數值或是已有的 THREE.Color 實例。
setHex(value) 將當前顏色設置爲指定的十六進制數字值。
setRGB(r,g,b) 根據提供的 RGB 值設置顏色。參數範圍從 0 到 1。
setHSL(h,s,l) 根據提供的 HSL 值設置顏色。參數範圍從 0 到 1。
setStyle(style) 根據 css 設置顏色的方式來設置顏色。例如:可使用 "rgb(25, 0, 0)"、"#ff0000"、"#ff" 或 "red"。
copy(color) 從提供的顏色對象複製顏色值到當前對象。
getHex() 以十六進制值形式從顏色對象中獲取顏色值:435241。
getHexString() 以十六進制字符串形式從顏色對象中獲取顏色值:"0c0c0c"。
getStyle() 以 css 值的形式從顏色對象中獲取顏色值:"rgb(112, 0, 0)"
getHSL(optionalTarget) 以 HSL 值的形式從顏色對象中獲取顏色值。若是提供了 optionTarget 對象, Three.js 將把 h、s 和 l 屬性設置到該對象。
toArray 返回三個元素的數組:[r,g,b]。
clone() 複製當前顏色。

總結

能夠這麼說:

Three.js 的一切都創建在 Scene 對象之上。有了場景這一空間以後,咱們就能夠往裏面添加咱們要展現的拍攝對象了。固然有了拍攝對象以後咱們還須要一個光源,讓咱們看的見咱們的對象。這時候咱們還須要一個相機,用以拍攝咱們的拍攝對象。固然咱們實際還須要靠咱們的渲染器將實際圖像繪製在畫布上。

經過不斷變換對象的屬性,而且不斷地繪製咱們的場景,這就產生了動畫!

附源碼

<html>
  <head>
    <title>Cube</title>
    <style> body { margin: 0; overflow: hidden; } canvas { width: 100%; height: 100%; } </style>
  </head>

  <body>
    <script src="https://cdn.bootcss.com/three.js/r83/three.min.js"></script>
    <script> var scene = new THREE.Scene(); var axes = new THREE.AxisHelper(7); scene.add(axes); var geometry = new THREE.BoxGeometry(4, 4, 4); var geometryMeshBasicMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true }); var geometryMeshLambertMaterial = new THREE.MeshLambertMaterial({ color: 0x242424 }); var cube = THREE.SceneUtils.createMultiMaterialObject(geometry, [ geometryMeshBasicMaterial, geometryMeshLambertMaterial ]); scene.add(cube); var spotLight = new THREE.SpotLight(0xffffff); spotLight.position.set(0, 20, 20); spotLight.intensity = 5; scene.add(spotLight); var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.x = 5; camera.position.y = 10; camera.position.z = 10; camera.lookAt(cube.position); var renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); var animate = function() { requestAnimationFrame(animate); cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, camera); }; animate(); </script>
  </body>
</html>
複製代碼

-EFO-


筆者專門在 github 上建立了一個倉庫,用於記錄平時學習全棧開發中的技巧、難點、易錯點,歡迎你們點擊下方連接瀏覽。若是以爲還不錯,就請給個小星星吧!👍


2019/04/14

AJie

相關文章
相關標籤/搜索