历史由来

Perlin 噪声的来源1983 年 Ken Perlin 为电影 TRON 发明,1985 年发表论文 An Image Synthesizer,后来凭此获得奥斯卡技术奖。

核心问题是:怎么生成一个看起来自然、连续、不重复的随机场?

为什么不直接用随机数直接对每个像素生成随机数,因为结果是纯噪点——相邻像素之间没有任何关联,看起来像电视雪花,完全不自然。

纯随机:  0.3  0.9  0.1  0.7  0.2  0.8  ...  (相邻值毫无关联)
Perlin:  0.3  0.4  0.5  0.6  0.5  0.4  ...  (相邻值平滑过渡)

自然界的东西都是连续变化的,需要相邻位置之间平滑过渡。

Perlin 的思路(3D)

把空间分成格子,在每个格子顶点上放一个随机梯度向量(方向随机的单位向量),然后对格子内部做插值。

三个步骤

第一步:格子顶点分配梯度向量

2D 情况下,每个格子有 4 个顶点,每个顶点一个随机方向的箭头:

↗ ─────── ↙
│         │
│    p    │
│         │
↘ ─────── ↑

3D 情况下则是立方体,每个格子有 8 个顶点,原理一样。

第二步:计算梯度

对于格子内的查询点 p,计算它到每个顶点的偏移向量或者说就是对角线向量,然后和该顶点的随机方向向量做点积得到随机的梯度,因为点积其实这里是在做投影,理解成投影也没问题:

顶点 A 的梯度向量:gradient_A = (0.7, 0.7)
p 到顶点 A 的偏移:offset_A   = p - A
点积:dot(gradient_A, offset_A)

点积的结果:

  • p 在梯度方向上 → 正值
  • p 在梯度反方向上 → 负值
  • p 垂直于梯度 → 零

第三步:插值

简单的线性插值权重计算如下,就是0~1,对应就是(A + (B - A) * t )lerp函数

f(t)=tf(t) = t

常规的三次插值权重,也就是SmoothStep,它满足一阶导数相同,也就是两端0和1时一阶导数都为0,这也是为什么平滑,但是二阶导数不相等,所有依旧有瑕疵

f(t)=3t2−2t3f(t) = 3t² - 2t³

所以这里采用了 5 次曲线,这里对应有6个条件刚好对应5次曲线的6个未知数,同时满足二阶导数相同。 要找一个 5 次多项式 f(t) = at⁵ + bt⁴ + ct³ + dt² + et + f,满足:

f(0) = 0
f(1) = 1
f'(0) = 0   (一阶导数)
f'(1) = 0
f''(0) = 0  (二阶导数)
f''(1) = 0

这样解方程组得到:

a = 6, b = -15, c = 10, d = 0, e = 0, f = 0

所以:

f(t) = 6t⁵ - 15t⁴ + 10t³
     = t³(6t² - 15t + 10)
     = t³(t(6t - 15) + 10)

也就是

f(t)=t3(t(6t−15)+10)f(t) = t³(t(6t - 15) + 10)

所以最fade函数:

// fade 函数:普通线性插值在边界处有明显棱角
// Ken Perlin 用这个 5次曲线 替代线性插值
float fade(float t)
{
    return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}

这条曲线在 t=0 和 t=1 处一阶和二阶导数都是0,过渡极其平滑。


第四步:计算梯度

和 Worley 的对比

|Worley|Perlin| |---|---|---| |每格存的东西|一个随机位置|每个顶点一个随机方向| |输出|到最近点的距离|梯度场的插值| |值域|[0, 1]|[-1, 1]| |视觉|气泡/细胞状|平滑波浪状|

Perlin代码

float perlin(float3 p, float tileCount)
{
    // 第一步:分格子
    float3 cell  = floor(p);
    float3 local = frac(p);

    // 第二步:fade 函数平滑插值权重
    float3 u = local * local * local * (local * (local * 6.0 - 15.0) + 10.0);

    // 8个顶点都取模, 无缝处理
    float3 c000 = fmod(cell + float3(0,0,0) + tileCount, tileCount);
    float3 c100 = fmod(cell + float3(1,0,0) + tileCount, tileCount);
    float3 c010 = fmod(cell + float3(0,1,0) + tileCount, tileCount);
    float3 c110 = fmod(cell + float3(1,1,0) + tileCount, tileCount);
    float3 c001 = fmod(cell + float3(0,0,1) + tileCount, tileCount);
    float3 c101 = fmod(cell + float3(1,0,1) + tileCount, tileCount);
    float3 c011 = fmod(cell + float3(0,1,1) + tileCount, tileCount);
    float3 c111 = fmod(cell + float3(1,1,1) + tileCount, tileCount);

    // 第三步:8个顶点各算一个梯度点积
    // 立方体格子的顶点(计算随机反向),拿对角线向量进行投影得到随机梯度
    float v000 = dot(normalize(hash3(c000) * 2.0 - 1.0), local - float3(0,0,0));
    float v100 = dot(normalize(hash3(c100) * 2.0 - 1.0), local - float3(1,0,0));
    float v010 = dot(normalize(hash3(c010) * 2.0 - 1.0), local - float3(0,1,0));
    float v110 = dot(normalize(hash3(c110) * 2.0 - 1.0), local - float3(1,1,0));
    float v001 = dot(normalize(hash3(c001) * 2.0 - 1.0), local - float3(0,0,1));
    float v101 = dot(normalize(hash3(c101) * 2.0 - 1.0), local - float3(1,0,1));
    float v011 = dot(normalize(hash3(c011) * 2.0 - 1.0), local - float3(0,1,1));
    float v111 = dot(normalize(hash3(c111) * 2.0 - 1.0), local - float3(1,1,1));

    // 第四步:三线性插值,用 fade 后的 u 做权重
    float result = lerp(lerp(lerp(v000, v100, u.x), lerp(v010, v110, u.x), u.y), lerp(lerp(v001, v101, u.x), lerp(v011, v111, u.x), u.y), u.z);

    // 输出范围 [-1, 1] → remap 到 [0, 1]
    return result * 0.5 + 0.5;
}

Perlin(2D)

float PerlinNoise(float2 p)
{
    float2 i = floor(p);
    float2 f = frac(p);
    float2 u = f * f * (3.0 - 2.0 * f);

    return lerp(
        lerp(dot(hash2(i + float2(0,0)), f - float2(0,0)),
             dot(hash2(i + float2(1,0)), f - float2(1,0)), u.x),
        lerp(dot(hash2(i + float2(0,1)), f - float2(0,1)),
             dot(hash2(i + float2(1,1)), f - float2(1,1)), u.x),
        u.y
    );
}