vulkano/examples/interactive-fractal/main.rs

393 lines
13 KiB
Rust
Raw Normal View History

// This is an example demonstrating an application with some more non-trivial functionality.
// It should get you more up to speed with how you can use Vulkano.
//
// It contains:
//
// - A compute pipeline to calculate Mandelbrot and Julia fractals writing them to an image.
// - A graphics pipeline to draw the fractal image over a quad that covers the whole screen.
// - A renderpass rendering that image on the swapchain image.
// - An organized renderer with functionality good enough to copy to other projects.
// - A simple `FractalApp` to handle runtime state.
// - A simple `InputState` to interact with the application.
use fractal_compute_pipeline::FractalComputePipeline;
use glam::Vec2;
use input::InputState;
use place_over_frame::RenderPassPlaceOverFrame;
use std::{
error::Error,
sync::Arc,
time::{Duration, Instant},
};
use vulkano::{
command_buffer::allocator::{
StandardCommandBufferAllocator, StandardCommandBufferAllocatorCreateInfo,
},
descriptor_set::allocator::StandardDescriptorSetAllocator,
image::ImageUsage,
swapchain::PresentMode,
sync::GpuFuture,
};
use vulkano_util::{
context::{VulkanoConfig, VulkanoContext},
renderer::{VulkanoWindowRenderer, DEFAULT_IMAGE_FORMAT},
window::{VulkanoWindows, WindowDescriptor},
};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop},
window::{Fullscreen, WindowId},
};
mod fractal_compute_pipeline;
mod input;
mod pixels_draw_pipeline;
mod place_over_frame;
const MAX_ITERS_INIT: u32 = 200;
const MOVE_SPEED: f32 = 0.5;
fn main() -> Result<(), impl Error> {
// Create the event loop.
let event_loop = EventLoop::new().unwrap();
let mut app = App::new(&event_loop);
println!(
"\
Usage:
WASD: Pan view
Scroll: Zoom in/out
Space: Toggle between Mandelbrot and Julia
Enter: Randomize color palette
Equals/Minus: Increase/Decrease max iterations
F: Toggle full-screen
Right mouse: Stop movement in Julia (mouse position determines c)
Esc: Quit\
",
);
event_loop.run_app(&mut app)
}
struct App {
context: VulkanoContext,
windows: VulkanoWindows,
descriptor_set_allocator: Arc<StandardDescriptorSetAllocator>,
command_buffer_allocator: Arc<StandardCommandBufferAllocator>,
rcx: Option<RenderContext>,
}
struct RenderContext {
/// Pipeline that computes Mandelbrot & Julia fractals and writes them to an image.
fractal_pipeline: FractalComputePipeline,
/// Our render pipeline (pass).
place_over_frame: RenderPassPlaceOverFrame,
/// Toggle that flips between Julia and Mandelbrot.
is_julia: bool,
/// Toggle that stops the movement on Julia.
is_c_paused: bool,
/// C is a constant input to Julia escape time algorithm (mouse position).
c: Vec2,
/// Our zoom level.
scale: Vec2,
/// Our translation on the complex plane.
translation: Vec2,
/// How long the escape time algorithm should run (higher = less performance, more accurate
/// image).
max_iters: u32,
/// Time tracking, useful for frame independent movement.
time: Instant,
dt: f32,
dt_sum: f32,
frame_count: f32,
avg_fps: f32,
/// Input state to handle mouse positions, continuous movement etc.
input_state: InputState,
render_target_id: usize,
}
impl App {
fn new(_event_loop: &EventLoop<()>) -> Self {
let context = VulkanoContext::new(VulkanoConfig::default());
let windows = VulkanoWindows::default();
let descriptor_set_allocator = Arc::new(StandardDescriptorSetAllocator::new(
context.device().clone(),
Default::default(),
));
let command_buffer_allocator = Arc::new(StandardCommandBufferAllocator::new(
context.device().clone(),
StandardCommandBufferAllocatorCreateInfo {
secondary_buffer_count: 32,
..Default::default()
},
));
App {
context,
windows,
descriptor_set_allocator,
command_buffer_allocator,
rcx: None,
}
}
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let _id = self.windows.create_window(
event_loop,
&self.context,
&WindowDescriptor {
title: "Fractal".to_string(),
present_mode: PresentMode::Fifo,
..Default::default()
},
|_| {},
);
// Add our render target image onto which we'll be rendering our fractals.
let render_target_id = 0;
let window_renderer = self.windows.get_primary_renderer_mut().unwrap();
// Make sure the image usage is correct (based on your pipeline).
window_renderer.add_additional_image_view(
render_target_id,
DEFAULT_IMAGE_FORMAT,
ImageUsage::SAMPLED | ImageUsage::STORAGE | ImageUsage::TRANSFER_DST,
);
let gfx_queue = self.context.graphics_queue();
self.rcx = Some(RenderContext {
render_target_id,
fractal_pipeline: FractalComputePipeline::new(
gfx_queue.clone(),
self.context.memory_allocator().clone(),
self.command_buffer_allocator.clone(),
self.descriptor_set_allocator.clone(),
),
place_over_frame: RenderPassPlaceOverFrame::new(
gfx_queue.clone(),
self.command_buffer_allocator.clone(),
self.descriptor_set_allocator.clone(),
window_renderer.swapchain_format(),
window_renderer.swapchain_image_views(),
),
is_julia: false,
is_c_paused: false,
c: Vec2::new(0.0, 0.0),
scale: Vec2::new(4.0, 4.0),
translation: Vec2::new(0.0, 0.0),
max_iters: MAX_ITERS_INIT,
time: Instant::now(),
dt: 0.0,
dt_sum: 0.0,
frame_count: 0.0,
avg_fps: 0.0,
input_state: InputState::new(),
});
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_window_id: WindowId,
event: WindowEvent,
) {
let renderer = self.windows.get_primary_renderer_mut().unwrap();
let rcx = self.rcx.as_mut().unwrap();
let window_size = renderer.window().inner_size();
match event {
WindowEvent::CloseRequested => {
event_loop.exit();
}
WindowEvent::Resized(..) | WindowEvent::ScaleFactorChanged { .. } => {
renderer.resize();
}
WindowEvent::RedrawRequested => {
// Tasks for redrawing:
// 1. Update state based on events
// 2. Compute & Render
// 3. Reset input state
// 4. Update time & title
// Skip this frame when minimized.
if window_size.width == 0 || window_size.height == 0 {
return;
}
rcx.update_state_after_inputs(renderer);
// Start the frame.
let before_pipeline_future = match renderer.acquire(
Some(Duration::from_millis(1000)),
|swapchain_image_views| {
rcx.place_over_frame
.recreate_framebuffers(swapchain_image_views)
},
) {
Err(e) => {
println!("{e}");
return;
}
Ok(future) => future,
};
// Retrieve the target image.
let image = renderer.get_additional_image_view(rcx.render_target_id);
// Compute our fractal (writes to target image). Join future with
// `before_pipeline_future`.
let after_compute = rcx
.fractal_pipeline
.compute(
image.clone(),
rcx.c,
rcx.scale,
rcx.translation,
rcx.max_iters,
rcx.is_julia,
)
.join(before_pipeline_future);
// Render the image over the swapchain image, inputting the previous future.
let after_renderpass_future = rcx.place_over_frame.render(
after_compute,
image,
renderer.swapchain_image_view(),
renderer.image_index(),
);
// Finish the frame (which presents the view), inputting the last future. Wait for
// the future so resources are not in use when we render.
renderer.present(after_renderpass_future, true);
rcx.input_state.reset();
rcx.update_time();
renderer.window().set_title(&format!(
"{} fps: {:.2} dt: {:.2}, Max Iterations: {}",
if rcx.is_julia { "Julia" } else { "Mandelbrot" },
rcx.avg_fps(),
rcx.dt(),
rcx.max_iters
));
}
_ => {
// Pass event for the app to handle our inputs.
rcx.input_state.handle_input(window_size, &event);
}
}
if rcx.input_state.should_quit {
event_loop.exit();
}
}
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
let window_renderer = self.windows.get_primary_renderer_mut().unwrap();
window_renderer.window().request_redraw();
}
}
impl RenderContext {
/// Updates app state based on input state.
fn update_state_after_inputs(&mut self, renderer: &mut VulkanoWindowRenderer) {
// Zoom in or out.
if self.input_state.scroll_delta > 0. {
self.scale /= 1.05;
} else if self.input_state.scroll_delta < 0. {
self.scale *= 1.05;
}
// Move speed scaled by zoom level.
let move_speed = MOVE_SPEED * self.dt * self.scale.x;
// Panning.
if self.input_state.pan_up {
self.translation += Vec2::new(0.0, move_speed);
}
if self.input_state.pan_down {
self.translation += Vec2::new(0.0, -move_speed);
}
if self.input_state.pan_right {
self.translation += Vec2::new(move_speed, 0.0);
}
if self.input_state.pan_left {
self.translation += Vec2::new(-move_speed, 0.0);
}
// Toggle between Julia and Mandelbrot.
if self.input_state.toggle_julia {
self.is_julia = !self.is_julia;
}
// Toggle c.
if self.input_state.toggle_c {
self.is_c_paused = !self.is_c_paused;
}
// Update c.
if !self.is_c_paused {
// Scale normalized mouse pos between -1.0 and 1.0.
let mouse_pos = self.input_state.normalized_mouse_pos() * 2.0 - Vec2::new(1.0, 1.0);
// Scale by our zoom (scale) level so when zooming in the movement on Julia is not so
// drastic.
self.c = mouse_pos * self.scale.x;
}
// Update how many iterations we have.
if self.input_state.increase_iterations {
self.max_iters += 1;
}
if self.input_state.decrease_iterations {
if self.max_iters as i32 - 1 <= 0 {
self.max_iters = 0;
} else {
self.max_iters -= 1;
}
}
// Randomize our palette.
if self.input_state.randomize_palette {
self.fractal_pipeline.randomize_palette();
}
// Toggle full-screen.
if self.input_state.toggle_full_screen {
let is_full_screen = renderer.window().fullscreen().is_some();
renderer.window().set_fullscreen(if !is_full_screen {
Some(Fullscreen::Borderless(renderer.window().current_monitor()))
} else {
None
});
}
}
/// Returns the average FPS.
fn avg_fps(&self) -> f32 {
self.avg_fps
}
/// Returns the delta time in milliseconds.
fn dt(&self) -> f32 {
self.dt * 1000.0
}
/// Updates times and dt at the end of each frame.
fn update_time(&mut self) {
// Each second, update average fps & reset frame count & dt sum.
if self.dt_sum > 1.0 {
self.avg_fps = self.frame_count / self.dt_sum;
self.frame_count = 0.0;
self.dt_sum = 0.0;
}
self.dt = self.time.elapsed().as_secs_f32();
self.dt_sum += self.dt;
self.frame_count += 1.0;
self.time = Instant::now();
}
}