大家好,我是西瓜,你現在看到的是 2021 iThome 鐵人賽『如何在網頁中繪製 3D 場景?從 WebGL 的基礎開始說起』系列文章的第 2 篇文章。本系列文章從 WebGL 之基礎開始介紹,最後建構出繪製 3D、光影效果之網頁。本章節講述的是 WebGL 基本的運作機制以及如何使用其提供的功能
在讓電腦繪製一個三維場景時,我們實際在做的事情把這三維場景中物體的『表面』畫在畫面上,而構成一個面最少需要三個點,三個點構成一個三角形,而所有更複雜的形狀或是表面都可以用複數個三角形做出來,因此使用 3D 繪製相關的工具時基本的單位往往是三角形,我們就來使用 WebGL 畫一個三角形吧!
在使用 WebGL 時,你寫的主程式 (.js
) 在 CPU 上跑,透過 WebGL API 對 GPU 『一個口令,一個動作』;不像是 HTML/CSS 那樣,給系統一個結構,然後系統會根據這個結構直接生成畫面。而且我們還要先告訴好 GPU 『怎麼畫』、『畫什麼』,講好之後再叫 GPU 進行『畫』這個動作
我們會把一種特定格式的程式(program)傳送到 GPU 上,在『畫』的動作時執行,這段程式稱為 shader,而且分成 vertex(頂點)及 fragment(片段)兩種 shader,vertex shader 負責計算每個形狀(通常是三角形)的每個頂點在畫布上的位置、fragment shader 負責計算填滿形狀時每個 pixel 使用的顏色,兩者組成這個所謂特定格式的程式
除了 shader 之外,還要傳送給程式(主要是 vertex shader)使用的資料,在 shader 中這些資料叫做 attribute,並且透過 buffer 來傳送到 GPU 上
首先執行 vertex shader,每執行一次產生一個頂點,且每次執行只會從 buffer 中拿出對應的片段作為 attribute,接著 GPU 會把每三個頂點組成三角形(模式是三角形的話),接著點陣化(rasterization)以對應螢幕的 pixel,最後為每個 pixel 分別執行 fragment shader
以接下來要畫的三角形為例,筆者畫了簡易的示意圖表示這個流程:
為什麼是這樣的流程其實筆者也不得而知,或許就是維基百科 openGL 頁面這邊所說的:『它是為大部分或者全部使用硬體加速而設計的』,稍微想像一下,每個頂點位置以及每個 pixel 著色的計算工作可以高度平行化,而在顯示卡硬體上可以針對這個特性使這些工作平行地在大量的 ALU / FPU 上同時計算以達到加速效果
當筆者第一次看到這個的時候,第一個反應是『原來可以在瀏覽器裡面寫 C 呀』,這個語言稱為 OpenGL Shading Language,簡稱 GLSL,雖然看起來很像 C 語言,但是不能直接當成 C 來寫,他有自己的資料格式,我們直接來看畫三角形用的 vertex shader:
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0, 1);
}
void main()
attribute vec2 a_position
是從 buffer 拿出對應的部份作為 attribute 變數 a_position
,型別 vec2
表示有兩個浮點數的 vector
vec2
gl_Position
是 GLSL 規定用來輸出在畫布上位置的變數,其型別是 vec4
-1
至 +1
才會落在畫布中,這個範圍稱為 clip spacevec4()
建構一個 vec4
,理論上應該寫成 vec4(x, y, z, w)
,因為 a_position
是 vec2
,這邊有語法糖自動展開,所以也可以寫成 vec4(a_position[0], a_position[1], 0, 1)
1
,到後面的章節再討論假設有個
vec4
的變數叫做var
,不僅可以使用var[i]
這樣的寫法取得第 i 個元素(當然,從 0 開始),還可以用var.x
/var.y
/var.z
/var.w
取得第一、第二、第三、第四個元素,甚至有種叫做 swizzling 的寫法:var.xzz
等同於vec3(var[0], var[2], var[2])
這個 shader 其實沒做什麼事,只是直接把輸入到 buffer 的位置資料放到 gl_Position
,接著是 fragment shader,這次更簡單了:
void main() {
gl_FragColor = vec4(0.4745, 0.3333, 0.2823, 1);
}
void main()
gl_FragColor
是 GLSL 規定用來輸出在畫布上顏色的變數,其型別是 vec4
0
到 1
之間的 red, green, blue, alpha為了不要讓資訊量太爆炸,我們先不要介紹更多功能,這個 fragment shader 只會輸出一種顏色,所以我們會得到的三角形是純色的
由於 shader 建立的 WebGL API 實在太繁瑣,這邊直接建立兩個 function:
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
const ok = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (ok) return shader;
console.log(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
const ok = gl.getProgramParameter(program, gl.LINK_STATUS);
if (ok) return program;
console.log(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
我們可以分別把 vertex shader, fragment shader 的 GLSL 原始碼以 template literals (backtick 字串) 寫在 .js
中,並傳給 createShader(gl, type, source)
的 source
進行『編譯』:
const vertexShaderSource = `
attribute vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0, 1);
}
`;
const fragmentShaderSource = `
void main() {
gl_FragColor = vec4(0.4745, 0.3333, 0.2823, 1);
}
`;
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
編譯完成後,使用 createProgram(gl, vertexShader, fragmentShader)
把 GPU 內『怎麼畫』的流程串起來:
const program = createProgram(gl, vertexShader, fragmentShader);
這樣一來 program
就建立完成,下一篇我們再繼續『畫什麼』的資料部份
本篇的完整程式碼可以在這邊找到:
身為初學者有點混淆,
請問a_position是自己定義的名稱還是GLSL定義的?
還有文中提到:
attribute vec2 a_position 是
「從 buffer 拿出對應的部份」
作為 attribute 變數 a_position
請問是從buffer中拿出對應「誰」的部分?會這樣問主要是小弟懂得結構不多,大概只知道JS物件會有key對應value,所以object.key就很好理解,就是從 object 拿出對應key的變數
首先感謝閱讀與提問,a_position
這個變數名稱是開發者(我們)定義的,關於
buffer中拿出對應「誰」的部分
可以參考下篇文:畫一個三角形(下),這篇有針對 buffer 與 attribute 之關聯做說明