Skip to content

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 实现 ​

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);
}

// 🌊 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 🎨 效果对比 ​

尝试修改参数,观察效果变化:

glsl
// 🎨 配置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 层后人眼已经难以察觉差异 正确做法:
glsl
// 根据应用场景选择:
const int octaves = 4;  // 移动端/实时应用
const int octaves = 6;  // 桌面端/一般场景 ✓ 推荐
const int octaves = 8;  // 高品质静态渲染

2.7.2 误区 2: 不归一化结果 ​

问题: fBm 的输出范围不是 [0, 1],可能导致颜色溢出 正确做法:

glsl
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: 忽略性能优化 ​

优化技巧:

glsl
// ❌ 差: 每次都重新计算
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: 程序化云朵 ​

glsl
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: 程序化地形 ​

glsl
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: 木纹纹理 ​

glsl
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: 大理石纹理 ​

glsl
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))
                            ↑
                      用噪声偏移坐标!

关键理解:

  1. 第一步:生成噪声A → 得到偏移量
  2. 第二步:用偏移量修改坐标 → 扭曲的坐标系
  3. 第三步:在扭曲的坐标系中再次采样噪声B → 复杂结果

可视化:

原始网格:            扭曲后的网格:
┌─┬─┬─┬─┐           ╱╲╱╲╱╲
├─┼─┼─┼─┤          ╱╲ ╲╱ ╱╲
├─┼─┼─┼─┤         ╱  ╲╱╲╱  ╲
└─┴─┴─┴─┘        ╱   ╱╲╱╲   ╲
规整              扭曲变形

4.2 基础实现 ​

glsl
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: 火焰效果 ​

glsl
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: 水面波纹 ​

glsl
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 🔧 实现方法 ​

glsl
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) ​

💡 什么是曼德布洛特集?

想象你在玩一个游戏:

  1. 选一个起点 c (复数坐标)
  2. 从 z=0 开始,不断重复计算: z = z² + c
  3. 如果 z 的值越来越大 → c 不在集合里 (用颜色标记多少步逃逸)
  4. 如果 z 的值始终很小 → c 在集合里 (用黑色表示)

这就是那个著名的"无限放大都有细节"的分形!

数学定义:

对于复数 c = (x + yi),迭代公式:
z₀ = 0
z_{n+1} = z_n² + c

如果 |z_n| 在迭代中保持有界 (< 2.0),则 c 属于曼德布洛特集

5.2.1 🔧 实现 ​

glsl
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 创建重复图案:

glsl
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 🔷 伊斯兰几何图案 ​

glsl
#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 🛠️ 实用技巧总结 ​

  1. 性能优化

    glsl
    // 根据距离动态调整质量
    float dist = length(cameraPos - fragPos);
    int octaves = dist < 10.0 ? 8 : 4;  // 近处高质量,远处低质量
  2. 时间动画

    glsl
    // 让噪声随时间演化
    vec2 animatedPos = st + vec2(u_time * 0.1, 0.0);
    float n = fbm(animatedPos);
  3. 多层混合

    glsl
    // 组合不同频率的效果
    float base = fbm(st);           // 基础层
    float detail = fbm(st * 5.0);   // 细节层
    float result = base * 0.7 + detail * 0.3;  // 加权混合
  4. 颜色映射技巧

    glsl
    // 彩虹色映射 (Inigo Quilez 技巧)
    vec3 color = 0.5 + 0.5 * cos(6.28318 * (value + vec3(0.0, 0.33, 0.67)));

7.2.3 ⚠️ 初学者易错点 ​

  1. 忘记归一化坐标 → 图案变形

    glsl
    st.x *= u_resolution.x / u_resolution.y;  // 修正宽高比
  2. 颜色值超出范围 → 画面过曝

    glsl
    color = clamp(color, 0.0, 1.0);  // 限制到合法范围
  3. 循环次数不匹配 → 编译错误

    glsl
    for(int i = 0; i < 100; i++) {
        if(i >= octaves) break;  // GLSL 要求循环上限是常量
    }

7.3 推荐练习 (按难度递增) ​

7.3.1 🌱 初级 (巩固基础) ​

  1. 参数实验

    • 修改 octaves, lacunarity, gain 观察效果
    • 尝试不同组合找到最佳配置
    • 记录参数与效果的关系
  2. 简单动画

    • 让云朵左右飘动
    • 让火焰上下跳动
    • 用 sin(u_time) 创造周期性变化

7.3.2 🌿 中级 (综合应用) ​

  1. 动态云层

    • 使用 fBm 生成云
    • 添加多层云层深度感 (远近不同速度)
    • 实现日出日落的颜色变化
  2. 程序化行星表面

    • fBm 生成高度图
    • 根据高度分层着色 (海洋/陆地/雪山)
    • 添加大气层效果 (边缘渐变)

7.3.3 🌳 高级 (挑战自我) ​

  1. 交互式曼德布洛特集

    • 鼠标控制缩放和平移
    • 彩色迭代映射 (彩虹效果)
    • 实现平滑着色算法 (避免色带)
  2. 分形树

    • 用递归思想绘制树枝
    • 添加随机扰动 (每根树枝略有不同)
    • 实现风吹动画 (域扭曲应用)

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 标准模板 ​

glsl
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 🎨 常用参数配置速查 ​

glsl
// 自然地形
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 参考资源 ​

核心教程:

数学背景:

实战案例: