零基礎玩轉 WebGL - 快速上手

語言: CN / TW / HK

theme: qklhk-chocolate highlight: an-old-hope


WebGL 作為非常底層的 API,學習上手難度非常大,這是因為 WebGL 要求的背景知識比較多。而網上的教程一般沒有過多介紹直接就介紹 API 開始渲染了,容易讓人云裏霧裏,很容易被勸退,就算學到了 API 使用,也是隻懂表面知識,沒有了解背後的原理,很容易就忘記了。

《零基礎玩轉 WebGL》系列教程將從最基本知識開始,漸進的講解 WebGL 使用和 WebGL 背後原理還有必不可少的數學知識。讓你學完之後不光可以開發 WebGL 應用,還能輕鬆閲讀渲染引擎源碼,如 Three.js,並且能夠開發自己的 3D 渲染引擎庫。

這是《零基礎玩轉 WebGL》的第一篇文章,系列文章目錄請查看:零基礎玩轉 WebGL - 目錄

什麼是 WebGL?

WebGL(Web Graphics Library)是一個 Web 標準 JavaScript API,通過 HTML5 的 canvas 元素進行暴露,無需使用插件,即可在瀏覽器中渲染高性能的交互式 3D 和 2D 圖形。目前是由非營利 Khronos Group 設計和維護。

使用 WebGL 的方式和 canvas 2d 類似,都是通過 getContext 方法獲取渲染上下文,如下所示。

```js const canvas = document.createElement('canvas')

const gl = ( canvas.getContext('webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl') ) ```

上面代碼中是按照 webgl2webglexperimental-webgl 的順序獲取 WebGL 渲染上下文。webgl2 是最新版本,它幾乎完全兼容 WebGL1。experimental-webgl 用來兼容老瀏覽器,如 IE 11。

兼容性

image.png

大多數瀏覽器都支持 WebGL1。也有很多現代瀏覽器支持 WebGL2,但是蘋果還不支持 WebGL2,所以編寫 WebGL 程序時,需要向下降級到 WebGL1。

image.png

OpenGL

在深入 WebGL 之前,我們還需要先了解 OpenGL,因為 WebGL 是基於 OpenGL 的。OpenGL(Open Graphics Library) 是用於渲染2D、3D矢量圖形的跨語言、跨平台的應用程序編程接口,常用於CAD、虛擬現實、科學可視化程序和電子遊戲開發。OpenGL 通常是顯卡生產商根據規範來實現的。

OpenGL 前身是 SGI 的 IRIS GL API 它在當時被認為是最先進的科技併成為事實上的行業標準,後由 SGI 轉變為一項開放標準 OpenGL。1992年 SGI 創建 OpenGL架構審查委員會,2006年將 OpenGL API 標準的控制權交給 Khronos Group。

OpenGL 是跨平台的,在移動設備上是使用 OpenGL ES(OpenGL for Embedded Systems), 它是 OpenGL 的子集。下圖展示了 OpenGL 和 OpenGL ES 的時間線。

image.png

WebGL 基於 OpenGL,是 OpenGL 的子集。WebGL1 基於 OpenGL ES 2.0。WebGL2 基於 OpenGL ES 3.0。

GPU

WebGL 性能高的原因是它使用到了 GPU。GPU 和 CPU 針對的是兩種不同的應用場景,大家可以把 CPU 想象為一個切圖專家,而 GPU 是一羣初級切圖仔,現在有一大堆非常簡單的頁面,大街上隨便抓個人都能切。那麼對於這個任務不用想就知道一羣初級切圖仔更快,切圖專家當然厲害,但是也奈不了對面人多。所以對於大量簡單計算 GPU 的執行速度是遠大於 CPU 的。

862LRm.gif

E9Rozv.gif

ZV6Q2J.gif

上面圖片中,第一個是 CPU,第二是 GPU,CPU 只有一杆槍,而 GPU 有一大捆槍。CPU 要一下一下的打,就像切圖專家一個一個的切,而 GPU 一次性全打了,就像一羣初級切圖仔,沒人切一個,一次性全切完了。

image.png

上圖是顯卡 3090 的配置參數,我們可以看到它有 1 萬多個核心,24G 顯存。支持 3D API,DirectX 12 Ultimate 和 OpenGL 4.6 (DirectX 是微軟的圖形 API)。

座標系

WebGL 的座標系和 canvas 2d 中的是不太一樣的。因為 WebGL 是 OpenGL 子集,所以 WebGL 座標系和 OpenGL 座標系性值一樣。

canvas 2d 中的座標系如下所示。

js const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d')

canvas 2d 的座標原點在左上角,X 軸和 Y 軸的正值分別向右和向下。

而 WebGL 的座標系和 OpenGL 一樣,它更符合我們的常識一點。

原點在正中間,右邊為 X 軸正方向,上面為 Y 軸正方向,就和數學中的一樣。

需要注意的是 WebGL 中座標值的範圍是 -11,而 canvas 2d 是根據 canvas 的寬高大小來的。如果 canvas 寬度為 500,那麼 WebGL 中的 1 就相當於 5000.5 就相當於 250,這樣的好處是我們無需關心 canvas 的寬高,無論 canvas 多大對於渲染圖形來説範圍都是 -11

當然 WebGL 中還有一個 Z 軸。Z 軸有兩種形式,一種是正值朝外,另一種是正值朝內。

當 Z 軸正值朝外時,我們稱為右手座標系,當 Z 軸正值朝內時稱為左手座標系。可以伸出雙手像下圖一樣比劃下,就知道為什麼稱為左手座標系和右手座標系了。

那麼 WebGL 是左手座標系還是右手座標系呢?答案為都不是。但是在實際開發中是使用 右手座標系,當然並不是右手座標系比左手座標系好,而是右手座標系是 OpenGL 的慣例。例如微軟的 DirectX 中慣用的是左手座標系。

這裏為什麼説 WebGL 既不是左手座標系也不是右手座標系,原因將在後續文章中講解,現在只用知道 WebGL 中使用的是右手座標系,也就是 Z 軸正值朝外。

三角形

WebGL 算是比較底層的圖形 API,不同於 canvas 2d,WebGL 只能用它來渲染點,線和三角形。那些複雜的 3D 模型其實都是由一個個三角形組成。

image.png

比如上方這輛汽車模型,它其實是由 267300 個三角形組成。

點擊這個鏈接查看模型詳情http://sketchfab.com/3d-models/the-argonaut-4982efe9a03e42e6a867c33afd863ca5 。

可能有同學會問了,為什麼就是三角形,而不是 5 邊形,6 邊形呢?

因為三角形有很多的優勢,比如三角形一定在一個平面上,任何多邊形都可以使用三角形組成等性值。

渲染一個三角形

瞭解了這麼多背景知識,現在讓我們來實際使用 WebGL 來渲染一個最簡單的三角形吧。

```js const canvas = document.createElement('canvas') canvas.width = canvas.height = 300 document.body.append(canvas) const gl = canvas.getContext('webgl')

gl.viewport(0, 0, gl.canvas.width, gl.canvas.height) // 設置 webgl 視口,將 -1 到 1 映射為 canvas 上的座標

const vertexShader = gl.createShader(gl.VERTEX_SHADER) // 創建一個頂點着色器 gl.shaderSource(vertexShader, ` attribute vec4 a_position;

void main() { gl_Position = a_position; // 設置頂點位置 } `) // 編寫頂點着色器代碼 gl.compileShader(vertexShader) // 編譯着色器

const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER) // 創建一個片元着色器 gl.shaderSource(fragmentShader, ` precision mediump float; uniform vec4 u_color;

void main() { gl_FragColor = u_color; // 設置片元顏色 } `) // 編寫片元着色器代碼 gl.compileShader(fragmentShader) // 編譯着色器

const program = gl.createProgram() // 創建一個程序 gl.attachShader(program, vertexShader) // 添加頂點着色器 gl.attachShader(program, fragmentShader) // 添加片元着色器 gl.linkProgram(program) // 連接 program 中的着色器

gl.useProgram(program) // 告訴 webgl 用這個 program 進行渲染

const colorLocation = gl.getUniformLocation(program, 'u_color') // 獲取 u_color 變量位置 gl.uniform4f(colorLocation, 0.93, 0, 0.56, 1) // 設置它的值

const positionLocation = gl.getAttribLocation(program, 'a_position') // 獲取 a_position 位置 const positionBuffer = gl.createBuffer() // 創建一個頂點緩衝對象,返回其 ID,用來放三角形頂點數據, gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer) // 將這個頂點緩衝對象綁定到 gl.ARRAY_BUFFER // 後續對 gl.ARRAY_BUFFER 的操作都會映射到這個緩存 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 0, 0.5, 0.5, 0, -0.5, -0.5 ]), // 三角形的三個頂點 // 因為會將數據發送到 GPU,為了省去數據解析,這裏使用 Float32Array 直接傳送數據 gl.STATIC_DRAW // 表示緩衝區的內容不會經常更改 ) // 將頂點數據加入的剛剛創建的緩存對象

gl.enableVertexAttribArray(positionLocation); // 開啟 attribute 變量,使頂點着色器能夠訪問緩衝區數據 gl.vertexAttribPointer( // 告訴 OpenGL 如何從 Buffer 中獲取數據 positionLocation, // 頂點屬性的索引 2, // 組成數量,必須是1,2,3或4。我們只提供了 x 和 y gl.FLOAT, // 每個元素的數據類型 false, // 是否歸一化到特定的範圍,對 FLOAT 類型數據設置無效 0, // stride 步長 數組中一行長度,0 表示數據是緊密的沒有空隙,讓OpenGL決定具體步長 0 // offset 字節偏移量,必須是類型的字節長度的倍數。 )

gl.clearColor(0, 1, 1, 1) // 設置清空顏色緩衝時的顏色值 gl.clear(gl.COLOR_BUFFER_BIT) // 清空顏色緩衝區,也就是清空畫布

gl.drawArrays( // 從數組中繪製圖元 gl.TRIANGLES, // 渲染三角形 0, // 從數組中哪個點開始渲染 3 // 需要用到多少個點,三角形的三個頂點 ) ```

渲染結果如下所示。

代碼片段

可以發現 WebGL 的代碼非常複雜繁瑣,一個非常簡單的三角形就需要編寫這麼多的代碼。

上面實例代碼中有詳細的註釋,不過相信大家看了也還是滿頭問號。我們再來看看 WebGL 渲染的整個流程,一般 WebGL 程序是 JS 提供數據(在 CPU 中運行),然後將數據發送到顯存中,交給 GPU 渲染,我們可以使用着色器控制 GPU 渲染管線部分階段。

``js // CPU const vertexShader =shader source code// 頂點着色器代碼 const fragmentShader =shader source code` // 片段着色器代碼 const points = [{ x: 1, y: 1, z: 1 }/ ... /] // 準備數據 gl.draw(points, vertexShader, fragmentShader) // 將數據和着色器發送給 GPU

// GPU const positions = data.map(point => vertexShader(point)) // 運行頂點着色器 const frags = Rasterization(positions) // 光柵化 const colors = frags.map(frag => fragmentShader(frag)) // 運行片段着色器 Display(colors) // 渲染到屏幕 ```

上面的偽代碼,簡單展示了 WebGL 程序的執行流程。OpenGL 中着色器是使用 GLSL 編寫,WebGL 中也是使用的 GLSL 着色器語言,它的語法有點類似 C 語言,我們可以通過頂點着色器和片段着色器控制 GPU 渲染的部分環節。

WebGL 中有兩個着色器分別是頂點着色器和片段(也可稱為“片元”)着色器。頂點着色器用於處理圖形的每個點,也就是上面例子中三角形的三個頂點。處理完畢後會進行光柵化,大家可以把光柵化理解成把圖形變成一個個像素,我們顯示器屏幕是一個個像素組成的,要顯示圖形就需要計算出圖形中的每個像素點。片段着色器可以先理解成像素着色器,也就是將光柵化中的每個像素拿過來,給每個像素計算一個顏色。整個流程如下所示。

image.png

上圖中頂點數據傳送給 GPU 後,頂點着色器計算出每個點的位置,光柵化計算出圖形的每個像素,片段着色器計算出每個像素的顏色,然後就可以渲染到顯示器上了。(可以忽略上圖的幾何着色器,WebGL 中沒有這個着色器)着色器先簡單介紹到這裏,還不瞭解着色器也沒有關係,下篇文章會更加詳細的講解。

其實 WebGL 是一個非常大的狀態機,它提供的方法都是改變 WebGL 的某個狀態。我們需要在 CPU 中使用 JS 設置 WebGL 的狀態,準備數據和着色器程序,然後發送給 GPU 執行。

上方代碼可以分為如下幾步。

  1. 因為 WebGL 的座標是 -1 到 1,所以首先我們使用 viewport 設置視口大小信息。
  2. 創建頂點和片段着色器(關於着色器情況下篇文章),然後創建一個程序,來連接頂點和片段着色器。
  3. 然後獲取着色器中的變量,設置如何將值傳遞給着色器。三角形是由 3 個頂點組成,所以準備了 3 個點的座標。
  4. 設置清屏顏色,並清屏,和座標類似,WebGL 中的顏色是 0 到 1,而不是 0 到 255。
  5. 將數據發送給 GPU 來渲染三角形

例子

上面這個簡單的三角形一點都不炫酷,其實 WebGL 可以做出非常炫酷的效果,下面列舉一些不錯例子,大家感興趣可以看一看。

ThreeJS

image.png

http://threejs.org/

WebGL Samples

image.png

http://webglsamples.org/

Experiments with Google

image.png

http://experiments.withgoogle.com/

Adult Swim

image.png

http://www.adultswim.com/etcetera/

Evan Wallace

image.png

http://madebyevan.com/

總結

這篇文章講解了什麼是 WebGL,瞭解了 WebGL 的大致輪廓,並且完成了一個最簡單的 WebGL 應用。下篇文章將詳細講解非常重要的 WebGL 着色器

如果覺得文章還不錯歡迎點贊關注支持,我會盡快更新系列教程的下一篇文章。

零基礎玩轉 WebGL 系列文章目錄請查看:零基礎玩轉 WebGL - 目錄