【零基礎】充分理解WebGL(一)
在前端方向裡,WebGL算是典型的小眾領域,真正理解的人並不多。甚至一些拿Three.js寫3D應用的同學,對WebGL本身也是一知半解。
之所以這樣,是因為WebGL的技術棧和傳統的Web前端技術有極大的差別。相對而言,傳統Web前端使用的API比較高階,不存在太多需要理解的底層原理合概念,而WebGL的核心是OpenGL,它是OpenGL在Web上的實現。OpenGL是通過操作GPU來完成圖形繪製渲染的,因此它的API相對比較底層,使用起來較為繁瑣,這使得一些習慣於前端開發的工程師很難適應,所以就會覺得學習門檻較高。
實際上,要理解和學會WebGL,並沒有那麼困難,我們只需要理解一下GPU,瞭解它與CPU的不同點,然後再理解執行GPU程式碼的語言——glsl,瞭解著色器的基本概念和用法,就可以輕鬆理解WebGL的本質原理,然後在花一點時間和耐心,慢慢學習WebGL的API,就可以掌握WebGL這門技術了。
那麼什麼是GPU,它與CPU有什麼不同?我們通過下面幾張圖來看一下:
上面這張圖,是CPU的工作原理,它就像是一個管道,資料(圖中的箱子)從左側輸入,在CPU中完成處理,然後從右側輸出。CPU是由多個這樣的管道構成的,每個管道我們叫做一個CPU核心,如果你開啟你電腦的作業系統,檢視本機資訊,你可能會看到類似這樣的資訊:2.6 GHz 六核Intel Core i7
,這裡的六核,你可以理解成有6個這樣的管道,因此可以同時處理6個任務。
CPU的工作能力,與管道本身的處理速度(頻率)合管道的數量(核心數)有關係,頻率越高,那麼運算處理單一任務的速度就越快,核心數越多,那麼能同時並行處理的任務數就越多。
雖然現代計算機的CPU運算能力很強,但是它也有侷限性,對於某些場景,它並不擅長,比如圖形渲染。
我們知道,計算機影象是由畫素構成,所謂畫素,可以簡單理解為最終呈現在顯示裝置上的一個1x1的顏色小方塊。
現在的顯示裝置非常先進,可以用非常多的畫素小方塊來精確構圖。前端的CSS中的px
單位,就是畫素單位,一張800px
長、600px
寬的圖片,邏輯上是由600*800
,也就是48萬個畫素點構成的。如果要對這張圖片的畫素進行計算,用CPU來運算的話,單核CPU需要處理48萬個微小任務,就像下面這張圖:
不是說CPU不能完成這樣的處理,每一個畫素的計算可能是非常簡單的(只是處理一下顏色),但是數量太多,對CPU這樣的結構也會造成負擔。因此,在這個時候,另外一種高併發結構,也就是GPU就登場了。
與CPU不同,GPU可以看成是由數量非常多的微小管道構成的結構,每一個管道恰好可以處理“一粒沙子”,這樣,如果對於一張600畫素x800畫素的圖片,有48萬個管道組成的GPU,就可以同時處理這48萬個畫素點了!事實上,GPU幾乎就是這樣做的。
mermaid
flowchart LR
準備資料 --> 著色處理 --> 幀緩衝 --> 渲染輸出
WebGL利用JavaScript來準備資料,將資料通過共享資料結構(ArrayBuffer) 傳遞給GPU,由GPU進行著色處理,然後再將處理後的資料輸出到幀緩衝區,最後再渲染出來。
這其中的關鍵是頭兩個步驟,也就是準備資料和著色處理,其中準備資料一般是通過JavaScript的型別陣列(TypedArray),著色處理是通過WebGL Program執行一種特殊的glsl語言來實現。在WebGL中,著色階段通常分成兩步,分別是頂點著色和片段著色。
我前面也說過,WebGL的API比較底層,所以操作起來比較繁瑣,但是也有許多JS庫,幫我們封裝了基本的操作,因此在這裡,我們可以先跳過繁瑣的API部分,利用我簡單封裝的一個開源庫gl-renderer來學習一下WebGL的資料和著色部分。
兩個著色器
動手實踐是最好的理解問題的方法之一,所以我們來動手寫一寫程式碼。
```js const canvas = document.querySelector('canvas'); const renderer = new GlRenderer(canvas, {webgl2: true});
const fragment = #version 300 es
precision highp float;
out vec4 FragColor;
void main() {
FragColor = vec4(1, 0, 0, 1);
}
;
const program = renderer.compileSync(fragment);
renderer.useProgram(program);
renderer.render();
```
https://code.juejin.cn/pen/7098235100726296589
上面的程式碼裡面,JS的部分很好理解,但是其中有一段fragment變數中的字串,是需要我們關注的部分,沒錯,它就是兩個著色器之一的片段著色器程式碼。
這段程式碼是用glsl語言寫的,但是並不難理解,第一句 #version 300 es
宣告這個著色器是webgl 2.0版本的著色器,瀏覽器目前同時支援webgl 1.0和webgl 2.0兩個版本,它們有一些差異,但差異不是很大。
第二句precision highp float;
表示設定浮點數精度為高精度,這個可以暫時忽略,以後系列文章中再詳細講解。
第三句out vec4 FragColor
宣告FragColor
是輸出變數,它的型別是vec4
,是一個四維向量,用來表示一個RGBA顏色值,它與CSS的顏色區別是,CSS的RGB值是0到255,Alpha值是0到1,但是在著色器裡面,RGBA的值都是從0到1。
void main
是主函式,vec4(1, 0, 0, 1)
表示將 FragColor 設定為紅色。
👉🏻 你可以試著修改碼上掘金中的程式碼,把 vec4(1, 0, 0, 1) 改成 vec4(0, 1, 0, 1),看看會發生什麼?
在這裡有必要解釋一下為什麼這麼設定整個畫布會變成紅色。還記得前面說的,GPU是平行計算的,也就是說,這段著色器程式碼,是每個畫素都並行執行(嚴格來說是根據圖元裝配的結果來執行對應區域的畫素,但在這裡我們先略過這一點),因此,整個畫布中,所有的畫素點,你可以認為都執行了一遍上面的著色器程式碼,而且是同時、並行執行的,由於是無差別執行的設定顏色為紅色,因此整塊畫布都成為了紅色的。
我們可以修改一下上面例子的程式碼,看一下給每個畫素設定不同顏色的情況:
```js const canvas = document.querySelector('canvas'); const renderer = new GlRenderer(canvas, {webgl2: true});
const fragment = #version 300 es
precision highp float;
out vec4 FragColor;
uniform vec2 resolution;
void main() {
vec2 st = gl_FragCoord.xy / resolution;
FragColor = vec4(st, 0, 1);
}
;
const program = renderer.compileSync(fragment);
renderer.useProgram(program);
renderer.uniforms.resolution = [canvas.width, canvas.height];
renderer.render();
```
我們修改了上面的程式碼,在著色器中增加了一個uniform vec2 resolution
,這是聲明瞭一個resolution變數,它的型別是二維向量,我們通過renderer.uniforms.resolution
將畫布的寬高傳入。
gl_FragCoord.xy
是一個內建變數,它表示當前渲染的畫素在畫布內的座標,左下角是[0,0],右上角是[width,height],所以gl_FragCoord.xy / resolution
可以將座標值“歸一”(即將值限制到0~1區間,這是一種在寫著色器的時候經常使用的數學技巧)。然後我們將st的值傳給FragColor,這樣最終執行的結果如下:
https://code.juejin.cn/pen/7098245656740888612
通過這個例子,我們可以簡單理解片段著色器的執行方式,片段著色器對圖形十分重要,在後續的文章裡,我們還有很多技巧需要逐步學習。
但是我們現在先回過頭來,解決一個問題,為什麼這裡片段著色器對整個canvas生效?
所以,接下來我們就來了解另一個著色器:頂點著色器。
還是老辦法,動手實踐——讓我們來修改程式碼:
```js const canvas = document.querySelector('canvas'); const renderer = new GlRenderer(canvas, {webgl2: true});
const fragment = #version 300 es
precision highp float;
out vec4 FragColor;
void main() {
FragColor = vec4(1, 0, 0, 1);
}
;
const program = renderer.compileSync(fragment);
renderer.useProgram(program);
renderer.setMeshData([
{
positions: [[0, 1, 0], [-1, -1, 0], [1, -1, 0]],
cells: [[0, 1, 2]],
},
]);
renderer.render();
```
https://code.juejin.cn/pen/7098248752191766542
在這裡,我們沒有新增頂點著色器,只是給renderer設定了一些資料,在資料裡我們指定了三個頂點,它們的三維座標分別是[0, 1, 0], [-1, -1, 0], [1, -1, 0]
,這裡我們沒有用到z軸,所以z保持0,x和y在WebGL中,預設範圍是從-1到1,所以WebGL的平面座標系原點在中心,左下角是[-1,-1],右下角是[1, -1],右上角是[1, 1],左上角是[-1,1]。我們設定了position這三個頂點之後,繪製在畫面上的就變成了一個三角形。
之所以我們沒有指定頂點著色器,是因為gl-renderer有預設的頂點著色器,程式碼如下:
```glsl
version 300 es
precision highp float; precision highp int;
in vec3 a_vertexPosition;
void main() { gl_PointSize = 1.0; gl_Position = vec4(a_vertexPosition, 1); } ```
我們也可以指定頂點著色器,我們可以繼續修改程式碼:
```js const canvas = document.querySelector('canvas'); const renderer = new GlRenderer(canvas, {webgl2: true});
const vertex = `#version 300 es precision highp float; precision highp int;
in vec3 a_vertexPosition;
void main() { gl_PointSize = 1.0; gl_Position = vec4(0.5 * a_vertexPosition, 1); }`;
const fragment = #version 300 es
precision highp float;
out vec4 FragColor;
void main() {
FragColor = vec4(1, 0, 0, 1);
}
;
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);
renderer.setMeshData([
{
positions: [[0, 1, 0], [-1, -1, 0], [1, -1, 0]],
cells: [[0, 1, 2]],
},
]);
renderer.render();
```
https://code.juejin.cn/pen/7098253710907670564
上面的程式碼我們指定了頂點著色器,在它裡面我們把a_vertexPosition
,也就是我們傳入的頂點座標給乘以了0.5,所以最終繪製出來的三角形周長就是原來的1/2。
為什麼是三角形?
因為三角形是WebGL的基本圖元,WebGL支援點、線、三角形等基本圖元。下面的程式碼我們更換了圖元。
```js const canvas = document.querySelector('canvas'); const renderer = new GlRenderer(canvas, {webgl2: true});
const vertex = `#version 300 es precision highp float; precision highp int;
in vec3 a_vertexPosition;
void main() { gl_PointSize = 1.0; gl_Position = vec4(0.5 * a_vertexPosition, 1); }`;
const fragment = #version 300 es
precision highp float;
out vec4 FragColor;
void main() {
FragColor = vec4(1, 0, 0, 1);
}
;
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);
renderer.setMeshData([
{
mode: 'LINE_STRIP',
positions: [[0, 1, 0], [-1, -1, 0], [1, -1, 0]],
cells: [[0, 1, 2, 0]],
},
]);
renderer.render();
```
https://code.juejin.cn/pen/7098254635743313927
最後剩下一個問題,我們預設不設定頂點的時候,繪製的圖形是整個canvas範圍,但是WebGL並不支援四邊形圖元,那麼我們原本的繪製範圍是如何界定的呢?
我把這個問題作為本篇文章的課後問題,留給下一講來解決吧。
以上內容有任何問題,歡迎在評論區討論。
- Day1:用原生JS把你的裝置變成一臺架子鼓!
- 【零基礎】充分理解WebGL(七)
- 【零基礎】充分理解WebGL(六)
- 【零基礎】充分理解WebGL(五)
- 冷知識:不起眼但有用的String.raw方法
- 【零基礎】充分理解WebGL(四)
- 【零基礎】充分理解WebGL(三)
- 【零基礎】充分理解WebGL(二)
- 【零基礎】充分理解WebGL(一)
- css-doodle:如何讓CSS成為藝術?
- 建立合輯,將【碼上掘金】作為開源專案的demo庫使用
- 使用 babel 外掛來打造真正的“私有”屬性
- 使用 Node.js 對文字內容分詞和關鍵詞抽取
- 用訊號來控制非同步流程
- 設計 Timeline 時間軸來更精確地控制動畫
- 簡單構建 ThinkJS Vue2.0 前後端分離的多頁應用
- 冷門函式之Math.hypot
- 你還在用charCodeAt那你就out了
- 巧用 currentColor 屬性來實現自定義 checkbox 樣式
- 在什麼情況下 a === a - 1 ?