GPU Gems 3 · Chapter 6. GPU-Generated Procedural Wind Animations for Trees

用二维风场、浅层树枝层级、噪声与周期规则,在 vertex shader 中合成可扩展的树木风动,并比较 simulation LOD、instancing 与 DirectX 10 stream-out。

学习目标

  • 能解释 wind field、树枝层级和周期规则如何在 vertex shader 中合成无需回读上一帧的树木运动
  • 能修改 Procedural Wind Lab 的风力、gust 频率、turbulence、stiffness 与 simulation LOD,观察幅度、phase spread 和 shader work 的变化
  • 能回答:大批量植被、远处树干和近处树枝分别应该采用什么 simulation、instancing 与 stream-out 组合

先把“像风”的线索交给 GPU

想象一片森林:远处的树干只需要一起缓慢偏摆,近处的枝条才需要有松紧、回弹和不完全同步的细节。真正的风包含太多力、质量和约束,逐枝求解会让实时程序先耗尽预算,画面也未必更像自然界。

本章解决的问题是:如何用少量可控规则,合成大批树木看起来可信的风动,并把运动计算放进 GPU 以配合 instancing?如果所有树都使用同一个相位,森林会像一排同步的雨刷;如果每根枝条都运行完整物理模拟,CPU 和 GPU 都会被不必要的状态依赖拖住。

把复杂风力换成可控的视觉规则wind fielddirection + velocitytree hierarchytrunk → branchesmotion synthesisnoise + periodic rulesvertex shaderangles → transforms每个 vertex 都能独立读取场、层级和参数不需要等待上一帧,也不必让 CPU 逐枝求解物理方程适合 instancing:同一套规则可以作用于大批树实例

1. phenomenological model:追求可见现象,不模拟每个力

树的运动受枝条长度、刚度、叶冠形状和湍流影响。若直接把每个分支当成物理物体,必须处理力的传播、约束和上一时刻状态;对需要大规模植被的实时场景来说,这通常得不偿失。更适合的做法是挑出观众能看见的现象:主干缓慢偏摆、背风枝条大幅晃动、侧风枝条弯曲并带一点扭转。

这些现象可以由多个简单周期函数和 noise 叠加得到。低频、低幅度的成分负责整体摆动,高频成分负责小枝条的混沌细节;规则不是物理方程的近似答案,而是直接面向屏幕结果的运动合成器。

2. wind field 与浅层树枝层级

把风简化成地形上的二维 vector field:给定位置和时间,函数 v = G(x, t) 返回方向与速度。全局风提供稳定主方向,稀疏的 wind primitive 可以表达局部旋涡、爆炸或飞行器经过的扰动。vertex shader 可以从二维 texture 采样它,也可以从额外的 instance buffer 读取每棵树的风向和风速。

树本身只保留一层浅层 hierarchy:trunk 是 root,branch segment 连接到 parent branch,叶片和更小枝条的直接反作用隐含在 noise 中。官方章节指出两到三层节点通常已经足以产生可信的树木运动;这让每个顶点的 branch list 保持短,适合在 shader 中遍历。

wind field 是一张随时间变化的二维力场terrain vector fieldlocalprimitivevertex shader input2D wind textureinstance vectorv = G(x, t)direction + velocity

远处树的主干运动是最重要的视觉线索,近处才值得加入枝条的 suppression、bending 和 twisting。把 simulation LOD 与几何 LOD 分开理解:几何 LOD 决定画多少三角形,simulation LOD 决定算多少层运动。两者可以协同切换,但不应该用“减少三角形”掩盖“仍在计算所有枝条”的问题。

先让 trunk 有节奏,再让 branches 加细节shallow hierarchytrunk → branch → leaf areasimulation LODfartrunk motion onlyneartrunk + branch rulessmall phase variation

3. 用风向关系组合 trunk 与 branch motion

主干可以把平行风向、垂直风向和绕轴旋转分成不同频段。平行方向的幅度最大;垂直方向和旋转幅度较小,但能避免树像一根只会前后摆的棍子。更低频的函数表达较重、较硬的树,高频的小波叠加则模拟小枝条的 chaotic detail。

枝条可以按它相对风向的关系分成三类:迎风时 drag 和 lift 将它压向 trunk,摆幅较小;背风时 turbulence 和 lift 占主导,枝条大幅摆动或 flap;与风向垂直时,较大的 attack angle 让它绕 parent branch 弯曲,并带一点绕自身轴的扭转。最终 motion 是这几类周期函数按权重的加权和。

风向关系决定规则的权重,而不是决定一条物理轨迹迎风suppression幅度小背风turbulence摆动大侧风bend + twist绕父枝轴motion = weighted sum of periodic cases + phase variation

如果所有枝条从同一个时间点开始同一个 cosine,规则本身再好也会出现同步摆动。phase shift 可以在预处理阶段为每个 branch 生成,运行时只需把它加入时间项;turbulence 或局部风 primitive 还可以进一步改变不同树实例之间的相关性。

4. vertex shader 中的 branch transform 合成

每个顶点保存影响它的 branch indices 和 weights,从直接所属 branch 一路列到 trunk root。shader 按这条 list 计算每个 branch 的旋转角度,再沿 hierarchy 顺序合成旋转。这里更适合用 quaternion 组合角度,最后再转成 rotation matrix;直接对变换矩阵做线性插值会产生不正确的旋转。

每个顶点沿自己的 branch list 走一遍层级vertex attributesindices + weightsbranch traversallocal branchparent branchtrunk roottransform outputqquaternionmatrixposition + normal
float angle = branchAmplitude * cos(time + phaseShift);
float facing = dot(normalize(branchAxis), windDirection);
float suppression = saturate(1.0 - facing);
angle = lerp(angle, -0.5 * angle, suppression * branchSuppressPower);
 
float4 localRotation = quatAxisAngle(branchTangent, angle);
float4 twistRotation = quatAroundY(angle * twistPower);
float4 branchRotation = quatConcat(localRotation, twistRotation);
 
for (uint i = 0; i < branchCount; ++i) {
  finalRotation = quatConcat(finalRotation, branchRotationFor(i));
}
 
float3 animatedPosition = rotate(finalRotation, position - branchOrigin);
output.position = animatedPosition + branchOrigin;

这段伪代码保留了三个关键决策:风向影响 suppression,周期项由 phase shift 打散,层级旋转用 composition 累积。它不需要读取上一帧,因为本帧的 wind field、hierarchy 和树参数已经足够计算结果。

5. DirectX 9 与 DirectX 10:把共享的 branch work 留下来

在 DirectX 9 路线上,若每个几何顶点都重新遍历同一 branch hierarchy,属于同一分支的顶点会重复计算相同 transform。DirectX 10 的 stream-out 可以把 simulation 单独运行:先把每个 branch 当成一个 vertex,算出 branch transform 并写进临时 buffer;之后的 skinning shader 再把临时结果作为 instance data 读回几何。

DX10 把“每个顶点重复算”改成“每个 branch 先算一次”DirectX 9branchper vertexbranchper vertexbranchper vertexbranchper vertexshared branch work repeatsDirectX 10branch simulationstream-outskinning shader + instancestemporary transform shared by geometry

这不是把完整物理状态写回 CPU,而是把本帧可复用的 GPU 中间结果留给下一阶段。再配合 hardware instancing,同一套 coarse tree geometry 可以批量绘制大量实例;若需要和 GPU fluid simulation 联动,wind field 也可以一直留在 GPU,避免 readback。

一棵树的风动从场到顶点1fielddirection + power2rulesdrag / lift / noise3composequaternion chain4renderskin + instance现象学规则不是物理求解的半成品它直接把“看起来像风”的线索映射成可并行的 vertex work播放、暂停、单步或拖动进度,比较 simulation 与 rendering 的边界

第 1 / 4 步 · 采样 wind field 的方向与速度

GPU 负责合成运动,CPU 只提供场、层级和树实例所需的参数。

先预测:把 simulation LOD 从 full detail 切换为 trunk only,应该减少哪些 shader work?把 DX9 换成 DX10 stream-out 后,哪项共享计算会被移出每个 geometry vertex?打开 Lab,分别调高 stiffness 和 turbulence 来验证。

Procedural Wind 实验

先猜一猜:提高 turbulence 会让每棵树更大幅摆动,还是主要让树群不再同步?把 stiffness 调高后,哪一项应该下降?

wind 62 · turbulence 48 · trunk + branchwind directionsway amplitude 43phase spread 55%
sway amplitude43
phase spread55%
shader work / instance323
crowd signal低相关 phase,群体更不同步

把幅度、频率、相位分散和 simulation LOD 分开观察;视觉上更乱不等于每根枝条都需要更深的物理模拟。

6. 用视觉目标选择模拟预算

procedural wind 的优势是大规模、可并行和可控,不是物理精确。若应用需要“风吹得像真的”,应该先问画面能看见什么:远景只要 trunk motion;近景需要 branch cases、phase variation 和 inertia;局部爆炸或旋翼则需要 wind primitive 影响场。

同一棵树还应把 simulation LOD 与 geometry LOD 一起设计。远处降低模拟复杂度并减少几何,近处恢复 branch motion 并保留足够顶点;DX10 stream-out 适合把 branch transform 在 simulation 与 skinning 间复用,但不能替代合理的实例批次和相位分散。

三步验收:从风场到可扩展的树群

分步1 / 3

第一步:把风场和树层级压缩成输入

先确定 wind field 的方向、速度和局部 primitive,再保存 trunk-rooted hierarchy、branch 参数与每个顶点的影响列表。不要从一开始就为每根枝条建立完整物理状态。

把复杂风力换成可控的视觉规则wind fielddirection + velocitytree hierarchytrunk → branchesmotion synthesisnoise + periodic rulesvertex shaderangles → transforms每个 vertex 都能独立读取场、层级和参数不需要等待上一帧,也不必让 CPU 逐枝求解物理方程适合 instancing:同一套规则可以作用于大批树实例
wind field 是一张随时间变化的二维力场terrain vector fieldlocalprimitivevertex shader input2D wind textureinstance vectorv = G(x, t)direction + velocity

本章小结

  • phenomenological model 用规则和噪声换取可扩展的 believable motion。
  • wind field 提供方向速度,浅层 hierarchy 提供可并行的影响范围。
  • trunk、branch、phase shift 与 simulation LOD 共同控制视觉层次。
  • quaternion composition 在 vertex shader 中合成 branch transforms。
  • DX10 stream-out、skinning 和 instancing 复用 GPU 中间结果。

练习

问题 1|判断规则权重。 一个 branch 位于风向正面、背面和侧面时,分别应该加强哪类视觉规则?为什么不能让三类 branch 使用同一个 sway amplitude?

问题 2|修改 Demo 代码。 给 Procedural Wind Lab 增加一个 phaseCorrelation 控件:它越高,branch phase shift 越接近;它越低,phase spread 越大。你应该如何改变规则,才能让树群从“同步摆动”变成“各自有节奏”?

问题 3|场景选型。 一个远景森林有 1,000 个实例,镜头只看见树干整体摆动;另一个近景角色树需要看见枝条回弹且使用 DX10。分别如何选择 simulation LOD 和数据路径?

名词解释

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

phenomenological model
wind field
simulation LOD
phase shift
quaternion composition

讨论

评论区加载中…