前一個章節提到的 Shader,就是負責繪製每個像素的顏色跟位置。跟 canvas 本身的原理很像,我們先決定 context 的狀態(fillColor, lineTo, stroke) 等等,最後在調用 storke
fill
等語法來繪製(在 webGL 中是 drawArrays)。shader 使用 GSGL 撰寫,一個 C/C++ like 的程式語言。webGL 的章節當中,我們會常常看到他。
一般來說,我們有四種方式可以讓 shader 接收資料:
buffers 跟 attributes
我們可以將資料放進去 buffer 當中,當 GPU 需要使用時就從 buffer 取用,通常會存放像是頂點位置、紋理、顏色等等的資料;attribute 則代表我們如何從緩衝區中獲得資料
uniforms 則是執行 program 之前,能夠設定的全域變數。
varyings 是 vertex shader 將數據傳給 fragment shader 的一種方法,根據渲染內容不同,執行 fragment shader 的時候,會根據 vertex shader 提供的值進行計算
如果看完上面的解釋,還是覺得非常迷惘,別擔心,之後的幾天我們會常常遇到這幾種資料型態,也會慢慢解釋他們的用途。
歡迎來到修羅道,以下我們會用僅僅 100 行程式碼畫出一個...點(?
gl_
開頭的變數為保留字關於 GSGL 概覽,我們只介紹到這邊,GSGL 裡頭其實有蠻豐富的內建函數,我們之後遇到範例時再一起講解。
終於可以開始寫 code 了!在這之前,先讓我們設定一下 webGL 環境。
我會將代碼放在 codepen 上,方便各位 fork 回來自己動手玩玩看!
因為自己也算是在摸索中,難免會有解釋錯誤的地方,如果各位有發現問題,歡迎在底下留言,我會趕快修正!
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>My first shader</title>
<script id="shader-fs" type="x-shader/x-fragment">
// shader program
// gl_FragColor 是個 fragment shader 的全域變數
void main(void) {
gl_FragColor = vec4(0.0,0.0,0.0,1.0);
}
</script>
<script id="shader-vs" type="x-shader/x-vertex">
// 我們在這裡撰寫 GSGL
// vec4 代表一個向量,接收四個參數(x,y,z,w)
// 0,0,0 在 3d 座標中代表中心
void main(void) {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 10.0;
}
</script>
</head>
<body>
<canvas width="600" height="600" id="canvas">
</body>
<script src="main.js"></script>
</html>
首先看到上面兩個 script
tag,待會我們就要在裡頭撰寫我們的 vertex shader
跟 fragment shader
,再來為了使用 webGL,我們使用 canvas,待會我們就會取用 webgl context,這樣一來就能夠操作 webGL API 了!
script 的 type 當中,其實並沒有特別規範名稱,這邊的 x-shader
只是慣例上的使用方式,只要能夠讓 webGL 讀取到內容就好。不過如果是使用 IDE 的話,寫入 type 的好處是 IDE 會幫你做高亮處理。
<script id="shader-fs" type="x-shader/x-fragment">
// shader program
void main(void) {
gl_FragColor = vec4(0.0,0.0,0.0,1.0);
}
</script>
<script id="shader-vs" type="x-shader/x-vertex">
// 我們在這裡撰寫 GSGL
// vec4 代表一個向量,接收四個參數(x,y,z,w)
// 0,0,0 在 3d 座標中代表中心
void main(void) {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 10.0;
}
</script>
在 fragment shader 當中,我們做了一些事情:
gl_FragColor = vec4(0.0,0.0,0.0,1.0)
宣告我們這個 fragment 的顏色為 0,0,0,第四個參數我們先暫時不理他。注意這邊規定的型別必須為浮點數,所以我們需要加入小數點。gl_FragColor 是一個全域變數,每一個 fragment 會依照 gl_FragColor 來繪圖。
gl_Position = vec4(0.0,0.0,0.0,1.0);
宣告我們的頂點在 x=0, y=0, z=0 這個位置
gl_PointSize = 10.0 代表我們點的大小為 10
注意到 gl_
開頭的變數都是保留字。
// 1. 我們先用 canvas.getContext('webgl') 來獲得 webGL context
// 2. gl.viewport 定義了視窗大小
// 3. gl.clearColor 將視窗顏色清除為 (1(R),1(G),1(B),1(A))
// 注意 1 代表 255
function initGL() {
var canvas = document.querySelector('#canvas');
var gl = canvas.getContext('webgl');
gl.viewport(0,0, canvas.width, canvas.height);
gl.clearColor(1,1,1,1);
return gl;
}
function createShaders(gl, type) {
var shaderScript = '';
var shader;
switch(type) {
case 'fragment':
shaderScript = document.querySelector('#shader-fs').textContent;
shader = gl.createShader(gl.FRAGMENT_SHADER);
break;
case 'vertex':
shaderScript = document.querySelector('#shader-vs').textContent;
shader = gl.createShader(gl.VERTEX_SHADER);
break;
}
gl.shaderSource(shader, shaderScript);
gl.compileShader(shader);
return shader;
}
function initShaders(gl) {
var vertexShader = createShaders(gl, 'vertex');
var fragmentShader = createShaders(gl, 'fragment');
var shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
var isSuccessful = gl.getProgramParameter(program, gl.LINK_STATUS);
if(!isSuccessful) {
throw Error();
}
gl.useProgram(shaderProgram);
}
// 我們畫圖的邏輯一律在 draw function 裡頭
function draw(gl) {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0,1);
}
function main() {
var gl = initGL();
initShaders(gl)
draw(gl);
}
main();
比較重要的幾個部分在 createShader 跟 initShaders 當中。我們使用 gl.createShader(gl.FRAGMENT_SHADER)
跟 gl.VERTEX_SHADER
來建立 shader 並宣告型別。同時用 gl.shaderSource(shader, shaderScript)
跟 gl.compileShader
讓 shader 能夠被 webGL 使用。
initShaders 目的則是連結 shader 並且加入 program 裡頭,步驟是 gl.attachShader
(fragment 跟 vertex shader 都要加入) > gl.linkProgram
> gl.useProgram
draw 函式裡頭,我們調用了 gl.drawArrays(mode, first, count)
來畫出圖形。
費盡千辛萬苦,我們終於將一個正方形給畫出來了,嚴格來說也不算正方形,只是一個點而已。從以上的程式當中,我們知道,要讓 webGL 正確繪圖,我們需要 fragment shader 及 vertex shader,同時我們需要用 webGL 與 shader 做連結,最後再調用 drawArrays
或 drawElements
畫出圖形。
光是畫一個簡單的圖形,我們的程式碼就已經一長串了,很難想像如果開發更複雜的圖形,我們要寫的程式碼一定會用指數型增長吧!
不過了解他們的原理還是有好處的,所以在繼續之前,我想請各位讀者到 webGL API 瞧瞧,並且熟悉一下今天出現的 API。你也可以修改 script
中的 GSGL 程式碼,看看會發生什麼事。確定熟悉了基本操作之後,我們再往下前進!
這四天有關於 webGL 的章節,會是本系列文當中最艱難的幾篇,之後就會輕鬆許多了XD,還撐得下去的讀者們加油!
gl.LINK_STATUS
gl.COMPILE_STATUS
等全域變數來看看目前的狀態是否正確,需要更多資訊,可以參考這一頁 Constant
Hi 目前正在學習 three.js ,很高興有您的文章貢獻
目前發現文中程式碼部分:
var isSuccessful = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!isSuccessful) {
throw Error();
}
「program」好像是錯誤的呢,會出現錯誤:
program is not defined
更改為:shaderProgram 就沒問題了
var isSuccessful = gl.getProgramParameter(shaderProgram, gl.LINK_STATUS);
if (!isSuccessful) {
throw Error();
}