[CG从零开始] 5. 搞清 MVP 矩阵理论 + 实践

在 4 中成功绘制了三角形以后,下面我们来加载一个 fbx 文件,然后构建 MVP 变换(model-view-projection) 。简单介绍一下:

  1. 从我们拿到模型(主要是网格信息)文件开始,模型网格(Mesh)里记录模型的顶点位置信息,比方说 (-1,1,1) 点,那么这个点是相对于这个模型的(0,0,0)点来说的,这和我们在制作模型的时候有关,例如我可以让这个(0,0,0)点位于模型的中心也可以是底部 。
  2. 接着我们需要通过放置许多的模型来构建整个场景 , 为了描述每个物体的位姿(位置和姿态) , 我们需要一个世界原点 , 然后所有物体的位姿信息都是相对于这个世界原点的 。如果用过游戏引擎或者 DCC 软件的话,一般每个物体都会有一个 transform 来描述这件事情 。因此第一步我们需要将物体的顶点从建模时候的坐标系,变换到世界坐标系下,这个变换矩阵就是我们说的 model 矩阵,也就是引擎中 transform 组件描述的变换 。
  3. 将模型的顶点位置变换到世界坐标系下以后 , 我们还需要进行 view 矩阵的变换 , view 变换的过程模拟眼睛看东西的过程,一般用一个相机来描述,这个相机是一般是看向 -z 方向的 。我们需要将模型变换到相机的坐标系下,方便的后面的投影操作 。这个 view 变换,其实不是相机特有的,因为我们可以将物体变换到任意一下坐标系下 。
  4. 将物体变换到相机坐标系下后,最后要做一个投影的操作 , 一般来说三维场景做的都是透视变换,符合我看到的近大远小的规律 。
上面用大白话简单描述了一下这几个矩阵,相关资料有很多 , 本系列重在实践,因为看再多的理论,不如自己亲手实践一下印象深刻 , 有时候不明白的原理,动手做一下就明白了 。如果希望看相关的数学推导理论,证明之类的可以搜一搜有很多 。我这里提供一下我之前写的关于变换的两个文章:
  • 投影变换
  • 世界坐标->相机坐标
下面来实践一下,代码基于第 4 篇文章继续完善 。完整的代码:https://github.com/MangoWAY/CGLearner/tree/v0.2 , tag v0.2
1. 加载 fbx 模型在第 3 篇中介绍了如何安装 pyassimp,这回我们来用一下,我们先定义一个简单的 Mesh 和 SubMesh 类保存加载的模型的数据 , 然后再定义一个模型加载类,用来加载数据,代码如下所示,比较简单 。
# mesh.pyclass SubMesh:def __init__(self, indices) -> None:self.indices = indicesclass Mesh:def __init__(self) -> None:self.vertices = []self.normals = []self.subMeshes = []# model_importer.py# pyassimp 4.1.4 has some problem will lead to randomly crash, use 4.1.3 to fix# should set link path to find the dylibimport pyassimpimport numpy as npfrom .mesh import Mesh, SubMeshclass ModelImporter:def __init__(self) -> None:passdef load_mesh(self, path: str):scene = pyassimp.load(path)mmeshes = []for mesh in scene.meshes:mmesh = Mesh()mmesh.vertices = np.reshape(np.copy(mesh.vertices), (1,-1)).squeeze(0)print(mmesh.vertices)mmesh.normals = np.reshape(np.copy(mesh.normals),(1,-1)).squeeze(0)mmesh.subMeshes = []mmesh.subMeshes.append(SubMesh(np.reshape(np.copy(mesh.faces), (1,-1)).squeeze(0)))mmeshes.append(mmesh)return mmeshes2. 定义 TransformTransform 用来描述物体的位置、旋转、缩放信息 , 可以说是比较基础的,所以必不可少,详细的解释在代码的注释里 。
import numpy as npfrom scipy.spatial.transform import Rotation as Rclass Transform:def __init__(self) -> None:# 为了简单,目前我用欧拉角来存储旋转信息self._eulerAngle = [0,0,0]self._pos = [0,0,0]self._scale = [1,1,1]# -- 都是常规的 get set,这里略去# ......# 这就是我们所需要的 model 矩阵,注意这里没有考虑的物体的层级# 关系,默认物体都是在最顶层,所以 local 和 world 坐标是一样# 后续的文章会把层级关系考虑进来def localMatrix(self):# 按照 TRS 的构建方式# 位移矩阵 * 旋转矩阵 * 缩放矩阵mat = np.identity(4)# 对角线是缩放for i in range(3):mat[i,i] = self._scale[i]rot = np.identity(4)rot[:3,:3] = R.from_euler("xyz", self._eulerAngle, degrees = True).as_matrix()mat = rot @ matfor i in range(3):mat[i,3] = self._pos[i]return mat# 将世界坐标变换到当前物体的坐标系下,注意这里也是没有考虑层级关系的# 这个可以用来获得从世界坐标系到相机坐标系的转换 。def get_to_Local(self):mat = self.localMatrix()ori = np.identity(4)ori[:3,:3] = mat[:3,:3]ori = np.transpose(ori)pos = np.identity(4)pos[0:3,3] = -mat[0:3,3]return ori @ pos3.定义相机最后我们定义相机 , 目前相机的 Transform 信息可以用来定义 View 矩阵,其他例如 fov 等主要用来定义投影矩阵 。

推荐阅读