在 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.py
class SubMesh:
def __init__(self, indices) -> None:
self.indices = indices class 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 dylib
import pyassimp
import numpy as np
from .mesh import Mesh, SubMesh class ModelImporter:
def __init__(self) -> None:
pass def 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 mmeshes

2. 定义 Transform

Transform 用来描述物体的位置、旋转、缩放信息,可以说是比较基础的,所以必不可少,详细的解释在代码的注释里。

import numpy as np
from scipy.spatial.transform import Rotation as R class 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 @ mat
for 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 @ pos

3.定义相机

最后我们定义相机,目前相机的 Transform 信息可以用来定义 View 矩阵,其他例如 fov 等主要用来定义投影矩阵。

from math import cos, sin
import math
import numpy as np class Camera:
def __init__(self) -> None:
self._fov = 60
self._near = 0.3
self._far = 1000
self._aspect = 5 / 4 # -- 都是常规的 get set,这里略去
# ...... # 完全参照投影矩阵的公式定义
def getProjectionMatrix(self):
r = math.radians(self._fov / 2)
cotangent = cos(r) / sin(r)
deltaZ = self._near - self._far
projection = np.zeros((4,4))
projection[0,0] = cotangent / self._aspect
projection[1,1] = cotangent
projection[2,2] = (self._near + self._far) / deltaZ
projection[2,3] = 2 * self._near * self._far / deltaZ
projection[3,2] = -1
return projection

4. 构建 MVP 矩阵

完成了上述的步骤后,我们就可以构建 MVP 矩阵了。

...
# 定义物体的 transform
trans = transform.Transform()
trans.localPosition = [0,0,0]
trans.localScale = [0.005,0.005,0.005]
trans.localEulerAngle = [0,10,0]
# 获取 model 矩阵
model = trans.localMatrix() # 定义相机的 transform
viewTrans = transform.Transform()
viewTrans.localPosition = [0,2,2]
viewTrans.localEulerAngle = [-40,0,0]
# 获取 view 矩阵
view = viewTrans.get_to_Local() # 定义相机并获得 projection 矩阵
cam = Camera()
proj = cam.getProjectionMatrix()
# 构建 MVP 矩阵
mvp = np.transpose(proj @ view @ model)
# 作为 uniform 传入 shader 中,然后 shader 中将顶点位置乘上mvp矩阵。
mshader.set_mat4("u_mvp", mvp)
...

然后加载模型,构建一下顶点数组和索引数组,我给每个顶点额外添加了随机的颜色

importer = ModelImporter()

meshes = importer.load_mesh("box.fbx")
vert = []
for i in range(len(meshes[0].vertices)):
if i % 3 == 0:
vert.extend([meshes[0].vertices[i],meshes[0].vertices[i + 1],meshes[0].vertices[i + 2]])
vert.extend([meshes[0].normals[i],meshes[0].normals[i + 1],meshes[0].normals[i + 2]])
vert.extend([random.random(),random.random(),random.random()])
inde = meshes[0].subMeshes[0].indices
# 开一下深度测试
gl.glEnable(gl.GL_DEPTH_TEST)

我们可以看一下最终效果。

总结:

  1. 通过 Transform 我们可以获得 model 矩阵和 view 矩阵;
  2. 通过相机的参数,我们可以获得 projection 矩阵;
  3. 按照 p * v * m * pos 的顺序,即可将顶点位置进行投影;
  4. 本文代码没有考虑层级关系,为了简洁,原理都是一样的;
  5. 为了简洁旋转采用的欧拉角进行存储,没有用四元数。

    希望本文的例子,可以帮助理解 MVP 矩阵,以及学习一下如何加载、渲染模型的 API 等。

[CG从零开始] 5. 搞清 MVP 矩阵理论 + 实践的更多相关文章

  1. [CG从零开始] 6. 加载一个柴犬模型学习UV贴图

    在第 5 篇文章中,我们成功加载了 fbx 模型,并且做了 MVP 变换,将立方体按照透视投影渲染了出来.但是当时只是随机给顶点颜色,并且默认 fbx 文件里只有一个 mesh,这次我们来加载一个柴犬 ...

  2. Android应用中MVP最佳实践

    转自:http://www.jianshu.com/p/ed2aa9546c2c 文/Jude95(简书作者)原文链接:http://www.jianshu.com/p/ed2aa9546c2c著作权 ...

  3. 2017微软 MVP 数据实践技术活动日(北京站)

    Power BI | 交互式数据可视化 BI 工具 EXCEL BI :无所不能的业务数据分析利器 EXCEL +POWERBI=EXCEL BI https://edu.hellobi.com/co ...

  4. [CG从零开始] 3. 安装 pyassimp 库加载模型文件

    assimp 是一个开源的模型加载库,支持非常多的格式,还有许多语言的 binding,这里我们选用 assimp 的 python 的 binding 来加载模型文件.不过社区主要是在维护 assi ...

  5. [CG从零开始] 4. pyopengl 绘制一个正方形

    在做了 1-3 的基础工作后,我们的开发环境基本 OK 了,我们可以开始尝试利用 pyopengl 来进行绘制了. 本文主要有三个部分 利用 glfw 封装窗口类,并打开窗口: 封装 shader 类 ...

  6. Android开发MVP模式解析

    http://www.cnblogs.com/bravestarrhu/archive/2012/05/02/2479461.html 在开发Android应用时,相信很多同学遇到和我一样的情况,虽然 ...

  7. android MVP框架

    原文地址:http://blog.csdn.net/guxiao1201/article/details/40147209 在开发Android应用时,相信很多同学遇到和我一样的情况,虽然项目刚开始构 ...

  8. Android MVP Presenter 中引发的空指针异常

    一.概述 最近对 googlesamples/android-architecture 中的 MVP-dagger 进行了学习.对照项目的 MVP-dagger 分支,对 MVP-dagger 进行了 ...

  9. 使用MVP模式重构代码

    之前写了两篇关于MVP模式的文章,主要讲得都是一些概念,这里谈谈自己在Android项目中使用MVP模式的真实感受,并以实例的形式一起尝试来使用MVP模式去重构我们现有的代码. 有兴趣的童鞋可以先去阅 ...

随机推荐

  1. YII缓存操作

    //文件依赖 $dependency = new \yii\caching\FileDependency(['filename'=>'hw.txt'])}; $cache->add(&qu ...

  2. 中高级Java程序员,挑战20k+,知识点汇总(一),Java修饰符

    1 前言 工作久了就会发现,基础知识忘得差不多了.为了复习下基础的知识,同时为以后找工作做准备,这里简单总结一些常见的可能会被问到的问题. 2 自我介绍 自己根据实际情况发挥就行 3 Java SE ...

  3. 趣味问题《寻人启事》的Python程序解决

    偷懒了很久,今天我终于又来更新博客了~ 最近,我看到了一个趣味问题,或者说是数学游戏:<寻人启事>. 在表述这个问题前,我们需要了解一下"冰雹猜想": 对于任意一个正整 ...

  4. Apache DolphinScheduler 2.0.1 来了,备受期待的一键升级、插件化终于实现

    ✎ 编 者 按:好消息!Apache DolphinScheduler 2.0.1 版本正式发布! 本版本中,DolphinScheduler 经历了一场微内核+插件化的架构改进,70% 的代码被重构 ...

  5. Rsync数据备份工具

    Rsync数据备份工具 1.Rsync基本概述 rsync是一款开源的备份工具,可以在不同主机之间进行同步(windows和Linux之间 Mac和 Linux Linux和Linux),可实现全量备 ...

  6. Linux虚拟机 RHEL8.0安装步骤

    一. 创建空白虚拟机 1.打开 VMware 虚拟机软件依次选择新建虚拟机并选择选择"自定义" 自定义功能更加全面,典型就是比较简单的配置 2.选择对应的 VMware 版本,此则 ...

  7. KingbaseES 数据脱敏功能介绍

    数据脱敏,指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护. KingbaseES主要提供动态数据脱敏功能. 动态数据脱敏(Dynamic Data Masking)是与生产环境 ...

  8. 通过IIS部署Flask项目

      本文主要介绍在Windows Server 2012R2上通过IIS部署Flask项目的过程,以及对TTFB延迟大问题的思考.关于如何申请云服务器,注册(子)域名,备案,开放云服务器端口,获取SS ...

  9. Java 多线程:线程池

    Java 多线程:线程池 作者:Grey 原文地址: 博客园:Java 多线程:线程池 CSDN:Java 多线程:线程池 工作原理 线程池内部是通过队列结合线程实现的,当我们利用线程池执行任务时: ...

  10. Cat Theme

    将博客皮肤设置为: SimpleMemory 插入CSS代码 #EntryTag{margin-top:20px;font-size:9pt;color:gray}.topicListFooter{t ...