第 21 集通常是 Cherno 教程中从“固定画面”转向“动态交互”的关键点,也是将 M、V、P 三者真正融合进代码的时刻。
按照程序运行的时间线,我们可以把这套复杂的逻辑分为以下阶段:
1. 初始化阶段:建立“尺子” (P)#
在程序启动时,你先定义了投影矩阵 (Projection Matrix)。
- 动作:
glm::ortho(0.0f, 960.0f, 0.0f, 540.0f, -1.0f, 1.0f)。 - 目的:建立一套“像素坐标系”。它就像一把尺子,告诉 GPU:“以后我给你 这个数,你就把它当成屏幕的最右边。”
- 状态:此时你还没决定看哪,也没决定物体在哪。
2. 空间布局阶段:摆放相机与物体 (V & M)#
在每一帧渲染开始前,你需要在 CPU 中计算当前的位置。
- 视图矩阵 (View Matrix):
- 动作:
glm::translate(glm::mat4(1.0f), glm::vec3(x, y, z))(通常是反向平移)。 - 目的:模拟摄像机的移动。如果你想让相机往右移,代码里其实是把整个世界往左拽。
- 动作:
- 模型矩阵 (Model Matrix):
- 动作:
glm::translate或glm::rotate。 - 目的:决定这个特定的物体(比如那个 logo)在世界里的具体坐标、旋转角度和缩放比例。
- 动作:
分工#
旋转、缩放、平移,这“三兄弟”通常全部都在 M(Model 矩阵)里完成。
虽然在数学上,你可以把它们放进任何矩阵,但在图形学的标准逻辑(即第 21 集所讲的逻辑)中,它们分工非常明确:
1. M (Model 矩阵) —— 物体自己的“属性”#
适用范围:旋转、缩放、平移(全占了)。
- 平移 (Translation):你想把方块放到屏幕左边还是右边?
- 旋转 (Rotation):你想让方块站着还是躺着?
- 缩放 (Scaling):你想让方块变大还是变小?
核心逻辑:Model 矩阵负责定义物体在世界里的样子。一个 logo 无论它怎么转、怎么挪,那都是它作为一个“物体”的行为。
2. V (View 矩阵) —— 相机的“视角”#
适用范围:平移、旋转(极少用缩放)。
- 平移:相机往前走,还是往后退。
- 旋转:相机低头看,还是转过头去看。
- 为什么不用缩放?:在现实生活中,相机本身是不会“变大变小”的。如果你想让画面里的东西变大,你要么挪近相机(平移),要么换个长焦镜头(修改 P 矩阵)。
核心逻辑:View 矩阵负责定义观察者的位置。
3. P (Projection 矩阵) —— 镜头的“参数”#
适用范围:视野(FOV)、宽高比、远近裁剪。
- 它不处理物体的旋转或平移,它只负责把 3D 空间里的物体投影到 2D 屏幕上。
3. 组合阶段:矩阵连乘 (MVP)#
这是第 21 集最核心的操作,在传给 Shader 之前,你在 C++ 里把它们乘在了一起。
- 计算顺序:
proj * view * model(注意:必须是从左往右写的这个顺序,因为矩阵运算是从右向左生效的)。 - 意义:这三者合并成了一个单一的
glm::mat4矩阵,它包含了“物体在哪”、“相机在哪”和“投影范围”的所有信息。
例子#
//宽高比:960:540即16:9
glm::mat4 proj = glm::ortho(0.0f, 960.0f, 0.0f, 540.0f, -1.0f, 1.0f);
//这里在原来的基础上,左移了100
//即向右移动相机
glm::mat4 view = glm::translate(glm::mat4(1.0f),
glm::vec3(-100, 0, 0));
glm::mat4 mvp = proj * view;
//.....
shader.SetUniformMat4f("u_MVP", mvp);
移动后:

//宽高比:960:540即16:9
glm::mat4 proj = glm::ortho(0.0f, 960.0f, 0.0f, 540.0f, -1.0f, 1.0f);
//即向右移动相机
glm::mat4 view = glm::translate(glm::mat4(1.0f),
glm::vec3(-100, 0, 0));
//向右向上移动200
glm::mat4 model = glm::translate(glm::mat4(1.0f),
glm::vec3(200, 200, 0));
glm::mat4 mvp = proj * view * model;
glm::vec4 vp(100.0f, 100.0f, 0.0f, 1.0f);
glm::vec4 result = proj * vp * model;
4. 传输阶段:Uniform 传递#
- 动作:
glUniformMatrix4fv(location, 1, GL_FALSE, &mvp[0][0])。 - 目的:把你在 CPU 里算好的这块“综合翻译表”送进显卡的内存。
5. GPU 执行阶段:顶点变换#
这是在 顶点着色器 (Vertex Shader) 内部发生的:
代码:
gl_Position = u_MVP * position;时间轴位置:
- 第一步 (Model):顶点从本地坐标变成世界坐标。
- 第二步 (View):世界坐标变成相对于你的观察坐标
这句很重要,解释了learnopengl中为何出现欧拉角等等概念。 - 第三步 (Projection):观察坐标被“压缩”到 。
结果:GPU 得到了最终的 NDC 坐标。
6. 光栅化阶段:视口变换 (Viewport)#
- 时间点:顶点着色器执行完后,像素着色器执行前。
- 动作:GPU 自动调用你设置的
glViewport。 - 目的:把你之前在
proj里定义的比例映射回真正的 个像素格子里。
💡 核心总结:21 集教会了我们什么?#
| 步骤 | 矩阵 | 对应操作 | 你的认知 |
|---|---|---|---|
| 第一步 | Model | 改变物体的位置 | “我想让方块出现在屏幕中间” |
| 第二步 | View | 改变观察者的位置 | “我想让相机离远一点” |
| 第三步 | Projection | 改变显示的范围 | “我想让这块屏幕代表 像素” |
在这一集中,你可能已经通过 ImGui 做了滑动条来实时改变 Model 矩阵的平移值。当你拖动滑动条时,你其实是在实时更新时间线上的“第 2 阶段”,从而影响了最终生成的那个 u_MVP。