3、几何阶段:顶点着色器、模型变换、视图变换、投影变换(透视与正交)

好,咱们进入正题。几何阶段,说白了就是决定「物体在屏幕上长什么样」的第一步。你想想看,一个3D模型从建模软件里导出来,坐标都是自己局部的。怎么把它放到世界场景里?怎么从摄像机的角度去看?最后怎么变成屏幕上那个扁平的画面?

嗯,这三个问题,正好对应了今天要聊的三个核心变换:模型变换、视图变换、投影变换。而顶点着色器,就是执行这些变换的「第一道工序」。

3.1 顶点着色器:几何阶段的入口

我个人习惯把顶点着色器看作「每个顶点的私人管家」。你送进来一个顶点,它帮你处理完,再送出去。它干的事很纯粹:坐标变换逐顶点光照(如果走固定管线的话)。

我在项目中遇到过一个问题:一个角色模型在远处闪烁。查了半天,发现是顶点着色器里精度不够,导致坐标计算有抖动。后来换了高精度浮点,问题就解决了。所以这里提醒一句——精度问题,别忽视

核心职责:

  • 接收模型局部坐标(Object Space)
  • 输出裁剪坐标(Clip Space)
  • 可选:传递纹理坐标、法线、颜色等属性到片元着色器

一个最简单的顶点着色器长这样(GLSL):

#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

你看,就一行核心代码。但这一行背后,藏着三个矩阵的故事。

3.2 模型变换:从局部到世界

模型变换,就是把物体从它自己的「小世界」(模型空间)搬到「大世界」(世界空间)。

说白了,就是平移、旋转、缩放。你想想看,一个茶壶在建模软件里原点位置,你要把它放到场景的(10, 0, -5)位置,还要旋转45度,再放大1.2倍。这就是模型变换干的事。

我曾经犯过一个低级错误:先缩放后平移,结果物体跑偏了。因为矩阵乘法不满足交换律。正确的顺序是:先缩放,再旋转,最后平移。嗯,这个顺序我建议你刻在脑子里。

小技巧: 如果你用Unity或Unreal,它们的Transform组件默认就是按这个顺序组合的。但如果你手写矩阵,一定要自己注意顺序。

3.3 视图变换:从世界到观察

视图变换,其实就是「摄像机怎么摆」。你想想看,摄像机放在哪里?朝哪看?头顶朝哪?这三个问题决定了视图矩阵。

我记得刚学图形学时,总觉得视图变换很玄乎。后来想通了:视图变换就是把摄像机移到原点,让它朝-Z方向看。所有物体跟着一起动,但相对位置不变。

构建视图矩阵的经典方法是使用lookAt函数:

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)   // 上方向
);

这里有个坑:上方向向量不能和视线方向平行,否则矩阵会退化。我曾经在项目中遇到过摄像机突然翻转的情况,就是因为上方向设置成了(0, 1, 0),但摄像机正好从正上方往下看——嗯,万向锁的另一种表现形式。

3.4 投影变换:从3D到2D

投影变换,就是把3D场景「压扁」到2D屏幕上。有两种方式:透视投影和正交投影。

3.4.1 透视投影

透视投影模拟人眼效果:近大远小。这是最常用的投影方式,尤其是游戏和影视。

它的核心参数有四个:

参数 含义 典型值
FOV (Field of View) 视野角度 45° - 90°
Aspect Ratio 宽高比 16:9 或 4:3
Near Plane 近裁剪面 0.1
Far Plane 远裁剪面 100.0

注意: Near Plane 不要设得太小(比如0.001),否则会导致深度缓冲精度问题。我见过有人设成0.0001,结果远处物体闪烁得厉害。一般0.1就够用。

透视投影矩阵的构建(GLM):

glm::mat4 proj = glm::perspective(
    glm::radians(45.0f),   // FOV
    800.0f / 600.0f,       // 宽高比
    0.1f,                  // 近平面
    100.0f                 // 远平面
);

3.4.2 正交投影

正交投影没有近大远小效果。物体大小不随距离变化。常用于2D游戏、UI渲染、工程制图。

它的参数是左右上下前后六个面:

glm::mat4 proj = glm::ortho(
    -10.0f, 10.0f,   // 左右
    -7.5f, 7.5f,     // 上下
    0.1f, 100.0f     // 近远
);

我建议你在做2D游戏时,把近平面设成-1,远平面设成1,这样z值在-1到1之间,方便控制层级。

3.5 三个变换的串联

好了,三个变换都讲完了。它们是怎么串起来的?

顶点坐标的变换流程:

  1. 模型空间 → 模型变换 → 世界空间
  2. 世界空间 → 视图变换 → 观察空间
  3. 观察空间 → 投影变换 → 裁剪空间
  4. 裁剪空间 → 透视除法 → NDC(归一化设备坐标)
  5. NDC → 视口变换 → 屏幕坐标

你想想看,每一步都在做一件事:把坐标从一个「坐标系」转换到另一个「坐标系」。而顶点着色器,就是执行前三步的地方。

核心公式:

gl_Position = projection * view * model * vec4(position, 1.0);

注意矩阵乘法顺序:从右往左读。先model,再view,最后projection。

3.6 知识体系图

下面我用一张SVG图来总结本章的核心逻辑。你一看就明白了:

几何阶段核心变换流程 模型空间 世界空间 观察空间 裁剪空间 NDC 模型变换 视图变换 投影变换 透视除法 关键说明: • 顶点着色器负责执行前三步变换(模型→世界→观察→裁剪) • 透视除法由硬件自动完成,将裁剪坐标转换为NDC(范围-1到1) • 视口变换将NDC映射到屏幕像素坐标,这一步由光栅化阶段处理 • 矩阵乘法顺序:gl_Position = projection * view * model * vertex 记住:从右往左读矩阵乘法

3.7 避坑指南

最后,我把自己踩过的坑总结一下,你遇到了可以直接翻回来看看:

  • 矩阵乘法顺序搞反:我曾经把projection和view写反了,结果画面全黑。调试了半天才发现。
  • 近平面设太小:深度精度不够,远处物体闪烁。建议近平面不小于0.1。
  • 忘记透视除法:如果你自己写Shader,记得gl_Position输出后,硬件会自动做透视除法。但如果你在CPU端做计算,要手动除以w分量。
  • 视图矩阵的上方向向量:不要和视线方向平行,否则矩阵会奇异。

嗯,这一章的内容就到这里。几何阶段是渲染管线的基石,你把这些变换吃透了,后面的光栅化、片元着色器学起来会轻松很多。


专注资料整理