游戏渲染技术全方位解析 - 从一根头发到一片云的技术全景

游戏渲染技术全方位解析 🎮✨

本文基于网易游戏学堂的技术分享,深入拆解游戏渲染的核心原理——从角色皮肤上的次表面散射,到天空中飘过的体积云,看技术美术如何用代码和数学在屏幕上重构一个真实可感的世界。每个技术点都会讲清楚是什么、为什么、怎么做,辅以数学公式、Shader 伪代码、实际游戏案例和性能数据。

原文: 游戏渲染技术|全方位解析游戏画面的实现逻辑 — 网易游戏学堂


一、什么是游戏渲染?

1.1 定义

游戏渲染(Game Rendering),简单来说就是让你能在显示器上看到游戏画面的技术。更精确地说,它是一系列计算机图形学算法的集合,负责将虚拟的 3D 场景(模型、材质、灯光、动画)转化为显示器上的 2D 像素阵列。

这个过程的本质可以概括为**”光的模拟”**——计算光子从光源出发,经过场景中各种物体的反射、折射、散射后,最终到达虚拟相机传感器的过程。

1.2 20 年的画面进化

年份 游戏 渲染特征 技术里程碑
1997 Quake II 逐像素光照、软件光栅化 第一个商业硬件加速 3D 游戏
2004 Half-Life 2 HDR 光照、法线贴图 Source 引擎的光照系统
2007 Crysis 实时阴影、SSAO、体积光 “显卡危机”——PC 图形的标杆
2013 The Last of Us 延迟渲染、后处理管线 主机平台的 PBR 先驱
2015 The Witcher 3 PBR 材质、开放世界流送 大规模场景的 PBR 实践
2017 Horizon Zero Dawn 体积云、大气散射、植被系统 Decima 引擎的自然环境渲染
2018 Red Dead Redemption 2 物理天空、全局光照、ECS 极致的细节和交互
2020 Cyberpunk 2077 RTX 光追、DLSS、虚拟几何 光线追踪实时化的先驱
2022 Horizon Forbidden West 体积云升级、水面交互 大气渲染的新高度
2023 Alan Wake 2 全路径追踪、Meshlet 实时光追的全面突破

从 2000 年到 2025 年,游戏画面发生了翻天覆地的变化,但底层做的事情始终如一:通过计算机图形学算法,将 3D 场景转化为 2D 图像。变化的是算法的复杂度和硬件的性能——从简单的 Phong 光照模型,到基于物理的渲染(PBR),再到实时光线追踪。

1.3 渲染管线概览

理解游戏渲染,首先要理解渲染管线(Rendering Pipeline)——从 3D 数据到屏幕像素的完整流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──────────────────────────────────────────────────────────────────────┐
│ 渲染管线(Rendering Pipeline) │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌────────────────┐ │
│ │ Application│ │ Geometry │ │ Rasterize │ │ Pixel/ │ │
│ │ Stage │──▶│ Processing│──▶│ (光栅化) │──▶│ Fragment │ │
│ │ (CPU) │ │ Stage │ │ │ │ Stage │ │
│ └──────────┘ └──────────┘ └───────────┘ └────────────────┘ │
│ │
│ ① 场景遍历 ④ 顶点变换 ⑦ 三角形→像素 ⑩ 像素着色 │
│ ② 视锥剔除 ⑤ 裁剪 ⑧ 重心坐标插值 ⑪ 纹理采样 │
│ ③ 排序/合批 ⑥ 屏幕映射 ⑨ Early-Z 测试 ⑫ 混合/输出 │
│ │
└──────────────────────────────────────────────────────────────────────┘

关键阶段详解

  1. Application Stage(应用层):CPU 端执行碰撞检测、物理模拟、动画更新、可见性判断。这是唯一完全由开发者控制的阶段。

  2. Geometry Processing(几何处理):顶点着色器(Vertex Shader)执行模型-视图-投影变换(MVP)、法线变换、骨骼蒙皮。可选的曲面细分(Tessellation)和几何着色器(Geometry Shader)在此阶段运行。

  3. Rasterization(光栅化):将三角形转换为像素片段(fragments)。这一步由 GPU 硬件固定管线完成,速度极快。

  4. Fragment Processing(片元处理):像素/片元着色器执行材质计算——这就是 PBR、NPR、SSS 等技术”发生”的地方。每个像素都要执行一次 Shader 程序。

现代引擎还广泛使用延迟渲染(Deferred Rendering)前向+(Forward+)等变体,它们改变了管线的组织方式,但核心流程不变。

1.4 两种主流渲染架构

特性 前向渲染(Forward) 延迟渲染(Deferred)
光照复杂度 O(物体数 × 光源数) O(像素数 × 光源数)
多光源 困难(需要多 pass) 友好(G-Buffer 解耦)
透明物体 天然支持 需要额外前向 pass
MSAA 天然支持 不兼容(需 FXAA/TAA)
带宽 高(G-Buffer 读写)
代表引擎 Unity 默认、移动端 UE4/UE5 默认
移动端友好度 ✅ 高 ❌ 低(带宽开销大)

Forward+(分块前向渲染)结合了两者的优势:先用深度预pass将屏幕分块,计算每块的光源列表,然后在前向 pass 中只处理相关光源。这是目前主机和 PC 上的主流方案之一。


二、渲染画面的三大核心模块

游戏渲染可以归纳为三个互相交织的核心模块:

模块 关注点 目标 技术难度
角色(Character) 材质与交互:头发、皮肤、眼睛、服饰 让角色看起来真实或有风格 ⭐⭐⭐⭐⭐
场景(Scene) LOD 与流送:植被、云、水、地形 构建可信的游戏世界 ⭐⭐⭐⭐
氛围(Atmosphere) 参数与控制:天气、光影(TOD 时间变化) 营造沉浸感 ⭐⭐⭐

为什么是这三个模块?

这不是随意的分类,而是反映了游戏画面构成的物理本质

  • 角色是玩家注意力的焦点——人脸识别的本能让玩家对角色的每一处细节都极度敏感。一个微小的皮肤渲染错误(比如看起来像塑料)会立刻破坏沉浸感。
  • 场景是玩家活动的空间——它需要足够大(开放世界)、足够丰富(植被、建筑)、足够真实(光影正确),同时又不能超出硬件预算。
  • 氛围是情绪的载体——同一个场景,晴天和雨天给玩家的感受完全不同。大气散射、体积云、天气系统是连接角色和场景的”胶水”。

三者的交集:交互

关键洞察:三者并非割裂,而是存在大量交集——这些交集恰恰是技术上最难、也是提升沉浸感最关键的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
               ┌──────────────┐
│ 交互效果 │
│ │
┌───────┤ 角色踩水 ├───────┐
│ │ 风吹头发 │ │
│ │ 雨滴打伞 │ │
│ │ 场景破坏 │ │
│ └──────┬───────┘ │
┌──────┴──────┐ │ ┌────────┴──────┐
│ 角色 │ │ │ 场景 │
Character │ │ │ Scene │
└──────┬──────┘ │ └────────┬──────┘
│ ┌──────┴───────┐ │
└───────┤ 氛围 ├───────┘
│ Atmosphere │
└──────────────┘

典型的交互渲染效果

交互类型 角色侧 场景侧 氛围侧 技术方案
角色踩水 溅水动画 水面波纹 泥地反射 物理模拟 + Render to Texture
风吹头发 发丝摆动 草地弯曲 风向参数 风场 + Verlet 积分
雨天行走 湿身效果 地面水洼 雨滴粒子 SSS 变体 + 粒子系统
场景破坏 碎片物理 几何变形 灰尘特效 Destructible Mesh + GPU 粒子
雪地行走 脚印标记 雪地变形 积雪厚度 Splat Map + Tessellation

理解了这三个模块及其交互,就掌握了游戏渲染的全局视图。接下来我们逐个深入。


三、角色渲染技术详解

角色是游戏画面的核心。一个角色的渲染质量,直接决定了玩家对游戏画面水平的整体评判。

3.1 角色制作流程

从原画设计到游戏画面,角色需经历一个完整的工业化管线:

1
2
3
4
5
6
7
8
原画设计 → 高模雕刻 → 拓扑简化 → UV 展开 → 烘焙贴图 → 材质制作 → 渲染输出
│ │ │ │ │ │ │
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
概念图 ZBrush 3ds Max/ RizomUV Substance PBR/NPR 游戏引擎
确定风格 百万面 Blender UV布局 Painter 材质参数 实时渲染
细节雕刻 万面级别 纹理密度 法线+金属 粗糙度等 LOD/剔除
AO等 +粗糙度 参数调整 最终效果

角色材质的两大分类

  1. 通用材质库:解决一般物体表面的光照问题。金属、木材、布料、皮革等通过 PBR 参数化描述,一套材质系统覆盖大部分需求。
  2. 特殊材质:眼球、皮肤、头发、丝袜等需要特殊的 Shader 处理——它们的光照行为无法被标准 PBR 模型准确描述。

通用材质用一套标准化的 PBR 流程就能搞定,但特殊材质才是区分”一般画面”和”3A 画面”的关键。下面我们逐个拆解。


3.2 PBR vs NPR:两大渲染范式

这是游戏渲染中最核心的概念分野。理解它们的差异,是理解后续所有技术的基础。

3.2.1 PBR(基于物理的渲染)

核心思想:用物理公式描述光与材质的交互。给定材质参数(粗糙度、金属度等)和光照条件,渲染结果是确定的、可预测的

为什么需要 PBR? 在 PBR 时代之前,美术需要手动调整每个材质的每一项参数来”凑出”看起来对的效果。一个问题:在日光下看起来正确的材质,在室内灯光下可能完全不对。PBR 通过物理模型保证了材质在任何光照条件下都正确。

特性 PBR NPR
驱动方式 公式驱动(Formula Driven) 结果驱动(Result Driven)
追求目标 物理正确性 艺术风格表达
代表类型 写实类游戏 二次元 / 风格化游戏
美术工作流 标准化,工具成熟 需自定义,成本较高
光照一致性 ✅ 任意光照下正确 ❌ 通常依赖烘焙,动态光需特殊处理
材质参数 Roughness, Metallic, AO 等 色阶贴图、SDF 轮廓、法线阈值等
代表游戏 The Last of Us, Cyberpunk 原神, 崩坏:星穹铁道, Zelda BotW
工具链 Substance 生态完善 自研 Shader + 美术管线

3.2.2 NPR(非真实感渲染)

核心思想:最终效果由美术直接控制,Shader 的角色是”把美术的意图忠实呈现”,而不是”用公式算出物理正确的结果”。

NPR 不是”不追求真实”,而是追求另一种真实——动画、漫画、油画等非写实风格的视觉语言。原神的渲染效果在物理上完全错误(阴影是硬编码的色阶、高光位置不符合物理),但在玩家的感知中,它”看起来就是对的”——因为玩家期待的是二次元风格,而不是物理真实。


3.3 渲染方程:一切 PBR 的基础

所有基于物理的渲染都建立在一个方程之上——渲染方程(Rendering Equation),由 James Kajiya 于 1986 年提出:

$$L_o(\omega_o) = L_e(\omega_o) + \int_{\Omega} f_r(\omega_i, \omega_o) \cdot L_i(\omega_i) \cdot (\mathbf{n} \cdot \omega_i) , d\omega_i$$

这个方程看起来吓人,但拆解后每个部分都有清晰的物理含义:

符号 名称 物理含义 直觉理解
$L_o(\omega_o)$ 出射辐射度 沿观察方向 $\omega_o$ 离开表面的光量 “你看到了什么”
$L_e(\omega_o)$ 自发光 表面自身发出的光 “它自己是灯泡吗?”
$\int_{\Omega}$ 半球积分 对上半球所有方向求和 “从四面八方收集光线”
$f_r(\omega_i, \omega_o)$ BRDF 双向反射分布函数 “这个表面有多能反光?”
$L_i(\omega_i)$ 入射辐射度 从方向 $\omega_i$ 来的光量 “那里有多少光?”
$(\mathbf{n} \cdot \omega_i)$ Lambert 余弦 表面法线与入射方向的点积 “光有多正着照过来?”
$d\omega_i$ 立体角微元 方向上的微小变化 “收集每个方向的一小份”

直觉翻译

“你看到的颜色 = 表面自己发的光 + 从四面八方来的光,经过表面反射后的总和。每束入射光的贡献取决于三个因素:光有多强($L_i$)、表面在该方向的反射能力(BRDF)、光线照射的角度(Lambert 余弦项)。”

为什么这个方程重要?

渲染方程是能量守恒的数学表达——表面反射出去的光能永远不会超过接收到的光能。这是 PBR 的基石:只要你的 BRDF 满足能量守恒,材质在任何光照条件下都会表现正确。

为什么渲染方程无法精确求解?

积分域 $\Omega$ 是连续的半球空间——理论上需要无限多次采样才能得到精确解。这就是为什么游戏渲染永远在做近似

  • 实时渲染用环境贴图(Cubemap)或球谐函数(SH)近似 $L_i$
  • 离线渲染用蒙特卡洛方法(Monte Carlo)采样估计积分
  • 光线追踪用光线采样直接求解积分

3.4 BRDF:表面的”身份证”

BRDF(Bidirectional Reflectance Distribution Function,双向反射分布函数)是渲染方程的核心——它描述了材质的光学特性。不同的材质就是不同的 BRDF

3.4.1 BRDF 的物理约束

一个物理合理的 BRDF 必须满足:

  1. 非负性:$f_r(\omega_i, \omega_o) \geq 0$(反射光不能为负)
  2. 互易律(Helmholtz Reciprocity):$f_r(\omega_i, \omega_o) = f_r(\omega_o, \omega_i)$(交换入射和出射方向,结果不变)
  3. 能量守恒:$\int_{\Omega} f_r(\omega_i, \omega_o) (\mathbf{n} \cdot \omega_o) d\omega_o \leq 1$(反射出去的总能量不超过入射能量)

3.4.2 从 MERL 数据库到参数化 BRDF

在 PBR 流行之前,研究者用专业设备(测角光度计)测量真实材质的反射特性,建立了 MERL BRDF 数据库(Matusik et al., 2003),包含 100 种真实材质的 BRDF 数据。

MERL 数据库的问题

问题 详情
数据量巨大 每个材质 33MB+,100 个材质就是 3.3GB
查询性能低 需要三线性插值,GPU 友好度差
表达能力受限 无法表达各向异性(如拉丝金属)、次表面散射等复杂现象
外推能力差 无法从采样数据生成数据库中没有的材质
参数不直观 没有美术能理解的参数化方式

这意味着:数据驱动这条路走不通。我们需要参数化的数学模型来近似真实材质。

3.4.3 Disney Principled BRDF(2012)——革命性的简化

Brent Burley 在 SIGGRAPH 2012 发表的 Disney Principled BRDF 带来了革命性的改变。它不是一个新的物理模型,而是一套美术友好的参数化方案,用少数直观参数控制复杂的材质外观。

设计原则

  1. 参数直观:用”粗糙度”而非晦涩的”微表面斜率分布参数”
  2. 参数尽可能少:控制在 10 个左右的关键参数
  3. 值范围 0-1:标准化,便于理解和调整(美术最喜欢滑块!)
  4. 允许参数超出范围:在有意义时突破 [0,1] 限制以表达特殊效果
  5. 组合健壮:任意参数组合不产生明显错误

核心参数详解

参数 英文名 范围 效果说明 物理对应
基础色 Base Color [0,1]³ 材质本身的颜色(非金属)或反射率(金属) 反射率 albedo
粗糙度 Roughness [0,1] 0 = 镜面光滑,1 = 完全粗糙 微表面斜率的方差
金属度 Metallic [0,1] 0 = 非金属(有漫反射),1 = 金属(无漫反射) 导体/介质区分
高光强度 Specular [0,1] 非金属的镜面反射强度 菲涅尔 F0 值
各向异性 Anisotropic [0,1] 反射高光的拉伸程度(如拉丝金属) 切线方向上的粗糙度差异
次表面 Subsurface [0,1] 光线在表面下的散射程度 次表面散射近似
光泽 Sheen [0,1] 掠射角的布料光泽 织物纤维的散射
光泽色 Sheen Tint [0,1] 光泽的色调偏移
清漆 Clearcoat [0,1] 表面额外的透明涂层 汽车漆面的两层结构
清漆粗糙度 Clearcoat Roughness [0,1] 透明涂层的粗糙度

最核心的参数是粗糙度(Roughness)。它和高光的光泽度(Glossiness)是互补关系:Glossiness = 1 - Roughness。不同的引擎用不同的约定——UE 用 Roughness,Unity 的 HDRP 默认也用 Roughness,但一些旧管线用 Smoothness(= 1 - Roughness)。

一个关键的实现细节:在 Metallic/Roughness 工作流中,如果 Base Color 贴图在金属-非金属边界处没有完美对齐,就会产生黑色或白色的边缘伪影。解决方案包括:

  • 使用更精确的 UV 和更细致的手绘
  • 后处理 dilate/erode 边缘
  • Unity HDRP 和 UE5 都有内置的边缘修复选项

3.5 微表面模型(Microfacet Model)

微表面模型是理解 PBR 镜面反射的关键——它是 Disney BRDF 和游戏中几乎所有 Specular BRDF 的理论基础。

3.5.1 核心思想

在微观尺度上,任何表面都不是完全光滑的。微表面模型假设:

表面由无数个微小的、完美镜面的平面组成,每个微平面有自己的法线方向。表面的”粗糙度”由微平面法线分布的集中程度决定。

1
2
3
4
5
6
7
8
9
10
光滑表面(Roughness ≈ 0):        粗糙表面(Roughness ≈ 1):

╲ │ ╱ ╲ ╱ ╲ │ ╱ ╲ ╱
╲ │ ╱ ╲╱ ╲│╱ ╲╱
────┼──── ─╱╲─╱╲─╱╲─╱╲─
╱ │ ╲ ╱ ╲╱ ╲╱ ╲
╱ │ ╲ ╱ ╱ ╲ │ ╲ ╲

微平面法线高度集中 微平面法线高度分散
→ 反射光集中(镜面反射) → 反射光分散(漫反射)

3.5.2 Cook-Torrance BRDF

游戏中最常用的 Specular BRDF 是 Cook-Torrance 模型

$$f_{specular}(\omega_i, \omega_o) = \frac{D(\mathbf{h}) \cdot F(\omega_o, \mathbf{h}) \cdot G(\omega_i, \omega_o, \mathbf{h})}{4 \cdot (\mathbf{n} \cdot \omega_i) \cdot (\mathbf{n} \cdot \omega_o)}$$

其中 $\mathbf{h} = \frac{\omega_i + \omega_o}{|\omega_i + \omega_o|}$ 是半程向量(Half Vector)。

三个组成部分各有分工:

函数 名称 作用 物理含义
$D(\mathbf{h})$ 法线分布函数(NDF) 决定高光的形状和大小 微平面的法线朝 $\mathbf{h}$ 方向的概率密度
$F(\omega_o, \mathbf{h})$ 菲涅尔项(Fresnel) 决定不同角度的反射率 从正面看vs从侧面看的反射强度差异
$G(\omega_i, \omega_o, \mathbf{h})$ 几何遮蔽项(Geometry) 补偿自遮挡 微平面之间的相互遮挡

3.5.3 法线分布函数:为什么选 GGX?

GGX(Trowbridge-Reitz) 是目前最主流的 NDF,由 Walter et al. 2007 提出:

$$D_{GGX}(\mathbf{h}) = \frac{\alpha^2}{\pi \cdot ((\mathbf{n} \cdot \mathbf{h})^2 \cdot (\alpha^2 - 1) + 1)^2}$$

其中 $\alpha = roughness^2$。

GGX vs Beckmann 对比

特性 Beckmann GGX (Trowbridge-Reitz)
高光形状 较平滑的衰减 明显的”拖尾”(long tail)
高光核心 稍宽 更窄、更亮
材质表现 适合塑料、油漆 适合金属、玻璃
为什么选 GGX 更长的拖尾更符合真实金属的反射特性
代表引擎 早期 OpenGL 固定管线 UE4/5、Unity HDRP、Filament

拖尾(tail) 是关键区别。真实金属的高光不是完美的圆——它有一条长长的”尾巴”,在远离高光中心的地方仍然有可感知的反射。GGX 的长尾特性恰好匹配了这一现象,这就是为什么所有现代引擎都选择了 GGX。

Shader 伪代码

1
2
3
4
5
6
7
8
// GGX / Trowbridge-Reitz NDF
float D_GGX(float NoH, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float NoH2 = NoH * NoH;
float denom = NoH2 * (a2 - 1.0) + 1.0;
return a2 / (PI * denom * denom);
}

3.5.4 菲涅尔项:Schlick 近似

菲涅尔效应描述了一个日常现象:从正面看水面是透明的,从侧面看水面是反射的。所有材质都有这个特性。

完整的菲涅尔公式(Fresnel Equation)非常复杂,Schlick (1994) 提出了一个优雅的近似:

$$F_{Schlick}(\cos\theta) = F_0 + (1 - F_0) \cdot (1 - \cos\theta)^5$$

其中 $F_0$ 是垂直入射时的反射率(法线方向的菲涅尔值)。

$F_0$ 的典型值

材质 $F_0$ (sRGB) $F_0$ (线性) 说明
0.02 (0.02, 0.02, 0.02)
塑料 0.04 (0.04, 0.04, 0.04) 大多数非金属
玻璃 0.04-0.08
(1.00, 0.71, 0.29) 有色菲涅尔!
(0.91, 0.92, 0.92)
(0.95, 0.64, 0.54) 有色菲涅尔!

Shader 伪代码

1
2
3
4
5
6
7
8
9
// Schlick 菲涅尔近似
vec3 F_Schlick(float VoH, vec3 F0) {
return F0 + (1.0 - F0) * pow(1.0 - VoH, 5.0);
}

// 在 UE5/Metallic 工作流中,F0 的计算:
// 非金属(metallic=0): F0 = specular * 0.08 (默认 0.04)
// 金属(metallic=1): F0 = baseColor
vec3 F0 = mix(vec3(0.04), baseColor, metallic);

3.5.5 几何遮蔽项

微平面之间会相互遮挡——这在高粗糙度和掠射角时尤为明显。

Smith-GGX 模型将遮蔽分为两个独立部分:

$$G(\omega_i, \omega_o) = G_1(\omega_i) \cdot G_1(\omega_o)$$

其中 $G_1$ 是方向无关的遮蔽函数。UE4/5 使用的是 Smith-Joint 近似,它是 Smith-GGX 的简化版:

1
2
3
4
5
6
7
8
// UE4 Smith-Joint 近似
float G_SmithJoint(float NoV, float NoL, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float Vis_SmithV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2);
float Vis_SmithL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2);
return 0.5 / max(Vis_SmithV + Vis_SmithL, 1e-5);
}

3.5.6 完整的 PBR Shader 片段

把所有部分组合起来,一个完整的直接光照 PBR 计算如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// PBR 直接光照计算(伪代码)
vec3 PBR_DirectLight(vec3 N, vec3 V, vec3 L, vec3 baseColor,
float metallic, float roughness) {
vec3 H = normalize(V + L); // 半程向量
float NoL = max(dot(N, L), 0.0);
float NoV = max(dot(N, V), 0.0);
float NoH = max(dot(N, H), 0.0);
float VoH = max(dot(V, H), 0.0);

// 1. 计算基础 F0
vec3 F0 = mix(vec3(0.04), baseColor, metallic);

// 2. Cook-Torrance 镜面反射
float D = D_GGX(NoH, roughness); // 法线分布
vec3 F = F_Schlick(VoH, F0); // 菲涅尔
float G = G_SmithJoint(NoV, NoL, roughness); // 几何遮蔽

vec3 specular = (D * F * G) / max(4.0 * NoV * NoL, 0.001);

// 3. 漫反射(金属没有漫反射)
vec3 kD = (1.0 - F) * (1.0 - metallic);
vec3 diffuse = kD * baseColor / PI;

// 4. 最终颜色 = (漫反射 + 镜面反射) × 光照强度
return (diffuse + specular) * lightColor * NoL;
}

3.5.7 多层材质混合的演进

很多真实材质是多层结构——车漆(清漆+底漆)、湿润的地面(水层+地面)、皮肤(油脂+表皮+真皮)。

多层混合的三种方案

方案 思路 优点 缺点
结果混合 分别计算两层 BRDF,混合结果 简单 物理不正确(能量不守恒)
参数预混合 在 BRDF 计算前混合参数 部分正确 无法表达层间多次反射
分层模型(Slab) 每层独立光学计算,层间传播 物理正确 计算量大

UE5 的 Strata 系统是第三种方案的代表——它用”slab”概念表达每层的光学结构(折射率、吸收系数、散射系数),层与层之间通过物理公式传播光线,实现了更准确的线性混合。


3.6 NPR(非真实感渲染)风格化

NPR 是游戏渲染的另一半天空——原神、塞尔达、纪念碑谷等风格化游戏都使用 NPR。

3.6.1 核心原则:所见即所得

NPR 的核心哲学是:美术画什么,玩家就看到什么。Shader 的角色是忠实地呈现美术的意图,而不是用公式计算物理正确的结果。

具体实现手段

  1. 贴图即结果:美术画的贴图直接对应最终效果——亮度、阴影、高光都在贴图中预先定义好。

  2. 光照烘焙(Baked Lighting in Texture):光照信息预烘焙到贴图中。原神的角色阴影就是一个经典的阶梯函数——通过色阶贴图(Ramp Texture)控制阴影的过渡:

1
2
3
4
5
6
7
8
// 阶梯色阶阴影(Cel Shading 核心)
float NdotL = dot(N, L);
// 将 NdotL 映射到 [0,1] 并采样色阶贴图
float rampUV = NdotL * 0.5 + 0.5;
// 色阶贴图:硬编码的明暗分界线
vec3 shadeColor = texture(rampTexture, vec2(rampUV, 0.5)).rgb;
// 最终颜色 = 基础色 × 色阶结果
vec3 finalColor = baseColor * shadeColor;
  1. 边缘光(Rim Light):利用菲涅尔系数实现角色与背景的视觉分离:
1
2
3
4
// 边缘光(Rim Light / Fresnel Outline)
float rimFactor = 1.0 - max(dot(N, V), 0.0);
rimFactor = pow(rimFactor, rimPower); // 通常 power = 3-5
vec3 rimColor = rimFactor * rimLightColor * rimIntensity;
  1. 屏幕空间轮廓描边:在屏幕空间做卷积得到更精细的边缘检测:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Sobel 边缘检测描边(后处理方式)
float SobelOutline(sampler2D depthTex, vec2 uv, vec2 texelSize) {
float depth[9];
// 3x3 邻域采样
depth[0] = texture(depthTex, uv + texelSize * vec2(-1,-1)).r;
depth[1] = texture(depthTex, uv + texelSize * vec2( 0,-1)).r;
depth[2] = texture(depthTex, uv + texelSize * vec2( 1,-1)).r;
depth[3] = texture(depthTex, uv + texelSize * vec2(-1, 0)).r;
// ... 省略其余5个采样

// Sobel 算子
float sobelX = depth[2] + 2*depth[5] + depth[8]
- depth[0] - 2*depth[3] - depth[6];
float sobelY = depth[0] + 2*depth[1] + depth[2]
- depth[6] - 2*depth[7] - depth[8];
return sqrt(sobelX * sobelX + sobelY * sobelY);
}

3.6.2 描边的多种实现方式

描边是 NPR 中最标志性的视觉效果。不同游戏根据性能和风格需求选择了不同的方案:

方案 原理 优点 缺点 代表游戏
法线外扩 两遍渲染,第二遍沿法线放大 线宽一致 需要额外 pass 崩坏3、战双帕弥什
后处理边缘检测 深度/法线 buffer 做 Sobel 简单 线宽不均 Guilty Gear Xrd
SDF 描边 利用 SDF 距离场 精确控制 需要额外贴图 部分手游
几何着色器 沿法线生成额外三角形 GPU 高效 几何着色器慢

法线外扩描边的 Shader 实现:

1
2
3
4
5
6
7
// 顶点着色器:沿法线方向外扩
// 第二遍渲染,Cull Front(只渲染背面)
vec3 outlineOffset = normal * outlineWidth;
gl_Position = MVP * vec4(position + outlineOffset, 1.0);

// 片元着色器:纯色输出
fragColor = outlineColor;

3.6.3 NPR 的特殊挑战

NPR 面临一些 PBR 不存在的独特问题:

  1. 面部阴影控制:由于卡通渲染的阴影是硬边缘的,角色的脸部稍微转动就可能导致阴影突变。解决方案是用一张 SDF 面部阴影贴图 精确控制面部明暗分界线的形状。

  2. 多光源处理:PBR 天然支持多光源叠加,但 NPR 的色阶阴影一旦叠加就会失去风格化特征。通常只取主导光源做色阶,其余光源做简单叠加。

  3. LOD 一致性:角色远距离时,描边线宽需要随距离缩放,否则会出现”远景描边比角色还粗”的问题。


3.7 皮肤渲染(Skin Rendering)

皮肤是角色渲染中最复杂的特殊材质之一。理解皮肤渲染需要从皮肤的物理结构出发。

3.7.1 为什么皮肤不能直接用 PBR?

皮肤的物理结构是多层的半透明介质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
光线进入皮肤后的行为:

光线 → → → → → → →
╔══════════════════════╗
║ 油脂层(Sebum) ║ ← 约 1-2μm,产生高光反射
╠══════════════════════╣
║ 表皮(Epidermis) ║ ← 约 0.05-0.1mm,轻微散射
╠══════════════════════╣
║ 真皮(Dermis) ║ ← 约 1-4mm,主要散射层
║ · 血管 · 胶原蛋白 ║ ← 红色血液吸收短波光
║ · 色素细胞 · ║ 导致散射光偏红
╠══════════════════════╣
║ 皮下组织(Subcutis) ║ ← 脂肪层,进一步散射
╚══════════════════════╝

关键现象:光线进入皮肤后,在真皮层中经历多次散射,路径长度可达数毫米。短波光(蓝色)被血红蛋白大量吸收,而长波光(红色)更容易穿透——这就是为什么手指对着光看会呈现红色透光效果。

3.7.2 次表面散射(Subsurface Scattering, SSS)

SSS 是让皮肤看起来”活”的关键。没有 SSS 的皮肤看起来像塑料,因为光线在边界处被硬切断——而真实的皮肤中,光线会在皮下扩散,产生柔和的明暗过渡。

核心现象

  • 正面通透感:亮部到暗部的柔和过渡——阴影边界不是锐利的,而是模糊的
  • 背面透射:光从背面穿透薄皮肤(耳朵、手指之间的缝隙发红光)
  • 细节高光:油脂层产生的细小高光——比一般的 Specular 更锐利

3.7.3 SSS 的实现方案

方案 原理 性能 适用场景 代表游戏/引擎
纹理空间模糊(TSSS) 在 UV 空间做高斯模糊 主机/PC UE4 早期方案
屏幕空间模糊(SSSSS) 在屏幕空间沿法线方向模糊 中-高 主机/PC GDC 2015 方案
预积分皮肤 Shader 用预计算 LUT 近似 SSS 移动端/全平台 UE4 Mobile
可分离 SSS 将 2D 模糊分离为两次 1D 中-高 主机/PC UE4/UE5 默认
路径追踪 SSS 随机游走模拟 极高 离线/RTX UE5 Lumen+RT

预积分皮肤 Shader 是移动端最实用的方案。核心思想:预计算一个 LUT(查找表),输入曲率和 NdotL,输出 SSS 近似结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 预积分皮肤 Shader(Penner & Borshukov 2011)
// 核心思想:用预计算的 LUT 替代运行时的模糊操作
vec3 PreintegratedSkin(float NdotL, float curvature) {
// curvature = 1/radius,反映皮肤曲面的弯曲程度
// 曲率越大(如鼻尖),SSS 效果越明显

// 采样预积分 LUT
vec2 uv = vec2(NdotL * 0.5 + 0.5, curvature);
vec3 sssColor = texture(sssLUT, uv).rgb;

// 叠加细节高光(油脂层)
float specular = D_GGX(NoH, 0.3); // 低粗糙度 = 锐利高光

return sssColor * diffuseColor + specular * vec3(1.0);
}

为什么预积分有效? SSS 的核心效果是模糊阴影边界和增加暗部亮度。通过对不同曲率的表面预计算这个模糊效果,可以用一个简单的纹理采样代替运行时的卷积操作。

曲率(Curvature) 的计算:

1
2
3
4
// 从法线贴图和几何法线计算曲率
float curvature = length(fwidth(normalWorld)) / length(fwidth(positionWorld));
// 或者从预烘焙的曲率贴图采样
float curvature = texture(curvatureMap, uv).r;

3.7.4 背面透射

耳朵、手指等薄皮肤部位的背面透射效果:

1
2
3
4
5
6
// 简化的背面透射
float NdotLBack = dot(N, -L); // 背面光照
float thickness = texture(thicknessMap, uv).r; // 预烘焙的厚度贴图
// 指数衰减:越厚,透射越少
float transmittance = exp(-thickness * scatterDistortion);
vec3 transColor = transmittance * subsurfaceColor * NdotLBack * lightColor;

3.8 头发渲染(Hair Rendering)

头发渲染的特殊性在于:头发是圆柱形的纤维,不是平面。这导致光在发丝上的行为与普通表面完全不同。

3.8.1 头发的光学特性

一根头发纤维的光路行为有三种:

1
2
3
4
5
6
7
8
9
10
11
12
     光线 →

╭───────────╮
R: 直接反射 │ ← 光在表面直接反射(高光)
│ ╭─────╮ │
│ │ TT: │ │ ← 光进入纤维,从另一侧穿出
│ │同侧透│ │ (透射,导致头发半透明感)
│ │射 │ │
│ ╰─────╯ │
TRT: │ ← 光进入纤维,在内壁反射一次后穿出
│ 穿透散射 │ (导致头发颜色偏移和高光加宽)
╰───────────╯
  • R(Reflection):直接反射,产生主要高光
  • TT(Transmission-Transmission):同侧透射,产生头发的半透明感
  • TRT(Transmission-Reflection-Transmission):穿透散射,产生次级高光和颜色偏移

3.8.2 Kajiya-Kay 模型(1989)

Kajiya-Kay 是最早的头发光照模型之一,至今仍是手游的主流选择——因为它足够简单。

核心思想:用切线方向(而非法线)计算高光。因为头发是圆柱形的,法线在截面上处处不同,但切线方向是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Kajiya-Kay 高光计算
float KajiyaKaySpec(vec3 T, vec3 H, float exponent) {
// T = 头发切线方向
// H = 半程向量
float TdotH = dot(T, H);
// sin(theta) 近似
float sinTheta = sqrt(1.0 - TdotH * TdotH);
return pow(sinTheta, exponent);
}

// 最终颜色
float spec1 = KajiyaKaySpec(tangent, H, primaryExponent); // 主高光
float spec2 = KajiyaKaySpec(tangentShifted, H, secondaryExponent); // 次高光(切线偏移)
vec3 hairColor = diffuse + spec1 * vec3(1.0) + spec2 * hairBaseColor;

优点:计算简单,一个 dot + pow 就搞定。移动端友好。
缺点:物理不准确——没有区分 R/TT/TRT 三条光路,高光形状是各向同性的(真实头发的高光应该沿发丝方向拉伸)。

3.8.3 Marschner 模型(2003)

Marschner 模型完整地解决了 R、TT、TRT 三条光路,是 3A 游戏头发渲染的标准方案。

核心贡献

  • 建立了头发纤维的椭圆截面模型
  • 推导了每条光路的精确 BRDF
  • 发现了高光会随光线入射角偏移(不是在法线方向上,而是在切线方向上偏移)

实现方案:完整的 Marschner 计算量很大(需要大量三角函数和指数运算),所以实际游戏中都用预计算 LUT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Marschner LUT 方案
// 预计算 4 张 LUT,运行时查表
// M_R, M_TT, M_TRT:纵向散射函数
// N_R, N_TT, N_TRT:方位角散射函数

vec3 MarschnerHair(vec3 T, vec3 V, vec3 L) {
float cosThetaI = dot(T, L);
float cosThetaR = dot(T, V);

// 采样预计算 LUT
vec3 R = texture(LUT_M_R, vec2(cosThetaI, cosThetaR)).rgb;
vec3 TT = texture(LUT_M_TT, vec2(cosThetaI, cosThetaR)).rgb;
vec3 TRT = texture(LUT_M_TRT, vec2(cosThetaI, cosThetaR)).rgb;

return R + TT + TRT;
}

实际使用中,Frostbite(EA)和 UE5 都采用了 Marschner 的简化变体,通常只保留 R 和 TT 项(TRT 用近似代替),并将 LUT 压缩到更小的纹理中。

3.8.4 头发建模方案

方案 面数 效果 性能开销 适用场景
Card(卡片) 低(几十个面片) 中等 手游、远处 LOD
Strand(发丝级) 高(每根发丝一条曲线) 最好 高(需特殊管线) 3A 近景

Strand Hair 在 UE5 中通过 Groom 系统实现,支持物理模拟和自阴影。但发丝级渲染的开销非常大——一个角色可能有 10000+ 根发丝,每根都要独立计算光照。


3.9 眼球渲染(Eye Rendering)

眼球是角色中最复杂的小型渲染对象。关键在于精确的结构拆解——眼球不是一个球体,而是一个多层光学系统。

3.9.1 眼球的光学结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─────────────────────────────────────────────┐
│ 眼球结构(剖面图) │
│ │
│ 角膜(Cornea) │
│ ┌──────┐ ← 折射率 n1.376
│ │ ╭──╮│ ← 凸面,产生折射+反射 │
│ │ │虹││ │
│ │ │膜││ ← 色素纹理 + 散射 │
│ │ │ ││ │
│ │ ╰──╯│ │
│ │ 瞳孔 │ ← 瞳孔深度(光线进入内部的开口) │
│ └──────┘ │
│ │
│ 巩膜(Sclera) │
│ ┌──────────────────┐ │
│ │ 血管纹理 + SSS │ ← 白眼球,有次表面散射 │
│ └──────────────────┘ │
│ │
│ 泪膜(Tear Film) │
│ 最外层薄膜 → 产生锐利的高光反射点 │
│ │
└─────────────────────────────────────────────┘

3.9.2 眼球渲染的技术要点

1. 角膜折射

角膜是一个凸面透镜,光线在进入眼球时会发生折射。使用 Snell 定律:

$$n_1 \sin\theta_1 = n_2 \sin\theta_2$$

其中 $n_1 = 1.0$(空气),$n_2 \approx 1.376$(角膜)。

1
2
3
4
5
6
7
8
9
10
11
12
// Snell 折射(简化版)
vec3 Refract(vec3 incident, vec3 normal, float eta) {
float cosI = -dot(incident, normal);
float sinT2 = eta * eta * (1.0 - cosI * cosI);
float cosT = sqrt(1.0 - sinT2);
return eta * incident + (eta * cosI - cosT) * normal;
}

vec3 corneaNormal = ...; // 角膜曲面法线
vec3 refractedDir = Refract(viewDir, corneaNormal, 1.0 / 1.376);
// 用折射后的方向采样虹膜纹理
vec3 irisColor = texture(irisMap, refractedUV).rgb;

2. 虹膜散射

虹膜的纹理不是简单的平面贴图——它是一个半透明的散射介质。光线进入虹膜后会在内部散射,导致虹膜颜色看起来有深度感。

3. 巩膜 SSS

白眼球(巩膜)不是纯白的——它有血管纹理,并且表现出次表面散射效果。通常使用与皮肤相同的 SSS Shader,但参数不同。

4. 泪膜高光

泪膜是角膜表面的一层极薄的液体,产生非常锐利的高光反射点。这个高光是眼球看起来”有神”的关键。

3.9.3 最新趋势

  • 肌肉跟随:眼球的运动不是简单的旋转——周围有 6 条肌肉控制,限制了眼球的运动范围和速度
  • Look At 功能:角色的视线追踪目标,需要精确的 IK 计算
  • 微表情:瞳孔大小变化、眨眼频率、视线方向都是角色表现力的关键
  • Caustics inside eye:光在眼球内部产生的焦散效果,让虹膜看起来更有层次

角色渲染小结

核心原则:用更低成本达到更好效果。

渲染技术 性能开销 视觉提升 推荐平台
标准 PBR 基准 基准 全平台
SSS 皮肤 中-高 显著 PC/主机
预积分 SSS 全平台
Kajiya-Kay 头发 移动端优先
Marschner 头发 中-高 显著 PC/主机
精细眼球 中(近距离才有效果) 全平台
NPR 色阶 取决于风格 全平台

PBR 方案因标准化工作流而成本更低;NPR 方案需自定义工具链,成本较高。**选择的关键不是”哪个更好”,而是”哪个更适合你的游戏风格和目标平台”**。


四、场景渲染技术详解

场景渲染的目标是构建一个可信的游戏世界。与角色渲染不同,场景渲染的核心挑战不是”单个物体的质量”,而是”大规模内容的效率”。

4.1 场景制作的 PCG 流程

现代 3A 游戏的场景规模已经达到了惊人的程度——《荒野大镖客2》的地图约 29 平方英里,纯手工制作是不可能的。程序化内容生成(PCG) 是解决方案。

步骤 工具/技术 输出 人工干预程度
地形生成 高度图雕刻(World Machine/Gaea) 基础地貌
河流切割 程序化侵蚀模拟 河道、冲积扇
悬崖细节 Houdini 程序化建模 岩石细节
植被布置 生态刷子(Scatter Tool) 树木、草地
建筑放置 PCG + 手动调整 村庄、城市
最终整合 场景编辑器(UE5/Unity) 完整场景

UE5 的 PCG 框架是目前的最佳实践——它用图节点(Graph Node)描述生成规则,支持实时预览和迭代,并可以与 Houdini 等外部工具通过 Houdini Engine 插件对接。


4.2 地形渲染(Terrain Rendering)

4.2.1 何时需要地形系统?

场景规模 推荐方案 理由
< 200 米 普通网格模型 地形系统有额外开销,小场景不值得
200 米 - 1 公里 视情况选择 评估性能预算和内容需求
> 1 公里 地形系统(推荐) 网格模型数据量太大,必须用 LOD 管理
1000+ 平方公里 大世界地形系统 需要 Streaming + GPU Driven + 分页

4.2.2 高度图(Heightmap)

地形渲染的基础数据结构是高度图——一个 2D 数组,每个像素存储一个高度值。

1
2
3
4
5
6
7
高度图示例(灰度图表示):

= 高海拔 暗 = 低海拔
████████████ ░░░░░░░░░░░░
████████████ ░░░░░░░░░░░░
██████暗暗████ ░░░░河░░河░░
████████████ ░░░░░░░░░░░░

参数选择

  • 分辨率:通常 1 米/像素到 0.5 米/像素。一个 4km × 4km 的地形,1m 分辨率需要 16M 个采样点
  • 精度:8-bit(256 级,适合平缓地形)或 16-bit(65536 级,适合高山)
  • 存储:16-bit float(half float)是最佳选择——节省空间且精度足够

从高度图生成网格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 地形网格生成(Vertex Shader)
vec3 TerrainVertex(vec2 uv, sampler2D heightmap) {
float height = texture(heightmap, uv).r * heightScale;
vec3 position = vec3(uv.x * terrainSize, height, uv.y * terrainSize);

// 法线从高度图梯度计算
float hL = texture(heightmap, uv + vec2(-texelSize, 0)).r;
float hR = texture(heightmap, uv + vec2( texelSize, 0)).r;
float hD = texture(heightmap, uv + vec2(0, -texelSize)).r;
float hU = texture(heightmap, uv + vec2(0, texelSize)).r;
vec3 normal = normalize(vec3(hL - hR, 2.0 * texelSize * heightScale, hD - hU));

return position;
}

4.2.3 LOD(Level of Detail)

地形 LOD 的核心问题:远处的地形需要较少的三角形,近处的需要较多。如何在不产生裂缝(crack)的情况下实现平滑过渡?

Clipmap 方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Clipmap 层级示意(俯视):

┌─────────────────────────────────────┐
Level 0: 最粗(覆盖全区) │
│ ┌─────────────────────────┐ │
│ │ Level 1: 较粗 │ │
│ │ ┌───────────────┐ │ │
│ │ │ Level 2: │ │ │
│ │ │ ┌─────┐ │ │ │
│ │ │ │ L3 │← 最细│ │ │
│ │ │ │玩家 │ │ │ │
│ │ │ └─────┘ │ │ │
| │ └───────────────┘ │ │
│ └─────────────────────────┘ │
└─────────────────────────────────────┘

每一层的分辨率是上一层的一半,
只有玩家周围是最细的 LOD

四叉树(Quadtree)方案

将地形分成四叉树结构,每个节点可以独立选择 LOD 等级。UE4/UE5 的地形系统使用的就是这种方案。

1
2
3
4
5
6
7
8
9
10
11
12
四叉树 LOD
┌────────┐
Root
Level 0
└───┬────┘
┌─────┼─────┐
┌───┴──┐┌┴───┐┌─┴───┐┌────┐
L1-A ││L1-B││L1-C ││L1-D
└──┬───┘└────┘└─────┘└────┘
┌──┼──┐
│ │ │ ← 近处细分,远处不细分
L2 L2 L2

消除 LOD 裂缝(Crack Fixing)

当相邻的两个 LOD 等级不同时,会在接缝处产生”裂缝”。常见的解决方案:

  1. 裙边(Skirt):在 LOD 块的边缘向内添加一圈额外三角形,遮挡裂缝
  2. 过渡区(Transition):在接缝区域做几何 Morph(逐顶点插值)
  3. T-Junction 消除:在低 LOD 的一侧添加额外顶点匹配高 LOD

4.2.4 GPU Driven 地形

传统地形在 CPU 端生成网格,然后上传到 GPU。GPU Driven 方案直接在 GPU 上生成地形网格,CPU 只发送高度图和 LOD 参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// GPU Driven 地形(Compute Shader 生成 mesh)
[numthreads(8, 8, 1)]
void GenerateTerrainMesh(uint3 id : SV_DispatchThreadID) {
// 1. 根据 LOD 等级确定采样位置
float2 uv = (id.xy + 0.5) / gridResolution;

// 2. 采样高度图
float height = Heightmap.SampleLevel(sampler, uv, mipLevel);

// 3. 生成顶点
float3 position = float3(uv.x * terrainSize, height, uv.y * terrainSize);

// 4. 写入 Vertex Buffer
outputVertices[id.x + id.y * gridWidth] = position;
}

性能对比

方案 CPU 开销 GPU 开销 带宽 适用场景
CPU 生成 高(逐帧更新) 高(每帧上传 VB) 小地形
GPU Driven 极低 低-中 低(只传参数) 大世界
Mesh Shader 极低 低(硬件加速) 最低 RTX 30+ / RDNA2+

4.2.5 地形材质混合

真实的地形不是单一材质——山坡是岩石,平地是草地,低洼处是泥土。需要混合多种材质。

权重图(Splat Map / Weight Map)

每个像素存储各材质层的权重(总和 = 1)。例如 4 层材质需要 RGBA 四个通道。

1
2
3
4
5
6
7
8
9
10
11
// 地形材质混合
vec4 weights = texture(splatMap, uv); // 各层权重
vec3 grass = texture(grassAlbedo, uv * grassTiling).rgb;
vec3 rock = texture(rockAlbedo, uv * rockTiling).rgb;
vec3 dirt = texture(dirtAlbedo, uv * dirtTiling).rgb;
vec3 snow = texture(snowAlbedo, uv * snowTiling).rgb;

vec3 terrainColor = grass * weights.r
+ rock * weights.g
+ dirt * weights.b
+ snow * weights.a;

优化策略

  • 只保留权重最高的两层:大多数像素只有 1-2 层材质占主导,省略次要层可以减少采样次数
  • Virtual Texture(VT):将混合结果预计算到虚拟纹理中,运行时只需一次采样。UE5 的 Virtual Texture 系统就是这个思路

4.3 植被系统(Vegetation System)

植被渲染的核心挑战:在有限性能预算内渲染足够密度的植被。《荒野大镖客2》中有超过 300 种植物,每种可能有上千个实例。

4.3.1 渲染方案选择

方案 描述 三角形数 性能 代表游戏
Billboard 永远面向相机的面片 2 极低 远景植被
Cross-Plane 两个交叉面片 4 手游植被
Impostor 多角度烘焙到面片 8-24 中远景植被
简化 3D 模型 低面树模型 100-500 中-高 近景植被
完整 3D 模型 高精度模型 1000+ 极近距离

LOD 策略通常是组合使用:远处用 Billboard,中距离用 Impostor,近处用简化/完整模型。

4.3.2 创作方式

  • 密度图刷(Density Map Brush):用刷子绘制密度值,运行时根据阈值生成植被实例。适合大范围的草地、灌木。
  • 精确撒点(Manual Placement):手动指定每个植被的位置。适合需要精确控制的区域,如村庄周围的花园。
  • 生态规则(Ecological Rules):PCG 根据高度、坡度、湿度等参数自动决定植被种类和密度。UE5 PCG 框架的强项。

4.3.3 植被交互:风场与物理

植被不是静止的——风会吹动草和树,角色经过会压弯草丛。这些交互是提升沉浸感的关键。

风场模型(Wind Field)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 简化的植被风场 Shader
vec3 WindDisplacement(vec3 objectPos, float time, float windStrength) {
// 使用正弦波叠加模拟风的效果
// 不同频率的波叠加产生自然的风吹效果
float phase = objectPos.x * 0.5 + objectPos.z * 0.3;
float wind1 = sin(time * 2.0 + phase) * 0.3;
float wind2 = sin(time * 3.7 + phase * 1.3) * 0.1; // 高频细节
float wind3 = sin(time * 0.5 + phase * 0.2) * 0.6; // 低频主导

float totalWind = (wind1 + wind2 + wind3) * windStrength;

// 顶部位移大,底部位移小(模拟根部固定)
float heightFactor = objectPos.y / maxPlantHeight;
heightFactor = heightFactor * heightFactor; // 二次方增长

return vec3(totalWind * heightFactor, 0.0, totalWind * 0.5 * heightFactor);
}

角色交互:角色附近绑定一个”风力源”,当角色经过植被时,风力源对附近的植被施加力——本质上就是牛顿力学 F = ma。植被的弯曲程度与距离成反比。


4.4 水体渲染(Water Rendering)

水是游戏场景中最复杂也最迷人的渲染对象。真实的水体需要同时处理反射、折射、波浪、焦散等多个光学现象。

4.4.1 波浪模拟

水体表面的波浪可以用不同的数学模型描述:

方案 原理 复杂度 适用场景
正弦波叠加 多个不同频率的正弦波叠加 简单水面
Gerstner Wave 圆形运动的水面质点 写实海面
FFT 海洋 频域统计模型 + FFT 3A 海洋
物理模拟(Shallow Water) Navier-Stokes 简化 极高 河流、水池交互

Gerstner Wave 是最常见的方案,它比简单的正弦波更真实——因为 Gerstner Wave 的水面质点做的是圆形运动,产生的波峰更尖、波谷更宽,这与真实海浪的形状一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Gerstner Wave(单一波浪)
vec3 GerstnerWave(vec2 position, vec2 direction, float steepness,
float wavelength, float time) {
float k = 2.0 * PI / wavelength; // 波数
float c = sqrt(9.8 / k); // 相速度(深水波)
float a = steepness / k; // 振幅

vec2 d = normalize(direction);
float f = k * (dot(d, position) - c * time);

return vec3(
d.x * a * cos(f), // 水平位移
a * sin(f), // 垂直位移
d.y * a * cos(f) // 水平位移
);
}

// 通常叠加 4-8 个 Gerstner Wave 得到自然的水面
vec3 totalDisplacement = vec3(0.0);
totalDisplacement += GerstnerWave(pos, dir1, steep1, wl1, time);
totalDisplacement += GerstnerWave(pos, dir2, steep2, wl2, time);
totalDisplacement += GerstnerWave(pos, dir3, steep3, wl3, time);
// ... 更多波

FFT 海洋是最高端的方案,基于 Tessendorf (2001) 的统计模型。核心思想:海洋表面可以看作大量不同频率、不同方向的波浪的叠加——这在频域中可以用 FFT 高效计算。代表游戏:《盗贼之海》、《死亡搁浅》。

4.4.2 水体光照成分

水体渲染需要同时处理多个光学现象:

1. 反射(Reflection)

水面像一面镜子,反射天空和周围的场景。

方案 原理 质量 性能
Planar Reflection 从水面下方翻转会渲染整个场景 高(额外一帧渲染)
SSR(Screen Space Reflection) 在屏幕空间追踪反射光线
Cubemap Reflection 用预烘焙的环境贴图
Ray Traced Reflection 光线追踪 最高 极高(需 RTX)

2. 折射(Refraction)

透过水面看到的水下场景。使用 Snell 定律偏移水下图像的 UV:

1
2
3
4
5
6
// 水面折射
vec2 refractUV = screenUV + normal.xy * refractionStrength;
vec3 waterBottom = texture(sceneColorTex, refractUV).rgb;
// 深度调制:越深,折射越不明显
float depthFactor = exp(-waterDepth * absorptionCoeff);
vec3 refractedColor = mix(deepWaterColor, waterBottom, depthFactor);

3. 焦散(Caustics)

水面波浪聚焦光线形成的光斑——就像游泳池底部的波纹光网。

1
2
3
4
5
// 简化的焦散效果
vec2 causticUV = worldPos.xz * causticScale + time * causticSpeed;
float caustic = texture(causticTexture, causticUV).r;
// 焦散强度随深度衰减
float causticIntensity = caustic * exp(-depth * causticFalloff);

4. 次表面散射(SSS)

水的通透感——阳光照射下的水面边缘会发出蓝绿色的光。这与皮肤 SSS 的原理相同。

5. 深浅颜色渐变

浅水区域(岸边)呈现透明的浅蓝绿色,深水区域呈现深蓝色。使用指数衰减模拟水的吸收:

1
2
3
4
// 水的吸收系数(Beer-Lambert 定律)
vec3 absorptionCoeff = vec3(0.4, 0.1, 0.01); // R 吸收多,B 吸收少
float opticalDepth = waterDepth * 2.0; // 光学路径长度
vec3 waterColor = baseWaterColor * exp(-absorptionCoeff * opticalDepth);

4.4.3 水体网格 LOD(CDLOD)

水体使用 CDLOD(Continuous Distance LOD) 实现平滑过渡:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CDLOD 原理:

近距离:高密度网格(每米一个顶点)
┌─┬─┬─┬─┬─┬─┬─┬─┐
├─┼─┼─┼─┼─┼─┼─┼─┤
├─┼─┼─┼─┼─┼─┼─┼─┤
└─┴─┴─┴─┴─┴─┴─┴─┘

远距离:低密度网格
┌───────────────┐
│ │
│ │
└───────────────┘

过渡区:两套 LOD 共存,渐变混合
两套网格重叠,alpha 从 01 过渡

4.5 天气系统:天空、大气、云、雾

这一组技术有一个统一的核心算法:Ray Marching(光线步进)。所有介质(大气、云、雾)的光照问题都可以通过这一算法求解。

4.5.1 Ray Marching 核心算法

Ray Marching 的思想非常直观:沿着光线方向一步步前进,每一步计算该位置的密度和光照贡献,累积起来就是最终效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Ray Marching 伪代码(体积渲染通用框架)
vec4 RayMarch(vec3 rayOrigin, vec3 rayDir, float maxDist) {
vec3 accumulatedColor = vec3(0.0);
float accumulatedAlpha = 0.0;

float stepSize = maxDist / NUM_STEPS;

for (int i = 0; i < NUM_STEPS; i++) {
vec3 pos = rayOrigin + rayDir * (i * stepSize);

// 1. 采样该位置的密度
float density = SampleDensity(pos);

if (density > 0.0) {
// 2. 计算该点的光照(向光源方向也做一次 Ray March)
float lightDensity = SampleLightDensity(pos, lightDir);
float transmittance = exp(-lightDensity * absorptionCoeff);

// 3. 累积颜色和透明度
vec3 localColor = scatteringCoeff * density * transmittance * lightColor;
accumulatedColor += localColor * (1.0 - accumulatedAlpha);
accumulatedAlpha += density * stepSize * (1.0 - accumulatedAlpha);
}

// 提前退出
if (accumulatedAlpha > 0.99) break;
}

return vec4(accumulatedColor, accumulatedAlpha);
}

4.5.2 物理大气(Atmospheric Scattering)

天空不是蓝色的”背景板”——它是一个巨大的发光球壳。蓝色的天空是大气散射的结果。

经典论文:Preetham et al. (2008)《A Practical Analytic Model for Daylight》

物理模型:两层壳模型

1
2
3
4
5
6
7
8
9
10
11
╭─────────────────────────╮  ← 大气层顶部(约 100km)
│ 外层大气 │
│ 稀薄气体 │
│ │
│ ←──── 光线散射 ────→ │
│ │
│ │
╰──────────┬──────────────╯
┌────┴────┐
│ 地球表面 │ ← 地形/海面
└─────────┘

两种散射类型

散射类型 粒子大小 效果 公式
瑞利散射(Rayleigh) << 光波长 蓝天、日落红霞 $\sigma_s \propto \frac{1}{\lambda^4}$
米氏散射(Mie) ≈ 光波长 雾天、光晕 前向散射为主

为什么天空是蓝色的? 瑞利散射强度与波长的四次方成反比。蓝光(λ≈450nm)的散射强度约是红光(λ≈650nm)的 $(650/450)^4 ≈ 4.3$ 倍——所以白光经过大气后,蓝光被大量散射到各个方向,形成了蓝色的天空。

为什么日落是红色的? 日落时太阳光穿过的大气层更厚,蓝光已经被散射殆尽,只剩下红光和橙光能到达你的眼睛。

预计算 LUT 方案

完整的物理大气计算每帧开销太大(每像素都要做 Ray Marching),所以业界使用预计算查找表(LUT)

1
2
3
4
5
6
7
8
// 大气散射预计算 LUT(通常是 3D 或 2D 纹理)
// 输入参数:
// u = 视角高度(0-1 映射到 0-100km)
// v = 视角与天顶的夹角(0-π)
// w = 太阳与天顶的夹角(0-π)

vec3 atmosphericColor = texture(atmosphereLUT,
vec2(viewZenith, sunZenith)).rgb;

多次散射:光在大气中会经历多次散射(散射出去的光又散射回来)。单次散射只能产生简单的蓝天效果,而多次散射才能产生真实的地平线泛白、日落时的橙色渐变。实现方式是迭代 LUT——用前一次的结果作为输入计算下一次。

4.5.3 大气雾(Aerial Perspective)

问题:传统的高度雾(Height Fog)与物理大气渲染是割裂的——天空用的是大气散射,地面用的是简单的指数雾。两者在衔接处会有明显的颜色/密度跳变。

解决方案:使用深度切片(Froxel)将大气计算融入统一的框架。

1
2
3
4
5
6
7
8
9
10
11
12
13
深度切片示意:

相机 ──────────────────────────→ 远处

F1F2F3F4F5F6F7│F8│F9│F10│
│ │ │ │ │ │ │ │ │ │ │
近 远

每个切片(Froxel)存储:
- 散射亮度(Scattering Luminance)
- 透射率(Transmittance)

通常分辨率:32×32×1664×64×32

通过在每个 Froxel 中计算散射和透射率,大气雾与天空渲染完全统一——不再有”天空”和”雾”的割裂感。

4.5.4 体积云(Volumetric Cloud)

体积云是场景渲染中最耗性能但也最壮观的效果。

核心方案:3D 密度场 + Ray Marching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
体积云渲染流程:

1. 生成 3D 密度场
→ 使用噪声函数描述云的形状
→ Perlin/Worley 噪声叠加

2. Ray Marching 采样
→ 沿视线方向步进
→ 每步采样密度

3. 光照计算
→ 向光源方向也做 Ray Marching
→ 累积散射光

4. 合成到场景
→ 云的 alpha 混合到场景颜色上

噪声函数:云的形状由噪声函数定义。

噪声类型 特点 用途
Perlin Noise 平滑的梯度噪声 云的大致形状
Worley/Voronoi 细胞状结构 云的细节(蓬松感)
分形叠加(FBM) 多个八度叠加 自然的多尺度细节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 云密度采样(FBM 噪声)
float CloudDensity(vec3 pos) {
// 基础形状:低频 Perlin 噪声
float base = perlinNoise(pos * 0.001);

// 细节层:高频 Worley 噪声叠加
float detail = worleyNoise(pos * 0.01) * 0.5
+ worleyNoise(pos * 0.02) * 0.25
+ worleyNoise(pos * 0.04) * 0.125;

// 高度曲线:云只在特定高度范围存在
float heightFade = 1.0 - smoothstep(cloudBase, cloudBase + cloudThickness, pos.y);

// 最终密度 = 基础形状 × 细节 × 高度
float density = max(0.0, base - cloudThreshold) * (1.0 - detail * 0.5) * heightFade;

return density;
}

云的层级化方案(根据性能预算选择):

类型 实现方式 性能 视觉质量 适用平台
雾(低层云) 体积雾方案 全平台
普通云层 Billboard/Impostor 全平台
精灵云 精灵层叠 中-高 主机
体积云 Ray Marching 密度场 PC/主机
高品质体积云 多散射+锥形采样 极高 最高 高端 PC

性能数据参考(1080p 分辨率):

云方案 Ray March 步数 GPU 时间 代表
简单体积云 32 步 ~2ms Unity 默认
中等体积云 64 步 ~4ms UE4 插件
高品质体积云 128 步 + light march ~8-12ms 《地平线》
超高品质 256 步 + multi-scatter ~15-20ms 《死亡搁浅》

4.5.5 天气系统的时间控制(TOD)

TOD(Time of Day) 系统是连接所有天气元素的控制层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
TOD 参数 → 控制所有天气元素:

太阳角度 ──┬──→ 大气散射颜色(日出/正午/日落/夜空)
├──→ 阴影方向和长度
├──→ 体积云的光照颜色
└──→ 环境光强度和颜色

云量 ──────┬──→ 云的密度阈值
├──→ 散射光强度(阴天 vs 晴天)
└──→ 地面直射光 vs 间接光比例

风向/风速 ─┬──→ 云的移动速度和方向
├──→ 植被摆动幅度
└──→ 粒子(雨/雪)方向

湿度 ──────┬──→ 雾的浓度
├──→ 地面湿润度(反射率变化)
└──→ Puddle(水洼)的面积

UE5 的天气系统通过一个统一的 Sky Atmosphere 组件 + Exponential Height Fog + Volumetric Cloud 三者联动实现 TOD 控制。


场景渲染小结

场景制作的三大挑战

挑战 说明 核心技术
内容 创作工具(笔刷、PCG、AIGC)决定生产效率 Houdini, UE5 PCG, AI 辅助
性能 LOD、剔除、流送、GPU Driven Nanite, Virtual Texture, Streaming
效果 风/水交互、场景破坏、UGC 挖洞等 物理模拟, Destructible Mesh

五、总结与实战指南

5.1 渲染技术选择原则

场景 推荐方案 理由
写实类 3A 游戏(PC/主机) PBR + 物理准确方案 + SSS + Marschner 硬件预算充足,追求极致效果
移动端写实游戏 简化 PBR + LUT 预计算 + Kajiya-Kay 性能敏感,需要预计算优化
二次元/风格化 NPR + 所见即所得 + 色阶阴影 艺术风格驱动,不是物理正确驱动
混合风格 分层材质系统(如 UE5 Strata) 支持多层材质的物理正确混合
开放世界 LOD + Streaming + GPU Driven 大规模场景管理的核心
竞技/电竞 最低延迟 + 稳定帧率 > 画质 画面质量服从竞技需求

5.2 不同平台的技术选型对比

技术领域 PC/主机(PS5/XSX) 移动端(骁龙8 Gen3) Web/小程序
渲染架构 延迟渲染 / Forward+ 前向渲染(TBDR) 前向渲染
PBR Cook-Torrance 完整版 简化版(无 GGX 长尾) 基础 PBR
阴影 CSM + Contact Shadow CSM + 简化 单一 Shadow Map
SSS 屏幕空间/可分离 预积分 LUT 无 / 简化 tint
头发 Marschner + Strand Kajiya-Kay + Card Card / 无特殊处理
地形 LOD GPU Driven + Nanite 简化 Clipmap 固定 LOD
水体 Gerstner + FFT 正弦波叠加 法线贴图模拟
大气 物理大气 + 体积云 简化天空盒 天空盒
后处理 TAA + SSR + Bloom FXAA + 简化 Bloom 基础后处理
光追 RT 反射/阴影/GI
帧率目标 60fps(4K)/ 30fps(光追) 30-60fps(1080p) 30fps(720p)

5.3 性能优化通用套路

  1. LOD 策略:距离越远,细节越少。不只是几何 LOD,还有 Shader LOD(远处用更简单的材质)和阴影 LOD(远处物体不产生阴影)。

  2. 剔除优化

    • 视锥剔除(Frustum Culling):不在视野内的物体不渲染
    • 遮挡剔除(Occlusion Culling):被其他物体遮挡的不渲染
    • 距离剔除(Distance Culling):超过一定距离的小物体直接不渲染
  3. 流送管理(Streaming):只加载玩家周围的资源。Mipmap 的本质就是一种流送——远处用低分辨率贴图。

  4. GPU Driven:让 GPU 承担更多工作。Nanite 的核心思想就是完全在 GPU 端管理 LOD 和剔除。

  5. 预计算 LUT:空间换时间。SSS、大气、BRDF 都可以用预计算查找表将运行时计算变为纹理采样。

  6. 实例化渲染(Instancing):相同几何体的多个实例,一次 Draw Call 渲染全部。植被系统的核心优化手段。

5.4 核心学习路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
学习路径(建议按此顺序):

基础(1-3个月)
├── 渲染管线架构
├── 渲染方程 → BRDF → 微表面模型
├── 光源模型(点/方向/聚光/面光源)
└── Shader 编程入门(GLSL/HLSL

进阶(3-6个月)
├── Disney Principled BRDFIBL(基于图像的光照)
├── SSS → 各向异性 → Clearcoat
├── 后处理管线(TAA, Bloom, Tonemapping
└── 阴影技术(CSM, PCF, VSM, Ray Traced

场景(6-12个月)
├── 地形 LODClipmap, Quadtree, GPU Driven
├── 植被渲染(Wind Shader, Instancing
├── 水体模拟(Gerstner, FFT, SSS
└── 大规模场景管理(Streaming, Virtual Texture

氛围(6-12个月)
├── 大气散射(Rayleigh, Mie, Preetham
├── Ray Marching 核心算法
├── 体积云(噪声函数, 3D 密度场)
└── 天气系统(TOD, 参数联动)

前沿(持续学习)
├── 实时光线追踪(RTX, DLSS
├── Nanite / Mesh Shader
├── Neural Rendering
└── AI 辅助渲染(神经辐射场, 3D Gaussian Splatting

写在最后

“渲染的本质是用最低的成本,达到最好的效果。”

游戏渲染是一个效果、性能、成本三者平衡的艺术。没有银弹方案,只有最适合当前项目的选择。

从渲染方程的数学精确性,到 NPR 色阶贴图的艺术自由度;从微表面模型的统计学优雅,到体积云 Ray Marching 的暴力美学——每一个像素背后,都是数学和工程的精密配合。

理解底层原理,不是为了能写出更复杂的公式,而是为了在遇到问题时能找到正确的近似方向。渲染方程的精确解不可能实时算出,但理解它之后,你就知道每一次近似牺牲了什么、保留了什么、在什么条件下近似会失效。

这正是游戏渲染技术的魅力所在——在无限的可能性中,找到有限预算下的最优解。


本文基于网易游戏学堂的技术分享整理扩展,融入了图形学经典论文和实际游戏引擎的实现经验。如有疏漏,欢迎指正。


游戏渲染技术全方位解析 - 从一根头发到一片云的技术全景
https://alex-rachel.github.io/2026/06/03/game-rendering-deep-dive/
作者
Alex
发布于
2026年6月3日
许可协议