diff --git a/Cargo.toml b/Cargo.toml index c2e914f..b60a5bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,5 @@ edition = "2021" easy-gltf = "1.1.1" clap = { version = "4.4.8", features = ["derive"] } cgmath = "0.18.0" -rayon = "1.8.0" \ No newline at end of file +rayon = "1.8.0" +image = "0.24.7" \ No newline at end of file diff --git a/result_image.png b/result_image.png new file mode 100644 index 0000000..df85266 Binary files /dev/null and b/result_image.png differ diff --git a/scenes/cube_on_plane.blend b/scenes/cube_on_plane.blend index a345c0b..ffb9fdb 100644 Binary files a/scenes/cube_on_plane.blend and b/scenes/cube_on_plane.blend differ diff --git a/scenes/cube_on_plane.glb b/scenes/cube_on_plane.glb index ca0d948..da2f056 100644 Binary files a/scenes/cube_on_plane.glb and b/scenes/cube_on_plane.glb differ diff --git a/src/geometry.rs b/src/geometry.rs index ad7626f..4b4d38d 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -1,10 +1,10 @@ -use cgmath::Vector3; -use easy_gltf::Camera; +use cgmath::{Angle, InnerSpace, Matrix4, SquareMatrix, Vector3, Vector4}; +use easy_gltf::{Camera, Projection}; use easy_gltf::model::Triangle; pub struct Ray { - source: Vector3, - direction: Vector3, + source: Vector3, + direction: Vector3, } pub trait Intersectable { @@ -18,10 +18,70 @@ pub trait Intersectable { impl Intersectable for Triangle { //perform muller trumbore intersection fn test_isec(&self, ray: &Ray) -> Option> { - todo!() + //TODO: implement correct intersection here + if ray.direction.x > 0.5 && + ray.direction.x < 0.6 && + ray.direction.y > 0.1 && + ray.direction.y < 0.6 { + return Some(Vector3::new(1.0, 1.0, 1.0)); + } + return None; } } -pub fn construct_rays(camera: &Camera) -> Vec { - todo!() +pub fn construct_primary_rays(camera: &Camera, + (width, height): (usize, usize), + (pixel_x_coord, pixel_y_coord): (usize, usize), +) -> Vec { + //only allow perspective rendering + //TODO: ignoring aspect ratio here + let fovy = match camera.projection { + Projection::Perspective { yfov, aspect_ratio: _aspect_ratio } => { yfov } + Projection::Orthographic { .. } => { panic!("Orthographic rendering not supported.") } + }; + + //ray origin in world space + let origin_world_space = camera.position(); + + //seems to be the distance from the camera origin to the view plane + let z: f32 = height as f32 / fovy.tan(); + + //obtain the inverse transformation Matrix + let inverse_transform = camera.transform.invert().unwrap_or_else(|| + panic!("Non invertible transform Matrix. giving up.") + ); + + //TODO: take ray multiplier per pixel into account here + let mut rays: Vec = Vec::with_capacity(height * width); + + //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_ray( + width, + height, + &inverse_transform, + z, + pixel_x_coord, + pixel_y_coord, + origin_world_space)); + + rays +} + +fn generate_ray(image_width: usize, + image_height: usize, + inverse_transform: &Matrix4, + focal_length: f32, + u: usize, + v: usize, + ray_origin: Vector3) -> 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), + -focal_length, + 0.0); + let direction_world_space = inverse_transform * direction_view_space; + + Ray { source: ray_origin, direction: direction_world_space.normalize().truncate() } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 29b1d1b..741de35 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,31 +2,50 @@ mod renderer; mod geometry; use std::string::String; +use std::sync::{Arc, Mutex}; use clap::{Parser}; use easy_gltf::Scene; +use image::{DynamicImage}; use crate::renderer::render; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] -struct Args { +pub struct Args { gltf_file_path: String, /// which scene to use in the gltf file - #[arg(short, long)] + #[arg(long)] scene_index: usize, /// which camera to render from - #[arg(short, long)] + #[arg(long)] camera_index: usize, + /// image width + #[arg(long)] + width: usize, + /// image height + #[arg(long)] + height: usize, } fn main() { //parse clargs - let args = Args::parse(); + let args: Args = Args::parse(); //load gltf scene let scenes: &Vec = &easy_gltf::load( &args.gltf_file_path) .expect(&*format!("Failed to load glTF file {}", &args.gltf_file_path)); - render(scenes, args.scene_index, args.camera_index) + //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!") } + }; } diff --git a/src/renderer.rs b/src/renderer.rs index 401d6b6..ee61fe5 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,26 +1,75 @@ +use std::sync::{Arc, Mutex}; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use easy_gltf::model::{Mode}; -use easy_gltf::Scene; -use crate::geometry::{construct_rays, Intersectable, Ray}; +use easy_gltf::{Camera, Scene}; +use image::{DynamicImage, GenericImage, Rgba}; +use crate::Args; +use crate::geometry::{construct_primary_rays, Intersectable, Ray}; -pub fn render(scenes: &Vec, scene_idx: usize, camera_idx: usize) { - //construct all rays and cast them all - let rays: Vec = construct_rays(&scenes[scene_idx].cameras[camera_idx]); - rays.into_par_iter().for_each(|ray| { - //test intersection with all models in the scene - //TODO: Improve, to avoid iterating all models - scenes[scene_idx].models.iter().for_each(|model| { - match model.mode() { - Mode::Triangles => { - //in triangle mode there will always be a triangle vector - let triangles = model.triangles().unwrap(); - triangles.iter().for_each(|triangle| { - triangle.test_isec(&ray); - }); +pub fn render(scenes: &Vec, + cl_args: &Args, + output_image: &Arc>) { + let render_scene: &Scene = &scenes[cl_args.scene_index]; + let render_camera: &Camera = &scenes[cl_args.scene_index].cameras[cl_args.camera_index]; + + //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), + ); + + //let the initial pixel color be white and opaque + let mut pixel_color: Rgba = Rgba([0, 0, 0, 255]); + + //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); + }); + + //save pixel to output image + match output_image.lock() { + Ok(mut image) => { + image.put_pixel(px as u32, py as u32, pixel_color); } - _ => { panic!("Unable to render model in mode {:?}", model.mode()) } + Err(_) => { panic!("Unable to obtain lock on image!") } } }); }); +} + + +fn raytrace(ray: &Ray, scene: &Scene) -> Rgba { + let mut pixel_color: Rgba = Rgba([0, 0, 0, 255]); + + //test intersection with all models in the scene + //TODO: Improve, to avoid iterating all models + scene.models.iter().for_each(|model| { + //get all triangles in the model + let triangles = match model.mode() { + Mode::Triangles => { + //in triangle mode there will always be a triangle vector + model.triangles().unwrap() + } + _ => { panic!("Unable to render model in mode {:?}", model.mode()) } + }; + + //iterate all triangles in a model + triangles.iter().for_each(|triangle| { + match triangle.test_isec(&ray) { + //boilerplate implementation to set pixel to red if any intersection happened + None => {} + Some(point) => { + pixel_color.0 = [255, 255, 255, 255]; + return + } + }; + }); + }); + pixel_color } \ No newline at end of file