Shader 进阶 - 分形与程序化图案的奥秘
1. 什么是分形?为什么它如此重要?
在上一篇文章中,我们学习了如何生成单一层次的噪声。但观察自然界,你会发现一个惊人的规律:
自相似性 (Self-Similarity) - 无论你放大多少倍,结构看起来都是相似的!
1.1 先理解一个问题:为什么单层噪声不够用?
想象你在用画笔画一座山:
- 只画粗略轮廓:看起来像个三角形,太假
- 只画细小颗粒:看不出整体形状,一团乱麻
- 正确做法:先画大轮廓,再画中等细节,最后加小颗粒
这就是分形的本质 - 将不同尺度的细节组合在一起!
看看这些例子:
| 自然现象 | 分形特征 |
|---|---|
| 🌳 树木 | 树枝的结构和整棵树的形状相似 |
| ⛰️ 山脉 | 小山丘和大山脉的轮廓形状一致 |
| ☁️ 云朵 | 云的边缘在任何尺度都是复杂的 |
| 🌊 海岸线 | 无论多近看,海岸线都充满细节 |
| ❄️ 雪花 | 六边形的每个分支都有相似的结构 |
这就是分形 (Fractal) 的本质 - 在不同尺度上重复相似的模式。
1.2 分形在 Shader 中的价值
问题: 单层噪声看起来太"光滑"、太"人工"
单层 Perlin Noise: 多层分形噪声:
~~~~~~ ~∿~∾~∿~∾~
~~~~~~~ ∿~∾~∿~∾~∿~
~~~~~~~~ ~∾~∿~∾~∿~∾
平滑但单调 细节丰富、自然解决方案: 叠加多个频率的噪声,就像自然界一样!
2. 分形布朗运动 (Fractal Brownian Motion, fBm)
2.1 数学原理
fBm 的核心思想: 将不同频率和振幅的噪声叠加起来
💡 通俗理解:就像音乐的和弦 - 低音提供基础(低频),中音增加旋律(中频),高音增加细节(高频),三者叠加才动听!
数学公式:
fBm(x) = Σ(i=0 to octaves) amplitude(i) × noise(frequency(i) × x)
其中:
- frequency(i) = initial_frequency × lacunarity^i (频率指数增长)
- amplitude(i) = initial_amplitude × gain^i (振幅指数衰减)为什么要指数变化?
- 频率指数增长:每层细节是上一层的2倍密集 → 像素级细节
- 振幅指数衰减:每层影响力减半 → 避免高频噪声"喧宾夺主"
参数解析:
| 参数 | 含义 | 典型值 | 效果 |
|---|---|---|---|
| octaves | 叠加层数 | 4-8 | 越多越细致,但性能下降 |
| lacunarity | 频率倍增系数 | 2.0 | 每层频率的增长速度 |
| gain | 振幅衰减系数 | 0.5 | 每层振幅的衰减速度 |
| initial frequency | 初始频率 | 1.0 | 基础波长 |
| initial amplitude | 初始振幅 | 0.5 | 基础强度 |
2.3 可视化理解
想象你在画一座山:
第1层 (octave 0): 基础轮廓
amplitude = 0.5 ╱‾‾‾╲___
frequency = 1.0 ╱ ╲___
第2层 (octave 1): 添加中等细节
amplitude = 0.25 ╱‾╲_╱‾╲_
frequency = 2.0 ╱ ╲╱ ╲╱
第3层 (octave 2): 添加小细节
amplitude = 0.125 ╱‾╲╱‾╲╱‾╲╱
frequency = 4.0 ╱ ╲╱ ╲╱ ╲╱
最终叠加: ╱‾╲_╱‾╲_
╱‾╲ ╲╱ ╲╱╲_
╱ ╲╲╱╲_╱ ╲╱╲_
细节丰富的山脉!关键规律:
- 🔊 低频 (大波长): 控制整体形状
- 🔉 中频: 添加中等细节
- 🔈 高频 (小波长): 添加微小细节
2.4 🔧 基础 fBm 实现
#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);
}
// 🌊 2D Value Noise (从上一篇复制)
float noise(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);
}
// 🏔️ 分形布朗运动 (核心!)
float fbm(vec2 st) {
float value = 0.0; // 累积值 - 用来存储所有层叠加的结果
float amplitude = 0.5; // 初始振幅 - 第一层的影响力(0.5 = 50%)
float frequency = 1.0; // 初始频率 - 第一层的波长(1.0 = 基准大小)
// ⚙️ 配置参数
const int octaves = 6; // 叠加6层 - 层数越多细节越丰富
const float lacunarity = 2.0; // 频率每层翻倍 - 2.0 表示每层比上层密集2倍
const float gain = 0.5; // 振幅每层减半 - 0.5 表示每层影响力减少50%
// 🔄 叠加多层噪声
for(int i = 0; i < octaves; i++) {
// 第i层的贡献 = 当前振幅 × 当前频率下的噪声值
value += amplitude * noise(st * frequency);
// 为下一层准备参数
frequency *= lacunarity; // 频率增加 → 下一层更细密
amplitude *= gain; // 振幅衰减 → 下一层影响力更小
}
// 💡 关键点:
// 第1层: amplitude=0.5, frequency=1.0 (粗糙轮廓,影响最大)
// 第2层: amplitude=0.25, frequency=2.0 (中等细节)
// 第3层: amplitude=0.125, frequency=4.0 (精细细节)
// ...以此类推
return value;
}
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
st *= 3.0; // 缩放坐标
float n = fbm(st);
vec3 color = vec3(n);
gl_FragColor = vec4(color, 1.0);
}2.5 🎨 效果对比
尝试修改参数,观察效果变化:
// 🎨 配置1: 粗糙地形
octaves = 4
lacunarity = 2.0
gain = 0.5
→ 效果: 山脉轮廓清晰,细节适中
// 🎨 配置2: 细腻云朵
octaves = 8
lacunarity = 2.0
gain = 0.5
→ 效果: 极其细腻,像真实云朵
// 🎨 配置3: 粗糙岩石
octaves = 5
lacunarity = 2.5
gain = 0.6
→ 效果: 尖锐的纹理,适合岩石
// 🎨 配置4: 平滑水面
octaves = 3
lacunarity = 2.0
gain = 0.3
→ 效果: 平滑起伏,适合水面2.6 📊 参数影响速查表
| 参数变化 | 视觉效果 | 应用场景 |
|---|---|---|
| octaves ↑ | 细节增加,性能下降 | 需要极致细节时(远景) |
| octaves ↓ | 更光滑,性能提升 | 实时应用,移动端 |
| lacunarity ↑ | 高频细节更明显 | 粗糙表面(岩石) |
| lacunarity ↓ | 各层过渡更平滑 | 柔和效果(雾气) |
| gain ↑ | 细节层影响更大 | 尖锐纹理(闪电) |
| gain ↓ | 基础层主导 | 平滑纹理(沙丘) |
2.7 ⚠️ 常见误区与优化建议
2.7.1 误区 1: Octaves 越多越好
错误想法: 设置 octaves = 20 让效果更细腻 问题:
- GPU 计算量指数增长,帧率暴跌
- 超过 8 层后人眼已经难以察觉差异 正确做法:
// 根据应用场景选择:
const int octaves = 4; // 移动端/实时应用
const int octaves = 6; // 桌面端/一般场景 ✓ 推荐
const int octaves = 8; // 高品质静态渲染2.7.2 误区 2: 不归一化结果
问题: fBm 的输出范围不是 [0, 1],可能导致颜色溢出 正确做法:
float fbmNormalized(vec2 st) {
float value = fbm(st);
// 归一化: fBm 的理论范围是 [0, 1/(1-gain)]
// 当 gain=0.5 时,范围约为 [0, 2]
return value * 0.5; // 映射到 [0, 1]
}2.7.3 误区 3: 忽略性能优化
优化技巧:
// ❌ 差: 每次都重新计算
float result = fbm(st) + fbm(st * 2.0) + fbm(st * 3.0);
// ✓ 好: 复用计算
float base = fbm(st);
float detail = fbm(st * 2.0);
float result = base + detail * 0.5;3. 🌊 fBm 实战应用
3.1 应用 1: 程序化云朵
uniform vec2 u_resolution;
uniform float u_time;
float noise(vec2 st) {
// ... (使用之前的噪声函数)
}
float fbm(vec2 st) {
// ... (使用之前的 fBm 函数)
}
void main() {
// 📐 归一化坐标到 [0, 1]
vec2 st = gl_FragCoord.xy / u_resolution.xy;
// ☁️ 云朵运动
vec2 cloudPos = st * 3.0; // 放大3倍 → 云朵尺寸适中
cloudPos.x += u_time * 0.1; // 向右飘动 (0.1 = 速度,越大越快)
// 🌊 生成云朵密度
float clouds = fbm(cloudPos);
// 🎨 增强对比度 - 让云朵边界更清晰
// smoothstep(0.4, 0.8, x) 的效果:
// - x < 0.4: 输出 0 (完全透明)
// - 0.4 < x < 0.8: 平滑过渡
// - x > 0.8: 输出 1 (完全不透明)
clouds = smoothstep(0.4, 0.8, clouds);
// 🌈 天空颜色渐变 (从地平线到天顶)
vec3 skyColor = mix(
vec3(0.3, 0.5, 0.9), // 地平线: 浅蓝 (底部 st.y=0)
vec3(0.1, 0.3, 0.7), // 天顶: 深蓝 (顶部 st.y=1)
st.y // 用Y坐标作为混合系数
);
// ☁️ 云朵颜色
vec3 cloudColor = vec3(0.95, 0.95, 1.0); // 白色带点蓝 (真实云的颜色)
// 🎨 混合天空和云朵
// clouds 作为混合系数: 0=完全显示天空, 1=完全显示云朵
vec3 color = mix(skyColor, cloudColor, clouds);
gl_FragColor = vec4(color, 1.0);
}
// 💡 调试提示:
// - 如果看不到云: 尝试减小 smoothstep 的阈值 (如 0.3, 0.7)
// - 如果云太密集: 增大 cloudPos 的缩放系数 (如 5.0)
// - 如果想要动态变化: 可以让 octaves 或 gain 随时间变化效果: 真实感的流动云朵,天空颜色渐变!
3.2 应用 2: 程序化地形
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
st *= 5.0; // 放大5倍 → 地形细节合适
// 🏔️ 生成高度图 (0~1 范围,可以理解为海拔高度)
float height = fbm(st);
// 🎨 根据高度映射颜色 (模拟真实地形的颜色分层)
vec3 color;
if(height < 0.3) {
// 🌊 水面 (低海拔 0~0.3)
color = vec3(0.1, 0.3, 0.6); // 深蓝色
} else if(height < 0.5) {
// 🏖️ 沙滩/海岸 (海拔 0.3~0.5)
color = vec3(0.8, 0.7, 0.5); // 沙黄色
} else if(height < 0.7) {
// 🌲 森林/草地 (海拔 0.5~0.7)
color = vec3(0.2, 0.5, 0.2); // 深绿色
} else {
// ⛰️ 山顶/雪山 (高海拔 0.7~1.0)
color = vec3(0.9, 0.9, 0.95); // 白色/灰色
}
// 📐 添加地形阴影 (基于梯度) - 让地形更立体
// 原理: 计算高度变化率 → 坡度越陡越暗
float dx = fbm(st + vec2(0.01, 0.0)) - height; // X方向的高度差
float dy = fbm(st + vec2(0.0, 0.01)) - height; // Y方向的高度差
float slope = length(vec2(dx, dy)) * 3.0; // 合成坡度 (3.0 是增强系数)
color *= (1.0 - slope * 0.3); // 陡峭的地方减少亮度 (0.3 = 阴影强度)
gl_FragColor = vec4(color, 1.0);
}
// 💡 进阶优化:
// 1. 添加平滑过渡: 用 smoothstep 替代 if-else 让颜色渐变
// 2. 添加细节: 在每个高度带叠加小噪声 (如岩石纹理)
// 3. 添加光照: 根据坡度方向计算光照角度3.3 应用 3: 木纹纹理
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
st *= 5.0; // 放大5倍 → 木纹尺寸合适
// 🪵 创建年轮效果 (同心圆)
// 原理: 从中心向外,距离越远值越大
vec2 center = vec2(2.5); // 中心点 (5.0 × 0.5 = 2.5)
float rings = length(st - center) * 5.0; // 距离中心的半径 × 5 (控制年轮密度)
// 🌊 用 fBm 扰动年轮 (模拟自然生长的不规则性)
vec2 noisePos = st;
float distortion = fbm(noisePos) * 0.3; // 0.3 = 扰动强度
rings += distortion; // 将噪声添加到半径上 → 年轮变得不规则
// 🎨 创建条纹 (年轮的明暗交替)
float pattern = fract(rings); // 取小数部分 → [0, 1] 循环 → 周期性条纹
pattern = smoothstep(0.3, 0.7, pattern); // 让过渡更平滑
// 效果: 0.3以下是深色, 0.7以上是浅色, 中间平滑过渡
// 🪵 木材颜色 (两种棕色)
vec3 darkWood = vec3(0.4, 0.25, 0.1); // 深色年轮 (深棕)
vec3 lightWood = vec3(0.7, 0.5, 0.3); // 浅色年轮 (浅棕)
vec3 color = mix(darkWood, lightWood, pattern); // 根据 pattern 混合
// ✨ 添加木材光泽 (细微的纹理细节)
float grain = fbm(st * 20.0) * 0.1; // 高频噪声 (20.0) × 小振幅 (0.1)
color += grain; // 添加微小的亮度变化
gl_FragColor = vec4(color, 1.0);
}
// 💡 进阶技巧:
// 1. 偏心圆: 修改 center 位置模拟树木切面
// 2. 方向性纹理: 添加 sin(atan(st.y, st.x)) 创造木材纤维感
// 3. 随机化: 用噪声控制 darkWood 和 lightWood 的颜色3.4 应用 4: 大理石纹理
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
st *= 3.0; // 放大3倍 → 纹理尺寸适中
// 🌀 创建扭曲的流动感 (大理石的基础流向)
float pattern = st.x + st.y * 0.5; // 基础倾斜线 (45度倾斜)
// 解释: st.x 是水平方向, st.y*0.5 添加倾斜,系数越大倾斜越明显
// 🌊 用多层 fBm 创造复杂的扭曲 (模拟地质形变)
pattern += fbm(st * 2.0) * 0.5; // 大尺度扭曲
pattern += fbm(st * 4.0) * 0.25; // 中等尺度扭曲
// 原理: 每层扭曲频率翻倍,振幅减半 → 类似fBm的思想
// 🎨 正弦波创造条纹 (大理石的纹理)
float marble = abs(sin(pattern * 3.14159)); // sin产生周期性,-1~1, abs变为0~1
marble = pow(marble, 0.5); // 幂运算调整对比度 (0.5使明暗更均匀)
// 💡 调整 pow 的指数可以改变纹理样式:
// - pow(marble, 0.3): 更多浅色区域
// - pow(marble, 2.0): 更多深色区域
// 💎 大理石颜色 (经典白色大理石带黑色纹理)
vec3 veinColor = vec3(0.1, 0.1, 0.1); // 深色纹路 (几乎黑色)
vec3 baseColor = vec3(0.9, 0.85, 0.8); // 浅色底色 (米白色)
vec3 color = mix(veinColor, baseColor, marble); // marble=0黑, marble=1白
// ✨ 添加细微的噪声细节 (微观质感)
float detail = fbm(st * 10.0) * 0.05; // 高频 (10.0) 小振幅 (0.05)
color += detail;
gl_FragColor = vec4(color, 1.0);
}
// 💡 变体:
// - 绿色大理石: baseColor = vec3(0.7, 0.9, 0.8)
// - 金色大理石: baseColor = vec3(1.0, 0.9, 0.7)
// - 更密集的纹路: pattern * 6.28318 (2倍频率)4. 🌀 域扭曲 (Domain Warping) - 噪声的噪声
4.1 🎯 核心思想
不直接使用噪声,而是用噪声来扭曲坐标系,然后在扭曲的空间中采样噪声!
💡 生活类比:想象你在看一张照片
- 传统方式:直接拍照 → 得到规则的图像
- 域扭曲:透过水面拍照 → 水波扭曲了影像,产生奇妙效果
传统方式: 域扭曲:
noise(x, y) noise(x + noise(x,y), y + noise(x,y))
↑
用噪声偏移坐标!关键理解:
- 第一步:生成噪声A → 得到偏移量
- 第二步:用偏移量修改坐标 → 扭曲的坐标系
- 第三步:在扭曲的坐标系中再次采样噪声B → 复杂结果
可视化:
原始网格: 扭曲后的网格:
┌─┬─┬─┬─┐ ╱╲╱╲╱╲
├─┼─┼─┼─┤ ╱╲ ╲╱ ╱╲
├─┼─┼─┼─┤ ╱ ╲╱╲╱ ╲
└─┴─┴─┴─┘ ╱ ╱╲╱╲ ╲
规整 扭曲变形4.2 基础实现
float noise(vec2 st) {
// ... (之前的噪声函数)
}
float fbm(vec2 st) {
// ... (之前的 fBm 函数)
}
// 🌀 域扭曲版本的 fBm
float fbmWarped(vec2 st) {
// 1️⃣ 第一次扭曲 - 生成基础偏移向量
vec2 q = vec2(
fbm(st + vec2(0.0, 0.0)), // X方向的偏移量 (从原始位置采样)
fbm(st + vec2(5.2, 1.3)) // Y方向的偏移量 (从偏移位置采样,保证x/y独立)
);
// 💡 q 的值范围大约 [0, 1],表示每个点要偏移多少
// 2️⃣ 第二次扭曲 (用第一次的结果) - 更复杂的变形
vec2 r = vec2(
fbm(st + 4.0 * q + vec2(1.7, 9.2)), // 用q的4倍放大扭曲幅度
fbm(st + 4.0 * q + vec2(8.3, 2.8)) // 再加上随机偏移保证多样性
);
// 💡 r 基于已经扭曲的坐标(st + 4.0*q)再次扭曲 → 扭曲的扭曲!
// 3️⃣ 最终采样 - 在双重扭曲的空间中采样
return fbm(st + 4.0 * r);
// 💡 最终坐标 = 原始坐标 + 4倍的二次扭曲 → 极其复杂的有机形状!
}
// 🎓 学习提示:
// - 参数 4.0 控制扭曲强度,可以尝试 1.0 ~ 8.0
// - vec2(5.2, 1.3) 等是"魔法数字",用来避免x/y方向的扭曲相同
// - 扭曲次数越多,效果越复杂,但性能越低
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
st *= 3.0;
float n = fbmWarped(st);
vec3 color = vec3(n);
gl_FragColor = vec4(color, 1.0);
}效果: 产生类似火焰、液体、有机生物的复杂纹理!
4.3 🎨 域扭曲的艺术应用
4.3.1 应用 1: 火焰效果
void main() {
// 📐 坐标预处理
vec2 st = gl_FragCoord.xy / u_resolution.xy;
st.x *= u_resolution.x / u_resolution.y; // 修正宽高比
st -= 0.5; // 中心化到 [-0.5, 0.5]
st.y -= 0.3; // 火焰从底部开始 (向下偏移)
// 🔥 向上流动 (模拟火焰上升)
st.y += u_time * 0.2; // Y坐标随时间增加 → 图案向上移动
// 🌀 域扭曲创造火焰形状
// 第一次扭曲: 生成火焰的基础扭曲向量
vec2 q = vec2(
fbm(st * 2.0), // X方向的扭曲
fbm(st * 2.0 + vec2(5.2, 1.3)) // Y方向的扭曲 (偏移避免重复)
);
// 第二次扭曲: 在扭曲的坐标系中再次采样
float flame = fbm(st * 3.0 + 4.0 * q); // 4.0 控制扭曲强度
// 🎨 火焰颜色映射 (从外到内: 暗红 → 橙 → 黄)
vec3 color1 = vec3(1.0, 0.8, 0.0); // 黄色 (核心,最热)
vec3 color2 = vec3(1.0, 0.3, 0.0); // 橙色 (中间)
vec3 color3 = vec3(0.3, 0.0, 0.0); // 深红 (边缘,较冷)
// 根据 flame 值分段混合颜色
vec3 color = mix(color3, color2, smoothstep(0.2, 0.5, flame)); // 低值: 红→橙
color = mix(color, color1, smoothstep(0.5, 0.8, flame)); // 高值: 橙→黄
// 📉 底部更亮,顶部更暗 (模拟火焰自然消散)
// smoothstep(-0.5, 0.5, -st.y) 的效果:
// - 底部 (st.y=-0.3): 输出接近1 (完全显示)
// - 顶部 (st.y=0.2): 输出接近0 (逐渐消失)
color *= smoothstep(-0.5, 0.5, -st.y);
gl_FragColor = vec4(color, 1.0);
}
// 💡 调优建议:
// - 速度: 修改 u_time * 0.2 的系数 (0.1~0.5)
// - 扭曲强度: 修改 4.0 * q 的系数 (2.0~8.0)
// - 火焰高度: 修改 st.y -= 0.3 的值 (-0.5~0.0)4.3.2 应用 2: 水面波纹
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
st *= 5.0;
// 🌊 用域扭曲模拟水流
vec2 flow = vec2(
fbm(st + vec2(u_time * 0.1, 0.0)),
fbm(st + vec2(0.0, u_time * 0.1))
) * 0.3;
float water = fbm(st + flow);
// 🌊 创建波纹高光
float specular = pow(water, 3.0);
// 🎨 水的颜色
vec3 deepWater = vec3(0.0, 0.2, 0.4);
vec3 shallowWater = vec3(0.2, 0.5, 0.7);
vec3 highlight = vec3(0.9, 0.95, 1.0);
vec3 color = mix(deepWater, shallowWater, water);
color = mix(color, highlight, specular);
gl_FragColor = vec4(color, 1.0);
}5. 📐 分形几何 - 数学之美
除了噪声,Shader 还可以绘制经典的分形几何图形!
5.1 🔺 谢尔宾斯基三角形 (Sierpiński Triangle)
规则: 从一个三角形开始,递归地移除中间的三角形
迭代0: 迭代1: 迭代2:
▲ ▲ ▲
▼▼ ▲ ▲ ▲ ▲
▼▼▼▼ ▲▼▼▲ ▲ ▲ ▲ ▲
▲▼▼▲▼▼▲5.1.1 🔧 实现方法
uniform vec2 u_resolution;
float triangle(vec2 st) {
// 使用 SDF 定义三角形
float d = abs(st.x) * 0.866025 + st.y * 0.5;
return step(d, 0.5) * step(-st.y, 0.5);
}
float sierpinski(vec2 st, int iterations) {
float result = triangle(st);
vec2 scale = vec2(1.0);
vec2 offset = vec2(0.0);
for(int i = 0; i < 5; i++) {
if(i >= iterations) break;
scale *= 0.5;
// 检查是否在中间的三角形内 (要移除的部分)
vec2 st_scaled = (st - vec2(0.0, -0.25)) / 0.5;
if(triangle(st_scaled) > 0.5) {
result = 0.0;
break;
}
// 递归到子三角形
st = st * 2.0;
if(st.y < 0.0) {
st.y += 1.0;
} else if(st.x < 0.0) {
st += vec2(0.5, -0.5);
} else {
st += vec2(-0.5, -0.5);
}
}
return result;
}
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
st = st * 2.0 - 1.0;
st.x *= u_resolution.x / u_resolution.y;
float pattern = sierpinski(st, 5);
vec3 color = vec3(pattern);
gl_FragColor = vec4(color, 1.0);
}5.2 🌀 曼德布洛特集 (Mandelbrot Set)
💡 什么是曼德布洛特集?
想象你在玩一个游戏:
- 选一个起点 c (复数坐标)
- 从 z=0 开始,不断重复计算: z = z² + c
- 如果 z 的值越来越大 → c 不在集合里 (用颜色标记多少步逃逸)
- 如果 z 的值始终很小 → c 在集合里 (用黑色表示)
这就是那个著名的"无限放大都有细节"的分形!
数学定义:
对于复数 c = (x + yi),迭代公式:
z₀ = 0
z_{n+1} = z_n² + c
如果 |z_n| 在迭代中保持有界 (< 2.0),则 c 属于曼德布洛特集5.2.1 🔧 实现
uniform vec2 u_resolution;
uniform vec2 u_mouse;
void main() {
// 📐 坐标归一化
vec2 st = gl_FragCoord.xy / u_resolution.xy;
st = st * 2.0 - 1.0; // 映射到 [-1, 1]
st.x *= u_resolution.x / u_resolution.y; // 修正宽高比
// 🎯 缩放到感兴趣的区域 (曼德布洛特集主体在 [-2, 1] × [-1.5, 1.5])
vec2 c = st * 2.0 - vec2(0.5, 0.0);
// 💡 st*2.0 放大视野, -vec2(0.5, 0.0) 向右偏移让集合居中
// 🌀 迭代计算
vec2 z = vec2(0.0); // 初始值 z₀ = 0 (复数表示: 0+0i)
int maxIterations = 100; // 最大迭代次数 (越大越精确但越慢)
int iterations = 0; // 记录实际迭代次数
for(int i = 0; i < 100; i++) {
// 📐 复数乘法: z² = (a+bi)² = (a²-b²) + (2ab)i
z = vec2(
z.x * z.x - z.y * z.y, // 实部: a² - b²
2.0 * z.x * z.y // 虚部: 2ab
) + c; // 加上 c (核心迭代公式)
// ✅ 检查是否发散 (逃逸判定)
// 数学证明: 如果 |z| > 2, 则 z 会趋向无穷
if(length(z) > 2.0) {
iterations = i; // 记录在第几步逃逸
break; // 跳出循环
}
}
// 如果循环结束还没跳出 → iterations=0 → 属于曼德布洛特集
// 🎨 根据迭代次数着色
float brightness = float(iterations) / float(maxIterations);
// brightness: 0=从未逃逸(黑色), 1=立即逃逸(亮色)
// 🌈 彩虹色映射 (Inigo Quilez 经典技巧)
vec3 color = 0.5 + 0.5 * cos(
6.28318 * (brightness + vec3(0.0, 0.33, 0.67))
);
// 原理: cos 产生周期性颜色, vec3(0.0, 0.33, 0.67) 是相位偏移 (RGB三通道)
// ⚫ 集合内部用黑色
if(iterations == 0) color = vec3(0.0);
gl_FragColor = vec4(color, 1.0);
}
// 💡 交互增强:
// 1. 鼠标缩放: c = (st - u_mouse) * zoom + offset
// 2. 平滑着色: brightness = (i + 1 - log2(log2(length(z)))) / maxIterations
// 3. 提高精度: 使用双精度浮点 (但GLSL ES不支持)效果: 无限复杂的分形边界,可以无限放大!边界处的细节永远探索不完。
5.3 🌿 科赫雪花 (Koch Snowflake)
规则: 将每条直线段的中间三分之一替换为等边三角形的两边
迭代0: _______________
迭代1: ____╱╲____
迭代2: __╱╲╱ ╲╱╲__这个需要用 SDF 和布尔运算实现,较为复杂,建议使用递归绘制。
6. 🎨 程序化图案设计
6.1 📏 瓷砖图案 (Tiling Patterns)
利用 mod 和 fract 创建重复图案:
void main() {
vec2 st = gl_FragCoord.xy / u_resolution.xy;
st *= 10.0; // 10x10 网格
// 🎨 方案1: 棋盘格
vec2 grid = floor(st);
float checker = mod(grid.x + grid.y, 2.0);
// 🎨 方案2: 每个格子内画圆
vec2 localSt = fract(st); // 局部坐标 (0~1)
localSt -= 0.5; // 中心化
float circle = smoothstep(0.21, 0.20, length(localSt));
// 🎨 方案3: 砖墙图案
float row = floor(st.y);
st.x += step(1.0, mod(row, 2.0)) * 0.5; // 奇数行偏移
vec2 brickSt = fract(st);
float brick = step(0.05, brickSt.x) * step(0.05, brickSt.y);
vec3 color = vec3(brick);
gl_FragColor = vec4(color, 1.0);
}6.2 🔷 伊斯兰几何图案
#define PI 3.14159265359
#define TWO_PI 6.28318530718
uniform vec2 u_resolution;
// 🌟 旋转函数
mat2 rotate2d(float angle) {
return mat2(cos(angle), -sin(angle), sin(angle), cos(angle));
}
void main() {
// 📐 坐标归一化
vec2 st = gl_FragCoord.xy / u_resolution.xy;
st = st * 2.0 - 1.0; // 从 [0,1] 映射到 [-1,1]
st.x *= u_resolution.x / u_resolution.y; // 修正宽高比
// 🔷 创建8重对称 (玫瑰窗效果)
float angle = atan(st.y, st.x); // 获取极坐标角度 [-π, π]
float radius = length(st); // 获取距离中心的半径
// 🪞 折叠到1/8扇形 (360° ÷ 8 = 45°)
// 原理: 将所有角度映射到 [0, 45°] 范围内
angle = mod(angle, TWO_PI / 8.0); // 取模到 [0, 45°]
angle = abs(angle - TWO_PI / 16.0); // 镜像折叠,轴线在 22.5°
// 🔄 重建坐标 (从极坐标转回笛卡尔坐标)
st = vec2(cos(angle), sin(angle)) * radius;
// 现在 st 在折叠后的空间中,绘制一次图案会自动复制8次!
// 🎨 在折叠空间中绘制简单图案
float pattern = 0.0;
// 绘制垂直条纹 (距离Y轴0.3单位的线)
pattern += step(0.1, abs(st.x) - 0.3);
// 绘制水平条纹 (距离X轴0.5单位的线)
pattern += step(0.1, abs(st.y) - 0.5);
vec3 color = vec3(pattern);
gl_FragColor = vec4(color, 1.0);
}
// 💡 原理解释:
// - 对称折叠: 只画 1/8,其余 7/8 自动镜像
// - 角度折叠: mod() 创造旋转对称, abs() 创造镜像对称
// - 实用技巧: 修改 TWO_PI/8.0 为 TWO_PI/6.0 可得6重对称(雪花)7. 本章总结
7.1 你学到了什么
分形布朗运动 (fBm):
- 🌊 多层噪声叠加的原理 - 低频控制轮廓,高频控制细节
- ⚙️ octaves, lacunarity, gain 参数的作用 - 控制复杂度与性能平衡
- 🎨 应用: 云朵、地形、木纹、大理石 - 涵盖自然与人工纹理
域扭曲:
- 🌀 用噪声扭曲坐标系 - "噪声的噪声"创造复杂效果
- 🔥 创造有机、流动的效果 - 火焰、烟雾、液体的核心技术
- 🎨 应用: 火焰、水面、液体 - 模拟动态自然现象
分形几何:
- 🔺 谢尔宾斯基三角形 - 递归分割的经典案例
- 🌀 曼德布洛特集 - 无限复杂的数学美
- 📐 自相似结构 - 理解分形的数学本质
程序化图案:
- 📏 瓷砖和重复图案 - 利用 mod/fract 创造无缝纹理
- 🔷 对称和折叠技术 - 用简单代码创造复杂几何
- 🎨 伊斯兰几何纹样 - 艺术与数学的结合
7.2 关键技术回顾
7.2.1 🎓 核心概念对比
| 技术 | 适用场景 | 性能开销 | 学习难度 |
|---|---|---|---|
| 单层噪声 | 简单纹理,水波 | 低 ⚡ | 入门 |
| fBm | 自然地形,云朵 | 中 ⚡⚡ | 中级 |
| 域扭曲 | 火焰,有机物 | 高 ⚡⚡⚡ | 高级 |
| 分形几何 | 艺术效果 | 极高 ⚡⚡⚡⚡ | 专家 |
7.2.2 🛠️ 实用技巧总结
性能优化
glsl// 根据距离动态调整质量 float dist = length(cameraPos - fragPos); int octaves = dist < 10.0 ? 8 : 4; // 近处高质量,远处低质量时间动画
glsl// 让噪声随时间演化 vec2 animatedPos = st + vec2(u_time * 0.1, 0.0); float n = fbm(animatedPos);多层混合
glsl// 组合不同频率的效果 float base = fbm(st); // 基础层 float detail = fbm(st * 5.0); // 细节层 float result = base * 0.7 + detail * 0.3; // 加权混合颜色映射技巧
glsl// 彩虹色映射 (Inigo Quilez 技巧) vec3 color = 0.5 + 0.5 * cos(6.28318 * (value + vec3(0.0, 0.33, 0.67)));
7.2.3 ⚠️ 初学者易错点
忘记归一化坐标 → 图案变形
glslst.x *= u_resolution.x / u_resolution.y; // 修正宽高比颜色值超出范围 → 画面过曝
glslcolor = clamp(color, 0.0, 1.0); // 限制到合法范围循环次数不匹配 → 编译错误
glslfor(int i = 0; i < 100; i++) { if(i >= octaves) break; // GLSL 要求循环上限是常量 }
7.3 推荐练习 (按难度递增)
7.3.1 🌱 初级 (巩固基础)
参数实验
- 修改 octaves, lacunarity, gain 观察效果
- 尝试不同组合找到最佳配置
- 记录参数与效果的关系
简单动画
- 让云朵左右飘动
- 让火焰上下跳动
- 用 sin(u_time) 创造周期性变化
7.3.2 🌿 中级 (综合应用)
动态云层
- 使用 fBm 生成云
- 添加多层云层深度感 (远近不同速度)
- 实现日出日落的颜色变化
程序化行星表面
- fBm 生成高度图
- 根据高度分层着色 (海洋/陆地/雪山)
- 添加大气层效果 (边缘渐变)
7.3.3 🌳 高级 (挑战自我)
交互式曼德布洛特集
- 鼠标控制缩放和平移
- 彩色迭代映射 (彩虹效果)
- 实现平滑着色算法 (避免色带)
分形树
- 用递归思想绘制树枝
- 添加随机扰动 (每根树枝略有不同)
- 实现风吹动画 (域扭曲应用)
7.3.4 💡 调试技巧
- 可视化中间步骤: 把每个octave单独输出看效果glsl
gl_FragColor = vec4(vec3(noise(st * frequency)), 1.0); // 只看某一层 - 使用调试颜色: 用颜色标记不同区域glsl
if(value < 0.5) color = vec3(1.0, 0.0, 0.0); // 红色标记低值 else color = vec3(0.0, 1.0, 0.0); // 绿色标记高值
7.4 快速参考卡片
7.4.1 📋 fBm 标准模板
float fbm(vec2 st, int octaves, float lacunarity, float gain) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
for(int i = 0; i < 10; i++) { // 最大循环次数
if(i >= octaves) break;
value += amplitude * noise(st * frequency);
frequency *= lacunarity;
amplitude *= gain;
}
return value;
}
// 使用示例:
float n = fbm(st, 6, 2.0, 0.5); // 6层,频率翻倍,振幅减半7.4.2 🎨 常用参数配置速查
// 自然地形
fbm(st, 6, 2.0, 0.5);
// 柔和云朵
fbm(st, 8, 2.0, 0.4);
// 粗糙岩石
fbm(st, 5, 2.5, 0.6);
// 平滑水面
fbm(st, 3, 2.0, 0.3);
// 火焰/闪电
fbm(st, 4, 3.0, 0.7);7.4.3 🔧 性能优化检查清单
- [ ] octaves 是否 ≤ 8?
- [ ] 是否复用计算结果?
- [ ] 是否需要 LOD (远处降低质量)?
- [ ] 是否可以预计算静态部分?
- [ ] 循环上限是否是常量?
7.4.4 🐛 调试检查清单
- [ ] 坐标是否归一化?
- [ ] 宽高比是否修正?
- [ ] 颜色值是否在 [0,1] 范围?
- [ ] 噪声函数是否正确实现?
- [ ] uniform 变量是否正确传递?
7.5 参考资源
核心教程:
- Inigo Quilez - FBM - fBm 的权威指南
- Inigo Quilez - Domain Warping - 域扭曲深度解析
- The Book of Shaders - Fractal Brownian Motion - 交互式教程
数学背景:
- Wikipedia - Fractal - 分形数学理论
- Wikipedia - Brownian Motion - 布朗运动原理
实战案例:
- Shadertoy - Clouds - Inigo Quilez 的经典云朵
- Shadertoy - Seascape - 程序化海洋
- Shadertoy - Volcanic - 火山效果