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

相关推荐
雪儿waii3 小时前
Unity 中的 Quaternion(四元数)详解
unity·游戏引擎
RReality3 小时前
【Unity UGUI】ScrollRect 与 Scrollbar 深度用法
unity·游戏引擎
人邮异步社区3 小时前
如何自学游戏引擎的开发?
unity·程序员·游戏引擎
郝学胜-神的一滴5 小时前
[简化版 Games 101] 计算机图形学 05:二维变换下
c++·unity·图形渲染·three.js·opengl·unreal
mxwin18 小时前
Unity URP 热更新兼容性:Shader 在 IL2CPP 打包下的注意事项
unity·游戏引擎
mxwin1 天前
Unity shader中TransformWorldToShadowCoord原理解析
unity·游戏引擎·shader
mxwin1 天前
Unity Shader 中 ShadowCaster的作用和疑问
unity·游戏引擎
mxwin1 天前
Unity Shader中如何学习阴影技术 产生阴影,接受阴影,联级阴影,软阴影
学习·unity·游戏引擎·shader
♡すぎ♡1 天前
ShaderLab:线条几何体旋转
unity·计算机图形学·着色器·shaderlab