
前阵子在项目里做植被渲染优化美术那边反馈“树在风里像纸片”“树叶透光油亮得像塑料”我一开始以为是贴图问题后来把UE4的SpeedTree Shader源码完整过了一遍才发现问题其实出在Shader对顶点数据和材质标签的处理上。这篇就把我的分析过程、源码里几个关键逻辑、以及排查坑的经验一起写出来。想做植被渲染、想搞懂UE4树木材质原理、或者单纯想啃一遍SpeedTree Shader源码的朋友都可以参考。1. 为什么我想拆这套Shader项目背景与SpeedTree整体架构1.1 从一棵“会动的树”说起SpeedTree在游戏里几乎成了植被标准但大多数人只把它当建模工具用美术导出FBX拿到UE4里面拖个材质就完事。真正的问题在于SpeedTree模型和普通静态网格体不一样它自带很多“隐藏数据”顶点色、额外UV、LOD相位、风力权重这些在普通模型上根本没有如果你用通用PBR材质去接等于把这些信息全丢了树自然动不起来透光效果也全是假的。我这次分析的版本对应的是UE4.27左右那套Shader实现主要涉及游戏端的材质Shader不讨论SpeedTree Modeler端的光照烘焙逻辑。UE4官方给出的支持方式是你通过SpeedTree导出.SRT或.FBX时带上材质标识进UE4后引擎会自动匹配对应的Shader分支材质面板里也允许你开启“双面”“Subsurface Profile”这类选项但真正决定一棵树怎么动、怎么被裁剪、怎么算风的并不是材质节点本身而是引擎内置的SpeedTree Shader库。1.2 源码入口从哪里找如果你想自己翻源码路径一般在Engine/Shaders/Private/下面重点看这几个文件SpeedTreeCommon.ush公共工具函数风效、HSV颜色偏移、LOD过渡、Billboard朝向、树叶裁剪都在这里。SpeedTreeVertexFactory.ush顶点工厂定义顶点数据怎么从buffer里取出来以及VS阶段如何拼装。SpeedTreeDataDriven.ush数据驱动部分处理从模型顶点读取到的各种附加数据比如顶点色、UV2/UV3里存的风权重、LOD阈值等。ShadingModels.ush和DeferredShadingCommon.ush不是专门给SpeedTree写的但处理次表面、Tree AO时会走到这些逻辑。打开任何一个.ush文件之前建议你先在材质编辑器里建一个普通材质然后点击“Code”查看生成的HLSL搜一下GetSpeedTreeWind这类函数这时候你才明白引擎到底把哪些变量传了进来。这个“材质-着色器源码”的跳转方式比直接看引擎源码更贴近实际因为你看到的是最终编译版本。2. 顶点着色器风、弯曲与LOD的起点2.1 顶点数据里到底塞了什么SpeedTree的顶点数据比普通模型复杂很多这也是很多人第一次看Shader源码时懵的原因。普通模型只要Position、Normal、Tangent、UV0就够了SpeedTree模型至少还会有UV1、UV2部分版本还会用UV3顶点色RGBA四个通道全部都有含义。在UE4的SpeedTreeVertexFactory.ush里顶点输入结构体和普通StaticMesh的差异很大常见排布大概是通道存储内容作用UV0贴图UV树干、树皮、树叶的纹理采样坐标UV1光照UV / 风相位用于lightmap或者风摆动相位根据导出设置不同会有差异UV2LOD阈值、边缘权重、Billboard数据控制距离切换、边缘裁剪、Billboard朝向顶点色R树枝风力权重曲率弯曲和摆动的幅度控制顶点色G树叶风力权重树叶局部摆动、翻卷控制顶点色B边缘裁剪权重实现树叶边缘渐隐、雪覆盖遮罩等顶点色AAO或者自阴影用于树冠内部的AO模拟不同版本、不同导出SDK可能把权重放在不同通道所以源码里经常看到GetSpeedTreeVertexColor这类包装函数就是怕你直接读顶点色把自己绕晕。真的要改风效时不建议绕过这些函数去硬算你很难保证所有平台、所有LOD状态下的通道语义一致。2.2 风效计算拆解风效是SpeedTree Shader里最值得读的一块也是美术同学最常调的。它整体分为两级树干/树枝的弯曲和树叶的局部摆动。树枝弯曲在VS里通过World Position Offset实现核心是在顶点着色器里把顶点位置沿着风向偏移。思路是先拿到一个“风向量”和“风强度”再根据顶点离树根的高度或树枝权重做衰减最后把它加到世界坐标上。这部分在SpeedTreeCommon.ush里能看到类似GetSpeedTreeWind的逻辑里面会用时间、顶点在模型空间的位置、以及风速参数一起计算。树叶摆动则复杂一点因为叶子不是刚体它会有“抖、翻、颤”三种状态。源码里常见的做法是给每片叶子分配一个随机相位这个相位一般来自模型自带的UV或者顶点位置哈希然后叠加多层正弦波。这样即使所有叶子用同一张贴图看起来也不会像“整齐划一的波浪舞”。我实测下来有个心得如果你发现树的风效“整体平移感很强、但局部不动”多半是WindVector这个材质参数没接对或者顶点色G通道没导出。还有一种情况是材质里把WPO输入搞成了常量导致所有顶点用同一个偏移那看起来就是整棵树被风吹成一块铁板。2.3 平滑LOD与裁切SpeedTree的LOD不像普通模型那样直接切换Mesh而是在Shader里做平滑过渡常见做法是“Dither LOD”和“边缘渐隐”。核心思路是当相机距离超过阈值时当前LOD的顶点开始按某种模式被裁剪掉同时下一个LOD的顶点从被裁剪状态恢复出来。这个裁剪并不是随意的点裁剪而是依据顶点数据里的LOD相位和距离参数生成一个ClipThresh值然后在PS阶段使用clip()或discard让像素逐渐消失。因为是要在屏幕空间看到细碎颗粒过渡所以UE4里一般配合DitherTemporalAA或者FadeOut效果使用避免明显的跳变。这个阶段最容易踩的坑是裁剪阈值和贴图alpha没有对齐。比如树干部分不需要边缘裁剪但如果你把树叶和树干的材质共用一套顶点色B通道的权重又没分开就可能出现树干表面突然出现一片暗斑一样的“黑洞”。3. 材质编辑器里的SpeedTree节点换个角度看节点作用3.1 那张著名的“材质节点大全”里SpeedTree占了哪几类很多人手边都收藏过“UE4材质节点大全”里面密密麻麻列了各种节点但真到了SpeedTree这里你会发现直接关联的节点其实不多。严格来说材质面板里并没有叫“SpeedTree”的专属节点真正的逻辑埋在VertexFactory和公用Shader文件里材质只是“数据装配层”。所以你在材质编辑器里能看到的东西基本是这些组合World Position Offset接风效和LOD弯曲输入的是顶点偏移量。Opacity Mask接边缘裁剪、叶子的alpha贴图让树叶边缘能透。Vertex Color提取顶点色里的风力、AO、边缘权重。Subsurface Profile给树叶指定次表面配置模拟透光。Two Sided树叶双面渲染配合法线翻转。如果你对照“UE4材质节点大全”去看会发现这些节点都是通用节点但组合起来就是一套SpeedTree专用模板。我建议团队里做植被的同学把常用组合存成Material Function不然每次新建材质都要从零拼一遍很容易漏掉某一项导致透光异常。3.2 叶子双面光照是怎么做的叶子双面的难点在于从背面看时叶子的法线方向是反的如果直接按常规光照计算背面会黑成一片。SpeedTree的做法是用“双面光照”模型在PS阶段根据当前像素的正面/背面状态对法线做一次翻转或插值。这样叶片正面有高光、背面有透光看起来才像真正的树叶。在SpeedTreeCommon.ush里你能看到类似GetSpeedTreeBentNormal的函数它不只是翻转法线还会结合顶点色里的AO做“卷曲法线”效果。这个“卷曲”是SpeedTree叶子看起来有厚度的关键——让叶片边缘的法线稍微偏向光源方向模拟叶肉透光。这里有个小坑如果你在材质里开启了Two Sided但没开“双面光照”的选项那背面还是会被当反面处理。而且在移动端双面光照的采样开销比PC高有些项目为了让手机跑得动会选择“只开Opacity Mask不计算背面法线”代价是树叶在背光时会显得特别单薄。3.3 Billboard与Impostor处理的坑SpeedTree除了标准模型外还有一个Billboard模式当树离相机足够远时用一张预先烘焙好的树的图片代替3D模型图片始终朝向相机。这套机制在SpeedTreeCommon.ush里也有专门的处理逻辑主要是把顶点位置从模型空间转换成“始终朝向相机”的世界空间位置。我在项目里遇到过一个很典型的问题Billboard和真实模型切换时树的整体颜色会跳变。排查到最后发现是Billboard图片的烘焙光照和场景动态光照差异太大与Shader无关。真正和Shader有关的坑在于Billboard的朝向计算如果顶点数据里没有导出Billboard的朝向轴或者引擎版本不一致远处的树会出现“躺倒”或者“悬浮”的情况。部分项目会放弃引擎自带的Billboard自己用Impostor贴图做原理虽然类似但Shader就得手写。手写Impostor的难点不是“让面片朝向相机”而是如何在不同视角采样正确的贴图区域还要处理深度偏移否则会出现和地面穿插的问题。4. 像素着色器与光照模型从树叶透光到精度控制4.1 Subsurface与Tree AO很多新手做树叶材质时直接上一个标准的Subsurface Profile结果树亮得像灯箱。真正让树叶透光自然的是“厚度图次表面半径”的组合SpeedTree的Shader里会在PS阶段读取一个厚度或者AO值控制光线穿透的强度。UE4的材质模型里树叶用的光照模型一般不是完全写死的你可以选Subsurface、PerPixelWorldNormal这些组合。源码层面对应的函数在ShadingModels.ush里核心是用SubsurfaceColor和Transmission计算半透光效果。这里需要特意说一句树叶的AO不要只依赖贴图SpeedTree导出时一般会把环境光遮蔽算进顶点色A通道。你会发现很多树叶材质没有单独的AO贴图但树冠内部依然有明暗层次就是靠这个顶点色A在起作用。这也是为什么用纯PBR材质去复现SpeedTree效果总是“少了点什么”的原因。4.2 精度相关的问题Half Float 与移动端Shader源码分析如果只看PC端你会漏掉很多移动端才暴露的问题。SpeedTree的Shader在PC上很多中间量用的是float但在移动端为了带宽和性能引擎会把一部分变量降成half比如风向量、法线、UV偏移量。降精度带来的直接后果就是风的相位在高频噪声上会断裂远处树叶会出现“闪烁”或“抖动”。这不一定是你代码写错了而是Mobile管线的编译精度策略导致的。排查方法是先看看该平台使用的Feature Level必要时在材质里加上Full Precision限定或者手动把风效函数放到float作用域里。我在实际项目里遇到“远看没什么问题、近看树叶边缘一直在闪”的情况多半就是法线精度或Dither LOD阈值精度不够跟素材本身没关系。移动端如果实在不想折腾Shader可以降低风效频率让叶子动得幅度大一点、频率低一点反而观感更稳。4.3 从MagicaVoxel Shader得到的启发聊到Shader的精度和顶点数据利用我突然想到之前玩MagicaVoxel时看它导出的Shader一个很厉害的思路是把体素模型的法线数据和AO预计算好然后压缩进顶点数据渲染时直接读不再依赖屏幕空间计算。SpeedTree其实也是这个思路风权重、AO、LOD裁剪权重全部放到顶点色和附加UV里Shader里只做“读取和组合”不做高成本的三维纹理采样。这个“预计算顶点传输”的思想在做大世界植被时特别值钱。你想一棵树可能有几千个顶点如果每个顶点都要在Shader里去计算AO、计算风场、计算遮挡代价太高了但如果你把这些结果事先烘焙到模型数据里运行时只是一次Unpack和一次Lerp性能会好很多。这也是我建议团队里做植被的同学多去了解SpeedTree底层数据组织方式的原因往大了说它甚至会影响你如何选择贴图打包格式和顶点缓冲布局。5. 常见问题与排查技巧实录5.1 树发黑、阴影闪烁、风效不动我整理了几个项目里反复出现的问题基本都能从Shader层找到原因现象常见原因排查方向树整体黑尤其树冠内部顶点色A通道AO权重过大或者Subsurface设置过弱看材质中“Sky/自阴影”相关节点的输入检查顶点色A是否导出正常远处树的阴影在闪Dither LOD和阴影深度偏移冲突或者Billboard没有参与阴影查看LOD过渡距离试关掉阴影的Dither或者把LOD裁剪改成FadeOut风完全不动材质里没有接WindVector或WPO输入为0在材质实例里手动调WindVector确认顶点着色器确实有偏移只动不弯顶点色R通道没有正确导出或者权重全都是1在引擎里查看顶点色确认树枝权重有梯度变化树叶像玻璃一样发亮高光强度过高Subsurface Color过亮降低Specular检查是否有额外的高光贴图被误接其中“风完全不动”是最常见的但也是最好解决的。我见过一个项目美术把SpeedTree模型导入UE4时选择了“作为骨骼网格体”导入结果顶点数据全被重新排列了WPO怎么改都没有效果。最后在导入设置里把Mesh Type改回“Static Mesh”才恢复。5.2 排查套路拿出RenderDoc翻顶点遇到疑难杂症我推荐直接上RenderDoc不要只在材质编辑器里“调参数碰运气”。RenderDoc能把你Shader的所有输入常量、顶点属性、贴图采样全部dump出来你很快能看到顶点色每个通道的值范围是否正常是不是全为1。UV2里是否有LOD阈值阈值范围是否符合预期。风向量是否真的传进了VS还是被某些优化Pass裁剪掉了。有一次我们排查“远处树冠边缘出现黑边”用RenderDoc看PS输入发现是法线贴图采样出来的法线z分量变成负数导致背面光照被错误计算。这个问题在材质编辑器里根本看不出来因为材质预览用的模型面和场景光照都比较理想一到真实场景就露馅。5.3 性能优化清单SpeedTree的Shader性能问题主要集中在顶点数和PS采样上。我的优化清单一般是WPO计算尽量留在VS不要在PS里做任何风效相关的采样。树叶和树干的材质尽量拆成两个Slot避免为了树干去采样树叶的Alpha贴图。远近LOD切换用Dither可以接受但阴影通道里尽量别开Dither阴影闪烁会很明显。Billboard的距离阈值要匹配摄像机移动速度开Run速度调参时总会发现远处树“突然跳出来”。移动端关闭Subsurface或者只用一层近似保留顶点色AO即可。顺便说一句经常有人问UE4里“查询”和“物理模拟器”有什么区别碰到植被交互时很多人想用“外接设备映射”给树做一个“碰一下就动”的效果。这类需求其实跟SpeedTree关系不大外接设备映射更适合做参数调试物理模拟器则负责真实的刚体/受力模拟而SpeedTree的风效是程序化噪声驱动三者属于不同层面。如果你只想让角色碰树时树叶晃动最好用骨骼或PivotPainter方案而不是硬把物理模拟器接到Shader里否则性能会让你怀疑人生。6. 我的一些实测心得和扩展思路最后分享一个我自己的习惯拿到一棵SpeedTree模型后我不会直接去调材质参数而是先导出一份“最低限度”材质把WPO、Opacity Mask、双面、Subsurface都先关掉确认模型本身没有导错。等基础光照正常了再逐步打开双面、边缘裁剪、风效、LOD过渡。这个过程看起来慢实际上是排查问题最快的方式因为如果一上来就套用复杂模板一旦出了问题你根本不知道是哪一层导致的。如果你们项目对植被要求不是特别高我还会推荐一个偷懒做法把SpeedTree Shader里的风函数抽象出来自己写一份“伪SpeedTree”的材质函数只保留基础摆动和边缘裁剪不对应特定模型。这样即便美术换了一批树Shader也能复用。实际效果虽然比不上原生SpeedTree那么精致但胜在可控、可调、性能可预估。这套Shader源码如果继续往下延展其实可以接着做两块一是把风效从CPU时间参数改成GPU噪声场支持区域风、旋风这些特殊效果二是把LOD过渡从“Dither裁剪”改成“顶点位移渐隐”在高端平台上会好看很多。不管是做开放世界植被还是做风格化场景SpeedTree这套数据组织和Shader分层思路都值得反复研究。