正交投影矩陣


到目前為止,在設置座標資訊時,總是基於裁剪空間而設置,然而這並不方便,你也許會想要自訂一個空間,例如一個 X、Y、Z 分別為 100、100、100 的空間,原點位於空間的中心,三個軸各被畫分為 100 個單位,物件的大小以每個單位來設置,然後,有個方式可以自動將你自訂的空間投影片裁剪空間。

其實在〈使用 attribute 變數〉做過類似的事,只不過那時是從 Canvas 繪圖座標投影至裁剪空間,因為當時是 2D 投影至 3D,將 Z 設為 0 罷了,現在我們來將之擴充為 3D 投影至 3D,並且可以任意設置邊界。

首先定義你的自訂空間需要的幾個值,left 是左邊界值,right 是右邊界值,top 是上邊界值,bottom 是下邊界值,near 是近面距離,far 是遠面距離:

正交投影矩陣

接下來,找出自訂空間的中心點:

midX = (left + right) / 2
midY = (top + bottom) / 2
midZ = (near + far) / 2

在自訂空間中的一點 (x, y, z),相對於空間的中心點之座標為:

x' = x - midX
y' = y - midY
z' = z - midZ

可以用矩陣的方式將之表達出來:

正交投影矩陣

為了要能對應至裁剪空間 -1.0 ~ 1.0 的範圍,必須對座標進行縮放:

裁剪空間 x = sX * x' = 2 / (right - left) * x'
裁剪空間 y = sY * y' = 2 / (top - bottom) * y'
裁剪空間 z = sZ * z' = 2 / (far - near) * z'

也就是使用矩陣表示的話會是:

正交投影矩陣

矩陣計算後,將全部算式展開結果,就可以得到正交投影(Orthographic Projection)矩陣:

正交投影矩陣

因此,可以在 mat4 中加上個正交投影矩陣的方法實作:

const mat4 = {
    ....

    ortho(left, right, top, bottom, near, far) {
        const rl = 1 / (right - left);
        const tb = 1 / (top - bottom);
        const fn = 1 / (far - near);
        return [
            2 * rl, 0, 0, 0,
            0, 2 * tb, 0, 0,
            0, 0, 2 * fn, 0,
           -(left + right) * rl, -(top + bottom) * tb, -(far + near) * fn, 1
        ];
    }
};

這個正交投影矩陣要怎麼用呢?首先,來改一下著色器:

<script id="vertex-shader" type="x-shader/x-vertex">
    uniform mat4 projection;
    uniform mat4 transformation;

    attribute vec3 position;
    attribute vec4 color;

    varying vec4 fColor;

    void main(void) {
        gl_Position = projection * transformation * vec4(position.x, position.y, position.z, 1.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>

其中有個 projection 將用來設置投影矩陣,在這邊注意到,不需要接受 aspect 了,因為投影矩陣的計算中,會包含寬高比的計算,最主要的是投影矩陣的設置:

renderer.uniformMatrix4fv('projection', 
    mat4.ortho(
        -canvas.clientWidth / 2,  // 左邊界
        canvas.clientWidth / 2,   // 右邊界
        canvas.clientHeight / 2,  // 上邊界
        -canvas.clientHeight / 2, // 下邊界
        0.1,                      // 近面
        canvas.clientWidth        // 遠面
    )
);

因為使用了 Canvas 的顯示寬高作為邊界資訊,這表示幾何物件的設置,可以用寬高作為基準,例如設置邊長 100 的立方體:

const geometry = new CubeGeometry(100);

繪製時的方式是類似的,只不過因為近面被設為 0.1,遠面被設為 Canvas 的寬度,記得將立方體移至深度為 canvas.clientWidth / 2 的位置:

let zRotation = mat4.create();

function drawCube() {
    zRotation = mat4.zRotate(zRotation, 0.025);

    let transformation = mat4.translate(zRotation, canvas.clientWidth / 8, 0, canvas.clientWidth / 2);
    transformation = mat4.xRotate(transformation, 0.5);
    transformation = mat4.yRotate(transformation, 0.5);

    renderer.uniformMatrix4fv('transformation', 
        transformation
    );

    renderer.clear();
    renderer.bindBuffer(GL.ARRAY_BUFFER, vertBuffer);
    renderer.bufferSubData(GL.ARRAY_BUFFER, 0, geometry.verteices);
    renderer.render(cube);

    requestAnimationFrame(drawCube);                
}

drawCube();

其他部份與〈後乘?右乘?〉中的範例大致相同,你可以看看範例網頁,效果是相同的。