使用Rust和并发实现一个高性能的彩色分形图案渲染

发布于:2025-05-31 ⋅ 阅读:(33) ⋅ 点赞:(0)

分形与 Mandelbrot

Mandelbrot 集 (Mandelbrot Set) 是复数平面上一个点的集合,以数学家 Benoît Mandelbrot 的名字命名。它是最著名的分形之一。一个复数 c 是否属于 Mandelbrot 集,取决于一个简单的迭代过程:
z n + 1 = z n 2 + c z_{n+1}=z_{n}^2+c zn+1=zn2+c

如果这个序列 z0​,z1​,z2​,… 的大小(模)保持在一定范围内(具体来说,不超过 2),那么我们就说复数 c 属于 Mandelbrot 集。如果序列的大小趋向于无穷大,那么 c 就不属于这个集合。

当我们为复数平面上的每个点 c 运行这个迭代过程,并根据它“逃逸”到无穷大的速度(或者它是否保持有界)给它上色时,我们就会得到 Mandelbrot 集那标志性的、无限复杂的图像。

那些“逃逸”得慢的点或者不逃逸的点,通常被涂成黑色,而那些“逃逸”得快的点,则根据它们的逃逸速度被赋予不同的颜色,从而形成了图像中绚丽多彩的部分。

项目准备

创建一个 Rust 项目:

cargo new mandelbrot

程序需要以下依赖:

[dependencies]
clap = { version = "4.x", features = ["derive"] }
image = "0.25.1"
num = "0.4.3"

计算 Mandelbrot 迭代

计算相对而言是比较简单的,我们需要引入 Rust 的一个数值类型库 num,从而支持复数类型。

如果对复数不熟悉,也没关系,就把实部想象成笛卡尔坐标系的 x 轴(横向),虚部想象成 y 轴(只不过以 i 为单位)。

use num::Complex;

fn escape_time(c: Complex<f64>, limit: usize) -> Option<usize> {
   
    let mut z = Complex {
    re: 0.0, im: 0.0 };
    for i in 0..limit {
   
        if z.norm_sqr() > 4.0 {
   
            return Some(i);
        }
        z = z * z + c;
    }
    None
}

程序上非常简单,就是持续迭代计算 z * z + c,如果 limit 次迭代里它没有越界,我们就认为它属于 Mandelbrot 集合。如果超过了,就返回迭代的次数(即逃逸时间);如果在达到上限之前都没有超过,就返回 None,表示我们认为这个点可能属于 Mandelbrot 集。

像素到点的映射

我们需要一种方法来将图像中的每个像素(如 (25, 175))映射到复数平面上的一个点(如 -0.5 - 0.75i)。

fn pixel_to_point(
    bounds: (usize, usize),    // 图像尺寸 (宽, 高)
    pixel: (usize, usize),     // 像素坐标 (列, 行)
    upper_left: Complex<f64>,  // 左上角对应的复数
    lower_right: Complex<f64>, // 右下角对应的复数
) -> Complex<f64> {
   
    let (width, height) = (
        lower_right.re - upper_left.re, // 复数平面的宽度
        upper_left.im - lower_right.im, // 复数平面的高度
    );
    Complex {
   
        re: upper_left.re + pixel.0 as f64 * width / bounds.0 as f64,
        im: upper_left.im - pixel.1 as f64 * height / bounds.1 as f64,
    }
}

由于数学上的坐标系和计算机通常的屏幕坐标系的 y 轴方向是相反。计算机图像通常左上角是 (0, 0),y 轴向下增长,复平面通常 y 轴(虚部)向上增长。因此,在计算虚部 im 时,我们是从 upper_left.im 减去

渲染图片

要绘制 Mandelbrot 集,只需要将 escape_time 用在复平面上的点,根据逃逸时间赋以不同的灰度值。

fn render(
    pixels: &mut [u8],
    bounds: (usize, usize),
    upper_left: Complex<f64>,
    lower_right: Complex<f64>,
) {
   
    assert!(pixels.len() == bounds.0 * bounds.1);

    for row in 0..bounds.1 {
   
        for column in 0..bounds.0 {
   
            let point = pixel_to_point(bounds, (column, row), upper_left, lower_right);
            pixels[row * bounds.0 + column] = match escape_time(point, 255) {
   
                None => 0, // 属于 Mandelbrot 集,设为黑色 (0)
                Some(count) => 255 - count as u8, // 不属于,颜色与逃逸时间相关
            }
        }
    }
}

如果没有逃逸,则是黑色 0,否则就根据逃逸时间赋值 255 - count

到这里,我们就完成了核心的程序。下面的部分属于渲染和保存。

写入图片

use std::fs::File;
use image::{
   ExtendedColorType, ImageEncoder

网站公告

今日签到

点亮在社区的每一天
去签到