第一章 坐标系统与变换:从模型到屏幕的奇幻漂流

大家好,我是你们这门课的讲师。今天咱们来聊聊图形学里最基础、也最绕不开的一个话题——坐标系统与变换。

说实话,我刚开始学图形学的时候,被这些坐标系搞得晕头转向。模型坐标系、世界坐标系、视图坐标系、投影坐标系……这玩意儿到底在干嘛?后来做了几个项目才明白,说白了就是一件事:把一个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变换的流程。你可以把它当作本章的思维导图。

MVP矩阵变换流程 模型坐标系 Model Space 世界坐标系 World Space 视图坐标系 View Space 投影坐标系 Clip Space M V P 矩阵运算公式: gl_Position = P × V × M × vertex_position 各矩阵作用: M (Model): 将物体从模型空间变换到世界空间 V (View): 将世界空间变换到摄像机视角 P (Projection): 将3D场景投影到2D裁剪空间 最终输出: 屏幕上的像素坐标

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