Rust图像处理第17节-Julia 朱利亚分形:拖动常数 c 看图实时变化

0 阅读6分钟

Jul-13-2026 23-02-08.gif

Julia 朱利亚分形:拖动常数 c 看图实时变化

🦀 Rust + WASM 实战系列 第 17 篇 阅读时间:约 6 分钟 | 实战可运行

📌 写在前面

上一篇讲了 Mandelbrot——c 在复平面上变化(每个像素一个 c),z₀ = 0 固定。

Julia 是 Mandelbrot 的孪生兄弟——迭代公式完全一样z_{n+1} = z_n² + c,但参数角色互换

Mandelbrot:每个像素一个 cz0=0 固定Juliac 固定为某个常数,z0 在复平面上变化\begin{aligned} &\textbf{Mandelbrot}:\text{每个像素一个 } c,z_0 = 0 \text{ 固定}\\ &\textbf{Julia}:c \text{ 固定为某个常数,} z_0 \text{ 在复平面上变化} \end{aligned}

算法代码几乎一样(只交换两个参数的循环范围)。真正的价值不是"换参数"——而是实时拖动 c 看图变化


🚀 TL;DR

zn+1=zn2+cz_{n+1} = z_n^2 + c
  • 跟 Mandelbrot 公式完全一样
  • 区别:Julia 的 c 是固定的常数(前端滑块实时调)
  • 结果:拖动 c 时整个图案会戏剧性变化——这是 Mandelbrot 做不到的

📖 目录

  1. Julia 的真正乐趣
  2. 为什么公式一样但效果完全不同
  3. 算法:和 Mandelbrot 的关键 2 行差异
  4. 关键代码
  5. 经典 Julia 常数(5 个必看)
  6. 前端效果展示
  7. 踩坑提醒
  8. 下篇预告:性能优化

一、Julia 的真正乐趣

Mandelbrot 没法"调 c"——因为 c 已经是图的 x 轴(整个集合就是 c 的可视化)。

Julia 可以拖动 c 滑块实时看图变化——这是 Mandelbrot 做不到的。

拖动 c 实部 → 图案的"骨架"在水平方向拉伸拖动 c 虚部 → 图案的"细节密度"在垂直方向变化拖动到某些区域 → 图案突然从"无限细节"变成"全黑 / 全白"(c 落在 Mandelbrot 集合内

直觉:Mandelbrot 像是"一张固定的地图",Julia 像是"一个可以变形的地图"。


二、为什么公式一样但效果完全不同?

zn+1=zn2+cz_{n+1} = z_n^2 + c

Mandelbrot 和 Julia 完全相同的迭代规则只是初始化不同

算法cc 来源z0z_0 来源
Mandelbrot每个像素一个 c(从像素映射)永远 0
Julia固定常数(滑块控制)每个像素一个 z0z_0(从像素映射)

为什么效果天差地别?

  • Mandelbrot 把 c 当"主角"——所以整个集合就是 c 的所有可能性的可视化
  • Julia 把 c 当"背景设定"——所以每个 c 决定一种"物理规则",整个复平面在这个规则下演化

类比:Mandelbrot 像是"一张地图",Julia 像是"一个地图 + 一组物理参数"。


三、算法:和 Mandelbrot 的关键 2 行差异

公式回顾(详见 本系列上一节)

迭代公式 z=z2+cz = z^2 + c 拆成实部/虚部(task 16 §二 有详细推导):

Re(z2+c)=zr2zi2+creIm(z2+c)=2zrzi+cim\begin{aligned} \text{Re}(z^2 + c) &= \text{zr}^2 - \text{zi}^2 + c_{\text{re}} \\ \text{Im}(z^2 + c) &= 2 \cdot \text{zr} \cdot \text{zi} + c_{\text{im}} \end{aligned}

逃逸判断:zr2+zi2>4\text{zr}^2 + \text{zi}^2 > 4

Mandelbrot vs Julia:参数角色对比

维度MandelbrotJulia
cc 来自每个像素(从像素映射)固定常数(滑块控制)
z0z_0 来自永远 0每个像素(从像素映射)
物理意义看每个cc "命运如何"看每个z0z_0 在固定 cc 规则下"命运如何"

Julia 的核心循环

for each pixel (px, py):
    // ① 像素 → z₀(不再是 c!)
    z_re = center_x + (px - w/2) / zoom
    z_im = center_y + (py - h/2) / zoom
  
    // ② c 是循环外的固定常数
    for iter:
        if zr² + zi² > 4: 停止     // 逃逸
        new_zr = zr² - zi² + c_re  // 实部
        new_zi = 2*zr*zi + c_im    // 虚部
        zr, zi = new_zr, new_zi

就这 2 行差异(Mandelbrot 里像素→c,Julia 里像素→z0z_0),其他完全一样。

一个具体的 Julia 计算

c=0.8+0.156ic = -0.8 + 0.156i(海马谷),画布中心点 z0=0z_0 = 0(注意 Julia 从像素位置出发,所以画布中心对应 z0=0z_0 = 0):

步骤zr\text{zr}zi\text{zi}zr2zi2\text{zr}^2 - \text{zi}^22zrzi2 \cdot \text{zr} \cdot \text{zi}newzr\text{zr}newzi\text{zi}
000000.8-0.80.1560.156
10.8-0.80.1560.1560.6160.6160.250-0.2500.184-0.1840.094-0.094
20.184-0.1840.094-0.0940.0250.0250.0350.0350.775-0.7750.1910.191
30.775-0.7750.1910.1910.5640.5640.296-0.2960.236-0.2360.105-0.105
40.236-0.2360.105-0.1050.0450.0450.0500.0500.755-0.7550.2060.206
.....................

每步的 zr2+zi2\text{zr}^2 + \text{zi}^2

步骤zr2+zi2\text{zr}^2 + \text{zi}^2逃逸?
10.6640.664
20.0430.043
30.3150.315
40.0660.066
...(持续震荡)...

可以看到 zr2+zi2\text{zr}^2 + \text{zi}^2 始终 < 4,这个 z0z_0 点最终在集合内——涂黑色

要点:Julia 的迭代过程和 Mandelbrot 完全一样,只是 ccz0z_0 的角色互换了。


四、关键代码

#[wasm_bindgen]
pub fn julia(
    width: u32,
    height: u32,
    c_re: f64,
    c_im: f64,
    zoom: f64,
    center_x: f64,
    center_y: f64,
    max_iter: u32,
    palette: u32,
) -> Vec<u8> {
    let w = width as usize;
    let h = height as usize;
    let mut out = vec![0u8; w * h * 4];

    let inv_zoom = 1.0 / zoom;
    let cr = c_re;  // ← c 提出来,不进内循环
    let ci = c_im;

    for py in 0..h {
        for px in 0..w {
            // 像素 → 复数(这是 z₀,不是 c)
            let zr0 = center_x + (px as f64 - w as f64 / 2.0) * inv_zoom;
            let zi0 = center_y + (py as f64 - h as f64 / 2.0) * inv_zoom;

            // 迭代 z = z² + c(c 固定!)
            let mut zr = zr0;
            let mut zi = zi0;
            let mut iter: u32 = 0;
            while iter < max_iter && (zr * zr + zi * zi) < 4.0 {
                let new_zr = zr * zr - zi * zi + cr;
                let new_zi = 2.0 * zr * zi + ci;
                zr = new_zr;
                zi = new_zi;
                iter += 1;
            }

            let (r, g, b) = color_from_iter(iter, max_iter, palette);
            let idx = (py * w + px) * 4;
            out[idx]     = r;
            out[idx + 1] = g;
            out[idx + 2] = b;
            out[idx + 3] = 255;
        }
    }

    out
}

调色板函数和 Mandelbrot 共用同样的公式(fire / ocean / forest / grayscale)——项目里调色板代码完全相同,只是函数被 julia.rs 和 mandelbrot.rs 各自定义一份。

未来重构:调色板可以抽到独立模块(如 fractal/palette.rs),让两个分形共用——这是架构优化的机会,留给读者自行尝试。


五、经典 Julia 常数(5 个必看)

打开前端页面,依次点 5 个预设按钮看效果:

常数名称视觉特点
c = -0.8 + 0.156i🐴海马谷细丝状结构,密集旋转
c = -0.4 + 0.6i🌸连分数颗粒状粉尘,分散细节
c = 0.285 + 0.01i星形对称的树枝状,从中心向外
c = -0.835 - 0.2321i🔥火焰螺旋 + 火焰般的流动
c = -0.4 - 0.59i🌑全黑几乎所有点逃逸 → 接近纯黑

关键观察

拖动 c 时会看到:

  • 某些 c 值 → 整张图几乎全黑(所有点都逃逸)
  • 某些 c 值 → 整张图几乎全彩(所有点都不逃逸)
  • 过渡区域 → 图案剧烈变化,几个像素的 c 变化就能改天换地

为什么? 因为 c 落在 Mandelbrot 集合内部时 Julia 集是连通的(不会全黑),落在外部时可能不连通(容易全黑)。


六、前端效果展示

Jul-13-2026 23-02-08.gif

打开页面后:

  1. 默认显示 🐴 海马谷(c = -0.8 + 0.156i,调色板火焰)
  2. 拖动 c 实部 / 虚部滑块——实时看图变化(这是 Mandelbrot 做不到的)
  3. 点击 5 个预设按钮:海马谷 / 连分数 / 星形 / 火焰 / 全黑
  4. 切换调色板:火焰 / 海洋 / 绿色 / 灰度
  5. 缩放 / 平移:和 Mandelbrot 一样
  6. 重置按钮:回到默认

七、踩坑提醒

1. c_rec_im 提到循环外

// ❌ 错(虽然对,但每次循环多一次读)
let new_zr = zr * zr - zi * zi + c_re;

// ✅ 对(提前 let cr = c_re,循环内用 cr)
let cr = c_re;
let new_zr = zr * zr - zi * zi + cr;

虽然编译器通常会优化掉重复读,但显式提到循环外是好习惯——性能 + 可读性都更好。

2. Julia 在某些 c 值会"几乎全黑"

c = -0.4 - 0.59i  →  大部分点快速逃逸 → 整张图全黑

不是 bug——是 Julia 集合本身不连通("尘埃"型)导致视觉上很暗。

调高迭代次数 + 换调色板能稍微缓解,但根本上 c 决定图案

3. 拖动 c 时性能

WASM 串行 f64 在 600×600 画布上每次拖动都要重算 360,000 像素——拖动滑块时可能卡

下一篇(任务 18)会用 Web Worker 多线程 + 增量渲染解决。本篇先用单线程跑通,性能优化留给下一篇。


八、下篇预告:性能优化

任务 18:交互缩放 + 拖拽 + 性能深度优化

前一篇(task 16)讲的"性能优化阶梯"只是预告——本篇会真正实现

  1. Web Worker 多线程:开 4 个线程并行渲染(Mandelbrot / Julia 各 2 个),加速 4×
  2. 增量渲染:拖动时只重算变化的区域,不是每次全图重算
  3. f32 vs f64:用 f32 替代 f64 加速 2×
  4. 平滑着色初探:消除"色带",让 Julia 的细节更平滑

核心目标:把 Mandelbrot / Julia 从"150 ms" 降到 "16 ms 以下"——实现 60 FPS 实时拖动


🎁 写在最后

这一篇最大的认知升级:

Julia 不是"换参数"——是"换物理规则"

拖动 c 时,每个像素都在新规则下重新演化——这就是 Julia 的"动力学"魅力。

任务套路关注点
16 曼德博迭代c 变化(每个像素一个 c)
17 Julia迭代c 固定(滑块控制 + z₀ 变化)

变的是参数角色,不是算法。掌握这一点,你就能在**"任何 c 值"** 下看到 Julia 的"个性"——和真实世界一样,每个常数有不同"性格"

你已经走过了 16 篇,现在掌握了两大经典分形。下一篇学性能优化——把这两个分形真正玩起来(60 FPS 拖动)。


📦 项目地址pixel-math-wasm 🦀 Rust + WebAssembly 实战系列


🏷️ 标签#Rust #WebAssembly #分形 #Julia #复数 #迭代 #逃逸时间 #算法 #算法