Unity Shader实战:从零构建噪声着色器实现熔岩、腐蚀、全息特效

发布时间:2026/7/22 17:23:04
Unity Shader实战:从零构建噪声着色器实现熔岩、腐蚀、全息特效 1. 项目概述从“噪声”到“视觉魔法”如果你在Unity里鼓捣过材质想让模型表面有点风吹草动的感觉或者想做出那种老电视雪花屏、水面波纹、云层流动的效果那你大概率绕不开一个词——噪声。这可不是指你耳机里的杂音在图形学里噪声是一种用来生成随机但连续、可控的图案的算法。它不像纯随机数那样杂乱无章而是能产生一种“有机的”、“自然的”随机感这正是模拟自然界绝大多数不规则纹理如木头纹理、大理石纹、火焰、地形的核心。这次要聊的就是如何亲手在Unity里利用Shader把这种“噪声”变成你想要的任何自定义视觉效果。网上有很多现成的Noise Shader包但直接拿来用总感觉隔了一层纱出了问题不知道怎么调想做个独特效果也无从下手。所以不如我们自己从原理开始一步步拆解写一个属于自己的噪声Shader。这不仅能让你彻底理解噪声如何驱动视觉更能让你获得“凭空造物”的能力——任何你能想象到的、基于随机扰动的高级效果都有了实现的根基。整个过程我们会聚焦于ShaderLab和HLSL/CG语言在Unity的渲染管线中操作。不需要你已经是Shader大师但需要你对Unity材质、贴图有基本了解并且能勇敢地打开那个看起来有点吓人的Shader代码文件。我们的目标很明确不依赖任何第三方插件从一行代码开始构建一个可灵活调控的噪声着色器并以此为基础实现几个常见的炫酷效果。2. 核心思路噪声Shader的构建逻辑写一个噪声Shader听起来很高深但其实它的核心逻辑是一条非常清晰的流水线。理解这条流水线比死记硬背几句HLSL代码重要得多。2.1 噪声算法的选择为什么是“值噪声”和“柏林噪声”首先我们得选一把“噪声生成器”。图形学中噪声算法很多但对于Shader入门和绝大多数视觉效果来说值噪声和柏林噪声是两把最称手、也最需要理解的利器。值噪声是最好理解的。想象一下你在一个三维空间的整数坐标点比如(1,1,1), (2,1,1)…上随机撒上一些“值”比如0到1之间的随机数。那么对于空间中任意一个非整数坐标点比如(1.3, 1.7, 1.2)它的值是多少呢值噪声的做法是找到这个点所在的“单位立方体”的8个角点根据这个点距离8个角点的位置用插值算法通常是平滑插值如lerp或smoothstep计算出最终值。这样得到的结果是块状的、有明显网格感的低频噪声。它的优点是计算简单易于理解是学习噪声概念的最佳起点。柏林噪声则是Ken Perlin大师的杰作也是目前使用最广泛的梯度噪声。它的思路更巧妙在每个整数坐标点上不是存储一个随机值而是存储一个随机的梯度向量一个方向。对于空间中的任意点我们计算该点到周围整数点的向量并与该整数点上的梯度向量做点积得到一组影响值最后再将这些影响值平滑地混合起来。柏林噪声生成的结果更加自然、平滑没有明显的网格瑕疵频率感更丰富非常适合模拟云、烟雾、大理石等自然纹理。实操心得入门时强烈建议你先亲手实现一遍值噪声。虽然最终效果上柏林噪声更优但值噪声的实现过程能让你透彻理解“空间离散采样插值”这一噪声的本质。理解了值噪声再看柏林噪声你会恍然大悟原来它只是把“标量值”换成了“向量点积”核心的插值思想一脉相承。在我们的自定义Shader中我们会从实现一个基础的值噪声函数开始然后再进阶到柏林噪声。这样Shader就拥有了一个可配置的“噪声源”。2.2 Shader的数据流从模型顶点到屏幕像素有了噪声算法我们得知道把它放在Shader的哪个环节来用。一个标准的Unity表面着色器或顶点/片元着色器数据流大致是这样的顶点着色器输入是模型的顶点位置、法线、UV等。在这里我们可以利用噪声去扰动顶点的位置。比如传入时间变量让噪声值随时间变化然后让顶点沿着法线方向轻微移动就能做出模型表面波浪起伏的动画效果。这个阶段修改的是顶点的几何信息。片元着色器输入是经过插值后的顶点数据如UV坐标。在这里我们可以利用噪声去决定像素的颜色。这是最常用的方式。例如用世界空间坐标或UV坐标作为输入采样噪声函数得到一个灰度值。然后我们可以用这个灰度值来混合两种颜色、作为透明度、或者作为凹凸贴图的强度从而在像素级别创造出丰富的表面细节。方案选型的考量对于纯视觉特效如腐蚀效果、动态污渍我们通常在片元着色器里操作噪声因为这样精度最高不改变模型形状。对于需要形体变化的特效如熔岩地表起伏、旗帜飘动则必须在顶点着色器里操作。一个强大的自定义噪声Shader应该能同时支持这两种模式或者至少让使用者能清晰地知道在哪里添加噪声代码。我们的构建思路是先创建一个在片元着色器中输出纯噪声图案的Shader把它当作我们的“调试视图”。确保噪声生成正确后再将它作为输入去驱动颜色、透明度、纹理混合等更复杂的效果。3. 基础搭建创建你的第一个噪声Shader理论说再多不如动手写一行。我们就在Unity里从零开始创建一个Shader文件。3.1 创建Shader与材质在Unity项目窗口中右键选择Create - Shader - Standard Surface Shader。先以表面着色器模板入门它帮我们处理了光照等复杂问题让我们更专注于噪声本身。给Shader起个名字比如CustomNoiseShader。创建一个新的材质球将它的Shader选择为我们刚创建的CustomNoiseShader。将这个材质赋给一个简单的Plane或Sphere对象以便预览。现在双击打开这个Shader文件。你会看到一大堆代码。别慌我们一步步来简化并注入我们的逻辑。3.2 实现基础值噪声函数我们首先在CGPROGRAM块内surf函数之前编写我们的噪声函数。这里我们实现一个2D值噪声输入是一个float2坐标比如UV输出一个float噪声值。// 简单的伪随机函数用于生成整数坐标点的随机值 float rand(float2 co) { return frac(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453); } // 2D 值噪声 float valueNoise(float2 st) { float2 i floor(st); // 获取当前点所在的网格整数坐标 float2 f frac(st); // 获取当前点在网格内的小数部分坐标 // 获取网格四个角点的随机值 float a rand(i); float b rand(i float2(1.0, 0.0)); float c rand(i float2(0.0, 1.0)); float d rand(i float2(1.0, 1.0)); // 对小数部分进行平滑处理让过渡更自然 float2 u f * f * (3.0 - 2.0 * f); // 平滑插值因子 // 双线性插值 return lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y); }代码解析rand函数是一个经典的哈希函数它给一个float2输入返回一个看似随机的float。注意它并不是“真随机”相同的输入永远得到相同的输出这在Shader中是可重复的、必需的。valueNoise函数中floor和frac是关键。它们把连续的空间坐标st拆解成了离散的网格索引i和网格内的局部位置f。我们计算出当前点所在网格四个角i,i(1,0),i(0,1),i(1,1)的随机值a, b, c, d。直接使用f进行线性插值lerp会产生明显的折线感。因此我们对f用公式f*f*(3-2*f)进行平滑处理得到u。这个公式是smoothstep函数的一种简化实现能让插值在起点和终点时导数为零过渡极其平滑。最后先在x方向插值a,b和c,d再在y方向插值这两个结果完成双线性插值。3.3 在Surface Shader中应用噪声现在修改surf函数让我们材质的颜色由噪声决定。找到void surf (Input IN, inout SurfaceOutputStandard o)函数。Input结构体通常包含了UV信息uv_MainTex。我们用它作为噪声的输入坐标。void surf (Input IN, inout SurfaceOutputStandard o) { // 使用模型的UV坐标作为噪声输入 float2 noiseUV IN.uv_MainTex * _Scale; // _Scale 是一个可调节的缩放属性 float n valueNoise(noiseUV); // 将噪声值直接赋给颜色产生灰度图 o.Albedo fixed3(n, n, n); o.Metallic 0.0; o.Smoothness 0.0; }你还需要在Properties块和CGPROGRAM中声明_Scale这个属性用于控制噪声的“密度”或“频率”。Properties { _Scale (Noise Scale, Range(0.1, 10)) 1.0 } // ... float _Scale;保存Shader回到Unity编辑器。稍等片刻编译后你应该能看到你的模型表面变成了黑白相间、类似云雾或大理石的灰度图案。拖动Scale滑块可以看到噪声纹理被拉伸或压缩。第一个里程碑至此你已经成功创建了一个完全自制的、可调节的基础噪声Shader它虽然简单但所有复杂的效果都将从这个基础之上生长出来。4. 进阶柏林噪声与分形噪声基础值噪声的网格感有时太强我们需要更自然的柏林噪声。同时单一频率的噪声看起来过于平滑缺乏细节。这就需要引入分形布朗运动的概念。4.1 实现柏林噪声柏林噪声的实现比值噪声稍复杂但结构相似。我们替换掉之前的valueNoise函数或者新增一个perlinNoise函数。// 根据网格点生成一个随机的2D单位梯度向量 float2 grad(float2 p) { // 使用一个简单的哈希来生成看似随机的角度然后转换为向量 float random frac(sin(dot(p, float2(12.9898, 78.233))) * 43758.5453); float angle random * 6.2831853; // 0 到 2PI return float2(cos(angle), sin(angle)); } // 2D 柏林噪声 float perlinNoise(float2 st) { float2 i floor(st); float2 f frac(st); // 计算四个角点的梯度向量 float2 g00 grad(i); float2 g10 grad(i float2(1.0, 0.0)); float2 g01 grad(i float2(0.0, 1.0)); float2 g11 grad(i float2(1.0, 1.0)); // 计算当前点到四个角点的向量 float2 d00 f; float2 d10 f - float2(1.0, 0.0); float2 d01 f - float2(0.0, 1.0); float2 d11 f - float2(1.0, 1.0); // 点积梯度向量 * 距离向量 float p00 dot(g00, d00); float p10 dot(g10, d10); float p01 dot(g01, d01); float p11 dot(g11, d11); // 平滑插值因子同上 float2 u f * f * (3.0 - 2.0 * f); // 双线性插值 float x1 lerp(p00, p10, u.x); float x2 lerp(p01, p11, u.x); float result lerp(x1, x2, u.y); // 柏林噪声结果通常在[-1, 1]之间我们映射到[0,1]方便使用 return result * 0.5 0.5; }将surf函数中的valueNoise调用改为perlinNoise你会发现生成的图案更加柔和、自然那种生硬的网格边界消失了。这就是柏林噪声的魔力。4.2 构建分形噪声Fractal / FBM Noise自然界的纹理如地形、云层包含从大到小多个尺度的细节。分形噪声通过叠加多层八度不同频率和振幅的噪声来模拟这一点。float fbmNoise(float2 st, int octaves, float persistence, float lacunarity) { float value 0.0; float amplitude 1.0; float frequency 1.0; float maxValue 0.0; // 用于最终归一化 for (int i 0; i octaves; i) { float noiseVal perlinNoise(st * frequency); // 使用柏林噪声作为基础 value noiseVal * amplitude; maxValue amplitude; // 累加最大可能值 amplitude * persistence; // 振幅逐层衰减 frequency * lacunarity; // 频率逐层倍增 } // 归一化到[0,1]范围防止叠加后超出 return value / maxValue; }参数解析octaves叠加的层数。层数越多细节越丰富计算量也越大。通常4-8层效果和性能比较平衡。persistence持久度即振幅衰减系数。通常设为0.5意味着下一层噪声的强度是上一层的一半。这个值决定了细节的“锐利”程度。lacunarity间隙度即频率倍增系数。通常设为2.0意味着下一层噪声的频率是上一层的两倍。这个值决定了细节的“密集”程度。现在在Properties中暴露这些参数并在surf函数中调用fbmNoise。你会立刻获得一个细节层次感极强的噪声纹理非常接近真实的地形高度图或云图。注意事项分形噪声计算量随八度数量线性增长。在移动平台或需要高性能的场景务必控制octaves数量如3-4层。另外循环中的perlinNoise调用是性能热点确保你的基础噪声函数尽可能高效。5. 实战应用将噪声转化为具体效果有了强大的噪声生成器我们就可以像使用调色板一样用它来调制各种视觉属性。下面实现几个经典效果。5.1 效果一动态熔岩地表这个效果结合了顶点动画和片元着色。顶点位移在顶点着色器中使用基于世界坐标和时间的分形噪声沿着顶点法线方向扰动顶点位置。// 在顶点着色器函数中如果是Surface Shader需在vertex:vert中声明 void vert(inout appdata_full v) { float3 worldPos mul(unity_ObjectToWorld, v.vertex).xyz; float noise fbmNoise(worldPos.xz * _DisplaceScale _Time.y * _Speed, 4, 0.5, 2.0); // 将噪声值映射到位移范围并沿法线方向移动 float displacement (noise - 0.5) * _DisplaceStrength; v.vertex.xyz v.normal * displacement; }在surf函数的Input结构体中需要添加float3 worldPos;并在顶点修改器中赋值。片元着色在surf函数中使用另一组参数的分形噪声来生成颜色。例如用噪声值在“暗红色”到“亮黄色”之间进行插值模拟熔岩的冷热区域。float colorNoise fbmNoise(IN.worldPos.xz * _ColorScale, 3, 0.5, 2.0); fixed3 coolColor fixed3(0.3, 0.0, 0.0); // 暗红 fixed3 hotColor fixed3(1.0, 0.6, 0.1); // 亮黄橙 o.Albedo lerp(coolColor, hotColor, colorNoise); // 增加一些自发光让热的地方更亮 o.Emission o.Albedo * _GlowIntensity;通过分别控制_DisplaceScale和_ColorScale你可以让地表的起伏和颜色变化产生不同的节奏感效果更生动。5.2 效果二腐蚀风化金属模拟金属表面被腐蚀、氧化的效果关键在于用噪声控制多个贴图或属性的混合。准备纹理在Properties中引入两张贴图一张是干净的金属底色贴图_MainTex一张是锈迹贴图_RustTex。噪声作为蒙版使用基于世界坐标或UV的柏林噪声生成一个腐蚀区域的遮罩。float mask perlinNoise(IN.worldPos.xz * _RustScale); // 对噪声阈值化产生更明确的腐蚀区域边界 float rustThreshold smoothstep(_RustThreshold - _RustSmooth, _RustThreshold _RustSmooth, mask);_RustThreshold控制锈蚀的多少_RustSmooth控制边界的模糊程度。混合纹理与属性fixed4 mainTex tex2D(_MainTex, IN.uv_MainTex); fixed4 rustTex tex2D(_RustTex, IN.uv_MainTex); o.Albedo lerp(mainTex.rgb, rustTex.rgb, rustThreshold); // 金属度和光滑度也进行混合锈蚀区域金属度低、光滑度低 o.Metallic lerp(_Metallic, 0.0, rustThreshold); o.Smoothness lerp(_Smoothness, _RustSmoothness, rustThreshold);你还可以用另一个不同频率的噪声去扰动锈蚀遮罩的UV让锈迹边缘产生更不规则、更自然的变化。5.3 效果三全息投影扰动模拟科幻片中全息投影信号不稳定、受到干扰的效果核心在于用噪声扰动UV采样。准备一张全息纹理通常是一张带有科技线条、网格或图形的半透明纹理。动态UV扰动// 使用时间和屏幕空间坐标结合产生动态噪声 float2 screenUV IN.screenPos.xy / IN.screenPos.w; float2 noiseOffset float2( perlinNoise(float2(screenUV.x _Time.y * _ScanSpeed, screenUV.y)) - 0.5, perlinNoise(float2(screenUV.y _Time.y * _ScanSpeed, screenUV.x 0.5)) - 0.5 ) * _DistortStrength; // 用噪声偏移去采样主纹理 fixed4 holoTex tex2D(_HoloTex, IN.uv_MainTex noiseOffset);颜色与透明度处理o.Albedo _HoloColor.rgb * holoTex.rgb; o.Alpha holoTex.a * _HoloColor.a; // 关键启用透明度混合并设置渲染队列为Transparent o.Emission o.Albedo; // 添加自发光增强全息感在SubShader的Tags中需要设置QueueTransparent RenderTypeTransparent并在Pass中启用Blend SrcAlpha OneMinusSrcAlpha。这样噪声对UV的扰动就会造成图像局部的扭曲和闪烁完美模拟信号干扰。6. 性能优化与调试技巧在Shader世界里功能和性能永远在博弈。写一个效果炫酷的Shader不难难的是让它能在目标平台上流畅运行。6.1 性能优化要点控制计算频率顶点 vs 片元在顶点着色器中进行噪声计算其计算频率等于顶点数在片元着色器中计算频率等于像素数。后者通常高出几个数量级。因此对于大范围的、低频的扰动如地形起伏优先在顶点着色器计算。对于需要高频细节的如表面纹理才在片元着色器计算。预计算与采样如果噪声参数是静态的可以考虑将噪声图预先烘焙成一张纹理在Shader中通过tex2D采样。纹理采样在GPU上是非常高效的操作远比实时计算复杂的噪声函数要快。这就是很多游戏中使用噪声贴图而非程序化噪声的原因。简化噪声算法减少octaves分形层数是最直接的优化手段。考虑使用计算更简单的噪声变种比如Simplex噪声尤其是3D及以上时比Perlin噪声计算量更小。在保证视觉效果的前提下降低噪声计算的维度例如用2D噪声模拟3D效果。利用Shader变体与宏通过#pragma multi_compile或shader_feature为你的噪声Shader创建简化版本。例如一个“_HIGH_QUALITY”变体使用8层FBM和复杂的柏林噪声而“_LOW_QUALITY”变体只使用3层值噪声。根据目标平台的性能自动或手动切换。6.2 调试与问题排查Shader调试不像C#代码那样可以单步跟踪主要依靠“可视化输出”。颜色调试法这是最常用的方法。当你不知道某个中间值是多少时直接把它作为颜色输出到o.Albedo上。例如怀疑噪声值范围不对o.Albedo float3(noiseValue, noiseValue, noiseValue);观察是否是预期的灰度图。怀疑UV坐标o.Albedo float3(IN.uv_MainTex.x, IN.uv_MainTex.y, 0);会显示一个红绿渐变的图。常见问题速查表问题现象可能原因排查步骤模型全黑或全白噪声值超出[0,1]范围或光照模型设置错误。输出纯噪声值到颜色检查是否在灰阶范围内。检查Metallic、Smoothness是否设置异常。噪声图案有明显网格/条纹插值函数不够平滑或随机函数质量差。检查是否使用了smoothstep或类似平滑函数处理插值因子。尝试更换rand函数的种子参数。效果在摄像机移动时闪烁噪声计算中使用了摄像机相关坐标且未考虑抗锯齿。尝试使用世界空间坐标而非屏幕空间坐标。或在噪声计算前对输入坐标进行抖动。分形噪声看起来“脏”或不自然persistence和lacunarity参数设置不当。尝试标准的“0.5和2.0”组合。确保maxValue归一化正确防止叠加后数值饱和。顶点动画导致模型撕裂相邻顶点位移计算不一致导致三角形被拉断。确保用于顶点位移的噪声函数是连续且可导的如Perlin噪声。使用相同的世界坐标计算避免在物体局部空间计算导致接缝。使用Frame Debugger和RenderDocUnity的Frame Debugger可以查看每一帧的绘制调用和渲染状态。RenderDoc是更强大的独立工具可以捕获一帧完整的GPU渲染流水线查看任意一个像素在片元着色器中经历的所有计算和中间结果。当问题极其复杂时它们是终极武器。写Shader是一个不断试错和观察的过程。不要怕出错把每一次异常显示都当作一个线索用颜色把它“画”出来你会逐渐培养出对Shader代码和渲染结果的直觉。当你看到脑海中的效果通过一行行代码呈现在屏幕上时那种成就感是无与伦比的。这份教程只是一个起点噪声的世界远不止于此还有沃利噪声、域扭曲、噪声导数等高级话题等待探索。但掌握了这些基础你已经拥有了打开这扇大门的钥匙。