在 Unity URP Shader 里,完全靠数学算法生成 Simplex/Perlin 噪波、几何网格与无限图案------ 不依赖任何贴图,所有细节从数字里长出来。

01基础数学工具:哈希与伪随机
Shader 里没有 Random(),但我们有无穷的数学技巧。 程序化生成的根基是一个哈希函数------给定相同输入,永远返回同样的"看起来随机"的值。
∑
核心思路
取一个输入坐标,用大质数乘法、位运算或 sin 截断, 把它"打碎"成均匀分布在 [0,1] 的值。 相邻坐标的输出不相关 → 看起来随机。
1.1 最常用的 2D 哈希
这是 Shader 界最流行的 hash21------把 float2 映射到单个 float:
cs
// 把 float2 坐标映射到 [0,1] 的伪随机值
float hash21(float2 p) {
p = frac(p * float2(443.897, 441.423));
p += dot(p, p.yx + 19.19);
return frac((p.x + p.y) * p.x);
}
// 生成 float2 随机向量(用于梯度噪波)
float2 hash22(float2 p) {
float3 a = frac(p.xyx * float3(443.897, 441.423, 437.195));
a += dot(a, a.yxz + 19.19);
return frac(float2(a.x + a.y, a.y + a.z) * a.z);
}
⚠️
精度陷阱
基于 sin 的哈希在手机 GPU(half 精度)上会出现明显条带。 生产代码请优先使用整数位运算版本,或将输入缩放到 [-1, 1] 后再截断。
1.2 整数哈希(移动端友好)
cs
// 整数位运算哈希 --- 移动端 half 精度安全
uint hash1u(uint x) {
x ^= x >> 17;
x *= 0xbf324c81u;
x ^= x >> 11;
x *= 0x9c7f5779u;
x ^= x >> 16;
return x;
}
// float2 → float [0,1],适合移动端
float hashInt21(float2 p) {
uint2 ip = (uint2)(p * 1000.0 + 0.5);
uint h = hash1u(ip.x + hash1u(ip.y));
return (float)h / 4294967295.0; // 2^32 - 1
}

02Value Noise:最简单的噪波
Value Noise 把空间划分成整数格子,在每个格点赋予随机值, 然后在格子内做平滑插值(smoothstep)。简单、快速,是入门噪波。
noise(p) = lerp(lerp(v00, v10, u), lerp(v01, v11, u), v)
其中 u = smoothstep(0,1, frac(p.x)),v = smoothstep(0,1, frac(p.y))
cs
// 五次淡化曲线(比 smoothstep 更平滑)
float fade(float t) {
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
// 2D Value Noise
float valueNoise(float2 p) {
float2 i = floor(p);
float2 f = frac(p);
float2 u = float2(fade(f.x), fade(f.y));
float v00 = hash21(i + float2(0, 0));
float v10 = hash21(i + float2(1, 0));
float v01 = hash21(i + float2(0, 1));
float v11 = hash21(i + float2(1, 1));
return lerp(lerp(v00, v10, u.x),
lerp(v01, v11, u.x), u.y);
}
ℹ️
Smoothstep vs Quintic
用 smoothstep(3t²-2t³)时格子边界处一阶连续但二阶不连续,会产生"网格感"。 升级为五次曲线 t³(6t²-15t+10) 可消除这个伪影,这正是 Perlin 的改进点。
03Perlin Noise:梯度噪波
1985 年 Ken Perlin 为电影《创》(Tron)发明了这个算法。 与 Value Noise 不同,Perlin 在每个格点存储的不是随机值 , 而是随机梯度向量,再用格点到查询点的距离向量与梯度做点积。
gradient(p) = dot(g[floor(p)], frac(p) - [0或1])
Perlin(p) = fade 曲线插值所有相邻格点的 gradient 贡献之和
3.1 2D Perlin Noise 完整 HLSL 实现
cs
// 2D 梯度查找(8 方向简化版)
float gradDot(float2 gradSeed, float2 dist) {
float2 h = hash22(gradSeed) * 2.0 - 1.0;
return dot(h, dist);
}
// 2D Perlin Noise --- 输出 [-1, 1]
float perlinNoise(float2 p) {
float2 i = floor(p);
float2 f = frac(p);
float2 u = float2(fade(f.x), fade(f.y));
float g00 = gradDot(i, f);
float g10 = gradDot(i + float2(1,0), f - float2(1,0));
float g01 = gradDot(i + float2(0,1), f - float2(0,1));
float g11 = gradDot(i + float2(1,1), f - float2(1,1));
return lerp(lerp(g00, g10, u.x),
lerp(g01, g11, u.x), u.y);
}
// 使用:float n = perlinNoise(uv * scale) * 0.5 + 0.5;

✅
梯度为什么更好?
Value Noise 在格点处二阶不连续,视觉上有"方块感"。 梯度噪波的格点值天然为零(梯度向量与零向量点积为 0), 使得噪波在格点平滑过渡,最终产生更自然的有机感。
04Simplex Noise:更快更好的升级版
2001 年 Perlin 本人发明了 Simplex Noise。它把插值格子从超立方体(2D=正方形,3D=立方体) 改成单纯形(2D=三角形,3D=四面体),大幅减少了计算量。
| 特性 | Perlin Noise | Simplex Noise |
|---|---|---|
| 2D 插值点数 | 4 个格点 | 3 个顶点 |
| 3D 插值点数 | 8 个格点 | 4 个顶点 |
| nD 计算复杂度 | O(2ⁿ) | O(n²) |
| 方向性伪影 | 有轻微 | 无明显 |
| 移动端性能 | 中等 | 更快 |
| 版权注意 | 公共领域 | 专利已过期 |
4.1 2D Simplex Noise HLSL
cs
// 2D Simplex Noise(基于 Ian McEwan / Inigo Quilez 版本)
float simplexNoise(float2 v) {
const float4 C = float4(0.211324865405187, // (3-sqrt(3))/6
0.366025403784439, // (sqrt(3)-1)/2
-0.577350269189626, // -1 + 2*C.x
0.024390243902439); // 1/41
float2 i = floor(v + dot(v, C.yy));
float2 x0 = v - i + dot(i, C.xx);
float2 i1 = (x0.x > x0.y) ? float2(1,0) : float2(0,1);
float4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = frac(i * float3(1,1,1).yz + 17.yz); // 卷绕以保证周期性
float3 p = permute(permute(i.y + float3(0,i1.y,1)
+ i.x + float3(0,i1.x,1));
float3 m = max(0.5 - float3(
dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m * m; m = m * m;
float3 x = 2.0 * frac(p * C.www) - 1.0;
float3 h = abs(x) - 0.5;
float3 a0 = x - floor(x + 0.5);
m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
float3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g); // 输出范围约 [-1, 1]
}

05分形布朗运动(fBm)
单层噪波太"圆润",自然界的云、山脉、火焰都有多尺度的细节。 分形布朗运动(Fractional Brownian Motion)通过叠加多个不同频率、 不同振幅的噪波层(称为"八度",octave)来模拟这种特性。
fBm(p) = Σᵢ amplitude_i × noise(p × frequency_i)
frequency_i = lacunarity^i (通常 lacunarity ≈ 2.0)
amplitude_i = gain^i (通常 gain ≈ 0.5,使高频层衰减)
cs
// 旋转矩阵:破坏轴对齐,减少"网格感"伪影
static float2x2 rot = float2x2(1.6, 1.2, -1.2, 1.6);
// 分形布朗运动(fBm)
float fbm(float2 p, int octaves, float lacunarity, float gain) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
for (int i = 0; i < octaves; i++) {
value += amplitude * valueNoise(p * frequency);
frequency *= lacunarity;
amplitude *= gain;
p = mul(rot, p); // 旋转坐标
}
return value;
}
// 典型调用:6 层,lacunarity=2.0,gain=0.5
// float n = fbm(uv * 3.0, 6, 2.0, 0.5);
💡
旋转矩阵破坏轴对齐
每次叠加前旋转采样坐标,可以避免噪波在水平/垂直方向上出现可见的对齐伪影, 让 fBm 看起来更自然有机。开销仅 4 次乘法,性价比极高。
5.1 Ridged fBm(山脊噪波)
对每层噪波取绝对值再翻转 1 - abs(n), 可以产生尖锐的山脊感,适合地形、岩石等效果。
cs
float ridgedFbm(float2 p, int octaves) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
float weight = 1.0;
for (int i = 0; i < octaves; i++) {
float n = valueNoise(p * frequency);
n = 1.0 - abs(n * 2.0 - 1.0); // 翻转→山脊
n *= n; // 尖锐化
n *= weight; // 前一层影响后一层权重
weight = saturate(n * 2.0);
value += n * amplitude;
amplitude *= 0.5;
frequency *= 2.0;
}
return value;
}
06程序化图案:Voronoi / 六边形 / 棋盘
噪波之外,还有一类完全确定性的几何图案。 它们同样不需要贴图,全部来自数学。
6.1 Voronoi(细胞噪波)
把空间分成格子,每格内随机放一个"种子点", 对每个像素找最近的种子,距离就是 Voronoi 值------产生细胞/蜥蜴鳞片般的图案。
cs
// 2D Voronoi --- 返回 float2(minDist, cellId)
float2 voronoi(float2 p) {
float2 n = floor(p);
float2 f = frac(p);
float md = 8.0;
float mid = 0.0;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
float2 g = float2(float(i), float(j));
float2 o = hash22(n + g); // 格内随机偏移
float2 r = g + o - f;
float d = dot(r, r);
if (d < md) { md = d; mid = hash21(n + g); }
}}
return float2(sqrt(md), mid); // .x 距离 .y 细胞 ID
}
6.2 六边形网格
六边形平铺是游戏中最常见的网格之一,可以纯数学实现:
cs
// 六边形网格 --- 返回: .xy=最近中心坐标偏移 .z=到边缘的距离
float3 hexGrid(float2 uv, float scale) {
uv *= scale;
const float2 s = float2(1.7320508, 1.0); // sqrt(3), 1
float4 hC = floor(float4(uv, uv - float2(1,.5)) / s.xyxy) + .5;
float4 h = float4(uv - hC.xy*s, uv - (hC.zw+float2(.5,.5))*s);
return dot(h.xy, h.xy) < dot(h.zw, h.zw)
? float3(h.xy, hC.xy)
: float3(h.zw, hC.zw + float2(.5,.5));
}
// 使用示例
float3 hx = hexGrid(uv, 10.0);
float edge = length(hx.xy); // 到中心距离
float id = hash21(hx.zw); // 每格随机 ID
float wire = step(0.45, edge); // 网格线
6.3 棋盘 / 条纹 / 圆点
cs
// 棋盘格
float checkerboard(float2 uv, float scale) {
float2 i = floor(uv * scale);
return fmod(i.x + i.y, 2.0);
}
// 圆点(抗锯齿)
float dots(float2 uv, float scale, float radius) {
float2 c = frac(uv * scale) - 0.5;
float d = length(c);
return 1.0 - smoothstep(radius - 0.01, radius + 0.01, d);
}
// 条纹
float stripes(float x, float freq, float sharpness) {
float s = sin(x * freq * 6.2832);
return smoothstep(-sharpness, sharpness, s);
}
// 同心圆环
float rings(float2 uv, float2 center, float freq) {
float d = length(uv - center);
return frac(d * freq);
}

07URP Shader Graph 集成
以上所有算法都可以封装成 Custom Function Node, 插入 Shader Graph 拖拽节点流中使用。
7.1 Custom Function Node 写法
cs
// 文件:Assets/Shaders/Includes/SimplexCustomNode.hlsl
// 在 Shader Graph Custom Function Node 中 Source=File
// Function Name = SimplexNoiseNode
// 注意:函数签名需与 Shader Graph 里的端口对应
void SimplexNoiseNode_float(
float2 UV, // In: UV 坐标
float Scale, // In: 缩放
int Octaves, // In: fBm 层数
out float Out // Out: 噪波值 [0,1]
) {
float n = fbm(UV * Scale, Octaves, 2.0, 0.5);
Out = n * 0.5 + 0.5; // 从 [-1,1] 映射到 [0,1]
}
// half 精度重载(移动端)
void SimplexNoiseNode_half(
half2 UV, half Scale, int Octaves, out half Out
) {
SimplexNoiseNode_float((float2)UV, (float)Scale, Octaves, (float)Out);
}

📁
文件引用方式
将 .hlsl 文件放在 Assets/Shaders/Includes/ 目录下, 在 Custom Function Node 的 Source 选择 File, 浏览到该文件。函数名填对应的函数名(不含括号)。
08实战:熔岩流 Shader 完整示例
把前面所有知识合在一起:用 fBm 变形 UV,用 Voronoi 生成细胞裂纹, 用颜色渐变映射温度,最终产生动态熔岩流效果。
cs
Shader "Custom/URP/LavaFlow" {
Properties {
_TimeScale ("Time Scale", Range(0,2)) = 0.3
_FbmScale ("fBm Scale", Range(1,20)) = 5.0
_LavaColor1 ("Lava Hot", Color) = (1, 0.2, 0, 1)
_LavaColor2 ("Lava Cool", Color) = (0.05, 0.01, 0, 1)
_CrackWidth ("Crack Width", Range(0,0.3)) = 0.1
_Emission ("Emission", Range(0,5)) = 2.0
}
SubShader {
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
Pass {
Name "ForwardLit"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Assets/Shaders/Includes/ProceduralNoise.hlsl"
struct Attributes { float4 posOS : POSITION; float2 uv : TEXCOORD0; };
struct Varyings { float4 posCS : SV_POSITION; float2 uv : TEXCOORD0; };
CBUFFER_START(UnityPerMaterial)
float _TimeScale, _FbmScale, _CrackWidth, _Emission;
float4 _LavaColor1, _LavaColor2;
CBUFFER_END
Varyings vert(Attributes IN) {
Varyings OUT;
OUT.posCS = TransformObjectToHClip(IN.posOS.xyz);
OUT.uv = IN.uv;
return OUT;
}
half4 frag(Varyings IN) : SV_Target {
float2 uv = IN.uv;
float t = _Time.y * _TimeScale;
// Step 1: fBm 扭曲 UV,产生流动感
float2 q;
q.x = fbm(uv + float2(0, t), 5, 2.0, 0.5);
q.y = fbm(uv + float2(5.2, t * 1.3), 5, 2.0, 0.5);
float2 warpedUV = uv * _FbmScale + 4.0 * q;
// Step 2: Voronoi 产生熔岩裂缝
float2 vor = voronoi(warpedUV);
float cracks = smoothstep(0, _CrackWidth, vor.x);
// Step 3: fBm 提供温度场
float heat = fbm(warpedUV + t * 0.3, 4, 2.1, 0.45);
heat = pow(heat, 1.5);
// Step 4: 颜色映射
half3 col = lerp(_LavaColor2.rgb, _LavaColor1.rgb, heat);
col *= cracks; // 裂缝处变暗
col += pow(heat, 3.0) * _LavaColor1.rgb * _Emission; // 自发光
return half4(col, 1.0);
}
ENDHLSL
}
}
}
🚀
性能建议
在移动端限制 fBm 层数为 3-4 层;将 Voronoi 的邻居搜索范围从 3×3 缩减到 2×2; 对静止部分使用 LOD Bias;考虑把噪波预烘焙到 R16 贴图以换取极致性能。