GPU Gems 1 · Chapter 7. Rendering Countless Blades of Waving Grass
从透明草纹理、相交草丛对象和顶点着色器风场出发,在少量多边形与自然摆动之间取得平衡。
学习目标
- 能解释如何用少量多边形覆盖大面积草地,并说明交叉草丛为什么不怕视线变化
- 能实现只移动草叶上部的风动画,并比较按簇、按顶点和按对象三种方法的代价
- 能修改实验中的风强度、局部相位和密度,观察自然感与 draw call / shader 成本的取舍
- 能回答:为什么按 grass object 的中心位置计算动画,常常是本章三种方法的折中解?
直觉引入:草地的“多”来自重复,不来自建模
想象一张印着许多草叶的半透明卡片。把它竖在地上只能骗过一个方向;把几张卡片交叉起来,从不同方向看仍然有厚度,再把这样的草丛密密铺在地面上,就能得到一片开阔的草地。
本章解决的是两个同时存在的问题:草必须看起来足够密,还要随着风摆动。如果直接为每根草建模,面积一大就无法实时;如果所有草同步摇摆,面积再大也像一张布。我们要把几何数量、视线稳定性和局部变化分开处理。
1. 先把很多草叶压进一张纹理
本章的第一个压缩动作是把许多草叶打包成一张 ↡包含多根草叶颜色与透明轮廓的二维图像。颜色通道用几种绿色和黄色区分年龄、状态与朝向;透明通道只保留草叶形状,尽量不要留出巨大的空白方块。
这不是把草地变成一张平面图片。纹理只负责提供“这里有很多细草叶”的局部线索,实际的厚度和视线稳定性由下一层几何结构完成。不同状态的草叶也可以共用同一张图,从而让重复不那么明显。
2. 用相交 quad 组成不怕转身的草丛
把几张贴有草纹理的四边形穿过同一个中心,得到一个 ↡由多张相交四边形组成、从多种视线都像一团草的几何单元。三张互相穿插的 quad 通常已经足够;它们不要排成一条线,否则从垂直方向看仍会变薄。
因为这些面要从正反两面都能看到,需要关闭 ↡决定是否丢弃背向摄像机三角形的光栅化开关。为了让坡地上的草丛不随着地形明暗跳变,顶点法线可以统一沿着四边形的竖直方向,而不是直接复制地面的法线。
铺开许多 grass object 后,渲染顺序要结合深度测试、深度写入与透明混合一起检查。远处对象可以淡入淡出,而不是在某一帧突然生成或消失;这让地平线上的草地更稳定,也把对象管理从“有没有”变成“以什么透明度出现”。
3. alpha blending 让透明草叶保持层次
草叶纹理包含透明区域,因此需要 ↡按源 alpha 在草叶颜色与背景之间插值的透明混合方式。它让草叶边缘能够和地面、其他草丛渐变融合,但也带来排序和过度绘制成本:后画的透明面会继续改写前面的颜色,顺序不当就会出现边缘发黑或层次错乱。
不要把“关闭深度写入”理解成“关闭深度测试”。深度测试仍然可以帮助遮挡被前方草丛挡住的片元;是否写深度则取决于你的透明排序策略。远处密集草地常常需要在视觉稳定和准确排序之间取一个可测量的折中。
4. 顶点着色器只移动叶尖
风动画的共同骨架是:根部固定,顶部受风,位移同时由时间、风向、风强度和一个位置相关的相位决定。↡在顶点阶段读取位置与纹理坐标并计算新顶点位置的 GPU 程序 可以用草叶纹理坐标的 v 值区分上下:底部 v 接近 0,顶部 v 接近 1。
一个最小的 WebGL2 风动画片段如下。这里的 topMask 让位移从根部的 0 平滑增长到叶尖的 1;如果整张 quad 都平移,草丛会像硬纸片被搬走,而不是弯曲。
float topMask = smoothstep(0.72, 1.0, aTexCoord.y);
float phase = dot(aObjectCenter.xz, windDirection) + time;
float wave = sin(phase) * windStrength;
vec3 offset = vec3(windDirection.x, 0.0, windDirection.y);
position.xyz += offset * wave * topMask;这个片段只负责“哪一部分动”和“动多少”,并没有决定局部变化的粒度。粒度由下一节的三种策略决定;这正是同一套 vertex shader 框架可以复用、而动画部分可以替换的地方。
5. 三种风动画策略:从 CPU 控制到局部折中
5.1 按簇动画:画面稳定,提交次数高
↡让一组相邻草丛共享 CPU 计算出的平移向量的风动画方法 在 CPU 上根据簇中心计算平移向量,再把它作为常量传给 shader。它可以容纳很复杂的风场,草叶边长不会因为逐顶点移动而改变;代价是每个簇都要更新常量并发起 draw call,簇太大时还会暴露整齐的同步摆动。
5.2 按顶点动画:提交少,可能产生拉伸
↡让每个顶点依据自己的位置、时间和风向独立计算位移的风动画方法 把计算移进 vertex shader,因此一大片草地甚至可以一次提交。它能形成连续的风波,也不容易看出簇边界;但是相邻顶点位移不同会改变 quad 上边缘的长度和厚度,近景可能出现纹理拉伸。用位置相关的伪随机相位可以减少“整片同频”,但不能消除形变成本。
5.3 按对象中心动画:少量提交与局部变化的折中
↡让同一草丛的顶点共享中心位置、而相邻草丛使用不同相位的风动画方法 把每个 grass object 的中心位置放进顶点属性。shader 以中心位置而不是单个顶点计算位移,所以同一草丛内部保持几何稳定;相邻草丛又因为中心不同而拥有不同风相位。
它需要额外的顶点数据,并且为了保持实时性能,风函数不能无限复杂,但通常能同时满足少量 draw call、局部混沌和不明显拉伸三个目标。这就是本章推荐的默认起点。
三种方法并不是互斥的渲染模式:近处可以用按对象动画保持轮廓,远处可以降低密度或减少簇数量;若 CPU 有足够预算,也可以只对关键区域启用更复杂的簇风场。真正的验收指标应当同时包括 draw call、vertex shader 时间、纹理边缘稳定性和镜头转动时的重复感。
6. 用可重放的变化打破同步感
局部变化不等于每个参数都随机。对每个草丛固定一个种子或由中心位置推导相位,能保证同一镜头在调试时可复现。相位可以写成:
这个式子在说:第 i 个草丛的风相位由中心位置 c_i、风向 d、当前时间 t 和一个稳定的小变化 r_i 共同决定。相邻草丛因此不会完全同相,但每次运行仍能重现。
还要控制变化尺度。太大的相位差会让草地像许多独立机关,太小则会恢复成同步波。先在实验中把风强度、方向、时间和密度分开调,再根据远景、近景和镜头转身三个视角设定范围。
猜一猜:把“同步簇误区”打开,草地为什么会立刻变得像布?再把策略切换到 object,风强度保持不变,哪些相邻草丛开始拥有不同的相位?
Waving Grass Lab
观察:同样的风,局部相位会不会改变“自然感”?
提示:按对象策略把对象中心写进顶点属性,换来少量 draw call 与局部变化。
实验把错误模式保留下来,是为了让“局部变化”成为可观察的现象,而不是一句抽象建议。按簇模式的同步范围较大,按顶点模式的每根草变化最大,按对象模式则以草丛为单位共享位移。
7. 性能预算:先问提交、顶点还是片元
草地优化不能只看三角形数量。透明纹理会带来过度绘制,密集 grass object 会带来排序和片元混合,按顶点动画会增加 shader 算术,而按簇动画会增加 draw call。四个因素可能在不同镜头下轮流成为瓶颈。
建议记录四组数据:每帧草丛的 draw call 数、vertex shader 时间、透明片元数量,以及不同距离下的淡入淡出切换次数。若远景草叶只占几个像素,继续增加纹理细节不会改善观感;若近景出现边缘拉伸,应优先调整动画策略或 quad 结构,而不是盲目增加草叶数量。
小结:让密度、视线和风各自承担一部分真实感
- 透明 grass texture 用很少的几何提供草叶密度。
- 相交 grass object 让草丛从多个视线保持厚度。
- vertex shader 只移动顶部,根部固定才能像弯曲而非搬运。
- cluster、per-vertex、object-centered 分别交换 CPU、形变和额外数据。
- 固定相位、深度策略和性能测量共同决定最终自然感。
练习
问题 1|改 Demo 代码 将实验策略依次设为 cluster、vertex、object,保持风强度与密度不变。记录每种策略的同步范围、叶片形变和你认为应关注的成本。
问题 2|推导题 若草丛中心为 c=(2,1)、风向为 d=(0.5,0.25)、时间为 t=3、稳定变化为 r=0.2,用 φ=dot(c,d)+t+r 算出相位,并说明把 r 固定下来有什么好处。
问题 3|独立实现题 实现一个三张相交 quad 的草丛,并在移动镜头时比较开启和关闭 back-face culling 的结果。除了截图,你会记录哪些指标来决定是否采用这种结构?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- grass texture
- 一张带透明边缘的草叶图片;它把许多小草的颜色和轮廓打包在一起。
- grass object
- 几张相交的草叶四边形;从不同方向看都像一小团有厚度的草。
- back-face culling
- GPU 丢弃背向摄像机三角形的开关;草片需要两面可见时通常要关闭它。
- alpha blending
- 用透明度把草叶颜色和背景混合的方式;它自然但需要留意顺序和过度绘制。
- vertex shader
- 在 GPU 上逐个处理顶点的小程序;本章用它移动草叶顶部。
- cluster animation
- 让一组草丛共享一个 CPU 风位移的方法;控制力强,但提交次数较多。
- per-vertex animation
- 每个顶点根据自身位置计算风位移的方法;提交少,但可能拉伸草纹理。
- object-centered animation
- 让同一草丛共享中心位置、相邻草丛拥有不同相位的方法;它是本章推荐的折中方案。