5. Adding a Sphere

Adding a Sphere

Let’s add a single object to our ray tracer. People often use spheres in ray tracers because calculating whether a ray hits a sphere is pretty straightforward.

Ray-Sphere Intersection

Recall that the equation for a sphere centered at the origin of radius RR is x2+y2+z2=R2x^2 + y^2 + z^2 = R^2. Put another way, if a given point (x,y,z)(x,y,z) is on the sphere, then x2+y2+z2=R2x^2 + y^2 + z^2 = R^2. If the given point (x,y,z)(x,y,z) is inside the sphere, then x2+y2+z2<R2x^2 + y^2 + z^2 < R^2, and if a given point (x,y,z)(x,y,z) is outside the sphere, then x2+y2+z2>R2x^2 + y^2 + z^2 > R^2.

It gets uglier if the sphere center is at (Cx,Cy,Cz)(C_x, C_y, C_z):

(xβˆ’Cx)2+(yβˆ’Cy)2+(zβˆ’Cz)2=r2(x - C_x)^2 + (y - C_y)^2 + (z - C_z)^2 = r^2

In graphics, you almost always want your formulas to be in terms of vectors so all the x/y/z stuff is under the hood in the Vec3 class. You might note that the vector from center C=(Cx,Cy,Cz)\mathbf{C} = (C_x,C_y,C_z) to point P=(x,y,z)\mathbf{P} = (x,y,z) is (Pβˆ’C)(\mathbf{P} - \mathbf{C}), and therefore

(Pβˆ’C)β‹…(Pβˆ’C)=(xβˆ’Cx)2+(yβˆ’Cy)2+(zβˆ’Cz)2(\mathbf{P} - \mathbf{C}) \cdot (\mathbf{P} - \mathbf{C}) = (x - C_x)^2 + (y - C_y)^2 + (z - C_z)^2

So the equation of the sphere in vector form is:

(Pβˆ’C)β‹…(Pβˆ’C)=r2(\mathbf{P} - \mathbf{C}) \cdot (\mathbf{P} - \mathbf{C}) = r^2

We can read this as β€œany point P\mathbf{P} that satisfies this equation is on the sphere”. We want to know if our ray P(t)=A+tb\mathbf{P}(t) = \mathbf{A} + t\mathbf{b} ever hits the sphere anywhere. If it does hit the sphere, there is some tt for which P(t)\mathbf{P}(t) satisfies the sphere equation. So we are looking for any tt where this is true:

(P(t)βˆ’C)β‹…(P(t)βˆ’C)=r2(\mathbf{P}(t) - \mathbf{C}) \cdot (\mathbf{P}(t) - \mathbf{C}) = r^2

or expanding the full form of the ray P(t)\mathbf{P}(t):

(A+tbβˆ’C)β‹…(A+tbβˆ’C)=r2(\mathbf{A} + t \mathbf{b} - \mathbf{C}) \cdot (\mathbf{A} + t \mathbf{b} - \mathbf{C}) = r^2

The rules of vector algebra are all that we would want here. If we expand that equation and move all the terms to the left hand side we get:

t2bβ‹…b+2tbβ‹…(Aβˆ’C)+(Aβˆ’C)β‹…(Aβˆ’C)βˆ’r2=0t^2 \mathbf{b} \cdot \mathbf{b} + 2t \mathbf{b} \cdot (\mathbf{A}-\mathbf{C}) + (\mathbf{A}-\mathbf{C}) \cdot (\mathbf{A}-\mathbf{C}) - r^2 = 0

The vectors and rr in that equation are all constant and known. The unknown is tt, and the equation is a quadratic, like you probably saw in your high school math class. We can solve for tt using the quadratic formula:

t=βˆ’bΒ±b2βˆ’4ac2at = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}

Where:

a=bβ‹…bb=2bβ‹…(Aβˆ’C)c=(Aβˆ’C)β‹…(Aβˆ’C)βˆ’r2\begin{aligned} a & = \mathbf{b} \cdot \mathbf{b} \\ b & = 2 \mathbf{b} \cdot (\mathbf{A}-\mathbf{C}) \\ c & = (\mathbf{A}-\mathbf{C}) \cdot (\mathbf{A}-\mathbf{C}) - r^2 \end{aligned}

The square root part is called discriminant. It is either positive (meaning two real solutions), zero (meaning one real solution), or negative (meaning no real solutions). In graphics, the algebra almost always relates very directly to the geometry. What we have is:

Ray-sphere intersection results
Ray-sphere intersection results

Creating Our First Ray-traced Image

If we take that math and hard-code it into our program, we can test it by coloring red any pixel that hits a small sphere we place at -1 on the z-axis:

main.rs | Rendering a red sphere
mod color;
mod ray;
mod vec3;
 
use std::io;
 
use color::Color;
use ray::Ray;
use vec3::{Point3, Vec3};
 
fn hit_sphere(center: Point3, radius: f64, r: &Ray) -> bool {
    let oc = r.origin() - center;
    let a = vec3::dot(r.direction(), r.direction());
    let b = 2.0 * vec3::dot(oc, r.direction());
    let c = vec3::dot(oc, oc) - radius * radius;
    let discriminant = b * b - 4.0 * a * c;
    discriminant >= 0.0
}
 
fn ray_color(r: &Ray) -> Color {
    if hit_sphere(Point3::new(0.0, 0.0, -1.0), 0.5, r) {
        return Color::new(1.0, 0.0, 0.0);
    }
 
    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");
}

What we get is this:

A simple red sphere
A simple red sphere

Now this lacks all sorts of things β€” like shading and reflection rays and more than one object β€” but we are closer to halfway done than we are to our start! One thing to be aware of is that we tested whether the ray hits the sphere at all, but t<0t < 0 solutions work fine. If you change your sphere center to z=+1z = +1 you will get exactly the same picture because you see the things behind you. This is not a feature! We’ll fix those issues next.