Files
rust-raytracer/src/gui.rs

616 lines
22 KiB
Rust

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<imgui::MouseCursor>,
pub event: Option<GuiEvent>,
render_start: Option<Instant>,
render_elapsed: Option<Duration>,
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::<Vector3<f64>>()
.register_fn("V", Vector3::<f64>::new);
engine
.register_type::<Point3<f64>>()
.register_fn("P", Point3::<f64>::new);
engine
.register_type::<Camera>()
.register_fn("Camera", Camera::new);
engine
.register_type::<Scene>()
.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::<Node>()
.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::<Light>()
.register_fn("Light", Light::new)
.register_fn("Ambient", Light::ambient)
.register_fn("active", Light::set_active);
engine
.register_type::<Material>()
.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::<Sphere>()
.register_fn("Sphere", Sphere::new)
.register_fn("SphereUnit", Sphere::unit);
engine
.register_type::<Cube>()
.register_fn("Cube", Cube::new)
.register_fn("CubeUnit", Cube::unit);
engine
.register_type::<Triangle>()
.register_fn("Triangle", Triangle::new)
.register_fn("TriangleUnit", Triangle::unit);
engine
.register_type::<Cone>()
.register_fn("Cone", Cone::new)
.register_fn("ConeUnit", Cone::unit);
engine
.register_type::<Cylinder>()
.register_fn("Cylinder", Cylinder::new);
engine
.register_type::<Circle>()
.register_fn("Circle", Circle::new)
.register_fn("CircleUnit", Circle::unit);
engine
.register_type::<Cube>()
.register_fn("Cube", Cube::new)
.register_fn("CubeUnit", Cube::unit);
engine
.register_type::<Steiner>()
.register_fn("Steiner", Steiner::new);
engine
.register_type::<Steiner2>()
.register_fn("Steiner2", Steiner2::new);
engine
.register_type::<Roman>()
.register_fn("Roman", Roman::new);
engine
.register_type::<CrossCap>()
.register_fn("CrossCap", CrossCap::new);
engine
.register_type::<CrossCap2>()
.register_fn("CrossCap2", CrossCap2::new);
engine
.register_type::<Torus>()
.register_fn("Torus", Torus::new);
engine
.register_type::<Gnonom>()
.register_fn("Gnonom", Gnonom::new);
engine
.register_type::<Mesh>()
.register_fn("Mesh", Mesh::from_file);
engine
.register_type::<RectangleXY>()
.register_fn("Rectange", RectangleXY::new)
.register_fn("RectangleUnit", RectangleXY::unit);
engine
}