至今為止的範例,片段著色器的 gl_FragColor
都是寫死的,然而 attribute
只能用在頂點著色器中,若想自行指定顏色資訊,應當如何傳遞給片段著色器?
著色器的設計,基本上應該只針對各個頂點與像素,不能直接指定資訊給片段著色器,是可以理解的,想要傳遞給片段著色器的資訊,可以透過 varying
變數,它是頂點著色器的輸出,片段著色器的輸入。
最單純的做法是,將顏色資訊指定給頂點著色器,然後透過 varying
傳遞至片段著色器,例如:
<script id="vertex-shader" type="x-shader/x-vertex">
uniform float aspect;
attribute vec3 position;
attribute vec4 color;
varying vec4 fColor;
void main(void) {
gl_Position = vec4(position.x, position.y * aspect, position.z, 1.0);
gl_PointSize = 5.0;
fColor = color;
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
varying vec4 fColor;
void main(void) {
gl_FragColor = fColor;
}
</script>
在頂點著色器中,使用 varying
宣告了 fColor
變數,color
會接受 JavaScript 指定的資訊,並在執行時指定給 fColor
。
頂點著色器會有預設的精度 highp
,然而片段著色器沒有預設精度,因此在片段著色器必須指定精度,precision mediump float
的指定方式會適用整個片段著色器,也可以使用 varying mediump vec4 fColor
的方式個別指定,可以指定的精度有 highp
、mediump
與 lowp
,越高的精度負擔越大,在某些效能不好的行動裝置上可能會有問題,然而低精度可能有損繪製的效果呈現,通常 mediump
是個風險較低的權衡做法。
片段著色器的 varying
變數與頂點著色器的 varying
名稱相同,這表示頂點著色器中被指定的值,會傳遞到片段著色器中的同名變數。
來寫個隨機畫彩色線的例子作為示範,先建立頂點以及顏色資訊:
// 頂點數
const n = 20;
const verteices = [];
for(let i = 0; i < 3 * n; i += 3) {
verteices[i] = Math.random() - 0.5; // x
verteices[i + 1] = Math.random() - 0.5; // y
verteices[i + 2] = Math.random() - 0.5; // z
}
// 頂點 Buffer
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verteices), gl.STATIC_DRAW);
const colors = [];
for(let i = 0; i < 4 * n; i += 4) {
colors[i] = Math.random(); // R
colors[i + 1] = Math.random(); // G
colors[i + 2] = Math.random(); // B
colors[i + 3] = Math.random(); // Alpha
}
// 顏色 Buffer
const colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
顏色的資訊是隨機產生的,接下來,指定頂點與顏色等資訊給各個 attribute
變數並繪製:
// 指定給各 attribute
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
const attr_position = gl.getAttribLocation(prog, 'position');
gl.vertexAttribPointer(attr_position, 3, gl.FLOAT, false, 0 , 0);
gl.enableVertexAttribArray(attr_position);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
const attr_color = gl.getAttribLocation(prog, 'color');
gl.vertexAttribPointer(attr_color, 4, gl.FLOAT, false, 0 , 0);
gl.enableVertexAttribArray(attr_color);
gl.uniform1f(
gl.getUniformLocation(prog, 'aspect'),
gl.canvas.clientWidth / gl.canvas.clientHeight
);
// 繪製
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.LINES, 0, 20);
可以按一下範例網頁來看看效果,這邊也擷個圖來展示一下:
varying
之所為 varying,是因為在片段著色器中,varying
變數的值是會變化的,也就是片段著色器會自動根據兩個頂點座標處指定的 varying
變數值,為每個像素計算出插值來執行演算內容,最後指定給 gl_FragColor
,這也就是為何你會看到有漸層色的線段。
另一種常見的著色方式,是根據頂點資訊來做些顏色資訊的演算,例如,來個正四面體旋轉:
正四面體的四個面都是正三角,只不過,該怎麼計算頂點呢?拿出三角函數是一個方式,不過,其實正四面體可以連接正立方體的四個頂點來畫出來:
那麼要用 drawArray
還是 drawElement
呢?也就是,該用無索引頂點還是搭配頂點索引陣列呢?對正四面體來說都可以!這邊先示範使用無索引頂點,也就是使用 drawArray
的方式,正四面體可以有共用邊,將之展開的話就可以清楚看出:
這邊打算讓正四面體的四個面呈現漸層色,漸層的資訊是根據頂點而來:
<script id="vertex-shader" type="x-shader/x-vertex">
uniform float aspect;
attribute vec3 position;
varying vec3 vPosition;
void main(void) {
gl_Position = vec4(position.x, position.y * aspect, position.z, 1.0);
vPosition = position;
}
</script>
<script id="fragment-shader" type="x-shader/x-fragment">
precision mediump float;
varying vec3 vPosition;
void main(void) {
gl_FragColor = vec4((vPosition + vec3(1.0, 1.0, 1.0)) * 0.5, 1.0);
}
</script>
由於頂點會是 -1.0 ~ 1.0,然而顏色值會是 0.0 ~ 1.0,vPosition
會先轉換為 0.0 ~ 1.0 的值,加上 alpha 再指定給 gl_FragColor
。
在 JavaScript 的部份,就只要指定頂點等資訊:
// 正四面體
const n = 0.25;
const verteices = [
n, -n, -n,
-n, -n, n,
n, n, n,
-n, n, -n,
n, -n, -n,
-n, -n, n
];
rotateXY(verteices, Math.PI / 3, 0);
// 頂點 Buffer
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verteices), gl.DYNAMIC_DRAW);
const attr_position = gl.getAttribLocation(prog, 'position');
gl.vertexAttribPointer(attr_position, 3, gl.FLOAT, false, 0 , 0);
gl.enableVertexAttribArray(attr_position);
gl.uniform1f(
gl.getUniformLocation(prog, 'aspect'),
gl.canvas.clientWidth / gl.canvas.clientHeight
);
接下來就是繪製的部份,結合了滑鼠事件以及 requestAnimationFrame
,因為繪製時會有共用邊,因此 drawArrays
時搭配的是 gl.TRIANGLE_STRIP
:
let animation = false;
function drawTetrahedron() {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 6);
rotateXY(verteices, 0, 0.025);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(verteices));
if(animation) {
requestAnimationFrame(drawTetrahedron);
}
}
gl.canvas.addEventListener('mousedown', () => {
animation = !animation;
if(animation) {
drawTetrahedron();
}
});
drawTetrahedron();
你可以按一下範例網頁看看效果,看起來好像不錯,不過其實並不是正確的繪製結果,仔細看的話,會發現應該是看不見的背面也被繪製了,這有兩種方式可以解決,一是啟用深度測試讓較深的點不會繪製,二是啟用面剔除(Face culling),不繪製背面,這就留待下一篇來說明。