Shader 进阶 - 随机与噪声的艺术
1. 为什么需要随机和噪声?
在前一篇文章中,我们学会了绘制精确的几何图形:圆形、矩形、三角形。但自然界中的事物很少是完美规则的:
- 海浪不是精确的正弦波,而是充满细节的起伏
- 岩石表面不是光滑的平面,而是粗糙不平的纹理
- 云朵不是规则的几何形状,而是有机的、流动的形态
- 树木的生长模式看似随机,实际上遵循某种自然规律
这就是随机和噪声的价值所在!它们让我们能够:
- 创造自然、有机的视觉效果
- 生成无限不重复的程序化纹理
- 构建复杂的地形和材质
- 实现游戏中的粒子、云雾、火焰效果
2. 随机 vs 噪声:核心区别
在开始之前,必须理解这两个概念的本质差异:
| 特性 | 随机 (Random) | 噪声 (Noise) |
|---|---|---|
| 定义 | 完全不可预测的值 | 连续平滑的伪随机值 |
| 视觉效果 | 电视雪花点、像素噪点 | 云朵、大理石纹理 |
| 空间连续性 | 相邻值毫无关联 | 相邻值平滑过渡 |
| 用途 | 粒子初始位置、抖动 | 地形生成、纹理、动画 |
核心区别的可视化:
随机 (Random): 噪声 (Noise):
█ ░█░ ░█ ░░▒▒▓▓██▓▓▒▒░░
░ █░ █ ░ █ ░▒▓███████▓▒░
█ ░█░ █░ ▒▓███████▓▒░
█ ░ █░█ ░ ░▒▓██████▓▒░░
░█ █ ░ █ ░░▒▓▓▓▓▒▒░░
完全离散,像素间无关 平滑渐变,有机连续3. 第一步:在 Shader 中生成伪随机数
3.1 GPU 上的挑战:为什么没有真随机?
在传统编程中,我们有 Math.random()。但在 Shader 中:
float random = ???; // ❌ GLSL 没有内置随机函数!为什么? 因为 GPU 的设计哲学:
- ✅ 确定性:相同输入 → 相同输出(便于并行计算)
- ✅ 可重现性:每一帧渲染结果一致
- ❌ 真随机会破坏这两个特性
解决方案:伪随机函数
我们需要一个函数 f(x),看起来随机,但实际上是确定性的:
输入相同 → 输出相同
输入微小变化 → 输出剧烈变化(看起来"随机")3.2 经典伪随机算法:fract(sin(x) * big_number)
这是 Shader 社区最常用的伪随机函数:
float random(float x) {
return fract(sin(x) * 43758.5453123);
// ─┬── ──┬─ ──────┬────────
// │ │ └─ 大质数(放大微小差异)
// │ └─ 正弦函数(振荡,制造混沌)
// └─ 取小数部分(映射到 0~1)
}数学原理拆解:
3.2.1 步骤 1:sin(x) - 引入周期性混沌
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 - 放大微小差异
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() - 提取小数部分
fract(36813.27) = 0.27 // 丢弃整数部分
fract(38994.40) = 0.40
fract(-123.78) = 0.22 // 注意:负数的 fract 也是正值!原理: fract(x) = x - floor(x) 通过减去向下取整的结果来提取小数部分。
// 正数的情况:
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 可视化测试:这真的随机吗?
让我们用代码验证效果:
#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 伪随机函数
现在我们需要让每个像素都有不同的随机值:
// 🔧 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) 的妙用
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 完整示例:静态噪点效果
#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 进阶技巧:网格化随机
真实应用中,我们经常需要分块的随机值(例如像素风格、马赛克效果):
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:闪烁星空
随机生成星星位置,并让它们闪烁。
// 提示代码框架
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:彩色马赛克
网格化的彩色块效果。
// 提示:
vec2 grid = floor(st * gridSize);
vec3 color = vec3(random(grid), random(grid + 0.1), random(grid + 0.2));练习 3:动态噪点动画
噪点随时间变化。
// 提示:将时间混入随机函数
float noise = random(st + u_time);4. 噪声 (Noise) - 平滑的随机
前面的随机函数产生的是离散的、跳跃的值。但自然界的随机往往是连续的、平滑的。
4.1 噪声的核心需求
想象一下:
- 🏔️ 山脉高度:相邻位置的高度不会突变,而是平滑过渡
- 🌊 海浪:水面高度连续变化,形成波浪
- ☁️ 云朵:密度渐变,不是突然的黑白分界
数学定义:
噪声函数 n(x) 满足:
- 确定性:相同输入 → 相同输出
- 连续性:输入微小变化 → 输出微小变化(可导)
- 无周期性:没有明显重复模式(或周期极长)
- 有界性:输出在固定范围内(通常 0~1 或 -1~1)
4.2 Value Noise(值噪声)- 最简单的噪声
Value Noise 的核心思路:
- 在整数坐标上放置随机值(格点)
- 在格点之间进行平滑插值
可视化过程:
步骤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 实现
// 📐 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 实现(核心!)
// 📐 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.674.3 Value Noise 的视觉效果
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:生成随机梯度向量
// 🎲 为每个格点生成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:计算点到格点的向量并做点积
// 计算网格内某点的影响值
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:双线性插值
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 实现
#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 Noise | Perlin Noise |
|---|---|---|
| 格点数据 | 随机标量值 | 随机梯度向量 |
| 计算复杂度 | ⚡⚡⚡ 简单 | ⚡⚡ 稍复杂 |
| 视觉质量 | ⚠️ 有网格痕迹 | ✅ 自然平滑 |
| 用途 | 快速原型、低质量纹理 | 专业级纹理、地形 |
| 发明者 | 民间算法 | Ken Perlin (奥斯卡奖) |
视觉对比:
Value Noise: Perlin Noise:
╔══╦══╦══╗ ≋≈∼∽∼≈≋∼
║░▒▓█▓▒░║ ∼≋≈∽≈≋∼≈
║▓█ ░▒▓█║ ≈∽∼≋≈∽∼≋
╠══╬══╬══╣ ∽∼≈≋∽∼≈∽
║█▓▒░ ▒▓║ ∼≈≋∽∼≈≋∼
║ ░▒▓█▓░║ ≋∽∼≈≋∽∼≈
╚══╩══╩══╝
明显网格特征 完全有机平滑6. Simplex Noise - Perlin 的升级版
6.1 为什么需要 Simplex Noise?
Ken Perlin 在 2001 年发明 Simplex Noise 来解决 Perlin Noise 的问题:
| 问题 | Perlin Noise | Simplex 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 的实现比较复杂,涉及空间变换和高级数学。如果你是初学者,建议:
- 先熟练掌握 Perlin Noise(2D 应用中已足够)
- 理解 Simplex Noise 的核心思想即可(用三角形代替正方形)
- 把下面的代码当作工具函数直接使用,不必深究每一行
6.2.1 实现代码(可直接使用的库代码)
// 🎲 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 年发明,专门用于生成细胞状、有机的纹理。
核心思路:
- 在空间中随机放置"特征点"(种子点)
- 每个像素的值 = 到最近特征点的距离, Worley Noise 的核心不是随机数,而是 “距离”。
想象这样一个过程:
- 想象一个棋盘被分成很多小格子, 每个格子里随机放一个点(不是固定在格子中心,而是随机一点, 称为 feature points)。
- 现在我们取一个坐标点 (x, y)。
- 我们要问:“这个点距离最近的随机点有多远?”
- 这个距离就是这个点的“噪声值”!
- 如果这个像素就在某个随机点旁边,那距离很小(颜色就很暗)。
- 如果离最近的点很远,那距离就大(颜色就亮)。 👉 用公式写就是:
noise(x, y) = min(distance(x, y, random_points))
7.2 Worley Noise 算法
7.2.1 基础版本(暴力搜索)
#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(细胞边界)
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)
// 将 length(diff) 替换为:
float dist = abs(diff.x) + abs(diff.y); // 曼哈顿距离效果: 更方正的细胞形状
7.3.3 变体 3:切比雪夫距离(Chebyshev Distance)
// 将 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:水彩纸纹理
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:岩石/龟裂纹理
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:细胞生物学可视化
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 叠加技术:分形布朗运动(下一篇预告)
// 预告:下一篇文章将详细讲解
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 混合技巧
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 下一步
下一篇文章《分形与程序化图案》将教你:
- 分形布朗运动 (fBm):叠加多层噪声创造自然纹理
- 域扭曲 (Domain Warping):用噪声扭曲噪声
- 分形几何:科赫曲线、谢尔宾斯基三角
- 程序化图案:瓷砖、迷宫、晶体结构
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 参考资源
继续探索噪声的无限可能吧!下一篇见! ✨