/ Ch22 游戏中的计算机图形学

第22章:游戏中的计算机图形学 — 零基础极详细讲义

讲义说明
本讲义基于 Steve Marschner & Peter Shirley 所著《虎书5》第5版第22章(p.623-644),由10年经验游戏引擎/渲染工程师执笔。
本章是"游戏图形学"——所有前21章的集大成者。这一章不谈新算法,而是把之前所有内容(光栅化、纹理、光照、动画、加速结构)放到 "30帧/秒、100W三角形" 的真实约束下重新审视。
本版插画采用 Guizang 材质插画风格重新绘制。

学习目标

  1. 理解游戏平台的硬件差异与选型策略
  2. 掌握有限资源下的预算分配思维(CPU/GPU/Memory/团队时间)
  3. 理解优化技术矩阵:Culling、LOD、Batching、Prepass
  4. 掌握场景管理的数据结构(BVH/Octree/BSP/ECS)
  5. 理解碰撞检测的Broad/Narrow两阶段架构
  6. 掌握游戏渲染循环的完整生命周期
  7. 理解游戏类型对图形技术选型的根本影响
  8. 掌握美术资产生产管线(高模→低模→法线→纹理→Shader→动画)

22.1 游戏平台(Platforms)

22.1.1 为什么需要关注"平台"?

核心矛盾:游戏不像离线渲染电影——它必须在某个具体硬件上以确定的帧率运行。离线渲染可以花10小时渲染一帧,游戏每帧必须小于33ms(30fps)甚至小于16ms(60fps)。

💡 生活类比:搬家

离线渲染就像"请搬家公司搬一整个房子"——你可以花一整天精雕细琢每件家具怎么摆。游戏渲染就像"在高速公路上边开车边组装宜家家具"——你必须在车子开到下一个出口之前把椅子组装好,否则就"撞车"(掉帧)。

22.1.2 五大平台的差异

平台硬件OSAPI特点对渲染的影响
PC任意(千差万别)Windows/Linux/MacDirectX/Vulkan/OpenGL配置差异巨大,必须可配置需要动态分辨率、多档画质设置
主机(PS5/Xbox)固定(完全确定)定制OS厂商SDK硬件确定,可极致优化可以写死参数,利用硬件特性
手机弱(散热差)iOS/AndroidMetal/Vulkan/OpenGL续航敏感,发热严重必须降分辨率、减少pass
Web任意(浏览器内)浏览器沙箱WebGL/WebGPU沙箱限制多,性能受限Draw Call上限低
VR/AR头显(双目)定制OpenXR90/120fps强制要求渲染分辨率要乘2(双眼)
🔧 技术选型:选A不选B的理由
场景推荐选不推荐选理由
移动端游戏Unity + VulkanUnreal + D3D12Unity移动生态成熟,Unreal移动端功耗大
主机AAAUnreal Engine自研引擎UE5已针对PS5/Xbox优化,自研成本高
Web小游戏WebGL + Three.js原生Vulkan浏览器不支持原生Vulkan,WebGL是唯一选择
🔥 重要洞察:平台选择是游戏开发的"第一性原理"——它决定了你的三角形预算、纹理大小、shader复杂度。一个PS5游戏和一个手机游戏的渲染管线是完全不同的。
🤔 想一想:为什么主机(Console)的游戏画面通常比同配置PC好?
因为主机硬件完全确定!开发者可以针对固定硬件做极致优化——比如PS5的定制SSD可以实现近乎即时的场景加载,这是PC无法比拟的。同样,因为没有"兼容千万种显卡"的负担,驱动程序的开销也小得多。

22.2 有限资源(Limited Resources)

22.2.1 核心矛盾:硬件永远不够用

游戏开发中需要管理 5 类有限资源,缺一不可:

💡 生活类比:家庭预算

想象你每个月有10000元收入(=一帧的33ms),需要分配在房租(GPU渲染)、食物(CPU渲染)、交通(物理模拟)、娱乐(动画)、储蓄(音频/网络)上。如果房租占太多,其他方面就得省。游戏引擎的"帧预算"完全一样——每个子系统只能分到固定的毫秒数。

资源衡量紧张程度应对策略
CPU时间ms/帧⭐⭐⭐⭐预算分配、Job System、多线程
GPU时间ms/帧⭐⭐⭐⭐⭐LOD、剔除、Shader优化
内存MB/GB⭐⭐⭐流式加载、引用计数、压缩
带宽GB/s⭐⭐⭐数据压缩、合并Draw Call
开发时间人月⭐⭐⭐⭐⭐工具链、复用、Asset Store

22.2.2 帧率预算(Frame Budget)

帧预算分配饼图
图22-1:30fps下33ms的帧预算分配——GPU渲染是最大头

30fps = 33.3ms/帧 = 一帧的"时间钱包"。每一项任务都在"抢钱":

任务预算(30fps)预算(60fps)占比优化方向
输入处理0.5ms0.25ms1.5%合并事件、批量处理
游戏逻辑5ms2.5ms15%Job System、ECS
物理模拟3ms1.5ms9%LOD物理、简化碰撞体
动画更新2ms1ms6%LOD动画、GPU蒙皮
CPU渲染8ms4ms24%剔除、批处理、Job System
GPU渲染13ms6.5ms39%LOD、Shader优化、分辨率
音频1ms0.5ms3%音频压缩、流式
网络0.5ms0.25ms1.5%异步、预测
🔥 重要洞察:GPU渲染占了39%(13ms)!如果你在优化其他部分(比如音频省了0.3ms),但GPU还是13ms,帧率不会有任何变化。你必须找到当前最慢的环节(瓶颈)来优化。

22.2.3 异构资源(Heterogeneous Resources)

GPU不是单一资源——它是多个并行单元的总和:

资源类型任务数量级例子
CPU核心通用计算4-16核游戏逻辑、AI
Vertex Shader顶点变换数千单元蒙皮、顶点动画
Pixel Shader像素计算数千单元光照、纹理采样
纹理单元采样纹理数十-百TexRead、过滤
ROP写帧缓冲数十深度测试、混合
🤔 想一想:为什么"减少材质复杂度"有时完全不影响帧率?
因为瓶颈可能在Vertex Shader而不是Pixel Shader!如果场景的瓶颈是"三角形太多",你改材质(只影响Pixel Shader)完全没用。你要么减少三角形,要么优化Vertex Shader。这就是"一次只能优化一个瓶颈"原则——你必须先找到瓶颈在哪儿。

22.3 优化技术(Optimization Techniques)

22.3.1 核心理念:"最便宜的渲染 = 不渲染"

💡 生活类比:商场推广

你在一家大商场做推广活动。与其给每一个路人发传单(=渲染所有物体),不如:只给走进商场的人发(=视锥剔除)、不给背对你的人发(=背面剔除)、不给已经在其他柜台排队的人发(=遮挡剔除)。

技术原理节省量实现难度必备程度
视锥剔除摄像机视野外的不画50-90%三角形⭐⭐⭐⭐⭐ 必备
遮挡剔除被挡住的不画50-95%像素⭐⭐⭐⭐ 必备
背面剔除看不到的背面不画30-50%三角形无(硬件自带)⭐⭐⭐⭐⭐ 自动
LOD远距离用粗模50-90%三角形⭐⭐⭐⭐ 必备
小对象剔除像素<1的不画0-5%⭐⭐ 锦上添花

22.3.2 视锥剔除(Frustum Culling)

视锥体剔除示意图
图22-2:视锥剔除——只绘制摄像机视锥体内的物体

为什么需要视锥剔除?想象一个开放世界游戏,场景中有10万个物体。如果不做剔除,每帧都要遍历全部10万个物体,仅遍历开销就超过10ms。但实际只有几百个物体在视野内——剩下99.5%的工作都是浪费。

// 基本的视锥剔除——6个平面测试
function IsInFrustum(BoundingBox bbox, Frustum frustum):
  // 对6个平面依次测试
  // 左、右、上、下、近、远,共6个裁剪平面
  for Plane p in frustum.planes:
    // 如果包围盒完全在平面外侧 → 不可见
    if bbox.IsOutside(p):
      return false
  return true

为什么是6个平面?视锥体像一个平截头的金字塔——它有4个侧面(左/右/上/下)加上近裁剪面和远裁剪面。物体必须同时位于这6个平面内侧才可见。

🔧 技术选型:视锥剔除加速结构
结构查询复杂度适合场景
无结构(暴力)O(n) — n=10万时不可接受小场景
BVHO(log n)静态+动态混合
四叉树/八叉树O(log n)户外大场景
均匀网格O(1)~O(n)粒子系统

22.3.3 LOD(Level of Detail)

LOD细节级别选择
图22-3:LOD选择——距离越远,细节越少

为什么需要LOD?一个角色模型有10万个三角形。当它在100米外时,在屏幕上只占50个像素。用10万三角形渲染50个像素的空间是巨大的浪费——每个像素平均处理2000个三角形。

距离LOD级别三角形数视觉差异节省
<10mLOD 010,000完整细节0%
10-50mLOD 15,000几乎看不出50%
50-200mLOD 21,000轻微差异90%
200-1000mLOD 3200大致轮廓98%
>1kmLOD 40完全看不见100%
// LOD选择——基于距离的简化版
function SelectLOD(Object obj, Camera cam):
  dist = distance(cam.position, obj.position)
  if dist < 10:   return 0   // 最近距离→最高细节
  if dist < 50:  return 1
  if dist < 200: return 2
  if dist < 1000:return 3
  return -1       // 太远→不画
🔥 重要洞察:LOD不只是"远的用粗模"那么简单。HLOD(Hierarchical LOD)把远处一群物体合并成一个简化模型——这极大减少了Draw Call。一个10000棵树的森林,用HLOD可以合并成几十个"树丛"模型,Draw Call从10000降到50。

22.3.4 遮挡剔除(Occlusion Culling)

方法原理适合优点缺点
PVS预计算"每个格子能看到什么"室内(Doom/Quake)运行时零开销只适合静态场景
BSP预计算前后关系树静态室内确定性构建复杂
Hardware Occlusion QueryGPU实时查询"这个物体被挡了多少"户外准确有延迟(一帧延迟)
HiZ用上一帧深度做粗粒度遮挡测试现代GPU通用低开销帧间依赖

22.3.5 Depth Prepass(深度预渲染)

为什么需要Depth Prepass?GPU的Early-Z只在你没有修改深度时生效。如果你的shader写了深度(比如用clip/discard),Early-Z就失效了。Depth Prepass先画一遍纯深度(不画颜色),让所有物体的深度都写入Z-buffer,第二遍画颜色时就可以用Early-Z拒绝被遮挡的像素。

// Depth Prepass 标准实现

// 第一遍:只写深度,不写颜色
for obj in opaque_objects:
  draw(obj, depth_only=true, color_mask=false)

// 排序:不透明物体从前到后
sort(opaque_objects, front_to_back)

// 第二遍:画颜色,Early-Z会拒绝被遮挡的像素
for obj in opaque_objects:
  draw(obj, color=true)
🔥 重要洞察:Depth Prepass的代价是所有不透明物体画两遍!但收益是Fragment Shader节省30-70%。因为现代游戏的大部分GPU时间花在Fragment Shader上,这个交换通常是值得的。

22.3.6 Draw Call优化

为什么Draw Call是"天敌"?每次Draw Call,CPU需要准备顶点数据、设置渲染状态、提交命令——这些操作有0.1-1ms的固定开销。如果一帧有10000个Draw Call,仅开销就超过10ms。

技术原理效果
Static Batching把多个静态网格合并成一个1000个物体 → 1个Draw Call
GPU Instancing同mesh不同变换,一次提交1000棵树 → 1个Draw Call
Dynamic Batching运行时合并材质相同的物体节省30-50% Draw Call
🤔 想一想:10000个Draw Call和1000个Draw Call,对帧率的影响有多大?
10000个Draw Call的CPU开销大约是10ms(假设每个1μs),加上GPU提交延迟,很容易超过16ms的60fps预算。而1000个只有1ms——这就是为什么优化目标通常是"每帧<3000 Draw Call"。在移动设备上,这个目标要更严格,通常希望 < 500。

22.4 游戏类型(Game Types)

22.4.1 为什么游戏类型决定技术选型?

核心原因:不同类型的游戏对渲染的约束完全不同。一个格斗游戏只有两个角色在屏幕上,可以给每个角色3万个三角形。一个RTS游戏有上千个单位,每个单位只能分到200个三角形。

游戏引擎架构层次
图22-4:游戏引擎架构层次——平台→渲染→玩法→资源→工具

22.4.2 按场景规模分类

类型例子场景规模图形策略核心挑战
室内封闭Devil May Cry, God of War1-2个房间高细节、烘焙光照有限空间内的极致画质
线性关卡Uncharted, Tomb Raider几十个关卡中细节、混合光照引导注意力
沙盒开放GTA, Skyrim几km²远粗近细、流式加载大世界的性能一致性
MMO大世界WoW, FF14几十km²严格LOD、极致流式多人同屏的性能保障

22.4.3 按美术风格分类

风格例子技术特点图形投入
写实Crysis, CODPBR、HDR、CSM、SSR极高
风格化写实战神、神秘海域风格化材质、强光影对比
卡通喷射战士、原神描边、平面色、Cel-shading
NPROkami, 茶杯头水彩/油画滤镜、手绘风格特殊
🔥 重要洞察:渲染目标决定一切!《大神》(Okami)不用PBR,但每个像素都是艺术品。你的游戏不需要"最好的渲染技术",需要的是"最适合游戏风格的渲染技术"。

22.5 游戏生产流程(The Game Production Process)

22.5.1 标准开发流程

概念设计 (Concept Art)
   ↓
立项 (Greenlight) — 给发行商看Demo
   ↓
Pre-production (引擎选型、工具链搭建、技术验证)
   ↓
Full Production (主要内容制作——耗时最长)
   ↓
Alpha (内部测试、功能冻结)
   ↓
Beta (外部测试、修Bug)
   ↓
Gold / Release (送审、发布)
   ↓
补丁 / DLC (持续运营1-5年)

22.5.2 资产创建流程

💡 生活类比:做雕塑

想象一个雕塑家的工作流程:先用粗泥塑一个大概的形状(=高模),然后用工具刻出细节(=ZBrush雕刻),最后翻模做出一个轻便的塑料复制品(=低模+法线贴图)。游戏资产创建完全一样——先做高模(细节丰富但太"重"),然后"烤"出法线贴图用在低模上。

高模 (Hi-poly, ZBrush, 数百万面)
  ↓ 烘焙 (Bake Normal Map)
法线贴图 (Normal Map, 编码细节到颜色)
  ↓
低模 (Low-poly, Maya/Blender, 几千面)
  ↓
UV展开 (UV Mapping, 连续无撕裂)
  ↓
纹理贴图 (Diffuse, Normal, Specular, Roughness, AO)
  ↓
Shader配置 (PBR参数调试)
  ↓
骨骼绑定 (Rigging) + 蒙皮 (Skinning)
  ↓
动画 (Animation)
  ↓
入库 (Import to Engine)

22.5.3 法线贴图深度解析

为什么需要法线贴图?一个角色的皮肤上有毛孔、皱纹、伤疤——这些细节如果用三角形建模,需要几百万个三角形。法线贴图用一张图片"假装"有这些细节,让光照计算认为表面是凹凸不平的,但实际几何是平滑的。

// Shader中解码法线贴图
vec3 normal = texture(normalMap, uv).rgb * 2.0 - 1.0;
// 法线贴图的RGB编码了切线空间中的法线方向
// *2-1 把[0,1]映射回[-1,1]
normal = normalize(TBN * normal);
// 从切线空间变换到世界空间
🔥 重要洞察:法线贴图为什么是紫色的?因为B通道(蓝色)= Z分量,通常朝外(接近1);R和G通道接近0.5(中灰)。中灰+蓝色=紫色。看到紫色法线贴图时,你就知道"这是正确的"。

22.6 场景管理(Scene Management)

22.6.1 为什么需要场景管理?

核心问题:没有场景管理 = O(n)检查所有物体。10万个物体时,每帧要遍历10万次——仅这个操作就超过10ms。

无场景管理: for each obj in 100000: test(obj)   → 100000次测试
有Octree:   for each leaf in visible_set: test(obj) → 8-64次测试
加速比: 1000倍以上!

22.6.2 空间数据结构对比

数据结构原理查询构建动态更新适合场景
BVH二叉树,任意轴划分O(log n)O(n log n)光线追踪、静态几何
Octree8等分立方体O(log n)O(n log n)户外、Voxel
K-d Tree二叉树,坐标轴交替O(log n)O(n log n)光线追踪(最佳)
BSP二叉平面分割O(log n)O(n²)不可变静态室内(Quake)
Uniform Grid规则网格O(1)~O(n)O(n)粒子、规则场景
🔧 技术选型:选A不选B的理由
场景推荐不推荐理由
光线追踪加速BVH + SAH均匀网格BVH自适应空间分布,比网格更紧凑
室内静态场景BSPOctreeBSP的PVS预计算对室内最有效
动态大世界Octree/Dynamic BVHK-d TreeK-d Tree重构代价太高

22.7 碰撞检测(Collision Detection)

22.7.1 包围体(Bounding Volumes)

类型表示紧密性测试速度适合
AABB6个轴对齐面最快静态物体粗筛
OBB任意方向盒旋转物体
Sphere球体球类、快速测试
Capsule圆柱+半球角色碰撞(腿+身)
Convex Hull凸包任意凸形
Mesh三角形精确完美精确需求
🔥 重要洞察:永远不要用Mesh做碰撞!一个角色的碰撞应该用Capsule(胶囊体),而不是角色的精细三角网格。精细碰撞的角色就像用放大镜检查每个螺丝——99%的情况根本不需要。

22.8 渲染循环(Render Loop)

渲染循环流程图
图22-5:游戏渲染循环——每帧的完整生命周期

22.8.1 标准游戏循环

While (game is running):
  ProcessInput()     // 处理键盘、鼠标、手柄输入
  Update()           // 游戏逻辑 + 物理 + 动画 + AI
  Render()           // 裁剪→排序→渲染→后处理
  WaitForFrameEnd()  // 限流到30/60fps

22.8.2 一帧的完整渲染管线

Frame N
├─ CPU阶段
│   ├─ 输入处理: 读取鼠标/键盘/手柄
│   ├─ 游戏逻辑: 玩家状态、AI决策、事件处理
│   ├─ 物理模拟: 碰撞检测、刚体/软体
│   ├─ 动画更新: 骨骼动画、变形目标、IK
│   └─ 相机更新: 设置视锥体参数
│
├─ 场景更新 (CPU)
│   ├─ 视锥剔除 → 生成可见列表
│   ├─ 遮挡剔除 → 精炼可见列表
│   ├─ 排序: 不透明前到后、半透明后到前
│   └─ 构建Command Buffer
│
├─ GPU渲染
│   ├─ Shadow Pass: 所有阴影投射体
│   ├─ Depth Prepass: 深度预渲染(可选)
│   ├─ Opaque Pass: 不透明物体(Early-Z)
│   ├─ Sky: 天空盒/天空球
│   ├─ Transparent Pass: 半透明(无Early-Z)
│   ├─ Post-Process: Bloom、Tonemap、AA
│   └─ UI: 界面层
│
└─ Present: 交换前后缓冲
🔥 重要洞察:现代引擎中CPU和GPU是完全并行的。CPU在处理第N帧的输入和逻辑时,GPU正在渲染第N-1帧。流水线深度通常为2-3帧。这就是为什么游戏延迟通常有2-3帧——不是"慢",是"并行化的代价"。

22.9 调试与性能分析(Debug & Profiling)

22.9.1 常见瓶颈速查

症状原因排查工具解决方案
GPU利用率100%GPU满载RenderDoc, PIX降低分辨率、简化Shader
CPU利用率100%渲染线程忙Unreal InsightsJob System、减少Draw Call
Draw Call太高物体太碎stat unit (UE)合批、Instancing
SetPass太多材质切换频繁stat shader材质合并、排序
三角形太多几何没LODstat rhi加LOD、HLOD
🤔 想一想:为什么"我觉得这里慢"几乎总是错的?
因为人的直觉对于"多线程并行环境下的瓶颈定位"完全不可靠。可能你觉得"场景物体太多"所以去优化剔除,但实际瓶颈是"阴影贴图分辨率太高"。必须用Profiler(如RenderDoc、Unreal Insights)先测量,再优化。这就是游戏引擎的黄金法则:Measure first, optimize second

全章总结

🔥 核心洞察:本章不是讲"一个具体算法",而是讲"如何在有限资源下做出产品"。游戏图形学的本质不是"做出最美画面",而是"在16ms内做出最好的画面"。
主题一句话
平台没有"通用游戏"——选型看平台,平台决定约束
资源CPU/GPU/Memory/团队时间,全是瓶颈,一个都不能忽视
预算30fps=33ms,每毫秒都要精打细算
优化不画 > 画简单 > 画复杂,从易到难
场景管理没有空间分割 = O(n) = 卡死
碰撞包围体粗筛(Broad Phase)+ 精确细检(Narrow Phase)
LOD远粗近细,HLOD是大世界终极武器
生产流程美术是团队的70%,程序员为美术服务
渲染循环CPU/GPU完全并行,流水线深度2-3
ProfileMeasure first, optimize second
💡 最终类比:厨师做菜

游戏图形学就像米其林大厨做菜——不是"能做出什么"的问题,而是"在30分钟内给100人做出可口的菜"。你需要知道每种食材的准备时间(=渲染开销),合理安排工序(=管线),在时间压力下做出最好的味道(=画面)。真正的厨师(=游戏工程师)不是只懂做菜(=图形学理论),更要懂厨房管理(=资源预算、团队协作)。

给读者的最后一句

"游戏图形学不是科学,是工程。"
你不需要发明新算法——你需要把已知算法用对地方。99%的性能问题都来自"在错的地方用了对的算法"。
Measure. Profile. Optimize. Repeat.


讲义完成。第22章覆盖了从平台到优化的全部"工程"视角——这是你从"学生"到"工程师"的分水岭。

目录 X
章节 X