use bevy::{ diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}, prelude::*, reflect::TypePath, render::render_resource::{AsBindGroup, ShaderRef, ShaderType}, }; use bevy_egui::{ egui::{self, RichText}, EguiContexts, EguiPlugin, }; use bevy_pixel_buffer::prelude::*; fn main() { App::new() .add_plugins(( DefaultPlugins, EguiPlugin, FrameTimeDiagnosticsPlugin::default(), PixelBufferPlugin, ComputeShaderPlugin::::default(), )) .add_systems(Startup, setup) .add_systems(Update, (process_input, ui)) .run(); } fn setup( mut commands: Commands, mut images: ResMut>, mut cs: ResMut>, ) { PixelBufferBuilder::new() .with_size((1280, 720)) .with_fill(Fill::window().with_stretch(true).with_scaling_multiple(8)) .spawn(&mut commands, &mut images) .entity() .insert(cs.add(MandelbrotSetShader::default())); } fn process_input( pb: Query<&Handle>, mut cs: ResMut>, keyboard_input: Res>, ) { let state = &mut cs.get_mut(pb.single()).unwrap().params; const MOVE_SPEED: f32 = 0.02; if keyboard_input.pressed(KeyCode::KeyA) { state.center.x -= state.scale * MOVE_SPEED; } if keyboard_input.pressed(KeyCode::KeyD) { state.center.x += state.scale * MOVE_SPEED; } if keyboard_input.pressed(KeyCode::KeyW) { state.center.y += state.scale * MOVE_SPEED; } if keyboard_input.pressed(KeyCode::KeyS) { state.center.y -= state.scale * MOVE_SPEED; } const SCALE_SPEED: f32 = 1.1; if keyboard_input.pressed(KeyCode::KeyQ) { state.scale *= SCALE_SPEED; } if keyboard_input.pressed(KeyCode::KeyE) { state.scale /= SCALE_SPEED; } } fn ui( mut egui_ctx: EguiContexts, pb: Query<&Handle>, mut cs: ResMut>, diagnostics: Res, ) { let params = &mut cs.get_mut(pb.single()).unwrap().params; let fps = diagnostics .get(&FrameTimeDiagnosticsPlugin::FPS) .unwrap() .average() .unwrap_or_default(); let ctx = egui_ctx.ctx_mut(); egui::Window::new("Mandelbrot set visualization").show(ctx, |ui| { ui.collapsing(RichText::new("About").heading(), |ui| { ui.label(concat!( "Mandelbrot set visuzalizer in the GPU. Fast. ", "CPU alternative can zoom more because it uses 64 bits of precision, ", "which is currently not supported by WGSL." )); }); ui.heading("Controls"); egui::Grid::new("controls") .num_columns(2) .striped(true) .show(ui, |ui| { ui.label("Move"); ui.label("WASD / Arrows"); ui.end_row(); ui.label("Zoom in"); ui.label("Q"); ui.end_row(); ui.label("Zoom out"); ui.label("E"); ui.end_row(); }); ui.separator(); ui.horizontal(|ui| { ui.label("FPS: "); ui.label(RichText::new(format!("{fps:.1}")).code()); }); ui.horizontal(|ui| { ui.label("Max iterations"); ui.add(egui::Slider::new(&mut params.max_iter, 64..=2048)); }); }); } #[derive(Asset, AsBindGroup, TypePath, Clone, Debug, Default)] #[type_path = "example::mandelbrot_set_shader"] struct MandelbrotSetShader { #[uniform(0)] params: Params, } #[derive(ShaderType, Clone, Debug)] struct Params { max_iter: i32, scale: f32, center: Vec2, } impl Default for Params { fn default() -> Self { Self { max_iter: 128, scale: 1.0, center: Vec2::ZERO, } } } impl ComputeShader for MandelbrotSetShader { fn shader() -> ShaderRef { "mandelbrot.wgsl".into() } fn entry_point() -> std::borrow::Cow<'static, str> { "update".into() } fn workgroups(texture_size: UVec2) -> UVec2 { texture_size / 8 } }