feat: working interactive mandelbrot

todo: fix the zoom issue (not completely centered)
This commit is contained in:
Rekai Nyangadzayi Musuka 2020-11-26 02:47:03 -06:00
parent d370a8fd5f
commit e8e31ad5b1
7 changed files with 3779 additions and 103 deletions

22
.cargo/config.toml Normal file
View File

@ -0,0 +1,22 @@
# Rename this file to `config.toml` to enable "fast build" configuration. Please read the notes below.
# NOTE: For maximum performance, build using a nightly compiler
# If you are using rust stable, remove the "-Zshare-generics=y" below.
[target.x86_64-unknown-linux-gnu]
linker = "/usr/bin/clang"
rustflags = ["-Clink-arg=-fuse-ld=lld", "-Zshare-generics=y"]
# NOTE: you must manually install https://github.com/michaeleisel/zld on mac. you can easily do this with the "brew" package manager:
# `brew install michaeleisel/zld/zld`
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=/usr/local/bin/zld", "-Zshare-generics=y", "-Zrun-dsymutil=no"]
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"
rustflags = ["-Zshare-generics=y"]
# Optional: Uncommenting the following improves compile times, but reduces the amount of debug info to 'line number tables only'
# In most cases the gains are negligible, but if you are on macos and have slow compile times you should see significant gains.
#[profile.dev]
#debug = 1

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
/target
/.vscode
test.png
/assets

3468
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy = "0.3"
image = "0.23"
num-complex = "0.3"
rayon = "1.5"

2
src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub use mandelbrot::Mandelbrot;
mod mandelbrot;

View File

@ -1,110 +1,107 @@
use image::{ImageBuffer, Rgb, RgbImage};
use num_complex::Complex;
use rayon::prelude::*;
const IMG_WIDTH: usize = 1920;
const IMG_HEIGHT: usize = 1080;
const MAX_ITERATIONS: u16 = 1000;
use bevy::{prelude::*, render::texture::TextureFormat};
use mandelbrot::Mandelbrot;
use std::time::Instant;
fn main() {
let mut img: RgbImage = ImageBuffer::new(IMG_WIDTH as u32, IMG_HEIGHT as u32);
escape_time(&mut img);
img.save("test.png").unwrap();
// Mandelbrot::escape_time_image();
App::build()
.add_plugins(DefaultPlugins)
.add_resource(SpriteScale::default())
.add_startup_system(png_startup.system())
.add_system(mandelbrot_render_system.system())
.add_system(mandelbrot_input_scale_system.system())
.run();
}
fn escape_time(img: &mut ImageBuffer<Rgb<u8>, Vec<u8>>) {
let mut mandelbrot: Vec<u16> = vec![0; IMG_HEIGHT * IMG_WIDTH];
struct SpriteScale {
zoom: f64,
horiz: f64,
verti: f64,
step: f64,
}
mandelbrot
.par_iter_mut()
.enumerate()
.for_each(|(i, value)| {
let px = i % IMG_WIDTH;
let py = i / IMG_WIDTH;
let c = coords_to_complex(px, py);
*value = calc_num_iterations(c);
});
for (px, py, pixel) in img.enumerate_pixels_mut() {
let i = px as usize + IMG_WIDTH * py as usize;
let num_iterations = mandelbrot[i];
match find_colour(num_iterations) {
Colour::White => *pixel = Rgb([0x00, 0x00, 0x00]),
Colour::Black => *pixel = Rgb([0xFF, 0xFF, 0xFF]),
Colour::Gray => *pixel = Rgb([0xAA, 0xAA, 0xAA]),
impl Default for SpriteScale {
fn default() -> Self {
Self {
zoom: 1.0,
horiz: 0.0,
verti: 0.0,
step: 0.01,
}
}
}
enum Colour {
White,
Black,
Gray,
}
fn mandelbrot_input_scale_system(
input: Res<Input<KeyCode>>,
mut scale: ResMut<SpriteScale>,
// camera: Res<Camera>,
) {
let zoom = input.pressed(KeyCode::Q) as i8 - input.pressed(KeyCode::E) as i8;
let horizontal = input.pressed(KeyCode::D) as i8 - input.pressed(KeyCode::A) as i8;
let vertical = input.pressed(KeyCode::W) as i8 - input.pressed(KeyCode::S) as i8;
let step_mod = input.pressed(KeyCode::R) as i8 - input.pressed(KeyCode::F) as i8;
fn find_colour(iteration: u16) -> Colour {
match iteration {
MAX_ITERATIONS => Colour::Black,
other => match other % 2 {
0 => Colour::White,
1 => Colour::Gray,
_ => unreachable!(),
},
scale.step += (scale.step / 10.0) * step_mod as f64;
scale.verti += scale.step * vertical as f64;
scale.horiz += scale.step * horizontal as f64;
scale.zoom += scale.step * zoom as f64;
if scale.zoom < 0.0 {
// We can't go below 0
scale.zoom -= scale.step * zoom as f64;
}
}
fn calc_num_iterations(c: Complex<f64>) -> u16 {
let mut z: Complex<f64> = Complex::new(0.0, 0.0);
let mut num_iterations: u16 = 0;
const X_BOUNDS: (f64, f64) = (-2.5, 1.0);
const Y_BOUNDS: (f64, f64) = (-1.0, 1.0);
for i in 0..MAX_ITERATIONS {
if z.norm_sqr() > 4.0 {
num_iterations = i;
break;
fn mandelbrot_render_system(
materials: Res<Assets<ColorMaterial>>,
mut textures: ResMut<Assets<Texture>>,
scale: Res<SpriteScale>,
mut query: Query<(&mut Mandelbrot, &Handle<ColorMaterial>)>,
) {
for (mut fractal, handle) in query.iter_mut() {
if let Some(material) = materials.get(handle) {
if let Some(texture_handle) = &material.texture {
if let Some(texture) = textures.get_mut(texture_handle) {
let z = scale.zoom;
let h = scale.horiz;
let v = scale.verti;
let start = Instant::now();
texture.data = fractal.generate_scaled_image(
((-2.5 * z) + h, (1.0 * z) + h),
((-1.0 * z) - v, (1.0 * z) - v),
);
let diff = Instant::now() - start;
dbg!(z);
dbg!(diff);
}
z = (z * z) + c;
}
num_iterations
}
fn coords_to_complex(px: usize, py: usize) -> Complex<f64> {
Complex::new(scale_width(px), scale_height(py))
}
fn scale_width(px: usize) -> f64 {
const X_MIN: f64 = -2.5;
const X_MAX: f64 = 1.0;
X_MIN + ((X_MAX - X_MIN) * (px as f64 - 0.0)) / (IMG_WIDTH as f64 - 0.0)
}
fn scale_height(py: usize) -> f64 {
const Y_MIN: f64 = -1.0;
const Y_MAX: f64 = 1.0;
Y_MIN + ((Y_MAX - Y_MIN) * (py as f64 - 0.0)) / (IMG_HEIGHT as f64 - 0.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scale_width_bounds() {
assert_eq!(scale_width(0), -2.5f64);
assert_eq!(scale_width(IMG_WIDTH), 1f64);
}
#[test]
fn scale_height_bounds() {
assert_eq!(scale_height(0), -1f64);
assert_eq!(scale_height(IMG_HEIGHT), 1f64);
}
}
fn png_startup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
let texture_handle = asset_server.load("test.png");
commands
.spawn(Camera2dComponents::default())
.spawn(SpriteComponents {
material: materials.add(texture_handle.into()),
..Default::default()
})
.with(Mandelbrot::new());
}
fn fractal_startup(mut commands: Commands) {
commands
.spawn(Camera2dComponents::default())
.spawn(SpriteComponents::default())
.with(Mandelbrot::new());
}

205
src/mandelbrot.rs Normal file
View File

@ -0,0 +1,205 @@
use image::{ImageBuffer, Rgb, RgbImage};
use num_complex::Complex;
use rayon::prelude::*;
const MAX_ITERATIONS: u16 = 256;
const IMG_WIDTH: usize = 1280;
const IMG_HEIGHT: usize = 720;
#[derive(Debug, Clone)]
pub struct Mandelbrot {
iterations: Vec<u16>,
}
enum Colour {
White,
Black,
Gray,
}
impl Mandelbrot {
pub fn new() -> Self {
Mandelbrot {
iterations: vec![0; IMG_WIDTH * IMG_HEIGHT],
}
}
pub fn generate_image(&mut self) -> Vec<u8> {
// Create the image
// Pixel Format can be Rgba8UnormSrgb
self.escape_time();
let mut texture_buffer = vec![0; (IMG_WIDTH * IMG_HEIGHT) * 4];
for i in 0..texture_buffer.len() {
if i % 4 == 0 {
let iter_i = i / 4;
match Self::find_colour(self.iterations[iter_i]) {
Colour::White => {
texture_buffer[i] = 0x00;
texture_buffer[i + 1] = 0x00;
texture_buffer[i + 2] = 0x00;
texture_buffer[i + 3] = 0xFF;
}
Colour::Black => {
texture_buffer[i] = 0xFF;
texture_buffer[i + 1] = 0xFF;
texture_buffer[i + 2] = 0xFF;
texture_buffer[i + 3] = 0xFF;
}
Colour::Gray => {
texture_buffer[i] = 0xAA;
texture_buffer[i + 1] = 0xAA;
texture_buffer[i + 2] = 0xAA;
texture_buffer[i + 3] = 0xFF;
}
}
}
}
texture_buffer
}
pub fn generate_scaled_image(&mut self, x_bounds: (f64, f64), y_bounds: (f64, f64)) -> Vec<u8> {
let mut texture_buffer = vec![0; (IMG_WIDTH * IMG_HEIGHT) * 4];
self.iterations
.par_iter_mut()
.enumerate()
.for_each(|(i, value)| {
let px = i % IMG_WIDTH;
let py = i / IMG_WIDTH;
let c = Self::coords_to_complex(px, py, x_bounds, y_bounds);
*value = Self::calc_num_iterations(c);
});
for i in 0..texture_buffer.len() {
if i % 4 == 0 {
let iter_i = i / 4;
match Self::find_colour(self.iterations[iter_i]) {
Colour::White => {
texture_buffer[i] = 0x00;
texture_buffer[i + 1] = 0x00;
texture_buffer[i + 2] = 0x00;
texture_buffer[i + 3] = 0xFF;
}
Colour::Black => {
texture_buffer[i] = 0xFF;
texture_buffer[i + 1] = 0xFF;
texture_buffer[i + 2] = 0xFF;
texture_buffer[i + 3] = 0xFF;
}
Colour::Gray => {
texture_buffer[i] = 0xAA;
texture_buffer[i + 1] = 0xAA;
texture_buffer[i + 2] = 0xAA;
texture_buffer[i + 3] = 0xFF;
}
}
}
}
texture_buffer
}
fn escape_time(&mut self) {
self.iterations
.par_iter_mut()
.enumerate()
.for_each(|(i, value)| {
let px = i % IMG_WIDTH;
let py = i / IMG_WIDTH;
let c = Self::coords_to_complex(px, py, (-2.5, 1.0), (-1.0, 1.0));
*value = Self::calc_num_iterations(c);
});
}
pub fn escape_time_image() {
const IMG_WIDTH: usize = 1280;
const IMG_HEIGHT: usize = 720;
let mut img: RgbImage = ImageBuffer::new(IMG_WIDTH as u32, IMG_HEIGHT as u32);
let mut mandelbrot: Vec<u16> = vec![0; IMG_HEIGHT * IMG_WIDTH];
mandelbrot
.par_iter_mut()
.enumerate()
.for_each(|(i, value)| {
let px = i % IMG_WIDTH;
let py = i / IMG_WIDTH;
let c = Self::coords_to_complex(px, py, (-2.5, 1.0), (-1.0, 1.0));
*value = Self::calc_num_iterations(c);
});
for (px, py, pixel) in img.enumerate_pixels_mut() {
let i = px as usize + IMG_WIDTH * py as usize;
let num_iterations = mandelbrot[i];
match Self::find_colour(num_iterations) {
Colour::White => *pixel = Rgb([0x00, 0x00, 0x00]),
Colour::Black => *pixel = Rgb([0xFF, 0xFF, 0xFF]),
Colour::Gray => *pixel = Rgb([0xAA, 0xAA, 0xAA]),
}
}
img.save("assets/test.png").unwrap();
}
fn find_colour(iteration: u16) -> Colour {
match iteration {
MAX_ITERATIONS => Colour::Black,
other => match other % 2 {
0 => Colour::White,
1 => Colour::Gray,
_ => unreachable!(),
},
}
}
fn calc_num_iterations(c: Complex<f64>) -> u16 {
let mut z: Complex<f64> = Complex::new(0.0, 0.0);
let mut num_iterations: u16 = 0;
for i in 0..MAX_ITERATIONS {
if z.norm_sqr() > 4.0 {
num_iterations = i;
break;
}
z = (z * z) + c;
}
num_iterations
}
fn coords_to_complex(
px: usize,
py: usize,
x_bounds: (f64, f64),
y_bounds: (f64, f64),
) -> Complex<f64> {
Complex::new(
Self::scale_width(px, x_bounds),
Self::scale_height(py, y_bounds),
)
}
fn scale_width(px: usize, x_bounds: (f64, f64)) -> f64 {
// const X_MIN: f64 = -2.5;
// const X_MAX: f64 = 1.0;
x_bounds.0 + ((x_bounds.1 - x_bounds.0) * (px as f64 - 0.0)) / IMG_WIDTH as f64 - 0.0
}
fn scale_height(py: usize, y_bounds: (f64, f64)) -> f64 {
// const Y_MIN: f64 = -1.0;
// const Y_MAX: f64 = 1.0;
y_bounds.0 + ((y_bounds.1 - y_bounds.0) * (py as f64 - 0.0)) / IMG_HEIGHT as f64 - 0.0
}
}