varying 傳遞著色資訊


至今為止的範例,片段著色器的 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 的方式個別指定,可以指定的精度有 highpmediumplowp,越高的精度負擔越大,在某些效能不好的行動裝置上可能會有問題,然而低精度可能有損繪製的效果呈現,通常 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 變數的值是會變化的,也就是片段著色器會自動根據兩個頂點座標處指定的 varying 變數值,為每個像素計算出插值來執行演算內容,最後指定給 gl_FragColor,這也就是為何你會看到有漸層色的線段。

另一種常見的著色方式,是根據頂點資訊來做些顏色資訊的演算,例如,來個正四面體旋轉:

varying 傳遞著色資訊

正四面體的四個面都是正三角,只不過,該怎麼計算頂點呢?拿出三角函數是一個方式,不過,其實正四面體可以連接正立方體的四個頂點來畫出來:

varying 傳遞著色資訊

那麼要用 drawArray 還是 drawElement 呢?也就是,該用無索引頂點還是搭配頂點索引陣列呢?對正四面體來說都可以!這邊先示範使用無索引頂點,也就是使用 drawArray 的方式,正四面體可以有共用邊,將之展開的話就可以清楚看出:

varying 傳遞著色資訊

這邊打算讓正四面體的四個面呈現漸層色,漸層的資訊是根據頂點而來:

<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),不繪製背面,這就留待下一篇來說明。