GPU Gems 3 · Chapter 3. DirectX 10 Blend Shapes: Breaking the Limits

从顶点属性上限出发,对比 stream-out 多 pass 与 buffer-template 单次 shader 循环,让复合表情突破四个 active blend shapes 的旧限制。

学习目标

  • 能解释 blend shape 如何由 neutral mesh 的 per-vertex delta 组成,并说明为什么同时绑定很多表达式会撞上顶点属性预算
  • 能修改 Blend Shape Lab 的 active shapes、delta 通道、压缩和缓存选项,比较 stream-out 与 buffer-template 的 pass、读取和调用信号
  • 能回答:表情数量较少、需要缓存最终网格,或表情数量较多、希望一次 draw 完成时,应该怎样选累积策略

先看一张会叠加的脸

想象一张中性的脸:嘴角、眼皮、眉毛都是可以单独推拉的小纸片。一次只动嘴角很简单;但如果同时眨眼、挑眉、微笑和说话,真正困难的不是“能不能相加”,而是“这些小纸片怎样送到 GPU”。

本章解决的问题是:如何让几十个表情组件参与同一次变形,而不把所有数据都塞进有限的顶点输入槽?如果不解决它,程序要么把合成工作退回 CPU,要么把表情限制在很小的集合;两者都会牺牲响应速度或表现力。

表情不是一张新网格,而是 neutral 上的加权 deltaneutral meshposition · normal · tangentexpression deltasD₁…Dₙ · per-vertex deltaweighted sumw₁…wₙ · active setfinal facefinal position + shadingP_final = P_neutral + Σ(weightᵢ × deltaᵢ)位置、法线和切线可以改变;不变的 UV 不必重复存储同一套表达式组件可以组合出眨眼、挑眉、微笑等复合表情

1. blend shape:neutral 加上带权重的差分

一个表情不是一张完整的新网格,而是 neutral mesh 到目标姿势的差分向量。对一个顶点来说,可以把它写成:deltaᵢ = poseᵢ - neutral;最终位置则是 neutral + weight₁ × delta₁ + weight₂ × delta₂ + ...。法线和切线也可以采用同样的差分思路,UV 等没有变化的属性继续从 neutral mesh 复用。

这一定义让“复合表情”自然出现:眨眼、微笑和挑眉各自只影响自己的 delta,权重可以同时非零。动画曲线只需更新当前有效的权重和索引,不必每帧重新制作一张完整脸。

主要表情与正交表情可以叠加base Bblinkbase AA → B + blink权重不是只能选一个表情active weightsbase_B = 0.65blink = 0.35smirk = 0.20每帧只把非零项放入 active listnumBS 控制 shader 循环长度

2. attribute budget:为什么四个 active shapes 不够用

旧的 GPU 路线把每个 active blend shape 的位置、法线和切线差分都作为顶点属性绑定。neutral mesh 已经占据位置、法线、切线和 UV;再加四个表达式,很快就接近 DirectX 10 的 16 个输入属性预算。把 54 个表达式全部同时绑定,会产生远超输入槽的属性宽度。

先撞到的是输入槽上限,不是表情数量上限一个 pass:neutral + 4 shapes5 个 slot 组装出最多 4 个 active delta全部 54 shapes 同时绑定54 组表达式会形成远超上限的属性宽度解决思路:分 pass,或把 delta 放进 GPU buffer 后在 shader 内读取

这里的限制不是“艺术家只能做四种表情”,而是“一次 vertex shader invocation 只能直接看到很窄的一组输入”。因此本章的两条路线都保持同一个目标:静态数据可以很多,但每次处理只让当前需要的 delta 进入计算。

3. stream-out:用多 pass 拆开属性宽度

stream-out 方法把 active 表情分组,每轮最多合并一到四个 delta。第一轮从 neutral 开始,输出“neutral 加第一组表达式”;第二轮读取上一轮结果,再叠加下一组。两个 vertex buffer 交替作为输入和输出,直到所有 active shapes 都被累积,最后把结果当作普通 mesh 绘制。

多 pass 换输入宽度:每轮只处理一小组 deltapass 1neutral+ A + B + C + Dwrite resultpass 2result 1+ E + F + G + Hwrite resultpass 3…Nresult 2+ next groupswrite resultdrawfinalmeshbuffer A / B 交替:输入和输出不在同一块 buffer适合:active shapes 较少,或想把最终结果缓存下来

如果每轮能处理 k 个表达式,active shapes 为 n 时,累积轮数大致是 ceil(n / k)。这增加了 pass 和中间 buffer 流转,但有一个重要优点:最终顶点可以留在 stream-out buffer 中。角色定制完成后,可以缓存结果,后续帧不必再次合成全部表情。

4. buffer template:把迭代留在 vertex shader

第二条路线把所有静态 blend shape 放进一个大 buffer,neutral mesh 仍由 input assembler 提供,delta 则通过 shader resource view 在顶点着色器中显式 Load()。另有两个动态的小 buffer 保存 active indices 和 weights,numBS 决定本帧循环多少项。

SV_VertexID 让每个顶点知道自己在 mesh 中的编号。着色器用 shapeOffset + 3 × vertexID + channel 定位位置、法线和切线差分,再乘权重累加到当前顶点。静态数据可以远超输入属性槽,单次 draw 便能完成 active list 的迭代;代价是每个顶点要执行更多 Load 和循环。

把迭代留在 vertex shader:一次 draw,按需 Load deltaneutral meshposition · normal · tangent · UVall deltasimmutable SRV bufferactive listindices + weights + numBSvertex shaderSV_VertexID → Load loopfor each active shape: offset → Load(delta) → add(weight × delta)buffer 可容纳远超输入槽的静态数据;每个顶点只读取当前需要的表达式代价从多次 stream-out 变成 vertex shader 的循环与 buffer 读取
float3 pos = input.position;
float3 normal = input.normal;
float3 tangent = input.tangent;
 
for (uint i = 0; i < numBS; ++i) {
  uint base = bsPitch * bsOffsets.Load(i);
  float weight = bsWeights.Load(i);
  float3 dp = bsVertices.Load(base + 3 * vertexID + 0);
  float3 dn = bsVertices.Load(base + 3 * vertexID + 1);
  float3 dt = bsVertices.Load(base + 3 * vertexID + 2);
  pos += dp * weight;
  normal += dn * weight;
  tangent += dt * weight;
}

注意“无限”只描述地址空间相对输入槽更宽,不代表 GPU 工作量无限免费:active list 越长,Load 越多;如果每个 delta 还包含切线,带宽和 shader 循环都会增加。

5. 两条路线的取舍:调用、读取与缓存

多 pass 的可缓存性,换一次 draw 的 shader 循环stream-outpass 数随 active shapes 增长12345优点:结果可 stream-out 后复用代价:中间 buffer 往返与多次提交buffer template一次 draw · shader 内循环优点:active shapes 多时更少 API 流转代价:shader Load 与循环成本会增长

stream-out 的性能会随着表达式组数增加而下降,因为 CPU 需要驱动更多轮 buffer 流转;buffer-template 把迭代保留在 vertex shader,通常能减少中间流转和 API 调用。官方样例报告,在 6 个表达式时第二种方法约为第一种的 1.34 倍,在 50 个表达式时约为 2.4 倍;这些是该样例和旧硬件上的相对结果,不是跨引擎承诺。

但 buffer-template 并非永远取胜:如果表情很少、最终结果要跨多帧复用,stream-out 可以把合成后的 vertex buffer 缓存下来;如果表达式很多、每帧都变且不想反复 stream-out,buffer-template 更适合。还要检查 buffer view 的元素宽度:不同宽度的属性需要 padding 或 shader 重组,不能把任意结构体直接当作模板元素。

6. 把实验信号和角色需求对齐

官方 Dawn 样例把 54 个表情分成多个组,用动画曲线驱动一部分权重,同时保留手动滑杆来叠加其他表达式。下面的实验把这个思路缩成可观察信号:active shapes 代表当前非零表达式,delta channels 代表每个表达式需要改变的位置、法线和切线的宽度,压缩代表更紧的输入预算,缓存代表是否值得保留最终结果。

一帧里最重要的四个检查点1deltaneutral + vectors2budget16 attributes3accumulatepasses / Load loop4renderdraw / cache先问“数据在哪里”,再问“要调用几次”单步观察:输入槽限制如何被拆开,或如何被 shader buffer 读取绕过播放、暂停、单步或拖动进度,比较两条路线的中间状态

第 1 / 4 步 · 准备 neutral 与 per-vertex delta

多 pass 与单 pass 不是绝对优劣,而是缓存、提交和 shader 读取之间的取舍。

先猜一猜:把 active shapes 从 18 调到 54,stream-out 的 blend passes 和 API calls 会怎样变化?切到 buffer-template 后,哪一项保持为 1,哪一项会随表达式数增加?

调整表情预算:先预测 pass 还是 shader Load 上升

当前可检查信号

blend passes1
API / draw calls1
shapes per stream pass5
delta reads / vertex54
attribute pressure94%

单次 draw 把迭代留在 vertex shader,active list 决定 Load 次数,不受输入槽数量直接限制。

提高 active shapes 会增加表情自由度;是否变慢取决于 pass、Load 循环和是否需要跨帧复用。

三步验收:从 delta 定义到策略选择

分步1 / 3

第一步:把表情写成 neutral 加 delta

先区分不变的 UV 与会改变的位置、法线、切线,再用权重把多个 delta 叠加。之后检查一次输入最多能容纳多少 active shapes,而不是把硬件槽位误写成艺术限制。

表情不是一张新网格,而是 neutral 上的加权 deltaneutral meshposition · normal · tangentexpression deltasD₁…Dₙ · per-vertex deltaweighted sumw₁…wₙ · active setfinal facefinal position + shadingP_final = P_neutral + Σ(weightᵢ × deltaᵢ)位置、法线和切线可以改变;不变的 UV 不必重复存储同一套表达式组件可以组合出眨眼、挑眉、微笑等复合表情
先撞到的是输入槽上限,不是表情数量上限一个 pass:neutral + 4 shapes5 个 slot 组装出最多 4 个 active delta全部 54 shapes 同时绑定54 组表达式会形成远超上限的属性宽度解决思路:分 pass,或把 delta 放进 GPU buffer 后在 shader 内读取

本章小结

  • blend shape 是 neutral 到目标姿势的逐顶点 delta,可按权重组合复合表情。
  • 顶点属性预算限制一次输入的宽度,不限制可制作的表情总数。
  • stream-out 用多 pass 和 ping-pong buffer 拆开属性压力,也能缓存最终网格。
  • buffer template 用 shader resource buffer、SV_VertexID 和 Load 循环支持更宽的 active set。
  • 选型要同时看 pass、draw calls、delta reads、带宽和跨帧缓存需求。

练习

问题 1|计算属性预算。 neutral mesh 占 4 个属性,每个 active blend shape 需要位置、法线和切线 3 个属性,输入上限是 16。一次最多能直接绑定多少个 active shapes?如果把切线移出这一轮,预算会怎样变化?

问题 2|修改 Demo 代码。 给 Blend Shape Lab 增加 maxShapesPerPass 控件,并让 stream-out 模式直接使用它计算 ceil(activeShapes / maxShapesPerPass)。切到 buffer-template 模式时,这个控件应该改变 pass 数吗?为什么?

问题 3|场景选型。 一个角色有 50 个表情:加载时生成一次定制脸,之后数百帧不再改变;另一个角色每帧都由语音驱动 20 个权重。分别优先选择哪条路线?

名词解释

本章出现的专业名词,用大白话再讲一遍。

blend shape
stream-out
ping-pong buffer
buffer template
SV_VertexID
active list

资料与写作方式声明

本章以GPU Gems 3 · Chapter 3. DirectX 10 Blend Shapes: Breaking the Limits权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…