细胞噪声:离我最近的那个点
Cellular Noise
另一种噪声
上一课的噪声是在格点上放随机值,再平滑地插值。1996 年,Steven Worley 发表了《A Cellular Texture Basis Function》,换了一个完全不同的出发点:在空间里撒一把点,每个像素只关心离自己最近的那一个点有多远。
书里特意标了时间:这比 Perlin 最初的噪声晚了大约十六年,又比他的 Simplex Noise(2001)早五年。这种噪声叫细胞噪声 (cellular noise),也叫 Worley 噪声。
先用笨办法
最直接的写法:对每个像素,和每一个点都算一次距离,留下最小的那个。
float minDist = 1.0;
for (int i = 0; i < 5; i++) {
minDist = min(minDist, distance(st, pts[i]));
}
也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。
这个办法有两个硬伤:
- GLSL 的循环上界必须是编译期常量,没法在运行时增减点的数量;
- 每个像素都要和所有点比一遍,点一多,开销就跟着点数线性增长。
所以不能把「所有的点」都塞给每一个像素。
格子:每格一个点,只看邻居
Worley 的关键想法用的还是前几课那一套 floor 和 fract:
- 把空间放大并切成格子:
i_st = floor(st),f_st = fract(st); - 用格子编号生成随机数,在每一格里放一个随机的特征点:
random2(i_st); - 离当前像素最近的点,一定在自己这一格或者周围 8 格里。所以只需要比 9 个点。
核心是一个双重循环,x 和 y 都从 −1 走到 1:
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
vec2 neighbor = vec2(float(x), float(y)); // 邻居格子的偏移
vec2 point = random2(i_st + neighbor); // 那一格里的点(格内坐标)
vec2 diff = neighbor + point - f_st; // 从像素指向那个点
m_dist = min(m_dist, length(diff));
}
}
也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。
换个角度看:每个点都在向外「生长」,直到撞上邻居的生长前沿。这很像自然界里的生长:向外扩张,又被外力限制住。
Voronoi:不只要多远,还要「是谁」
细胞噪声输出的是一个距离。如果再记住最近的那个点是谁,就能把平面分成一块块区域,每块归离它最近的那个点管。这就是 Georgy Voronoi 提出的划分,也叫 Voronoi 图。
代码只要多存一样东西。这时不能再只写 min:找到更近的点时,除了更新距离,还要顺手记下它的身份:
if (dist < m_dist) {
m_dist = dist;
m_cell = cell; // 记住最近的点属于哪一格
}
也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。
拿它来做点什么
细胞噪声的距离本身就很好用。把每一块的颜色交给格子编号,再让离点越远越暗,一块块中间鼓、边缘陷下去的鹅卵石就出来了:
也可以直接改右边的代码:停手约 0.3 秒会自动重新运行;改坏了点「还原代码」。
后来的改进
- Gustavson(2011):把邻域从 3 × 3 缩到 2 × 2,工作量大减,代价是格子边界上可能出现不连续。
- Quilez(2012):讲了怎么画出精确的 Voronoi 边界。
- voro-noise(2014):在「插值随机值或梯度」(噪声)和「到最近点的距离」(Voronoi)之间找到一种更一般的度量,两者都成了同一个格子生成器的特例,可以连续地从一个过渡到另一个。
在游戏里怎么用
取舍与常见的坑
小结与练习
- 细胞噪声 = 每个像素到最近特征点的距离。
- 把空间切成格子、每格放一个点,最近的点一定在 3 × 3 的邻域里,所以每个像素只比 9 次。
- 记住最近的点是谁,就得到 Voronoi 图;距离和身份一起用,能做出石块、细胞、裂纹。
练习:做一块龟裂的干泥地:用 Voronoi 的身份给每块泥土一个略有不同的颜色,再用距离把每块的边缘压暗,最后用上一课的噪声给整块地面加一点起伏。
Steven Worley(1996)《A Cellular Texture Basis Function》——书中注明约在 Perlin 原始噪声发表十六年之后、其 Simplex Noise(2001)五年之前——把程序纹理建立在「到最近特征点的距离」上,即细胞噪声 / Worley 噪声。GPU 上用格子 + 邻域搜索代替「对全体点求最小距离」;再多存最近特征点身份即可得到 Voronoi 图。后续有 2×2 邻域加速、精确边界与「voro-noise」等变体。
距离场视角
对每个像素,维护到一组特征点的最小距离:朴素写法是对每个点 min_dist = min(min_dist, distance(st, p))。用数组 + for 循环更干净,但 GLSL 有硬约束:
- 循环上界必须是编译期常量,不能动态增减点数;
- 点数一大,片元成本线性爆掉。
因此不能把「全体点」塞进每个像素线程。
格子化:每格一个点,查 3×3 邻域
Worley 的关键思想(与图案/随机/噪声章同一套 floor/fract):
- 放大空间并分格:
i_st = floor(st),f_st = fract(st); - 用整数格坐标经
random2(i_st)在格内生成一个随机特征点; - 当前像素只与本格 + 周围 8 格(共 9 个)的点比距离;
- 双重循环
y,x ∈ {-1,0,1},point = random2(i_st + neighbor),
diff = neighbor + point - f_st,m_dist = min(m_dist, length(diff))。
Íñigo Quilez 强调的精度技巧:随机点生成与距离计算放到格局部坐标,丢掉大整数「世界坐标」,避免远离原点时的精度灾难;整颗行星尺度的 Voronoi 也可先用双精度做 floor/fract,其余仍用单精度。同一技巧也适用于 Perlin 类噪声,但少见文档化。
从「点」的视角:每个点向外生长直到撞上邻居的生长前沿——即自然中的扩张与外力限制;经典对应是 Georgy Voronoi 的划分。
Voronoi:不止距离,还要「是谁」
细胞噪声常输出标量距离场;Voronoi 图还需要最近特征点的身份。用 vec2 m_point 存指向最近点的向量(或点位置)作标识:当 dist < m_dist 时同时更新距离与 m_point(不能只用 min,因为要附带副作用)。颜色可按最近点位置着色,鼠标绑定的一格会随位置变色。
规模化时仍走 Worley 格子法。原论文每格特征点数可变以便 CPU 早停;GLSL 循环不可变长,教学实现通常每格固定一点。
改进与变体
- Gustavson(2011):邻域从 3×3 收到 2×2,工作量大减,但格边可能不连续。
- Quilez(2012):如何画精确 Voronoi 边。
- voro-noise(2014):在「插值/平均随机值或梯度」(噪声)与「到最近特征点距离」(Voronoi)之间找更一般度量,使二者成为同一格子生成器的特例,可连续混合。
与本库距离场页的分工
- 隐式曲面与有符号距离函数:一般 SDF、布尔、光线步进语境;
- 二维 SDF 形状:圆/矩形等解析形;
- 本页:特征点集上的 Worley/Voronoi 程序纹理与 GPU 格子算法。
在游戏开发中的意义
- 石材裂纹、生物组织、岛链、城市地块、程序化破碎;
- 运行时:每像素 9 次(或 4 次)哈希+距离,贵于单次 Perlin,常烘焙或降频;
- 取舍:2×2 加速要接受边伪影;动态增删特征点不适合纯片元循环,需换计算着色器或预计算点表。
工具箱:LYGIA generative。
原书问题
- ch.12 多处将 Íñigo Quilez 写作「Inigo's Quilez / Inigo Quilez」(缺重音);链接仍指向 iquilezles.org。本页统一为 Íñigo Quilez。