Games104学习笔记(4)
GAMES104:游戏引擎中的渲染实践
课程:GAMES104《现代游戏引擎:从入门到实践》
讲次:Lecture 04 / Rendering on Game Engine
讲者:王希(Xi Wang,Booming Tech)
视频时长:约 1 小时 40 分 56 秒
笔记性质:根据本地视频中的幻灯片与讲解主题整理,不是逐字稿;时间点为复习定位用的近似值。
一句话理解
游戏引擎中的渲染系统,不只是“调用图形 API 把模型画出来”,而是一套围绕现代硬件高度优化的工程流程:它要把场景数据组织为 GPU 可读的资源,筛掉不该画的对象,复用相同资源,并在有限帧时间内高效提交绘制工作。
场景 / Game Object / Mesh Renderer
↓
可见性判断、排序、批处理、实例化
↓
Mesh / Material / Texture / Shader 等资源绑定
↓
CPU 准备命令与数据,尽量减少传输
↓
GPU 并行执行绘制与着色
↓
屏幕画面
本讲学习目标
- 从工程角度理解实时渲染系统,而不是孤立地学习某个绘制算法。
- 认识 CPU、GPU、主存、显存与总线对渲染设计的影响。
- 理解纹理采样、SIMD / SIMT、顶点与索引缓冲区等基础数据路径。
- 了解 Mesh Renderer、Submesh、Material、Shader、Texture 和实例化的关系。
- 掌握可见性裁剪、PVS、纹理压缩与 GPU-Driven 的核心动机。
- 建立“尽量少传数据、尽量少画对象、尽量让 GPU 批量工作”的渲染工程意识。
1. 游戏引擎渲染的定位
视频将游戏引擎渲染定义为:
为满足 PC、主机与移动设备等现代硬件上的关键游戏渲染需求,而高度优化的实用软件框架。
它的重点是如何组织和运行渲染系统,包括数据、资源、硬件、调度和性能;不等同于一门只讨论某种光照公式或图形学效果的课程。
1.1 本讲关注什么
- CPU 与 GPU 的协作方式。
- 资源如何从引擎进入 GPU。
- 网格、材质、纹理与 Shader 如何组织。
- 如何减少冗余绘制和数据搬运。
- 如何利用实例化、批处理和 GPU 并行能力。
- 如何从传统渲染走向 GPU-Driven 管线。
1.2 本讲不展开什么
视频明确将以下方向排除在本讲的主要范围之外:
- 卡通 / 非真实感渲染。
- 完整的 2D 渲染引擎设计。
- 次表面散射等特定材质效果。
- 毛发、毛皮等高成本专用效果。
这不是说这些技术不重要,而是本讲优先建立现代实时渲染系统的主干框架。
2. 实时渲染的核心挑战
2.1 渲染必须服从硬件结构
现代计算机不是一个统一的计算体:CPU、GPU、主存、显存、缓存和总线各自擅长不同工作。渲染系统必须面对这种异构结构,而不能假设所有数据都能被任意处理器以相同速度访问。
| 部件 | 更适合的工作 |
|---|---|
| CPU | 逻辑控制、资源调度、场景遍历、命令准备、复杂分支决策 |
| 主存 | 存放 CPU 侧的世界状态、资源元数据和准备中的数据 |
| GPU | 大规模并行的顶点处理、像素处理、纹理采样和光栅化 |
| 显存 | 存放 GPU 频繁访问的网格、纹理、缓冲区和渲染目标 |
| CPU-GPU 总线 | 传输资源与命令,但延迟较高、带宽有限 |
2.2 数据搬运往往比计算更昂贵
视频给出的工程提示是:
在可能的情况下,始终最小化 CPU 与 GPU 之间的数据传输。
原因是:
- CPU 到 GPU 的路径具有更高延迟和有限带宽。
- GPU 本地显存中的并行计算与读取通常快得多。
- 每帧重复上传同一份网格、纹理或状态,会快速耗尽带宽预算。
- 频繁同步可能让 CPU 等待 GPU,或让 GPU 因数据未准备好而空转。
因此,常见策略包括:
- 将长期使用的网格和纹理常驻或缓存到显存。
- 上传共享资源一次,让多个对象复用。
- 使用句柄、实例数据或小型参数更新,而不是重复复制完整资源。
- 批量提交相关绘制任务。
- 尽可能把可并行的筛选和生成工作留在 GPU 侧。
- 尽量让数据单向流动传输。
3. GPU 计算基础:纹理采样、SIMD 与 SIMT
3.1 纹理采样:画面质量与性能的平衡
纹理并不是简单地“按一个坐标取一个像素”。当纹理在屏幕上被缩小、倾斜或远离相机时,直接采样容易造成闪烁、锯齿和细节不稳定。
视频以 Mipmap 与三线性过滤为例:
- 选择最接近目标像素密度的两个 Mipmap 层级。
- 分别在这两个层级中执行双线性插值。
- 再对两次采样结果做线性插值。
两个相邻 Mipmap
↓
各自双线性过滤
↓
两个结果线性插值
↓
较稳定的纹理采样结果
这类操作看似基础,但会在屏幕上的大量像素中重复执行,因此非常适合 GPU 的并行架构。
3.2 SIMD 与 SIMT
| 概念 | 含义 | 常见场景 |
|---|---|---|
| SIMD | Single Instruction, Multiple Data;一条指令处理多份数据 | CPU 向量化、批量数学运算 |
| SIMT | Single Instruction, Multiple Threads;大量线程以相近指令流执行 | GPU 着色器、并行像素与顶点处理 |
两者的共同点是:通过让同一种操作作用于很多数据,提高吞吐量。区别在于,SIMD 更强调向量指令,SIMT 更强调 GPU 对大量线程组的调度。
对引擎开发者而言,重要启示是:
- 连续、相似、可批量处理的数据更容易获得并行收益。
- 过多分支或每个对象完全不同的处理路径,会降低并行效率。
- 资源和数据布局会影响硬件能否真正发挥吞吐量。
4. 从 Game Object 到可绘制数据
4.1 Mesh Renderer Component
第三讲中的 Game Object / Component 模型,在渲染系统中通常通过 Mesh Renderer Component 落地:
Game Object
├─ Transform:位置、旋转、缩放
├─ Mesh Renderer:负责可见时的绘制描述
│ ├─ Mesh / Submesh
│ ├─ Material
│ ├─ Texture
│ └─ Shader
└─ 其他组件:动画、物理、逻辑等
Mesh Renderer 并不保存全部顶点和纹理副本,而是通常引用由资源系统管理的资源。这样一个场景中大量对象可以共享网格、材质、Shader 和纹理。
4.2 Mesh、Submesh 与多材质
一个完整模型可能需要多种材质,例如角色的皮肤、金属武器、布料和透明部件。为了支持这种情况,模型通常会被拆成多个 Submesh:
Model
├─ Submesh 0 → Material A
├─ Submesh 1 → Material B
└─ Submesh 2 → Material C
本讲的明确结论是:Submesh 设计用于支持一个模型使用多个材质。
这也意味着,材质数和 Submesh 划分会影响绘制调用、资源绑定和批处理机会;不是拆得越细越好。
4.3 顶点与索引缓冲区
GPU 需要以结构化缓冲区读取几何数据。
| 数据 | 作用 |
|---|---|
| Vertex Declaration | 声明每个顶点包含哪些属性,以及它们的布局 |
| Vertex Buffer | 存放顶点位置、法线、UV、颜色、骨骼权重等数据 |
| Index Declaration | 声明索引数据格式 |
| Index Buffer | 用索引复用顶点,并定义三角形连接关系 |
一个顶点可能包含:
Position + Normal + UV + Tangent + Color + Bone Weights ...
索引缓冲区的价值在于复用顶点。例如相邻三角形共享边时,不需要重复存储相同顶点,只需让多个索引引用它。
视频还展示了常见图元组织方式:
- Triangle List:每三个索引定义一个三角形。
- Triangle Strip:后续顶点与前面的顶点组合成连续三角形。
- Triangle Fan:多个三角形共享一个中心顶点。
不同图元拓扑影响数据组织和绘制方式,但现代资产管线中最常见的基础仍是三角形。
5. Shader、Material 与资源复用
5.1 Shader 的角色
Shader 是运行在 GPU 上的程序,用来定义顶点如何变换、像素如何着色,以及资源如何参与最终画面。
视频从固定功能着色到自定义 Shader 的演变说明:现代 GPU 把越来越多可配置的计算交给开发者。引擎需要负责:
- 管理 Shader 资源和变体。
- 将材质参数与 Shader 对应。
- 绑定纹理、常量和缓冲区。
- 为不同平台选择可用的实现。
- 控制状态切换,避免无效的重复绑定。
5.2 Material 是资源绑定与参数组合
可以把 Material 理解为“某个 Shader + 一组资源 + 一组参数”的可复用配置:
Material
├─ Shader
├─ Base Color Texture
├─ Normal Texture
├─ 其他贴图 / 缓冲区
└─ 浮点、颜色、开关等参数
不同对象可以共享一个 Material,也可以在共享基础上只传递少量实例差异。
5.3 Instance:使用 Handle 复用资源
视频展示了以句柄(Handle)复用 Mesh、Shader 和 Texture Resource Pool 的方法:
资源池
├─ Mesh 资源
├─ Shader 资源
└─ Texture 资源
↑
多个对象实例只保存对这些资源的引用
这能避免“每个士兵都保存一份完全相同的模型和贴图”。每个实例只保留自己的 Transform、动画状态、少量参数或对象 ID,而共享大体积资源。
资源复用带来两类收益:
- 降低内存和显存占用。
- 更容易将相同资源的对象组织为批次或实例化绘制。
6. 可见性裁剪:只画真正需要的对象
6.1 为什么裁剪是渲染系统的核心
一个大型场景中,绝大多数对象在当前相机视角下并不需要绘制:它们可能在屏幕外、被遮挡、距离过远,或位于封闭空间的另一侧。
视频强调的原则是:
使用裁剪算法,尽可能少地绘制对象。
可以把渲染前的筛选理解为:
全场景对象
↓
视锥裁剪:是否在相机视野内?
↓
遮挡 / 区域裁剪:是否可能被看到?
↓
距离、LOD、状态和材质筛选
↓
仅提交候选可见对象
6.2 Portal 与 PVS
在室内、走廊、房间等被墙体明显分隔的场景中,可以用 Portal 表示区域之间的开口,并预先或运行时计算 PVS(Potentially Visible Set,潜在可见集合)。
核心思路是:
- 不必精确判断每个像素是否可见。
- 先根据 Portal 找出从当前位置“有可能看见”的空间叶节点。
- 只把这些区域内的对象交给后续绘制或更精细裁剪。
视频中的表述是:从 Portal 直接确定潜在可见的叶节点集合。
PVS 的价值是用空间结构和预处理数据减少无意义的对象遍历,尤其适用于几何遮挡明显的场景。
该技术或许不算最优,但思路想法值得研究。
6.3 裁剪与实例化的协同
实例化可以减少资源重复和调用开销,但不意味着应该把所有实例都绘制出来。理想流程是:
- 先筛掉明显不可见的对象或 Cluster。
- 再把同资源、同材质的可见实例组织在一起。
- 最后以更少的调用提交给 GPU。
即:先减少工作量,再提高剩余工作的吞吐量。
7. 纹理压缩与内容规模
7.1 纹理压缩是引擎必备能力
视频把 Texture Compression 标为“游戏引擎必须掌握”的能力。原因是纹理通常是游戏资源中最占空间、最消耗带宽的一类数据。
纹理压缩的目标是在可接受的画质损失下:
- 减少磁盘包体。
- 降低主存和显存占用。
- 减少加载与上传时间。
- 改善 GPU 采样时的带宽压力。
选择压缩格式时需要同时考虑:
- 目标平台的硬件支持。
- 颜色、法线、遮罩、HDR 等纹理用途。
- 画质、压缩比和解码成本。
- 是否需要 Mipmap。
7.2 内容制作与运行时表示不同
建模、雕刻等 DCC 工具会产生高细节的原始内容;运行时渲染系统则需要把它们转换为适合硬件访问的网格、Submesh、纹理和材质资源。
引擎的资产 Pipeline 通常会处理:
- 网格划分与顶点格式。
- 材质 / Submesh 对应关系。
- 纹理生成、压缩和 Mipmap。
- 平台特定数据格式。
- LOD、边界盒或其他裁剪辅助数据。
这说明渲染质量不只由 Shader 决定,也由内容如何被组织和烘焙决定。
8. GPU-Driven 与 Cluster-Based Mesh Pipeline
8.1 为什么要把更多工作移向 GPU
视频的第四条总结是:随着 GPU 越来越强大,更多工作被移到 GPU 中执行,这就是 GPU-Driven 的趋势。
传统方式中,CPU 需要遍历大量对象、执行可见性判断、生成绘制调用,并通过总线将命令交给 GPU。场景越来越大时,CPU 侧的对象管理和 Draw Call 提交可能成为瓶颈。
GPU-Driven 的方向是:
- 让 GPU 对更多绘制单元进行筛选。
- 使用间接绘制减少 CPU 的逐对象提交。
- 用更细粒度的 Cluster 管理几何。
- 把适合并行的数据处理保留在 GPU 侧。
8.2 Cluster-Based Mesh Pipeline
视频展示了一个 GPU-Driven 的 Cluster-Based Mesh Pipeline:
- 一个 Draw Call 可以处理任意数量的网格实例。
- GPU 可依据 Cluster Bounds 进行裁剪。
- 可以对 Cluster 做深度排序。
并给出一层从粗到细的几何组织:
Batch
└─ 一次 API Draw,可由很多 Surf 组成
└─ Surf:基于材质的 Submesh,由多个 Cluster 组成
└─ Cluster:更细粒度的三角形集合
视频示例中,Cluster 以约 64 个三角形的 Strip 为一个处理单位。具体大小和组织方式会随引擎、硬件和资产特征变化,但关键思想是:把大网格拆成足够细的可裁剪、可并行处理的单元。
8.3 与传统批处理的差别
传统批处理主要目标是减少 CPU 发起的 API 调用;Cluster 化进一步把可见性与几何调度推向 GPU。两者的共同目标是降低无效工作,但优化位置和粒度不同:
| 方法 | 主要优化对象 | 典型收益 |
|---|---|---|
| 资源共享 / 实例化 | 重复网格、材质、纹理 | 降低资源占用和调用开销 |
| CPU 侧批处理 | Draw Call 与状态切换 | 减少 CPU 提交压力 |
| 传统裁剪 | 不可见对象 | 减少无效绘制 |
| GPU-Driven Cluster | 几何细粒度可见性与调度 | 提高大规模场景的并行吞吐 |
9. 本讲四条 Take Away
视频最后给出四条总结:
- 游戏引擎设计与硬件架构设计密切相关。
- Submesh 用于支持一个模型具有多个材质。
- 使用裁剪算法,尽可能少地绘制对象。
- 随着 GPU 更强,越来越多工作被转移到 GPU,即 GPU-Driven。
可以把它们压缩成一条工程原则:
理解硬件 → 组织正确数据 → 删除无效工作 → 让并行硬件完成更多合适的工作
10. 复习用速记
渲染系统 = 数据组织 + 资源管理 + 可见性筛选 + 绘制提交 + GPU 并行执行
核心约束:
CPU ↔ GPU 传输有延迟和带宽限制
→ 尽量少传大数据,尽量复用资源
Mesh Renderer:
Game Object + Transform + Mesh / Submesh + Material + Texture + Shader
几何数据:
Vertex Buffer 存属性
Index Buffer 复用顶点并定义三角形
性能原则:
先裁剪不可见对象
再共享资源、实例化与批处理
最后让 GPU 处理高吞吐并行工作
趋势:
传统 CPU 主导提交 → GPU-Driven → Cluster 级细粒度裁剪与调度
11. 自测题
- 为什么游戏引擎渲染系统需要理解 CPU、GPU、主存、显存和总线的关系?
- CPU 到 GPU 的频繁数据传输会带来哪些问题?
- 三线性过滤的三个基本步骤是什么?
- SIMD 与 SIMT 的共同点和区别是什么?
- Mesh Renderer Component 通常需要关联哪些数据?
- Vertex Buffer 和 Index Buffer 分别解决什么问题?
- Submesh 为什么能支持一个模型使用多个材质?
- Material、Shader 与 Texture 各自处于什么关系?
- 实例化为什么可以降低内存占用和提交成本?
- 为什么“只画当前可见对象”比盲目增加 GPU 算力更重要?
- Portal 与 PVS 分别用来解决什么问题?
- 纹理压缩为什么是游戏引擎必须掌握的能力?
- GPU-Driven 与传统 CPU 侧 Draw Call 提交的主要差别是什么?
- Cluster-Based Mesh Pipeline 如何帮助大规模几何渲染?
13. 术语表
| 术语 | 含义 |
|---|---|
| Rendering System | 将场景数据组织、筛选并提交给 GPU 生成画面的系统 |
| Mesh Renderer | 描述一个游戏对象如何使用网格和材质被绘制的组件 |
| Mesh | 由顶点、索引和拓扑构成的几何资源 |
| Submesh | 具有独立材质绑定的网格子集 |
| Vertex Buffer | 存放顶点属性的 GPU 缓冲区 |
| Index Buffer | 存放顶点索引、用于复用顶点和定义图元的缓冲区 |
| Triangle List / Strip / Fan | 三种常见三角形图元组织方式 |
| Shader | 运行在 GPU 上、定义顶点或像素等计算过程的程序 |
| Material | Shader、纹理与参数的可复用组合 |
| Texture Sampling | 按坐标从纹理中读取或插值颜色 / 数据的过程 |
| Mipmap | 同一纹理的多级分辨率版本 |
| Trilinear Filtering | 在两个 Mipmap 层内双线性过滤,再对两者结果插值 |
| SIMD | 单指令多数据,常用于 CPU 向量化 |
| SIMT | 单指令多线程,常用于 GPU 大规模并行执行 |
| Instancing | 多个对象共享资源,只传递实例差异的绘制方式 |
| Culling | 筛除当前不需要绘制对象的过程 |
| PVS | Potentially Visible Set,潜在可见集合 |
| Draw Call | CPU 提交一次绘制工作的 API 调用 |
| GPU-Driven | 将更多筛选、调度或绘制准备工作转移至 GPU 的架构方向 |
| Cluster | 用于细粒度裁剪与处理的一小组几何单元 |
14. 参考资料
- B 站视频:04.游戏引擎中的渲染实践
- GAMES104 官方课程主页
- GAMES104 官方课程 PPT 与视频页面
- 推荐阅读:Jason Gregory,《Game Engine Architecture / 游戏引擎架构》