Unity Shader 程序化生成:Shader 中的数学宇宙

在 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 贴图以换取极致性能。

相关推荐
mxwin1 天前
unity shader中 ddx ddy是什么
unity·游戏引擎·shader
郝学胜-神的一滴1 天前
[简化版 GAMES 101] 计算机图形学 08:三角形光栅化上
c++·unity·游戏引擎·godot·图形渲染·opengl·unreal
nnsix1 天前
Unity ILRuntime 笔记
unity·游戏引擎
nnsix1 天前
Unity API 兼容的 .NET Standard 2.1 和 .NET Framework 区别
unity·游戏引擎·.net
mxwin1 天前
Unity Shader 制作半透明物体 使用多Pass提前写入深度的方式 避免穿模
unity·游戏引擎
nnsix1 天前
Unity HybridCLR 笔记
笔记·unity·游戏引擎
nnsix1 天前
Unity Addressables 笔记
unity·游戏引擎
RReality1 天前
【Unity Shader URP】视差贴图 实战教程
ui·平面·unity·游戏引擎·图形渲染·贴图
小清兔2 天前
Addressable的设置打包流程
笔记·游戏·unity·c#
3D霸霸2 天前
Sourcetree 拉取新工程
数据仓库·unity