Rays, a Camera, and Background
The Ray Struct
The one thing that all ray tracers have is a ray struct and a computation of what color is seen along a ray. Let’s think of a ray as a function . Here is a 3D position along a line in 3D. is the ray origin and is the ray direction. The ray parameter is a real number (f64
in the code). Plug in a different and moves the point along the ray. Add in negative values and you can go anywhere on the 3D line. For positive , you get only the parts in front of , and this is what is often called a half-line or ray.
The function in more verbose code form we call ray::at(t)
:
use crate::vec3::{Point3, Vec3};
#[derive(Default)]
pub struct Ray {
orig: Point3,
dir: Vec3,
}
impl Ray {
pub fn new(origin: Point3, direction: Vec3) -> Ray {
Ray {
orig: origin,
dir: direction,
}
}
pub fn origin(&self) -> Point3 {
self.orig
}
pub fn direction(&self) -> Vec3 {
self.dir
}
pub fn at(&self, t: f64) -> Point3 {
self.orig + t * self.dir
}
}
Sending Rays Into the Scene
Now we are ready to turn the corner and make a ray tracer. At the core, the ray tracer sends rays through pixels and computes the color seen in the direction of those rays. The involved steps are (1) calculate the ray from the eye to the pixel, (2) determine which objects the ray intersects, and (3) compute a color for that intersection point.
When first developing a ray tracer, we can do a simple camera for getting the code up and running. We also can make a simple ray_color()
function that returns the color of the background (e.g., a simple gradient).
It’s common to get into trouble using square images for debugging because transposing and is a common operation, so we’ll use a non-square image. For now we’ll use a 16:9 aspect ratio, since that’s so common.
In addition to setting up the pixel dimensions for the rendered image, we also need to set up a virtual viewport through which to pass our scene rays. For the standard square pixel spacing, the viewport’s aspect ratio should be the same as our rendered image. We’ll just pick a viewport two units in height. We’ll also set the distance between the projection plane and the projection point to be one unit. This is referred to as the “focal length”, not to be confused with “focus distance”, which we’ll present later.
We’ll put the “eye” (or camera center if you think of a camera) at . We will have the y-axis go up, and the x-axis to the right. In order to respect the convention of a right handed coordinate system, into the screen is the negative z-axis. We will traverse the screen from the upper left hand corner, and use two offset vectors along the screen sides to move the ray endpoint across the screen. Note that we do not make the ray direction a unit length vector because not doing that makes for simpler and slightly faster code.
Below in code, the ray r
goes to approximately the pixel centers (We won’t worry about exactness for now because we’ll add antialiasing later):
mod color;
mod ray;
mod vec3;
use std::io;
use color::Color;
use ray::Ray;
use vec3::{Point3, Vec3};
fn ray_color(r: &Ray) -> Color {
let unit_direction = vec3::unit_vector(r.direction());
let t = 0.5 * (unit_direction.y() + 1.0);
(1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0)
}
fn main() {
// Image
const ASPECT_RATIO: f64 = 16.0 / 9.0;
const IMAGE_WIDTH: i32 = 400;
const IMAGE_HEIGHT: i32 = (IMAGE_WIDTH as f64 / ASPECT_RATIO) as i32;
// Camera
let viewport_height = 2.0;
let viewport_width = ASPECT_RATIO * viewport_height;
let focal_length = 1.0;
let origin = Point3::new(0.0, 0.0, 0.0);
let horizontal = Vec3::new(viewport_width, 0.0, 0.0);
let vertical = Vec3::new(0.0, viewport_height, 0.0);
let lower_left_corner =
origin - horizontal / 2.0 - vertical / 2.0 - Vec3::new(0.0, 0.0, focal_length);
// Render
print!("P3\n{} {}\n255\n", IMAGE_WIDTH, IMAGE_HEIGHT);
for j in (0..IMAGE_HEIGHT).rev() {
eprint!("\rScanlines remaining: {} ", j);
for i in 0..IMAGE_WIDTH {
let u = i as f64 / (IMAGE_WIDTH - 1) as f64;
let v = j as f64 / (IMAGE_HEIGHT - 1) as f64;
let r = Ray::new(
origin,
lower_left_corner + u * horizontal + v * vertical - origin,
);
let pixel_color = ray_color(&r);
color::write_color(&mut io::stdout(), pixel_color);
}
}
eprint!("\nDone.\n");
}
The ray_color()
function linearly blends white and blue depending on the height of the coordinate after scaling the ray direction to unit length (so ). Because we’re looking at the height after normalizing the vector, you’ll notice a horizontal gradient to the color in addition to the vertical gradient.
We then did a standard graphics trick of scaling that to . When we want blue. When we want white. In between, we want a blend. This forms a “linear blend”, or “linear interpolation”, or “lerp” for short, between two things. A lerp is always of the form:
with going from zero to one. In our case this produces: