feat(raytracer): add minimal phong lighting demo

This commit is contained in:
Jonas Röger 2024-11-17 23:50:05 +01:00
parent e8a2b0f059
commit b458b99c82
Signed by: jonas
GPG Key ID: 4000EB35E1AE0F07
11 changed files with 1192 additions and 51 deletions

891
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -21,5 +21,6 @@ path = "src/bin/repl.rs"
[dependencies]
as-any = "0.3.1"
futures = "0.3.30"
image = "0.25.5"
nalgebra = "0.33.2"
nix = "0.29.0"

View File

@ -75,6 +75,10 @@
type = "app";
program = "${packages.default}/bin/repl";
};
rt_demo = {
type = "app";
program = "${packages.default}/bin/rt_demo";
};
default = demo;
};

View File

@ -1,5 +1,86 @@
use lispers::raytracer::{scene::Scene, types::Light};
use lispers::raytracer::{
camera::Camera,
plane::Plane,
scene::Scene,
sphere::Sphere,
types::{Color, Light, Material, Point3, Vector3},
};
extern crate nalgebra as na;
fn main() {
let scene = Scene::new();
let mut scene = Scene::new();
scene.set_ambient(Color::new(0.2, 0.2, 0.2));
scene.add_light(Light {
position: Point3::new(4.0, 7.0, 10.0),
color: Color::new(1.0, 1.0, 1.0),
});
scene.add_light(Light {
position: Point3::new(-2.0, 7.0, 10.0),
color: Color::new(1.0, 1.0, 1.0),
});
scene.add_object(Box::new(Plane::new(
Point3::new(0.0, -1.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Material::new(
Color::new(0.5, 0.5, 0.5),
Color::new(0.5, 0.5, 0.5),
Color::new(0.0, 0.0, 0.0),
0.0,
0.6,
),
)));
scene.add_object(Box::new(Sphere::new(
Point3::new(-2.0, 0.0, 1.0),
1.0,
Material::new(
Color::new(0.0, 1.0, 0.0),
Color::new(0.0, 1.0, 0.0),
Color::new(0.6, 0.6, 0.6),
20.0,
0.3,
),
)));
scene.add_object(Box::new(Sphere::new(
Point3::new(0.2, -0.5, -0.2),
0.5,
Material::new(
Color::new(0.0, 0.0, 1.0),
Color::new(0.0, 0.0, 1.0),
Color::new(0.6, 0.6, 0.6),
20.0,
0.3,
),
)));
scene.add_object(Box::new(Sphere::new(
Point3::new(-0.5, 0.5, -2.0),
1.5,
Material::new(
Color::new(1.0, 0.0, 0.0),
Color::new(1.0, 0.0, 0.0),
Color::new(0.6, 0.6, 0.6),
20.0,
0.3,
),
)));
let camera = Camera::new(
Point3::new(0.0, 0.7, 5.0),
Point3::new(-1.0, -0.5, 0.0),
Vector3::new(0.0, 1.0, 0.0),
60.0,
400,
300,
);
let fname = "demo-scene.png";
match camera.render(&scene, 5, 2).save(fname) {
Ok(_) => println!("Image saved to {}", fname),
Err(e) => println!("Error saving image: {}", e),
}
}

View File

@ -1,70 +1,102 @@
use super::{
scene::Scene,
types::{Point3, Ray, Scalar, Vector3},
types::{Color, Point3, Ray, Scalar, Vector3},
};
// use image::Rgb32FImage;
use image::RgbImage;
/// A camera that can render a scene.
pub struct Camera {
/// Position of the camera's eye.
position: Point3,
up: Vector3,
right: Vector3,
upper_left: Point3,
/// The lower left point of the image plane.
lower_left: Point3,
/// The direction of the x-axis on the image plane. (length is equal to the image width)
x_dir: Vector3,
/// The direction of the y-axis on the image plane. (length is equal to the image height)
y_dir: Vector3,
/// The width of the image. [px]
width: usize,
/// The height of the image. [px]
height: usize,
}
impl Camera {
/// Create a new camera at `position` looking at `center` with `up` as the up vector.
/// The camera has a field of view of `fovy` degrees and an image size of `width` x `height`.
pub fn new(
position: Point3,
direction: Vector3,
center: Point3,
up: Vector3,
fovy: Scalar,
width: usize,
height: usize,
) -> Camera {
let aspect_ratio = width as Scalar / height as Scalar;
let fovx = fovy * aspect_ratio;
let right = direction.cross(&up).normalize();
let x_dir = right * (fovx / 2.0).tan();
let y_dir = -up * (fovy / 2.0).tan();
let upper_left = position + direction - x_dir + y_dir;
let view = (center - position).normalize();
let dist = (center - position).norm();
let aspect = width as Scalar / height as Scalar;
let im_height = 2.0 * dist * (fovy.to_radians() / 2.0).tan();
let im_width = aspect * im_height;
let x_dir = view.cross(&up).normalize() * im_width;
let y_dir = x_dir.cross(&view).normalize() * im_height;
let lower_left = center - 0.5 * x_dir - 0.5 * y_dir;
Camera {
position,
up,
right,
upper_left,
lower_left,
x_dir,
y_dir,
width,
height,
}
}
}
impl Camera {
/// Get a ray pointing from the camera to a relative position on the image plane.
/// `x` and `y` are expected to be in the range `[0, 1]`.
pub fn ray_at_relative(&self, x: Scalar, y: Scalar) -> Ray {
let x_dir = self.x_dir * x;
let y_dir = self.y_dir * y;
Ray::new(
self.position,
(self.upper_left + x_dir - y_dir - self.position).normalize(),
(self.lower_left + x_dir + y_dir - self.position).normalize(),
)
}
/// Get a ray pointing from the camera to a pixel on the image plane.
/// `x` and `y` are expected to be in the range `[0, width-1]` and `[0, height-1]` respectively.
pub fn ray_at(&self, x: usize, y: usize) -> Ray {
let x = x as Scalar / self.width as Scalar;
let y = y as Scalar / self.height as Scalar;
self.ray_at_relative(x, y)
self.ray_at_relative(x, 1.0 - y)
}
// pub fn render(&self, scene: &Scene, depth: u32) -> Rgb32FImage {
// Rgb32FImage::from_fn(self.width, self.height, |x, y| {
// let ray = self.ray_at(x, y);
// let color = scene.trace(&ray, depth);
// color.into()
// })
// }
/// Render the scene from the camera's perspective.
/// - `depth` is the maximum number of reflections to calculate.
/// - `subp` is the number of subpixels to use for antialiasing.
pub fn render(&self, scene: &Scene, depth: u32, subp: u32) -> RgbImage {
let dx = 1.0 / self.width as Scalar;
let dy = 1.0 / self.width as Scalar;
let dsx = dx / subp as Scalar;
let dsy = dy / subp as Scalar;
RgbImage::from_fn(self.width as u32, self.height as u32, |x, y| {
let x = x as Scalar * dx;
let y = y as Scalar * dy;
let mut color = Color::new(0.0, 0.0, 0.0);
for sx in 0..subp {
for sy in 0..subp {
color += scene.trace(
&self.ray_at_relative(
x + sx as Scalar * dsx,
1.0 - (y + sy as Scalar * dsy),
),
depth,
);
}
}
color *= 255.0 / (subp * subp) as Scalar;
[color.x as u8, color.y as u8, color.z as u8].into()
})
}
}

View File

@ -1,5 +1,6 @@
pub mod camera;
pub mod plane;
pub mod scene;
pub mod sphere;
pub mod types;
pub mod vec;
mod vec;

51
src/raytracer/plane.rs Normal file
View File

@ -0,0 +1,51 @@
use super::types::{Intersect, Material, Point3, Vector3};
extern crate nalgebra as na;
/// An infinite plane in 3D space.
pub struct Plane {
/// The position of the plane.
position: Point3,
/// The normal of the plane.
normal: Vector3,
/// The material of the plane.
material: Material,
}
impl Plane {
/// Create a new plane.
/// - `position` is the position of the plane.
/// - `normal` is the normal of the plane.
/// - `material` is the material of the plane.
pub fn new(position: Point3, normal: Vector3, material: Material) -> Plane {
Plane {
position,
normal,
material,
}
}
}
impl Intersect for Plane {
fn intersect<'a>(
&'a self,
ray: &super::types::Ray,
) -> Option<(
Point3,
Vector3,
super::types::Scalar,
&'a super::types::Material,
)> {
let denom = self.normal.dot(&ray.direction);
if denom != 0.0 {
let d = self.normal.dot(&self.position.coords);
let t = (d - self.normal.dot(&ray.origin.coords)) / denom;
if t > 1e-5 {
let point = ray.origin + ray.direction * t;
return Some((point, self.normal, t, &self.material));
}
}
None
}
}

View File

@ -4,32 +4,49 @@ use super::types::Light;
use super::types::Material;
use super::types::Point3;
use super::types::Ray;
use super::types::Scalar;
use super::types::Vector3;
use super::vec::mirror;
use super::vec::reflect;
extern crate nalgebra as na;
/// A scene is a collection of objects and lights, and provides a method to trace a ray through the scene.
pub struct Scene {
/// The ambient light of the scene
ambient: Color,
/// The objects in the scene
objects: Vec<Box<dyn Intersect>>,
/// The lights in the scene
lights: Vec<Light>,
}
impl Scene {
/// Create a new empty scene
pub fn new() -> Scene {
Scene {
ambient: na::Vector3::new(0.0, 0.0, 0.0),
objects: Vec::new(),
lights: Vec::new(),
}
}
/// Set the ambient light of the scene
pub fn set_ambient(&mut self, ambient: Color) {
self.ambient = ambient;
}
/// Add an object to the scene
pub fn add_object(&mut self, obj: Box<dyn Intersect>) {
self.objects.push(obj);
}
/// Add a light to the scene
pub fn add_light(&mut self, light: Light) {
self.lights.push(light);
}
/// Trace a ray through the scene and return the color of the ray.
/// - `ray` is the ray to be traced
/// - `depth` is the maximum recursion depth aka the number of reflections
pub fn trace(&self, ray: &Ray, depth: u32) -> Color {
if depth == 0 {
return na::Vector3::new(0.0, 0.0, 0.0);
@ -41,13 +58,9 @@ impl Scene {
.filter_map(|obj| obj.intersect(ray))
.min_by(|(_, _, t1, _), (_, _, t2, _)| t1.partial_cmp(t2).unwrap())
{
None => {
return na::Vector3::new(0.0, 0.0, 0.0);
}
Some((isect_pt, isect_norm, isect_dist, material)) => {
Some((isect_pt, isect_norm, _, material)) => {
// Lighting of material at the intersection point
let color =
self.lighting(-&ray.direction, material, isect_pt, isect_norm, isect_dist);
let color = self.lighting(-&ray.direction, material, isect_pt, isect_norm);
// Calculate reflections, if the material has mirror properties
if material.mirror > 0.0 {
@ -61,26 +74,50 @@ impl Scene {
return color;
}
}
_ => {
return na::Vector3::new(0.0, 0.0, 0.0);
}
}
}
/// Calculate Phong lighting from a `view` on a `material` at an intersection point `isect_pt` with a normal `isect_norm`.
fn lighting(
&self,
view: Vector3,
material: &Material,
isect_pt: Point3,
isect_norm: Vector3,
isect_dist: Scalar,
) -> Color {
let mut color: Color = na::Vector3::new(0.0, 0.0, 0.0);
// Start with ambient lighting
let mut color = material.ambient_color.component_mul(&self.ambient);
for light in &self.lights {
let l = (isect_pt - light.position).normalize();
let cos_theta = l.dot(&isect_norm);
// Cast Shadow-Ray
let shadow_ray = Ray {
origin: isect_pt,
direction: (light.position - isect_pt).normalize(),
};
if self
.objects
.iter()
.any(|obj| obj.intersect(&shadow_ray).is_some())
{
continue;
}
if cos_theta > 0.0 {
// Diffuse
let l = (light.position - isect_pt).normalize();
let cos_theta = l.dot(&isect_norm);
if cos_theta > 0.0 {
color += material.diffuse_color.component_mul(&light.color) * cos_theta;
// Specular
let r = mirror(l, isect_norm);
let cos_alpha = r.dot(&view);
if cos_alpha > 0.0 {
color += material.specular_color.component_mul(&light.color)
* cos_alpha.powf(material.shininess);
}
}
}

View File

@ -2,12 +2,27 @@ use super::types::{Intersect, Material, Point3, Ray, Scalar, Vector3};
extern crate nalgebra as na;
/// A sphere in 3D space
pub struct Sphere {
/// Center of the sphere
center: Point3,
/// Radius of the sphere
radius: Scalar,
/// PHONG material of the sphere
material: Material,
}
impl Sphere {
/// Create a new sphere at `center` with `radius` and `material`.
pub fn new(center: Point3, radius: Scalar, material: Material) -> Sphere {
Sphere {
center,
radius,
material,
}
}
}
/// Numerical error tolerance
const EPSILON: Scalar = 1e-5;
@ -35,6 +50,7 @@ impl Intersect for Sphere {
if t < Scalar::MAX {
let isect_pt: Point3 = ray.origin + ray.direction * t;
return Some((
isect_pt,
(isect_pt - self.center) / self.radius,

View File

@ -1,57 +1,87 @@
extern crate nalgebra as na;
pub type Scalar = f32;
/// The Scalar type to use for raytracing (f32 may result in acne effects)
pub type Scalar = f64;
/// The Vector3 type to use for raytracing
pub type Vector3 = na::Vector3<Scalar>;
/// The Point3 type to use for raytracing
pub type Point3 = na::Point3<Scalar>;
/// The Color type to use for raytracing
pub type Color = Vector3;
/// A trait indicating, that an object can be intersected by a ray
pub trait Intersect {
/// Intersect the object with a ray.
/// Returns None if the ray does not intersect the object.
/// Otherwise the intersection point, a normal vector at the intersection point,
/// the distance from the ray origin to the intersection point and
/// the material of the object are returned.
fn intersect<'a>(&'a self, ray: &Ray) -> Option<(Point3, Vector3, Scalar, &'a Material)>;
}
/// A point light source
pub struct Light {
/// Position of the light source
pub position: Point3,
/// Light color
pub color: Color,
}
impl Light {
/// Create a new light source at position with color
pub fn new(position: Point3, color: Color) -> Light {
Light { position, color }
}
}
/// A ray with origin and direction
pub struct Ray {
/// Ray origin
pub origin: Point3,
/// Ray direction
pub direction: Vector3,
}
impl Ray {
/// Create a new ray with origin and direction
pub fn new(origin: Point3, direction: Vector3) -> Ray {
Ray { origin, direction }
}
}
/// A Material used for PHONG shading
pub struct Material {
/// Ambient color, aka color without direct or indirect light
pub ambient_color: Color,
/// Diffuse color, aka color with direct light and reflected light
pub diffuse_color: Color,
/// Specular color, aka color of the highlights from direct light sources
pub specular_color: Color,
pub shinyness: Scalar,
/// A shininess factor, used to calculate the size of the highlights. `pow(angle, shininess) * specular_color = intensity`
pub shininess: Scalar,
/// A mirror factor, used to calculate the reflection of the object. `self_color * reflected_color = final_color`
pub mirror: Scalar,
}
impl Material {
/// Create a new material with ambient, diffuse, specular color, shininess and mirror factor.
/// - `ambient_color` is the color of the object without direct or indirect light
/// - `diffuse_color` is the color of the object with direct light and reflected light
/// - `specular_color` is the color of the highlights from direct light sources
/// - `shininess` is a factor used to calculate the size of the highlights. `pow(angle, shininess) * specular_color = intensity`
/// - `mirror` is a factor used to calculate the reflection of the object. `self_color * reflected_color = final_color`
pub fn new(
ambient_color: Color,
diffuse_color: Color,
specular_color: Color,
shinyness: Scalar,
shininess: Scalar,
mirror: Scalar,
) -> Material {
Material {
ambient_color,
diffuse_color,
specular_color,
shinyness,
shininess,
mirror,
}
}

View File

@ -2,13 +2,12 @@ use super::types::Vector3;
extern crate nalgebra as na;
/// Reflects a vector `v` around a normal `n`.
pub fn reflect(v: Vector3, n: Vector3) -> Vector3 {
v - 2.0 * v.dot(&n) * n
}
pub fn rotate(v: &Vector3, axis: &Vector3, angle: f32) -> Vector3 {
//let axis = na::Unit::new_normalize(axis);
//let rot = na::Rotation3::from_axis_angle(&axis, angle);
//(rot * v)
todo!()
/// Mirrors a vector `v` around a normal `n`.
pub fn mirror(v: Vector3, n: Vector3) -> Vector3 {
2.0 * v.dot(&n) * n - v
}