GPU Gems 3 · Chapter 4. Next-Generation SpeedTree Rendering

从树枝轮廓、叶片自阴影、双面光照到 alpha-to-coverage,拆解 SpeedTree 如何把户外植被的真实感放回正确的 GPU 阶段。

学习目标

  • 能解释低多边形树枝为什么需要 silhouette fin 与 height tracing 才能补足视角相关的轮廓细节
  • 能修改 SpeedTree Rendering Lab 的距离、轮廓宽度、阴影偏移与 coverage,观察几何、阴影和 MSAA 信号的变化
  • 能回答:远景植被、叶片自阴影和透明边缘分别应该把预算放在哪个渲染阶段

先看一棵“太像模型”的树

想象你用相机拍一棵树:树干内部的纹理已经很清楚,但枝条边缘仍像被尺子切过一样笔直。观众不需要知道多边形数量,只要看到这条过于规整的轮廓,就会立刻察觉它是模型。

本章解决的问题是:怎样在不把整片森林都换成高密度网格的情况下,补上枝干轮廓、叶片阴影、背光颜色与透明边缘?如果把所有细节都交给一种纹理或一个 shader,近景会失真,远景又会浪费 GPU;更好的做法是让每个症状回到它真正所属的阶段。

先修轮廓,再修叶片,最后修采样边缘树枝 / 叶片输入camera-facing geometry轮廓剪裁fins + height trace动态阴影leaf self-shadow叶片光照two-sided + HDRGeForce 8800 时代的关键取舍让不规则细节出现在正确的阶段,而不是把所有工作堆到一张纹理或一个 pass近处启用细节,远处逐渐关闭,保持画面质量与 GPU 预算平衡

1. silhouette clipping:给低模树枝补一圈视角相关的边

低多边形树干可以用 relief mapping 补足表面内部的凹凸,却无法改变几何轮廓:当相机看到一条三角形的直边时,纹理只会在直边内部制造细节。SpeedTree 的做法是把“轮廓”单独拿出来处理,近处树枝才会出现不规则的枝皮边缘。

先在 vertex shader 中计算每个顶点法线与视线的点积。如果一个三角形的两条边跨过正负号,说明可见性在三角形内部发生变化;geometry shader 就在点积为零的位置插入顶点,沿垂直视线方向挤出两片窄 fin。没有符号变化的三角形不输出 fin,避免给普通表面增加无用几何。

轮廓边不是纹理问题:它需要额外的可见几何1 · sign testn·v < 0n·v = 0n·v > 02 · geometry shader零点两侧生成两片 fin3 · height trace沿视线采样高度图

这里的几何只负责“把可能的轮廓位置送到屏幕”。真正的 bark 高度和遮挡仍在 pixel shader 中沿视线追踪高度图;若追踪没有交点,片元会被丢弃或交给边缘抗锯齿。也因此,fin 很窄时像素成本可控,但它不应该对所有距离的树都开启。

2. height tracing 与 silhouette LOD:细节要随距离淡出

fin 的视线通常贴着树干表面,搜索路径比普通 relief mapping 更长。实践中可以把线性步进的最大长度截断,并让 fin 的像素 footprint 保持较小;这是一种“只为能看见的边缘付费”的预算控制,而不是假设 height map 搜索免费。

轮廓细节同样需要距离预算。近处可以保留较宽的 fin;进入过渡区后,把 fin 宽度按距离连续缩小到零,远处直接移除。连续淡出比瞬间切换更不容易 pop,而且官方样例指出典型场景中真正可见的 silhouette 数量通常很少。

silhouette width 调大不等于永远更真实:当树只占几个屏幕像素时,额外的 fin 和追踪步数只会增加成本。先观察它是否改变了可见轮廓,再决定是否值得保留。

3. leaf self-shadowing:平面叶卡需要两次位置修正

SpeedTree 的叶片既有平面 leaf card,也有三维叶片。平面卡片总是朝向相机;为了生成 shadow map,它又会转向光源。如果直接把转向后的卡片投影回相机朝向的卡片,影子会沿平面拉成长条,树叶的自阴影看起来像贴纸上的污痕。

修正分两步。生成 shadow map 时,把转向光源的 caster 沿光源方向移动大约半张卡片的高度,让它覆盖合理的叶片区域。应用 shadow map 时,不改已经光栅化的叶片位置,只把用于阴影查询的位置沿 view direction 偏移;偏移系数可以存进每片叶子一致的 texture。这样每个叶片内部可以得到不同的 shadowed position,而不会把整个 card 拉成一条线。

远景户外场景还需要 cascaded shadow map:近级联每帧更新并绘制全部对象,较远级联可以降低更新频率,只保留对该距离有意义的叶片或枝条。级联不是越多越好,而是让 shadow texel 密度跟视距匹配。

平面叶片要先修投影位置,再查 shadow map直接投影lightstreaks on leaf cardcaster offset按约半张卡片高度移动receiver offset沿 view 方向查错位

4. two-sided leaf lighting:背光要表现“穿透”

真实叶片背光时并不是简单地把漫反射乘黑:光线穿过薄薄的叶肉,会产生偏黄或偏红的 transmitted light。SpeedTree 的简化模型在 pixel shader 中构造暖色版本,再根据光方向与视线的夹角在原色和暖色之间插值;当光从背后照来时,也降低 specular,避免把不该反射的背面做成镜子。

叶片的高光还需要抑制远处闪烁。官方实现观察到真实叶片的高光更像沿叶脉分成两块的宽反射,因此使用较粗的 V 形 normal map,并偏向较低 mip level。这个取舍把“微小纹理细节”换成“整棵树稳定的光照形状”。

背光的叶片不是“变暗”,而是出现透射色正面光diffuse + regular specular背面光混入黄红色 transmitted lightshader 取舍背面降低 specular避免远处闪烁

5. alpha-to-coverage:把透明边缘交给 MSAA 样本

植被常用 alpha cutout:一个样本要么保留,要么丢弃。边缘因此容易像锯齿,风吹或 LOD 切换时还会 scintillate。alpha-to-coverage 把 alpha 转成 MSAA 的覆盖样本,resolve 时得到较平滑的透明边缘;对于叶片和 frond 这种 1-bit cutout,它不要求按深度排序,也不会把大块半透明物体误当成普通 alpha blend。

LOD cross-fade 是一个常见陷阱:两个 LOD 同时绘制、各自使用 50% coverage 时,它们可能共享同一组 coverage mask,深度测试后留下的结果会比预期更透明。修法不是退回普通 blending,而是错开两条 LOD 的 alpha fade 曲线,让两个模型在过渡区承担互补的 coverage。

silhouette clipping 的边缘也可以复用这条思路。geometry shader 给 fin 两端写入不同的 view-vector W 值,pixel shader 追踪高度图时记录最近 miss,再把距离边缘的片元 alpha 淡出到 coverage。这样抗锯齿的是“高度图决定的可见边”,而不只是原始三角形边。

alpha 不只是一种透明度:它也可以选择 MSAA 样本alpha texture1-bit leaf cutoutMSAA coverage边缘更平滑,不必排序叶片LOD fade错开两条曲线
float3 shadowedPosition = input.position;
float offset = offsetTexture.Sample(linearSampler, input.texcoord).r;
shadowedPosition += input.viewVector * offset;
 
float4 lightSpace = mul(shadowMatrix, float4(shadowedPosition, 1));
float shadow = shadowMap.SampleCmpLevelZero(
  comparisonSampler,
  lightSpace.xy / lightSpace.w,
  lightSpace.z / lightSpace.w
);
return shadow;

这段伪代码的关键不是某个 API 名称,而是“用偏移后的位置查阴影”。几何的实际屏幕位置没有被移动;如果把两者混为一谈,叶片会出现漂移或深度错位。

6. 用症状而不是术语做选型

画面症状决定修复阶段症状主要技术GPU 阶段先看什么树枝边缘太直silhouette clippingGS + pixel距离 / fin 宽度叶卡出现条纹leaf shadow offsetPS + shadow mapcaster / receiver背光叶片发灰two-sided lightingpixel shader透射色 / 高光透明边缘闪烁alpha-to-coverageMSAA resolvecoverage / LOD fade不要用更高分辨率的纹理去掩盖本该由几何或采样解决的问题

本章的几种技术不是一张“质量开关”上的互斥选项。树枝轮廓太直,优先看 silhouette fin 是否在近处生成;叶片阴影有条纹,先看 caster/receiver 的空间偏移;背光发灰,检查 two-sided lighting 的透射色;透明边缘闪烁,检查 MSAA coverage 和 LOD 曲线。先定位症状,再决定是几何、pixel shader、shadow map 还是 resolve 阶段需要预算。

一棵树的质量预算沿管线逐步展开1silhouettefins + trace2shadowcaster + receiver3lightingfront / back4coverageMSAA samples每一步都对应一个可观察的画面症状播放、暂停、单步或拖动进度,观察细节从几何走到采样覆盖远处关闭 silhouette,近处保留 shadow 与 lighting,是预算而非开关魔法

第 1 / 4 步 · 先给树枝补上视角相关的轮廓

轮廓、阴影、光照和边缘抗锯齿各自有正确的计算位置。

先预测:把相机从 42 推到 90 时,轮廓宽度、shadow 级联和 alpha coverage 哪一个应该自动减少?再打开下面的实验,分别把 shadow offset 调到很低、把 coverage 调高,观察“边缘更平滑”是否真的能消除“投影位置错误”。

SpeedTree 渲染实验

先猜一猜:把树推远会先关闭哪种细节?把 coverage 调高能不能修复错误的 caster 偏移?

distance 42 · near / fadefin pixels 34MSAA samples 5
silhouette modenear / fade
shadow streak risk52%
coverage samples5
leaf lightingbacklight adds warm transmission

观察距离、几何轮廓、阴影投影和 sample coverage 是四个不同信号;一个变好不代表其他问题已经消失。

三步验收:从轮廓症状到完整植被管线

分步1 / 3

第一步:用视角识别真正的 silhouette edge

先对比树枝的表面细节和外轮廓,再用顶点法线与视线的点积找符号变化。只有跨过零点的三角形才需要 geometry shader 挤出 fin,随后由 height tracing 决定片元是否保留。

先修轮廓,再修叶片,最后修采样边缘树枝 / 叶片输入camera-facing geometry轮廓剪裁fins + height trace动态阴影leaf self-shadow叶片光照two-sided + HDRGeForce 8800 时代的关键取舍让不规则细节出现在正确的阶段,而不是把所有工作堆到一张纹理或一个 pass近处启用细节,远处逐渐关闭,保持画面质量与 GPU 预算平衡
轮廓边不是纹理问题:它需要额外的可见几何1 · sign testn·v < 0n·v = 0n·v > 02 · geometry shader零点两侧生成两片 fin3 · height trace沿视线采样高度图

本章小结

  • silhouette clipping 用 fin 与高度图补足树枝的视角轮廓。
  • 叶片自阴影要修正 caster、receiver,并按距离管理级联。
  • 背光叶片需要透射色,远处高光要降低闪烁。
  • alpha-to-coverage 用 MSAA coverage 平滑 cutout 边缘。
  • 先按画面症状定位几何、阴影、光照或采样阶段。

练习

问题 1|判断 silhouette edge。 一个三角形三个顶点的 n·v 符号分别为负、正、正。它是否应该生成 silhouette fin?如果三个值都为正,geometry shader 应该做什么?

问题 2|修改 Demo 代码。 给 SpeedTree Rendering Lab 增加 maxTraceSteps 控件,并让 silhouette 的 height tracing 信号随它变化。为什么增加步数不能修复 leaf shadow 的条纹?

问题 3|场景选型。 一棵近处树枝需要打破直线轮廓;一片远处森林需要稳定的自阴影;一批叶片使用 1-bit cutout 并随风运动。分别优先选择什么策略?

名词解释

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

silhouette clipping
height tracing
cascaded shadow map
two-sided leaf lighting
alpha-to-coverage

资料与写作方式声明

本章以GPU Gems 3 · Chapter 4. Next-Generation SpeedTree Rendering权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

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

讨论

评论区加载中…