第一章 坐标系统与变换:从模型到屏幕的奇幻漂流
大家好,我是你们这门课的讲师。今天咱们来聊聊图形学里最基础、也最绕不开的一个话题——坐标系统与变换。
说实话,我刚开始学图形学的时候,被这些坐标系搞得晕头转向。模型坐标系、世界坐标系、视图坐标系、投影坐标系……这玩意儿到底在干嘛?后来做了几个项目才明白,说白了就是一件事:把一个3D物体,最终显示到你的2D屏幕上。
你想想看,一个模型在建模软件里好好的,怎么到了游戏里就能跟着摄像机转?怎么还能有近大远小的效果?这一切的背后,就是MVP矩阵变换在默默工作。
1.1 四大坐标系,一个都不能少
我们先来认识一下这四个坐标系。它们就像流水线上的四个工位,每个工位干一件事。
| 坐标系 | 作用 | 通俗理解 |
|---|---|---|
| 模型坐标系 (Model Space) | 物体自身的坐标系统 | 模型在建模软件里的原始位置 |
| 世界坐标系 (World Space) | 所有物体共享的全局坐标 | 游戏场景里的大地图 |
| 视图坐标系 (View Space) | 以摄像机为原点的坐标 | 你眼睛看到的画面 |
| 投影坐标系 (Clip Space / NDC) | 将3D压平到2D屏幕 | 拍照片时,把立体景物压到胶片上 |
模型坐标系,也叫局部坐标系。每个模型都有自己的原点。比如一个茶壶,它的壶嘴在(1,0,0),壶把在(-1,0,0),这些都是相对于茶壶自己的中心点说的。
世界坐标系,就是整个场景的绝对坐标。你把茶壶放在(10,0,5)的位置,旋转45度,缩放两倍——这些操作都是在世界坐标系里完成的。
视图坐标系,这个我得多说两句。我曾经在项目里犯过一个低级错误:把视图矩阵和模型矩阵搞混了。结果摄像机一动,整个场景跟着乱飞。视图坐标系说白了就是:把摄像机放到原点,让它朝Z轴负方向看。所有物体都跟着摄像机一起变换,这样我们才能得到「从摄像机视角看出去」的画面。
投影坐标系,这是最后一步。把3D场景压到一个2D平面上。有透视投影(近大远小)和正交投影(没有透视效果)两种。
核心要点:这四个坐标系是串联的。数据从模型坐标系出发,经过世界坐标系、视图坐标系,最后到达投影坐标系。每一步都对应一个矩阵变换。
1.2 MVP矩阵:三个矩阵的接力赛
MVP 是 Model、View、Projection 三个单词的首字母。这三个矩阵就是上面三个变换的数学表达。
公式很简单:
// 顶点最终位置 = 投影矩阵 × 视图矩阵 × 模型矩阵 × 顶点原始位置
gl_Position = projection * view * model * vec4(aPos, 1.0);
注意顺序!是 先模型,再视图,最后投影。矩阵乘法是从右往左读的。我刚开始学的时候总写成 model * view * projection,结果画面全乱了。嗯,这个坑我替你们踩过了。
模型矩阵 (Model Matrix)
模型矩阵负责把物体从模型坐标系变换到世界坐标系。它通常包含平移、旋转、缩放三种操作。
// 一个简单的模型矩阵示例
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(2.0f, 1.0f, 0.0f)); // 平移
model = glm::rotate(model, glm::radians(45.0f), glm::vec3(0.0f, 1.0f, 0.0f)); // 旋转
model = glm::scale(model, glm::vec3(1.5f, 1.5f, 1.5f)); // 缩放
视图矩阵 (View Matrix)
视图矩阵的本质是把摄像机移到原点。你想想看,摄像机往前移动,画面里的物体应该往后移动。所以视图矩阵其实就是摄像机的逆变换。
// 使用glm库创建视图矩阵
glm::mat4 view = glm::lookAt(
glm::vec3(0.0f, 0.0f, 3.0f), // 摄像机位置
glm::vec3(0.0f, 0.0f, 0.0f), // 目标点
glm::vec3(0.0f, 1.0f, 0.0f) // 上方向
);
个人经验:我在做VR项目时,视图矩阵的构建特别关键。因为VR需要左右眼两个摄像机,它们的视图矩阵只有微小的偏移。如果算错了,用户会头晕。我建议你调试时先把视图矩阵打印出来看看,确认摄像机位置和朝向是对的。
投影矩阵 (Projection Matrix)
投影矩阵有两种:透视投影和正交投影。
- 透视投影:模拟人眼效果,近大远小。游戏里99%的情况用这个。
- 正交投影:没有透视效果,物体大小不随距离变化。常用于UI、2D游戏、工程图纸。
// 透视投影矩阵
glm::mat4 proj = glm::perspective(
glm::radians(45.0f), // 视野角度 FOV
800.0f / 600.0f, // 宽高比
0.1f, // 近裁剪面
100.0f // 远裁剪面
);
// 正交投影矩阵
glm::mat4 orthoProj = glm::ortho(
0.0f, 800.0f, // 左、右
0.0f, 600.0f, // 下、上
0.1f, 100.0f // 近、远
);
避坑指南:我曾经在项目里把近裁剪面设成了0.0f,结果深度测试全乱了。记住,近裁剪面必须大于0,远裁剪面必须大于近裁剪面。另外,FOV值一般取45°-60°比较自然,太大会有鱼眼效果,太小会像望远镜。
1.3 知识体系总览
下面这张图是我手绘的,展示了整个MVP变换的流程。你可以把它当作本章的思维导图。
1.4 实战中的坑与技巧
理论说完了,咱们聊聊实战。我在做图形学项目时,遇到过不少跟坐标系相关的问题。
第一个坑:矩阵乘法的顺序。 我前面提过,很多人会搞反。记住一个口诀:「先模型,再视图,最后投影」。在代码里写的时候,是从右往左读的。
第二个坑:坐标系的手性。 OpenGL用的是右手坐标系,DirectX用的是左手坐标系。如果你在两个API之间切换,记得把Z轴方向反过来。我曾经把一个左手系的模型直接丢到OpenGL里,结果模型是镜像的,找了半天bug。
第三个坑:深度缓冲的精度。 投影变换会把深度值映射到[0,1]区间。但近裁剪面和远裁剪面的比值会影响精度。如果近裁剪面设得太小(比如0.001),远裁剪面设得太大(比如10000),远处的物体深度值会挤在一起,导致Z-fighting。我建议近裁剪面至少设0.1,远裁剪面不要超过1000。
调试技巧:当你发现画面不对时,可以先把投影矩阵设成正交投影,排除透视变换的干扰。如果正交投影下画面正常,那问题大概率出在投影矩阵上。如果正交投影也不对,那就检查模型矩阵和视图矩阵。
1.5 小结
这一章我们聊了四个坐标系和MVP矩阵变换。说白了,这就是一个流水线:模型坐标 → 世界坐标 → 视图坐标 → 投影坐标。每一步都有一个矩阵在干活。
我个人觉得,理解MVP矩阵的关键不在于背公式,而在于理解「为什么要这么做」。为什么要有视图矩阵?因为我们要模拟摄像机。为什么要有投影矩阵?因为我们要把3D压到2D。想通了这些,代码怎么写自然就清楚了。
下一章我们会深入矩阵运算的细节,包括平移、旋转、缩放的具体实现,以及四元数的使用。嗯,到时候再聊。
公众号:蓝海资料掘金营,微信deep3321