GPU Driven Effects of The Last of Us Part II
GPU Driven Effects of The Last of Us Part II by Artem Kovalovs || SIGGRAPH 2020 - YouTube
GPU 驱动粒子的动机与优势
- GPU 驱动粒子(GPU Driven Particles) 并非全新概念,业界已有大量先例(如 Wihlidal 等人的工作),但 Naughty Dog 为本作构建了一套 全新的 GPU 粒子系统
- 粒子天然适合 GPU 并行计算 ,每个粒子的更新逻辑高度独立,非常契合 GPU 的 SIMT 架构
- 最初目标:将 CPU 上的粒子工作负载卸载(Offload)到 GPU ,释放 CPU 算力
- 更深层的收获:团队发现 屏幕空间(Screen Space) 中蕴含大量可利用的信息(深度缓冲、G-Buffer 参数等),可以用来驱动各种全新效果:
- 液体滴落模拟(Liquid Drip Simulation) :用于血液和水流
- 飘带效果(Ribbons)
- 物体附着(Attachment to Objects) :粒子廉价地附着在角色/物体表面,大幅提升真实感
粒子系统的基本架构
缓冲区与调度设置
- 系统使用 双缓冲区(Double Buffering / Ping-Pong) 方案:
- 每帧在两个 Buffer 之间 翻转(Flip) ,一个用于读取上帧数据,一个用于写入本帧结果
- 每个缓冲区附带 GDS 原子计数器(GDS Atomic Counters) ,用于追踪当前存活粒子数量,支持无锁地向列表追加(Append)粒子
- 系统提供了一个类似 命令行界面(CLI-like Interface) 的配置列表,上层有 UI 封装:
- 可以指定不同的 Pass(更新 Pass、生成 Pass、绘制 Pass 等)
- 调度方式支持 DispatchIndirect :
- 可以基于 计数器大小(Counter Size) 动态决定线程组数量
- 也可以指定 固定大小(Fixed Size)
- 绘制使用 DrawIndirect :并非所有粒子都需要绘制,可选执行
单帧执行流程
- Update Pass(更新阶段) :对现有缓冲区中的每个粒子执行 DispatchIndirect
- 存活的粒子 → 添加到新缓冲区的存活列表
- 死亡的粒子 → 丢弃
- Spawn Pass(生成阶段) :在另一条路径中生成新粒子,追加到同一缓冲区
- Draw Pass(绘制阶段) :通过 DrawIndirect 渲染粒子
- 下一帧 :翻转(Flip)读/写缓冲区,重复上述流程
核心设计思想:全流程 GPU 自驱动,CPU 无需回读粒子数量,通过 Indirect 调用让 GPU 自行决定工作量。
实战案例:雨滴飞溅(Rain Splashes)
这是 GPU 粒子系统的 标杆案例 ,充分展示了屏幕空间信息驱动粒子生成的威力。
工作原理
- 分析深度缓冲(Depth Buffer) :派发多个 Wave Front 在屏幕空间大量采样深度值
- 读取 G-Buffer 参数 :获取法线、材质等信息,判断当前表面是否适合生成飞溅
- 满足条件时生成雨滴飞溅粒子 :直接在 GPU 上完成生成决策
相比传统 CPU 方案的优势
| 维度 | 传统 CPU 粒子 | GPU 驱动粒子 |
|---|---|---|
| 放置方式 | 美术手动放置发射器,或绑定骨骼偏移 | 无需任何手动放置 ,自动适配场景几何 |
| 几何贴合度 | 难以精确跟随复杂几何表面 | 直接从深度缓冲采样,完美贴合几何 |
| 角色支持 | 需要绑定骨骼关节 + 偏移,灵活性差 | 自动生效于角色 ,无额外设置 |
| 性能 | 大量发射器开销高 | 仅在屏幕可见区域生成 ,天然剔除不可见部分,非常廉价 |
| 美术工作量 | 需要大量手工摆放 | 零放置工作量 |
系统间通信与 CPU/GPU 事件机制
全局缓冲区(Global Buffers)—— 粒子系统间通信
-
当需要更复杂的行为时,系统支持创建 全局共享缓冲区(Global Buffers) ,多个粒子系统可以 写入 和 读取 同一个缓冲区
-
具体案例——水流碰撞飞溅:
- 存在一套 幻影粒子(Phantom Particles) 系统:这些粒子从水流中下落,本身不可见
- 幻影粒子与 深度缓冲 做碰撞检测
- 碰撞发生时,向一个名为 "Splash Hints Buffer" 的全局缓冲区写入碰撞提示
- 多个系统都可以向该缓冲区贡献数据
- 雨滴飞溅系统在生成新粒子时 读取该 Hints 缓冲区 ,在碰撞点额外生成飞溅
-
效果 :角色 Abby 走入水流时,幻影粒子碰撞到她的身体,触发飞溅效果——雨滴飞溅不仅来自降雨,还来自水流碰撞,效果自然丰富
GPU → CPU 事件队列(Event Queue)
-
系统实现了一个 从 GPU 向 CPU 发送事件的队列 :
- GPU 端:粒子在 Compute Shader 中将事件 写入事件队列缓冲区
- CPU 端:下一帧(或延迟若干帧)回读(Readback)事件数据
- CPU 收到事件后 → 转发给 脚本系统(Script) 或其他游戏逻辑
-
实际应用举例:
- Abby 走入水流时,角色会 做出躲避/畏缩的肢体反应(Flinch Gesture)
- 实现原理:水流碰撞粒子在 GPU 上检测到碰撞 → 写入事件 → CPU 回读 → 脚本接收事件 → 播放对应动画
- 该事件队列还用于 触发音效(Sound Spawning) 和 生成其他 CPU 粒子
-
核心价值 :打通了 GPU 粒子世界与 CPU 游戏逻辑世界之间的 双向通信桥梁 ,使 GPU 粒子不再是孤立的视觉效果,而是能够影响游戏玩法和角色行为的完整系统
关键架构总结
┌─────────────────────────────────────────────────┐
│ GPU 端 │
│ │
│ Depth Buffer / G-Buffer │
│ │ │
│ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ 幻影粒子系统 │───▶│ Global Splash │ │
│ │ (碰撞检测) │ │ Hints Buffer │ │
│ └──────────────┘ └───────┬──────────┘ │
│ │ │
│ ┌────────────────────┘ │
│ ▼ │
│ ┌──────────────┐ │
│ │ 雨滴飞溅系统 │──▶ DrawIndirect 渲染 │
│ │ (读取 Hints) │ │
│ └──────┬───────┘ │
│ │ 写入事件 │
│ ▼ │
│ ┌──────────────┐ │
│ │ GPU→CPU 事件 │ │
│ │ 队列 │ │
│ └──────┬───────┘ │
└─────────┼───────────────────────────────────────┘
│ Readback
▼
┌─────────────────────┐
│ CPU 端 │
│ 脚本 / 音效 / 动画 │
└─────────────────────┘
核心设计理念:
- 屏幕空间即数据源 :深度缓冲和 G-Buffer 是粒子生成的天然信息宝库
- 全 GPU 自驱动 :通过 DispatchIndirect / DrawIndirect 避免 CPU-GPU 同步
- 全局缓冲区解耦通信 :多个粒子系统通过共享缓冲区协作,无需知道彼此存在
- 事件队列打通双向 :GPU 效果可以反馈影响游戏逻辑,实现物理反馈、音效触发等
持久附着于变形几何体(Persistent Attachment to Deforming Geometry)
为什么需要"持久附着"
- 之前介绍的雨滴飞溅(Rain Splashes)是 瞬时效果 ——粒子生成后很快消亡,无需跟踪
- 但许多效果需要粒子 持续停留在物体表面 ,并 跟随物体运动/变形 :
- 爆炸碎片 粘在表面
- 粘性积雪 落在角色身上
- 昆虫 在表面爬行
- 液滴 沿表面流淌
- 即便是雨滴飞溅,也最好能在角色移动时 短暂跟随 ,而非静止不动(否则视觉上会显得僵硬)
静态场景中的碰撞附着
基本原理
- 粒子与世界发生碰撞后,停留在碰撞点 不再运动
- 每帧可能产生 数百次碰撞 ,这正是 GPU 粒子的强项——大规模并行碰撞检测 + 快速生命周期管理
- 粒子寿命短、生灭快,整体开销很低
案例:蟑螂效果(Roaches)
- 蟑螂粒子 附着在场景表面,能够爬行、散开
- 利用 深度缓冲(Depth Buffer) 获取表面位置,利用 G-Buffer 法线 让蟑螂沿表面法线方向移动
- 实现了以下视觉效果:
- 蟑螂看起来像是 躲在几何体后面
- 沿着表面的 法线变化 爬行,贴合感极强
- 遇到手电筒光源时 四散逃离
- 关键优势:
- 全部在 GPU 完成 ,非常廉价
- 如果在 CPU 上实现类似行为,平面上还可以,但面对 复杂几何体 会非常困难
- 此效果由 FX 美术使用 PopcornFX 制作
附着于运动变形角色(核心难点)
问题定义
- 当粒子需要附着到 正在运动和变形的角色 上时(如雪花落在走动的角色身上),必须 逐帧更新粒子位置 ,使其跟随角色
- 核心难题 :在屏幕空间中,我们 没有从前一帧到下一帧的正向映射 (Forward Mapping)
- 也就是说:知道粒子上一帧的位置,并 不能直接得知 它下一帧应该在哪里
运动向量驱动的位置预测(Motion Vectors and Position Prediction)
可用信息:Motion Vector Buffer
- TAA(Temporal Anti-Aliasing) 流程中已经生成了 运动向量缓冲区(Motion Vector Buffer)
- 该缓冲区存储的是 反向运动 :对于每个像素,记录的是 该像素上一帧在哪里 (即从当前帧指向上一帧的 2D 屏幕空间偏移)
- 虽然它指向 过去 而非 未来 ,但仍可用于 预测
预测算法
第一步:速度预测
当粒子附着到表面时,读取当前像素的 Motion Vector ,计算 预测速度 :
- :由 Motion Vector 推算出的上一帧位置
- :当前帧位置
- 用该速度将粒子 外推 到下一帧的预测位置
第二步:下一帧校正(Correction)
由于 Motion Vector 只是基于上一帧的信息,无法处理加速度或减速度 ,预测必然存在误差:
- 粒子被预测到了一个 新位置(可能偏移了)
- 在该新位置处,读取 当前帧的 Motion Vector ——它告诉我们"这个新像素在上一帧位于哪里"
- 这条 Motion Vector 代表了 粒子落点处表面的实际运动量
关键校正策略(最终采用的稳定方案)
- 不是 简单计算误差 delta 再叠加(早期方案,不够稳定)
- 而是:在预测位置处采样 Motion Vector,得到 该表面点从上一帧到当前帧的实际位移
- 然后将这个 相同的位移 应用到粒子的 上一帧原始位置 上:
- 本质思路:"你所停留的表面移动了多少,你就跟着移动多少" ——利用预测位置来 查找 表面运动,再将运动应用回粒子
效果评价
- 效果出奇地好 ,且 非常廉价
- 雪花可以真实地落在角色身上,并 跟随角色运动/变形 ,不会出现"游泳"(swimming)现象
极端案例:草叶上的附着
挑战
- 草叶在渲染上只是 带 Alpha Test 的四边形(Quad)切片 ,是 Alpha Cutout 几何体
- 传统碰撞方式难以处理——即使碰撞到了 Quad,还需要做 Alpha Test 判断是否真的命中了草叶
- 这对 CPU 粒子系统来说成本极高
GPU 方案的优雅解法
- 直接与 深度缓冲 碰撞(深度缓冲中已经完成了 Alpha Test,只包含可见像素)
- 碰撞后利用上述 Motion Vector 跟踪方案 跟随草叶表面运动
- 零额外碰撞逻辑 ,一切信息都来自屏幕空间
方案的优缺点总结
| 维度 | 说明 |
|---|---|
| ✅ 极其廉价 | 全部基于已有的屏幕空间缓冲区,无需额外几何查询 |
| ✅ 通用性强 | 自动适配所有可见几何体,包括 Alpha Cutout 草叶等复杂情况 |
| ✅ 视觉效果好 | 雪花、昆虫等附着效果自然真实 |
| ❌ 跟踪不稳定 | 几何体消失、大幅运动、或预测偏差大时,粒子可能 穿透 或 落到完全不相关的表面 |
| ❌ 无法恢复 | 一旦丢失跟踪,没有办法"找回"正确位置,只能 杀死粒子 或让其自由下落 |
| ✅ 对雪等效果足够 | 不需要完美跟踪——少量粒子丢失不影响整体雪的真实感,反而 廉价方案带来的高密度粒子 提升了沉浸感 |
粒子飘带(Particle Ribbons)
飘带的概念与需求
- 之前讨论的都是 独立粒子(Individual Particles) ,但许多效果需要的是 一串有序粒子连接而成的带状结构 ——即 飘带(Ribbon)
- 典型案例:角色皮肤表面的 水滴滑痕 ——水滴在重力作用下沿表面向下流动,形成连续的水痕轨迹
- 在遇到曲率变化时,还会 溅射出小液滴
飘带的技术挑战
飘带比独立粒子复杂得多,需要解决以下问题:
- 顺序性(Ordering) :粒子必须维持 特定的序列顺序 ,才能正确连接成带状
- 连通性(Connectivity) :需要追踪粒子之间的 连接关系
- 粒子死亡处理 :序列中某个粒子消失后,不能盲目连接剩余粒子,否则会产生 跨越大段空间的错误连接
- 长度溢出 :飘带粒子数量超出容量时的处理
基于 Wavefront 的飘带实现
核心设计:飘带 = 一个 Wavefront
- 整条飘带的粒子 存活在同一个 Wavefront(波前) 中
- 一个 Wavefront 有 64 个线程 ,对应最多 64 个粒子
- 粒子按照年龄(Age)正常 生成和死亡
长度溢出处理
- 当飘带变得太长(超过 Wavefront 容量)时:
- 将飘带的 旧片段(Chunks) 复制到一个 静态 Wavefront 中
- 该静态 Wavefront 中的飘带 不再推进 ,只是在原地存活,逐渐消亡
- 活跃的飘带头部继续在原 Wavefront 中推进
丢失追踪时的断裂处理
- 当某个粒子 失去追踪(Lose Tracking) 时(如表面消失于屏幕外):
- 该粒子 仍然保留在 Wavefront 中 ,但标记为 死亡(Dead)
- 渲染时,遇到死亡粒子 不做跨越连接 ,而是 断开飘带 ,渲染为 两条独立飘带
- 避免了"盲目连接存活粒子导致跨大面积错误连线"的问题
- 随着粒子自然生灭,Wavefront 内的数据被 压缩(Compress) ,新生成的粒子形成 前进中的飘带头部
飘带的渲染方式
- 几何形状:本质上是 一串相互连接的四边形(Quads)
- 不直接作为可见几何渲染 ,而是用作 G-Buffer Decal :
- 修改 粗糙度(Roughness)
- 修改 法线(Specular Normals)
- 用于 湿润度(Wetness) 效果
- 这样就能在表面上产生 非常真实的水滴滑痕 的视觉错觉
UV 空间映射与 Object ID Buffer
持久化的需求
屏幕空间的局限
- 之前所有讨论的粒子效果都基于 屏幕空间追踪 ,存在天然缺陷:
- 视角移开后粒子消失 ——无法持久保留
- 追踪丢失 后效果断裂
- 对于 血液滴落 等效果,必须要有 持久化机制(Persistence)
Render Target 持久化方案
- 角色拥有专属的 血液渲染目标(Blood Render Target) ——一张纹理
- 一旦粒子被写入该 Render Target(写入某个三角面对应的 UV 区域),它就 永久存在 ,不依赖屏幕空间追踪
- 核心问题变成:如何将屏幕空间的粒子位置转换为角色的 UV 空间坐标?
Object ID Buffer 驱动的 UV 映射
Object ID Buffer 简介
- 引擎中已有的 Object ID 缓冲区 ,存储每个像素对应的:
- 网格 ID(Mesh ID) :属于哪个 Mesh
- 三角形重建信息 :顶点数据或三角形 ID,足以 重建该像素所在的三角面
- 这类缓冲区在现代 Compute Shader 架构 中越来越常见,引擎应当标配
从屏幕空间到 UV 空间的转换算法
完整流程如下:
- 粒子落在某个 像素 上
- 采样该像素的 Object ID Buffer → 获取 Mesh ID + 三角形信息
- 重建三角形 ,计算粒子位置在该三角形上的 重心坐标(Barycentric Coordinates)
- 查找该 Mesh 的 映射表(Mesh Mapping) ——由 CPU 预生成并上传至 GPU:
- 判断该 Mesh 是否是角色
- 是否拥有 Render Target
- 获取 Render Target ID
- 用重心坐标 插值该三角形的纹理坐标 → 得到 UV 坐标
- 向 CPU 发送事件:"在此 UV 坐标生成血液粒子"
血液系统的实际效果与优势
跨网格流动(Cross-Mesh Flow)
- 屏幕空间模拟的最大优势 :不关心底层是哪个 Mesh
- 血液可以 从一个 Mesh 流到另一个 Mesh (如从皮肤流到绷带)
- 可以 跨越 UV 接缝(UV Seams)
- 无需对 Mesh 做特殊的 UV 展开或拼接处理
材质感知的差异化行为
- 由于能读取 屏幕空间的材质信息(G-Buffer) ,血液在不同材质上表现不同:
- 皮肤上 :血液 先粗后细 ,逐渐收窄为细小的滴痕
- 布料上 :血液行为不同(可能扩散更均匀)
- Clicker(感染者)表面 vs 人类皮肤 :各有不同的视觉表现
动态累积效果
- 近战攻击的演示中:
- 每一刀都产生 新鲜切口 ,起初是干净的伤口
- 随着时间推移,血液 逐渐向下滴落
- 多刀累积后,角色 整体被血液覆盖
- 逐帧可观察 到血液在重力作用下不断向下流动
- 这种动态累积效果极大提升了 真实感和沉浸感
血液溅射(Blood Splatter)
- 流淌中的血液粒子还能 生成小液滴(Droplets)
- 这些液滴会与 自身 Mesh 或其他 Mesh 碰撞 ,产生 二次血液飞溅
性能特点
- 模拟完全在 屏幕空间 完成 → 非常廉价
- 仅在可见区域计算 → 天然的 性能友好
系统架构总结
屏幕空间模拟(GPU) 持久化存储
┌────────────────────────┐ ┌─────────────────────┐
│ 深度缓冲 + G-Buffer │ │ 角色 Blood Render │
│ + Motion Vectors │──模拟──→ │ Target (UV 空间) │
│ + Object ID Buffer │ 血液流动 │ │
└────────────────────────┘ └─────────────────────┘
│ │
│ 采样 Object ID │ 渲染时采样
│ → 重建三角形 │ → 叠加到角色
│ → 计算重心坐标 │ 材质上
│ → 插值得到 UV │
└──── 事件通知 CPU ──→ 写入 ──────────┘
核心洞察 :利用屏幕空间做 物理模拟 (重力、碰撞、材质交互),再通过 Object ID Buffer 转换到 UV 空间 做 持久化存储 ,两者结合既获得了屏幕空间模拟的灵活性(跨 Mesh、跨 UV 缝),又获得了 Render Target 的持久性(不依赖视角)。
在血液与伤口处生成飘带(Spawning Ribbons on Blood and Cuts)
核心问题:飘带从哪里生成?
- 飘带粒子生活在 世界空间(World Space) 中,最终需要转换到 UV 空间 进行持久化
- 关键问题:在世界空间中,应该在哪里开始生成血液飘带?
- 血液来源于 近战攻击产生的伤口(Cuts) ,这些伤口是 预烘焙的(Baked) ——每次近战命中时,对应的血液纹理会被写入角色的 血液渲染目标(Blood Render Target)
Box Spawner 采样机制
基本流程
- 在角色身体周围放置 Box Spawner(盒状生成区域)
- 在盒内 随机采样点 ,投射到屏幕空间
- 利用之前介绍的 屏幕空间 → UV 空间转换 流程:
- 判断采样点是否落在 角色表面
- 如果是,读取该像素在角色 Blood Render Target 中对应的 UV 位置
- 查询该 UV 位置的像素:"你是否有血(Is it bloody)?"
- 只有在检测到血液的位置 ,才生成血液飘带(Drip Ribbon)
效果
- 血液飘带 只从实际受伤的伤口处流出 ,不会在无伤区域生成
- 生成位置完全由 伤口纹理数据驱动 ,无需美术手动指定
生成频率控制
- 每个 Box Spawner 是 独立的生成器
- 系统使用一个 账本系统(Ledger System) 来管理:
- 追踪哪些 Spawner 在何时响应
- 控制 生成速率(Spawn Rate) ,不是每帧都生成
- 避免过度生成导致性能浪费
实际效果展示
角色 Abby 受伤出血
- Abby 手臂上有伤口,血液从伤口处开始 向下滴落
- 爬行时 :血液沿重力方向流动(与地面方向一致)
- 站起后 :重力方向改变,血液 自动改变流向 ,沿手臂侧面流淌
- 提供了极高的 真实感层次
基于三角形的持久化(不依赖渲染目标)
动机
- 并非所有效果都有专属的 Render Target 来持久化(如水滴不需要血液纹理)
- 但仍然需要粒子 持久附着于变形几何体
核心方法:存储三角形 + 重心坐标
原理
- 利用 Object ID Buffer 获取粒子所在的 三角形 ID 和 重心坐标(Barycentric Coordinates)
- 将这两个值 存储在粒子数据中
- 每帧通过三角形 ID + 重心坐标 重建(Reconstruct) 粒子的世界空间位置
- 角色骨骼动画驱动三角形变形 → 粒子位置自动跟随
案例:角色身上的水滴
- 之前在车辆表面展示的水滴飘带,现在应用到 角色身上
- 水流沿手臂向下流动,遇到 曲率变化 时溅射出 小液滴
- 调试视图中可以清楚看到:角色身上有 大量飘带和液滴 同时运行,模拟量很大,但效果极其真实
重力响应
- 改变角色朝向时,所有水滴飘带 自动跟随重力方向变化
- 无需 :
- 角色 UV 展开
- 复杂的液体模拟逻辑
- 任何特殊设置
LOD 切换问题与回退策略
问题
- 粒子依赖的 网格(Mesh) 可能因为 LOD 切换 而改变
- LOD 变化意味着三角形 ID 失效,存储的重心坐标 无法在新 Mesh 上正确重建位置
解决方案:屏幕空间回退
- 当检测到 LOD 切换时,回退到屏幕空间追踪 ——仅持续 一帧
- 在该帧内,利用屏幕空间位置重新 采样新 LOD 的 Object ID Buffer
- 获取新 Mesh 上的 新三角形 ID + 新重心坐标
- 从下一帧起恢复基于三角形的持久追踪
替代方案:也可以进行 重新蒙皮(Reskinning) ,但实践中三角形追踪 + 屏幕空间回退已经足够稳定。
Object ID Buffer 的局限性
| 场景 | Object ID 支持情况 | 备注 |
|---|---|---|
| 角色 | ✅ 已有 Object ID | 直接可用 |
| 背景物体 | ❌ 未渲染 Object ID | 如草地、树叶等无法使用此方案 |
| 扩展可能 | 可以为背景物体添加 Object ID 渲染 | 但需额外开销 |
- 对于不支持 Object ID 的物体(如背景叶片),落在上面的粒子 无法使用 三角形持久化方案
持久附着积雪效果
效果描述
- 雪花粒子 持久附着于角色表面
- 当相机飞离再飞回时,雪花仍然存在——真正的持久化,不因离开屏幕而消失
- 角色在雪中奔跑时,身上逐渐积雪
战斗场景中的细节
- 在雪地战斗中,敌方角色身上也会积雪
- 效果虽然微妙,但一旦注意到,就会显著提升 沉浸感
- 这种细节 无需美术为每个角色单独设置 ,系统自动对所有角色生效
粒子交互与表面采样(Particle Interactions and Surface Sampling)
飘带与表面纹理的交互
水滴遇血变色效果
- 由于粒子系统已经具备 屏幕空间 → UV 空间 的完整转换链路,飘带在流动过程中可以 实时采样角色表面纹理信息
- 具体案例——角色身上的 水滴飘带(Water Drips) :
- 水流在手臂等 干净区域 流过时,保持 透明/清澈 外观
- 当水流经过 有血迹的区域 时,飘带会 自动染成红色
- 实现方式:
- 飘带每推进一步,采样当前位置对应的 Blood Render Target
- 检查该 UV 位置是否标记为"有血(Bloody)"
- 若是,则将飘带的 颜色/着色参数(Tint) 设为红色
技术实现回顾
- 飘带本质上仍然是 一系列有序四边形(Quads) ,附着在角色表面
- 通过修改 G-Buffer 属性(粗糙度、法线、颜色等)来表现液体外观
- 飘带在生长过程中,可以:
- 获取网格数据(Mesh Data)
- 获取 UV 坐标
- 采样任意纹理 来驱动行为或外观变化
水滴清洗血迹效果
- 更进一步的交互:水滴飘带 流过血迹区域时,可以清除血液
- 实现原理极其简单:
- 正常血液粒子向 Blood Render Target 写入 1(有血)
- 水滴飘带经过时,生成一个粒子向 Blood Render Target 写入 0(无血)
- 血迹就被 "冲刷"掉了
- 这是屏幕空间 + UV 空间双向通信带来的 自然涌现行为 ——系统架构到位后,这类交互几乎"免费"获得
性能与异步计算(Performance and Async Compute)
性能数据
更新开销
| 指标 | 数值 |
|---|---|
| 粒子数量 | 最多 16,000 个粒子/帧 |
| 更新耗时 | 约 < 0.04 毫秒 |
| 渲染 vs 更新 | 渲染开销 高于 更新开销 |
为什么如此高效
- 纯屏幕空间操作 :仅采样深度缓冲、Stencil 缓冲、G-Buffer 等已有数据
- 无循环、无遍历 :不需要遍历场景几何体、不需要碰撞检测遍历
- 无预处理 :不需要展开角色 UV、不需要预计算世界空间位置来驱动模拟
- 与传统 CPU 方案相比,节省了大量的 预处理开销
异步计算(Async Compute)的重要性
核心策略
- GPU 粒子系统的 Compute Shader 工作 放入异步计算队列(Async Compute)
- 利用 图形管线的空隙(Gaps) 来执行:
- 图形管线在执行 几何 Pass(Geometry Passes) 时,GPU 的 Compute Unit 可能未被充分利用
- 将粒子更新的 Compute Shader 填入这些 "气泡(Bubbles)" 中
效果
- 异步计算使得粒子更新 "几乎免费"(Almost Free)
- 不是真正的零开销,但由于填充了原本浪费的 GPU 周期,实际增量成本极低
- 对于这类 大量轻量级 Compute Shader 的系统,异步计算是 必须利用 的特性
总结与启示(Conclusion)
项目初始目标 vs 实际收获
| 初始目标 | 实际收获 |
|---|---|
| 将 CPU 粒子工作 卸载到 GPU | ✅ 完成,这是"简单的部分" |
| —— | 发现 屏幕空间信息 蕴含巨大潜力,远超预期 |
屏幕空间驱动粒子的核心价值
开启的效果可能性
- 昆虫栖息在草叶上 :角色爬过草丛时,昆虫从草叶上飞走——利用深度缓冲检测角色接近
- 水滴遇血变色 :飘带采样 Blood Render Target 实时变色
- 水滴冲刷血迹 :向 Render Target 写入零值,清除血液标记
- 雨滴飞溅 :完美贴合任意几何,零手动放置
- 动态滴落 :跟随重力在变形角色表面流动
核心设计哲学
一旦建立了屏幕空间 ↔ UV 空间 ↔ 世界空间的完整转换管线,各种涌现式交互行为几乎"自然发生"。
- 系统之间通过 全局缓冲区(Global Buffers) 和 Render Target 通信
- 每个效果都是在已有管线基础上的 低成本增量
- 最终结果是 高保真度(High Fidelity) 和 丰富细节层次 的显著提升
关键技术栈回顾
屏幕空间信息(Depth / G-Buffer / Stencil / Motion Vectors / Object ID)
↓
GPU Compute Shader(DispatchIndirect / Async Compute)
↓
粒子行为(生成 / 更新 / 碰撞 / 追踪 / 附着)
↓
UV 空间持久化(Blood Render Target / Triangle + Barycentric)
↓
渲染输出(G-Buffer Decal / DrawIndirect)
整套系统实现了 从 CPU 卸载 → GPU 自驱动 → 屏幕空间感知 → UV 空间持久化 → 系统间交互 的完整闭环,为《最后生还者 Part II》的视觉真实感提供了关键支撑。