WebGL學習之從著色器入門
Intro
不論是WebGL、OpenGL、OpenGL ES、Vulkan、DirectX,這些聽起來就十分“底層”、“高效能”、“難寫”的東西似乎是和我一個後端開發都沒什麼關係。(遠處傳來聲音:別tm擅自改臺詞!)
咳,迴歸正題。
我看了好多的 OpenGL 入門書,固定管線的比較好懂,但過時已久。新的可程式設計管線則十分晦澀——沒有人試圖解釋一個繪製三角形的簡單 OpenGL 程式有哪些細節,紅寶書也是,上手便是一個"Framework",面對大片幾十上百行程式碼,試圖理解的人總會遇到各種“這個常量是什麼意思?glBind是在幹嘛?三角形是怎麼畫出來的?”
實在太魔術了。
所以本博文試圖從一個更全域性的角度,去解釋一個最簡單的 畫三角形 WebGL 程式都做了什麼。
零、縱觀全域性
一個OpenGL的Hello World無非是做了這些事情。
- 建立shader
- 編譯shader
- 連結shader為program
- 建立/初始化頂點buffer
- 將頂點buffer傳遞給頂點著色器,完成渲染
一、著色器
著色器是一段在GPU上執行的程式,它不是指令碼,有使用過編譯語言的同學應該明白,C要編譯成一個exe需要經過編譯、連結這兩個步驟,同樣的,著色器程式也需要這兩個步驟。
對於單個著色器我們需要先使用gl.compileShader(shader)
gl.getError
來取得。十分老派的posix/unix/c風格錯誤處理api,不是嗎。
著色器編譯不會帶來任何可見的改變,我們持有的shader
物件本質上是一個指向黑箱的索引,編譯好shader之後我們使用gl.createProgram
、gl.attachShader
和gl.linkProgram
來創造一個可用的著色器程式,就像是我們把一段c程式碼編譯成了可以在gpu上跑的exe。
看一個示例
const vs = gl.createShader(gl.VERTEX_SHADER); // 頂點著色器 const fs = gl.createShader(gl.FRAGMENT_SHADER); // 片元著色器 gl.shaderSource(vs, `... 頂點著色器程式碼略`); // 指定 shader 原始碼 gl.shaderSource(fs, `... 片元著色器程式碼略`); // 指定 shader 原始碼 gl.compileShader(vs); // 編譯頂點著色器 if(!gl.getShaderParameter(vs, gl.COMPILE_STATUS)) console.log(`頂點著色器編譯錯誤:${gl.getShaderInfoLog(vs)}`); gl.compileShader(fs); // 編譯片元著色器 if(!gl.getShaderParameter(fs, gl.COMPILE_STATUS)) console.log(`片元著色器編譯錯誤:${gl.getShaderInfoLog(fs)}`); const program = gl.createProgram(); // 建立一個著色器程式 gl.attachShader(program, vs); // 指定要連結的 頂點著色器 gl.attachShader(program, fs); // 指定要連結的 片元著色器 gl.linkProgram(program); // 連結著色器程式 if (!gl.getProgramParameter(program, gl.LINK_STATUS)) console.log(`著色器程式連結失敗:${gl.getProgramInfoLog(program)}`);
大多教程都封裝了這個載入、建立著色器程式的細節,通常來說像是叫做createShader
、loadShader
的做的事情和以上程式碼做的差不多。
GLSL
glsl
是OpenGL的著色器語言,可以理解為和C語言類似的編譯型語言,不過因為是在GPU上跑的緣故,所以有諸多限制,比如說不能遞迴。
從一個簡單的頂點著色器說起。
#version 120
attribute vec4 position;
void main() {
gl_Position = position;
}
上面這個頂點著色器和一個普通C程式類似,但有三個差異點。
差異一:#version
#version 120
指定GLSL
的版本,GLSL
版本和OpenGL
版本之間有個對照表。因為OpenGL細節是由驅動實現的,驅動支援到哪個版本的OpenGL
,GLSL
最多也就是跟進到那個版本而已。
差異二:attribute
attribute
是只能在頂點著色器裡,去宣告一個可以被GLSL外的環境修改的變數,由於現在說的是WebGL,所以這個_GLSL外的環境_指的就是js了。
為attribute
賦值的方式是通過兩個api:gl.getAttribLocation
和gl.vertexAttribPointer
。
其中gl.getAttribLocation
可以獲得指定attribute
名字的位置——用函式引數打比方的話,就是這個attribute
是第幾個引數。
const posAttribLocation = gl.getAttribLocation(program, "position");
有了這個posAttribLocation
,我們就能用gl.vertexAttribPointer
來賦值資料了。
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Flaot32Buffer([
0.75, 0.75, 0, 1.0,
0.75, -0.75, 0, 1.0,
-0.75, -0.75, 0, 1.0,
]), gl.STATIC_DRAW);
gl.vertexAttribPointer(posAttribLocation, 4, gl.FLOAT, false, 0, 0);
還記得OpenGL是狀態機模型吧?
gl.vertexAttribPointer
之所以不需要傳給他要用哪個buffer
,是因為我們前面先做了一個gl.bindBuffer
。
差異三:gl_Position
按照網上解釋,gl_Position是當前在處理的那個頂點在處理結束之後的位置,是一個GLSL
內建的變數,它的存在可以用C裡面extern vec4 gl_Position
的宣告來類比。不過在GLSL
裡,gl_Position
是不需要宣告的。