这两天正在从新实现maptalks.js的三维变换逻辑, 须要从底层从新实现一遍三维投影转换的算法。 好在三维投影算法已有很成熟的实现范式, 我选择了THREE.js做为参考对象, 这篇文章也是我对THREE.js中矩阵转换关系的总结。javascript
本文面向有必定webgl开发基础的读者,THREE的版本为写做时的最新版本r88
html
什么是三维投影转换? 简而言之,就是将三维空间中的物体,投射在相机视平面的转换算法, 如图:java
(图片截取自webglfundamentals):git
在这个场景中,如图摆放5个F字母和相机github
相机看到的景象以下:web
根据实际生活经验,相机看到的景象和如下因素有关,任何改变都会让相机眼中的世界发生变化:算法
三维投影算法就是将上诉因素抽象为数学算法,用来计算三维物体在相机视平面上的位置。app
实际应用中咱们是经过矩阵计算来实现的。简而言之,咱们将相机的位置方向, 相机的类型, 物体的位置和形变能转换为 矩阵, 将这些矩阵进行一系列计算后, 最终获得三维投影矩阵:less
固然,实际应用中状况会更复杂一些,例如三维图形引擎为了简化计算,通常将三维物体组织为层级结构,经过物体的本地位置和对上层的相对位置来计算出其在世界中的绝对位置,但归根到底,咱们须要的只是最终的位置矩阵。webgl
让咱们回到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
)三维投影矩阵计算公式以下:
const uMatrix = ProjectMatrix * CameraMatrixWorldInverse* ObjectMatrixWorld复制代码
是否是很简单?
若是你有兴趣,能够写一段最简单的THREE程序,跟踪一下THREE的绘制逻辑,看看THREE是怎么生成和运用这些矩阵的。
接下来咱们来解释一下怎么在THREE中获得上述三个矩阵:
相机投影矩阵决定了相机是透视投影相机仍是正射投影相机,现实世界都是透视投影,因此透视投影也是最经常使用的。
在THREE中,经过用不一样的相机类实例化,获得不一样类型的相机,例如定义一个透视投影相机:
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);复制代码
ProjectionMatrix
camera.projectMatrix复制代码
有的三维引擎或教程,会把视图矩阵称为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
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和一位网友给了精彩的回答。