Unity Shader噪声算法全解析:从Perlin到Worley的实战应用

发布时间:2026/7/22 5:34:05
Unity Shader噪声算法全解析:从Perlin到Worley的实战应用 1. 项目概述噪声在Shader中的核心价值如果你在Unity里做过地形、水体、火焰或者任何需要“自然感”的特效大概率已经和噪声打过交道了。它不像数学函数那样规整充满了随机、无序和不可预测性但恰恰是这种特性让它成为了模拟自然界中几乎所有复杂现象的秘密武器。从云层边缘的柔和过渡到木头表面的天然纹理再到水面波光粼粼的扰动背后都离不开噪声函数的巧妙运用。《Unity Shader入门精要》这本书将噪声放在“高级篇”我认为是非常精准的定位。当你掌握了基础的光照模型、纹理采样和顶点/片元着色器后噪声就是那个能让你作品从“能用”跃升到“生动”的关键技术。它不再是简单的贴图叠加而是一种程序化生成复杂视觉细节的核心算法。理解并善用噪声意味着你可以在运行时动态生成无限变化且毫无重复感的细节极大地节省美术资源并赋予场景更强的生命力。这次我们就来深入聊聊Shader中那些“超级有用”的噪声。我会结合书中的理论补充大量实战中的细节、性能考量和那些容易踩坑的地方目标是让你不仅能看懂原理更能立刻在项目里用起来。2. 噪声家族全解析从Perlin到Worley噪声算法有很多种每种都有其独特的“性格”和适用场景。选择对的噪声是做出好效果的第一步。2.1 经典之王Perlin噪声及其改进型Simplex噪声Perlin噪声是图形学领域的里程碑由Ken Perlin于1985年提出。它的核心思想是在整数坐标点生成随机的梯度向量然后在点与点之间进行平滑插值。最终得到的是一个连续、平滑且具有一定自然纹理感的标量场。为什么Perlin噪声如此经典因为它生成的噪声图具有以下几个关键特性非常适合模拟自然材质连续性数值变化平滑没有突兀的跳跃这符合大多数自然现象如海拔、云密度的变化规律。可控的频率和幅度通过“分形布朗运动”fBm叠加多个不同频率称为“倍频”的Perlin噪声可以轻松生成从宏观轮廓到微观细节的复杂结构。这就是所谓“分形”噪声是制作地形、云层的基石。各向同性在各个方向上统计特性基本一致没有明显的方向性纹理这使其用途非常广泛。然而经典Perlin噪声在视觉上有时会呈现出明显的网格状或“轴对齐”的瑕疵并且在更高维度如3D、4D计算效率不高。Simplex噪声作为Perlin的改进版被提出。它使用单形如2D中的三角形3D中的四面体网格而非方形网格进行插值。这样做的好处非常明显更少的计算量在N维空间中Simplex噪声只需要计算 (N1) 个顶点的贡献而Perlin需要计算 2^N 个。在3D和4D应用中性能优势巨大。更好的视觉效果消除了轴对齐的网格感噪声纹理看起来更自然、更“有机”。更规则的网格分布在更高维度上方向性更弱。实操心得在Unity Shader中如果追求效果和性能的平衡Simplex噪声通常是首选。Unity内置的noise函数例如在Shader Graph的Procedural Noise节点中其底层很多就是Simplex噪声的变种。对于移动平台如果发现Simplex计算压力大可以回退到优化过的2D Perlin噪声或者预先将噪声烘焙到纹理中。2.2 细胞纹理之王Worley噪声Voronoi噪声如果说Perlin/Simplex噪声擅长模拟连续、渐变的事物如云、大理石那么Worley噪声也称Voronoi噪声或细胞噪声则擅长模拟离散、细胞状的结构如皮革表皮、鹅卵石路面、细胞组织、破碎的地面等。它的原理非常直观在空间中随机分布一系列特征点。对于空间中的任意一点计算它到最近的第N个特征点的距离通常是最近的F1或第二近的F2与F1的差值F2-F1。将这个距离值作为噪声的输出。通过操作这些距离值例如使用F1生成细胞边界使用F2-F1生成内部细节你可以创造出极其丰富的图案。Worley噪声的变体与应用基础细胞直接输出F1得到清晰的细胞状图案。湍流效果将F2-F1作为输出可以得到类似湍流、火焰边缘的尖锐纹理。多重尺度混合像Perlin噪声一样将不同频率的Worley噪声叠加可以生成非常复杂且自然的多尺度细胞结构用于生成科幻风格的星球表面或腐蚀金属效果。2.3 其他实用噪声成员Value噪声比Perlin噪声更简单它在网格点生成随机标量值然后进行双线性插值。它的计算更快但产生的噪声视觉上比较“浑沌”缺乏Perlin那种明确的梯度变化带来的方向感常用于对性能要求极高或需要快速原型的情况。Gradient噪声Perlin噪声的本质。理解了它就理解了如何通过哈希函数生成伪随机梯度以及如何进行平滑插值通常使用五次埃尔米特插值曲线t*t*t*(t*(t*6-15)10)来保证一阶和二阶导数连续。分形噪声fBm这不是一种独立的噪声算法而是一种技术。通过将不同频率frequency lacunarity^i和振幅amplitude persistence^i的底层噪声如Perlin, Simplex, Worley叠加起来你就能得到从大尺度到小尺度的丰富细节。参数lacunarity间隙度控制频率倍增的速度persistence持久度控制振幅衰减的速度。调整这两个参数可以创造出完全不同的地貌特征。3. 核心细节解析与Shader实现要点理解了原理下一步就是动手实现。在Shader中编写噪声函数有几个必须注意的细节。3.1 伪随机性哈希函数的艺术所有程序化噪声的基础都是一个“伪随机”函数它给定一个输入如整数坐标返回一个可重复的“随机”值。在Shader中我们无法使用Random必须使用确定的哈希函数。一个经典且高效的2D哈希函数如下float2 hash22(float2 p) { p float2(dot(p, float2(127.1, 311.7)), dot(p, float2(269.5, 183.3))); return -1.0 2.0 * frac(sin(p) * 43758.5453123); }这个函数做了几件事用点乘将二维坐标映射到两个不同的浮点数增加随机性。使用sin函数和一个大数相乘然后取小数部分(frac)得到一个0-1的伪随机数。将结果从[0,1]映射到[-1,1]方便后续作为梯度向量。注意事项sin函数在GPU上计算并不廉价。在生产环境中为了极致性能可能会使用没有三角函数的哈希例如通过一系列位操作来实现。但对于大多数情况上述方法在可读性和性能之间取得了良好平衡。关键是相同的输入必须产生完全相同的输出这是程序化纹理的基石。3.2 梯度噪声Perlin/Simplex的实现骨架以2D Perlin噪声为例核心步骤包括确定网格单元对于输入点p找到它所在的单位网格的四个角点坐标i0, i1, j0, j1。生成梯度向量对四个角点分别调用哈希函数生成四个二维梯度向量g00, g10, g01, g11。计算距离向量计算点p到四个角点的距离向量d00, d10, d01, d11。计算点积计算每个梯度向量与其对应距离向量的点积n00 dot(g00, d00),n10 dot(g10, d10) 以此类推。这个点积值代表了该角点梯度对p点的影响程度。双线性平滑插值使用平滑曲线如五次曲线对p点在x和y方向上的位置进行插值先对n00和n10在x方向插值得nx0对n01和n11在x方向插值得nx1再对nx0和nx1在y方向进行插值得到最终结果。float perlinNoise(float2 p) { float2 i floor(p); float2 f frac(p); float2 u f * f * (3.0 - 2.0 * f); // 五次平滑曲线 // 获取四个角点的哈希梯度 float2 g00 hash22(i float2(0.0, 0.0)); float2 g10 hash22(i float2(1.0, 0.0)); float2 g01 hash22(i float2(0.0, 1.0)); float2 g11 hash22(i float2(1.0, 1.0)); // 计算距离向量和点积 float n00 dot(g00, f - float2(0.0, 0.0)); float n10 dot(g10, f - float2(1.0, 0.0)); float n01 dot(g01, f - float2(0.0, 1.0)); float n11 dot(g11, f - float2(1.0, 1.0)); // 双线性插值 float nx0 lerp(n00, n10, u.x); float nx1 lerp(n01, n11, u.x); return lerp(nx0, nx1, u.y); }3.3 分形叠加fBm的参数控制单一频率的噪声看起来很单调。分形叠加才是释放其威力的关键。float fbm(float2 p, int octaves, float persistence, float lacunarity) { float value 0.0; float amplitude 1.0; float frequency 1.0; float maxAmplitude 0.0; // 用于后续归一化 for (int i 0; i octaves; i) { value amplitude * perlinNoise(p * frequency); maxAmplitude amplitude; amplitude * persistence; // 振幅衰减 frequency * lacunarity; // 频率倍增 } // 将结果大致归一化到[-1, 1]范围注意这只是近似 return value / maxAmplitude; }参数解读与避坑指南octaves倍频数叠加的层数。层数越多细节越丰富但计算成本线性增加。通常4-8层已经能产生很好的效果。移动端建议不超过4层。persistence持久度常缩写为pers控制每一层振幅的衰减比例。persistence 1意味着高频细节的振幅更小。通常设置在0.3到0.7之间。值越小高频细节越弱整体看起来越平滑。lacunarity间隙度控制每一层频率的倍增比例。通常设置在1.5到3.0之间。值越大相邻两层噪声的尺度差异越大产生的细节越“跳跃”。归一化问题上述代码中的归一化 (value / maxAmplitude) 是一个简单近似能让结果大致落在[-1,1]区间。但严格来说分形噪声的输出范围并不是恒定的尤其是当persistence和lacunarity不是特定值时。如果需要对结果进行严格的阈值判断比如用于地形生成的高度图可能需要根据参数进行更精确的范围估计或者直接接受其非归一化的特性。4. 实战应用场景与Shader片段剖析理论说再多不如看实际怎么用。下面我们剖析几个典型应用场景的Shader实现思路。4.1 动态火焰效果火焰的核心是动态、扭曲、多层次的半透明形状。我们可以用分形噪声驱动一个Alpha遮罩和顶点动画。思路形状基础使用一个经过时间偏移的、低频的Worley噪声F2-F1作为火焰主体的基础形状。Worley噪声的尖锐边缘很适合模拟火苗的顶端。内部细节叠加一层高频、高persistence的Perlin噪声并让其随时间滚动模拟火焰内部翻滚的湍流细节。动态扭曲用另一个低频噪声对采样坐标进行扰动即流场扭曲让火焰的整体形状不是简单平移而是有机地变形。颜色与透明度将最终的噪声值映射到一个从亮黄到暗红的梯度纹理gradient ramp上同时噪声值也直接或间接地控制片元的透明度通常噪声值高的地方更不透明。简化Shader代码片段片元着色器部分float2 uv i.uv; float time _Time.y; // 1. 基础形状 (低频Worley) float2 p1 uv * _BaseFrequency; p1.y - time * _BaseSpeed; // 向上滚动 float baseShape worleyNoise(p1, 1); // 获取F2-F1 // 2. 内部细节 (高频Perlin) float2 p2 uv * _DetailFrequency; p2.x time * _DetailSpeedX; p2.y time * _DetailSpeedY; float detail perlinNoise(p2); // 3. 坐标扭曲 float2 distortion float2( perlinNoise(uv * _DistortFreq time * 0.1) * _DistortAmp, perlinNoise(uv * _DistortFreq time * 0.1 100.0) * _DistortAmp ); uv distortion; // 组合噪声 float finalNoise baseShape * _BaseStrength detail * _DetailStrength; finalNoise saturate((finalNoise 1) * 0.5); // 映射到[0,1] // 4. 颜色和透明度映射 fixed4 col tex2D(_FireRampTex, float2(finalNoise, 0.5)); col.a * finalNoise * _AlphaMultiplier; // 噪声值越高越不透明 return col;4.2 程序化地形生成顶点着色器应用在顶点着色器中使用噪声来动态改变顶点高度是创建广袤地形的经典方法。思路世界空间坐标在顶点着色器中将物体从模型空间变换到世界空间获取顶点的xz坐标作为噪声输入。这确保地形是基于世界位置生成的移动摄像机时地形不会“滑动”。分形噪声作为高度使用fbm函数输入世界xz坐标输出一个高度值。通过调整octaves,persistence,lacunarity你可以控制地形的粗糙度、山峰的陡峭和平原的广阔。应用高度将计算出的高度值乘以一个强度系数_HeightScale加到顶点的世界空间y坐标上然后再变换回裁剪空间。法线重计算顶点位置改变后模型自带的法线信息就失效了。必须在片元着色器中根据相邻顶点的位置差通过差分方法重新计算法线向量用于正确的光照计算。这通常通过计算高度图在x和z方向的偏导数导数来近似得到法线。简化Shader代码片段顶点着色器部分v2f vert (appdata v) { v2f o; // 将顶点变换到世界空间 float3 worldPos mul(unity_ObjectToWorld, v.vertex).xyz; // 基于世界xz坐标计算分形噪声高度 float2 noiseCoord worldPos.xz * _NoiseScale; float height fbm(noiseCoord, _Octaves, _Persistence, _Lacunarity); // 将高度从[-1,1]映射到[0,1]并缩放 height (height * 0.5 0.5) * _HeightScale; // 应用高度偏移 worldPos.y height; // 将修改后的世界坐标变换回裁剪空间 o.vertex mul(UNITY_MATRIX_VP, float4(worldPos, 1.0)); o.worldPos worldPos; // 传递给片元着色器用于计算法线 o.uv v.uv; return o; }在片元着色器中你需要使用ddx和ddy函数或通过采样周围点的方式根据worldPos重新计算法线。4.3 水面波纹与焦散效果模拟动态水面噪声主要用于两个方面法线扰动和光路模拟焦散。法线扰动思路生成法线图对水面网格的UV坐标通常叠加一个随时间偏移采样一张由噪声生成的法线纹理。这张纹理可以由分形Perlin噪声的导数生成或者直接使用一张Tileable的噪声图。多层滚动为了模拟不同尺度波浪的叠加通常会使用两套UV以不同速度和方向滚动两张法线图然后在切线空间下将它们混合。这能打破单一方向滚动带来的重复感。应用扰动将混合后的法线信息从切线空间转换到世界空间然后用于光照计算如Blinn-Phong或反射/折射向量计算。焦散效果简化思路焦散是光线通过不平整水面后在水底形成的明亮光斑。一个取巧的实时做法是在水底或接收焦散投影的物体的Shader中基于其世界坐标采样一个高频、高对比度的噪声图如Worley噪声。对这个噪声图进行强烈的扭曲和滚动模拟光路通过动态水面后的扭曲效果。将处理后的噪声值作为一个亮度叠加additive到水底的漫反射颜色上并使其强度随着水深和视角变化。5. 性能优化与常见问题排查在项目里大规模使用噪声尤其是复杂的分形噪声性能是必须考虑的问题。5.1 性能优化策略表策略具体做法适用场景优缺点烘焙到纹理在编辑器模式下或离线时将复杂的噪声函数计算结果渲染到一张纹理中。运行时只需采样纹理。静态或低频变化的效果如地形高度图、固定的材质纹理。优点运行时开销极低。缺点失去动态性占用显存分辨率固定。降低倍频数减少fbm函数中的octaves参数。所有动态噪声效果尤其是移动端。优点直接降低计算量。缺点细节减少。使用更简单的噪声用Value噪声替代Perlin/Simplex或用2D噪声替代3D噪声。对噪声质量要求不高的背景或次要细节。优点显著提升性能。缺点视觉效果下降。LOD细节层次根据物体与摄像机的距离使用不同复杂度的噪声计算或不同精度的噪声纹理。大型开放世界地形、远景物体。优点平衡整体性能。缺点实现稍复杂可能有视觉突变。简化哈希函数使用位操作等更廉价的伪随机函数。对性能极度敏感的场景。优点提升噪声函数本身性能。缺点可能影响随机性质量。利用Shader Graph Custom Function在Shader Graph中将复杂的噪声计算封装为Custom Function节点便于复用和性能分析。使用Shader Graph的工作流。优点可视化模块化。缺点可能隐藏了底层计算成本。5.2 常见问题与排查技巧实录问题1噪声有明显的重复感或网格感。原因噪声频率设置不当或者使用了单一的、平铺的UV。排查检查噪声的输入坐标。如果直接使用模型UV且模型UV在0-1范围外没有正确平铺就会导致接缝或重复。尝试使用世界空间坐标。检查fbm的lacunarity参数。如果设置为2.0这样的整数可能会产生谐波导致明显的重复模式。尝试使用无理数如2.2或1.7。增加噪声的“种子”。在哈希函数输入中加入一个全局的偏移量可以整体改变噪声图案。问题2分形噪声在移动设备上帧率下降严重。原因octaves太多或者噪声函数本身计算复杂。排查使用Unity的GPU Profiler如RenderDoc定位具体的Shader耗时。确认是噪声计算导致的。逐步降低octaves从8降到4观察效果和性能的平衡。考虑将最高频的1-2层octave替换为一张细节法线贴图用采样代替计算。检查是否在片元着色器中进行了全屏的复杂噪声计算。如果可能尝试在顶点着色器中计算并插值但要注意这会损失细节。问题3使用噪声进行顶点位移后光照法线错误。原因顶点位置改变后没有重新计算法线。排查确保在片元着色器中根据修改后的世界空间位置重新计算了法线。可以使用ddx和ddy函数求取高度场的偏导数来构造法线。// 假设 worldPos.y 是高度 float3 worldNormal; worldNormal.x -ddx(worldPos.y) * _HeightScale; // 注意符号和缩放 worldNormal.z -ddy(worldPos.y) * _HeightScale; worldNormal.y 1.0; // 假设主要偏移在Y轴 worldNormal normalize(worldNormal);或者如果位移是基于高度图的也可以预先计算一张法线贴图在片元着色器中采样。问题4不同材质或物体上的噪声看起来不连贯。原因噪声采样基于的坐标系不统一。例如一个物体用模型UV另一个用世界坐标它们接缝处的噪声就无法对齐。排查对于需要无缝连接的大环境如地形、海面**务必使用世界空间坐标xz**作为噪声输入并确保所有相关物体使用相同的_NoiseScale和_NoiseOffset参数。在Shader中暴露这些参数的全局变量以便在场景中统一调整。问题5Shader编译错误或噪声函数报错。原因Shader语言规范问题或函数未定义。排查确保所有自定义函数如hash22,perlinNoise都在使用之前正确定义。注意HLSL和GLSL的语法差异。如果编写的是Surface Shader或Unlit ShaderUnity会帮你处理大部分转换但在Custom Function或复杂包含中可能出错。避免在Shader中使用循环次数可变的for循环。如果octaves是变量可能导致编译问题或性能低下。通常使用编译时常量如#define OCTAVES 4或展开循环。掌握噪声就像是掌握了Shader世界的“混沌之力”。它从简单的伪随机数开始通过巧妙的插值和叠加最终能模拟出万千世界的复杂与美丽。我个人的体会是开始时不必追求实现所有噪声算法深入理解Perlin和Worley这两种并熟练运用fBm技术就足以应对项目中80%的需求。多动手调节参数观察每一个参数对最终视觉的影响这种手感经验比死记硬背公式更重要。最后永远要在效果和性能之间寻找平衡在移动平台上一张精心烘焙的噪声纹理往往比实时计算8个octaves的Simplex噪声要明智得多。