1. 程式人生 > >three.js中的矩陣變換(模型檢視投影變換)

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

[TOC] # 1. 概述 我在[《WebGL簡易教程(五):圖形變換(模型、檢視、投影變換)》][netlink1]這篇博文裡詳細講解了OpenGL\WebGL關於繪製場景的圖形變換過程,並推導了相應的模型變換矩陣、檢視變換矩陣以及投影變換矩陣。這裡我就通過three.js這個圖形引擎,驗證一下其推導是否正確,順便學習下three.js是如何進行圖形變換的。 # 2. 基本變換 ## 2.1. 矩陣運算 three.js已經提供了向量類和矩陣類,定義並且檢視一個4階矩陣類: ```javascript 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. 模型變換矩陣 在場景中新建一個平面: ```javascript // 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: ```javascript 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軸旋轉: ```javascript 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軸旋轉: ```javascript 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軸旋轉: ```javascript 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: ```javascript 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可以設定檢視矩陣: ```javascript camera.position.set(0, 0, 100); //相機的位置 camera.up.set(0, 1, 0); //相機以哪個方向為上方 camera.lookAt(new THREE.Vector3(1, 2, 3)); //相機看向哪個座標 ``` 根據[《WebGL簡易教程(五):圖形變換(模型、檢視、投影變換)》][netlink1]中的描述,可以通過three.js的矩陣運算來推導其檢視矩陣: ```javascript 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矩陣)是否正確。對於一個任何事情都不做的著色器來說: ```javascript 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. 程式碼 例項程式碼如下: ```html ``` ```javascript '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物件了。 [netlink1]:https://www.cnblogs.com/charlee44/p/11623502.html [imglink1]:https://img2020.cnblogs.com/blog/1000410/202005/1000410-20200504233111109-1296986072.png [imglink2]:https://img2020.cnblogs.com/blog/1000410/202005/1000410-20200504233124213-2003266496.png [imglink3]:https://img2020.cnblogs.com/blog/1000410/202005/1000410-20200504233137325-1938745765.png [imglink4]:https://img2020.cnblogs.com/blog/1000410/202005/1000410-20200504233149307-146617756.png [imglink5]:https://img2020.cnblogs.com/blog/1000410/202005/1000410-20200504233200646-95813364.png [imglink6]:https://img2020.cnblogs.com/blog/1000410/202005/1000410-20200504233216966-263627135.png [imglink7]:https://img2020.cnblogs.com/blog/1000410/202005/1000410-20200504233232583-1391840860.png [imglink8]:https://img2020.cnblogs.com/blog/1000410/202005/1000410-20200504233246394-1861545661.png [imglink9]:https://img2020.cnblogs.com/blog/1000410/202005/1000410-20200504233258032-171347