ASE复刻《原神》风格草地:从Shader原理到性能优化全解析

发布时间:2026/7/24 9:01:17
ASE复刻《原神》风格草地:从Shader原理到性能优化全解析 1. 项目概述为什么选择ASE复刻《原神》风格草地如果你在Unity里做过场景尤其是开放世界或者风格化渲染肯定对《原神》里那片随风摇曳、色彩层次丰富的草地印象深刻。那种草地的灵动感远不是贴一张草皮纹理就能搞定的。它涉及到复杂的多层纹理混合、基于风向的顶点动画以及微妙的光影响应。市面上有很多现成的草地Shader资产但要么太贵要么不够灵活没法完全匹配你项目独特的美术风格。这就是为什么我选择用Amplify Shader EditorASE来手动复刻一个。ASE是一个基于节点的可视化Shader编辑工具它把复杂的HLSL/GLSL代码变成了连线游戏对美术和TA技术美术极其友好。你不用再死磕语法可以把精力完全集中在效果逻辑的构建上。通过这个“保姆级”教程我的目标不仅是让你得到一个能用的草地Shader更是带你彻底吃透从UV动画到顶点偏移这一整套制作风格化植被的核心管线。无论你是想深入学习Shader原理的程序员还是希望提升场景表现力的美术这篇从零到一的拆解都能给你一套可以直接“抄作业”的方案。2. 核心效果拆解与设计思路在动手连节点之前我们必须先搞清楚《原神》风格的草地到底“神”在哪里。盲目堆叠效果只会做出又卡又乱的“光污染”。经过反复观察和测试我将其核心效果拆解为四个层次这也是我们Shader设计的蓝图。2.1 视觉层次解析多层纹理与色彩变化最基础的草地只是一片绿色。而《原神》的草地之所以生动是因为它包含了至少三层视觉信息基底色Base Color这是草地的主色调通常是一种偏暗或中性的绿色提供了整体的色彩基调。细节纹理Detail Texture在基底之上会叠加一层带有噪点、草叶形状或斑驳痕迹的细节纹理通常是一张Tileable的噪声图或细节图。这层纹理通过UV的轻微缩放和平移打破了基底色的单调模拟出草地的微观不均匀性。色彩变化层Color Variation这是营造“丰富感”的关键。我们不会让整片草地都是一个绿度。通常会在草地表面尤其是迎光或顶部混合进一些更亮、饱和度更高的黄绿色或青绿色同时在背光或根部保留较深的绿色。这种变化可以通过顶点颜色Vertex Color的R、G通道或者另一张世界空间投影的噪声图来驱动。实操心得千万不要用纯色或单一饱和度做基底。我习惯用一张经过精心调色的、带有微妙色彩变化的渐变纹理Gradient Ramp作为色彩查找表LUT然后根据草地的法线方向Y分量或高度信息来采样这样能得到非常自然的光影过渡。2.2 动态核心风场驱动的顶点动画静态的草地是死的风是赋予它灵魂的关键。《原神》草地的摆动不是简单的左右摇晃而是一种波浪状、有节奏的复合运动。其核心原理是顶点偏移Vertex Offset。运动波形我们使用正弦Sine或余弦Cosine函数来模拟周期性摆动。但单一的正弦波会显得非常机械。因此我们需要至少两个频率和幅度不同的波形进行叠加比如一个低频大波浪模拟整体的风向摆动一个高频小波浪模拟草尖的细微颤动。风场输入为了让整片草地的运动协调一致我们需要一个全局的风向量Wind Vector。这个向量通常由脚本控制包含风向和风力强度。Shader中的顶点动画需要基于这个向量和模型自身的世界位置进行计算确保同一区域的草摆动方向一致。基于草高度的差异化摆动草根部位几乎是固定的而草尖摆动幅度最大。这个效果通过将顶点在模型空间中的高度通常是Y坐标作为一个权重因子Mask来实现。用这个权重去调制波形函数的幅度就能实现“根部稳尖部飘”的自然效果。2.3 光影与交互简单的PBR与边缘光虽然《原神》是风格化渲染但它依然遵循一些物理光照的规律来保证物体能扎实地“坐”在场景里。简化PBRPhysically Based Rendering我们不需要完整的金属度/粗糙度工作流。但对于草地一个关键的参数是光滑度Smoothness。草叶表面通常不是完全粗糙的会有一些微弱的镜面高光。我们可以用一个较低的值如0.1-0.3来赋予草地湿润或油亮的感觉。同时一个简单的法线贴图Normal Map能极大地增加草叶的立体感和细节即使模型面数很低。风格化边缘光Rim Light在背光或侧光情况下给草地边缘添加一圈柔和的亮边能有效将物体从背景中分离出来增强卡通感和体积感。这可以通过计算视角方向与法线方向的点积Dot Product来实现越边缘点积接近0的地方叠加越强的自发光Emission或颜色叠加。2.4 性能考量Shader复杂度与优化点在移动平台或开放大世界草地往往是性能杀手。我们的设计必须在效果和效率间权衡顶点动画的成本顶点着色器中的计算是每个顶点执行的。虽然正弦函数不贵但叠加多个、并采样纹理如噪声图做随机化就会增加负担。要严格控制波形数量和纹理采样次数。纹理采样次数理想情况下我们将基底色、细节、法线等信息尽可能打包到更少的纹理中。例如使用RGB通道存储颜色Alpha通道存储光滑度。LODLevel of Detail对于远处的草地可以简化甚至完全关闭顶点动画和法线贴图使用一个更简单的Shader变体Variant。这通常通过Shader的#pragma multi_compile指令或直接使用不同的材质球来实现。3. 在ASE中搭建Shader框架与基础颜色打开ASE新建一个Unlit Shader或Lightweight Render Pipeline/Lit Shader模板。我推荐从LWRP/URP的Lit模板开始因为它已经集成了标准光照模型我们只需要修改表面数据即可。3.1 创建主节点网络与属性定义首先在Master Node的左侧我们会看到最终Shader输出的接口Albedo漫反射颜色NormalEmission等。我们的所有计算最终都要连接到这些端口。 在开始连线前先在Node Properties面板创建Shader属性Properties。这些属性会在材质球Material Inspector上显示为可调节的滑块、颜色块或纹理槽。关键属性包括_MainTex基底纹理。_MainColor基底色调色。_DetailTex细节噪声纹理。_WindVector一个二维向量Vector2用于控制风向X,Z和风力强度通常用其中一个分量或长度表示。_WindFrequency和_WindStrength控制风波的频率和整体强度。_ColorVariation色彩变化强度。_Smoothness光滑度。在ASE中你可以通过右键搜索Property节点来创建它们然后赋予一个引用名如_WindVector和默认值。3.2 实现多层纹理混合与色彩变化这是构建视觉丰富度的第一步。基底纹理采样创建一个Texture Sample节点将其Texture属性关联到_MainTex。将它的RGB输出乘以一个Color节点关联_MainColor得到基础颜色。细节纹理叠加创建第二个Texture Sample节点给_DetailTex。这里有个技巧为了让细节纹理无缝且自然我们需要使用一套独立的、经过缩放的UV。创建一个Tiling And Offset节点将默认的UV输入进去然后给Tiling一个较大的值如5或10让细节纹理重复多次这样在近距离看时才能看到丰富的微观变化。然后将细节纹理的RGB通常我们取单个通道如R通道与基础颜色进行某种混合。我常用Multiply正片叠底模式这样细节图的深色部分会让基底变暗亮色部分影响较小模拟出污渍或斑驳。引入色彩变化方法一顶点颜色如果你的草地模型在建模软件中预先绘制了顶点颜色比如顶部红色根部绿色那么事情就简单了。直接添加一个Vertex Color节点用它的R通道作为一个系数去Lerp线性插值两个不同的颜色。比如从Color A深绿插值到Color B黄绿。方法二世界空间噪声更通用的方法是使用世界空间坐标。添加一个World Pos节点取其X和Z分量忽略Y即高度构成一个二维坐标。用这个坐标去采样一张噪声纹理比如_DetailTex也可以复用。噪声值提供了一个随位置变化的随机因子。将这个噪声因子通过一个Power节点压暗使其大部分区域接近0少数区域接近1再乘以_ColorVariation强度最后用它去Lerp基础颜色和一个更亮/更饱和的VariationColor。最终混合将经过细节叠加和色彩变化处理后的颜色连接到Master Node的Albedo端口。注意事项纹理过滤模式Filter Mode非常重要。对于_DetailTex这类用于混合的Tileable纹理务必将其导入设置中的Wrap Mode设为RepeatFilter Mode设为Bilinear以避免接缝和像素化。同时所有用于颜色混合的纹理建议勾选sRGB (Color Texture)确保颜色空间正确。4. 核心动态效果UV动画与顶点偏移实现这是Shader最“炫技”的部分我们分两步走先让草地表面的细节“流动”起来再让整片草“摇摆”起来。4.1 基于时间的UV动画模拟微风扰动UV动画的目的是让草地表面的细节如光影斑点、高光产生缓慢的移动模拟阳光穿过云层或微风吹过草叶表面的光影变化。这不会移动顶点因此性能消耗极低。创建时间变量添加一个Time节点。它输出一个随时间不断增大的浮点数。控制动画速度将Time节点乘以一个较小的系数如0.1这个系数就是动画速度_UVAnimSpeed。你可以把它做成一个属性方便调节。构造UV偏移量我们希望UV沿着某个方向缓慢平移。创建一个Vector2节点假设我们希望UV沿对角线方向11移动但速度很慢。那么偏移量就是(time * speed, time * speed)。更自然的方式是使用两个不同速度的正弦波来驱动U和V的偏移产生更随机的运动感。// 伪节点逻辑示意 float2 uvOffset; uvOffset.x sin(Time * _SpeedX) * _AmplitudeX; uvOffset.y cos(Time * _SpeedY * 1.7) * _AmplitudeY; // 使用不同的频率和相位应用偏移将计算好的uvOffset加到一个Tiling And Offset节点的Offset输入上再将这个节点的输出连接到_DetailTex的UV输入。这样细节纹理就会随时间缓慢漂移了。4.2 构建风场驱动的顶点偏移网络顶点偏移在Shader中是通过修改Vertex Position节点在Vertex Data类别里的输出并将其连接到Master Node的Vertex Position端口来实现的。注意启用顶点偏移后Shader的渲染队列Queue可能需要调整为Geometryxxx并考虑关闭批处理Disable Batching因为每个物体的顶点动画可能不同。准备世界空间坐标添加World Pos节点。我们需要物体的世界位置来确保同一区域的草运动同步。生成基础波形使用World Pos的X和Z分量分别与风向量_WindVector的X和Z做点乘Dot Product得到一个标量代表该顶点在风向上的投影位置。这能保证草沿着风向摆动。将这个投影位置乘以风的频率_WindFrequency再加上Time * _WindSpeed作为正弦函数Sine的输入。Sine节点的输出是一个在[-1, 1]波动的值。将其乘以风的强度_WindStrength得到基础的波形幅度baseWave。叠加第二层波形复制上述流程但使用不同的频率如_WindFrequency * 1.3和时间偏移Time * _WindSpeed phaseOffset生成detailWave。将baseWave和detailWave相加得到更复杂的复合波combinedWave。制作高度权重遮罩这是实现“草尖动根不动”的关键。我们需要模型空间的高度。添加Object Pos节点或Vertex Position在模型空间下的Y分量。将其归一化(vertex.y - minHeight) / (maxHeight - minHeight)。在ASE中可以用Remap节点将原始Y值从模型的大致范围映射到[0,1]。这个0到1的值就是高度权重heightMask。应用遮罩与风向将combinedWave乘以heightMask。这样根部mask0的偏移为0尖部mask1偏移最大。然后将这个标量结果乘以风的方向向量_WindVector需要归一化得到一个表示偏移方向和力度的向量windOffset。添加随机性为了让草地摆动不那么整齐划一可以引入一些随机性。用世界位置的XZ坐标采样一张简单的噪声图比如_DetailTex的G通道得到一个[0,1]的随机值randomness。用这个值轻微地扰动heightMask或者_WindFrequency这样每株草的摆动周期和幅度就会有细微差别。最终合成顶点位置获取原始的模型空间顶点位置originalPosVertex Position节点。因为我们计算的风向windOffset是在世界空间的需要将其转换到模型空间或者将原始顶点转换到世界空间后再相加。更简单且常用的方法是直接在模型空间计算偏移。我们可以近似地认为风在世界空间的XZ方向对应到模型空间也是类似的水平方向。因此我们可以直接用windOffset的X和Z分量去修改originalPos的X和Z值。在ASE中使用Append节点将originalPos.x windOffset.x,originalPos.y,originalPos.z windOffset.z组合成一个新的三维向量。将这个新向量连接到Master Node的Vertex Position端口。踩坑实录最大的坑是空间转换混乱。最初我直接在模型空间用世界时间计算导致每个物体独立运动非常鬼畜。必须使用World Pos或Object Pos结合Time来驱动。另一个坑是偏移量过大导致裁剪问题。如果_WindStrength设得太大顶点可能会被移出相机的视锥体Frustum而被裁剪掉。如果发现草在摆动时突然消失请检查相机的远裁剪面距离或者降低风强。此外对于使用了合批Batching的静态物体顶点偏移会导致合批失败需要谨慎使用。5. 增强表现力法线、光滑度与边缘光颜色和动画有了接下来要让草地看起来更“高级”能与光照正确交互。5.1 法线贴图的应用与混合即使是一个简单的平面一张好的法线贴图也能瞬间赋予其草叶的起伏质感。采样法线贴图添加一个Texture Sample节点导入一张草地的法线贴图。确保其Texture Type设置为Normal map。解码法线从纹理采样节点直接输出的RGB值需要被解码为法线向量。ASE提供了Normal Unpack节点对于Unity的标准法线贴图或Unpack Scale Normal节点。直接连接即可。切线空间到世界空间默认采样的法线是在切线空间Tangent Space下的。为了与光照计算我们需要将其转换到世界空间。添加一个Transform节点选择From Tangent To World然后将解码后的法线向量和默认的World Normal、World Tangent等向量连接上去。转换后的世界空间法线连接到Master Node的Normal端口。与顶点法线混合如果你的模型本身有简单的顶点法线比如一个由多个面片组成的草簇模型你可以将贴图法线与顶点法线用Normal Blend节点进行混合获得更细腻的效果。5.2 光滑度与高光控制在URP/Lit模板下Master Node有Smoothness输入。常量光滑度最简单的方式是创建一个Float属性_Smoothness直接连过去。给一个0.1左右的值草地就会带有非常微弱的高光。纹理控制光滑度更精细的做法是使用一张纹理的某个通道通常是Alpha通道来控制。例如你可以让草叶中间部分光滑度高一点模拟叶面蜡质边缘粗糙一点。将纹理的A通道乘以一个系数后再连接到Smoothness。5.3 实现风格化边缘光Rim Light边缘光能极大地增强草地的卡通感和体积感尤其在逆光环境下。计算视角方向添加View Dir视角方向节点和Normal世界法线节点。计算边缘因子使用Dot Product节点计算视角方向与法线方向的点积。当视线与法线垂直即看物体边缘时点积接近0当视线与法线平行即看物体正面时点积接近1或-1取决于方向。我们需要的是边缘所以取1 - abs(dot(N, V))。在ASE中可以用Abs和One Minus节点组合实现。锐化与着色直接使用上一步的因子通常太柔和。我们用一个Power节点对其求幂比如5次方或10次方让亮边更集中在真正的边缘。然后将这个处理后的因子乘以一个Rim Color边缘光颜色通常是与主光互补的亮色。叠加到自发光将计算好的边缘光颜色连接到Master Node的Emission端口。这样它就会自发亮不受场景光照影响效果非常醒目。你也可以选择将其加到Albedo上做颜色叠加效果会更柔和。6. 性能优化与平台适配实战效果做漂亮了但如果跑起来只有10帧一切都没有意义。尤其是在移动端或大型场景中Shader优化是必须考虑的一环。6.1 关键节点性能分析与简化在ASE中不同类型的节点开销不同。你需要养成在连线的同时查看Precision和Instructions的习惯在节点属性面板或Shader编译日志中。纹理采样Texture Sample是最昂贵的操作之一。尽可能复用纹理。例如用_MainTex的RGB通道做颜色Alpha通道做光滑度用_DetailTex的R通道做颜色细节G通道做顶点动画随机数B通道做其他遮罩。复杂数学运算SinCosPowNoise噪声函数等比加减乘除要贵。尽量减少在片段着色器Fragment Shader中使用它们能移到顶点着色器Vertex Shader的就移过去。例如顶点动画的计算主要在顶点着色器每个顶点算一次而颜色计算在片段着色器每个像素算一次。显然把波形计算放在顶点着色器更划算。条件判断If与分支在Shader中应尽量避免。可以用Lerp线性插值或Smoothstep函数来替代简单的条件逻辑。实时查看看Shader复杂度在ASE编辑器菜单栏Window - Shader Analyzer可以打开分析器查看当前Shader在不同平台如GLES3 Vulkan下的指令数、纹理采样数等关键指标。6.2 使用Shader变体与LOD策略我们不可能用一个超级复杂的Shader通吃所有距离的草。定义变体关键字在ASE中你可以通过Keywords节点来定义Shader变体。例如定义一个_DETAIL_ON的关键字用来控制是否启用细节纹理混合。在远距离的LOD材质上我们不勾选这个关键字Shader就会编译一个不包含细节纹理采样的、更轻量的版本。制作LOD材质这是美术工作流。在Unity中为同一个草地模型准备两到三个材质球。一个HighQuality材质使用我们制作的全功能Shader。一个LowQuality或Far材质使用简化版的Shader或者同一个Shader但关闭了某些功能的关键字并分配给较低LOD级别的模型。在脚本中动态切换你也可以写一个简单的C#脚本根据摄像机与物体的距离动态更换材质球。这对于大量重复的植被实例非常有效。6.3 移动端与WebGL特殊处理移动平台iOS/Android和WebGL对Shader的支持和性能限制更为严格。精度选择在ASE的Master Node设置或每个属性节点上可以将精度从float高精度改为half中精度甚至fixed低精度。对于颜色和UV计算half通常足够能显著提升性能。在移动端优先使用half。避免discard操作在片段着色器中丢弃像素例如做透明裁剪在某些移动GPU上非常昂贵。对于草地尽量使用标准的Alpha Blend透明混合。简化光照模型在URP下可以考虑使用Simple Lit着色器模型代替Lit它使用了更简化的高光计算Blinn-Phong开销更低。测试测试再测试一定要在目标真机上进行性能测试。PC上流畅60帧在手机上可能直接卡成幻灯片。使用Unity的Frame Debugger和Profiler工具分析渲染耗时。7. 常见问题排查与调试技巧即使按照教程一步步来也难免会遇到各种妖魔鬼怪。这里记录了我踩过的一些坑和解决方法。7.1 效果异常问题速查表问题现象可能原因排查与解决思路草地一片漆黑1. 法线方向错误。2. 光照信息未正确传入。3. 颜色输出值全为0或负数。1. 检查法线贴图采样和转换节点是否正确连接。在场景视图中将Shading Mode切换到Normal查看法线颜色是否异常。2. 确认使用的是URP/Lit模板并且场景中有URP配置的光源。3. 断开所有后期节点直接从MainTex连到Albedo看是否有颜色。逐步恢复节点定位问题环节。顶点动画导致模型撕裂或闪烁1. 顶点偏移量计算错误导致顶点位置出现NaN或无限大。2. 不同LOD级别或合批的物体动画不同步。1. 检查波形计算中是否有除0操作。用Clamp节点将偏移量限制在一个合理范围内。2. 确保用于驱动动画的World Pos或Time节点是统一的。对于需要独立动画的物体考虑关闭动态合批在材质球上设置DisableBatchingTrue。边缘光Rim Light全屏可见或不可见1. 边缘因子计算错误。2. 颜色或强度值设置不当。1. 将边缘因子的中间计算结果1-abs(dot(N,V))直接输出到Emission查看其灰度图。应该是物体中心黑边缘白。如果不是检查View Dir和Normal向量是否正确是否归一化。2. 调整Power节点的指数让白色区域更窄。逐步增加Rim Color的亮度。细节纹理Detail Tex有接缝纹理的Wrap Mode未设置为Repeat或者UV缩放Tiling值不是整数。在Unity Project面板中选中细节纹理在Import Settings里将Wrap Mode改为Repeat。确保Tiling And Offset节点中的Tiling值在大多数情况下使用整数。在Game视图正常Scene视图异常Scene视图可能使用了不同的渲染管线或光照模式。检查Unity编辑器顶部确保Scene视图的Shading Mode不是Wireframe或Shadow Cascades等。这通常是视图设置问题不影响最终构建。7.2 ASE节点网络调试方法论当Shader效果不符合预期时系统性的调试比盲目改参数有效得多。隔离法从最终输出Master Node开始逐个断开输入连接。比如先断开Normal看颜色是否正确再断开Emission看基础光照是否正确。逐步缩小问题范围。可视化调试这是ASE最强大的功能之一。右键点击任何节点之间的连线选择Convert To Subgraph临时或者直接创建一个Custom Expression节点将你想查看的中间变量比如计算出的高度遮罩、波形值直接输出到Albedo或Emission。通过观察屏幕上的颜色通常把值映射到0-1的灰度你能直观地看到每个阶段的计算结果是否正确。例如将heightMask范围0-1直接输出为灰度色你应该看到草模型从底部黑到顶部白的渐变。将combinedWave范围约[-N, N]通过Remap节点映射到[0,1]再输出你应该看到一片随时间波动的灰度图。检查编译错误与警告ASE窗口下方有编译日志。任何红色错误Error都会导致Shader无法运行。黄色警告Warning有时也暗示着潜在问题比如未使用的属性或精度转换丢失。参考内置Shader当你对某个效果比如法线转换不确定时可以去Unity内置的URP Shader库里找参考。在ASE中可以通过Node Palette搜索一些内置函数节点它们的用法往往是最标准的。7.3 与Unity引擎的交互注意事项Shader不是孤立的它需要和Unity引擎的其他部分协同工作。渲染队列Render Queue与混合模式如果你的草地是半透明例如有毛茸茸的边缘需要将Master Node的Blend Mode设为Alpha并将渲染队列设置为Transparent。但注意透明物体渲染顺序靠后且不能写入深度可能引发排序问题。对于大多数草地使用Opaque不透明或AlphaTest透明测试更常见。深度写入ZWrite与深度测试ZTest对于使用AlphaTest的草地比如用一张透明贴图裁剪出草的形状通常ZWrite设为OnZTest设为LEqual。对于完全透明的部分要确保正确丢弃像素否则会出现深度冲突。接收与投射阴影在URP中确保材质球勾选了Receive Shadows和Cast Shadows。如果你的Shader是自定义的可能需要确保深度和阴影Pass的正确性。ASE生成的URP Shader通常会自动处理这些。与风区Wind Zone配合Unity自带的风区组件主要用于带动Tree Creator和SpeedTree模型。对于我们的自定义Shader风区数据需要通过脚本读取如WindZone.main.windMain等然后通过材质属性块MaterialPropertyBlock或全局Shader属性Shader.SetGlobalVector传递给我们的_WindVector和_WindStrength。这样可以实现场景内风力的统一和动态变化。