GPU Gems 2 · Chapter 26. Implementing Improved Perlin Noise

把 improved Perlin noise 的 permutation hash、gradient dot、quintic fade 与八角点插值精确搬到 GPU,并用 packed lookup 降低纹理读取。

学习目标

  • 能解释 Implementing Improved Perlin Noise 如何从整数格点生成可重复、范围有界且空间平滑的三维信号
  • 能实现与 CPU reference 一致的 permutation hash、gradient dot、quintic fade 和八角点三轴插值,并把查表映射到 GPU texture
  • 能回答:当噪声出现方格接缝、短周期重复或读取成本过高时,应检查哪项采样/fade/lookup 契约,并如何证明优化没有改变数值

从“每次都一样的自然起伏”开始

石头、烟雾和水面都需要不规则细节,但逐点独立掷骰会产生刺眼颗粒,手工绘制又难以覆盖无限空间。我们需要一种函数:同一位置永远返回同一结果,邻近位置缓慢变化,移动观察时也不会突然换图案。

若只把一小块随机体积反复铺开,边界和周期很快暴露;若每次真的生成随机数,帧与帧又无法复现。更稳妥的办法是让整数格点提供确定方向,再用平滑权重重建格点之间的值。猜一猜:把实验从 quintic 切到 cubic,颜色切片仍连续,但哪一级导数会先暴露格子?

Improved Perlin Noise GPU 实验

猜一猜:切到 coarse 3D volume 或 cubic fade 后,纹理读取会更便宜,但 cell 边界与 CPU parity 会发生什么?

improved noise slice · C² improved noisepositive / negative gradient contributions across a deterministic z sliceimplementation recordpathproceduralfadequinticinstructions53texture reads9CPU parityexactsame table + math as reference固定 permutation 保证可重复;fade 决定边界连续性;packed textures 只改变成本,不改数值

packed permutation 与 permuted gradients 将读取从 22 降到 9,同时保持 CPU reference 数值。

本章的 Implementing Improved Perlin Noise 不是“生成一张看起来随机的图”,而是精确实现一条确定数据流:坐标定位 cell,hash 选择梯度,角点点积产生局部贡献,平滑权重完成插值。

noise 不是逐像素掷骰:随机感必须受空间结构约束small 3D volume texturelinear filtering · short repeat periodprocedural improved noisedeterministic · bounded · smooth程序路径用更多 arithmetic 换更少 texture memory、精确 CPU parity 与可扩展 4D 输入

1. 程序计算与 3D volume 的取舍

合格的 noise 应具备五个工程性质:相同输入可重复;输出范围已知,通常落在约 [1,1][-1,1];空间频率带限且平滑;没有明显短周期图案;平移输入不会改变统计频率。它不是 white noise,而是受空间结构约束的伪随机函数。

预计算 3D texture 的优点是硬件采样便宜,缺点是占用体积内存、周期由 texture size 限制,hardware linear filtering 质量低于 improved interpolation,也无法直接表达四维动画输入。程序路径不保存完整噪声体积,只保存小型索引数据;代价是 shader 自己完成 hash、dot 与 interpolation。

2. 用 permutation 把晶格坐标变成 hash

对输入位置 p=(x,y,z)p=(x,y,z),先分离整数 cell 与局部坐标:

P=pmod256,q=pp.P=\lfloor p\rfloor\bmod 256,\qquad q=p-\lfloor p\rfloor.

这个式子在说:PP 决定当前 256 周期晶格 cell,qq 是查询点在该 cell 内的零到一位置。CPU 常把 permutation array 复制一遍以避免越界;GPU 版本可把 256 项放进 1D texture,并使用 wrap addressing。

integer lattice → permutation hash → gradient dot productcell coordinateP = floor(p)Px mod 256then + Pythen + Pzperm texture · 256point sample · wrapsame table as CPUhash0…255repeatablenot random stategradient16-entrytextureg · Δppermutation 与 gradient 都必须 point sample;线性过滤会把离散索引语义破坏掉

三维 hash 不是把 (x,y,z)(x,y,z) 直接拼接,而是逐轴嵌套:先查 perm(P.x),加 P.y 后再查一次,最后加 P.z 再查。这使相邻晶格角得到看似无规律、却完全可重复的索引。为了保持“离散表”语义,permutation sampler 必须 point filter、wrap U、关闭 mipmapping。

float perm(float index) {
  return texture(permTexture, index / 256.0).r * 256.0;
}
 
float a = perm(cell.x) + cell.y;
float aa = perm(a) + cell.z;
float ab = perm(a + 1.0) + cell.z;
float b = perm(cell.x + 1.0) + cell.y;
float ba = perm(b) + cell.z;
float bb = perm(b + 1.0) + cell.z;

3. gradient texture 与角点贡献

reference CPU 用 hash 的低位通过 bit manipulation 选择 gradient。原章所面对的 pixel shader 缺少所需 integer operations,因此把 16 个编码项预计算进小型 texture。表中存在重复方向,它们保持与 reference hash-to-gradient mapping 一致;不能自行“去重”后仍声称 exact match。

每个角点 cijkc_{ijk} 的贡献为

nijk=g ⁣(hijk)(q(i,j,k)),i,j,k{0,1}.n_{ijk}=g\!\left(h_{ijk}\right)\cdot\left(q-(i,j,k)\right), \qquad i,j,k\in\{0,1\}.

这个式子在说:hash 只负责选择方向 gg;真正随位置连续变化的是该方向与“角点到查询点”位移的 dot product。查询点周围共有八个角,因此每次三维求值要构造八个 contribution。

八个 corner contribution,三层 lerp 得到一个 noise valueg0g1g2g3g4g5g6g7blend order8 × dot(g, Δp)4 × lerp · fade.x2 × lerp · fade.y1 × lerp · fade.zhash 只选择梯度;连续变化来自局部位移点积与 fade-weighted interpolation

gradient texture 也必须 point sample。若格式会 normalize signed vectors,应核对编码与 shader 解码;若使用低精度 signed texture,还要把 quantization 纳入 CPU/GPU tolerance。最可靠的 fixture 包含八个角点、cell center、靠近边界的正负坐标和标准 permutation 的固定输出。

4. quintic fade 与三轴插值

原始 cubic 权重 3t22t33t^2-2t^3 在端点让一阶导数为零,但二阶导数跨 cell 跳变。improved algorithm 使用

f(t)=6t515t4+10t3.f(t)=6t^5-15t^4+10t^3.

这个式子在说:f(0)=0f(0)=0f(1)=1f(1)=1,并且两端的一阶、二阶导数均为零,所以相邻 cell 拼接时达到 C2C^2 连续。对 bump、normal 或任何 derivative-based effect,这能减少晶格边界显影。

quintic fade:改善的是晶格边界的导数连续性t = 0t = 1cubic 3t²−2t³quintic 6t⁵−15t⁴+10t³endpoint contractf(0)=0 · f(1)=1f′(0)=f′(1)=0quintic onlyf″(0)=f″(1)=0C² 连续让 noise 的二阶变化跨 cell 平滑,尤其影响 derivative-based shading

求出 f(qx),f(qy),f(qz)f(q_x),f(q_y),f(q_z) 后,先沿 x 把八个 corner contributions 混成四个,再沿 y 混成两个,最后沿 z 得到一个 noise value。这里不能把 quintic weights 交给普通 trilinear texture filtering,因为硬件过滤只使用局部线性权重。

vec3 w = fade(localPosition);
float z0 = mix(
  mix(c000, c100, w.x),
  mix(c010, c110, w.x),
  w.y
);
float z1 = mix(
  mix(c001, c101, w.x),
  mix(c011, c111, w.x),
  w.y
);
return mix(z0, z1, w.z);

5. packed lookup:把重复工作搬到纹理生成期

直接移植 reference 的优点是容易核对,代价是 81 条 Pixel Shader 2.0 instructions,其中 22 次 texture lookups。主要浪费来自递归 permutation reads,以及八个 gradient 读取前重复的最终 permutation lookup。

把重复 hash 组合离线打包,减少 dependent texture readsstraight CPU port6 recursive perm reads8 final gradient readsother table accesses81 instructions · 22 TEXpacked GPU lookup256×256 RGBA perm texturefour hashes / one lookup256-entry permuted gradientsremove final perm lookup53 instructions · 9 TEX优化保持同一 permutation 和 gradients,因此减少读取而不改变 reference output

第一项优化是构建 256×256256\times256 RGBA texture:每个 texel 预先保存四个相关 hash,一次 2D read 替代多次递归 1D reads。第二项是把 gradient table 扩成 256 项,并按 permutation 预先重排,从而去掉 gradient 前的最终 hash lookup。

优化后原章报告 53 条 instructions、9 次 texture lookups。这个变化只重排“在哪里算”,不应改变 permutation、gradient 或 fade;因此必须用同一批 CPU fixtures 做 bit-exact 或约定容差比较。只看纹理形状相似,无法证明优化保持语义。

6. 精确一致性、成本与四维扩展

程序 noise 的价值不只在视觉质量。相同表与数学允许 CPU authoring、offline baking 和 GPU preview 共享一套结果;四维输入还能把时间作为额外坐标,让三维体积平滑动画,而不依赖当时不存在的 4D texture hardware。

但 exact 仍需定义浮点边界:负坐标的 floor 与 modulo、texture coordinate center、normalized format 解码、compiler reassociation 都可能造成差异。测试应覆盖整数点、边界两侧、远离原点、周期 256 前后和四维时间切片,并记录比较是 bitwise 还是 epsilon-based。

约 50 条 pixel instructions 对早期实时应用仍然昂贵;原章将其定位为高质量、offline-like rendering 和 CPU parity 重要的场景。今天硬件更强,但“以 arithmetic 换 memory、以确定性换预烘焙体积”的决策仍需要根据采样次数、cache、目标分辨率与材质预算测量。

三步验证:先正确,再连续,最后优化

三阶段共享同一组 coordinate fixtures 和标准表。每完成一步,都保存 hash、gradient、corner contribution 和最终 noise,确保下一步出现差异时能定位到具体层级。

分步1 / 3

第一步:对齐 permutation 与 gradients

在 CPU 和直接 GPU port 上查询整数、负数和 255/256 边界。先比较 nested hash,再比较 16-entry gradient mapping,确保 point/wrap sampler 没有插值或半 texel 偏移。

integer lattice → permutation hash → gradient dot productcell coordinateP = floor(p)Px mod 256then + Pythen + Pzperm texture · 256point sample · wrapsame table as CPUhash0…255repeatablenot random stategradient16-entrytextureg · Δppermutation 与 gradient 都必须 point sample;线性过滤会把离散索引语义破坏掉

本章小结

  • gradient noise 用确定梯度与平滑插值制造可重复自然变化。
  • permutation 与 gradient texture 必须保持 reference 顺序并 point sample。
  • 八个 lattice corners 先点积,再按 quintic fade 沿三轴混合。
  • packed lookup 把 22 次 texture reads 降到 9 次而不应改变输出。
  • CPU/GPU parity 必须覆盖负坐标、边界、周期和 4D 切片。

练习

问题 1|手算数据流。 输入 p=(3.25,7.5,9.75)p=(3.25,7.5,9.75) 时,整数 cell PP、局部坐标 qq 和三轴 fade 分别如何得到?

问题 2|定位网格接缝。 shader 已经声明 quintic fade(),但 bump lighting 仍在整数边界出现线条。应按什么顺序检查?

问题 3|修改实验验证优化。 将 ImprovedNoiseLab 切到 direct port,再切回 packed lookup。除了读取数变化,还应保存哪些证据才能发布?

名词解释

本章出现的专业名词,用大白话再讲一遍。

gradient noise
permutation table
gradient texture
quintic fade
lattice corner
packed lookup texture

资料与写作方式声明

本章以GPU Gems 2 · Chapter 26. Implementing Improved Perlin Noise权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…