Cocos Shader 基礎(chǔ)入門(一):WebGL 著色器與 GLSL 著色器語言基礎(chǔ)
引言:
Shader(可編程著色器)是用來實現(xiàn)圖像渲染的可編輯程序。它并不是簡單的計算機應(yīng)用,要想真正地掌握 Shader,需要對圖形學(xué)、圖形 API、GPU、顯卡等都有個宏觀概念。而對大多數(shù)游戲開發(fā)者來說,“快速”入門又是當下的迫切需求。因此,Cocos 布道師放空整理了一個 Cocos Shader 基礎(chǔ)系列教程(8期入門+2期擴展),介紹 Shader 的基礎(chǔ)知識以及在 Cocos Creator 里的應(yīng)用,帶一些小伙伴快速入門 Shader 編寫。
在 Cocos Shader 開始之前,我會先介紹一些 Shader 基礎(chǔ)知識,從最淺顯的部分開始,揭開 Shader 的神秘面紗。本次講解采用的渲染引擎是 WebGL,首先來了解一些渲染管線知識。
CPU 與 GPU 的區(qū)別
在介紹 WebGL 之前先來了解一些前置知識,也就是 CPU 和 GPU。
CPU 和 GPU 都屬于處理單元,但是結(jié)構(gòu)不同。形象來說,CPU 有點像大型的傳輸管道,等待處理的任務(wù)只能依次通過,所以 CPU 處理任務(wù)的速度取決于處理單個任務(wù)的時間。又由于 CPU 內(nèi)部結(jié)構(gòu)異常復(fù)雜,能夠處理大量的數(shù)據(jù)和邏輯判斷,所以處理一些大型任務(wù)是足夠的。但是處理圖像卻不在行,因為處理圖像的邏輯通常不復(fù)雜,但是由于一幅圖像是有成千上萬的像素點構(gòu)成,每個像素的處理都是一個任務(wù),如果由 CPU 處理,那簡直就是大材小用。因此就需要用到 GPU。GPU 由大量的小型處理單元構(gòu)成,處理能力沒 CPU 強大,但勝在數(shù)量多,并且能夠并行處理。
渲染管線
在渲染過程中需要 CPU 和 GPU 之間的通力合作。CPU 如同進貨的卡車不斷地將要處理的數(shù)據(jù)丟給 GPU,GPU 工廠調(diào)動一個個如工人一般的計算單元對這些數(shù)據(jù)進行簡單的處理,最后組裝出產(chǎn)品——圖像。
什么是 WebGL?
WebGL 是一種 3D 繪圖標準,它的本質(zhì)是 JavaScript 操作 OpenGL 接口,所以 WebGL 是在 OpenGL 的基礎(chǔ)上做了一層封裝,底層本質(zhì)還是 OpenGL。利用 WebGL 可以根據(jù)你的代碼繪制出點、線和三角形。任何復(fù)雜的場景都可以組合使用點、線和三角形實現(xiàn)。WebGL 運行在 GPU 中,因此需要使用能夠在 GPU 上運行的程序。這樣的程序需要成對提供,每對方法中都包含一個頂點著色器和一個片斷著色器,并且使用 GLSL 語言(GL 著色語言)編寫。每對組合起來稱作一個 program(著色程序)。
在 WebGL 中,任何事物都處于 3D 空間中,但最終輸出呈現(xiàn)給觀眾的往往是屏幕或者窗口這種 2D 像素,因此,在渲染引擎底層大部分工作都是把 3D 坐標轉(zhuǎn)變成適應(yīng)屏幕的 2D 像素。3D 坐標轉(zhuǎn) 2D 的處理過程是由 WebGL 的圖形渲染管線處理,它的主要傳輸流程分為兩步:
將 3D 坐標轉(zhuǎn)換成 2D 坐標
把 2D 坐標轉(zhuǎn)變成實際有顏色的像素
這兩個流程又被劃分為幾個階段處理,每個階段都會把前一個階段的輸出作為輸入。

如上圖所見,圖形渲染管線包含多個階段,在轉(zhuǎn)換頂點數(shù)據(jù)到最終像素的這個過程中,每個階段都將處理各自的職責(zé)部分。接下來,簡單介紹管線各個階段的功能:
頂點數(shù)據(jù):頂點數(shù)據(jù)用來為后面的頂點著色器等階段提供處理的數(shù)據(jù),是渲染管線的數(shù)據(jù)主要來源。送入到渲染管線的數(shù)據(jù)包括頂點坐標、紋理坐標、頂點法線和頂點顏色等頂點屬性,WebGL 根據(jù)渲染指令傳遞對應(yīng)的圖元信息(常見圖元包括點、線和面)。
頂點著色器:頂點著色器的主要功能是坐標轉(zhuǎn)換。把一個單獨的頂點作為輸入,并對頂點進行從局部坐標到裁剪坐標的變換,其實就是將游戲里操作的 3D 坐標轉(zhuǎn)換成另一種 3D 坐標。

圖元裝配:圖元裝配階段將頂點著色器輸出的所有頂點作為輸入,根據(jù)指定的指令(點、線或面)將所有的點裝配成指定圖元的形狀。例如:提供兩個頂點時,是否要將兩個頂點連接成一條線段,以及多條線段之間是否需要連接。

光柵化:如何使用頂點和裝配的方式將矩形表示在屏幕上,就是光柵化的過程。遍歷所有的像素為止,依次判斷它們是否落入了組裝的圖形內(nèi),如果在圖形內(nèi),則對該像素進行下一步操作(著色)。還會對非頂點的位置進行插值處理,賦予每個像素其他的信息。
片段著色器:片段著色器主要是對圖形內(nèi)的片元進行著色處理,這里也是高級效果產(chǎn)生的階段。通常,片段著色器包含 3D 場景的數(shù)據(jù)(比如光照、陰影、光的顏色等等),這些數(shù)據(jù)可以被用來計算最終像素的顏色。
測試與混合:在所有對應(yīng)顏色值確定以后,最終的對象將會被傳到最后一個階段,叫做 Alpha 測試與混合階段。這個階段檢測片段的對應(yīng)的深度,用以判斷這個像素在其它物體的前面還是后面、決定是否應(yīng)該丟棄。這個階段也會檢查透明度 alpha 值,并對物體進行混合。所以,即使在片段著色器中計算出了一個像素輸出的顏色,在渲染多個三角形的時候最后的像素顏色也可能完全不同。
可以看出,圖形渲染管線非常復(fù)雜,簡單總結(jié)一下:WebGL 在 GPU 上的工作基本上分為兩部分,第一部分是將頂點數(shù)據(jù)轉(zhuǎn)換到裁剪空間坐標, 第二部分是基于第一部分的結(jié)果繪制像素點。接下來,我們一步步實踐,加深對這塊的理解。
GLSL 語言基礎(chǔ)
GLSL 是為圖形計算量身定制的,它包含一些針對向量和矩陣操作的特性。一個著色器通常包含輸入輸出變量、uniform 和 main 函數(shù)。每個著色器的入口都是 main 函數(shù),在 main 函數(shù)中處理所有輸入變量,并將結(jié)果輸出到輸出變量中。
一個常見頂點/片段著色器如下:
// 頂點著色器
// 輸入屬性 attribute 變量類型 vec4 變量名 a_position
attribute vec4 a_position;
attribute vec2 a_uv;
attribute vec4 a_color;
// 輸入輸出屬性
varying vec4 v_color;
varying vec2 v_uv;
// 每一個著色器都有 main 函數(shù),這是與 WebGL 對接的接口
void main() {
// 基礎(chǔ)賦值語句
v_color = a_color;
v_uv = a_uv;
// 內(nèi)置變量 gl_Position
gl_Position = a_position;
}
// 片段著色器
// lowp 精度
varying lowp vec4 v_color;
varying highp vec2 v_uv;
uniform sampler2D mainTexture;
void main(void) {
vec4 o = texture2D(mainTexture, v_uv);
o *= v_color
gl_FragColor = o;
}
此處會列舉一些變量、修飾符和常見用法,更多使用方式可以參考下方的 GLSL 詳解(基礎(chǔ)篇),里面的內(nèi)容寫的還是很詳細的,當然,有條件的同學(xué)可以直接上 GLSL 官網(wǎng)文檔查看。
1
變量和變量類型

常用的幾種使用方式如下:
// 標量
float myFloat = 1.0;
bool myBool = bool(myFloat); // float -> bool
// 向量
vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0}
vec2 myVec2 = vec2(1.0, 0.5); // myVec2 = {1.0, 0.5}
vec2 temp = vec2(myVec2); // temp = {1.0, 0.5}
myVec4 = vec4(myVec2, temp, 0.0);
// 向量和矩陣的計算是逐分量進行的,因此,也可以采用下面這兩個個方法
vec3 myVec3a = myVec2.xyx; // 通過分量訪問符 myVec3a = {1.0, 0.5, 1.0}
vec3 myVec3b = vec3(myVec2[0], myVec2[1], myVec2[2]); // 通過數(shù)組 myVec3b = {1.0, 0.5, 1.0}
// 矩陣
mat3 myMat3 = mat3(1.0, 0.0, 0.0, // 第一列
0.0, 1.0, 0.0, // 第二列
0.0, 1.0, 1.0); // 第三列
mat4 myMat4 = mat4(1.0) // myMat4 = {1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0}
// 向量、標量和矩陣的交互使用
vec3 v, u;
float f;
v = u + f;
// 等價于 v.x = u.x + f;
// v.y = u.y + f;
// v.z = u.z + f;
mat3 m;
u = v * m;
// 等價于 u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
// u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
// u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;
2
限定符
2.1 存儲限定符

使用方法:
attribute vec4 a_position;
varying vec4 v_color;
2.2 參數(shù)限定符

vec4 myFunc(inout float myFloat, // 輸入輸出參數(shù)
out vec4 myVec4, // 輸出
mat4 myMat4); // 輸入?yún)?shù)
2.3 精度限定符

使用方法:
// 預(yù)先聲明
precision highp float;
precision mediump int;
// 指定變量聲明
varying lowp vec4 v_color;
狀態(tài)機
在 WebGL 上,大多數(shù)元素都可以用狀態(tài)來描述,比如:是否啟用了光照、是否啟用了紋理、是否啟用了混合、是否啟用了深度測試等。通常 WebGL 會執(zhí)行默認狀態(tài),除非我們調(diào)用相關(guān)接口改變它,比如:gl.Enablexxx 或者 gl.Disablexxx。
上下文(Context)
WebGL 需要依賴 canvas 這個載體獲取對應(yīng)的繪圖上下文,通過繪圖上下文調(diào)用相對應(yīng)的繪圖 API,包括上面提到的各種狀態(tài)切換。每一個對象的繪制,都需要先設(shè)置一系列狀態(tài)值,然后通過調(diào)用 "gl.drawArrays" 或 "gl.drawElements" 運行一個著色方法對,使得你的著色器對能夠在 GPU 上運行。WebGL 渲染上下文創(chuàng)建如下:
// 定義一個 canvas 元素
var gl = canvas.getContext("webgl");
if(!gl){
// 你不能使用 WebGL
}
本章到此主要介紹一些 WebGL 的基礎(chǔ)知識,包括渲染管線流程、渲染使用語言等。下一章開始介紹繪制流程,重點內(nèi)容包括頂點著色器和片元著色器的作用。
內(nèi)容參考:
1. WebGL 基礎(chǔ):
https://webglfundamentals.org/webgl/lessons/zh_cn/webgl-fundamentals.html
2. WebGL API 對照表:
https://www.khronos.org/files/webgl/webgl-reference-card-1_0.pdf
3. OpenGL 中文文檔:
https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
4. GLSL 詳解:
https://colin1994.github.io/2017/11/11/OpenGLES-Lesson04/
5. 細說圖形渲染管線:
https://zhuanlan.zhihu.com/p/79183044
往期精彩



