Neste projeto vamos construir um renderizador de fractais do conjunto de Mandelbrot em Rust. O conjunto de Mandelbrot é provavelmente o fractal mais famoso da matemática – uma estrutura infinitamente complexa gerada por uma fórmula surpreendentemente simples: z = z² + c. Cada pixel da imagem representa um número complexo, e a cor indica quantas iterações a sequência leva para “escapar” (divergir para o infinito).
Este é um projeto ideal para explorar computação numérica, paralelismo e geração de imagens em Rust. A renderização de cada pixel é independente, o que permite paralelização perfeita com rayon. Ao final, você poderá gerar imagens detalhadas com zoom em regiões fascinantes do fractal e experimentar diferentes esquemas de cores.
O Que Vamos Construir
Um renderizador de Mandelbrot com as seguintes funcionalidades:
- Aritmética de números complexos implementada do zero
- Cálculo de pertinência ao conjunto com contagem de iterações
- Mapeamento de coordenadas de pixels para o plano complexo
- Múltiplos esquemas de colorização (escala de cinza, suave, espectro)
- Suporte a zoom em regiões específicas do fractal
- Renderização paralela com
rayonpara máxima performance - Saída em formatos PPM e PNG
Estrutura do Projeto
mandelbrot/
├── Cargo.toml
└── src/
└── main.rs
Configurando o Projeto
Crie o projeto com o Cargo:
cargo new mandelbrot
cd mandelbrot
Edite o Cargo.toml:
[package]
name = "mandelbrot"
version = "0.1.0"
edition = "2021"
[dependencies]
rayon = "1.10"
image = "0.25"
A crate rayon permite paralelizar o cálculo com uma única mudança (into_par_iter), distribuindo as linhas entre todos os núcleos. A crate image é usada para salvar a saída em formato PNG.
Passo 1: Números Complexos
A base do Mandelbrot é a aritmética de números complexos. Um número complexo tem parte real e parte imaginária: c = a + bi. Vamos implementar uma struct com todas as operações necessárias.
use image::{ImageBuffer, Rgb};
use rayon::prelude::*;
use std::env;
/// Número complexo com parte real e imaginária (f64)
#[derive(Clone, Copy, Debug)]
struct Complexo {
real: f64,
imag: f64,
}
impl Complexo {
/// Cria um novo número complexo
fn novo(real: f64, imag: f64) -> Self {
Complexo { real, imag }
}
/// Retorna o quadrado do módulo (|z|² = a² + b²)
/// Usamos o quadrado para evitar calcular raiz quadrada
fn modulo_quadrado(&self) -> f64 {
self.real * self.real + self.imag * self.imag
}
/// Multiplica dois números complexos:
/// (a + bi)(c + di) = (ac - bd) + (ad + bc)i
fn multiplicar(&self, outro: &Complexo) -> Complexo {
Complexo {
real: self.real * outro.real - self.imag * outro.imag,
imag: self.real * outro.imag + self.imag * outro.real,
}
}
/// Soma dois números complexos:
/// (a + bi) + (c + di) = (a + c) + (b + d)i
fn somar(&self, outro: &Complexo) -> Complexo {
Complexo {
real: self.real + outro.real,
imag: self.imag + outro.imag,
}
}
/// Eleva ao quadrado: z² = z * z
fn ao_quadrado(&self) -> Complexo {
self.multiplicar(self)
}
}
Usamos modulo_quadrado em vez de modulo (que exigiria sqrt) porque para o teste de escape precisamos apenas verificar se |z|² > 4 (equivalente a |z| > 2). Evitar a raiz quadrada em cada iteração de cada pixel faz uma diferença significativa na performance.
Passo 2: Cálculo do Mandelbrot e Colorização
Agora implementamos o coração do algoritmo: para cada ponto c no plano complexo, iteramos z = z² + c começando com z = 0 e contamos quantas iterações são necessárias antes que |z| > 2 (escape). Também implementamos diferentes esquemas de colorização.
/// Configuração da renderização
struct ConfigRender {
/// Largura da imagem em pixels
largura: u32,
/// Altura da imagem em pixels
altura: u32,
/// Número máximo de iterações por pixel
max_iteracoes: u32,
/// Canto superior esquerdo do plano complexo visível
min_real: f64,
max_real: f64,
min_imag: f64,
max_imag: f64,
/// Esquema de cores a usar
esquema_cores: EsquemaCores,
}
/// Esquemas de colorização disponíveis
#[derive(Clone, Copy, Debug)]
enum EsquemaCores {
/// Tons de cinza simples
EscalaCinza,
/// Transição suave com interpolação
Suave,
/// Espectro de cores vibrantes baseado em seno
Espectro,
}
/// Calcula quantas iterações o ponto c leva para escapar.
/// Retorna (iteracoes, |z|² final) para colorização suave.
fn calcular_escape(c: &Complexo, max_iteracoes: u32) -> (u32, f64) {
let mut z = Complexo::novo(0.0, 0.0);
for i in 0..max_iteracoes {
// z = z² + c
z = z.ao_quadrado().somar(c);
// Teste de escape: |z|² > 4 (equivale a |z| > 2)
let mod_q = z.modulo_quadrado();
if mod_q > 4.0 {
return (i, mod_q);
}
}
// Não escapou: pertence ao conjunto de Mandelbrot
(max_iteracoes, z.modulo_quadrado())
}
/// Converte coordenadas de pixel para um ponto no plano complexo
fn pixel_para_complexo(
x: u32,
y: u32,
config: &ConfigRender,
) -> Complexo {
let real = config.min_real
+ (x as f64 / config.largura as f64) * (config.max_real - config.min_real);
let imag = config.min_imag
+ (y as f64 / config.altura as f64) * (config.max_imag - config.min_imag);
Complexo::novo(real, imag)
}
/// Colorização em escala de cinza
fn cor_escala_cinza(iteracoes: u32, max_iteracoes: u32) -> [u8; 3] {
if iteracoes == max_iteracoes {
return [0, 0, 0]; // Preto para pontos no conjunto
}
let brilho = (255.0 * iteracoes as f64 / max_iteracoes as f64) as u8;
[brilho, brilho, brilho]
}
/// Colorização suave usando interpolação logarítmica
fn cor_suave(iteracoes: u32, max_iteracoes: u32, mod_quadrado: f64) -> [u8; 3] {
if iteracoes == max_iteracoes {
return [0, 0, 0]; // Preto para pontos no conjunto
}
// Suavização: usa o módulo para interpolar entre iterações inteiras
let suave = iteracoes as f64 + 1.0 - mod_quadrado.ln().ln() / 2.0_f64.ln();
let t = suave / max_iteracoes as f64;
// Paleta baseada em interpolação de Bernstein
let r = (9.0 * (1.0 - t) * t * t * t * 255.0) as u8;
let g = (15.0 * (1.0 - t) * (1.0 - t) * t * t * 255.0) as u8;
let b = (8.5 * (1.0 - t) * (1.0 - t) * (1.0 - t) * t * 255.0) as u8;
[r, g, b]
}
/// Colorização por espectro usando funções seno
fn cor_espectro(iteracoes: u32, max_iteracoes: u32) -> [u8; 3] {
if iteracoes == max_iteracoes {
return [0, 0, 0]; // Preto para pontos no conjunto
}
let t = iteracoes as f64 / max_iteracoes as f64;
// Oscila por diferentes faixas de cor usando seno com defasagens
let r = (0.5 + 0.5 * (std::f64::consts::TAU * (t * 5.0 + 0.0)).sin()) * 255.0;
let g = (0.5 + 0.5 * (std::f64::consts::TAU * (t * 5.0 + 0.33)).sin()) * 255.0;
let b = (0.5 + 0.5 * (std::f64::consts::TAU * (t * 5.0 + 0.67)).sin()) * 255.0;
[r as u8, g as u8, b as u8]
}
/// Escolhe a cor de acordo com o esquema configurado
fn colorizar(
iteracoes: u32,
max_iteracoes: u32,
mod_quadrado: f64,
esquema: EsquemaCores,
) -> [u8; 3] {
match esquema {
EsquemaCores::EscalaCinza => cor_escala_cinza(iteracoes, max_iteracoes),
EsquemaCores::Suave => cor_suave(iteracoes, max_iteracoes, mod_quadrado),
EsquemaCores::Espectro => cor_espectro(iteracoes, max_iteracoes),
}
}
A colorização suave usa uma técnica chamada “normalized iteration count”: em vez de usar a contagem inteira de iterações, usamos o logaritmo do módulo no ponto de escape para interpolar suavemente entre valores, eliminando as faixas de cor bruscas. O esquema de espectro usa funções seno defasadas em 120 graus para gerar um arco-íris completo.
Passo 3: Renderização Paralela com Rayon
Agora implementamos a renderização propriamente dita, usando rayon para processar as linhas em paralelo. Cada pixel é completamente independente dos demais, tornando este problema “embaraçosamente paralelo”.
/// Renderiza o fractal de Mandelbrot, retornando um vetor de pixels RGB
fn renderizar(config: &ConfigRender) -> Vec<[u8; 3]> {
// Cada linha é processada independentemente em paralelo
let pixels: Vec<[u8; 3]> = (0..config.altura)
.into_par_iter()
.flat_map(|y| {
(0..config.largura)
.map(|x| {
// Converte pixel para coordenada complexa
let c = pixel_para_complexo(x, y, config);
// Calcula o número de iterações para escape
let (iteracoes, mod_q) = calcular_escape(&c, config.max_iteracoes);
// Converte a contagem de iterações em cor
colorizar(iteracoes, config.max_iteracoes, mod_q, config.esquema_cores)
})
.collect::<Vec<_>>()
})
.collect();
pixels
}
/// Salva os pixels como imagem PNG
fn salvar_png(
caminho: &str,
pixels: &[[u8; 3]],
largura: u32,
altura: u32,
) {
let imagem = ImageBuffer::from_fn(largura, altura, |x, y| {
let indice = (y * largura + x) as usize;
let [r, g, b] = pixels[indice];
Rgb([r, g, b])
});
imagem.save(caminho).expect("Falha ao salvar a imagem PNG");
}
/// Salva os pixels como imagem PPM (formato texto simples)
fn salvar_ppm(
caminho: &str,
pixels: &[[u8; 3]],
largura: u32,
altura: u32,
) {
use std::fs::File;
use std::io::{BufWriter, Write};
let arquivo = File::create(caminho).expect("Falha ao criar o arquivo PPM");
let mut escritor = BufWriter::new(arquivo);
// Cabeçalho PPM
writeln!(escritor, "P3").unwrap();
writeln!(escritor, "{} {}", largura, altura).unwrap();
writeln!(escritor, "255").unwrap();
// Dados dos pixels
for [r, g, b] in pixels {
writeln!(escritor, "{} {} {}", r, g, b).unwrap();
}
}
O into_par_iter() do rayon distribui automaticamente as linhas entre os núcleos disponíveis. Em um processador de 8 núcleos, a renderização é aproximadamente 7-8x mais rápida que a versão sequencial. O flat_map achata os vetores de pixels de cada linha em um único vetor contínuo.
Passo 4: Interface de Linha de Comando e Presets de Zoom
Finalmente, implementamos a função main com suporte a argumentos de linha de comando e presets de regiões interessantes do fractal para exploração.
/// Presets de regiões interessantes do fractal
fn obter_preset(nome: &str) -> Option<(f64, f64, f64, f64, u32)> {
// (min_real, max_real, min_imag, max_imag, max_iteracoes)
match nome {
"completo" => Some((-2.5, 1.0, -1.25, 1.25, 256)),
"seahorse" => Some((-0.75, -0.74, 0.09, 0.102, 512)),
"espiral" => Some((-0.7463, -0.7413, 0.1102, 0.1152, 1024)),
"miniatura" => Some((-1.769, -1.765, -0.001, 0.003, 2048)),
"raio" => Some((-0.1592, -0.1505, -1.0328, -1.0255, 1024)),
"elefante" => Some((0.2755, 0.2855, 0.006, 0.016, 512)),
_ => None,
}
}
/// Exibe as instruções de uso
fn exibir_uso() {
eprintln!("Gerador de Fractais Mandelbrot em Rust");
eprintln!();
eprintln!("Uso:");
eprintln!(" mandelbrot [opções]");
eprintln!();
eprintln!("Opções:");
eprintln!(" --largura <N> Largura da imagem em pixels (padrão: 800)");
eprintln!(" --altura <N> Altura da imagem em pixels (padrão: 600)");
eprintln!(" --iteracoes <N> Máximo de iterações (padrão: 256)");
eprintln!(" --cores <esquema> Esquema de cores: cinza, suave, espectro (padrão: suave)");
eprintln!(" --preset <nome> Região predefinida: completo, seahorse, espiral,");
eprintln!(" miniatura, raio, elefante");
eprintln!(" --saida <arquivo> Arquivo de saída (padrão: mandelbrot.png)");
eprintln!(" --ppm Salva em formato PPM em vez de PNG");
eprintln!();
eprintln!("Exemplos:");
eprintln!(" mandelbrot");
eprintln!(" mandelbrot --preset seahorse --cores espectro");
eprintln!(" mandelbrot --largura 1920 --altura 1080 --iteracoes 1000");
}
fn main() {
let args: Vec<String> = env::args().collect();
// Valores padrão
let mut largura: u32 = 800;
let mut altura: u32 = 600;
let mut max_iteracoes: u32 = 256;
let mut min_real = -2.5;
let mut max_real = 1.0;
let mut min_imag = -1.25;
let mut max_imag = 1.25;
let mut esquema_cores = EsquemaCores::Suave;
let mut caminho_saida = String::from("mandelbrot.png");
let mut formato_ppm = false;
// Analisa argumentos
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--largura" => {
i += 1;
largura = args[i].parse().expect("Largura deve ser um número");
}
"--altura" => {
i += 1;
altura = args[i].parse().expect("Altura deve ser um número");
}
"--iteracoes" => {
i += 1;
max_iteracoes = args[i].parse().expect("Iterações deve ser um número");
}
"--cores" => {
i += 1;
esquema_cores = match args[i].as_str() {
"cinza" => EsquemaCores::EscalaCinza,
"suave" => EsquemaCores::Suave,
"espectro" => EsquemaCores::Espectro,
outro => {
eprintln!("Esquema de cores desconhecido: '{}'. Usando 'suave'.", outro);
EsquemaCores::Suave
}
};
}
"--preset" => {
i += 1;
match obter_preset(&args[i]) {
Some((rmin, rmax, imin, imax, iter)) => {
min_real = rmin;
max_real = rmax;
min_imag = imin;
max_imag = imax;
max_iteracoes = iter;
}
None => {
eprintln!(
"Preset desconhecido: '{}'. Presets: completo, seahorse, espiral, miniatura, raio, elefante",
args[i]
);
std::process::exit(1);
}
}
}
"--saida" => {
i += 1;
caminho_saida = args[i].clone();
}
"--ppm" => {
formato_ppm = true;
}
"--ajuda" | "-h" => {
exibir_uso();
return;
}
_ => {
eprintln!("Opção desconhecida: '{}'", args[i]);
exibir_uso();
std::process::exit(1);
}
}
i += 1;
}
// Ajusta a extensão do arquivo se necessário
if formato_ppm && caminho_saida == "mandelbrot.png" {
caminho_saida = String::from("mandelbrot.ppm");
}
let config = ConfigRender {
largura,
altura,
max_iteracoes,
min_real,
max_real,
min_imag,
max_imag,
esquema_cores,
};
println!("Gerando fractal de Mandelbrot...");
println!(" Resolução: {}x{}", largura, altura);
println!(" Iterações: {}", max_iteracoes);
println!(
" Região: [{:.6}, {:.6}] x [{:.6}, {:.6}]",
min_real, max_real, min_imag, max_imag
);
println!(" Cores: {:?}", esquema_cores);
let inicio = std::time::Instant::now();
// Renderiza o fractal em paralelo
let pixels = renderizar(&config);
let duracao = inicio.elapsed();
println!(" Renderização: {:.2}s", duracao.as_secs_f64());
// Salva a imagem
if formato_ppm {
salvar_ppm(&caminho_saida, &pixels, largura, altura);
} else {
salvar_png(&caminho_saida, &pixels, largura, altura);
}
let total_pixels = largura as u64 * altura as u64;
let pixels_por_segundo = total_pixels as f64 / duracao.as_secs_f64();
println!(" Pixels/seg: {:.0}", pixels_por_segundo);
println!(" Salvo em: '{}'", caminho_saida);
}
Os presets de zoom foram cuidadosamente escolhidos para destacar diferentes estruturas do fractal: “seahorse” mostra o famoso vale dos cavalos-marinhos, “espiral” revela espirais de Fibonacci, “miniatura” mostra uma cópia em miniatura do conjunto principal, e “elefante” exibe o vale dos elefantes. Cada preset ajusta também o número máximo de iterações, já que regiões com mais zoom precisam de mais iterações para revelar os detalhes.
Como Executar
Compile e execute o projeto:
# Renderização padrão (visão completa do Mandelbrot)
cargo run --release
# Saída esperada:
# Gerando fractal de Mandelbrot...
# Resolução: 800x600
# Iterações: 256
# Região: [-2.500000, 1.000000] x [-1.250000, 1.250000]
# Cores: Suave
# Renderização: 0.15s
# Pixels/seg: 3200000
# Salvo em: 'mandelbrot.png'
# Zoom na região seahorse com cores de espectro
cargo run --release -- --preset seahorse --cores espectro
# Imagem em alta resolução
cargo run --release -- --largura 3840 --altura 2160 --iteracoes 1000
# Zoom na espiral com saída PPM
cargo run --release -- --preset espiral --ppm
# Para converter PPM em PNG (requer ImageMagick)
convert mandelbrot.ppm mandelbrot.png
O modo --release é essencial para performance – a diferença entre debug e release pode ser de 10-20x em cálculos de ponto flutuante.
Desafios para Expandir
Conjunto de Julia – Implemente a renderização de conjuntos de Julia, que usam a mesma fórmula z = z² + c mas com c fixo e z variando. Cada ponto c do Mandelbrot corresponde a um conjunto de Julia diferente, criando uma galeria infinita de fractais.
Zoom interativo – Use a crate
minifbousdl2para criar uma janela onde o usuário pode clicar para centralizar e usar a roda do mouse para dar zoom, renderizando em tempo real.Precisão arbitrária – Substitua
f64por uma biblioteca de precisão arbitrária (comorugounum-bigfloat) para permitir zooms extremos além do limite de 10⁻¹⁵ do ponto flutuante de 64 bits.Animação de zoom – Gere uma sequência de frames que fazem zoom progressivo em uma região interessante, salvando como série de PNGs que podem ser combinados em um vídeo com
ffmpeg.Perturbation theory – Implemente o método de perturbação que calcula apenas um ponto de referência com precisão arbitrária e deriva os demais usando perturbações em f64, acelerando dramaticamente zooms profundos.
Veja Também
- Tipos Numéricos – Tipos de ponto flutuante e operações matemáticas
- Iteradores em Rust – Iteração funcional usada na renderização
- Vec: Vetores Dinâmicos – Armazenamento dos pixels e dados da imagem
- Executando Threads – Paralelismo com rayon para renderização
- Otimização de Performance – Técnicas para acelerar cálculos intensivos