diff --git a/result_image.png b/result_image.png index f75d13a..7bb8cb3 100644 Binary files a/result_image.png and b/result_image.png differ diff --git a/scenes/cube_on_plane.blend b/scenes/cube_on_plane.blend index 3e9ed9c..f8155e4 100644 Binary files a/scenes/cube_on_plane.blend and b/scenes/cube_on_plane.blend differ diff --git a/scenes/cube_on_plane.blend1 b/scenes/cube_on_plane.blend1 index 95f8750..3e9ed9c 100644 Binary files a/scenes/cube_on_plane.blend1 and b/scenes/cube_on_plane.blend1 differ diff --git a/scenes/cube_on_plane.glb b/scenes/cube_on_plane.glb index 81d23fb..507d0c8 100644 Binary files a/scenes/cube_on_plane.glb and b/scenes/cube_on_plane.glb differ diff --git a/scenes/emissive_cube.blend b/scenes/emissive_cube.blend new file mode 100644 index 0000000..b3469b9 Binary files /dev/null and b/scenes/emissive_cube.blend differ diff --git a/scenes/emissive_cube.blend1 b/scenes/emissive_cube.blend1 new file mode 100644 index 0000000..7b82a52 Binary files /dev/null and b/scenes/emissive_cube.blend1 differ diff --git a/scenes/emissive_cube.glb b/scenes/emissive_cube.glb new file mode 100644 index 0000000..876df6c Binary files /dev/null and b/scenes/emissive_cube.glb differ diff --git a/src/colors.rs b/src/colors.rs new file mode 100644 index 0000000..37fae8b --- /dev/null +++ b/src/colors.rs @@ -0,0 +1,105 @@ +use std::ops::{Mul, MulAssign}; +use cgmath::{ElementWise, Vector4}; +use image::{DynamicImage, GenericImage, Rgba}; + +/// normalizes the color for k rays on a single pixel +pub fn normalize_color_single_pixel(radiosity_vector: Vector4, rays_per_pixel: usize) -> Rgba { + radiosity_vector.map(|component| + f32::ceil(component / rays_per_pixel as f32) as u8 + ); + + let radiosity_as_arr = [ + radiosity_vector.x, + radiosity_vector.y, + radiosity_vector.z, + ]; + + //linearly map luminosity values to [0..255] + let range: f32 = + radiosity_as_arr.iter().fold(f32::NEG_INFINITY, |a, b| a.max(*b)) - + radiosity_as_arr.iter().fold(f32::INFINITY, |a, b| a.min(*b)); + + let color_value: Vector4 = + radiosity_vector.map(|lum_val| + f32::ceil((lum_val / range) * 255.0) as u8 + ); + + //just make the pixel opaque + //TODO: consider alpha + Rgba([color_value.x, color_value.y, color_value.z, 255]) +} + +pub fn normalize_colors_global(radiosity_buffer: &mut Vec>>) -> &Vec>> { + //largest radiosity found yet + let mut maximum_colors = Vector4::new( + f32::NEG_INFINITY, + f32::NEG_INFINITY, + f32::NEG_INFINITY, + f32::NEG_INFINITY); + //smallest radiosity found yet + let mut minimum_colors = Vector4::new( + f32::INFINITY, + f32::INFINITY, + f32::INFINITY, + f32::INFINITY); + //find maximum and minimum radiosity + radiosity_buffer.iter().for_each(|col| { + col.iter().for_each(|color| { + maximum_colors.x = f32::max(maximum_colors.x, color.x); + maximum_colors.y = f32::max(maximum_colors.y, color.y); + maximum_colors.z = f32::max(maximum_colors.z, color.z); + maximum_colors.w = f32::max(maximum_colors.w, color.w); + + minimum_colors.x = f32::min(minimum_colors.x, color.x); + minimum_colors.y = f32::min(minimum_colors.y, color.y); + minimum_colors.z = f32::min(minimum_colors.z, color.z); + minimum_colors.w = f32::min(minimum_colors.w, color.w); + }) + }); + + //calculate difference between min and max pixel radiosity + let range = maximum_colors - minimum_colors; + //normalize to range + for column in &mut *radiosity_buffer { + for mut radiosity_value in column { + //normalize to range + radiosity_value.div_assign_element_wise(range); + //map to [0.0..255] + radiosity_value.mul_assign(255.0); + } + } + + radiosity_buffer +} + +pub fn map_radmap_to_colors(radiosity_buffer: &Vec>>) -> Vec>> { + //copy radiositys to a color Buffer + let mut color_buffer: Vec>> = Vec::with_capacity(radiosity_buffer.len()); + radiosity_buffer.iter().enumerate().for_each(|(x, col)| { + color_buffer.push(Vec::with_capacity(radiosity_buffer[0].len())); + col.iter().enumerate().for_each(|(y, color_value)| { + //ceil and cast individual colors + let casted = color_value.map(|comp| { + f32::ceil(comp) as u8 + }); + color_buffer[x].push(Rgba::from([casted.x, casted.y, casted.z, casted.w])); + }); + }); + + color_buffer +} + +pub fn store_colors_to_image(color_buffer: Vec>>) -> DynamicImage { + //build an image + let mut output_image: DynamicImage = DynamicImage::new_rgba8( + color_buffer.len() as u32, color_buffer[0].len() as u32, + ); + + color_buffer.iter().enumerate().for_each(|(x, col)| { + col.iter().enumerate().for_each(|(y, color)| { + output_image.put_pixel(x as u32, y as u32, *color); + }); + }); + + output_image +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index cbb9793..d06457c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,12 +3,16 @@ mod renderer; mod geometry; mod ray; +mod scene_data; +mod colors; use std::string::String; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, LockResult, Mutex}; +use cgmath::Vector4; use clap::{Parser}; use easy_gltf::Scene; use image::{DynamicImage}; +use crate::colors::{map_radmap_to_colors, normalize_colors_global, store_colors_to_image}; use crate::renderer::render; #[derive(Parser, Debug)] @@ -30,6 +34,9 @@ pub struct Args { /// rays per pixel #[arg(long)] multiplier: usize, + ///start in debug mode (e.g. without parallelization) + #[arg(long)] + debug: bool, } fn main() { @@ -41,17 +48,21 @@ fn main() { &args.gltf_file_path) .expect(&*format!("Failed to load glTF file {}", &args.gltf_file_path)); - //build an image - let output_image: Arc> = Arc::new(Mutex::new(DynamicImage::new_rgba8( - args.width as u32, args.height as u32, - ))); - - render(scenes, &args, &output_image); - match output_image.lock() { - Ok(image) => { - image.save("result_image.png").expect("Unable to save image!"); - } - Err(_) => { panic!("Error aquiring lock on image while saving!") } + let mut radiosity_buffer: Vec>> = match render(scenes, &args).lock() { + Ok(buffer) => {buffer.to_vec()} + Err(_) => {panic!("Unable to lock radiosity buffer!")} }; + + //normalize radiosity values globally + let normalized = + normalize_colors_global(&mut radiosity_buffer); + //map radiositys to u8 colors + let as_colors = + map_radmap_to_colors(normalized); + //store colors to image + let output_image = + store_colors_to_image(as_colors); + + output_image.save("result_image.png").expect("Unable to save image!"); } diff --git a/src/ray.rs b/src/ray.rs index 1ac9a6f..b5c8170 100644 --- a/src/ray.rs +++ b/src/ray.rs @@ -27,6 +27,7 @@ pub fn construct_primary_rays((width, height): (usize, usize), focal_length, pixel_x_coord, pixel_y_coord, + //just random noise rng.gen(), rng.gen() )); diff --git a/src/renderer.rs b/src/renderer.rs index 1223969..ac61406 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,17 +1,16 @@ -use std::ops::Mul; -use std::sync::{Arc, Mutex}; -use cgmath::{Angle, Matrix4, Vector2, Vector3}; +use std::ops::{Add, Mul}; +use std::sync::{Arc, LockResult, Mutex}; +use cgmath::{Angle, ElementWise, Matrix4, Vector2, Vector3, Vector4}; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use easy_gltf::model::{Mode}; -use easy_gltf::{Camera, Projection, Scene}; -use image::{DynamicImage, GenericImage, Rgba}; +use easy_gltf::{Camera, Material, Projection, Scene}; use crate::Args; use crate::geometry::{Intersectable}; use crate::ray::{construct_primary_rays, Ray}; +use crate::scene_data::IntersectionData; pub fn render(scenes: &Vec, - cl_args: &Args, - output_image: &Arc>) { + cl_args: &Args) -> Arc>>>> { let render_scene: &Scene = &scenes[cl_args.scene_index]; let render_camera: &Camera = &scenes[cl_args.scene_index].cameras[cl_args.camera_index]; @@ -32,13 +31,20 @@ pub fn render(scenes: &Vec, Projection::Orthographic { .. } => { panic!("Orthographic rendering not supported.") } }; - // the distance from the camera origin to the view plane let z: f32 = cl_args.height as f32 / (2.0 * fovy.mul(0.5).tan()); + let mut radiosity_buffer: Arc>>>> = + Arc::new(Mutex::new(Vec::with_capacity(cl_args.width))); + + //prepare the radiosity buffer + (0..cl_args.height).into_iter().for_each(|py| { + radiosity_buffer.lock().unwrap().push(Vec::with_capacity(cl_args.height)) + }); + //iterate over all pixels in the image - (0..cl_args.width).into_par_iter().for_each(|px| { - (0..cl_args.height).into_par_iter().for_each(|py| { + (0..cl_args.width).into_iter().for_each(|px| { + (0..cl_args.height).into_iter().for_each(|py| { //construct all rays let rays: Vec = construct_primary_rays( (cl_args.width, cl_args.height), @@ -49,42 +55,35 @@ pub fn render(scenes: &Vec, ); //let the initial pixel color be black and opaque - let mut pixel_luminosity: [f32; 4] = [0.0, 0.0, 0.0, 255.0]; + let mut pixel_radiosity: Vector4 = Vector4::new(0.0, 0.0, 0.0, 1.0); - //cast each ray and get the output color + //cast each ray and get the output luminosity and sum them up rays.iter().for_each(|ray| { - pixel_luminosity = raytrace(ray, render_scene, 4) - .zip(pixel_luminosity) - .map(|(a, b)| a as f32 + b); + pixel_radiosity = pixel_radiosity.add( + raytrace(ray, render_scene, 4) + ); }); - - //save pixel to output image - match output_image.lock() { - Ok(mut image) => { - //save pixel - image.put_pixel( - px as u32, - py as u32, - colormap( - pixel_luminosity, - cl_args.multiplier), - ); + //store radiosity into the buffer + match radiosity_buffer.clone().lock() { + Ok(mut buffer) => { + buffer[px].push(pixel_radiosity); } - Err(_) => { panic!("Unable to obtain lock on image!") } + Err(_) => {panic!("Unable to lock pixel buffer!")} } + }); }); + radiosity_buffer } -fn raytrace(ray: &Ray, scene: &Scene, recursion_depth: u32) -> [u8; 4] { - let mut pixel_color: [u8; 4] = [0, 0, 0, 255]; +fn raytrace(ray: &Ray, scene: &Scene, recursion_depth: u32) -> Vector4 { + let mut pixel_radiosity: Vector4 = Vector4::new(0.0, 0.0, 0.0, 255.0); let mut smallest_t: f32 = f32::MAX; - let mut clostest_intersection_point: Option> = None; - let mut color_at_isec: [u8; 4] = [0, 0, 0, 255]; + let mut intersection_data: Option = None; //test intersection with all models in the scene //TODO: Improve, to avoid iterating all models @@ -107,49 +106,44 @@ fn raytrace(ray: &Ray, scene: &Scene, recursion_depth: u32) -> [u8; 4] { // a new closer Point is found if t < smallest_t { smallest_t = t; - //TODO: lighting model is not at all considered. Just a debug hack. - clostest_intersection_point = Option::from(isec); - let color_vec = model.material().get_base_color_alpha( - Vector2::new(0.0, 0.0) - ).map(|comp| comp * 255.0); - - color_at_isec = [ - color_vec[0] as u8, - color_vec[1] as u8, - color_vec[2] as u8, - color_vec[3] as u8 - ] + intersection_data = + Option::from( + IntersectionData::new( + isec, + model.material()) + ); } } }; }); }); - //make pixel opaque if intersection is found - match clostest_intersection_point { - Some(_) => { - pixel_color = color_at_isec; + match intersection_data { + Some(isec_data) => { + pixel_radiosity = accumulate_colors(isec_data.intersection_point(), + isec_data.material()); } None {} => {} } - pixel_color + pixel_radiosity } -fn colormap(luminosity_array: [f32; 4], rays_per_pixel: usize) -> Rgba { - luminosity_array.map(|component| - f32::ceil(component / rays_per_pixel as f32) as u8 - ); +/// called iff an intersection is detected to (recursively) accumulate radiosity at intersection +fn accumulate_colors(_intersection_point: Vector3, isec_material: &Arc) -> Vector4 { + let mut pixel_radiosity: Vector4 + = Vector4::new(0.0, 0.0, 0.0, 1.0); + //accumulate colors at point + //emmisive Component + pixel_radiosity += + isec_material.emissive.factor.extend(1.0) + .mul_element_wise( + isec_material.get_base_color_alpha(Vector2::new(0.0, 0.0)) + ); - //linearly map luminosity values to [0..255] - let range: f32 = - luminosity_array.iter().fold(f32::NEG_INFINITY, |a, b| a.max(*b)) - - luminosity_array.iter().fold(f32::INFINITY, |a, b| a.min(*b)); - let color_value: [u8; 4] = - luminosity_array.map(|lum_val| - f32::ceil((lum_val / range) * 255.0) as u8 - ); + //TODO: Path tracing. Scatter ray in one random direction + + pixel_radiosity +} - Rgba::from(color_value) -} \ No newline at end of file diff --git a/src/scene_data.rs b/src/scene_data.rs new file mode 100644 index 0000000..d212691 --- /dev/null +++ b/src/scene_data.rs @@ -0,0 +1,25 @@ +use std::sync::Arc; +use cgmath::Vector3; +use easy_gltf::Material; + +pub (crate) struct IntersectionData{ + intersection_point: Vector3, + material: Arc +} + +impl IntersectionData{ + pub fn new(intersection_point: Vector3, material: Arc) -> IntersectionData{ + IntersectionData{ + intersection_point, + material, + } + } + + + pub fn intersection_point(&self) -> Vector3 { + self.intersection_point + } + pub fn material(&self) -> &Arc { + &self.material + } +} \ No newline at end of file