從零開始學習時空資料視覺化(一)
前言:本教程為「從零開始學習時空資料視覺化」系列第一篇教程——three.js 簡介與示例教學,主要通過一個簡單示例教學讓讀者輕鬆上手 three.js 的一些基本 API。關於這個系列以及程式碼等細節可見:
- glmaps - GitHub 地址,歡迎 watch 關注與 star 鼓勵
- 從零開始學習時空資料視覺化(序)
- Zero to One: How I mastered Data Visualization and how you can too
眾所周知,得益於瀏覽器在 canvas 上實現了 WebGL 介面,開發者可以按照規範在 canvas 畫布上建立場景、幾何體與動畫。WebGL 是基於 OpenGL ES 2.0 的 Web 標準,可以通過 HTML5 Canvas 元素作為 DOM 介面訪問。但由於 WebGL (OpenGL ES) 的特殊性,基於 canvas 3D 上下文的開發相距常規的前端開發還是有不少差距,面對大量的 API 時,剛入門的同學大概率會被繞的頭暈目眩。
本文將以一個實際的幾何體動畫 Demo 為例,來闡明兩件事:
- 初步瞭解 WebGL 的一些基本概念與 three.js 的基本內容;
- 實戰編寫一個簡單的幾何體動畫來熟悉 three.js 中的一些基本 API;
在描述過程中我會將涉及到的 API 全部標註出來,如果需要進一步瞭解可以複製到 MDN 或者 http:// threejs.org 檢視 API 文件。
一、WebGL 碎碎念
首先在正式使用一個 Web API 之前,你可以通過 https:// caniuse.com/ 這個網站檢視當下瀏覽器對其的支援程度,比如 WebGL 的支援程度便如下圖所示:

鑑於 WebGL 已經得到了各大瀏覽器的廣泛支援,我們直接進入正題。你可以訪問 http:// get.webgl.org/ 或者 https:// get.webgl.org/webgl2/ 網站來檢視你的瀏覽器對 WebGL(2) 的支援情況。我簡單寫了一個函式,如果需要新增檢測使用者環境是否支援 WebGL 的邏輯,你可以在應用中呼叫如下函式:
const detectWebGLContext = () => { let canvas = document.createElement("canvas"); let gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); let msgTxt = "無法檢測到 WebGL 上下文,你的瀏覽器不支援 WebGL。"; if (gl && gl instanceof WebGLRenderingContext) { msgTxt = "恭喜,你的瀏覽器支援 WebGL!"; } alert(msgTxt); }
還有一個有用的 API 組合方法,想想你在 canvas 上繪製完影象後,如何清除畫布以便重新繪製其他影象呢?你可以試試如下函式,它的作用是通過一個單色清除整個區域內容:
const clearWithColor = (gl) => { gl.viewport( 0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight ); gl.clearColor(0.0, 0.5, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); }
簡單介紹一下,程式碼第一行通過 WebGLRenderingContext.viewport()
方法設定了視口的範圍,然後通過 clearColor()
設定清除色為綠色(只改變 WebGL 內部的一個狀態,但並不會繪製任何東西),最後使用 clear()
方法實際繪製。
WebGL 暫時就說這麼多,這些大致夠我們繼續往後看關於 three.js 的內容了。
二、初識 three.js
three.js 是一個輕量的 3D 視覺化庫,它通過抽象隱藏了 WebGL 的很多複雜性,使得在 Web 上構建 3D 場景變得非常簡單。

說到 3D 場景,我們有一些概念需要提前瞭解:
- 場景:是物體、光源等元素的容器;
- 相機:場景中的相機,代替人眼去觀察,場景中只能新增一個,決定哪些東西將在螢幕上渲染;
- 物體物件:包括二維物體(點、線、面)、三維物體,模型等等,他們是在相機透視圖裡主要的渲染物件;
- 光源:場景中的光照,如果不新增光照場景將會是一片漆黑,包括全域性光、平行光、點光源等;
- 渲染器:場景的渲染方式,如 WebGL/canvas2D/CSS3D;
- 控制器:可通過鍵盤、滑鼠控制相機的移動,用於互動;
關於 three.js 的一些詳細介紹,可以參閱如下資源:
- three.js 官網 https:// threejs.org/
- GitHub 地址 https:// github.com/mrdoob/three .js
- Stack Overflow https:// stackoverflow.com/quest ions/tagged/three.js
- Intro to WebGL with Three.js http:// davidscottlyons.com/thr eejs-intro/
- three.js 線上編輯器 https:// threejs.org/editor/
三、上手簡單動畫示例
開始敲程式碼之前,請準備好這幾樣東西:
- 一個支援 WebGL 的瀏覽器(推薦 Chrome)
- JavaScript 基礎知識
- 充滿一顆好奇心
簡單起見,我們的程式碼將全部放在一個 html 檔案中,並且我們使用 CDN 地址引入 three.js 框架(本文引用 three.js v103),以下為我們初始化的 index.html
檔案。
<!DOCTYPE html> <html lang="en"> <head> <title>從零開始學習時空資料視覺化示例 - glmaps</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> <style> body { background: #fff; padding: 0; margin: 0; overflow: hidden; } </style> </head> <body> <div id="glmapsTitle" > 從零開始學習時空資料視覺化示例 <a href="https://github.com/hijiangtao/glmaps">GitHub 程式碼地址</a> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/103/three.min.js" integrity="sha256-T4lfPbatZLyNhpEgCvtmXmlhOUq0HZHkDX4cMlQWExA=" crossorigin="anonymous"></script> </body> </html>
讓我們先仔細看看,把一個隨時間變化的幾何體動畫拆解一下,看看具體有哪些工作要做:
- 場景、相機及需要繪製的幾何體
- 事件響應函式
- three.js 渲染器與掛載元素
- 渲染函式
我們一個個來看。three.js 提供有多種相機,本例中我們建立一個透視相機(API THREE.PerspectiveCamera
),該相機投影類似人眼成像的模式,也是3D場景中最普通的投影模式。
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 10000); camera.position.z = 500;
接著是我們需要繪製的物體了。在本例中,我們準備繪製1000個立方體,它們大小相同、位置隨機分佈。我們可以直接繪製1000個幾何體然後把他們依次加入場景,但為了更好的管理,我們用組(API THREE.Group
)來作為這些立方體的容器,組在功能上和 Object3D
幾乎是相同的,其目的是使得組中物件在語法上的結構更加清晰。
而每個立方體我們用網格(API THREE.Mesh
)來構建,網格被用來表示基於以三角形為 polygon mesh(多邊形網格)的物體的類。而傳入的 Mesh 構造器的兩個引數,一個是幾何體(API THREE.BoxBufferGeometry
),你可以簡單把它想像成用於描述幾何體的一個有效表述集合,比如頂點位置,面片索引、法相量、顏色值等等;另一個是材質(API THREE.MeshNormalMaterial
),材質被用來描述幾何體的外觀呈現。
除此外,我們給 Mesh 新增一個x, y, z都分佈在-1000到1000之間的隨機位置,並加上一個隨機旋轉量。
let geometry = new THREE.BoxBufferGeometry(100, 100, 100); let material = new THREE.MeshNormalMaterial(); group = new THREE.Group(); for (let i = 0; i < 1000; i++) { let mesh = new THREE.Mesh(geometry, material); mesh.position.x = Math.random() * 2000 - 1000; mesh.position.y = Math.random() * 2000 - 1000; mesh.position.z = Math.random() * 2000 - 1000; mesh.rotation.x = Math.random() * 2 * Math.PI; mesh.rotation.y = Math.random() * 2 * Math.PI; mesh.matrixAutoUpdate = false; mesh.updateMatrix(); group.add(mesh); }
接著我們建立一個場景(API THREE.Scene
),並把剛剛建立的物件組加入場景:
const scene = new THREE.Scene(); scene.background = new THREE.Color(0xffffff); scene.fog = new THREE.Fog(0xffffff, 1, 10000); // 將物件組新增到場景中 scene.add(group);
到現在為止,我們都沒有對 DOM 進行操作,接下來我們構建渲染器(API THREE.WebGLRenderer
),並將通過 .domElement
屬性得到的 canvas 元素新增到 DOM 中。
const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement);
我們可以通過 JavaScript 或者 CSS 改變 canvas 的尺寸,但這樣一來原有的 canvas 內容可能就會模糊,由於前面的程式碼中我們用視窗尺寸來描述渲染器,當視窗尺寸變化時,我們需要重新設定渲染器的尺寸等屬性:
function onWindowResize() { windowHalfX = window.innerWidth / 2; windowHalfY = window.innerHeight / 2; camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } window.addEventListener('resize', onWindowResize, false);
至此,一個基本的 three.js 程式就寫完了。但我們好像還差了些什麼。是的,我們需要為這個場景新增動畫,讓它動起來。這個動畫包含兩個部分,一方面我們需要不斷的更改物體的旋轉位置,另一方面我們也準備不斷變化相機的位置。canvas 的動畫實現很簡單,利用 requestAnimationFrame
API 不斷的繪製新的場景。在每次呼叫的渲染函式中,我們在每次更新時先算出相機偏移以及物體位置,然後更新它們的位置並重新呼叫渲染器渲染場景與相機。
function render() { // 根據當前時間建立正弦偏移量 let time = Date.now() * 0.001; let rx = Math.sin(time * 0.7) * 0.5, ry = Math.sin(time * 0.3) * 0.5, rz = Math.sin(time * 0.2) * 0.5; // 更新相機的座標,並讓相機面朝場景對準 camera.position.x += (mouseX - camera.position.x) * 0.05; camera.position.y += (- mouseY - camera.position.y) * 0.05; camera.lookAt(scene.position); // 更新物件組的旋轉座標 group.rotation.x = rx; group.rotation.y = ry; group.rotation.z = rz; // renderer.render(scene, camera); }
記得呼叫相機的方法 camera.lookAt(scene.position)
讓相機始終對著幾何體。
效果圖如下:

總結
總結一下,我們簡單聊了下 WebGL,並對 three.js 的一些基本情況作了介紹。之後通過一個示例,我們接觸到了場景、相機、渲染器、幾何體、材質、物件組等概念,requestAnimationFrame API 以及更新幾何體位置等屬性的方法。
以上大致涵蓋了一個 three.js 程式所會用到的大部分特性,但這些特性都有很多「變種」,比如幾何體除了本文列出的 BoxBufferGeometry 外,還有 CircleBufferGeometry、ConeBufferGeometry、CylinderBufferGeometry、DodecahedronBufferGeometry、EdgesGeometry、ExtrudeBufferGeometry 等等。
但不要怕,想必通過本文你已經大致瞭解瞭如何編寫一個 three.js 程式,萬變不離其宗,你已經成功邁出了第一步。
在下一篇教程中,我們將會更進一步、詳細探討如何實現 glmaps 中截圖的示例——星空地球。

個人公眾號 - 微信搜尋「 黯曉 」或掃這個二維碼
知乎專欄 -初級前端工程師
生活中難免犯錯,請多多指教!