feat: working interactive mandelbrot
todo: fix the zoom issue (not completely centered)
This commit is contained in:
parent
d370a8fd5f
commit
e8e31ad5b1
|
@ -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,3 +1,4 @@
|
||||||
/target
|
/target
|
||||||
/.vscode
|
/.vscode
|
||||||
test.png
|
test.png
|
||||||
|
/assets
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,6 +7,7 @@ edition = "2018"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bevy = "0.3"
|
||||||
image = "0.23"
|
image = "0.23"
|
||||||
num-complex = "0.3"
|
num-complex = "0.3"
|
||||||
rayon = "1.5"
|
rayon = "1.5"
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub use mandelbrot::Mandelbrot;
|
||||||
|
mod mandelbrot;
|
181
src/main.rs
181
src/main.rs
|
@ -1,110 +1,107 @@
|
||||||
use image::{ImageBuffer, Rgb, RgbImage};
|
use bevy::{prelude::*, render::texture::TextureFormat};
|
||||||
use num_complex::Complex;
|
use mandelbrot::Mandelbrot;
|
||||||
use rayon::prelude::*;
|
use std::time::Instant;
|
||||||
|
|
||||||
const IMG_WIDTH: usize = 1920;
|
|
||||||
const IMG_HEIGHT: usize = 1080;
|
|
||||||
const MAX_ITERATIONS: u16 = 1000;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut img: RgbImage = ImageBuffer::new(IMG_WIDTH as u32, IMG_HEIGHT as u32);
|
// Mandelbrot::escape_time_image();
|
||||||
|
App::build()
|
||||||
escape_time(&mut img);
|
.add_plugins(DefaultPlugins)
|
||||||
|
.add_resource(SpriteScale::default())
|
||||||
img.save("test.png").unwrap();
|
.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>>) {
|
struct SpriteScale {
|
||||||
let mut mandelbrot: Vec<u16> = vec![0; IMG_HEIGHT * IMG_WIDTH];
|
zoom: f64,
|
||||||
|
horiz: f64,
|
||||||
|
verti: f64,
|
||||||
|
step: f64,
|
||||||
|
}
|
||||||
|
|
||||||
mandelbrot
|
impl Default for SpriteScale {
|
||||||
.par_iter_mut()
|
fn default() -> Self {
|
||||||
.enumerate()
|
Self {
|
||||||
.for_each(|(i, value)| {
|
zoom: 1.0,
|
||||||
let px = i % IMG_WIDTH;
|
horiz: 0.0,
|
||||||
let py = i / IMG_WIDTH;
|
verti: 0.0,
|
||||||
|
step: 0.01,
|
||||||
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]),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Colour {
|
fn mandelbrot_input_scale_system(
|
||||||
White,
|
input: Res<Input<KeyCode>>,
|
||||||
Black,
|
mut scale: ResMut<SpriteScale>,
|
||||||
Gray,
|
// 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 {
|
scale.step += (scale.step / 10.0) * step_mod as f64;
|
||||||
match iteration {
|
scale.verti += scale.step * vertical as f64;
|
||||||
MAX_ITERATIONS => Colour::Black,
|
scale.horiz += scale.step * horizontal as f64;
|
||||||
other => match other % 2 {
|
scale.zoom += scale.step * zoom as f64;
|
||||||
0 => Colour::White,
|
|
||||||
1 => Colour::Gray,
|
if scale.zoom < 0.0 {
|
||||||
_ => unreachable!(),
|
// We can't go below 0
|
||||||
},
|
scale.zoom -= scale.step * zoom as f64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calc_num_iterations(c: Complex<f64>) -> u16 {
|
const X_BOUNDS: (f64, f64) = (-2.5, 1.0);
|
||||||
let mut z: Complex<f64> = Complex::new(0.0, 0.0);
|
const Y_BOUNDS: (f64, f64) = (-1.0, 1.0);
|
||||||
let mut num_iterations: u16 = 0;
|
|
||||||
|
|
||||||
for i in 0..MAX_ITERATIONS {
|
fn mandelbrot_render_system(
|
||||||
if z.norm_sqr() > 4.0 {
|
materials: Res<Assets<ColorMaterial>>,
|
||||||
num_iterations = i;
|
mut textures: ResMut<Assets<Texture>>,
|
||||||
break;
|
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;
|
||||||
|
|
||||||
z = (z * z) + c;
|
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;
|
||||||
|
|
||||||
num_iterations
|
dbg!(z);
|
||||||
}
|
dbg!(diff);
|
||||||
|
|
||||||
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());
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue