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

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

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

📌 写在前面

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

Julia 是 Mandelbrot 的孪生兄弟 ------迭代公式完全一样z_{n+1} = z_n² + c,但参数角色互换
Mandelbrot:每个像素一个 c,z0=0 固定 Julia:c 固定为某个常数,z0 在复平面上变化 \begin{aligned} &\textbf{Mandelbrot}:\text{每个像素一个 } c,z_0 = 0 \text{ 固定}\\ &\textbf{Julia}:c \text{ 固定为某个常数,} z_0 \text{ 在复平面上变化} \end{aligned} Mandelbrot:每个像素一个 c,z0=0 固定Julia:c 固定为某个常数,z0 在复平面上变化

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


🚀 TL;DR

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

  • 跟 Mandelbrot 公式完全一样
  • 区别 :Julia 的 c 是固定的常数(前端滑块实时调)
  • 结果 :拖动 c 时整个图案会戏剧性变化------这是 Mandelbrot 做不到的

📖 目录

  1. [Julia 的真正乐趣](#Julia 的真正乐趣 "#%E4%B8%80julia-%E7%9A%84%E7%9C%9F%E6%AD%A3%E4%B9%90%E8%B6%A3")
  2. 为什么公式一样但效果完全不同
  3. [算法:和 Mandelbrot 的关键 2 行差异](#算法:和 Mandelbrot 的关键 2 行差异 "#%E4%B8%89%E7%AE%97%E6%B3%95%E5%92%8C-mandelbrot-%E7%9A%84%E5%85%B3%E9%94%AE-2-%E8%A1%8C%E5%B7%AE%E5%BC%82")
  4. 关键代码
  5. [经典 Julia 常数(5 个必看)](#经典 Julia 常数(5 个必看) "#%E4%BA%94%E7%BB%8F%E5%85%B8-julia-%E5%B8%B8%E6%95%B05-%E4%B8%AA%E5%BF%85%E7%9C%8B")
  6. 前端效果展示
  7. 踩坑提醒
  8. 下篇预告:性能优化

一、Julia 的真正乐趣

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

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

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

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


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

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

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

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

为什么效果天差地别?

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

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


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

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

迭代公式 z=z2+cz = z^2 + c z=z2+c 拆成实部/虚部(task 16 §二 有详细推导):
Re(z2+c) =zr2−zi2+cre Im(z2+c) =2⋅zr⋅zi+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} Re(z2+c)Im(z2+c)=zr2−zi2+cre=2⋅zr⋅zi+cim

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

Mandelbrot vs Julia:参数角色对比

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

Julia 的核心循环

rust 复制代码
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 里像素→ z0 z_0 z0),其他完全一样。

一个具体的 Julia 计算

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

步骤 zr\text{zr} zr zi\text{zi} zi zr2−zi2\text{zr}^2 - \text{zi}^2 zr2−zi2 2⋅zr⋅zi2 \cdot \text{zr} \cdot \text{zi} 2⋅zr⋅zi new zr\text{zr} zr new zi\text{zi} zi
0 0 0 0 0 −0.8-0.8 −0.8 0.1560.156 0.156
1 −0.8-0.8 −0.8 0.1560.156 0.156 0.6160.616 0.616 −0.250-0.250 −0.250 −0.184-0.184 −0.184 −0.094-0.094 −0.094
2 −0.184-0.184 −0.184 −0.094-0.094 −0.094 0.0250.025 0.025 0.0350.035 0.035 −0.775-0.775 −0.775 0.1910.191 0.191
3 −0.775-0.775 −0.775 0.1910.191 0.191 0.5640.564 0.564 −0.296-0.296 −0.296 −0.236-0.236 −0.236 −0.105-0.105 −0.105
4 −0.236-0.236 −0.236 −0.105-0.105 −0.105 0.0450.045 0.045 0.0500.050 0.050 −0.755-0.755 −0.755 0.2060.206 0.206
... ... ... ... ... ... ...

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

步骤 zr2+zi2\text{zr}^2 + \text{zi}^2 zr2+zi2 逃逸?
1 0.6640.664 0.664
2 0.0430.043 0.043
3 0.3150.315 0.315
4 0.0660.066 0.066
... (持续震荡) ...

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

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


四、关键代码

rust 复制代码
#[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.rsmandelbrot.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 集是连通 的(不会全黑),落在外部时可能不连通(容易全黑)。


六、前端效果展示

打开页面后:

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

七、踩坑提醒

1. c_rec_im 提到循环外

rust 复制代码
// ❌ 错(虽然对,但每次循环多一次读)
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 值会"几乎全黑"

ini 复制代码
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 #复数 #迭代 #逃逸时间 #算法 #算法

相关推荐
花褪残红青杏小5 小时前
Rust图像处理第18节-分形画布缩放 + 拖拽交互:图像缩放 + f32 性能优化
rust·webassembly·图形学
doiito6 小时前
【开源项目】用 Rust 重构中文 TTS!kokoroi-rs 高性能语音合成引擎介绍
ai·rust
铅笔侠_小龙虾7 小时前
Rust 学习目录
开发语言·学习·rust
SmalBox12 小时前
【节点】[EvaluateRefractionData节点]原理解析与实际应用
unity3d·游戏开发·图形学
铅笔侠_小龙虾13 小时前
Rust 学习(4)-函数与注释
开发语言·学习·rust
前端H15 小时前
Biome & Rolldown:Rust 工具链接管前端基建
开发语言·前端·rust
独孤留白16 小时前
从C到Rust:struct Cell / RefCell 线程内局部可变性
rust
Kapaseker16 小时前
Rust 是如何处理异常的
rust·kotlin
铅笔侠_小龙虾18 小时前
Rust 学习(8)-切片类型
学习·rust·c#
程序员爱钓鱼21 小时前
Rust 函数与返回值详解:参数、表达式与返回类型
后端·rust