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 :并非所有粒子都需要绘制,可选执行

单帧执行流程

  1. Update Pass(更新阶段) :对现有缓冲区中的每个粒子执行 DispatchIndirect
    • 存活的粒子 → 添加到新缓冲区的存活列表
    • 死亡的粒子 → 丢弃
  2. Spawn Pass(生成阶段) :在另一条路径中生成新粒子,追加到同一缓冲区
  3. Draw Pass(绘制阶段) :通过 DrawIndirect 渲染粒子
  4. 下一帧 :翻转(Flip)读/写缓冲区,重复上述流程

核心设计思想:全流程 GPU 自驱动,CPU 无需回读粒子数量,通过 Indirect 调用让 GPU 自行决定工作量。


实战案例:雨滴飞溅(Rain Splashes)

这是 GPU 粒子系统的 标杆案例 ,充分展示了屏幕空间信息驱动粒子生成的威力。

工作原理

  1. 分析深度缓冲(Depth Buffer) :派发多个 Wave Front 在屏幕空间大量采样深度值
  2. 读取 G-Buffer 参数 :获取法线、材质等信息,判断当前表面是否适合生成飞溅
  3. 满足条件时生成雨滴飞溅粒子 :直接在 GPU 上完成生成决策

相比传统 CPU 方案的优势

维度传统 CPU 粒子GPU 驱动粒子
放置方式美术手动放置发射器,或绑定骨骼偏移无需任何手动放置 ,自动适配场景几何
几何贴合度难以精确跟随复杂几何表面直接从深度缓冲采样,完美贴合几何
角色支持需要绑定骨骼关节 + 偏移,灵活性差自动生效于角色 ,无额外设置
性能大量发射器开销高仅在屏幕可见区域生成 ,天然剔除不可见部分,非常廉价
美术工作量需要大量手工摆放零放置工作量

系统间通信与 CPU/GPU 事件机制

全局缓冲区(Global Buffers)—— 粒子系统间通信

  • 当需要更复杂的行为时,系统支持创建 全局共享缓冲区(Global Buffers) ,多个粒子系统可以 写入读取 同一个缓冲区

  • 具体案例——水流碰撞飞溅:

    1. 存在一套 幻影粒子(Phantom Particles) 系统:这些粒子从水流中下落,本身不可见
    2. 幻影粒子与 深度缓冲 做碰撞检测
    3. 碰撞发生时,向一个名为 "Splash Hints Buffer" 的全局缓冲区写入碰撞提示
    4. 多个系统都可以向该缓冲区贡献数据
    5. 雨滴飞溅系统在生成新粒子时 读取该 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 只是基于上一帧的信息,无法处理加速度或减速度 ,预测必然存在误差:

  1. 粒子被预测到了一个 新位置(可能偏移了)
  2. 在该新位置处,读取 当前帧的 Motion Vector ——它告诉我们"这个新像素在上一帧位于哪里"
  3. 这条 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)
  • 典型案例:角色皮肤表面的 水滴滑痕 ——水滴在重力作用下沿表面向下流动,形成连续的水痕轨迹
    • 在遇到曲率变化时,还会 溅射出小液滴

飘带的技术挑战

飘带比独立粒子复杂得多,需要解决以下问题:

  1. 顺序性(Ordering) :粒子必须维持 特定的序列顺序 ,才能正确连接成带状
  2. 连通性(Connectivity) :需要追踪粒子之间的 连接关系
  3. 粒子死亡处理 :序列中某个粒子消失后,不能盲目连接剩余粒子,否则会产生 跨越大段空间的错误连接
  4. 长度溢出 :飘带粒子数量超出容量时的处理

基于 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 空间的转换算法

完整流程如下:

  1. 粒子落在某个 像素
  2. 采样该像素的 Object ID Buffer → 获取 Mesh ID + 三角形信息
  3. 重建三角形 ,计算粒子位置在该三角形上的 重心坐标(Barycentric Coordinates)
  4. 查找该 Mesh 的 映射表(Mesh Mapping) ——由 CPU 预生成并上传至 GPU:
    • 判断该 Mesh 是否是角色
    • 是否拥有 Render Target
    • 获取 Render Target ID
  5. 用重心坐标 插值该三角形的纹理坐标 → 得到 UV 坐标
  6. 向 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 采样机制

基本流程

  1. 在角色身体周围放置 Box Spawner(盒状生成区域)
  2. 在盒内 随机采样点 ,投射到屏幕空间
  3. 利用之前介绍的 屏幕空间 → UV 空间转换 流程:
    • 判断采样点是否落在 角色表面
    • 如果是,读取该像素在角色 Blood Render Target 中对应的 UV 位置
    • 查询该 UV 位置的像素:"你是否有血(Is it bloody)?"
  4. 只有在检测到血液的位置 ,才生成血液飘带(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 上正确重建位置

解决方案:屏幕空间回退

  1. 当检测到 LOD 切换时,回退到屏幕空间追踪 ——仅持续 一帧
  2. 在该帧内,利用屏幕空间位置重新 采样新 LOD 的 Object ID Buffer
  3. 获取新 Mesh 上的 新三角形 ID + 新重心坐标
  4. 从下一帧起恢复基于三角形的持久追踪

替代方案:也可以进行 重新蒙皮(Reskinning) ,但实践中三角形追踪 + 屏幕空间回退已经足够稳定。


Object ID Buffer 的局限性

场景Object ID 支持情况备注
角色✅ 已有 Object ID直接可用
背景物体❌ 未渲染 Object ID如草地、树叶等无法使用此方案
扩展可能可以为背景物体添加 Object ID 渲染但需额外开销
  • 对于不支持 Object ID 的物体(如背景叶片),落在上面的粒子 无法使用 三角形持久化方案

持久附着积雪效果

效果描述

  • 雪花粒子 持久附着于角色表面
  • 当相机飞离再飞回时,雪花仍然存在——真正的持久化,不因离开屏幕而消失
  • 角色在雪中奔跑时,身上逐渐积雪

战斗场景中的细节

  • 在雪地战斗中,敌方角色身上也会积雪
  • 效果虽然微妙,但一旦注意到,就会显著提升 沉浸感
  • 这种细节 无需美术为每个角色单独设置 ,系统自动对所有角色生效

粒子交互与表面采样(Particle Interactions and Surface Sampling)


飘带与表面纹理的交互

水滴遇血变色效果

  • 由于粒子系统已经具备 屏幕空间 → UV 空间 的完整转换链路,飘带在流动过程中可以 实时采样角色表面纹理信息
  • 具体案例——角色身上的 水滴飘带(Water Drips)
    • 水流在手臂等 干净区域 流过时,保持 透明/清澈 外观
    • 当水流经过 有血迹的区域 时,飘带会 自动染成红色
    • 实现方式:
      1. 飘带每推进一步,采样当前位置对应的 Blood Render Target
      2. 检查该 UV 位置是否标记为"有血(Bloody)"
      3. 若是,则将飘带的 颜色/着色参数(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》的视觉真实感提供了关键支撑。