游戏渲染技术全方位解析 🎮✨
本文基于网易游戏学堂的技术分享,深入拆解游戏渲染的核心原理——从角色皮肤上的次表面散射,到天空中飘过的体积云,看技术美术如何用代码和数学在屏幕上重构一个真实可感的世界。每个技术点都会讲清楚是什么、为什么、怎么做,辅以数学公式、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 数据到屏幕像素的完整流程:
123456789101112131415┌──────────────────────────────────────────────────────────────────────┐│ 渲染管线(Rendering Pipeline) │├──────────────────────────────────────────────────────────────────────┤│ ││ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌────────────────┐ ││ │ Application│ │ Geometry │ │ Rasterize │ │ Pixel/ │ ││ │ Stage │──▶│ Processing│──▶│ (光栅化) │──▶│ Fragment │ ││ │ (CPU 端) │ │ Stage │ │ │ │ Stage │ ││ └──────────┘ └──────────┘ └───────────┘ └────────────────┘ ││ ││ ① 场景遍历 ④ 顶点变换 ⑦ 三角形→像素 ⑩ 像素着色 ││ ② 视锥剔除 ⑤ 裁剪 ⑧ 重心坐标插值 ⑪ 纹理采样 ││ ③ 排序/合批 ⑥ 屏幕映射 ⑨ Early-Z 测试 ⑫ 混合/输出 ││ │└──────────────────────────────────────────────────────────────────────┘
关键阶段详解:
Application Stage(应用层):CPU 端执行碰撞检测、物理模拟、动画更新、可见性判断。这是唯一完全由开发者控制的阶段。
Geometry Processing(几何处理):顶点着色器(Vertex Shader)执行模型-视图-投影变换(MVP)、法线变换、骨骼蒙皮。可选的曲面细分(Tessellation)和几何着色器(Geometry Shader)在此阶段运行。
Rasterization(光栅化):将三角形转换为像素片段(fragments)。这一步由 GPU 硬件固定管线完成,速度极快。
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 时间变化)
营造沉浸感
⭐⭐⭐
为什么是这三个模块?这不是随意的分类,而是反映了游戏画面构成的物理本质:
角色是玩家注意力的焦点——人脸识别的本能让玩家对角色的每一处细节都极度敏感。一个微小的皮肤渲染错误(比如看起来像塑料)会立刻破坏沉浸感。
场景是玩家活动的空间——它需要足够大(开放世界)、足够丰富(植被、建筑)、足够真实(光影正确),同时又不能超出硬件预算。
氛围是情绪的载体——同一个场景,晴天和雨天给玩家的感受完全不同。大气散射、体积云、天气系统是连接角色和场景的”胶水”。
三者的交集:交互关键洞察:三者并非割裂,而是存在大量交集——这些交集恰恰是技术上最难、也是提升沉浸感最关键的部分:
12345678910111213141516 ┌──────────────┐ │ 交互效果 │ │ │ ┌───────┤ 角色踩水 ├───────┐ │ │ 风吹头发 │ │ │ │ 雨滴打伞 │ │ │ │ 场景破坏 │ │ │ └──────┬───────┘ │┌──────┴──────┐ │ ┌────────┴──────┐│ 角色 │ │ │ 场景 ││ Character │ │ │ Scene │└──────┬──────┘ │ └────────┬──────┘ │ ┌──────┴───────┐ │ └───────┤ 氛围 ├───────┘ │ Atmosphere │ └──────────────┘
典型的交互渲染效果:
交互类型
角色侧
场景侧
氛围侧
技术方案
角色踩水
溅水动画
水面波纹
泥地反射
物理模拟 + Render to Texture
风吹头发
发丝摆动
草地弯曲
风向参数
风场 + Verlet 积分
雨天行走
湿身效果
地面水洼
雨滴粒子
SSS 变体 + 粒子系统
场景破坏
碎片物理
几何变形
灰尘特效
Destructible Mesh + GPU 粒子
雪地行走
脚印标记
雪地变形
积雪厚度
Splat Map + Tessellation
理解了这三个模块及其交互,就掌握了游戏渲染的全局视图。接下来我们逐个深入。
三、角色渲染技术详解角色是游戏画面的核心。一个角色的渲染质量,直接决定了玩家对游戏画面水平的整体评判。
3.1 角色制作流程从原画设计到游戏画面,角色需经历一个完整的工业化管线:
12345678原画设计 → 高模雕刻 → 拓扑简化 → UV 展开 → 烘焙贴图 → 材质制作 → 渲染输出 │ │ │ │ │ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ ▼ 概念图 ZBrush 3ds Max/ RizomUV Substance PBR/NPR 游戏引擎 确定风格 百万面 Blender UV布局 Painter 材质参数 实时渲染 细节雕刻 万面级别 纹理密度 法线+金属 粗糙度等 LOD/剔除 AO等 +粗糙度 参数调整 最终效果
角色材质的两大分类:
通用材质库:解决一般物体表面的光照问题。金属、木材、布料、皮革等通过 PBR 参数化描述,一套材质系统覆盖大部分需求。
特殊材质:眼球、皮肤、头发、丝袜等需要特殊的 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 必须满足:
非负性:$f_r(\omega_i, \omega_o) \geq 0$(反射光不能为负)
互易律(Helmholtz Reciprocity):$f_r(\omega_i, \omega_o) = f_r(\omega_o, \omega_i)$(交换入射和出射方向,结果不变)
能量守恒:$\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 带来了革命性的改变。它不是一个新的物理模型,而是一套美术友好的参数化方案,用少数直观参数控制复杂的材质外观。
设计原则:
参数直观:用”粗糙度”而非晦涩的”微表面斜率分布参数”
参数尽可能少:控制在 10 个左右的关键参数
值范围 0-1:标准化,便于理解和调整(美术最喜欢滑块!)
允许参数超出范围:在有意义时突破 [0,1] 限制以表达特殊效果
组合健壮:任意参数组合不产生明显错误
核心参数详解:
参数
英文名
范围
效果说明
物理对应
基础色
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 核心思想在微观尺度上,任何表面都不是完全光滑的。微表面模型假设:
表面由无数个微小的、完美镜面的平面组成,每个微平面有自己的法线方向。表面的”粗糙度”由微平面法线分布的集中程度决定。
12345678910光滑表面(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 伪代码:
12345678// GGX / Trowbridge-Reitz NDFfloat 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 伪代码:
123456789// 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 = baseColorvec3 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 的简化版:
12345678// 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 计算如下:
1234567891011121314151617181920212223242526// 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 的角色是忠实地呈现美术的意图,而不是用公式计算物理正确的结果。
具体实现手段:
贴图即结果:美术画的贴图直接对应最终效果——亮度、阴影、高光都在贴图中预先定义好。
光照烘焙(Baked Lighting in Texture):光照信息预烘焙到贴图中。原神的角色阴影就是一个经典的阶梯函数——通过色阶贴图(Ramp Texture)控制阴影的过渡:
12345678// 阶梯色阶阴影(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;
边缘光(Rim Light):利用菲涅尔系数实现角色与背景的视觉分离:
1234// 边缘光(Rim Light / Fresnel Outline)float rimFactor = 1.0 - max(dot(N, V), 0.0);rimFactor = pow(rimFactor, rimPower); // 通常 power = 3-5vec3 rimColor = rimFactor * rimLightColor * rimIntensity;
屏幕空间轮廓描边:在屏幕空间做卷积得到更精细的边缘检测:
1234567891011121314151617// 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 实现:
1234567// 顶点着色器:沿法线方向外扩// 第二遍渲染,Cull Front(只渲染背面)vec3 outlineOffset = normal * outlineWidth;gl_Position = MVP * vec4(position + outlineOffset, 1.0);// 片元着色器:纯色输出fragColor = outlineColor;
3.6.3 NPR 的特殊挑战NPR 面临一些 PBR 不存在的独特问题:
面部阴影控制:由于卡通渲染的阴影是硬边缘的,角色的脸部稍微转动就可能导致阴影突变。解决方案是用一张 SDF 面部阴影贴图 精确控制面部明暗分界线的形状。
多光源处理:PBR 天然支持多光源叠加,但 NPR 的色阶阴影一旦叠加就会失去风格化特征。通常只取主导光源做色阶,其余光源做简单叠加。
LOD 一致性:角色远距离时,描边线宽需要随距离缩放,否则会出现”远景描边比角色还粗”的问题。
3.7 皮肤渲染(Skin Rendering)皮肤是角色渲染中最复杂的特殊材质之一。理解皮肤渲染需要从皮肤的物理结构出发。
3.7.1 为什么皮肤不能直接用 PBR?皮肤的物理结构是多层的半透明介质:
1234567891011121314光线进入皮肤后的行为: 光线 → → → → → → → ╔══════════════════════╗ ║ 油脂层(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 近似结果。
123456789101112131415// 预积分皮肤 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) 的计算:
1234// 从法线贴图和几何法线计算曲率float curvature = length(fwidth(normalWorld)) / length(fwidth(positionWorld));// 或者从预烘焙的曲率贴图采样float curvature = texture(curvatureMap, uv).r;
3.7.4 背面透射耳朵、手指等薄皮肤部位的背面透射效果:
123456// 简化的背面透射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 头发的光学特性一根头发纤维的光路行为有三种:
123456789101112 光线 → ↓╭───────────╮│ R: 直接反射 │ ← 光在表面直接反射(高光)│ ╭─────╮ ││ │ TT: │ │ ← 光进入纤维,从另一侧穿出│ │同侧透│ │ (透射,导致头发半透明感)│ │射 │ ││ ╰─────╯ ││ TRT: │ ← 光进入纤维,在内壁反射一次后穿出│ 穿透散射 │ (导致头发颜色偏移和高光加宽)╰───────────╯
R(Reflection):直接反射,产生主要高光
TT(Transmission-Transmission):同侧透射,产生头发的半透明感
TRT(Transmission-Reflection-Transmission):穿透散射,产生次级高光和颜色偏移
3.8.2 Kajiya-Kay 模型(1989)Kajiya-Kay 是最早的头发光照模型之一,至今仍是手游的主流选择——因为它足够简单。
核心思想:用切线方向(而非法线)计算高光。因为头发是圆柱形的,法线在截面上处处不同,但切线方向是一致的。
1234567891011121314// 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:
12345678910111213141516// 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 眼球的光学结构12345678910111213141516171819202122┌─────────────────────────────────────────────┐│ 眼球结构(剖面图) ││ ││ 角膜(Cornea) ││ ┌──────┐ ← 折射率 n≈1.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$(角膜)。
123456789101112// 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 数组,每个像素存储一个高度值。
1234567高度图示例(灰度图表示): 亮 = 高海拔 暗 = 低海拔 ████████████ ░░░░░░░░░░░░ ████████████ ░░░░░░░░░░░░ ██████暗暗████ ░░░░河░░河░░ ████████████ ░░░░░░░░░░░░
参数选择:
分辨率:通常 1 米/像素到 0.5 米/像素。一个 4km × 4km 的地形,1m 分辨率需要 16M 个采样点
精度:8-bit(256 级,适合平缓地形)或 16-bit(65536 级,适合高山)
存储:16-bit float(half float)是最佳选择——节省空间且精度足够
从高度图生成网格:
1234567891011121314// 地形网格生成(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 方案:
123456789101112131415161718Clipmap 层级示意(俯视):┌─────────────────────────────────────┐│ Level 0: 最粗(覆盖全区) ││ ┌─────────────────────────┐ ││ │ Level 1: 较粗 │ ││ │ ┌───────────────┐ │ ││ │ │ Level 2: │ │ ││ │ │ ┌─────┐ │ │ ││ │ │ │ L3 │← 最细│ │ ││ │ │ │玩家 │ │ │ ││ │ │ └─────┘ │ │ │| │ └───────────────┘ │ ││ └─────────────────────────┘ │└─────────────────────────────────────┘每一层的分辨率是上一层的一半,只有玩家周围是最细的 LOD
四叉树(Quadtree)方案:
将地形分成四叉树结构,每个节点可以独立选择 LOD 等级。UE4/UE5 的地形系统使用的就是这种方案。
123456789101112四叉树 LOD: ┌────────┐ │ Root │ │ Level 0│ └───┬────┘ ┌─────┼─────┐ ┌───┴──┐┌┴───┐┌─┴───┐┌────┐ │ L1-A ││L1-B││L1-C ││L1-D│ └──┬───┘└────┘└─────┘└────┘ ┌──┼──┐ │ │ │ ← 近处细分,远处不细分 L2 L2 L2
消除 LOD 裂缝(Crack Fixing):
当相邻的两个 LOD 等级不同时,会在接缝处产生”裂缝”。常见的解决方案:
裙边(Skirt):在 LOD 块的边缘向内添加一圈额外三角形,遮挡裂缝
过渡区(Transition):在接缝区域做几何 Morph(逐顶点插值)
T-Junction 消除:在低 LOD 的一侧添加额外顶点匹配高 LOD
4.2.4 GPU Driven 地形传统地形在 CPU 端生成网格,然后上传到 GPU。GPU Driven 方案直接在 GPU 上生成地形网格,CPU 只发送高度图和 LOD 参数。
123456789101112131415// 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 四个通道。
1234567891011// 地形材质混合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):
1234567891011121314151617// 简化的植被风场 Shadervec3 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 的水面质点做的是圆形运动,产生的波峰更尖、波谷更宽,这与真实海浪的形状一致:
1234567891011121314151617181920212223// 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:
123456// 水面折射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)
水面波浪聚焦光线形成的光斑——就像游泳池底部的波纹光网。
12345// 简化的焦散效果vec2 causticUV = worldPos.xz * causticScale + time * causticSpeed;float caustic = texture(causticTexture, causticUV).r;// 焦散强度随深度衰减float causticIntensity = caustic * exp(-depth * causticFalloff);
4. 次表面散射(SSS)
水的通透感——阳光照射下的水面边缘会发出蓝绿色的光。这与皮肤 SSS 的原理相同。
5. 深浅颜色渐变
浅水区域(岸边)呈现透明的浅蓝绿色,深水区域呈现深蓝色。使用指数衰减模拟水的吸收:
1234// 水的吸收系数(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) 实现平滑过渡:
12345678910111213141516CDLOD 原理:近距离:高密度网格(每米一个顶点)┌─┬─┬─┬─┬─┬─┬─┬─┐├─┼─┼─┼─┼─┼─┼─┼─┤├─┼─┼─┼─┼─┼─┼─┼─┤└─┴─┴─┴─┴─┴─┴─┴─┘远距离:低密度网格┌───────────────┐│ ││ │└───────────────┘过渡区:两套 LOD 共存,渐变混合两套网格重叠,alpha 从 0→1 过渡
4.5 天气系统:天空、大气、云、雾这一组技术有一个统一的核心算法:Ray Marching(光线步进)。所有介质(大气、云、雾)的光照问题都可以通过这一算法求解。
4.5.1 Ray Marching 核心算法Ray Marching 的思想非常直观:沿着光线方向一步步前进,每一步计算该位置的密度和光照贡献,累积起来就是最终效果。
123456789101112131415161718192021222324252627282930// 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》
物理模型:两层壳模型
1234567891011╭─────────────────────────╮ ← 大气层顶部(约 100km)│ 外层大气 ││ 稀薄气体 ││ ││ ←──── 光线散射 ────→ ││ ││ │╰──────────┬──────────────╯ ┌────┴────┐ │ 地球表面 │ ← 地形/海面 └─────────┘
两种散射类型:
散射类型
粒子大小
效果
公式
瑞利散射(Rayleigh)
<< 光波长
蓝天、日落红霞
$\sigma_s \propto \frac{1}{\lambda^4}$
米氏散射(Mie)
≈ 光波长
雾天、光晕
前向散射为主
为什么天空是蓝色的? 瑞利散射强度与波长的四次方成反比。蓝光(λ≈450nm)的散射强度约是红光(λ≈650nm)的 $(650/450)^4 ≈ 4.3$ 倍——所以白光经过大气后,蓝光被大量散射到各个方向,形成了蓝色的天空。
为什么日落是红色的? 日落时太阳光穿过的大气层更厚,蓝光已经被散射殆尽,只剩下红光和橙光能到达你的眼睛。
预计算 LUT 方案:
完整的物理大气计算每帧开销太大(每像素都要做 Ray Marching),所以业界使用预计算查找表(LUT):
12345678// 大气散射预计算 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)将大气计算融入统一的框架。
12345678910111213深度切片示意: 相机 ──────────────────────────→ 远处 │F1│F2│F3│F4│F5│F6│F7│F8│F9│F10│ │ │ │ │ │ │ │ │ │ │ │ 近 远 每个切片(Froxel)存储: - 散射亮度(Scattering Luminance) - 透射率(Transmittance) 通常分辨率:32×32×16 或 64×64×32
通过在每个 Froxel 中计算散射和透射率,大气雾与天空渲染完全统一——不再有”天空”和”雾”的割裂感。
4.5.4 体积云(Volumetric Cloud)体积云是场景渲染中最耗性能但也最壮观的效果。
核心方案:3D 密度场 + Ray Marching
12345678910111213141516体积云渲染流程:1. 生成 3D 密度场 → 使用噪声函数描述云的形状 → Perlin/Worley 噪声叠加2. Ray Marching 采样 → 沿视线方向步进 → 每步采样密度3. 光照计算 → 向光源方向也做 Ray Marching → 累积散射光4. 合成到场景 → 云的 alpha 混合到场景颜色上
噪声函数:云的形状由噪声函数定义。
噪声类型
特点
用途
Perlin Noise
平滑的梯度噪声
云的大致形状
Worley/Voronoi
细胞状结构
云的细节(蓬松感)
分形叠加(FBM)
多个八度叠加
自然的多尺度细节
123456789101112131415161718// 云密度采样(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) 系统是连接所有天气元素的控制层:
123456789101112131415161718TOD 参数 → 控制所有天气元素: 太阳角度 ──┬──→ 大气散射颜色(日出/正午/日落/夜空) ├──→ 阴影方向和长度 ├──→ 体积云的光照颜色 └──→ 环境光强度和颜色 云量 ──────┬──→ 云的密度阈值 ├──→ 散射光强度(阴天 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 性能优化通用套路
LOD 策略:距离越远,细节越少。不只是几何 LOD,还有 Shader LOD(远处用更简单的材质)和阴影 LOD(远处物体不产生阴影)。
剔除优化:
视锥剔除(Frustum Culling):不在视野内的物体不渲染
遮挡剔除(Occlusion Culling):被其他物体遮挡的不渲染
距离剔除(Distance Culling):超过一定距离的小物体直接不渲染
流送管理(Streaming):只加载玩家周围的资源。Mipmap 的本质就是一种流送——远处用低分辨率贴图。
GPU Driven:让 GPU 承担更多工作。Nanite 的核心思想就是完全在 GPU 端管理 LOD 和剔除。
预计算 LUT:空间换时间。SSS、大气、BRDF 都可以用预计算查找表将运行时计算变为纹理采样。
实例化渲染(Instancing):相同几何体的多个实例,一次 Draw Call 渲染全部。植被系统的核心优化手段。
5.4 核心学习路径12345678910111213141516171819202122232425262728293031学习路径(建议按此顺序):基础(1-3个月)├── 渲染管线架构├── 渲染方程 → BRDF → 微表面模型├── 光源模型(点/方向/聚光/面光源)└── Shader 编程入门(GLSL/HLSL)进阶(3-6个月)├── Disney Principled BRDF → IBL(基于图像的光照)├── SSS → 各向异性 → Clearcoat├── 后处理管线(TAA, Bloom, Tonemapping)└── 阴影技术(CSM, PCF, VSM, Ray Traced)场景(6-12个月)├── 地形 LOD(Clipmap, 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 的暴力美学——每一个像素背后,都是数学和工程的精密配合。
理解底层原理,不是为了能写出更复杂的公式,而是为了在遇到问题时能找到正确的近似方向。渲染方程的精确解不可能实时算出,但理解它之后,你就知道每一次近似牺牲了什么、保留了什么、在什么条件下近似会失效。
这正是游戏渲染技术的魅力所在——在无限的可能性中,找到有限预算下的最优解。
本文基于网易游戏学堂的技术分享整理扩展,融入了图形学经典论文和实际游戏引擎的实现经验。如有疏漏,欢迎指正。