diff --git a/Cargo.toml b/Cargo.toml index b60a5bb..8cf8ff1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,5 @@ easy-gltf = "1.1.1" clap = { version = "4.4.8", features = ["derive"] } cgmath = "0.18.0" rayon = "1.8.0" -image = "0.24.7" \ No newline at end of file +image = "0.24.7" +rand = "0.8.5" \ No newline at end of file diff --git a/result_image.png b/result_image.png index f901046..f75d13a 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 d1c0bb1..3e9ed9c 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 new file mode 100644 index 0000000..95f8750 Binary files /dev/null and b/scenes/cube_on_plane.blend1 differ diff --git a/src/main.rs b/src/main.rs index a4e58bd..cbb9793 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![feature(array_zip)] + mod renderer; mod geometry; mod ray; @@ -25,6 +27,9 @@ pub struct Args { /// image height #[arg(long)] height: usize, + /// rays per pixel + #[arg(long)] + multiplier: usize, } fn main() { diff --git a/src/ray.rs b/src/ray.rs index e187ba1..1ac9a6f 100644 --- a/src/ray.rs +++ b/src/ray.rs @@ -1,52 +1,36 @@ -use std::ops::Mul; -use cgmath::{Angle, InnerSpace, Matrix3, Matrix4, Point3, SquareMatrix, Vector3, Vector4}; -use easy_gltf::{Camera, Projection}; +use cgmath::{InnerSpace, Matrix4, Vector3, Vector4}; +use rand::{Rng}; pub struct Ray { pub(crate) source: Vector3, pub(crate) direction: Vector3, } -pub fn construct_primary_rays(camera: &Camera, - (width, height): (usize, usize), +pub fn construct_primary_rays((width, height): (usize, usize), (pixel_x_coord, pixel_y_coord): (usize, usize), + cam_to_world_matrix: &Matrix4, + focal_length: f32, + rays_per_pixel: usize, ) -> Vec { - //only allow perspective rendering - //TODO: ignoring aspect ratio here - let (fovy, aspect_ratio) = match camera.projection { - Projection::Perspective { yfov, aspect_ratio } => { - (yfov, aspect_ratio) - } - Projection::Orthographic { .. } => { panic!("Orthographic rendering not supported.") } - }; - //ray origin in world space - let origin_world_space = camera.position(); + let mut rays: Vec = Vec::with_capacity(rays_per_pixel); - //use a custom transform matrix because the one from easy_gltf is fucked - let transform_matrix = Matrix4::from_cols( - camera.right().extend(0.0), - -camera.up().extend(0.0), - -camera.forward().extend(0.0), - origin_world_space.extend(1.0), - ); - - // the distance from the camera origin to the view plane - let z: f32 = height as f32 / (2.0 * fovy.mul(0.5).tan()); - - //TODO: take ray multiplier per pixel into account here - let mut rays: Vec = Vec::with_capacity(height * width); + let mut rng = rand::thread_rng(); //generate all rays for this pixel and add them to the rays vector //TODO: use blue noise here to generate multiple rays per pixel - rays.push(generate_single_primary_ray( - width, - height, - &transform_matrix, - z, - pixel_x_coord, - pixel_y_coord, - origin_world_space)); + for _ in 0..rays_per_pixel { + rays.push(generate_single_primary_ray( + width, + height, + cam_to_world_matrix, + focal_length, + pixel_x_coord, + pixel_y_coord, + rng.gen(), + rng.gen() + )); + } rays } @@ -57,15 +41,20 @@ fn generate_single_primary_ray(image_width: usize, focal_length: f32, u: usize, v: usize, - ray_origin: Vector3) -> Ray { + u_offset: f32, + v_offset: f32) -> Ray { //calculate the ray direction and translate it to world space - let direction_view_space: Vector4 = - Vector4::new(u as f32 - (image_width as f32 / 2.0), - v as f32 - (image_height as f32 / 2.0), + let direction_camera_space: Vector4 = + Vector4::new(u as f32 - (image_width as f32 / 2.0) + u_offset, + v as f32 - (image_height as f32 / 2.0) + v_offset, focal_length, 0.0); - let direction_world_space = cam_to_world_transform * direction_view_space.normalize(); + let direction_world_space = + cam_to_world_transform * direction_camera_space.normalize(); - Ray { source: ray_origin, direction: direction_world_space.truncate().normalize() } + Ray { + source: cam_to_world_transform.w.truncate(), + direction: direction_world_space.truncate().normalize(), + } } \ No newline at end of file diff --git a/src/renderer.rs b/src/renderer.rs index d1457d5..1223969 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,9 +1,9 @@ -use std::cmp::max; +use std::ops::Mul; use std::sync::{Arc, Mutex}; -use cgmath::{Vector2, Vector3, Vector4, Zero}; +use cgmath::{Angle, Matrix4, Vector2, Vector3}; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use easy_gltf::model::{Mode}; -use easy_gltf::{Camera, Scene}; +use easy_gltf::{Camera, Projection, Scene}; use image::{DynamicImage, GenericImage, Rgba}; use crate::Args; use crate::geometry::{Intersectable}; @@ -15,29 +15,61 @@ pub fn render(scenes: &Vec, let render_scene: &Scene = &scenes[cl_args.scene_index]; let render_camera: &Camera = &scenes[cl_args.scene_index].cameras[cl_args.camera_index]; + //use a custom transform matrix because the one from easy_gltf is fucked + let transform_matrix = Matrix4::from_cols( + render_camera.right().extend(0.0), + -render_camera.up().extend(0.0), + -render_camera.forward().extend(0.0), + render_camera.position().extend(1.0), + ); + + //only allow perspective rendering + //TODO: ignoring aspect ratio here. Maybe implement an assertion? + let fovy = match render_camera.projection { + Projection::Perspective { yfov, aspect_ratio: _aspect_ratio } => { + yfov + } + 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()); + //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| { //construct all rays let rays: Vec = construct_primary_rays( - render_camera, (cl_args.width, cl_args.height), (px, py), + &transform_matrix, + z, + cl_args.multiplier, ); - //let the initial pixel color be black and transparent - let mut pixel_color: Rgba = Rgba::from([0, 0, 0, 255]); + //let the initial pixel color be black and opaque + let mut pixel_luminosity: [f32; 4] = [0.0, 0.0, 0.0, 255.0]; //cast each ray and get the output color - //TODO: in the end we will want to average the colors out here rays.iter().for_each(|ray| { - pixel_color = raytrace(ray, render_scene); + pixel_luminosity = raytrace(ray, render_scene, 4) + .zip(pixel_luminosity) + .map(|(a, b)| a as f32 + b); }); + //save pixel to output image match output_image.lock() { Ok(mut image) => { - image.put_pixel(px as u32, py as u32, pixel_color); + //save pixel + image.put_pixel( + px as u32, + py as u32, + colormap( + pixel_luminosity, + cl_args.multiplier), + ); } Err(_) => { panic!("Unable to obtain lock on image!") } } @@ -46,13 +78,13 @@ pub fn render(scenes: &Vec, } -fn raytrace(ray: &Ray, scene: &Scene) -> Rgba { - let mut pixel_color: Rgba = Rgba::from([0, 0, 0, 255]); +fn raytrace(ray: &Ray, scene: &Scene, recursion_depth: u32) -> [u8; 4] { + let mut pixel_color: [u8; 4] = [0, 0, 0, 255]; let mut smallest_t: f32 = f32::MAX; let mut clostest_intersection_point: Option> = None; - let mut color_at_isec = [0, 0, 0, 0]; + let mut color_at_isec: [u8; 4] = [0, 0, 0, 255]; //test intersection with all models in the scene //TODO: Improve, to avoid iterating all models @@ -81,7 +113,12 @@ fn raytrace(ray: &Ray, scene: &Scene) -> Rgba { 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] + color_at_isec = [ + color_vec[0] as u8, + color_vec[1] as u8, + color_vec[2] as u8, + color_vec[3] as u8 + ] } } }; @@ -91,10 +128,28 @@ fn raytrace(ray: &Ray, scene: &Scene) -> Rgba { //make pixel opaque if intersection is found match clostest_intersection_point { Some(_) => { - pixel_color = Rgba::from(color_at_isec); + pixel_color = color_at_isec; } None {} => {} } pixel_color +} + +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 + ); + + //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 + ); + + Rgba::from(color_value) } \ No newline at end of file