资讯动态

UE4 SpeedTree Shader源码深度解析:风场、LOD与植被渲染优化

发布时间:2026/10/5 6:02:16 来源:尧图企业网站定制
1. 为什么要把SpeedTree Shader源码翻个底朝天做UE4项目早晚会遇到一棵树遇到树就绕不开SpeedTree。美术从SpeedTree Modeler导出的. st、. srt资源在引擎里生成材质时引擎会自动组合出一套Shader。这套Shader不是美术手动连线搭出来的而是通过SpeedTree材质节点自动生成的很多人用完了就算从来没看过它背后的HLSL。我建议你找个项目不忙的下午把它生成的源码打开看一眼因为这棵树的渲染质量、性能开销、可以二次开发的所有入口几乎都藏在顶点着色器和片元着色器的细节里。SpeedTree Shader在UE4渲染链路里的位置很特殊。它同时承担了植被的智能风场模拟、近距离几何细节、远距离卡片替代、以及透光、阴影、半透明等一堆需求。引擎官方这套Shader虽然不是完全不可替代但它写成了一种比较典型的主流光栅化植被方案。你看懂了它再去调树叶的色相、控制风吹的强度、优化阴影锯齿甚至把SpeedTree资源迁移到别的渲染器思路都会清晰很多。这篇文章适合谁一类是刚接触UE4植被性能优化的TA一类是准备二次封装SpeedTree节点或做自定义植被渲染的程序员还有一类是纯好奇的图形学爱好者。我会把源码的阅读路径、关键计算逻辑、常见坑和调试方法一起说清楚。阅读之前你最好已经知道顶点着色器和片元着色器的大致分工但即使你只做过材质蓝图我这篇也会尽量把每个术语都拆开解释。在看源码之前要先认清一件事UE4的SpeedTree Shader并不是一个简单的单一文件它是根据你的材质节点参数、渲染特性、Lighting Mode、项目平台等组合出来的多个Shader变体。你直接在项目中搜SpeedTree生成的.hlsl往往是一大段自动生成代码。如果一头扎进去很容易绕晕。我的建议是先看它的逻辑骨架再对照材质节点理解参数如何传到代码里。下面几个小节就是我实际阅读时采用的切分方式按顺序走完一遍基本能掌握全貌。2. 顶点着色器源码核心拆解2.1 顶点输入与模型空间变换SpeedTree Shader的顶点输入部分比普通静态网格多了一堆自定义数据。普通网格顶点可能只有Position、Normal、Tangent、UV这些但SpeedTree的资源导进UE4之后Build Commandlet会把树木的特殊属性烘焙到额外的顶点颜色通道和UV通道里。这部分在源码里通常表现为FVertexFactoryInput结构看起来像这样不同版本字段名略有差异但含义一致struct FVertexFactoryInput { float4 Position : ATTRIBUTE0; float3 TangentX : ATTRIBUTE1; float3 TangentZ : ATTRIBUTE2; float2 TexCoord : ATTRIBUTE3; float2 LightMapUV : ATTRIBUTE4; float4 VertexColor : ATTRIBUTE5; float4 ExtraData : ATTRIBUTE6; uint InstanceId : ATTRIBUTE7; };其中VertexColor不是拿来当颜色用的它表示了树各部位的AO、树叶的密度权重、树枝的朝向约束、风场影响区域这些其实是SpeedTree的核心数据编码方式。源码里还没开始变换位置会先有一大段GetSpeedTreeData之类的函数把这些通道里的数据解压出来。你一旦在材质编辑器里勾选了Wind或Branch Seam开关映射关系又会多出若干条分支。模型空间变换本身并不复杂无非是local vertex position乘上ViewProjection但SpeedTree有一个特殊点是它的树木是曲面而非平面顶点位置在建模阶段就会离树干轴线偏移。如果只拿普通网格的MVP矩阵去处理会遇到树叶在远处摆动时“撕扯”变形的现象。所以源码里在变换之前会先做一次基于风场和顶点朝向的位置重算再把重算后的局部坐标放进世界矩阵。这个顺序其实已经告诉了你一个调优方向当树被风吹得异常抖动时应该先去检查顶点位置重算的逻辑而不是直接去改物理学速度参数。2.2 风场扭曲与LOD抖动SpeedTree的风效果不是由CPU物理计算驱动的而是完全靠顶点着色器里的数学函数模拟。源码中通常有一块名为Wind或Sway的代码分支它接收一系列参数风的全局方向、时间偏移、摆动频率、树叶团簇ID、以及顶点颜色里存储的AO权重。核心表达式类似这样float windTime gWindTimeVector.x View.GameTime * WindSpeed; float phase dot(ExtraData.xyz, WindDirectionAndSpeed.xyz) VertexColor.g * TreePhase; float3 offset WindDirectionAndSpeed.xyz * sin(windTime * Frequency phase) * (VertexColor.r * SwayAmount);注意这里dot不是随便写的它的意思是把每个顶点的“风向相位”与风的方向做点积不同的顶点朝向会产生不同的相位差于是整棵树不是整体平移而是产生层层交错、类似波浪的扭转效果。其中的VertexColor.g存的是该部位在树上的高度/层级而VertexColor.r是这个地方受到风的强度的权重。叶子在树冠外侧权重大内层权重小这样风出来才有层次感。LOD抖动是另一个重要逻辑。SpeedTree本身自带连续LOD所以在两个LOD层级切换时顶点位置的差异会导致视觉“弹跳”。UE4的Shader里针对这个做了专门的“Angle Sided LOD”处理代码会读一个LODTransition参数然后把位置的插值改成float lodFactor LODTransition; position lerp(NormalizedPosition, OriginalPosition, lodFactor);这里NormalizedPosition不是真正的单元归一化而是美术在SpeedTree里导出的“标准LOD位置”。这个插值过程会尽量让两个LOD层级下的顶点共面减少突跳。你看源码时如果看到LOD相关宏千万别跳过很多树在地面附近突然“闪”一下就是这个处理没做对或参数没传进去。2.3 自定义顶点数据解析顶点数据里有一个经常被忽略的字段叫ExtraData或者叫“物料系数”。UE4的SpeedTree材质自动生成器中有三个选项会直接映射到该字段Branch Seam枝干接缝、Subsurface次表面散射强度、Branch/Leaf Blend枝与叶混合权重。这三个值在片元阶段也会用到但在顶点阶段主要影响顶点位置的偏移方向和遮挡强度。举个例子树叶的BranchSeam权重越大顶点在风中摆动时越会往树枝末端方向倾斜反之树枝根部几乎不动。而朝向来决定摆动幅度的另一个维度。搜索源码时你先找BranchSeam或SPEEDTREE_SEAM相关宏然后顺着它的分支找下去会发现它把VertexColor的B通道当作一个“朝向屏蔽值”。如果美术把树叶模型建模成许多个独立小片这个值通常能手动调成0.2~0.8之间风吹时树叶和树枝之间的过渡就会自然很多。这里需要特别提醒的是引擎默认SpeedTree Shader只认它固定的顶点格式。如果你用第三方DCC工具手动修改了模型网格却没有重新运行SpeedTree Import后处理那么这些自定义通道数据可能会全部丢失风场效果和接缝混合都会直接失效。你在源码里看到一堆ifdef判断其实就是为了容忍“数据缺失”的情况。我自己的习惯是在查看源码前先在关卡里摆一棵树用stat gpu看是否正常消耗如果发现顶点数异常就先怀疑这些自定义数据通道有问题再去读代码。3. 片元着色器源码与光照模型3.1 叶子双面光照与透光片元着色器是SpeedTree视觉成败的关键。普通材质默认是单面渲染但树叶基本都是扁平的不管是看正面还是透过缝隙看背面颜色都要有区别。SpeedTree Shader里的做法是同时输出正面和背面的Albedo、Normal和ShadingModel然后在渲染管道里用TwoSidedSign去判断当前像素是从哪一面看到的。具体的代码逻辑往往是这样half3 albedoFront Texture2DSample(Tex, Sampler, UV).rgb; half3 albedoBack Texture2DSample(Tex, Sampler, UV BackUVOffset).rgb; half3 albedoFinal lerp(albedoFront, albedoBack, TwoSidedSign);TwoSidedSign不是普通的0/1变量它在实际渲染时是1或-1所以常见写法是lerp(albedoBack, albedoFront, TwoSidedSign * 0.5 0.5)。如果你直接用UE材质编辑器把sign节点接给混合节点极容易搞反。这个细节很多人不看源码就会怎么Tune都得不到想要的半透光效果。透光效果还和次表面散射Subsurface息息相关。SpeedTree叶子薄背后打光时整片叶子会点亮。UE4引擎内部的“本地次表面散射”和“Subsurface Profile”在这里会有一个交互片元着色器输出SubsurfaceColor和Opacity后会交给Lighting模型处理。源码中会计算LeafSubsurface通常来自纹理的绿色通道或顶点色的B通道。当这个值很高时背光面的亮度会明显提高正面又不至于整体泛白。3.2 遮罩与混合贴图通道SpeedTree的贴图通道安排非常紧凑。常见布局是RGB存AlbedoA通道存Opacity第二张纹理的R存Normal的XG存Normal的YB存SubsurfaceA存BranchSeam。为了节省采样器和带宽Shader中大量使用SampleBias或者SampleLevel对第二张纹理做低mip采样这样远处树木的绘制开销能明显降低。当你打开源码看到float4 PackedTexture ...;这种写法时不要害怕它其实是把树皮的A通道当成了“粗糙度AO切换开关”把叶子的B通道当成了“风场相位”。我在实际项目里曾经把树皮Albedo打错成了RGBA四通道结果叶子的次表面强度忽高忽低。后来跟踪源码后才发现引擎默认把Opacity通道混进OpacityMask如果材质混合模式不是Masked这一整套通道表都会乱套。如果你想快速定位通道建议先在材质编辑器里勾选“Debug”模式把每个自定义输出通道输出到颜色看到直观变化。然后回头再看源码中对应的DEFERRED和FORWARD分支你会发现Forward渲染下这些通道会被分到GBuffer的具体哪几位里。3.3 半透明阴影与阴影锥的边缘混叠SpeedTree的阴影不是传统的不透明物体阴影。树叶会有很多细小缝隙如果用纯不透明阴影地面会变成一片黑斑毫无生机。UE4在SpeedTree Shader中让片元着色器额外输出一个“着色率”或者“Opacity”值到阴影深度图使得树叶阴影边缘呈现出一种半透明的“软边”效果。这个过程在不同引擎版本里叫法不一样有的叫PerPixelShadowDepthBias有的直接用ShadowOpacity。源码中类似这样float shadowOpacity smoothstep(0.3, 0.6, OpacityMask); OUT.ShadowDepthBias max(ShadowBias, 0.0001); Out.SubstrateOutput-ShadowOpacity shadowOpacity;这里的OpacityMask不是完全二值的。它在片元阶段会取纹理A通道中高于某个阈值的部分并且做了一个dither抖动避免阴影边缘出现硬边。你可以把阈值调低让阴影范围变大但这会导致树叶看起来比实际厚。我做植被项目时习惯把阴影阈值保持在0.4上下再配合Contact Shadow使用阴影的过渡最好看。4. 材质节点到Shader变体的编译管线4.1 SpeedTree材质节点的生成逻辑当你在材质编辑器里新建一个材质并把Use SpeedTree选项打开时引擎并不会马上生成Shader。它会先遍历资源里的“SpeedTree数据对象”提取叶片和树干的插槽信息然后把输出引脚自动连接到Diffuse、Mask、Normal等通用引脚上最后交给Shader编译管理器。源码里体现为庞大的FNodeCodeGenContext和FMaterialShaderMap构建过程。一个特别容易引发混乱的地方是材质编辑器里你拖几个Texture Sample放到“Base Color”和“Opacity”上但真正被Shader读取的纹理绑定顺序是在FSpeedTreeVertexFactory中按约定好的属性来赋值的。也就是说表面上看你只连了三张贴图但实际编译后会变成很多层CBuffer。如果你发现改动贴图槽位但材质没有变化不要一味怀疑节点连线可以看编译后的MaterialShaderInfo里有没有对应的Parameter列表。4.2 变体数量失控怎么办SpeedTree Shader包含大量#if动态分支这会快速膨胀Shader变体数量。每添加一个材质特性比如“双面光照”“风”“LOD抖动”“次表面”编译时间就会大幅增加。移动平台上如果不做手动剔除项目发布时很容易碰到Shader编译超时或堆内存爆掉的问题。建议在源码分析后回头检查编译产物时顺手在Project Settings - Shader Permutations里关掉用不到的变体。常见保留组合是主流平台Forward shading Lit Masked Wind TwoSided移动端Mobile shading Unlit Masked NoSubsurfaceMetaPass/SelfShadow只保留Shadow Pass DepthOnly我在一个开放世界项目里只优化了SpeedTree一个类型的Shader变体整体编译时间就从三十分钟降到了十八分钟打包体积也少了将近500MB。源码分析里看的是逻辑但最终要落到项目配置上这一步比调效果还重要。4.3 源码分析工具链怎么搭分析HLSL源码最忌讳的就是用记事本硬看。我用的组合是使用RenderDoc抓取一帧Foliage的DrawCall然后在VS Input / Interpolator窗口里查看顶点数据的绑定和输出。使用r.ShaderDevelopmentMode1配合r.DumpShaderStats1在日志里输出每个Shader变体的统计。用VS Code或者Notepad打开引擎源码里的.ush.usf文件然后开启ShaderCompileWorker实时编译来验证修改。只能在引擎里打开的实际Shader文件路径一般位于Engine/Shaders/Private/和Engine/Shaders/Public/目录其中SpeedTreeCommon.ush是最核心的公共实现文件。你在Make工具或搜索功能中找不到的话可以直接在项目根目录里全文搜索ComputeSpeedTreeWind这个函数名基本就能定位到主逻辑。5. 我实测过的SpeedTree Shader调试坑5.1 风输入没有生效这是我在项目里遇到最多的一个问题模型、材质、风都开了但树纹丝不动。排查时先看Shader里的Wind代码是否真正参与编译。常用方法是渲染帧并截断VS输出查看顶点位置是否为常数。如果顶点位置没变那就去检查材质节点里“Global Wind”是否勾选以及剧情蓝图是否调用了SetWind接口。还有一个隐蔽点SpeedTree的“Grass Wind”和“Tree Wind”是两组参数很多人在全局风力系统里只设置了前者结果树木没反应而草却动得很欢。另外一个常见原因是静态网格的“Mobility”被设置成了Static。UE4的SpeedTree风效果默认依赖Primitive的GlobalDistanceField/PerInstanceRandom数据只有Movable或Stationary类型才能正常驱动。把移动性改成Movable后常常立竿见影。5.2 叶片颜色不对和背面发紫如果你用的是Lit光照模式但叶片背面呈现出大块蓝紫色那多半是Normal方向被读反了。SpeedTree叶子通常会翻转法线以做到双面光照都相对正常所以片元着色器里会有Normal normalize(lerp(NormalFront, -NormalFront, sign))这样的代码。如果你手动调整过Mesh的Scale为负数Normal通道的镜像符号会被打乱导致光照面反转。第一次遇到时可以先看材质面板里的TwoSided是否是开启状态再在材质里写一个VertexNormalWS的可视化节点输出看颜色。如果背面颜色依旧异常就要去Shader里搜索FlipNormalSign或Sign相关的宏把该标记从负值改成正值。5.3 性能剖析与优化实测SpeedTree Shader的开销大头通常在片元着色器的纹理采样上尤其是树叶的正面和背面要采样两次Albedo这使带宽压力翻倍。实测中打开stat gpu会发现DrawCall虽然不多但PixelShader的Duration到了一半以上。我降低开销的方法是把调试用的BackUVOffset改成采样一个LoD更高的Mipmap让背面纹理在远处强制降采样同时在材质面板里把DiffuseTexture的MipGen设置成“Sharpen”有效减少闪烁并提高缓存命中率。另外还要注意树叶的OpacityMask通常无法参与Early-Z优化。因为它在片元阶段才做clip会造成光栅化后的无效像素继续执行很长时间。所以尽量不要让树的OpacityMask用到过多纹理通道能不做的模糊就交给alpha-to-coverage能省则省。6. 参考源码后的二次开发思路看完SpeedTree Shader不用急着把它当黑盒。至少有三个方向可以做二次开发第一个是自定义风力输入。官方的风向和速度来自全局参数但你可以通过CustomPrimitiveData或Material Parameter Collection把自定义的风向图、高度曲线传给Shader模拟峡谷风或者城市楼宇间的回流。我在一个场景里就加了一张“风场高度图”让树冠在不同高度有不同的摆动方向效果比全局风干净很多。第二个是定制叶子的动态颜色。通过在片元着色器中读取世界场景的天气参数可以做成下雨时叶子变深天晴时偏黄的过渡效果。直接在材质蓝图里也能做但一旦变体多起来Shader里的分支比蓝图更快可控性也更强。第三个是让树参与地面力场交互。比如踩到落叶时周围的树叶抖动或者角色经过时树枝被推开。这类交互如果用物理骨骼做开销太大但利用SpeedTree顶点数据的“额外通道”可以在Shader里加一个包围盒内风力脉冲成本几乎为零。引擎本身没有开放这个功能但我在源码基础上加了一组USE_EXTRA_WIND_FIELD宏每棵树额外暴露一个ForceMap数组实测下来很稳。最后分享一个小技巧分析SpeedTree Shader时先忽略所有光源代码把注意力集中在纹理寻址和数学函数上。很多植被顶点动画的本质都是基于时间变量做“正弦/余弦扰动”明白这一点你以后看任何树叶Shader都会有种“原来如此”的感觉。希望这篇源码解析能帮你在植被渲染的路上少走点弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑