曼德勃罗集的 Three.js 实现

效果预览

经典的曼德勃罗集(Mandelbrot Set)分形渲染,配合动态缩放动画探索分形边界的无限细节。使用线性插值平滑着色,呈现出彩虹般的色彩过渡。

👉点击查看《曼德勃罗集》完整源码与效果演示

Shader实现原理

1.整体思路与数学模型

曼德勃罗集是复平面上的一组点,对于每一个复数c,迭代序列:

ini 复制代码
z_{n+1} = z_n^2 + c,  z_0 = 0

如果该序列不发散(即模长始终保持有界),则c属于曼德勃罗集。在计算机中,我们用有限迭代次数来近似判断:如果在res次迭代内|z| > 2,则认为序列发散,c不属于曼德勃罗集。

判别其他同类的数学指标是:如果|z_n| > 2,序列必然发散到无穷。2是曼德勃罗集的逃逸跳跃。

2. 复数乘法 --- cprod 宏的数学

css 复制代码
#define cprod(a, b) vec2(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x)

这是标准的复数乘法公式。设定a = a.x + i*a.y,b = b.x + i*b.y,则:

css 复制代码
a * b = (a.x + i*a.y)(b.x + i*b.y)
      = a.x*b.x - a.y*b.y + i(a.x*b.y + a.y*b.x)

实部a.x*b.x - a.y*b.y对应vec2x数量,虚部a.x*b.y + a.y*b.x对应x数量。这个宏把复数支出为vec2支出,在GPU上是零支出的(编译期展开)。

3. 迭代核心------mandel函数

ini 复制代码
float mandel(vec2 c, int res) {
    vec2 z = vec2(0.0, 0.0);
    float oldLen = 0.0;
    for (int i = 0; i < res; i++) {
        z += c;
        z = cprod(z, z);
        float newLen = length(z);

        if (newLen > 2.0) {
            float p = (2.0 - oldLen) / (newLen - oldLen);
            return float(i) + p;
        }
        oldLen = newLen;
    }
    return float(res);
}

3.1 迭代顺序

注意这里的迭代顺序是z += c顺序z = z^2。这与标准定义z = z^2 + c等价,但计算顺序不同:

标准定义:z_{n+1} = z_n^2 + c 代码实现:z_{n+1} = (z_n + c)^2 = z_n^2 + 2*z_n*c + c^2

,这等等都不行。实际上仔细看代码:

  • 初始z = 0
  • 第1次:z = 0 + c = c,然后z = c^2。此时z = c^2,但标准应该是z = 0^2 + c = c。

这个顺序实际上是z_{n+1} = (z_n + c)^2,与标准定义不同。但由于初始值z_0 = 0,两个顺序的差异只是索引偏移版本:

  • 代码中第i1次迭代后,z的值对应标准定义的第i+11次迭代
  • 返回的最终迭代次数与标准定义一致(因为初始oldLen = 0已经考虑了第 0 步)

3.2 线性插值平滑(Smooth Iteration Count)

css 复制代码
float p = (2.0 - oldLen) / (newLen - oldLen);
return float(i) + p;

如果不做平滑,就是返回离散的整数迭代次数。在分形边界处,相邻像素的迭代次数可能外围很大,导致明显的色带(banding)。

线性插值的思路:假设|z|从oldLen到newLen是线性增长的,则逃逸|z| = 2发生在迭代之间的一个分数位置p:

ini 复制代码
oldLen + p * (newLen - oldLen) = 2
=> p = (2 - oldLen) / (newLen - oldLen)

这样返回值是连续的浮点数,而不是离散的整数,消除了色带现象。

4.坐标变换与缩放系统

ini 复制代码
vec2 uv = vec2(gl_FragCoord.xy - iResolution.xy / 2.0);
uv = uv * 2.0 / min(iResolution.x, iResolution.y);

4.1 屏幕坐标到归一化坐标

  • gl_FragCoord.xy - iResolution.xy / 2.0:将坐标原点移到屏幕中心
  • * 2.0 / min(iResolution.x, iResolution.y):以屏幕短边为基准归一化,保证不同宽高比下图形不被拉伸

4.2 动态缩放

ini 复制代码
float range = sin(iTime * 0.25) * 0.5 + 0.5;
float zoom = 1.0 + 100050.0 * range;
  • sin(iTime * 0.25)周期为2π / 0.25 = 8π ≈ 25.1秒
  • * 0.5 + 0.5映射到[0, 1]
  • zoom范围:1.0到100051.0,覆盖从全局视图到极深放大

4.3 焦点位置

ini 复制代码
vec2 focus = 1.0 * vec2(-0.542738427, 0.615566608);

这个点是曼德勃罗集边界上一个著名的"象谷"(象谷)区域附近的点,分形细节极其丰富。缩放时域为中心,可以观察到无限循环的自相似结构。

4.4 最终UV变换

ini 复制代码
float f = mandel(focus + uv * 1.25 / zoom, res);
  • uv * 1.25 / zoom:1.25是基础视野范围,/ zoom是缩放因子
  • focus + ...:将局部坐标平移到焦点位置

5.颜色映射

scss 复制代码
float p = f / float(res);
vec3 col = vec3(0.1, 0.1, 0.1);
if (int(f) < res) {
    p *= float(res) / 16.0;
    col = 0.5 + 0.5 * vec3(sin(p), sin(p + PI / 3.0), sin(p + PI * 2.0 / 3.0));
    col = col / max(col.x, max(col.y, col.z));
}

5.1 集合内外的区别

  • int(f) < res:如果迭代次数小于最大迭代次数,说明点在集合外(逃逸了)
  • int(f) == res:点在集合内部,利用探索vec3(0.1)

5.2 色相循环

scss 复制代码
col = 0.5 + 0.5 * vec3(sin(p), sin(p + PI / 3.0), sin(p + PI * 2.0 / 3.0));

这是相位偏移的正弦函数 ,三个通道的相位差分别为0、π/3、2π/3:

  • R通道:sin(p)
  • G通道:sin(p + π/3)
  • B通道:sin(p + 2π/3)

0.5 + 0.5 * sin(...)把输出映射到[0, 1]。三个通道相位差120°,在RGB空间中形成平滑的色相循环。

p *= float(res) / 16.0把迭代统计放大512/16 = 32倍数,让颜色变化更明亮,增强视觉细节。

5.3 归一化增强对比

ini 复制代码
col = col / max(col.x, max(col.y, col.z));

这一步把颜色除以最大通道值,使至少一个通道达到1.0。效果是:

  • 增强彩色水位
  • 让暗色更暗、亮色更亮
  • 整体提升

总结

这个效果的精髓采用最简单的复数迭代公式生成无限复杂的分形图案。曼德勃罗集不是"画"出来的,而是""算出来的------每个像素的颜色都是一次独立的数学实验结果。

分形的核心魅力是自相似性 :无论你放大多少倍,边界的褶皱结构始终保持相似性。本着色器通过sin(iTime)驱动的动态缩放,让用户能窥探这种无限递归的美。

本文使用 mdnice 排版

相关推荐
总要学点什么7 小时前
第47篇:三维特效-雷达波效果——波纹一圈圈往外推,一半却埋在地下
3d·前端框架·gis·webgl·cesium
沧沧凉凉5 天前
一段需求、零素材,Claude Code 派了 33 个代理给我做了个 3D 动作游戏
游戏·ai编程·three.js
八荒启·交互动画7 天前
Web特效025—用 Canvas 2D 做Web特效的定义与边界:这支画笔能做到哪一步
前端·webgl·网页特效·八荒启-交互动画·八荒启
Bruce_Liuxiaowei8 天前
网页版 DeepSeek “harness desktop“ WebGL 上下文创建失败排查实录
开发语言·javascript·webgl
牧艺10 天前
浏览器端 3D 看车重磅升级:从三款展车到可试驾的八车展厅
前端·three.js·交互设计
像我这样帅的人丶你还13 天前
📱iPhone Duo 开屏动画咋实现的?
javascript·webgl·three.js
夏天要喝冰可乐13 天前
Antigravity + Blender MCP(下):3D 智慧仓储数字孪生进阶实战
前端·webgl·three.js
夏天要喝冰可乐13 天前
Antigravity + Blender MCP(上):打造3D 智慧仓储数字孪生
前端·webgl·three.js
supermapsupport18 天前
SuperMap iClient3D for WebGL 交互式栅格信息查询
webgl·supermap
八荒启·交互动画21 天前
# Web特效020—让 Web 特效真正动起来:时间循环应该怎么接
前端·webgl·网页特效·八荒启-交互动画·八荒启