在〈認識著色器〉中寫死了 gl_Position
等資訊,如果頂點的座標希望是由應用程式來提供,那麼使用 attribute
變數,例如:
<script id="vertex-shader" type="x-shader/x-vertex">
attribute vec3 position;
attribute float size;
void main(void) {
gl_Position = vec4(position, 1.0);
gl_PointSize = size;
}
</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
只能用在頂點著色器中,這表示應用程式會提供單個頂點相關資訊,像是座標、法向量、顏色、紋理等。
接下來做個簡單的範例,讓〈認識著色器〉中的紅點跟隨著滑鼠,若按下左鍵會縮小,右鍵會放大,不過,滑鼠的座標是左上角為 (0, 0),往右為 x 正方向,往下為 y 正方向,這與裁剪座標不同,因此先寫個座標轉換函式(公式就自己試著推導吧!):
// 從二維繪圖座標轉為三維裁剪空間座標
function toClipSpaceCord(canvas, cord) {
const half_width = canvas.width / 2;
const half_height = canvas.height / 2;
const x = cord.x / half_width - 1;
const y = -cord.y / half_height + 1;
return {x, y, z : 0}
}
想要能從應用程式傳遞頂點資訊給頂點著色器,必須先取得 attribute
變數的位置,這可以透過 WebGLRenderingContext
的 getAttribLocation
方法,從著色器程式中取得:
const gl = getGLContext(document.getElementById('glCanvas'));
const prog = installProgram(gl,
shaderSourceById('vertex-shader'),
shaderSourceById('fragment-shader')
);
const gl_position = gl.getAttribLocation(prog, 'position');
const gl_size = gl.getAttribLocation(prog, "size");
由於頂點著色器中,attribute
修飾的 position
與 size
分別是 vec3
與 float
型態,在傳遞資訊時,必須分別使用 vertexAttrib3f
與 vertexAttrib1f
方法,例如在初始畫面時,將紅點置於中心:
// 初始畫面
gl.vertexAttrib3f(attr_position, 0.0, 0.0, 0.0);
gl.vertexAttrib1f(attr_size, 5.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
接著就是滑鼠事件處理了:
// 跟隨滑鼠
gl.canvas.addEventListener('mousemove', evt => {
// 轉換座標
const cord = toClipSpaceCord(glCanvas, evt);
gl.vertexAttrib3f(attr_position, cord.x, cord.y, cord.z);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
});
// 不顯示快顯功能表
gl.canvas.addEventListener('contextmenu', evt => {
evt.preventDefault();
});
// 縮小、放大
let size = 5.0;
gl.canvas.addEventListener('mousedown', evt => {
size = evt.button === 0 ? // 左鍵
size * 0.75 :
size * 1.25;
gl.vertexAttrib1f(attr_size, size);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
});
你可以試一下範例頁面看看效果。
到這邊應該會有個問題,有關運算之類的東西,應該放在 JavaScript 裏,還是在著色器程式裏呢?例如,這邊的範例,也可以將座標轉換的運算實現在著色器裏,只是你也得更認識 GLSL 這門語言,這之後再來談。
要將運算實現在哪邊,完全取決於開發者,你可以撰寫簡單的著色器,其他都交給 JavaScript 來處理,或者是除了 HTML 頁面處理以外的全部運算,都使用著色器來實現,這會直接聯想到效能這件事!
理論上,GPU 在運算上會有比較好的效率,然而在〈Getting Started in WebGL, Part 1: Introduction to Shaders〉有談到,實際上只有 CPU 才知道怎麼渲染,GPU 還是得與 CPU 合作,亦有常見的效能瓶頸來自於溝通上的開銷,而不是運算。
另一方面,想將更多的運算交給 GPU,開發者得更熟悉 GLSL,而且 GLSL 在除錯上還蠻麻煩的,沒什麼殺手級的除錯工具,Firefox 是有個著色器編輯器,不過它說要廢棄掉了,Chrome 上是有些擴充工具,不過專案看來並不活躍。
(倒是在〈WebGL GLSL Shader Editor〉中,介紹了一些線上的編輯器,提供了一些可視化的方式,便於針對 3D 網格物件進行設計,有興趣可以自行調查一下是否合用。)
若是 JavaScript 開發者想直接接觸 WebGL,一開始建議撰寫簡單的著色器,其他由 JavaScript 來實現,不過,GLSL 本身在向量、矩陣運算上蠻友善的,若是發現 JavaScript 程式中有些通用的向量、矩陣運算,而且對相關的 GLSL 也能夠掌握,再來可以考慮實現在著色器之中。
另一方面,如果著色器摻雜越多瀏覽器的情境,就可能越侷在某些應用場合,為了讓著色器更通用,著色器中儘量只針對頂點進行處理,避免摻雜瀏覽器情境,也會是考量之一。