在〈使用 attribute 變數〉中,每次只傳遞一個向量或浮點數,如果有多個頂點要繪製呢?這時可以透過 Buffer,使用 JavaScript 將資料放入 Buffer,然後著色器從 Buffer 取得資料。
來看看怎麼畫兩個點好了,這只需要用到簡單的著色器:
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec3 position;
void main(void) {
gl_Position = vec4(position, 1.0);
gl_PointSize = 5.0;
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
void main(void) {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
</script>
在這邊只有一個 attribute
,頂點著色器會從 Buffer 中逐一取出頂點指定給 position
,然後執行 main
;JavaScript 的部份,使用陣列作為 Buffer,例如:
import {getGLContext, shaderSourceById, installProgram} from './js/gl-comm-1.js';
const gl = getGLContext(document.getElementById('glCanvas'));
const prog = installProgram(gl,
shaderSourceById('vertex-shader'),
shaderSourceById('fragment-shader')
);
// 兩個頂點
const verteices = [
0.5, 0.0, 0.0,
-0.5, 0.0, 0.0
];
// 建立、指定、綁定 Buffer
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verteices), gl.STATIC_DRAW);
在這邊可以看到,因為 getGLContext
、shaderSourceById
、installProgram
經常使用,就放在模組然後匯入了,createBuffer
用來建立 Buffer,bindBuffer
用來指定 gl
接下來的操作會是針對哪個 Buffer,bufferData
會在被綁定的 Buffer 中將指定的資料置入。
因為這邊不做動畫,指定了 gl.STATIC_DRAW
,這是個最佳化提示,表示會經常使用 Buffer,然而不常去變動它,在 MDN 的 bufferData
文件中,可以查看到其他提示,像是 gl.DYNAMIC_DRAW
、gl.STREAM_DRAW
等。
接下來可以指定 position
來取用這個 Buffer:
const attr_position = gl.getAttribLocation(prog, 'position');
gl.vertexAttribPointer(attr_position, 3, gl.FLOAT, false, 0 , 0);
gl.enableVertexAttribArray(attr_position);
vertexAttribPointer
的參數中 3 與 g.FLOAT
表示,每次從 Buffer 中取用三個單位的資料,型態是浮點數;false
是 normalized
指定,簡單來說,若設定為 true
,會把整數轉換為 -1 ~ 1 的值,因為這邊設定為 g.FLOAT
了,這個參數基本上就是 false
(這時設成 true
也沒作用);下一個 0 是 stride
,用來指定每單位資料是幾個位元組,被設成 0 的話,就表示使用指定的型態;最後一個 0 表示從 Buffer 哪裏開始讀取,這邊是從陣列開頭開始。
接下來就可以畫兩個點了:
let mode = gl.POINTS;
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(mode, 0, 2);
mode
如果改成 gl.LINES
的話,就會每兩個點連接起來,繪製出一條線來,例如,希望按下滑鼠時可以切換點與線的繪製:
gl.canvas.addEventListener('mousedown', () => {
mode = mode === gl.POINTS ? gl.LINES : gl.POINTS;
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(mode, 0, 2);
});
可以點一下範例網頁看看效果,因為會有兩個頂點,頂點著色器會被呼叫兩次,然而卻畫出了一條線,這是片段著色器的功勞,它會自動根據兩個頂點座標處指定的 varying
變數值,計算出插值來執行演算內容,最後指定給 gl_FragColor
,這個過程叫柵格化(Rasterisation),就目前來說,並沒有用到 varying
變數,片段著色器的顏色是寫死的,因此兩個點間的像素顏色也就固定了,之後會看到,頂點著色器如何使用 varying
變數傳遞資訊給片段著色器。
除了 gl.POINTS
、g.LINES
之外,還有 gl.LINE_STRIP
依序連接每個點,gl.LINE_LOOP
依序連接每個點外,最後一個點還會連接第一個點,在上頭的範例因為只有兩個點,設成這兩個效果都一樣,來看看正三角形的繪製好了:
// 正三角頂點
const verteices = [
0.25, 0.0, 0.0,
0.0, 0.433, 0.0,
-0.25, 0.0, 0.0
];
// 建立、指定、綁定 Buffer
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verteices), gl.STATIC_DRAW);
const attr_position = gl.getAttribLocation(prog, 'position');
gl.vertexAttribPointer(attr_position, 3, gl.FLOAT, false, 0 , 0);
gl.enableVertexAttribArray(attr_position);
// 繪製
let i = 0;
let modes = [gl.LINE_STRIP, gl.LINE_LOOP, gl.TRIANGLES];
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(modes[i], 0, 3);
gl.canvas.addEventListener('mousedown', () => {
i = (i + 1) % 3;
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(modes[i], 0, 3);
});
按下滑鼠的話,會依序展示 gl.LINE_STRIP
、gl.LINE_LOOP
與 gl.TRIANGLES
的效果,gl.TRIANGLES
是每次取三個點,畫出一個三角形,你可以先點一下範例網頁看看效果,嗯?不是要畫正三角嗎?你看到的不是正三角?只是個等腰三角?頂點設錯了嗎?
如果單看 verteices
中的三個頂點值,確實是正三角沒錯,不過別忘了,在〈認識著色器〉談到:
如果要將 Canvas 的顯示空間對應至裁剪空間,那麼通常可以將 Canvas 顯示空間最左邊對應至裁剪空間的 x 的 -1.0,最右邊對應至 1.0,顯示空間最下方對應至裁剪空間 y 的 -1.0,最上方對應至 1.0。
這表示,如果你的 Canvas 寬高並不是一比一的話,畫出來的東西是會變形的,若不想如此,必須依 Canvas 寬高比例做出修正:
<script id="vertex-shader" type="x-shader/x-vertex">
uniform float aspect;
attribute vec3 position;
void main(void) {
gl_Position = vec4(position.x, position.y * aspect, position.z, 1.0);
gl_PointSize = 5.0;
}
</script>
這邊看到了 uniform
修飾,它表示在繪製過程中,aspect
的值是固定的,配合這個變數,JavaScript 的部份加入變數設定:
gl.uniform1f(
gl.getUniformLocation(prog, 'aspect'),
gl.canvas.clientWidth / gl.canvas.clientHeight
);
要取得 uniform
變數的位置,必須使用 getUniformLocation
方法,而設定值時,使用的是 uniform1f
這類開頭為 uniform 的方法,點一下範例網頁看看效果,應該可以看到正三角了。
來順便看一下 gl.TRIANGLE_STRIP
與 gl.TRIANGLE_FAN
好了。gl.TRIANGLE_STRIP
可以共用頂點,從第三個頂點開始,每個頂點與前兩個頂點連成三角形,也就是後續的三角形,會使用前兩個頂點形成的邊作為共用邊。例如:
const verteices = [
0.25, 0.0, 0.0,
0.0, 0.433, 0.0,
-0.25, 0.0, 0.0,
-0.5, 0.433, 0.0
];
...略
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
看一下範例網頁,你會看到兩個正三角形成的平行四邊形,下圖列出了頂點關係:
至於 gl.TRIANGLE_FAN
,正如其名稱所示,適合繪製扇形,因為它從第三個頂點開始,都會與第一個和上個頂點相連,例如:
// 圓的頂點
const r = 0.25;
const fn = 24;
const verteices = [0.0, 0.0, 0.0];
for(let i = 0, step = 2 * Math.PI / fn; i < fn; i++) {
let a = step * i;
verteices.push(r * Math.cos(a));
verteices.push(r * Math.sin(a));
verteices.push(0.0);
}
verteices.push(r);
verteices.push(0.0);
verteices.push(0.0);
...
// 繪製
let i = 0;
gl.canvas.addEventListener('mousedown', () => {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_FAN, 0, 3 + i);
i = (i + 1) % fn;
});
看一下範例網頁,每點一次就會增加扇形的面積,直到畫出一個圓。