use crate::{ camera::Camera, light::Light, material::*, node::*, primitive::*, scene::*, state::{RaytracingOption, INIT_FILE, SAVE_FILE}, }; use imgui::*; use nalgebra::{Point3, Vector3}; use pixels::{wgpu, PixelsContext}; use rhai::Engine; use std::time::{Duration, Instant}; //BUFFER CONSTANTS const BUFFER_PROPORTION_MIN: f32 = 0.1; const BUFFER_PROPORTION_MAX: f32 = 1.0; //RAY CONSTANTS const MIN_THREADS: u32 = 1; const MAX_THREADS: u32 = 12; const RAYS_MIN: u32 = 100; const RAYS_MAX: u32 = 10000; const MIN_DEPTH: u8 = 1; const MAX_DEPTH: u8 = 10; const MIN_SAMPLES: u32 = 1; const MAX_SAMPLES: u32 = 10; const MIN_RANDOM: f64 = 100.0; const MAX_RANDOM: f64 = 1000.0; const MIN_EPSILON: f64 = 1e-11; const MAX_EPSILON: f64 = 1.0; //DIFFUSE CONSTANTS const MIN_DIFFUSE_RAYS: u8 = 1; const MAX_DIFFUSE_RAYS: u8 = 10; const MIN_DIFFUSE_COEFFICIENT: f32 = 0.0; const MAX_DIFFUSE_COEFFICIENT: f32 = 1.0; //MATERIAL CONSTANTS const MIN_SHINE: f32 = 0.0; const MAX_SHINE: f32 = 50.0; //TRANSFORMATION CONSTANTS const MIN_FALLOFF: f32 = 0.0; const MIN_SCALE: f64 = 0.0; //const MIN_POSITION: f64 = -10.0; const MIN_ROTATION: f64 = -180.0; const MIN_TRANSLATE: f64 = -10.0; //-- const MAX_FALLOFF: f32 = 1.0; const MAX_SCALE: f64 = 3.0; //const MAX_POSITION: f64 = 10.0; const MAX_ROTATION: f64 = 180.0; const MAX_TRANSLATE: f64 = 10.0; // CAMERA CONSTANTS const MIN_FOV: f64 = 10.0; const MAX_FOV: f64 = 160.0; //const CAMERA_INIT: f32 = 5.0; /// Manages all state required for rendering Dear ImGui over `Pixels`test. pub enum GuiEvent { RaytracerOption(RaytracingOption), CameraUpdate(Camera), SceneLoad(Scene), SaveImage(String), } pub struct Gui { imgui: imgui::Context, platform: imgui_winit_support::WinitPlatform, renderer: imgui_wgpu::Renderer, last_frame: Instant, last_cursor: Option, pub event: Option, render_start: Option, render_elapsed: Option, script_filename: String, script: String, engine: Engine, scene: Scene, raytracing_option: RaytracingOption, camera: Camera, image_filename: String, } impl Gui { /// Create Dear ImGui. pub fn new(window: &winit::window::Window, pixels: &pixels::Pixels) -> Self { // Create Dear ImGui context let mut imgui = imgui::Context::create(); imgui.set_ini_filename(None); // Initialize winit platform support let mut platform = imgui_winit_support::WinitPlatform::init(&mut imgui); platform.attach_window( imgui.io_mut(), window, imgui_winit_support::HiDpiMode::Default, ); // Configure Dear ImGui fonts let hidpi_factor = window.scale_factor(); let font_size = (16.0 * hidpi_factor) as f32; imgui.io_mut().font_global_scale = (1.0 / hidpi_factor) as f32; imgui .fonts() .add_font(&[imgui::FontSource::DefaultFontData { config: Some(imgui::FontConfig { oversample_h: 1, pixel_snap_h: true, size_pixels: font_size, ..Default::default() }), }]); // Create Dear ImGui WGPU renderer let device = pixels.device(); let queue = pixels.queue(); let config = imgui_wgpu::RendererConfig { texture_format: pixels.render_texture_format(), ..Default::default() }; let renderer = imgui_wgpu::Renderer::new(&mut imgui, device, queue, config); // Return GUI context let mut gui = Self { imgui, platform, renderer, last_frame: Instant::now(), last_cursor: None, event: None, render_start: None, render_elapsed: None, script_filename: String::from(INIT_FILE), script: String::new(), engine: init_engine(), scene: Scene::empty(), raytracing_option: RaytracingOption::default(), camera: Camera::unit(), image_filename: String::from(SAVE_FILE), }; // ------------ TESTING CODE (LOAD SCENE ON START) ----------------- match std::fs::read_to_string(&mut gui.script_filename) { Ok(script) => { gui.script = script; } Err(e) => println!("{}", e), } match gui.engine.eval(&gui.script) { Ok(scene) => { gui.scene = scene; gui.event = Some(GuiEvent::SceneLoad(gui.scene.clone())); } Err(e) => println!("{e}"), } // ------------ TESTING CODE (LOAD SCENE ON START) ----------------- gui } pub fn start_render_timer(&mut self) { self.render_start = Some(Instant::now()); self.render_elapsed = None; } pub fn stop_render_timer(&mut self) { if let Some(start) = self.render_start.take() { self.render_elapsed = Some(start.elapsed()); } } /// Prepare Dear ImGui. pub fn prepare( &mut self, window: &winit::window::Window, ) -> Result<(), winit::error::ExternalError> { let now = Instant::now(); self.imgui.io_mut().update_delta_time(now - self.last_frame); self.last_frame = now; self.platform.prepare_frame(self.imgui.io_mut(), window) } /// Render Dear ImGui. pub fn render( &mut self, window: &winit::window::Window, encoder: &mut wgpu::CommandEncoder, render_target: &wgpu::TextureView, context: &PixelsContext, ) -> imgui_wgpu::RendererResult<()> { // Start a new Dear ImGui frame and update the cursor let ui = self.imgui.new_frame(); let mouse_cursor = ui.mouse_cursor(); if self.last_cursor != mouse_cursor { self.last_cursor = mouse_cursor; self.platform.prepare_render(ui, window); } //Top Menu Bar // let mut about_open = false; // ui.main_menu_bar(|| { // ui.menu("Help", || { // about_open = ui.menu_item("About..."); // }); // }); //Raytracing options ------------------------------------------- if CollapsingHeader::new("Raytracer").build(ui) { ui.slider( "Threads", MIN_THREADS, MAX_THREADS, &mut self.raytracing_option.threads, ); // Numbers of rays to render per pass Drag::new("Rays Per Pass") .range(RAYS_MIN, RAYS_MAX) .speed(50.0) .build(ui, &mut self.raytracing_option.pixels_per_thread); // Proportion of the window the buffer occupies Drag::new("% Buffer: ") .range(BUFFER_PROPORTION_MIN, BUFFER_PROPORTION_MAX) .speed(0.005) .display_format("%.2f") .build(ui, &mut self.raytracing_option.buffer_proportion); //Clear colour for scene let mut clear_f32 = [ self.raytracing_option.clear_color[0] as f32 / 255.0, self.raytracing_option.clear_color[1] as f32 / 255.0, self.raytracing_option.clear_color[2] as f32 / 255.0, self.raytracing_option.clear_color[3] as f32 / 255.0, ]; if ui.color_edit4_config("Clear Colour", &mut clear_f32).alpha_bar(true).build() { self.raytracing_option.clear_color = [ (clear_f32[0] * 255.0) as u8, (clear_f32[1] * 255.0) as u8, (clear_f32[2] * 255.0) as u8, (clear_f32[3] * 255.0) as u8, ]; } //Clear colour if no intersect let mut pixel_clear_f32 = [ self.raytracing_option.pixel_clear[0] as f32 / 255.0, self.raytracing_option.pixel_clear[1] as f32 / 255.0, self.raytracing_option.pixel_clear[2] as f32 / 255.0, self.raytracing_option.pixel_clear[3] as f32 / 255.0, ]; if ui.color_edit4_config("Pixel Clear Colour", &mut pixel_clear_f32).alpha_bar(true).build() { self.raytracing_option.pixel_clear = [ (pixel_clear_f32[0] * 255.0) as u8, (pixel_clear_f32[1] * 255.0) as u8, (pixel_clear_f32[2] * 255.0) as u8, (pixel_clear_f32[3] * 255.0) as u8, ]; } //Ray depth slider ui.slider( "Ray Depth", MIN_DEPTH, MAX_DEPTH, &mut self.raytracing_option.ray_depth, ); //Ray samples slider ui.slider( "Ray Samples", MIN_SAMPLES, MAX_SAMPLES, &mut self.raytracing_option.ray_samples, ); //Ray randomness Drag::new("Ray Randomness") .range(MIN_RANDOM, MAX_RANDOM) .speed(5.0) .display_format("%.1f") .build(ui, &mut self.raytracing_option.ray_randomness); //Number of diffuse rays ui.slider( "Diffuse Rays", MIN_DIFFUSE_RAYS, MAX_DIFFUSE_RAYS, &mut self.raytracing_option.diffuse_rays, ); //Diffuse Coefficient Drag::new("Diffuse Coefficient") .range(MIN_DIFFUSE_COEFFICIENT, MAX_DIFFUSE_COEFFICIENT) .speed(0.005) .display_format("%.3f") .build(ui, &mut self.raytracing_option.diffuse_coefficient); // Fov of the buffer ui.slider( "fov", MIN_FOV, MAX_FOV, &mut self.raytracing_option.buffer_fov, ); // Enable BVH ui.checkbox("Enable BVH", &mut self.raytracing_option.bvh_active); ui.checkbox("Enable Shadows", &mut self.raytracing_option.shadows); ui.checkbox("Enable Reflections", &mut self.raytracing_option.reflect); ui.checkbox("Enable Specular", &mut self.raytracing_option.specular); ui.checkbox("Enable Diffuse", &mut self.raytracing_option.diffuse); // Render timer display ui.separator(); if let Some(start) = &self.render_start { let elapsed = start.elapsed().as_secs_f64(); ui.text(format!("Rendering: {:.2}s", elapsed)); } else if let Some(elapsed) = &self.render_elapsed { ui.text(format!("Render time: {:.2}s", elapsed.as_secs_f64())); } ui.separator(); // Apply stored changes if ui.button("Apply") { self.event = Some(GuiEvent::RaytracerOption(self.raytracing_option.clone())); }; } // CAMERA OPTIONS ---------------------------------------- if CollapsingHeader::new("Camera").build(ui) { // Eye, target and up vector inputs ui.text("Camera options:"); Drag::new("Eye") .range(MIN_TRANSLATE, MAX_TRANSLATE) .speed(0.05) .display_format("%.2f") .build_array(ui, self.camera.eye.coords.as_mut_slice()); Drag::new("Target") .range(MIN_TRANSLATE, MAX_TRANSLATE) .speed(0.05) .display_format("%.2f") .build_array(ui, self.camera.target.coords.as_mut_slice()); Drag::new("Up") .range(0.0, 1.0) .speed(0.005) .display_format("%.3f") .build_array(ui, self.camera.up.as_mut_slice()); if ui.button("Apply Camera") { println!("Camera changed"); self.event = Some(GuiEvent::CameraUpdate(self.camera.clone())); } } // SCRIPTING -------------------------------------------- if CollapsingHeader::new("Scripting").build(ui) { // Import file into multiline script ui.input_text("Scene file", &mut self.script_filename) .build(); if ui.button("Import from File") { match std::fs::read_to_string(&mut self.script_filename) { Ok(script) => { self.script = script; } Err(e) => println!("{}", e), } } ui.same_line(); // Load scene from multiline script using engine if ui.button("Load scene") { match self.engine.eval(&self.script) { Ok(scene) => { self.scene = scene; self.event = Some(GuiEvent::SceneLoad(self.scene.clone())); } Err(e) => println!("{e}"), } } ui.same_line(); // Save script to file if ui.button("Save script") { match std::fs::write(&self.script_filename, &self.script) { Ok(_) => println!("Script saved successfully"), Err(e) => println!("{}", e), } } // Multiline script ui.input_text_multiline("##", &mut self.script, [900., 300.]) .build(); } // IMAGE -------------------------------------------- if CollapsingHeader::new("Image").build(ui) { // Image filename ui.input_text("Image file", &mut self.image_filename) .build(); // Save image to file if ui.button("Save Image") { self.event = Some(GuiEvent::SaveImage(self.image_filename.clone())); } } // SCENE -------------------------------------------- if CollapsingHeader::new("Scene").build(ui) { if ui.button("Update Scene") { self.scene.compute(); self.event = Some(GuiEvent::SceneLoad(self.scene.clone())); } // Edit transformation of nodes if let Some(_t) = ui.tree_node("Nodes") { for (label, node) in &mut self.scene.nodes { ui.checkbox(format!("##active{label}"), &mut node.active); ui.same_line(); if let Some(_t) = ui.tree_node(label) { Drag::new("Translation") .range(MIN_TRANSLATE, MAX_TRANSLATE) .speed(0.05) .display_format("%.2f") .build_array(ui, &mut node.translation); Drag::new("Rotation") .range(MIN_ROTATION, MAX_ROTATION) .speed(1.0) .display_format("%.1f") .build_array(ui, &mut node.rotation); Drag::new("Scale") .range(MIN_SCALE, MAX_SCALE) .speed(0.01) .display_format("%.3f") .build_array(ui, &mut node.scale); } } } // Edit materials if let Some(_t) = ui.tree_node("Materials") { for (label, material) in &mut self.scene.materials { if let Some(_t) = ui.tree_node(label) { let mut ks_arr: [f32; 3] = material.ks.into(); if ui.color_edit3("ks", &mut ks_arr) { material.ks = Vector3::from(ks_arr); } let mut kd_arr: [f32; 3] = material.kd.into(); if ui.color_edit3("kd", &mut kd_arr) { material.kd = Vector3::from(kd_arr); } Drag::new("shine") .range(MIN_SHINE, MAX_SHINE) .speed(0.5) .display_format("%.1f") .build(ui, &mut material.shininess); } } } //Edit color, position and falloff of lights if let Some(_t) = ui.tree_node("Lights") { for (label, light) in &mut self.scene.lights { ui.checkbox(format!("##activelight{label}"), &mut light.active); ui.same_line(); if let Some(_t) = ui.tree_node(label) { let mut colour_arr: [f32; 3] = light.colour.into(); if ui.color_edit3("Colour", &mut colour_arr) { light.colour = Vector3::from(colour_arr); } Drag::new("Position") .range(MIN_TRANSLATE, MAX_TRANSLATE) .speed(0.05) .display_format("%.2f") .build_array(ui, light.position.coords.as_mut_slice()); Drag::new("Falloff") .range(MIN_FALLOFF, MAX_FALLOFF) .speed(0.005) .display_format("%.3f") .build_array(ui, light.falloff.as_mut_slice()); } } } //Use different cameras in the scene if let Some(_t) = ui.tree_node("Cameras") { for (label, camera) in &self.scene.cameras { if ui.button(label) { self.camera = camera.clone(); self.event = Some(GuiEvent::CameraUpdate(camera.clone())); } } } } // Render Dear ImGui with WGPU let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("imgui"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: render_target, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: true, }, })], depth_stencil_attachment: None, }); self.renderer.render( self.imgui.render(), &context.queue, &context.device, &mut rpass, ) } /// Update the GUI's camera to reflect external changes (e.g. from keyboard/mouse movement) pub fn update_camera(&mut self, camera: &Camera) { self.camera = camera.clone(); } /// Handle any outstanding events. pub fn handle_event( &mut self, window: &winit::window::Window, event: &winit::event::Event<()>, ) { self.platform .handle_event(self.imgui.io_mut(), window, event); } } pub fn init_engine() -> Engine { let mut engine = Engine::new(); engine .register_type::>() .register_fn("V", Vector3::::new); engine .register_type::>() .register_fn("P", Point3::::new); engine .register_type::() .register_fn("Camera", Camera::new); engine .register_type::() .register_fn("Scene", Scene::empty) .register_fn("addNode", Scene::add_node) .register_fn("addLight", Scene::add_light) .register_fn("addCamera", Scene::add_camera) .register_fn("addMaterial", Scene::add_material); engine .register_type::() .register_fn("Node", Node::new) .register_fn("translate", Node::translate) .register_fn("rotate", Node::rotate) .register_fn("scale", Node::scale) .register_fn("child", Node::child) .register_fn("active", Node::set_active); engine .register_type::() .register_fn("Light", Light::new) .register_fn("Ambient", Light::ambient) .register_fn("active", Light::set_active); engine .register_type::() .register_fn("Material", Material::new) .register_fn("MaterialRed", Material::red) .register_fn("MaterialBlue", Material::blue) .register_fn("MaterialGreen", Material::green) .register_fn("MaterialMagenta", Material::magenta) .register_fn("MaterialTurquoise", Material::turquoise); engine .register_type::() .register_fn("Sphere", Sphere::new) .register_fn("SphereUnit", Sphere::unit); engine .register_type::() .register_fn("Cube", Cube::new) .register_fn("CubeUnit", Cube::unit); engine .register_type::() .register_fn("Triangle", Triangle::new) .register_fn("TriangleUnit", Triangle::unit); engine .register_type::() .register_fn("Cone", Cone::new) .register_fn("ConeUnit", Cone::unit); engine .register_type::() .register_fn("Cylinder", Cylinder::new); engine .register_type::() .register_fn("Circle", Circle::new) .register_fn("CircleUnit", Circle::unit); engine .register_type::() .register_fn("Cube", Cube::new) .register_fn("CubeUnit", Cube::unit); engine .register_type::() .register_fn("Steiner", Steiner::new); engine .register_type::() .register_fn("Steiner2", Steiner2::new); engine .register_type::() .register_fn("Roman", Roman::new); engine .register_type::() .register_fn("CrossCap", CrossCap::new); engine .register_type::() .register_fn("CrossCap2", CrossCap2::new); engine .register_type::() .register_fn("Torus", Torus::new); engine .register_type::() .register_fn("Gnonom", Gnonom::new); engine .register_type::() .register_fn("Mesh", Mesh::from_file); engine .register_type::() .register_fn("Rectange", RectangleXY::new) .register_fn("RectangleUnit", RectangleXY::unit); engine }