Фракталы — это бесконечные самоподобные фигуры. Они определяются простыми математическими формулами, которые создают удивительную красоту!
В этой статье мы рассмотрим алгоритм визуализации одного из самых известных фракталов на языке Rust с аппаратным ускорением NVIDIA, масштабированием, сглаживанием и многопоточностью.
Множество Мандельброта
Пожалуй, это самый известный фрактал, описанный в 1905 году Пьером Фату. Множество задаётся следующей формулой, где и — комплексные числа:
Фрактал можно визуализировать последовательным вычислением функции , начиная с . Доказано, что множество ограничено окружностью с радиусом . Если за итераций точка осталась внутри окружности (), то считаем, что принадлежит множеству и закрашиваем её в чёрный цвет.
Реализация на CPU
Для начала давайте напишем программу для визуализации набора без использования NVIDIA CUDA. Создадим пустой проект с помощью cargo init
:
$ cargo init mandelbrot_set & cd mandelbrot_set
$ tree
├── Cargo.lock
├── Cargo.toml
├── README.md
└── src
└── main.rs
В Cargo.toml
добавим следующие зависимости:
[package]
name = "mandelbrot_set"
version = "0.1.0"
edition = "2021"
[dependencies]
+num = "0.4.3"
+image = "0.25.1"
-
image
для работы с изображениями -
num
для поддержки комплексных чисел
Импортируем эти библиотеки:
use image::{Rgb, RgbImage};
use num::complex::Complex;
Напишем функцию для :
fn num_iters(cx: f64, cy: f64, max_iters: u32) -> u32 {
let mut z = Complex::new(0.0, 0.0);
let c = Complex::new(cx, cy);
for i in 0..=max_iters {
if z.norm() > 2.0 {
return i;
}
z = z * z + c;
}
max_iters
}
Обозначим функцию generate_set
, которая сохранит визуализацию множества в файл:
fn generate_set(
file_name: String,
max_iters: u32,
x_min: f64,
y_min: f64,
x_max: f64,
y_max: f64,
resolution: u32,
) { }
Создадим и сохраним пустое RGB изображение:
let mut buffer = RgbImage::new(resolution, resolution);
buffer.save(&file_name).unwrap();
Рассчитаем количество итераций из num_iters
для каждого пикселя изображения:
for x in 0..resolution {
for y in 0..resolution {
let x_percent = x as f64 / resolution as f64;
let y_percent = y as f64 / resolution as f64;
let cx = x_min + (x_max - x_min) * x_percent;
let cy = y_min + (y_max - y_min) * y_percent;
let iters = num_iters(cx, cy, max_iters);
}
}
Если предел итераций достигнут, мы оставляем пиксель по умолчанию черным. В противном случае закрашиваем его белым цветом:
let pixel = buffer.get_pixel_mut(x, y);
if iters < max_iters {
*pixel = Rgb([255, 255, 255]);
}
Полный код функции generate_set
fn generate_set(
file_name: String,
max_iters: u32,
x_min: f64,
y_min: f64,
x_max: f64,
y_max: f64,
resolution: u32,
) {
let mut buffer = RgbImage::new(resolution, resolution);
for x in 0..resolution {
for y in 0..resolution {
let x_percent = x as f64 / resolution as f64;
let y_percent = y as f64 / resolution as f64;
let cx = x_min + (x_max - x_min) * x_percent;
let cy = y_min + (y_max - y_min) * y_percent;
let iters = num_iters(cx, cy, max_iters);
let pixel = buffer.get_pixel_mut(x, y);
if iters < max_iters {
*pixel = Rgb([255, 255, 255]);
}
}
}
buffer.save(&file_name).unwrap();
}
Вызовем функцию из main
:
fn main() {
let resolution = 1024;;
let max_iters = 100;
generate_set(
"fractal.png".to_string(),
max_iters,
-1.5,
-1.0,
0.5,
1.0,
resolution,
);
}
Полный код main.rs
use image::{Rgb, RgbImage};
use num::complex::Complex;
fn generate_set(
file_name: String,
max_iters: u32,
x_min: f64,
y_min: f64,
x_max: f64,
y_max: f64,
resolution: u32,
) {
let mut buffer = RgbImage::new(resolution, resolution);
for x in 0..resolution {
for y in 0..resolution {
let x_percent = x as f64 / resolution as f64;
let y_percent = y as f64 / resolution as f64;
let cx = x_min + (x_max - x_min) * x_percent;
let cy = y_min + (y_max - y_min) * y_percent;
let iters = num_iters(cx, cy, max_iters);
let pixel = buffer.get_pixel_mut(x, y);
if iters < max_iters {
*pixel = Rgb([255, 255, 255]);
}
}
}
buffer.save(&file_name).unwrap();
}
fn num_iters(cx: f64, cy: f64, max_iters: u32) -> u32 {
let mut z = Complex::new(0.0, 0.0);
let c = Complex::new(cx, cy);
for i in 0..=max_iters {
if z.norm() > 2.0 {
return i;
}
z = z * z + c;
}
max_iters
}
fn main() {
let resolution = 1024;;
let max_iters = 100;
generate_set(
"fractal.png".to_string(),
max_iters,
-1.5,
-1.0,
0.5,
1.0,
resolution,
);
}
Запустим программу — cargo run --release
. При первом запуске необходимо подождать, пока библиотеки скомпилируются.
В результате получается такой фрактал. Строго математически, это правильная визуализация — точка либо принадлежит множеству (черный цвет), либо нет (белый цвет), но никто не мешает нам добавить немного цвета! Один из способов раскрасить фрактал — рассмотреть, как быстро становится понятно, что точка не принадлежит множеству.
Добавим цвета
Пусть имеется массив gradient
с max_iters
элементов, в котором будут заключены различные цвета. Если iters < max_iters
, то мы берём gradient[iters]
и окрашиваем пиксель в этот цвет. В противном случае оставляем точку черной:
fn generate_set(
...
colors: Vec<&str>,
...
) {
...
let gradient = get_gradient(colors, max_iters);
...
for x in 0..resolution {
for y in 0..resolution {
...
let pixel = buffer.get_pixel_mut(x, y);
let color = gradient.get(iters as usize).unwrap_or(&[0, 0, 0]);
*pixel = Rgb(*color);
}
}
}
Здесь реализованы функции для создания линейного градиента из нескольких цветов HEX:
hex2rgb, lerp_color и get_gradient
fn hex2rgb(hex: &str) -> Result<Vec<u8>, String> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Err("Invalid HEX color length".to_string());
}
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid HEX color")?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid HEX color")?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid HEX color")?;
Ok(vec![r, g, b])
}
fn lerp_color(color1: &[u8; 3], color2: &[u8; 3], value: f64) -> [u8; 3] {
[
(color1[0] as f64 + (color2[0] as f64 - color1[0] as f64) * value) as u8,
(color1[1] as f64 + (color2[1] as f64 - color1[1] as f64) * value) as u8,
(color1[2] as f64 + (color2[2] as f64 - color1[2] as f64) * value) as u8,
]
}
fn get_gradient(gradient_colors: Vec<&str>, max_iters: u32) -> Vec<[u8; 3]> {
let mut colors = vec![];
let mut gradient_colors_rgb = vec![];
for color in &gradient_colors {
let rgb = hex2rgb(color).unwrap();
gradient_colors_rgb.push([rgb[0], rgb[1], rgb[2]]);
}
for i in 0..max_iters {
let color_index = (i as usize * (gradient_colors.len() - 1)) / max_iters as usize;
let color_value = (i as f64 * (gradient_colors.len() as f64 - 1.0)) / max_iters as f64;
let value = color_value % 1.0;
colors.push(lerp_color(
&gradient_colors_rgb[color_index],
&gradient_colors_rgb[color_index + 1],
value,
));
}
colors
}
Создавать цветовые палитры можно на этом сайте - https://color.adobe.com/
Полный код main.rs
use image::{Rgb, RgbImage};
use num::complex::Complex;
fn hex2rgb(hex: &str) -> Result<Vec<u8>, String> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Err("Invalid HEX color length".to_string());
}
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid HEX color")?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid HEX color")?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid HEX color")?;
Ok(vec![r, g, b])
}
fn lerp_color(color1: &[u8; 3], color2: &[u8; 3], value: f64) -> [u8; 3] {
[
(color1[0] as f64 + (color2[0] as f64 - color1[0] as f64) * value) as u8,
(color1[1] as f64 + (color2[1] as f64 - color1[1] as f64) * value) as u8,
(color1[2] as f64 + (color2[2] as f64 - color1[2] as f64) * value) as u8,
]
}
fn get_gradient(gradient_colors: Vec<&str>, max_iters: u32) -> Vec<[u8; 3]> {
let mut colors = vec![];
let mut gradient_colors_rgb = vec![];
for color in &gradient_colors {
let rgb = hex2rgb(color).unwrap();
gradient_colors_rgb.push([rgb[0], rgb[1], rgb[2]]);
}
for i in 0..max_iters {
let color_index = (i as usize * (gradient_colors.len() - 1)) / max_iters as usize;
let color_value = (i as f64 * (gradient_colors.len() as f64 - 1.0)) / max_iters as f64;
let value = color_value % 1.0;
colors.push(lerp_color(
&gradient_colors_rgb[color_index],
&gradient_colors_rgb[color_index + 1],
value,
));
}
colors
}
fn generate_set(
file_name: String,
max_iters: u32,
colors: Vec<&str>,
x_min: f64,
y_min: f64,
x_max: f64,
y_max: f64,
resolution: u32,
) {
let mut buffer = RgbImage::new(resolution, resolution);
let gradient = get_gradient(colors, max_iters);
for x in 0..resolution {
for y in 0..resolution {
let x_percent = x as f64 / resolution as f64;
let y_percent = y as f64 / resolution as f64;
let cx = x_min + (x_max - x_min) * x_percent;
let cy = y_min + (y_max - y_min) * y_percent;
let iters = num_iters(cx, cy, max_iters);
let pixel = buffer.get_pixel_mut(x, y);
let color = gradient.get(iters as usize).unwrap_or(&[0, 0, 0]);
*pixel = Rgb(*color);
}
}
buffer.save(&file_name).unwrap();
}
fn num_iters(cx: f64, cy: f64, max_iters: u32) -> u32 {
let mut z = Complex::new(0.0, 0.0);
let c = Complex::new(cx, cy);
for i in 0..=max_iters {
if z.norm() > 2.0 {
return i;
}
z = z * z + c;
}
max_iters
}
fn main() {
let resolution = 1024;
let max_iters = 100;
generate_set(
"fractal.png".to_string(),
max_iters,
vec!["#1C448E", "#6F8695", "#CEC288", "#FFE381", "#DBFE87"],
-1.5,
-1.0,
0.5,
1.0,
resolution,
);
}
Переносим код на NVIDIA CUDA
Связать C++ с Rust можно через библиотеки libc и cc (источник). Добавим их в Cargo.toml
:
[package]
name = "mandelbrot_set"
version = "0.1.0"
edition = "2021"
[dependencies]
num = "0.4.3"
image = "0.25.1"
+libc = "0.2.155"
+[build-dependencies]
+cc = "1.0.98"
Создадим в папке src
файлы mandelbrot_gpu.h, mandelbrot.cpp
и mandelbrot_gpu.cu
. В новых версиях CUDA встроена библиотека thrust/complex.h
для нативной поддержки комплексных чисел. Перепишем функцию num_iters
на CUDA:
#include <thrust/complex.h>
_device__ unsigned int num_iters(double cx, double cy, unsigned int max_iters) {
thrust::complex<double> z(0.0, 0.0);
thrust::complex<double> c(cx, cy);
for (unsigned int i = 0; i <= max_iters; ++i) {
if (thrust::abs(z) > 2.0) {
return i;
}
z = z * z + c;
}
return max_iters;
}
Создадим функцию gpu_mandelbrot
, которая в многопоточном режиме просчитает количество итераций для каждого пикселя изображения:
__global__ void mandelbrot_kernel(unsigned int* results, double* cx, double* cy, unsigned int max_iters, int num_points) {
int idx = blockDim.x * blockIdx.x + threadIdx.x;
if (idx < num_points) {
results[idx] = num_iters(cx[idx], cy[idx], max_iters);
}
}
unsigned int* gpu_mandelbrot(double* cx, double* cy, size_t num_points, unsigned int max_iters) {
unsigned int *dev_results, *results;
double *dev_cx, *dev_cy;
results = new unsigned int[num_points];
cudaMalloc((void**)&dev_cx, num_points * sizeof(double));
cudaMalloc((void**)&dev_cy, num_points * sizeof(double));
cudaMalloc((void**)&dev_results, num_points * sizeof(unsigned int));
cudaMemcpy(dev_cx, cx, num_points * sizeof(double), cudaMemcpyHostToDevice);
cudaMemcpy(dev_cy, cy, num_points * sizeof(double), cudaMemcpyHostToDevice);
int threads_per_block = 256;
int blocks_per_grid = (num_points + threads_per_block - 1) / threads_per_block;
mandelbrot_kernel<<<blocks_per_grid, threads_per_block>>>(dev_results, dev_cx, dev_cy, max_iters, num_points);
cudaMemcpy(results, dev_results, num_points * sizeof(unsigned int), cudaMemcpyDeviceToHost);
cudaFree(dev_cx);
cudaFree(dev_cy);
cudaFree(dev_results);
return results;
}
В файле mandelbrot.cpp
создадим основную функцию для вызова из Rust:
extern "C" {
void calculate_mandelbrot(const double* cx, const double* cy, int num_points, unsigned int max_iters, unsigned int* output) {
double* non_const_cx = const_cast<double*>(cx);
double* non_const_cy = const_cast<double*>(cy);
unsigned int* gpu_results = gpu_mandelbrot(non_const_cx, non_const_cy, num_points, max_iters);
for (int i = 0; i < num_points; ++i) {
output[i] = gpu_results[i];
}
delete[] gpu_results;
}
}
Полный код
mandelbrot_gpu.h
#ifndef MANDELBROT_GPU_H
#define MANDELBROT_GPU_H
unsigned int* gpu_mandelbrot(double* cx, double* cy, size_t num_points, unsigned int max_iters);
#endif // MANDELBROT_GPU_H
mandelbrot_gpu.cu
#include <thrust/complex.h>
#include <cuda_runtime.h>
#include "mandelbrot_gpu.h"
__device__ unsigned int num_iters(double cx, double cy, unsigned int max_iters) {
thrust::complex<double> z(0.0, 0.0);
thrust::complex<double> c(cx, cy);
for (unsigned int i = 0; i <= max_iters; ++i) {
if (thrust::abs(z) > 2.0) {
return i;
}
z = z * z + c;
}
return max_iters;
}
__global__ void mandelbrot_kernel(unsigned int* results, double* cx, double* cy, unsigned int max_iters, int num_points) {
int idx = blockDim.x * blockIdx.x + threadIdx.x;
if (idx < num_points) {
results[idx] = num_iters(cx[idx], cy[idx], max_iters);
}
}
unsigned int* gpu_mandelbrot(double* cx, double* cy, size_t num_points, unsigned int max_iters) {
unsigned int *dev_results, *results;
double *dev_cx, *dev_cy;
results = new unsigned int[num_points];
cudaMalloc((void**)&dev_cx, num_points * sizeof(double));
cudaMalloc((void**)&dev_cy, num_points * sizeof(double));
cudaMalloc((void**)&dev_results, num_points * sizeof(unsigned int));
cudaMemcpy(dev_cx, cx, num_points * sizeof(double), cudaMemcpyHostToDevice);
cudaMemcpy(dev_cy, cy, num_points * sizeof(double), cudaMemcpyHostToDevice);
int threads_per_block = 256;
int blocks_per_grid = (num_points + threads_per_block - 1) / threads_per_block;
mandelbrot_kernel<<<blocks_per_grid, threads_per_block>>>(dev_results, dev_cx, dev_cy, max_iters, num_points);
cudaMemcpy(results, dev_results, num_points * sizeof(unsigned int), cudaMemcpyDeviceToHost);
cudaFree(dev_cx);
cudaFree(dev_cy);
cudaFree(dev_results);
return results;
}
mandelbrot.cpp
#include <iostream>
#include "mandelbrot_gpu.h"
extern "C" {
void calculate_mandelbrot(const double* cx, const double* cy, int num_points, unsigned int max_iters, unsigned int* output) {
double* non_const_cx = const_cast<double*>(cx);
double* non_const_cy = const_cast<double*>(cy);
unsigned int* gpu_results = gpu_mandelbrot(non_const_cx, non_const_cy, num_points, max_iters);
for (int i = 0; i < num_points; ++i) {
output[i] = gpu_results[i];
}
delete[] gpu_results;
}
}
Создадим build.rs
— файл, который соберёт и скомпилирует CUDA‑часть:
use cc;
fn main() {
cc::Build::new()
.cuda(true)
.cpp(true)
.flag("-cudart=shared")
.flag("-ccbin=gcc")
.files(&["./src/mandelbrot.cpp", "./src/mandelbrot_gpu.cu"])
.compile("mandelbrot.a");
}
Убедитесь, что вы имеете установленный компилятор CUDA. Выполним cargo build
для сборки всего проекта:
$ cargo build
Compiling mandelbrot_set v0.1.0 (/home/danil/home/danil/RustroverProjects/mandelbrot_set)
warning: mandelbrot_set@0.1.0: nvcc warning : incompatible redefinition for option 'compiler-bindir', the last value of this option was used
warning: mandelbrot_set@0.1.0: nvcc warning : incompatible redefinition for option 'compiler-bindir', the last value of this option was used
warning: mandelbrot_set@0.1.0: nvcc warning : incompatible redefinition for option 'compiler-bindir', the last value of this option was used
warning: mandelbrot_set@0.1.0: nvcc warning : incompatible redefinition for option 'compiler-bindir', the last value of this option was used
warning: mandelbrot_set@0.1.0: nvcc warning : incompatible redefinition for option 'compiler-bindir', the last value of this option was used
warning: mandelbrot_set@0.1.0: nvcc warning : incompatible redefinition for option 'compiler-bindir', the last value of this option was used
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.87s
Если вы хотите использовать другой компилятор или получаете ошибки, замените gcc
в build.rs
на другое значение. Например:
...
.flag("-ccbin=clang")
...
Определим функцию calculate_mandelbrot
в main.rs
:
...
use libc::{c_double, c_int, c_uint};
extern "C" {
fn calculate_mandelbrot(
cx: *mut c_double,
cy: *mut c_double,
num_points: c_int,
max_iters: c_uint,
output: *mut c_uint,
);
}
...
Изменим generate_set
для работы с C++:
fn generate_set(
...
) {
...
let mut h_cx = vec![];
let mut h_cy = vec![];
let mut output = vec![0; (resolution * resolution) as usize];
for x in 0..resolution {
for y in 0..resolution {
...
h_cx.push(cx);
h_cy.push(cy);
}
}
unsafe {
calculate_mandelbrot(
h_cx.as_mut_ptr(),
h_cy.as_mut_ptr(),
(resolution * resolution) as c_int,
max_iters,
output.as_mut_ptr(),
);
}
for (x, row) in output.chunks(resolution as usize).enumerate() {
for (y, column) in row.iter().enumerate() {
let pixel = buffer.get_pixel_mut(x as u32, y as u32);
let color = gradient.get(*column as usize).unwrap_or(&[0, 0, 0]);
*pixel = Rgb(*color);
}
}
buffer.save(&file_name).unwrap();
}
Полный код main.rs
use image::{Rgb, RgbImage};
use num::complex::Complex;
use libc::{c_double, c_int, c_uint};
extern "C" {
fn calculate_mandelbrot(
cx: *mut c_double,
cy: *mut c_double,
num_points: c_int,
max_iters: c_uint,
output: *mut c_uint,
);
}
fn hex2rgb(hex: &str) -> Result<Vec<u8>, String> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Err("Invalid HEX color length".to_string());
}
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid HEX color")?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid HEX color")?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid HEX color")?;
Ok(vec![r, g, b])
}
fn lerp_color(color1: &[u8; 3], color2: &[u8; 3], value: f64) -> [u8; 3] {
[
(color1[0] as f64 + (color2[0] as f64 - color1[0] as f64) * value) as u8,
(color1[1] as f64 + (color2[1] as f64 - color1[1] as f64) * value) as u8,
(color1[2] as f64 + (color2[2] as f64 - color1[2] as f64) * value) as u8,
]
}
fn get_gradient(gradient_colors: Vec<&str>, max_iters: u32) -> Vec<[u8; 3]> {
let mut colors = vec![];
let mut gradient_colors_rgb = vec![];
for color in &gradient_colors {
let rgb = hex2rgb(color).unwrap();
gradient_colors_rgb.push([rgb[0], rgb[1], rgb[2]]);
}
for i in 0..max_iters {
let color_index = (i as usize * (gradient_colors.len() - 1)) / max_iters as usize;
let color_value = (i as f64 * (gradient_colors.len() as f64 - 1.0)) / max_iters as f64;
let value = color_value % 1.0;
colors.push(lerp_color(
&gradient_colors_rgb[color_index],
&gradient_colors_rgb[color_index + 1],
value,
));
}
colors
}
fn generate_set(
file_name: String,
max_iters: u32,
colors: Vec<&str>,
x_min: f64,
y_min: f64,
x_max: f64,
y_max: f64,
resolution: u32,
) {
let mut buffer = RgbImage::new(resolution, resolution);
let gradient = get_gradient(colors, max_iters);
let mut h_cx = vec![];
let mut h_cy = vec![];
let mut output = vec![0; (resolution * resolution) as usize];
for x in 0..resolution {
for y in 0..resolution {
let x_percent = x as f64 / resolution as f64;
let y_percent = y as f64 / resolution as f64;
let cx = x_min + (x_max - x_min) * x_percent;
let cy = y_min + (y_max - y_min) * y_percent;
h_cx.push(cx);
h_cy.push(cy);
}
}
unsafe {
calculate_mandelbrot(
h_cx.as_mut_ptr(),
h_cy.as_mut_ptr(),
(resolution * resolution) as c_int,
max_iters,
output.as_mut_ptr(),
);
}
for (x, row) in output.chunks(resolution as usize).enumerate() {
for (y, column) in row.iter().enumerate() {
let pixel = buffer.get_pixel_mut(x as u32, y as u32);
let color = gradient.get(*column as usize).unwrap_or(&[0, 0, 0]);
*pixel = Rgb(*color);
}
}
buffer.save(&file_name).unwrap();
}
fn num_iters(cx: f64, cy: f64, max_iters: u32) -> u32 {
let mut z = Complex::new(0.0, 0.0);
let c = Complex::new(cx, cy);
for i in 0..=max_iters {
if z.norm() > 2.0 {
return i;
}
z = z * z + c;
}
max_iters
}
fn main() {
let resolution = 1024;
let max_iters = 100;
generate_set(
"fractal.png".to_string(),
max_iters,
vec!["#1C448E", "#6F8695", "#CEC288", "#FFE381", "#DBFE87"],
-1.5,
-1.0,
0.5,
1.0,
resolution,
);
}
Масштабирование
Добавим переменную scale
, которая будет отвечать за приближения к определённой точке фрактала:
fn main() {
let resolution = 1024;
let target_x = -0.6582034218739634;
let target_y = 0.44967917993930356;
let max_iters = 5000;
let scale = 500_000.0;
let x_min = target_x - (1.0 / scale);
let x_max = target_x + (1.0 / scale);
let y_min = target_y - (1.0 / scale);
let y_max = target_y + (1.0 / scale);
generate_set(
"fractal.png".to_string(),
...
x_min,
y_min,
x_max,
y_max,
resolution,
);
}
К сожалению, из-за ограниченной точности float64 качественный масштаб может быть выполнен до раз:
let resolution = 256;
...
let scale = 10f64.powf(15.0);
...
Сглаживание
Как видно из последней визуализации, наше изображение имеет большое количество "случайных точек". Это не является ошибкой, но создаёт неприятный шум.
Решить эту проблему можно путём добавления сэмплирования - это наиболее простой, но прожорливый метод. Его суть заключается в том, что мы раз случайным образом изменяем координату целевой точки, просчитываем iters, берём среднее значение итераций и красим целевую точку. Вот, как это выглядит.
Для генерации случайных значений, добавим соответствующую библиотеку:
[package]
name = "mandelbrot_set"
version = "0.1.0"
edition = "2021"
[dependencies]
num = "0.4.3"
image = "0.25.1"
libc = "0.2.155"
+rand = "0.9.0-alpha.1"
[build-dependencies]
cc = "1.0.98"
Изменим generate_set
:
use rand::Rng;
fn generate_set(
...
samples: u32,
...
) {
let mut rng = rand::thread_rng();
...
for _ in 0..samples {
...
let x_percent = (x as f64 + rng.gen::<f64>()) / resolution as f64;
let y_percent = (y as f64 + rng.gen::<f64>()) / resolution as f64;
...
}
unsafe {
calculate_mandelbrot(
...
(resolution * resolution * samples) as c_int,
...
);
}
for (x, row) in output.chunks((resolution * samples) as usize).enumerate() {
for (y, column) in row.chunks(samples as usize).enumerate() {
let mut sum = 0;
for iteration in column {
sum += *iteration as usize;
}
let pixel = buffer.get_pixel_mut(x as u32, y as u32);
let color = gradient.get(sum / column.len()).unwrap_or(&[0, 0, 0]);
*pixel = Rgb(*color);
}
}
}
fn main() {
...
let samples = 16;
...
generate_set(
...
samples
);
}
Выглядит гораздо лучше!
Финальные штрихи
Воспользуемся экспоненциальным ростом масштаба, чтобы анимировать масштабирование фрактала:
let scale = 10.0_f64.powf((i as f64 / frames as f64) * args.max_scale.ilog10() as f64);
Добавим библиотеку rayon для рендера в многопоточном режиме и clap для поддержки аргументов командной строки. Итоговая версия проекта лежит на GitHub:
$ git clone https://github.com/0x7o/mandelbrot_set
$ cd mandelbrot_set
$ cargo build --release
$ ./target/release/mandelbrot_set
--resolution 1024
--colors "#00A3BC, #8B00BD, #81BD00, #BD5400"
--x -0.6582034218739634
--y 0.44967917993930356
--iters 3000
--max-scale 1000000000000000
--fps 24
--seconds 60
--n-samples 4
--output ./output
$ ffmpeg -framerate 24 -i output/frame_%09d.png -c:v libx264 -pix_fmt yuv420p -crf 18 -y video.mp4
Спасибо за прочтение! Я только начинаю изучать Rust, поэтому код далек от совершенства
Автор: Данил