历史由来
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函数
常规的三次插值权重,也就是SmoothStep,它满足一阶导数相同,也就是两端0和1时一阶导数都为0,这也是为什么平滑,但是二阶导数不相等,所有依旧有瑕疵
所以这里采用了 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)
也就是
所以最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
);
}