Raytracer Solutions
The full solution is available on Github. Feel free to browse through the commit history to see the stages of how I built my solution.
1: Images
Task 1.1
image
is great, this is dead simple if you find the right bits in the documentation. Learning to use Rust docs is an important skill.
use image::{Rgb, RgbImage};
fn main() {
let mut buffer = RgbImage::new(256, 256);
for (_, _, px) in buffer.enumerate_pixels_mut() {
*px = Rgb([255, 0, 0]);
}
buffer.save("render.png").expect("Could not save image");
}
This should yield you a big red square. Don't forget to include image
in your Cargo.toml
:
[dependencies]
image = "0.24.1"
Task 1.2
fn main() {
let width = 400;
let height = 400;
let mut buffer = RgbImage::new(256, 256);
for (i, j, px) in buffer.enumerate_pixels_mut() {
let r = i as f64 / (width - 1) as f64;
let g = j as f64 / (height - 1) as f64;
let b = 0.25;
*px = Rgb([r, g, b].map(|c| (c * 255.999) as u8))
}
buffer.save("render.png").expect("Could not save image");
}
We scale the range 0-1 from 0-255 by multiplying by 255.999, as the as
cast from float to int in Rust rounds down. I also increased the size of the image here to show off our nice gradient a bit better. I changed the size of the image here to demonstrate that it should work for images of any size (not just 256x256, and not just square). Try playing around with different image sizes and gradients.
2: Vectors
2.1
Our Vec3
struct with all its methods:
pub struct Vec3 {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl Vec3 {
pub fn len(&self) -> f64 {
(self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
}
pub fn normalise(self) -> Self {
self / self.len()
}
pub fn dot(&self, other: &Self) -> f64 {
self.x * other.x + self.y * other.y + self.z * other.z
}
pub fn cross(&self, other: &Self) -> Self {
Self {
x: self.y * other.z - self.z * other.y,
y: self.z * other.x - self.x * other.z,
z: self.x * other.y - self.y * other.x,
}
}
pub fn map<F>(self, mut f: F) -> Vec3
where
F: FnMut(f64) -> f64,
{
Vec3 {
x: f(self.x),
y: f(self.y),
z: f(self.z),
}
}
}
impl From<Vec3> for Rgb<u8> {
fn from(v: Vec3) -> Self {
image::Rgb(
[self.x, self.y, self.z].map(|c| (c * 255.999) as u8),
)
}
}
2.2
You want the #[derive]
to look like:
use derive_more::{Add, Constructor, Div, Mul, Neg, Sub};
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Add, Div, Mul, Sub, Neg, Constructor)]
pub struct Vec3 {
pub x: f64,
pub y: f64,
pub z: f64,
}
Your two handwritten Mul
impls:
impl Mul<Vec3> for f64 {
type Output = Vec3;
fn mul(self, rhs: Vec3) -> Self::Output {
rhs.map(|x| x * self)
}
}
impl Mul for Vec3 {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
Vec3 {
x: self.x * rhs.x,
y: self.y * rhs.y,
z: self.z * rhs.z,
}
}
}
// Optionally a `impl Div for Vec3` like the mult may be useful later
2.3
Your macro should look as shown in the instructions. Don't worry if it was kinda confusing, macros are hard.
3: Rays
3.1
You should have a new file ray.rs
, and a mod ray;
statement in main.rs
. In ray.rs
:
use derive_more::Constructor;
#[derive(Debug, PartialEq, PartialOrd, Clone, Constructor)]
pub struct Ray {
pub origin: Point,
pub direction: Vec3,
}
impl Ray {
pub fn at(&self, t: f64) -> Point {
self.origin + self.direction * t
}
}
3.2
Our updated main
function, with all the camera and geometry definitions:
fn main() {
//image
let aspect_ratio = 16.0 / 9.0;
let img_width: u32 = 400;
let img_height = (img_width as f64 / aspect_ratio) as u32;
//camera and viewport
let view_height = 2.0;
let view_width = view_height * aspect_ratio;
let focal_length = 1.0;
//geometry
let origin: Point = v!(0);
let horizontal: Vec3 = v!(view_width, 0, 0); //horizontal size vector
let vertical: Vec3 = v!(0, -view_height, 0); //vertical size vector, negated because we start in the top left and move *down* when rendering
let top_left: Point = origin - horizontal / 2.0 - vertical / 2.0 - v!(0, 0, focal_length); //the position of the top left corner of our imgae
let mut buffer = RgbImage::new(img_width, img_height);
for (i, j, px) in buffer.enumerate_pixels_mut() {
//pixel coordinates as scalars from 0.0 <= t <= 1.0
let u = i as f64 / (img_width - 1) as f64;
let v = j as f64 / (img_height - 1) as f64;
//the direction of the ray
//start at top left, then go horizontally scaled by u and vertically by v
let ray_direction: Vec3 = top_left + u * horizontal + v * vertical - origin;
//save pixel colour to buffer
*px = ray::colour(&Ray::new(origin, ray_direction)).into();
}
buffer.save("render.png").expect("Could not save image");
}
And the simple green colour
function, under ray.rs
:
pub fn colour(ray: &Ray) -> Colour {
v!(0,1,0)
}
3.3
Our lerp:
pub fn colour(ray: &Ray) -> Colour {
let direction = ray.direction.normalise();
let t = 0.5 * (direction.normalise().y + 1.0); //scale from -1 < y < 1 to 0 < t < 1
//two colours to blend
let white: Colour = v!(1);
let blue: Colour = v!(0.5, 0.7, 1);
//blend
blue * t + white * (1.0 - t)
}
4: Spheres
### 4.1
The entirety of object.rs
is shown below. Pay careful attention to the quadratic formula in hit
.
use derive_more::Constructor;
use crate::{ray::Ray, vector::Point};
//a sphere
#[derive(Debug, Constructor)]
pub struct Sphere {
pub center: Point,
pub radius: f64,
}
//calculate ray-sphere intersection stuff
impl Sphere {
pub fn hit(&self, ray: &Ray) -> bool {
let oc = ray.origin - self.center;
let a = ray.direction.dot(&ray.direction);
let b = 2.0 * oc.dot(&ray.direction);
let c = oc.dot(&oc) - self.radius * self.radius;
let discriminant = b * b - 4.0 * a * c;
discriminant >= 0.0
}
}
This is the condition you want to add to your colour function too
let sphere = object::Sphere::new(v!(0, 0, -1), 0.5);
if sphere.hit(ray) {
return v!(1, 0, 0);
}
4.2
Your new parallel for_each
iterator:
buffer.enumerate_pixels_mut() //create the iterator over the buffer
.par_bridge() // bridge it to a parallel iterator
.for_each(|(i, j, px)| { //for each item in the iterator, execute this closure
//loop body is unchanged
});
If you're still really struggling with performance, ask someone to have a look over your code with you and we'll see if there's anything else we can do to speed it up.
5: Surface Normals & Multiple Objects
5.1
The updated Sphere::hit()
method:
impl Sphere {
pub fn hit(&self, ray: &Ray) -> Option<f64> {
let oc = ray.origin - self.center;
let a = ray.direction.dot(&ray.direction);
let b = 2.0 * oc.dot(&ray.direction);
let c = oc.dot(&oc) - self.radius * self.radius;
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
None
} else {
Some((-b - discriminant.sqrt()) / (a * 2.0))
}
}
}
Since the discriminant is non-negative, we can discard the -b + discriminant.sqrt()
case, since the negative case is always closer.
And Ray::colour()
:
pub fn colour(ray: &Ray) -> Colour {
let sphere = object::Sphere::new(v!(0, 0, -1), 0.5);
//if the sphere and ray return Some(t)
if let Some(t) = sphere.hit(ray) {
//calculate normal, scale and return it
let normal = (ray.at(t) - sphere.center).normalise();
(normal + v!(1)) / 2.0
} else { //else, same as before
let direction = ray.direction.normalise();
let t = 0.5 * (direction.normalise().y + 1.0); //scale from -1 < y < 1 to 0 < t < 1
//two colours to blend
let white: Colour = v!(1);
let blue: Colour = v!(0.5, 0.7, 1);
//blend
blue * t + white * (1.0 - t)
}
}
5.2
The Hit
struct:
pub struct Hit {
pub impact_point: Point,
pub normal: Vec3,
pub paramater: f64,
}
And the Object
trait:
// Represents objects within the scene
pub trait Object {
//determines if an object has been hit by a ray
//returns the impact point, the surface normal to the impact point, and the solution to the impact equation
//if there is no intersection, return None
fn hit(&self, ray: &Ray, bounds: (f64, f64)) -> Option<Hit>;
}
Sphere
will still now have a hit method, but it will be part of it's Object
implementation:
impl Object for Sphere {
fn hit(&self, ray: &Ray, bounds: (f64, f64)) -> Option<Hit> {
//calculate intersection
let oc = ray.origin - self.center;
let a = ray.direction.dot(&ray.direction);
let b = 2.0 * oc.dot(&ray.direction);
let c = oc.dot(&oc) - self.radius * self.radius;
let d = b * b - 4.0 * a * c;
if d < 0.0 {
return None;
}
//get the correct root, if one lies in the bounds
let mut root = (-b - d.sqrt()) / (2.0 * a);
if !(bounds.0..bounds.1).contains(&root) {
root = (-b + d.sqrt()) / (2.0 * a);
if !(bounds.0..bounds.1).contains(&root) {
return None;
}
}
let impact_point = ray.at(root);
let normal = (impact_point - self.center) / self.radius;
Some(Hit {
impact_point,
normal,
paramater: root,
})
}
}
Sphere is still a sphere, but it's also an object too. Rust makes it really easy for us to build expressive abstractions like this, which we do more of down the line when we start working with different materials too.
5.3
Something like this will work:
let impact_point = ray.at(root);
//the normal that is always opposite to the incident ray
let normal = (impact_point - self.center) / self.radius;
//make sure the normals always point outward from the sphere's surface regardless of incident ray direction
//set front_face accordingly
let (normal, front_face) = if ray.direction.dot(&normal) > 0.0 {
(-normal, false)
} else {
(normal, true)
};
5.4
Your Scene
type and it's Object
impl. See how we're making nice use of that object trait from earlier?
pub type Scene = Vec<Box<dyn Object + Sync>>;
impl Object for Scene {
fn hit(&self, ray: &Ray, bounds: (f64, f64)) -> Option<Hit> {
self.iter()
.filter_map(|o| o.hit(ray, bounds)) //filter out the ones that don't intersect
.min_by(|h1, h2| h1.paramater.partial_cmp(&h2.paramater).unwrap()) //sort by smallest parameter, returning lowest
}
}
Try not to worry about trait objects too much now, there's a lot of complexity associated with them (vtables, object safety) once you start to dig into it. All you need to understand is that dyn Object + Sync
is a type that implements both Object
and Sync
, and we need to Box
it on the heap because we don't know what those types are at compile time, so we can't reason about how big they are.
5.5
Our entire scene is defined like so in main()
:
//world
let objects: Scene = vec![
Box::new(Sphere::new(v!(0, 0, -1), 0.5)),
Box::new(Sphere::new(v!(0, -100.5, -1), 100.0)),
];
We then pass this to ray::colour
, which is updated as shown:
pub fn colour(scene: &impl Object, ray: &Ray) -> Colour {
if let Some(hit) = scene.hit(ray, (0.0, f64::INFINITY)) {
(hit.normal + v!(1)) / 2.0
} else {
let direction = ray.direction.normalise();
let t = 0.5 * (direction.normalise().y + 1.0); //scale from -1 < y < 1 to 0 < t < 1
//two colours to blend
let white: Colour = v!(1);
let blue: Colour = v!(0.5, 0.7, 1);
//blend
blue * t + white * (1.0 - t)
}
}
6: Antialiasing
6.1
Pay careful attention to where the randomness is added here. Note also how the colour is not accumulated into an RGB
type, but one of our own Vec3
types, and then converted to rgb at the last stage for precision. The body of the updated rendering loop:
//colour is a vector
let mut colour = v!(0);
for _ in 0..samples {
//randomness here
let u = (i as f64 + rand::random::<f64>()) / (img_width - 1) as f64;
let v = (j as f64 + rand::random::<f64>()) / (img_height - 1) as f64;
let ray_direction: Vec3 = top_left + u * horizontal + v * vertical - origin;
colour = colour + ray::colour(&objects, &Ray::new(origin, ray_direction));
}
//save pixel colour to buffer
*px = (colour / (samples as f64)).into(); //convert to RGB here
You could also draw the entire scene 100 times and average those out if you wanted, but it might require a bit more work to implement so this is the easy route.
6.2
The camera file should look as follows. We're literally just moving stuff over from main and encapsulating a few bits, that'll come in handy later when we make the camera a bit fancier.
use crate::{ray::Ray, v, Point, Vec3};
pub struct Camera {
origin: Point,
top_left: Point,
horizontal: Vec3,
vertical: Vec3,
}
impl Camera {
pub fn default() -> Self {
let aspect_ratio = 16.0 / 9.0;
let viewport_height = 2.0;
let viewport_width = aspect_ratio * viewport_height;
let focal_length = 1.0;
let origin: Point = v!(0, 0, 0);
let horizontal = v!(viewport_width, 0, 0);
let vertical = v!(0, -viewport_height, 0);
//the top left of our image is the origin, -1 away from the camera and up and right by half the height/width
let top_left: Point = origin - horizontal / 2.0 - vertical / 2.0 - v!(0, 0, focal_length);
Camera {
origin,
top_left,
horizontal,
vertical,
}
}
pub fn get_ray(&self, u: f64, v: f64) -> Ray {
let px_position = self.top_left + u * self.horizontal + v * self.vertical;
Ray::new(self.origin, px_position - self.origin)
}
}
main
is also edited to remove all this and add a single call to camera::Camera::default()
instead. The compiler will tell you which variables are used and unused where and what you can remove from main
. The render loop should get its rays from the camera using Camera::get_ray()
. Calculate u
and v
the same as before, but pass them to the camera:
let ray = camera.get_ray(u, v);
colour = colour + ray::colour(&objects, &ray);
6.3
The Indicatif code added in main:
println!("Rendering Scene...");
let bar = ProgressBar::new((img_width * img_height) as u64);
bar.set_style(
ProgressStyle::default_bar()
.template(
"{spinner:.green} [{wide_bar:.green/white}] {percent}% - {elapsed_precise} elapsed {msg}",
)
.progress_chars("#>-")
.on_finish(ProgressFinish::WithMessage("-- Done!".into())),
);
.progress_with(bar)
is added to the iterator chain just before the for_each()
call
buffer
.enumerate_pixels_mut()
.par_bridge()
.progress_with(bar)
.for_each(|(i, j, px)| {...
Again, I encourage you to style the progress bar yourself.
7: Diffuse Materials
7.1 & 7.2
I added my random unit vector function to the Vec3
struct, but you can put it wherever you think makes sense.
pub fn rand_unit() -> Self {
loop {
//random f64 range 0-1, scale it -1 to 1
let v = v!(rand::random::<f64>() * 2.0 - 1.0);
//if the vector lies in the unit sphere
if v.len() < 1.0 {
//normalise so it lies *on* the sphere and is a unit vector
break v.normalise();
}
}
}
Your updated ray::colour
function should look as shown
pub fn colour(scene: &impl Object, ray: &Ray, depth: u8) -> Colour {
if depth == 0 {
return v!(0);
}
if let Some(hit) = scene.hit(ray, (0.0, f64::INFINITY)) {
let direction = hit.normal + Vec3::rand_unit();
let origin = hit.impact_point;
0.5 * colour(scene, &Ray::new(origin, direction), depth - 1)
} else {
//... as before
Make sure to update the call site for the function to add the max_depth
parameter.
7.3
I added a call to map()
in Vec3::to_rgb()
to take the square root of everything before we do the byte conversion.
pub fn to_rgb(self) -> image::Rgb<u8> {
image::Rgb(
[self.x, self.y, self.z]
.map(|c| c.sqrt())
.map(|c| (c * 255.999) as u8),
)
}
Image encoding and colours is a much more complex topic than you might expect, so its worth looking into if you're interested.
7.4
Just changed the 0.0
to 0.00001
in the call to Scene::hit
in ray::colour
:
if let Some(hit) = scene.hit(ray, (0.00001, f64::INFINITY)) { //...
8: Metal
8.1
The entire contents of material.rs
is shown below:
use derive_more::Constructor;
use crate::{
object::Hit,
ray::Ray,
vector::{Colour, Vec3},
};
#[derive(Debug)]
pub struct Reflection {
pub ray: Ray,
pub colour_attenuation: Colour,
}
pub trait Material {
fn scatter(&self, incident_ray: &Ray, hit: &Hit) -> Option<Reflection>;
}
#[derive(Debug, Constructor)]
pub struct Lambertian(Colour);
impl Material for Lambertian {
fn scatter(&self, _: &Ray, hit: &Hit) -> Option<Reflection> {
//calculate reflected ray
let scatter_direction = hit.normal + Vec3::rand_unit();
let reflected_ray = Ray::new(hit.impact_point, scatter_direction);
//return it, along with the colour attenuation of it for this material
Some(Reflection {
ray: reflected_ray,
colour_attenuation: self.0,
})
}
}
8.2
The new Sphere
struct should look as follows, with the bounded generic type variable.
#[derive(Debug, Constructor)]
pub struct Sphere<M: Material> {
center: Point,
radius: f64,
material: M,
}
Hit
should have a new field pub reflection: Option<Reflection>
, and it should be filled at the bottom of Sphere::hit
impl<M: Material> Object for Sphere<M> {
fn hit(&self, ray: &Ray, bounds: (f64, f64)) -> Option<Hit> {
// all the same as before
//...
let mut h = Hit {
impact_point,
normal,
paramater: root,
front_face,
reflection: None,
};
h.reflection = self.material.scatter(ray, &h);
Some(h)
}
}
ray::colour
should look like this now too:
ub fn colour(scene: &impl Object, ray: &Ray, depth: u8) -> Colour {
if depth == 0 {
return v!(0);
}
if let Some(hit) = scene.hit(ray, (0.00001, f64::INFINITY)) {
if let Some(reflection) = hit.reflection {
reflection.colour_attenuation * colour(scene, &reflection.ray, depth - 1)
} else {
v!(0, 0, 0)
}
} else {
let direction = ray.direction.normalise();
let t = 0.5 * (direction.normalise().y + 1.0); //scale from -1 < y < 1 to 0 < t < 1
//two colours to blend
let white: Colour = v!(1);
let blue: Colour = v!(0.5, 0.7, 1);
//blend
blue * t + white * (1.0 - t)
}
}
Don't forget to update the two spheres in main
:
let objects: Scene = vec![
Box::new(Sphere::new(v!(0, 0, -1), 0.5, Lambertian::new(v!(0.5)))),
Box::new(Sphere::new(
v!(0, -100.5, -1),
100.0,
Lambertian::new(v!(0.5)),
)),
];
8.3
I added Vec3::is_zero()
, but you could also add it as a private helper function at the bottom if you wanted, or just inline it. It should like this:
pub fn is_zero(&self) -> bool {
let tolerance: f64 = 1e-8;
self.x.abs() < tolerance && self.y.abs() < tolerance && self.z.abs() < tolerance
}
This conditional check is then added to Lambertian::scatter
if scatter_direction.is_zero() {
scatter_direction = hit.normal;
}
8.4
The metal struct and impl should look like this:
#[derive(Debug, Constructor)]
pub struct Metal(Colour);
impl Material for Metal {
fn scatter(&self, incident_ray: &Ray, hit: &Hit) -> Option<Reflection> {
//the reflected ray direction
let reflection = reflect(incident_ray.direction, &hit.normal);
//the scattered ray
let scattered = Ray::new(hit.impact_point, reflection);
if scattered.direction.dot(&hit.normal) > 0.0 {
Some(Reflection {
ray: scattered,
colour_attenuation: self.0,
})
} else {
None
}
}
}
fn reflect(v: Vec3, normal: &Vec3) -> Vec3 {
v - 2.0 * v.dot(normal) * *normal
}
The new Scene
with four spheres is shown below too. This bit isn't hard, it's just boilerplate with constructors so I wouldn't blame you for copy-pasting this.
let objects: Scene = vec![
Box::new(Sphere::new(
//center
v!(0, 0, -1),
0.5,
Lambertian::new(v!(0.7, 0.3, 0.3)),
)),
Box::new(Sphere::new(
//ground
v!(0, -100.5, -1),
100.0,
Lambertian::new(v!(0.8, 0.8, 0.0)),
)),
Box::new(Sphere::new(
//left
v!(-1.0, 0.0, -1.0),
0.5,
Metal::new(v!(0.8, 0.8, 0.8)),
)),
Box::new(Sphere::new(
//right
v!(1.0, 0.0, -1.0),
0.5,
Metal::new(v!(0.8, 0.6, 0.2)),
)),
];
8.5
You'll need a new field in Metal
:
#[derive(Debug, Constructor)]
pub struct Metal {
colour: Colour,
fuzz: f64,
}
The new reflected ray direction in Metal::scatter
should look add a small random vector, as shown.
let reflection = reflect(incident_ray.direction, &hit.normal) + self.fuzz * Vec3::rand_unit();
9: Dielectrics
9.1
The refract
function should look like this:
fn refract(incident: Vec3, normal: &Vec3, ratio: f64) -> Vec3 {
let cos_theta = -incident.dot(normal);
let r_out_perp = ratio * (incident + cos_theta * *normal);
let r_out_par = -(1.0 - r_out_perp.dot(&r_out_perp)).abs().sqrt() * *normal;
r_out_par + r_out_perp
}
9.2
Dielectric
and its Material
impl:
#[derive(Debug, Constructor)]
pub struct Dielectric {
ratio: f64;
};
impl Material for Dielectric {
fn scatter(&self, incident_ray: &Ray, hit: &Hit) -> Option<Reflection> {
let ratio = if hit.front_face { 1.0 / self.ratio } else { self.ratio };
let refracted = refract(incident_ray.direction.normalise(), &hit.normal, ratio);
let out_ray = Ray::new(hit.impact_point, refracted);
Some(Reflection {
ray: out_ray,
colour_attenuation: v!(1),
})
}
}
9.3
The updated Dielectric::scatter
method:
fn scatter(&self, incident_ray: &Ray, hit: &Hit) -> Option<Reflection> {
let ratio = if hit.front_face { 1.0 / self.ratio } else { self.ratio };
let unit_direction = incident_ray.direction.normalise();
let cos_theta = -unit_direction.dot(&hit.normal);
let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
let scatter_direction = if (ratio * sin_theta) > 1.0 {
reflect(unit_direction, &hit.normal)
} else {
refract(unit_direction, &hit.normal, ratio)
};
let out_ray = Ray::new(hit.impact_point, scatter_direction);
Some(Reflection {
ray: out_ray,
colour_attenuation: v!(1),
})
}
9.4
The reflectance function for the Schlick approximation:
fn reflectance(cos_theta: f64, n: f64) -> f64 {
let r0 = f64::powi((1.0 - n) / (1.0 + n), 2);
r0 + (1.0 - r0) * f64::powi(1.0 - cos_theta, 5)
}
powi
raises a float to an integer power.
The if
expression that binds to scatter_direction
needs updating to add an extra condition for reflectance:
let scatter_direction = if (ratio * sin_theta > 1.0)
|| reflectance(cos_theta, ratio) > rand::random() {
//reflect
} else {
//refract
}
10: Positionable Camera
10.1
The new()
method for the camera:
pub fn new(fov: f64, aspect_ratio: f64) -> Self {
let theta = fov.to_radians();
let h = f64::tan(theta / 2.0);
let view_height = 2.0 * h;
let view_width = aspect_ratio * view_height;
let focal_length = 1.0;
let origin: Point = v!(0, 0, 0);
let horizontal = v!(view_width, 0, 0);
let vertical = v!(0, -view_height, 0);
let top_left: Point = origin - horizontal / 2.0 - vertical / 2.0 - v!(0, 0, focal_length);
Camera {
origin,
top_left,
horizontal,
vertical,
}
}
10.2
More changes to Camera::new()
:
pub fn new(look_from: Point, look_at: Point, vup: Vec3, fov: f64, aspect_ratio: f64) -> Self {
let theta = fov.to_radians();
let h = f64::tan(theta / 2.0);
let view_height = 2.0 * h;
let view_width = aspect_ratio * view_height;
let w = (look_from - look_at).normalise();
let u = vup.cross(&w).normalise();
let v = w.cross(&u);
let origin = look_from;
let horizontal = view_width * u;
let vertical = -view_height * v;
let top_left: Point = origin - horizontal / 2.0 - vertical / 2.0 - w;
Camera {
origin,
top_left,
horizontal,
vertical,
}
}
The code for the new scene too, because it's long:
let camera = camera::Camera::new(v!(-2, 2, 1), v!(0, 0, -1), v!(0, 1, 0), 20.0, 16.0/9.0);
let objects: Scene = vec![
Box::new(Sphere::new(
v!(0, 0, -1),
0.5,
Lambertian::new(v!(0.1, 0.2, 0.5)),
)),
Box::new(Sphere::new(
v!(-1.0, 0.0, -1.0),
0.5,,
Dielectric::new(1.5))),
Box::new(Sphere::new(
v!(1.0, 0.0, -1.0),
0.5,
Metal::new(v!(0.8, 0.6, 0.2), 0.0),
)),
Box::new(Sphere::new(
v!(0, -100.5, -1),
100.0,
Lambertian::new(v!(0.8, 0.8, 0.0)),
)),
];
11: Defocus Blur
The random vector in a unit circle function:
fn random_in_unit_circle() -> Vec3 {
//want random numbers -1 to 1
let dist = rand::distributions::Uniform::new_inclusive(-1.0, 1.0);
let mut rng = rand::thread_rng();
loop {
let v = v!(dist.sample(&mut rng), dist.sample(&mut rng), 0);
//if the vector lies in the unit sphere
if v.len() < 1.0 {
//normalise so it lies *on* the sphere
break v.normalise();
}
}
}
The updated Camera::new()
.
pub fn new(
look_from: Point,
look_at: Point,
vup: Vec3,
fov: f64,
aspect_ratio: f64,
aperture: f64,
focus_distance: f64,
) -> Self {
let theta = fov.to_radians();
let h = f64::tan(theta / 2.0);
let view_height = 2.0 * h;
let view_width = aspect_ratio * view_height;
let w = (look_from - look_at).normalise();
let u = vup.cross(&w).normalise();
let v = w.cross(&u);
let origin = look_from;
let horizontal = view_width * u * focus_distance;
let vertical = -view_height * v * focus_distance;
let top_left: Point = origin - horizontal / 2.0 - vertical / 2.0 - w * focus_distance;
let lens_radius = aperture / 2.0;
Camera {
origin,
top_left,
horizontal,
vertical,
u,
v,
lens_radius,
}
}
And the updated Camera::get_ray()
. Note how the parameters have been changed from (u, v)
to (s, t)
, because u
and v
now refer to the camera geometry instead of pixel positions.
pub fn get_ray(&self, s: f64, t: f64) -> Ray {
let rand = random_in_unit_circle() * self.lens_radius;
let origin = self.origin + self.u * rand.x + self.v * rand.y;
let px_position = self.top_left + s * self.horizontal + t * self.vertical;
//return the ray pointing at those pixels from camera origin
Ray::new(origin, px_position - origin)
}