引言

为什么 3D 游戏能在 2D 屏幕上显示?这归功于渲染管线 (Rendering Pipeline) 中一系列精心设计的坐标变换。 核心公式是:

\[ P_{screen} = Viewport \times P_{ndc} = Viewport \times \frac{1}{w}(Projection \times View \times Model \times P_{local}) \]

Rendering Pipeline

本篇侧重管线后半段——从裁剪空间到屏幕像素——的数学细节。

1. 坐标空间速览 (Coordinate Spaces Recap)

MVP 矩阵的完整推导见矩阵与变换,这里侧重管线后半段的处理。

顶点在到达投影阶段之前,依次经过三个变换:

阶段 矩阵 作用
局部空间 → 世界空间 Model (\(M\)) 平移、旋转、缩放,将物体放入世界
世界空间 → 观察空间 View (\(V\)) 以摄像机为原点重新建系
观察空间 → 裁剪空间 Projection (\(P\)) 定义可见范围,引入透视

经过 \(P \times V \times M\) 之后,顶点进入裁剪空间 (Clip Space),坐标为齐次形式 \((x, y, z, w)\)。接下来的故事,是本文的重点。

2. 投影矩阵 (Projection Matrix)

投影矩阵决定了”摄像机能看到什么”。它把观察空间中的一块区域映射到标准化立方体 (NDC Cube) 内。根据映射方式不同,分为两种投影。

2.1 正交投影 (Orthographic Projection)

正交投影将一个轴对齐的长方体 (AABB) 直接映射为 \([-1,1]^3\) 的 NDC 立方体。没有近大远小的效果,常用于 2D 游戏和 CAD 工具。

给定左右 (\(l, r\))、下上 (\(b, t\))、近远 (\(n, f\)) 六个平面,正交投影矩阵为:

\[ P_{ortho} = \begin{bmatrix} \frac{2}{r-l} & 0 & 0 & -\frac{r+l}{r-l} \\ 0 & \frac{2}{t-b} & 0 & -\frac{t+b}{t-b} \\ 0 & 0 & \frac{-2}{f-n} & -\frac{f+n}{f-n} \\ 0 & 0 & 0 & 1 \end{bmatrix} \]

注意最后一行是 \((0,0,0,1)\),变换后 \(w\) 始终为 1——不会产生透视缩放。

2.2 透视投影 (Perspective Projection)

透视投影模拟人眼:近处的物体大,远处的物体小。它将一个视锥体 (View Frustum,一个截头锥体) 映射为 NDC 立方体。

关键参数: - FOV (Field of View,视场角):垂直方向的张角,常取 60°–90° - aspect:宽高比 \(= width / height\) - near (\(n\))far (\(f\)):近裁剪面和远裁剪面的距离

由 FOV 和 aspect 可以推导出矩阵各项。设 \(t = n \cdot \tan(\frac{FOV}{2})\),则 \(b = -t\)\(r = t \cdot aspect\)\(l = -r\)。OpenGL 风格的透视矩阵为:

\[ P_{persp} = \begin{bmatrix} \frac{n}{r} & 0 & 0 & 0 \\ 0 & \frac{n}{t} & 0 & 0 \\ 0 & 0 & \frac{-(f+n)}{f-n} & \frac{-2fn}{f-n} \\ 0 & 0 & -1 & 0 \end{bmatrix} \]

逐项解读: - \((0,0)\)\((1,1)\):FOV 越大,\(t\) 越大,这两个值越小,画面越”广角”。aspect 影响水平缩放。 - \((2,2)\)\((2,3)\):控制 \(z\) 如何映射到 \([-1, 1]\),即深度的非线性压缩。 - \((3,2) = -1\)关键! 它把观察空间的 \(-z\) 拷贝到 \(w\) 分量,为透视除法做准备。 - \((3,3) = 0\):变换后 \(w\) 完全由 \(z\) 决定,不再包含常数项。

视锥体到 NDC 的映射可以直觉地理解为:近平面上的矩形”不变”,远平面上的矩形被”压缩”成和近平面一样大,整个锥体被”捏”成长方体。

2.3 正交 vs 透视:对比

特性 正交 透视
\(w\) 分量 始终为 1 等于 \(-z_{view}\)
近大远小
典型用途 2D/UI/CAD 3D 游戏
深度分布 线性 非线性 (近密远疏)

3. 透视除法 (Perspective Division)

裁剪空间之后,GPU 自动执行透视除法——将 \((x, y, z)\) 各分量除以 \(w\)

\[ P_{ndc} = \left(\frac{x}{w},\; \frac{y}{w},\; \frac{z}{w}\right) \]

结果称为标准化设备坐标 (Normalized Device Coordinates, NDC),各分量范围为 \([-1, 1]\)(OpenGL)或 \([0, 1]\)(部分 API 的 z 范围)。

3.1 为什么远处的物体看起来小?

透视矩阵将 \(w\) 设置为 \(-z_{view}\)(观察空间中物体离摄像机的距离)。 距离越远 → \(w\) 越大 → 除以 \(w\)\(x, y\) 越小 → 屏幕上物体越小。 这就是透视缩短 (Foreshortening) 效果的数学本质。

3.2 当 w = 0 时:无穷远点

在齐次坐标中,\(w = 0\) 表示方向而非位置——几何上对应无穷远处的点。 例如,平行光的方向向量 \((d_x, d_y, d_z, 0)\) 就是典型的 \(w=0\) 情况。 对 \(w=0\) 做除法无意义,因此 GPU 在裁剪阶段会丢弃这些顶点(它们不在视锥体内)。

3.3 深度值的非线性分布

透视除法后,\(z_{ndc} = z_{clip} / w\) 的分布是高度非线性的:

  • 靠近近平面 (\(n\)) 的一小段距离,占据了 NDC \(z\) 范围的大部分精度。
  • 远处大片区域被压缩到极窄的范围内。

假设 \(n = 0.1\)\(f = 1000\),那么 NDC 范围的前 50% 仅覆盖相机前方约 0.1 到 0.2 的距离!剩下的 0.2 → 1000 全部挤在后 50%。这就是深度精度问题的根源(详见第 5 节)。

4. 视口变换 (Viewport Transform)

NDC 坐标统一在 \([-1,1]\) 范围内,与具体屏幕分辨率无关。视口变换负责将 NDC 映射到实际的像素坐标。

4.1 变换公式

给定视口参数:起点 \((x_0, y_0)\),宽 \(w\),高 \(h\),以及深度范围 \([d_{near}, d_{far}]\)(通常为 \([0, 1]\)):

\[ x_{screen} = x_0 + \frac{w}{2}(x_{ndc} + 1) \] \[ y_{screen} = y_0 + \frac{h}{2}(y_{ndc} + 1) \] \[ z_{buffer} = \frac{d_{far} - d_{near}}{2} \cdot z_{ndc} + \frac{d_{far} + d_{near}}{2} \]

4.2 视口设置

在 OpenGL 中通过 glViewport(x0, y0, width, height)glDepthRange(near, far) 设置。常见场景:

  • 全屏渲染glViewport(0, 0, screenWidth, screenHeight)
  • 分屏多人:左侧玩家 glViewport(0, 0, w/2, h),右侧玩家 glViewport(w/2, 0, w/2, h)
  • 小地图:在右上角开一个小视口 glViewport(w-200, h-200, 200, 200)

注意:\(y\) 轴方向因 API 而异——OpenGL 的原点在左下角,DirectX/Vulkan 在左上角。实际开发中需要留意翻转问题。

5. 深度缓冲与 Z-Fighting (Depth Buffer)

5.1 深度缓冲基础

深度缓冲 (Depth Buffer / Z-Buffer) 是一块与屏幕等大的缓冲区,每个像素存储一个深度值。GPU 在光栅化时,对每个片元 (Fragment) 比较其深度与缓冲区中已有的值:

  • 若新片元更近 → 写入颜色和深度
  • 若新片元更远 → 丢弃

这样无需对三角形排序即可正确处理遮挡关系。

5.2 Z-Fighting 问题

由于透视投影导致深度值非线性分布,两个距离很近的远处表面可能映射到相同的深度缓冲值。渲染时会出现两个面交替闪烁、产生条纹的现象,称为Z-Fighting

缓解手段: - 将近平面 \(n\) 尽量设大(如 0.1 → 1.0),显著改善精度分布 - 减小远近平面比 \(f/n\)

5.3 Reversed-Z 技巧

现代引擎(Unity URP/HDRP、Unreal Engine)广泛采用 Reversed-Z

  • 近平面映射到 \(z = 1\),远平面映射到 \(z = 0\)(与传统相反)
  • 配合浮点数在 0 附近精度高的特性,远处的深度值恰好落在浮点精度最高的区间

效果:配合 32 位浮点深度缓冲,Reversed-Z 可以做到 \(f/n > 10^6\) 而几乎无 Z-Fighting,远优于传统 24 位整数深度缓冲。

6. 代码示例 (Vertex Shader)

以下 GLSL 顶点着色器展示了管线前半段在 GPU 上的实际写法:

#version 330 core
layout (location = 0) in vec3 aPos;   // 局部空间顶点坐标
layout (location = 1) in vec3 aNormal; // 顶点法线

uniform mat4 model;      // Model 矩阵: Local → World
uniform mat4 view;       // View 矩阵:  World → View (Camera)
uniform mat4 projection; // Projection 矩阵: View → Clip

out vec3 FragPos;   // 传给片元着色器的世界坐标

void main()
{
    // 1. Model 变换: 局部空间 → 世界空间
    FragPos = vec3(model * vec4(aPos, 1.0));

    // 2. View + Projection 变换: 世界空间 → 裁剪空间
    //    GPU 随后自动执行透视除法 (÷w) → NDC,再经视口变换 → 屏幕像素
    gl_Position = projection * view * vec4(FragPos, 1.0);
}

gl_Position 输出的是裁剪空间坐标。透视除法和视口变换由 GPU 硬件自动完成,无需手动编写。

7. 总结

  • 投影矩阵是管线中信息密度最高的一步:FOV、aspect、near/far 四个参数决定了整个可视范围。
  • 透视除法 (\(\div w\)) 是产生近大远小效果的数学本质。
  • 深度值的非线性分布是 Z-Fighting 的根源;Reversed-Z 是现代引擎的标准解决方案。
  • 视口变换将抽象的 NDC 坐标落地为具体的像素位置。

< 上一篇: 碰撞响应 | 回到目录 | 下一篇: 光照模型 >

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。