2025-11-30 · game_math
【游戏中的数学】游戏中的数学 (11) - 光照模型
从 Lambert 到 PBR:详解漫反射、Blinn-Phong、Cook-Torrance BRDF、材质参数与 Shader 调试技巧。
为什么 3D 游戏能在 2D 屏幕上显示?这归功于渲染管线 (Rendering Pipeline) 中一系列精心设计的坐标变换。 核心公式是:
\[ P_{screen} = Viewport \times P_{ndc} = Viewport \times \frac{1}{w}(Projection \times View \times Model \times P_{local}) \]
本篇侧重管线后半段——从裁剪空间到屏幕像素——的数学细节。
MVP 矩阵的完整推导见矩阵与变换,这里侧重管线后半段的处理。
顶点在到达投影阶段之前,依次经过三个变换:
| 阶段 | 矩阵 | 作用 |
|---|---|---|
| 局部空间 → 世界空间 | Model (\(M\)) | 平移、旋转、缩放,将物体放入世界 |
| 世界空间 → 观察空间 | View (\(V\)) | 以摄像机为原点重新建系 |
| 观察空间 → 裁剪空间 | Projection (\(P\)) | 定义可见范围,引入透视 |
经过 \(P \times V \times M\) 之后,顶点进入裁剪空间 (Clip Space),坐标为齐次形式 \((x, y, z, w)\)。接下来的故事,是本文的重点。
投影矩阵决定了”摄像机能看到什么”。它把观察空间中的一块区域映射到标准化立方体 (NDC Cube) 内。根据映射方式不同,分为两种投影。
正交投影将一个轴对齐的长方体 (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——不会产生透视缩放。
透视投影模拟人眼:近处的物体大,远处的物体小。它将一个视锥体 (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 的映射可以直觉地理解为:近平面上的矩形”不变”,远平面上的矩形被”压缩”成和近平面一样大,整个锥体被”捏”成长方体。
| 特性 | 正交 | 透视 |
|---|---|---|
| \(w\) 分量 | 始终为 1 | 等于 \(-z_{view}\) |
| 近大远小 | 无 | 有 |
| 典型用途 | 2D/UI/CAD | 3D 游戏 |
| 深度分布 | 线性 | 非线性 (近密远疏) |
裁剪空间之后,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 范围)。
透视矩阵将 \(w\) 设置为 \(-z_{view}\)(观察空间中物体离摄像机的距离)。 距离越远 → \(w\) 越大 → 除以 \(w\) 后 \(x, y\) 越小 → 屏幕上物体越小。 这就是透视缩短 (Foreshortening) 效果的数学本质。
在齐次坐标中,\(w = 0\) 表示方向而非位置——几何上对应无穷远处的点。 例如,平行光的方向向量 \((d_x, d_y, d_z, 0)\) 就是典型的 \(w=0\) 情况。 对 \(w=0\) 做除法无意义,因此 GPU 在裁剪阶段会丢弃这些顶点(它们不在视锥体内)。
透视除法后,\(z_{ndc} = z_{clip} / w\) 的分布是高度非线性的:
假设 \(n = 0.1\),\(f = 1000\),那么 NDC 范围的前 50% 仅覆盖相机前方约 0.1 到 0.2 的距离!剩下的 0.2 → 1000 全部挤在后 50%。这就是深度精度问题的根源(详见第 5 节)。
NDC 坐标统一在 \([-1,1]\) 范围内,与具体屏幕分辨率无关。视口变换负责将 NDC 映射到实际的像素坐标。
给定视口参数:起点 \((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} \]
在 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 在左上角。实际开发中需要留意翻转问题。
深度缓冲 (Depth Buffer / Z-Buffer) 是一块与屏幕等大的缓冲区,每个像素存储一个深度值。GPU 在光栅化时,对每个片元 (Fragment) 比较其深度与缓冲区中已有的值:
这样无需对三角形排序即可正确处理遮挡关系。
由于透视投影导致深度值非线性分布,两个距离很近的远处表面可能映射到相同的深度缓冲值。渲染时会出现两个面交替闪烁、产生条纹的现象,称为Z-Fighting。
缓解手段: - 将近平面 \(n\) 尽量设大(如 0.1 → 1.0),显著改善精度分布 - 减小远近平面比 \(f/n\)
现代引擎(Unity URP/HDRP、Unreal Engine)广泛采用 Reversed-Z:
效果:配合 32 位浮点深度缓冲,Reversed-Z 可以做到 \(f/n > 10^6\) 而几乎无 Z-Fighting,远优于传统 24 位整数深度缓冲。
以下 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 硬件自动完成,无需手动编写。
< 上一篇: 碰撞响应 | 回到目录 | 下一篇: 光照模型 >
把当前热点继续串成多页阅读,而不是停在单篇消费。
2025-11-30 · game_math
从 Lambert 到 PBR:详解漫反射、Blinn-Phong、Cook-Torrance BRDF、材质参数与 Shader 调试技巧。
2025-11-30 · game_math
详解游戏引擎中常用的标量数学函数:Lerp, InverseLerp, Remap, Clamp, SmoothStep 等。不仅有公式,更有实际应用场景。
2025-11-30 · game_math
深入浅出地讲解游戏开发中的三角函数:Sin, Cos, Atan2。从单位圆原理到圆周运动、波浪动画和朝向计算的实际应用。
2025-11-30 · game_math
深入理解游戏开发中的矩阵:TRS 变换与代码实践、矩阵乘法与组合变换、MVP 管线详解、逆矩阵、法线变换,以及旋转表示方式对比。
为什么 3D 游戏能在 2D 屏幕上显示?这归功于渲染管线 (Rendering Pipeline) 中一系列精心设计的坐标变换。 核心公式是:
\[ P_{screen} = Viewport \times P_{ndc} = Viewport \times \frac{1}{w}(Projection \times View \times Model \times P_{local}) \]
本篇侧重管线后半段——从裁剪空间到屏幕像素——的数学细节。
MVP 矩阵的完整推导见矩阵与变换,这里侧重管线后半段的处理。
顶点在到达投影阶段之前,依次经过三个变换:
| 阶段 | 矩阵 | 作用 |
|---|---|---|
| 局部空间 → 世界空间 | Model (\(M\)) | 平移、旋转、缩放,将物体放入世界 |
| 世界空间 → 观察空间 | View (\(V\)) | 以摄像机为原点重新建系 |
| 观察空间 → 裁剪空间 | Projection (\(P\)) | 定义可见范围,引入透视 |
经过 \(P \times V \times M\) 之后,顶点进入裁剪空间 (Clip Space),坐标为齐次形式 \((x, y, z, w)\)。接下来的故事,是本文的重点。
投影矩阵决定了”摄像机能看到什么”。它把观察空间中的一块区域映射到标准化立方体 (NDC Cube) 内。根据映射方式不同,分为两种投影。
正交投影将一个轴对齐的长方体 (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——不会产生透视缩放。
透视投影模拟人眼:近处的物体大,远处的物体小。它将一个视锥体 (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 的映射可以直觉地理解为:近平面上的矩形”不变”,远平面上的矩形被”压缩”成和近平面一样大,整个锥体被”捏”成长方体。
| 特性 | 正交 | 透视 |
|---|---|---|
| \(w\) 分量 | 始终为 1 | 等于 \(-z_{view}\) |
| 近大远小 | 无 | 有 |
| 典型用途 | 2D/UI/CAD | 3D 游戏 |
| 深度分布 | 线性 | 非线性 (近密远疏) |
裁剪空间之后,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 范围)。
透视矩阵将 \(w\) 设置为 \(-z_{view}\)(观察空间中物体离摄像机的距离)。 距离越远 → \(w\) 越大 → 除以 \(w\) 后 \(x, y\) 越小 → 屏幕上物体越小。 这就是透视缩短 (Foreshortening) 效果的数学本质。
在齐次坐标中,\(w = 0\) 表示方向而非位置——几何上对应无穷远处的点。 例如,平行光的方向向量 \((d_x, d_y, d_z, 0)\) 就是典型的 \(w=0\) 情况。 对 \(w=0\) 做除法无意义,因此 GPU 在裁剪阶段会丢弃这些顶点(它们不在视锥体内)。
透视除法后,\(z_{ndc} = z_{clip} / w\) 的分布是高度非线性的:
假设 \(n = 0.1\),\(f = 1000\),那么 NDC 范围的前 50% 仅覆盖相机前方约 0.1 到 0.2 的距离!剩下的 0.2 → 1000 全部挤在后 50%。这就是深度精度问题的根源(详见第 5 节)。
NDC 坐标统一在 \([-1,1]\) 范围内,与具体屏幕分辨率无关。视口变换负责将 NDC 映射到实际的像素坐标。
给定视口参数:起点 \((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} \]
在 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 在左上角。实际开发中需要留意翻转问题。
深度缓冲 (Depth Buffer / Z-Buffer) 是一块与屏幕等大的缓冲区,每个像素存储一个深度值。GPU 在光栅化时,对每个片元 (Fragment) 比较其深度与缓冲区中已有的值:
这样无需对三角形排序即可正确处理遮挡关系。
由于透视投影导致深度值非线性分布,两个距离很近的远处表面可能映射到相同的深度缓冲值。渲染时会出现两个面交替闪烁、产生条纹的现象,称为Z-Fighting。
缓解手段: - 将近平面 \(n\) 尽量设大(如 0.1 → 1.0),显著改善精度分布 - 减小远近平面比 \(f/n\)
现代引擎(Unity URP/HDRP、Unreal Engine)广泛采用 Reversed-Z:
效果:配合 32 位浮点深度缓冲,Reversed-Z 可以做到 \(f/n > 10^6\) 而几乎无 Z-Fighting,远优于传统 24 位整数深度缓冲。
以下 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 硬件自动完成,无需手动编写。
< 上一篇: 碰撞响应 | 回到目录 | 下一篇: 光照模型 >
把当前热点继续串成多页阅读,而不是停在单篇消费。
2025-11-30 · game_math
从 Lambert 到 PBR:详解漫反射、Blinn-Phong、Cook-Torrance BRDF、材质参数与 Shader 调试技巧。
2025-11-30 · game_math
详解游戏引擎中常用的标量数学函数:Lerp, InverseLerp, Remap, Clamp, SmoothStep 等。不仅有公式,更有实际应用场景。
2025-11-30 · game_math
深入浅出地讲解游戏开发中的三角函数:Sin, Cos, Atan2。从单位圆原理到圆周运动、波浪动画和朝向计算的实际应用。
2025-11-30 · game_math
深入理解游戏开发中的矩阵:TRS 变换与代码实践、矩阵乘法与组合变换、MVP 管线详解、逆矩阵、法线变换,以及旋转表示方式对比。