Skip to content

Shader 进阶 - 随机与噪声的艺术 ​

1. 为什么需要随机和噪声? ​

在前一篇文章中,我们学会了绘制精确的几何图形:圆形、矩形、三角形。但自然界中的事物很少是完美规则的:

  • 海浪不是精确的正弦波,而是充满细节的起伏
  • 岩石表面不是光滑的平面,而是粗糙不平的纹理
  • 云朵不是规则的几何形状,而是有机的、流动的形态
  • 树木的生长模式看似随机,实际上遵循某种自然规律

这就是随机和噪声的价值所在!它们让我们能够:

  • 创造自然、有机的视觉效果
  • 生成无限不重复的程序化纹理
  • 构建复杂的地形和材质
  • 实现游戏中的粒子、云雾、火焰效果

2. 随机 vs 噪声:核心区别 ​

在开始之前,必须理解这两个概念的本质差异:

特性随机 (Random)噪声 (Noise)
定义完全不可预测的值连续平滑的伪随机值
视觉效果电视雪花点、像素噪点云朵、大理石纹理
空间连续性相邻值毫无关联相邻值平滑过渡
用途粒子初始位置、抖动地形生成、纹理、动画

核心区别的可视化:

随机 (Random):              噪声 (Noise):
  █ ░█░ ░█               ░░▒▒▓▓██▓▓▒▒░░
 ░ █░ █ ░ █              ░▒▓███████▓▒░
█  ░█░  █░               ▒▓███████▓▒░
  █ ░ █░█ ░              ░▒▓██████▓▒░░
 ░█  █ ░  █              ░░▒▓▓▓▓▒▒░░

完全离散,像素间无关      平滑渐变,有机连续

3. 第一步:在 Shader 中生成伪随机数 ​

3.1 GPU 上的挑战:为什么没有真随机? ​

在传统编程中,我们有 Math.random()。但在 Shader 中:

glsl
float random = ???;  // ❌ GLSL 没有内置随机函数!

为什么? 因为 GPU 的设计哲学:

  • ✅ 确定性:相同输入 → 相同输出(便于并行计算)
  • ✅ 可重现性:每一帧渲染结果一致
  • ❌ 真随机会破坏这两个特性

解决方案:伪随机函数

我们需要一个函数 f(x),看起来随机,但实际上是确定性的:

输入相同 → 输出相同
输入微小变化 → 输出剧烈变化(看起来"随机")

3.2 经典伪随机算法:fract(sin(x) * big_number) ​

这是 Shader 社区最常用的伪随机函数:

glsl
float random(float x) {
    return fract(sin(x) * 43758.5453123);
    //     ─┬──  ──┬─  ──────┬────────
    //      │     │          └─ 大质数(放大微小差异)
    //      │     └─ 正弦函数(振荡,制造混沌)
    //      └─ 取小数部分(映射到 0~1)
}

数学原理拆解:

3.2.1 步骤 1:sin(x) - 引入周期性混沌 ​

glsl
sin(1.0)  = 0.8414...
sin(1.1)  = 0.8912...
sin(10.0) = -0.5440...
sin(10.1) = -0.5063...

正弦函数在不同输入下产生 -1 ~ 1 的振荡值,看似有规律,但:

  • 🔄 周期性:sin(x + 2π) = sin(x)
  • 🌀 非线性:输入的小变化会导致输出的非线性变化

3.2.2 步骤 2:* 43758.5453123 - 放大微小差异 ​

glsl
sin(1.0) * 43758.5453 = 36813.27...
sin(1.1) * 43758.5453 = 38994.40...
//                      ──┬───
//                        └─ 小数部分完全不同!

乘以一个大质数后,正弦函数的微小差异被极度放大,使得小数部分看起来毫无规律。

为什么选 43758.5453123?

  • ✅ 这是一个精心挑选的"魔法数字"
  • ✅ 质数性质使得周期性被打破
  • ✅ 小数部分进一步增加随机性
  • ⚠️ 并非唯一选择,你也可以用其他大质数(如 12.9898, 78.233 等)

3.2.3 步骤 3:fract() - 提取小数部分 ​

glsl
fract(36813.27) = 0.27  // 丢弃整数部分
fract(38994.40) = 0.40
fract(-123.78) = 0.22   // 注意:负数的 fract 也是正值!

原理: fract(x) = x - floor(x) 通过减去向下取整的结果来提取小数部分。

glsl
// 正数的情况:
fract(123.78) = 123.78 - floor(123.78) = 123.78 - 123.0 = 0.78

// 负数的情况:
fract(-123.78) = -123.78 - floor(-123.78) = -123.78 - (-124.0) = 0.22

这样,所有值都被映射到 [0, 1) 范围,得到看似随机的均匀分布。

3.3 可视化测试:这真的随机吗? ​

让我们用代码验证效果:

glsl
#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;

// 📐 1D 随机函数
float random(float x) {
    return fract(sin(x) * 43758.5453123);
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    
    // 🎨 方法1:基于 X 坐标的随机值
    float r = random(st.x);
    
    // 每个垂直列都有相同的颜色(因为输入相同)
    gl_FragColor = vec4(vec3(r), 1.0);
}

效果: 垂直的彩色条纹(每列颜色不同,但同一列内相同)

3.4 升级:2D 伪随机函数 ​

现在我们需要让每个像素都有不同的随机值:

glsl
// 🔧 1D 随机函数
float random(float x) {
    return fract(sin(x) * 43758.5453123);
}

// 🔧 2D 随机函数(核心!)
float random(vec2 st) {
    // 关键:将 2D 坐标"混合"成 1D 值
    return fract(sin(dot(st, vec2(12.9898, 78.233)))  // 点积混合 XY
                 * 43758.5453123);
}

数学原理:点积 (Dot Product) 的妙用

glsl
dot(vec2(x, y), vec2(12.9898, 78.233)) = x * 12.9898 + y * 78.233

点积将 2D 坐标 (x, y) 压缩成一个标量,并且:

  • ✅ 唯一性:不同的 (x, y) → 不同的点积结果
  • ✅ 混合性:x 和 y 的权重不同(避免对称性)
  • ✅ 非线性:通过特殊系数打破规律

为什么是 12.9898 和 78.233?

  • 这是经典的 Gold Noise 算法的系数
  • 经过大量测试,分布均匀,周期性弱
  • 社区验证过的"魔法数字"

3.5 完整示例:静态噪点效果 ​

glsl
#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform float u_time;

// 📐 2D 随机函数
float random(vec2 st) {
    return fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453123);
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    
    // 🎲 方案1:每个像素随机灰度
    float r = random(st);
    
    // 🎨 方案2:随机 RGB(三个独立的随机值)
    vec3 color = vec3(
        random(st),
        random(st + 0.1),  // 偏移输入,得到不同的随机值
        random(st + 0.2)
    );
    
    // 🔄 方案3:动画噪点(每帧不同)
    // color = vec3(random(st + u_time));
    
    gl_FragColor = vec4(color, 1.0);
}

效果分析:

  • 方案1:黑白静态噪点(电视无信号效果)
  • 方案2:彩色静态噪点
  • 方案3:闪烁的动画噪点

3.6 进阶技巧:网格化随机 ​

真实应用中,我们经常需要分块的随机值(例如像素风格、马赛克效果):

glsl
uniform vec2 u_resolution;

float random(vec2 st) {
    return fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453123);
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    
    // 🎯 关键:将连续坐标离散化
    float gridSize = 20.0;  // 网格大小(越大块越大)
    
    st *= gridSize;         // 放大坐标
    st = floor(st);         // 向下取整(相邻像素变成相同整数)
    
    // 现在同一个"格子"内的所有像素都有相同的 st 值
    float r = random(st);
    
    gl_FragColor = vec4(vec3(r), 1.0);
}

可视化效果:

gridSize = 1.0           gridSize = 10.0          gridSize = 50.0
每像素不同               10x10 像素块              50x50 像素块
████▓▓▒▒░░              ████████░░░░            ████████████████
██▓▓▓▒▒░░░              ████████░░░░            ████████████████
▓▓▓▓▒▒▒░░░              ▒▒▒▒▒▒▒▒▓▓▓▓            ░░░░░░░░░░░░░░░░
▒▒▒▒▒░░░░░              ▒▒▒▒▒▒▒▒▓▓▓▓            ░░░░░░░░░░░░░░░░

3.7 随机函数的质量评估 ​

一个好的伪随机函数应该具备:

特性含义测试方法
均匀分布0~1 范围内概率相等直方图统计
低相关性相邻值独立自相关函数检测
高周期性重复周期长长序列测试
快速计算GPU 友好性能测试

我们的 fract(sin(dot(...))) 函数:

  • ✅ 分布较均匀(虽不完美)
  • ✅ 计算快速(仅基础数学运算)
  • ⚠️ 周期性存在(但在视觉应用中可接受)
  • ❌ 密码学上不安全(但 Shader 不需要)

3.8 实践练习 ​

尝试实现这些效果来巩固所学知识:

练习 1:闪烁星空

随机生成星星位置,并让它们闪烁。

glsl
// 提示代码框架
vec2 grid = floor(st * 100.0);  // 100x100 网格
float starSeed = random(grid);
float isStar = step(0.98, starSeed);  // 只有 2% 的位置有星星
float twinkle = 0.5 + 0.5 * sin(u_time + random(grid) * 6.28);  // 闪烁

练习 2:彩色马赛克

网格化的彩色块效果。

glsl
// 提示:
vec2 grid = floor(st * gridSize);
vec3 color = vec3(random(grid), random(grid + 0.1), random(grid + 0.2));

练习 3:动态噪点动画

噪点随时间变化。

glsl
// 提示:将时间混入随机函数
float noise = random(st + u_time);

4. 噪声 (Noise) - 平滑的随机 ​

前面的随机函数产生的是离散的、跳跃的值。但自然界的随机往往是连续的、平滑的。

4.1 噪声的核心需求 ​

想象一下:

  • 🏔️ 山脉高度:相邻位置的高度不会突变,而是平滑过渡
  • 🌊 海浪:水面高度连续变化,形成波浪
  • ☁️ 云朵:密度渐变,不是突然的黑白分界

数学定义:

噪声函数 n(x) 满足:

  1. 确定性:相同输入 → 相同输出
  2. 连续性:输入微小变化 → 输出微小变化(可导)
  3. 无周期性:没有明显重复模式(或周期极长)
  4. 有界性:输出在固定范围内(通常 0~1 或 -1~1)

4.2 Value Noise(值噪声)- 最简单的噪声 ​

Value Noise 的核心思路:

  1. 在整数坐标上放置随机值(格点)
  2. 在格点之间进行平滑插值

可视化过程:

步骤1: 在整数点生成随机值
   0    1    2    3    4
   ●────●────●────●────●
  0.2  0.8  0.3  0.9  0.1

步骤2: 在中间点插值
   0   0.5   1   1.5   2
   ●────○────●────○────●
  0.2  0.5  0.8  0.55 0.3
       ↑         ↑
      插值      插值

步骤3: 连接成平滑曲线
    ╱‾╲
   ╱    ╲   ╱‾╲
  ╱      ╲ ╱   ╲╱

4.2.1 1D Value Noise 实现 ​

glsl
// 📐 1D 随机函数
float random(float x) {
    return fract(sin(x) * 43758.5453123);
}

// 🌊 1D Value Noise
float valueNoise(float x) {
    // 1️⃣ 找到 x 两侧的整数格点
    float i = floor(x);  // 左侧格点(例如 x=2.7 → i=2.0)
    float f = fract(x);  // 小数部分(0.7)
    
    // 2️⃣ 获取两个格点的随机值
    float a = random(i);       // 左格点的随机值
    float b = random(i + 1.0); // 右格点的随机值
    
    // 3️⃣ 在两个值之间插值
    // return mix(a, b, f);  // ❌ 线性插值(会有棱角)
    
    // ✅ 平滑插值(Smoothstep)
    float u = f * f * (3.0 - 2.0 * f);  // Hermite 插值
    return mix(a, b, u);
}

// 🎨 使用示例
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution;
    
    float x = st.x * 5.0;  // 扩展到 0~5 范围(5个周期)
    float n = valueNoise(x);
    
    // 绘制噪声曲线
    float y = st.y;
    float line = smoothstep(n - 0.02, n, y) - smoothstep(n, n + 0.02, y);
    
    vec3 color = vec3(n);  // 或者 vec3(line) 显示曲线
    gl_FragColor = vec4(color, 1.0);
}

插值方法对比:

插值类型公式视觉效果连续性
线性插值mix(a, b, f)⚠️ 有明显棱角C0(值连续)
平滑插值f² × (3 - 2f)✅ 平滑过渡C1(导数连续)
更平滑插值f³ × (10 - 15f + 6f²)✨ 超级平滑C2(二阶导连续)
线性插值:          平滑插值:          超平滑插值:
  ╱╲╱╲             ╱‾╲╱‾╲            ╱~~╲╱~~╲
 ╱  ╲  ╲          ╱     ╲          ╱      ╲
╱    ╲  ╲        ╱       ╲        ╱        ╲
     有棱角          平滑              非常平滑

4.2.2 2D Value Noise 实现(核心!) ​

glsl
// 📐 2D 随机函数
float random(vec2 st) {
    return fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453123);
}

// 🌊 2D Value Noise
float valueNoise(vec2 st) {
    // 1️⃣ 找到当前点所在的网格
    vec2 i = floor(st);  // 网格左下角整数坐标
    vec2 f = fract(st);  // 网格内的小数位置 (0~1)
    
    // 2️⃣ 获取网格四个角的随机值
    //   c -- d
    //   |    |
    //   a -- b
    float a = random(i);                   // 左下
    float b = random(i + vec2(1.0, 0.0));  // 右下
    float c = random(i + vec2(0.0, 1.0));  // 左上
    float d = random(i + vec2(1.0, 1.0));  // 右上
    
    // 3️⃣ 平滑插值曲线(Hermite 插值)
    vec2 u = f * f * (3.0 - 2.0 * f);
    // 或者使用内置函数:vec2 u = smoothstep(0.0, 1.0, f);
    
    // 4️⃣ 双线性插值
    // 先在 X 方向插值(底边和顶边)
    float bottomEdge = mix(a, b, u.x);  // 底边插值
    float topEdge = mix(c, d, u.x);     // 顶边插值
    
    // 再在 Y 方向插值(最终结果)
    return mix(bottomEdge, topEdge, u.y);
}

// 🎨 完整示例
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    st *= 5.0;  // 缩放到 5x5(显示多个噪声周期)
    
    float n = valueNoise(st);
    
    vec3 color = vec3(n);
    gl_FragColor = vec4(color, 1.0);
}

双线性插值可视化:

网格示意(f = (0.6, 0.3)):

  c(0.2)────────────d(0.9)
    │                │
    │       ●P       │  ← P 点在网格内的位置
    │   (f.x, f.y)   │
    │                │
  a(0.5)────────────b(0.8)

计算过程:
1. 底边插值: mix(a, b, u.x) = mix(0.5, 0.8, smooth(0.6)) = 0.69
2. 顶边插值: mix(c, d, u.x) = mix(0.2, 0.9, smooth(0.6)) = 0.64
3. 最终插值: mix(0.69, 0.64, smooth(0.3)) = 0.67

结果:P 点的噪声值 = 0.67

4.3 Value Noise 的视觉效果 ​

glsl
uniform vec2 u_resolution;
uniform float u_time;

float random(vec2 st) {
    return fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453123);
}

float valueNoise(vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);
    
    float a = random(i);
    float b = random(i + vec2(1.0, 0.0));
    float c = random(i + vec2(0.0, 1.0));
    float d = random(i + vec2(1.0, 1.0));
    
    vec2 u = f * f * (3.0 - 2.0 * f);
    
    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    
    // 🎨 效果1:静态云朵纹理
    vec2 pos = st * 5.0;
    float n = valueNoise(pos);
    
    // 🎨 效果2:动画云朵(随时间流动)
    // pos.x += u_time * 0.2;
    // float n = valueNoise(pos);
    
    // 🎨 效果3:增强对比度
    // n = pow(n, 2.0);  // 暗部更暗
    
    // 🎨 效果4:二值化(云朵形状)
    // n = step(0.5, n);
    
    vec3 color = vec3(n);
    gl_FragColor = vec4(color, 1.0);
}

4.4 Value Noise 的问题 ​

虽然 Value Noise 简单易懂,但它有一个致命缺陷:

问题:网格特征明显(Grid Artifacts)

   看起来像这样:
   ╔═══╦═══╦═══╗
   ║ ░▒║▓█ ║░  ║  ← 能看出明显的格子边界
   ║▒▓ ║ ░▒║▓  ║
   ╠═══╬═══╬═══╣
   ║█▓ ║  ░║▒▓ ║
   ║ ░ ║▒▓█║  ░║
   ╚═══╩═══╩═══╝

原因: 插值仅基于格点的值,没有考虑方向性。

解决方案: 使用 Gradient Noise(梯度噪声),最著名的是 Perlin Noise!


5. Perlin Noise - 业界标准噪声 ​

5.1 什么是 Perlin Noise? ​

Perlin Noise 由 Ken Perlin 在 1983 年发明,专门为电影《创战纪》(Tron) 开发,并因此获得奥斯卡技术成就奖!

核心改进: 不是在格点存储随机值,而是存储随机梯度向量。

Value Noise:               Perlin Noise:
格点存储标量值             格点存储向量

  0.5●────●0.8              ↗●────●↘
      │    │                 │    │
  0.2●────●0.9              ↖●────●↙

5.2 Perlin Noise 算法原理 ​

5.2.1 步骤拆解 ​

步骤1:生成随机梯度向量

glsl
// 🎲 为每个格点生成2D随机梯度向量
vec2 randomGradient(vec2 ip) {
    // 使用格点坐标生成伪随机角度
    float random = fract(sin(dot(ip, vec2(12.9898, 78.233))) * 43758.5453123);
    float angle = random * 6.28318530718;  // 0 ~ 2π
    
    // 转换为单位向量
    return vec2(cos(angle), sin(angle));
}

步骤2:计算点到格点的向量并做点积

glsl
// 计算网格内某点的影响值
float dotGridGradient(vec2 ip, vec2 fp) {
    vec2 gradient = randomGradient(ip);  // 格点的随机梯度
    vec2 distance = fp - ip;             // 从格点指向当前点的向量
    return dot(gradient, distance);      // 点积(投影)
}

为什么用点积?

点积 dot(gradient, distance) 的几何意义:

  • 梯度方向 = 噪声"增长最快"的方向
  • 距离向量 = 从格点到采样点的偏移
  • 点积结果 = 采样点沿梯度方向的投影长度
梯度向量指向↗:            梯度向量指向↙:
    gradient                gradient
       ↗                       ↙
      ●────→ distance         ●────→ distance
   格点   采样点            格点   采样点

  dot > 0 (同向)           dot < 0 (反向)
  噪声值较高               噪声值较低

步骤3:双线性插值

glsl
float perlinNoise(vec2 st) {
    // 1️⃣ 网格坐标
    vec2 i = floor(st);  // 整数部分
    vec2 f = fract(st);  // 小数部分
    
    // 2️⃣ 计算四个角的影响值(点积)
    float a = dotGridGradient(i + vec2(0.0, 0.0), st);  // 左下
    float b = dotGridGradient(i + vec2(1.0, 0.0), st);  // 右下
    float c = dotGridGradient(i + vec2(0.0, 1.0), st);  // 左上
    float d = dotGridGradient(i + vec2(1.0, 1.0), st);  // 右上
    
    // 3️⃣ 平滑插值曲线
    vec2 u = f * f * (3.0 - 2.0 * f);
    
    // 4️⃣ 双线性插值
    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

5.3 完整 Perlin Noise 实现 ​

glsl
#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform float u_time;

// 🎲 生成2D随机梯度向量
vec2 randomGradient(vec2 ip) {
    // 为了更好的随机性,使用两个不同的随机数
    float random = fract(sin(dot(ip, vec2(12.9898, 78.233))) * 43758.5453123);
    float angle = random * 6.28318530718;
    return vec2(cos(angle), sin(angle));
}

// 📐 计算格点梯度的点积
float dotGridGradient(vec2 ip, vec2 fp) {
    vec2 gradient = randomGradient(ip);
    vec2 distance = fp - ip;
    return dot(gradient, distance);
}

// 🌊 2D Perlin Noise 主函数
float perlinNoise(vec2 st) {
    vec2 i = floor(st);
    vec2 f = fract(st);
    
    // 四个角的点积值
    float a = dotGridGradient(i + vec2(0.0, 0.0), st);
    float b = dotGridGradient(i + vec2(1.0, 0.0), st);
    float c = dotGridGradient(i + vec2(0.0, 1.0), st);
    float d = dotGridGradient(i + vec2(1.0, 1.0), st);
    
    // 平滑插值
    vec2 u = f * f * (3.0 - 2.0 * f);
    
    // 双线性插值
    float result = mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
    
    // 💡 值域映射说明:
    // 理论上 2D Perlin Noise 的范围约为 [-√2/2, √2/2] ≈ [-0.707, 0.707]
    // 但实际统计分布主要集中在 [-0.5, 0.5] 左右
    // 这里映射到 [0, 1] 以便显示(黑白灰度)
    return result * 0.5 + 0.5;
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    st *= 8.0;  // 缩放(显示更多细节)
    
    float n = perlinNoise(st);
    
    vec3 color = vec3(n);
    gl_FragColor = vec4(color, 1.0);
}

5.4 Value Noise vs Perlin Noise 对比 ​

特性Value NoisePerlin Noise
格点数据随机标量值随机梯度向量
计算复杂度⚡⚡⚡ 简单⚡⚡ 稍复杂
视觉质量⚠️ 有网格痕迹✅ 自然平滑
用途快速原型、低质量纹理专业级纹理、地形
发明者民间算法Ken Perlin (奥斯卡奖)

视觉对比:

Value Noise:           Perlin Noise:
╔══╦══╦══╗            ≋≈∼∽∼≈≋∼
║░▒▓█▓▒░║            ∼≋≈∽≈≋∼≈
║▓█ ░▒▓█║            ≈∽∼≋≈∽∼≋
╠══╬══╬══╣            ∽∼≈≋∽∼≈∽
║█▓▒░ ▒▓║            ∼≈≋∽∼≈≋∼
║ ░▒▓█▓░║            ≋∽∼≈≋∽∼≈
╚══╩══╩══╝            

明显网格特征          完全有机平滑

6. Simplex Noise - Perlin 的升级版 ​

6.1 为什么需要 Simplex Noise? ​

Ken Perlin 在 2001 年发明 Simplex Noise 来解决 Perlin Noise 的问题:

问题Perlin NoiseSimplex Noise
3D+ 性能❌ 指数级复杂度✅ 线性复杂度
方向性偏差⚠️ 对角线明显✅ 各向同性
采样点数2^N 个(2D=4, 3D=8, 4D=16)N+1 个(2D=3, 3D=4, 4D=5)

核心改进: 使用单纯形网格(三角形/四面体)代替正方形网格。

Perlin (正方形):        Simplex (三角形):
┌─────┬─────┐            ╱╲  ╱╲  ╱╲
│  ●  │  ●  │           ╱  ╲╱  ╲╱  ╲
├─────┼─────┤          ╱  ● ╲  ● ╲  ╲
│  ●  │  ●  │         ╱╲  ╱╲  ╱╲  ╱╲
└─────┴─────┘        ╱  ╲╱  ╲╱  ╲╱  ╲

需要采样 4 个点         只需采样 3 个点

6.2 Simplex Noise 实现 ​

⚠️ 对初学者的说明:

Simplex Noise 的实现比较复杂,涉及空间变换和高级数学。如果你是初学者,建议:

  1. 先熟练掌握 Perlin Noise(2D 应用中已足够)
  2. 理解 Simplex Noise 的核心思想即可(用三角形代替正方形)
  3. 把下面的代码当作工具函数直接使用,不必深究每一行

6.2.1 实现代码(可直接使用的库代码) ​

glsl
// 🎲 2D Simplex Noise(优化版实现)
// 基于 Ian McEwan 的无需查找表的算法
// 这是经过优化的"库代码",理解原理即可

// 辅助函数:模运算和置换
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }

float simplexNoise(vec2 v) {
    // 魔法常数(空间变换参数)
    const vec4 C = vec4(
        0.211324865405187,   // (3.0-sqrt(3.0))/6.0
        0.366025403784439,   // 0.5*(sqrt(3.0)-1.0)
        -0.577350269189626,  // -1.0 + 2.0 * C.x
        0.024390243902439    // 1.0 / 41.0
    );
    
    // 步骤1: 将正方形空间倾斜变换到三角形空间
    vec2 i  = floor(v + dot(v, C.yy));
    vec2 x0 = v - i + dot(i, C.xx);
    
    // 步骤2: 确定当前点在哪个三角形内(上三角还是下三角)
    vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
    vec4 x12 = x0.xyxy + C.xxzz;
    x12.xy -= i1;
    
    // 步骤3: 为三角形的三个顶点生成梯度
    i = mod289(i);
    vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0))
                   + i.x + vec3(0.0, i1.x, 1.0));
    
    // 步骤4: 计算距离衰减权重
    vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
    m = m * m;
    m = m * m;
    
    // 步骤5: 生成梯度向量并计算点积
    vec3 x = 2.0 * fract(p * C.www) - 1.0;
    vec3 h = abs(x) - 0.5;
    vec3 ox = floor(x + 0.5);
    vec3 a0 = x - ox;
    
    // 归一化梯度(优化版,避免使用 sqrt)
    m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
    
    // 步骤6: 累加三个顶点的贡献
    vec3 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);
}

// 🎨 使用示例
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    st *= 5.0;
    
    float n = simplexNoise(st);
    n = n * 0.5 + 0.5;  // 映射到 [0, 1]
    
    vec3 color = vec3(n);
    gl_FragColor = vec4(color, 1.0);
}

6.2.2 算法核心思想(原理解读) ​

如果你想理解算法,关键在于这几个步骤:

1. 空间变换

正方形网格 → 倾斜变换 → 三角形网格

通过坐标变换,将输入空间从正方形网格转换到三角形网格。

2. 确定三角形

判断采样点在哪个三角形内(斜对角线分割)

3. 计算三个顶点的贡献

距离越近,贡献越大(使用径向衰减函数)
每个顶点有随机梯度向量

4. 加权累加

最终噪声值 = 三个顶点贡献的加权和

6.2.3 实用建议 ​

对于大多数 2D 应用,Perlin Noise 已经足够。Simplex Noise 的优势主要体现在:

  • 3D 及以上维度(性能提升显著,复杂度从 O(2^N) 降到 O(N))
  • 需要极致的各向同性(没有方向性偏差)
  • 大规模实时应用(如游戏地形生成)

7. Worley Noise(细胞噪声)- 自然的蜂窝结构 ​

7.1 什么是 Worley Noise? ​

Worley Noise(也称 Cellular Noise 或 Voronoi Noise)由 Steven Worley 在 1996 年发明,专门用于生成细胞状、有机的纹理。

核心思路:

  1. 在空间中随机放置"特征点"(种子点)
  2. 每个像素的值 = 到最近特征点的距离, Worley Noise 的核心不是随机数,而是 “距离”。

想象这样一个过程:

  1. 想象一个棋盘被分成很多小格子, 每个格子里随机放一个点(不是固定在格子中心,而是随机一点, 称为 feature points)。
  2. 现在我们取一个坐标点 (x, y)。
  3. 我们要问:“这个点距离最近的随机点有多远?”
  4. 这个距离就是这个点的“噪声值”!
    • 如果这个像素就在某个随机点旁边,那距离很小(颜色就很暗)。
    • 如果离最近的点很远,那距离就大(颜色就亮)。 👉 用公式写就是:noise(x, y) = min(distance(x, y, random_points))

7.2 Worley Noise 算法 ​

7.2.1 基础版本(暴力搜索) ​

glsl
#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;

// 📐 2D 随机函数(返回 vec2)
vec2 random2(vec2 st) {
    st = vec2(
        dot(st, vec2(127.1, 311.7)),
        dot(st, vec2(269.5, 183.3))
    );
    return fract(sin(st) * 43758.5453123);
}

// 🍯 Worley Noise(基础版)
float worleyNoise(vec2 st) {
    // 1️⃣ 确定当前点所在的网格
    vec2 iSt = floor(st);
    vec2 fSt = fract(st);
    
    float minDist = 1.0;  // 到最近特征点的距离
    
    // 2️⃣ 遍历周围 3x3 = 9 个网格
    for(int y = -1; y <= 1; y++) {
        for(int x = -1; x <= 1; x++) {
            // 邻居网格坐标
            vec2 neighbor = vec2(float(x), float(y));
            
            // 该网格内的随机特征点位置
            vec2 point = random2(iSt + neighbor);
            
            // 特征点在世界空间的实际位置
            vec2 diff = neighbor + point - fSt;
            
            // 计算距离
            float dist = length(diff);
            
            // 更新最小距离
            minDist = min(minDist, dist);
        }
    }
    
    return minDist;
}

void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    st *= 5.0;  // 缩放到 5x5 网格
    
    float n = worleyNoise(st);
    
    vec3 color = vec3(n);
    gl_FragColor = vec4(color, 1.0);
}

可视化网格搜索:

当前点 P 在中心网格:

  ┌───┬───┬───┐
  │ ● │ ● │ ● │  ← 周围9个网格的特征点
  ├───┼───┼───┤
  │ ● │ P │ ● │  ← 计算 P 到所有9个点的距离
  ├───┼───┼───┤
  │ ● │ ● │ ● │
  └───┴───┴───┘

只保留最小距离 → 得到噪声值

7.3 Worley Noise 的变体 ​

Worley Noise 的魅力在于它的多样性!

7.3.1 变体 1:F2 - F1(细胞边界) ​

glsl
float worleyNoiseEdge(vec2 st) {
    vec2 iSt = floor(st);
    vec2 fSt = fract(st);
    
    float minDist1 = 1.0;  // 最近距离
    float minDist2 = 1.0;  // 第二近距离
    
    for(int y = -1; y <= 1; y++) {
        for(int x = -1; x <= 1; x++) {
            vec2 neighbor = vec2(float(x), float(y));
            vec2 point = random2(iSt + neighbor);
            vec2 diff = neighbor + point - fSt;
            float dist = length(diff);
            
            // 更新最近和第二近距离
            if(dist < minDist1) {
                minDist2 = minDist1;
                minDist1 = dist;
            } else if(dist < minDist2) {
                minDist2 = dist;
            }
        }
    }
    
    // ✨ 关键:第二近 - 最近 = 细胞边界!
    return minDist2 - minDist1;
}

效果: 显示出清晰的细胞边界(类似蜂巢、龟裂)

7.3.2 变体 2:曼哈顿距离(Manhattan Distance) ​

glsl
// 将 length(diff) 替换为:
float dist = abs(diff.x) + abs(diff.y);  // 曼哈顿距离

效果: 更方正的细胞形状

7.3.3 变体 3:切比雪夫距离(Chebyshev Distance) ​

glsl
// 将 length(diff) 替换为:
float dist = max(abs(diff.x), abs(diff.y));  // 切比雪夫距离

效果: 正方形细胞

7.4 距离度量对比 ​

距离类型公式细胞形状用途
欧几里得length(d)圆形自然纹理、泡沫
曼哈顿abs(d.x) + abs(d.y)菱形像素艺术、晶体
切比雪夫max(abs(d.x), abs(d.y))正方形棋盘、瓷砖

7.5 Worley Noise 应用示例 ​

7.5.1 应用 1:水彩纸纹理 ​

glsl
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    st *= 10.0;
    
    float cells = worleyNoise(st);
    
    // 模拟纸张纤维
    cells = 1.0 - cells;  // 反转
    cells = pow(cells, 0.5);  // 提亮
    
    vec3 color = vec3(0.95, 0.92, 0.88) * cells;  // 米黄色纸张
    gl_FragColor = vec4(color, 1.0);
}

7.5.2 应用 2:岩石/龟裂纹理 ​

glsl
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    st *= 8.0;
    
    float cracks = worleyNoiseEdge(st);  // F2 - F1
    cracks = smoothstep(0.0, 0.1, cracks);  // 增强裂缝
    
    vec3 rockColor = vec3(0.4, 0.35, 0.3);
    vec3 crackColor = vec3(0.1, 0.08, 0.05);
    vec3 color = mix(crackColor, rockColor, cracks);
    
    gl_FragColor = vec4(color, 1.0);
}

7.5.3 应用 3:细胞生物学可视化 ​

glsl
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    st *= 6.0;
    
    float cells = worleyNoise(st);
    
    // 细胞核心(暗)→ 细胞膜(亮)
    vec3 coreColor = vec3(0.8, 0.2, 0.3);    // 红色核心
    vec3 membraneColor = vec3(0.2, 0.8, 0.9);  // 青色膜
    
    vec3 color = mix(coreColor, membraneColor, cells);
    gl_FragColor = vec4(color, 1.0);
}

8. 组合噪声 - 1+1>2 的魔法 ​

单一噪声函数虽然有用,但组合多种噪声才能创造出真正复杂的纹理!

8.1 叠加技术:分形布朗运动(下一篇预告) ​

glsl
// 预告:下一篇文章将详细讲解
float fbm(vec2 st) {
    float value = 0.0;
    float amplitude = 0.5;
    
    // 叠加多个频率的噪声
    for(int i = 0; i < 6; i++) {
        value += amplitude * perlinNoise(st);
        st *= 2.0;        // 频率翻倍
        amplitude *= 0.5;  // 振幅减半
    }
    
    return value;
}

8.2 混合技巧 ​

glsl
void main() {
    vec2 st = gl_FragCoord.xy / u_resolution.xy;
    st *= 5.0;
    
    // 🌊 Perlin 噪声(大尺度起伏)
    float perlin = perlinNoise(st);
    
    // 🍯 Worley 噪声(细节纹理)
    float worley = worleyNoise(st * 3.0);
    
    // 🎨 组合方式1:加法(叠加)
    float combined1 = perlin * 0.7 + worley * 0.3;
    
    // 🎨 组合方式2:乘法(调制)
    float combined2 = perlin * worley;
    
    // 🎨 组合方式3:用一个噪声控制另一个噪声的位置(域扭曲)
    vec2 distorted = st + vec2(perlin * 0.5);
    float combined3 = worleyNoise(distorted);
    
    vec3 color = vec3(combined3);
    gl_FragColor = vec4(color, 1.0);
}

9. 本章总结 ​

恭喜你掌握了 Shader 噪声的核心知识!

9.1 你学到了什么 ​

随机部分:

  • GPU 上生成伪随机数的原理(fract(sin(dot(...))))
  • 1D 和 2D 随机函数的实现
  • 网格化随机(像素风格)

噪声部分:

  • Value Noise:基于格点值的插值
  • Perlin Noise:基于梯度向量的噪声(业界标准)
  • Simplex Noise:Perlin 的高维优化版本
  • Worley Noise:基于距离场的细胞噪声

核心概念:

  • 插值的重要性(线性 vs 平滑)
  • 点积在噪声生成中的作用
  • 不同距离度量产生不同视觉效果

9.2 下一步 ​

下一篇文章《分形与程序化图案》将教你:

  1. 分形布朗运动 (fBm):叠加多层噪声创造自然纹理
  2. 域扭曲 (Domain Warping):用噪声扭曲噪声
  3. 分形几何:科赫曲线、谢尔宾斯基三角
  4. 程序化图案:瓷砖、迷宫、晶体结构

9.3 推荐练习 ​

尝试实现这些效果来巩固知识:

练习 1:移动的云朵

  • 使用 Perlin Noise 生成基础纹理
  • 加入 u_time 让云朵流动
  • 用 smoothstep 控制云朵密度和边缘

练习 2:水彩纸纹理

  • 组合 Worley Noise(纤维结构)和 Perlin Noise(整体变化)
  • 添加微妙的颜色变化
  • 调整对比度模拟纸张质感

练习 3:星空效果

  • 用随机函数生成星星位置(网格 + 阈值)
  • 用 Perlin Noise 生成星云背景
  • 添加基于 sin(u_time) 的闪烁动画

练习 4:大理石纹理

  • Perlin Noise 作为基础扰动
  • 用 sin(perlinNoise * frequency) 创造条纹
  • 应用域扭曲(用噪声偏移采样位置)增加自然感

9.4 参考资源 ​

继续探索噪声的无限可能吧!下一篇见! ✨