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
|
||||
/.vscode
|
||||
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
|
||||
|
||||
[dependencies]
|
||||
bevy = "0.3"
|
||||
image = "0.23"
|
||||
num-complex = "0.3"
|
||||
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 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;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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);
|
||||
dbg!(z);
|
||||
dbg!(diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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