three.js中的矩陣變換(模型視圖投影變換)

1. 概述

我在《WebGL簡易教程(五):圖形變換(模型、視圖、投影變換)》這篇博文裏詳細講解了OpenGL\WebGL關於繪製場景的圖形變換過程,並推導了相應的模型變換矩陣、視圖變換矩陣以及投影變換矩陣。這裏我就通過three.js這個圖形引擎,驗證一下其推導是否正確,順便學習下three.js是如何進行圖形變換的。

2. 基本變換

2.1. 矩陣運算

three.js已經提供了向量類和矩陣類,定義並且查看一個4階矩陣類:

var m = new THREE.Matrix4();
m.set(11, 12, 13, 14,
    21, 22, 23, 24,
    31, 32, 33, 34,
    41, 42, 43, 44);
console.log(m);

輸出結果:
imglink1

說明THREE.Matrix4內部是列主序存儲的,而我們理論描述的矩陣都爲行主序。

2.2. 模型變換矩陣

在場景中新建一個平面:

// create the ground plane
var planeGeometry = new THREE.PlaneGeometry(60, 20);
var planeMaterial = new THREE.MeshBasicMaterial({
    color: 0xAAAAAA
});
var plane = new THREE.Mesh(planeGeometry, planeMaterial);

// add the plane to the scene
scene.add(plane);

three.js中場景節點的基類都是Object3D,Object3D包含了3種矩陣對象:

  1. Object3D.matrix: 相對於其父對象的局部模型變換矩陣。
  2. Object3D.matrixWorld: 對象的全局模型變換矩陣。如果對象沒有父對象,則與Object3D.matrix相同。
  3. Object3D.modelViewMatrix: 表示對象相對於相機座標系的變換。也就是matrixWorld左乘相機的matrixWorldInverse。

2.2.1. 平移矩陣

平移這個mesh:

plane.position.set(15, 8, -10);

根據推導得到平移矩陣爲:

\[\left[ \begin{matrix} 1 & 0 & 0 & Tx\\ 0 & 1 & 0 & Ty\\ 0 & 0 & 1 & Tz\\ 0 & 0 & 0 & 1 \end{matrix} \right] \]

輸出這個Mesh:
imglink2

2.2.2. 旋轉矩陣

2.2.2.1. 繞X軸旋轉矩陣

繞X軸旋轉:

plane.rotation.x = THREE.Math.degToRad(30);

對應的旋轉矩陣:

\[\left[ \begin{matrix} 1 & 0 & 0 & 0\\ 0 & cosβ & -sinβ & 0\\ 0 & sinβ & cosβ & 0\\ 0 & 0 & 0 & 1 \end{matrix} \right] \]

輸出信息:
imglink3

2.2.2.2. 繞Y軸旋轉矩陣

繞Y軸旋轉:

plane.rotation.y = THREE.Math.degToRad(30);

對應的旋轉矩陣:

\[\left[ \begin{matrix} cosβ & 0 & sinβ & 0\\ 0 & 1 & 0 & 0\\ -sinβ & 0 & cosβ & 0\\ 0 & 0 & 0 & 1 \end{matrix} \right] \]

輸出信息:
imglink4

2.2.2.3. 繞Z軸旋轉矩陣

繞Z軸旋轉:

plane.rotation.z = THREE.Math.degToRad(30);

對應的旋轉矩陣:

\[\left[ \begin{matrix} cosβ & -sinβ & 0 & 0\\ sinβ & cosβ & 0 & 0\\ 0 & 0 & 1 & 0\\ 0 & 0 & 0 & 1 \end{matrix} \right] \]

輸出信息:
imglink5

2.3. 投影變換矩陣

在場景中新建一個Camera:

var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

這裏創建了一個透視投影的相機,一般建立的都是對稱的透視投影,推導的透視投影矩陣爲:

\[P= \left[ \begin{matrix} \frac{1}{aspect*tan⁡(\frac{fovy}{2})} & 0 & 0 & 0 \\ 0 & \frac{1}{tan⁡(\frac{fovy}{2})} & 0 & 0 \\ 0 & 0 & \frac{f+n}{n-f} & \frac{2fn}{n-f} \\ 0 & 0 & -1 & 0 \\ \end{matrix} \right] \]

爲了驗證其推導是否正確,輸出這個camera,查看projectionMatrix,也就是透視投影矩陣:
imglink6

2.4. 視圖變換矩陣

通過Camera可以設置視圖矩陣:

camera.position.set(0, 0, 100);   //相機的位置
camera.up.set(0, 1, 0);         //相機以哪個方向爲上方
camera.lookAt(new THREE.Vector3(1, 2, 3));          //相機看向哪個座標

根據《WebGL簡易教程(五):圖形變換(模型、視圖、投影變換)》中的描述,可以通過three.js的矩陣運算來推導其視圖矩陣:

var eye = new THREE.Vector3(0, 0, 100);
var up = new THREE.Vector3(0, 1, 0);
var at = new THREE.Vector3(1, 2, 3);

var N = new THREE.Vector3();
N.subVectors(eye, at); 
N.normalize();
var U = new THREE.Vector3();
U.crossVectors(up, N);
U.normalize();
var V = new THREE.Vector3();
V.crossVectors(N, U);
V.normalize();

var R = new THREE.Matrix4();
R.set(U.x, U.y, U.z, 0,
    V.x, V.y, V.z, 0,
    N.x, N.y, N.z, 0,
    0, 0, 0, 1);  

var T = new THREE.Matrix4(); 
T.set(1, 0, 0, -eye.x,
    0, 1, 0, -eye.y,
    0, 0, 1, -eye.z,
    0, 0, 0, 1);  

var V = new THREE.Matrix4();
V.multiplyMatrices(R, T);   
console.log(V); 

其推導公式如下:

\[V=R^{-1} T^{-1}= \left[ \begin{matrix} Ux & Uy & Uz & 0 \\ Vx & Vy & Vz & 0 \\ Nx & Ny & Nz & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] * \left[ \begin{matrix} 1 & 0 & 0 & -Tx \\ 0 & 1 & 0 & -Ty\\ 0 & 0 & 1 & -Tz\\ 0 & 0 & 0 & 1\\ \end{matrix} \right] = \left[ \begin{matrix} Ux & Uy & Uz & -U·T \\ Vx & Vy & Vz & -V·T \\ Nx & Ny & Nz & -N·T \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \]

最後輸出它們的矩陣值:
imglink7
imglink8

兩者的計算結果基本時一致的。需要注意的是Camera中表達視圖矩陣的成員變量是Camera.matrixWorldInverse。它的邏輯應該是視圖矩陣與模型矩陣互爲逆矩陣,模型矩陣也可以稱爲世界矩陣,那麼世界矩陣的逆矩陣就是視圖矩陣了。

3. 着色器變換

可以通過給着色器傳值來驗證計算的模型視圖投影矩陣(以下稱MVP矩陣)是否正確。對於一個任何事情都不做的着色器來說:

vertexShader: ` 
    void main() { 
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );      
    }`
,

fragmentShader: `       
    void main() {    
        gl_FragColor = vec4(0.556, 0.0, 0.0, 1.0)                   
    }`

projectionMatrix和modelViewMatrix分別是three.js中內置的投影矩陣和模型視圖矩陣。那麼可以做一個簡單的驗證工作,將計算得到的MVP矩陣傳入到着色器中,代替這兩個矩陣,如果最終得到的值是正確的,那麼就說明計算的MVP矩陣是正確的。

3.1. 代碼

實例代碼如下:

<!DOCTYPE html>
<html>

<head>
    <title>Example 01.01 - Basic skeleton</title>
    <meta charset="UTF-8" />
    <script type="text/javascript" charset="UTF-8" src="../three/three.js"></script>
    <script type="text/javascript" charset="UTF-8" src="../three/controls/TrackballControls.js"></script>
    <script type="text/javascript" charset="UTF-8" src="../three/libs/stats.min.js"></script>
    <script type="text/javascript" charset="UTF-8" src="../three/libs/util.js"></script>
    <script type="text/javascript" src="MatrixDemo.js"></script>
    <link rel="stylesheet" href="../css/default.css">
</head>

<body>
    <!-- Div which will hold the Output -->
    <div id="webgl-output"></div>

    <!-- Javascript code that runs our Three.js examples -->
    <script type="text/javascript">
        (function () {
            // contains the code for the example
            init();
        })();
    </script>
</body>

</html>
'use strict';

THREE.StretchShader = {

    uniforms: {   
        "sw" : {type:'b', value : false},
        "mvpMatrix" : {type:'m4',value:new THREE.Matrix4()}    
    },

    // 
    vertexShader: `    
        uniform mat4 mvpMatrix;
        uniform bool sw;
        void main() { 
            if(sw) {
                gl_Position = mvpMatrix * vec4( position, 1.0 );  
            }else{
                gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 
            }       
        }`
    ,

    //
    fragmentShader: `   
        uniform bool sw; 
        void main() {    
            if(sw) {
                gl_FragColor = vec4(0.556, 0.0, 0.0, 1.0); 
            }else {
                gl_FragColor = vec4(0.556, 0.8945, 0.9296, 1.0); 
            }                    
        }`
};


function init() {
    //console.log("Using Three.js version: " + THREE.REVISION);   

    // create a scene, that will hold all our elements such as objects, cameras and lights.
    var scene = new THREE.Scene();

    // create a camera, which defines where we're looking at.
    var camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

    // position and point the camera to the center of the scene
    camera.position.set(0, 0, 100);   //相機的位置
    camera.up.set(0, 1, 0);         //相機以哪個方向爲上方
    camera.lookAt(new THREE.Vector3(1, 2, 3));          //相機看向哪個座標
 
    // create a render and set the size
    var renderer = new THREE.WebGLRenderer();
    renderer.setClearColor(new THREE.Color(0x000000));
    renderer.setSize(window.innerWidth, window.innerHeight);

    // add the output of the renderer to the html element
    document.getElementById("webgl-output").appendChild(renderer.domElement);

    
    // create the ground plane
    var planeGeometry = new THREE.PlaneGeometry(60, 20);
    // var planeMaterial = new THREE.MeshBasicMaterial({
    //     color: 0xAAAAAA
    // });

    var planeMaterial = new THREE.ShaderMaterial({
        uniforms: THREE.StretchShader.uniforms,
        vertexShader: THREE.StretchShader.vertexShader,
        fragmentShader: THREE.StretchShader.fragmentShader
    });

    var plane = new THREE.Mesh(planeGeometry, planeMaterial);

    // add the plane to the scene
    scene.add(plane);

    // rotate and position the plane    
    plane.position.set(15, 8, -10);
    plane.rotation.x = THREE.Math.degToRad(30);
    plane.rotation.y = THREE.Math.degToRad(45);
    plane.rotation.z = THREE.Math.degToRad(60);
 
    render();
  
    var farmeCount = 0;
    function render() {    
        
        var mvpMatrix = new THREE.Matrix4(); 
        mvpMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);    
        mvpMatrix.multiplyMatrices(mvpMatrix, plane.matrixWorld);   
        
        THREE.StretchShader.uniforms.mvpMatrix.value = mvpMatrix; 
        if(farmeCount % 60 === 0){
            THREE.StretchShader.uniforms.sw.value = !THREE.StretchShader.uniforms.sw.value;
        }          
        
        farmeCount = requestAnimationFrame(render);
        renderer.render(scene, camera);
    }
   
}

3.2. 解析

這段代碼的意思是,給着色器傳入了計算好的MVP矩陣變量mvpMatrix,以及一個開關變量sw。開關變量會每60幀變一次,如果爲假,會使用內置的projectionMatrix和modelViewMatrix來計算頂點值,此時場景中的物體顏色會顯示爲藍色;如果開關變量爲真,則會使用傳入的計算好的mvpMatrix計算頂點值,此時場景中的物體顏色會顯示爲紅色。運行截圖如下:
imglink9

可以看到場景中的物體的顏色在紅色與藍色之間來回切換,且物體位置沒有任何變化,說明我們計算的MVP矩陣是正確的。

4. 其他

在使用JS的console.log()進行打印camera對象的時候,會發現如果不調用render()的話(或者單步調式),其內部的matrix相關的成員變量仍然是初始化的值,得不到想要的結果。而console.log()可以認爲是異步的,調用render()之後,就可以得到正確的camera對象了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章