這兩天正在重新實現maptalks.js的三維變換邏輯, 需要從底層重新實現一遍三維投影轉換的演算法。 好在三維投影演算法已有很成熟的實現正規化, 我選擇了THREE.js作為參考物件, 這篇文章也是我對THREE.js中矩陣轉換關係的總結。
本文面向有一定webgl開發基礎的讀者,THREE的版本為寫作時的最新版本r88
三維投影演算法
什麼是三維投影轉換? 簡而言之,就是將三維空間中的物體,投射在相機視平面的轉換演算法, 如圖:
(圖片擷取自webglfundamentals):
在這個場景中,如圖擺放5個F字母和相機
相機看到的景象如下:
根據實際生活經驗,相機看到的景象和以下因素有關,任何改變都會讓相機眼中的世界發生變化:
三維投影演算法就是將上訴因素抽象為數學演算法,用來計算三維物體在相機視平面上的位置。
實際應用中我們是通過矩陣計算來實現的。簡而言之,我們將相機的位置方向, 相機的型別, 物體的位置和形變能轉換為 矩陣, 將這些矩陣進行一系列計算後, 最終得到三維投影矩陣:
基於它, 任意給定三維座標[x, y, z], 我們都能算出相機視平面上的位置:
當然,實際應用中情況會更復雜一些,例如三維圖形引擎為了簡化計算,一般將三維物體組織為層級結構,通過物體的本地位置和對上層的相對位置來計算出其在世界中的絕對位置,但歸根到底,我們需要的只是最終的位置矩陣。
THREE中的矩陣
讓我們回到THREE.js,來看看THREE是怎麼組織定義組織投影矩陣的。
我們知道,THREE定義了場景(Scene)和相機(Camera), Scene用來新增管理三維物體, Camera用來控制相機的位置, 角度等,程式碼大概如下:
const scene = new THREE.Scene();
const mesh = new THREE.Mesh(new THREE.Cube());
scene.add(mesh);
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
scene.add(camera);
renderer.render(scene, camera);複製程式碼
我們根據上面的總結按圖索驥,THREE中定義了下述三個矩陣:
- 相機投影型別:投影矩陣(
ProjectMatrix
) - 相機的位置和方向: 檢視矩陣 (
CameraMatrixWorldInverse
或ViewMatrix
) - 物體的位置和形變: 物體位置矩陣(
ObjectWorldMatrix
)
三維投影矩陣(u_matrix)計算公式
三維投影矩陣計算公式如下:
const uMatrix = ProjectMatrix * CameraMatrixWorldInverse* ObjectMatrixWorld複製程式碼
是不是很簡單?
如果你有興趣,可以寫一段最簡單的THREE程式,跟蹤一下THREE的繪製邏輯,看看THREE是怎麼生成和運用這些矩陣的。
接下來我們來解釋一下怎麼在THREE中得到上述三個矩陣:
相機投影矩陣(ProjectMatrix)
相機投影矩陣決定了相機是透視投影相機還是正射投影相機,現實世界都是透視投影,所以透視投影也是最常用的。
在THREE中,通過用不同的相機類例項化,得到不同型別的相機,例如定義一個透視投影相機:
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);複製程式碼
- 獲得
ProjectionMatrix
camera.projectMatrix複製程式碼
相機檢視矩陣(CameraMatrixWorldInverse)
有的三維引擎或教程,會把檢視矩陣稱為ViewMatrix
(例如webglfundamentals)
檢視矩陣的含義是,固定其他因素,我們改變了相機的位置和角度後,它眼中的世界也會發生變化,這種變化就是檢視矩陣。
前面提到,相機在三維空間中的位置是camera.matrixWorld
,而它的檢視矩陣是相機位置矩陣的逆矩陣CameraMatrixWorldInverse
,它也符合了我們的生活經驗:
- 固定相機,人向左移動
- 固定人,向右移動相機
這兩種情況在相機眼中是一樣的。
在THREE中,我們一般通過設定camera
的position和up,呼叫lookAt
來改變相機的檢視矩陣
camera.position.set(x, y, z);
camera.up.set(x, y, z);
camera.lookAt(x, y, z);複製程式碼
- 獲得
CameraMatrixInverse
camera.matrixWorldInverse複製程式碼
我們知道,最終的投影是在GLSL頂點著色器中計算的。在一次繪製中,
ProjectionMatrix
和CameraMatrixWorldInverse
一般不會發生變化,而ObjectMatrixWorld
每個物體都可能不同, 所以為了減少頂點著色器中的計算量,有些三維引擎會在javascript程式中提前計算出ProjectionMatrix * CameraMatrixWorldInverse
的值傳遞給頂點著色器,這個矩陣一般稱為ViewProjectionMatrix
物體位置矩陣(ObjectWorldMatrix)
ObjectWorldMatrix描述了物體在三維場景中的位置。
- 獲得
ObjectWorldMatrix
object.matrixWorld複製程式碼
前面提到,THREE中的物體是有層級關係的,所以THREE中物體的matrixWorld
是通過local matrix(object.matrix
)與父親的matrixWorld
遞迴相乘得到的, 其中的原理可以查閱webglfundamentals中的這篇教程
一些應用
獲取螢幕二維座標
給定三維座標[x, y, z],怎麼獲取它在螢幕上的二維座標呢?計算公式如下:
const [x, y] = ProjectionMatrix * CameraWorldMatrixInverse * [x, y, z]複製程式碼
THREE在Vector3上封裝了方法:
const v = new THREE.Vector3(x, y, z);
const xy = v.project(camera);複製程式碼
原始碼如下:
project: function () {
var matrix = new Matrix4();
return function project(camera) {
matrix.multiplyMatrices(camera.projectionMatrix, matrix.getInverse(camera.matrixWorld));
return this.applyMatrix4(matrix);
};
}(),複製程式碼
螢幕座標轉化為三維座標
給定螢幕二維座標[x, y],怎麼獲取它在三維空間中三維座標呢?計算公式如下:
const [x, y, z] = CameraWorldMatrix * ProjectionMatrixInverse * [x, y, z]複製程式碼
THREE在Vector3上封裝了方法:
const v = new THREE.Vector3(x, y, z);
const xyz = v.unproject(camera);複製程式碼
原始碼如下:
unproject: function () {
var matrix = new Matrix4();
return function unproject(camera) {
matrix.multiplyMatrices(camera.matrixWorld, matrix.getInverse(camera.projectionMatrix));
return this.applyMatrix4(matrix);
};
}(),複製程式碼
不過螢幕座標轉化為三維座標不是這麼簡單,因為螢幕上的二維座標在三維空間中其實對應的是一條射線,其可以對應了無限個三維座標點,更深入的原理可以閱讀這篇stackoverflow上的問題, THREE的作者mroob和一位網友給了精彩的回答。