The Vec3 Struct
Almost all graphics programs have some data structures for storing geometric vectors and colors. In many systems, these vectors are 4D (3D plus a homogeneous coordinate for geometry and RGB plus an alpha transparency channel for colors). For our purposes, using three coordinates suffices.
We’ll use the same Vec3
for colors, locations, directions, offsets, whatever. Some people don’t like this because it doesn’t prevent you from doing something silly, like adding a color to a location. They have a good point, but we’re going to take the “less code” route when it’s not obviously wrong.
We’re going to declare two aliases for Vec3
: Point3
and Color
. Since these two types are just aliases for Vec3
, we won’t get warnings if you pass a Color
to a function expecting a Point3
for example. We use them only to clarify intent.
Vec3 Variables and Methods
Here’s the basic part of the Vec3
struct:
#[derive(Copy, Clone, Default)]
pub struct Vec3 {
e: [f64; 3],
}
impl Vec3 {
pub fn new(x: f64, y: f64, z: f64) -> Vec3 {
Vec3 { e: [x, y, z] }
}
pub fn x(&self) -> f64 {
self.e[0]
}
pub fn y(&self) -> f64 {
self.e[1]
}
pub fn z(&self) -> f64 {
self.e[2]
}
}
// Type alias
pub type Point3 = Vec3;
We use f64
here, but some ray tracers use f32
. Either one is fine — follow your own tastes.
Note that we use the derive attribute to ask the compiler to implement the Copy
(opens in a new tab) trait for Vec3
, which changes the variable binding from move semantics to copy semantics. This is helpful because we can reuse and pass Vec3
variables without borrowing, making arithmetic operations easier. Besides, Vec3
is considered a small struct. For small structs, it is pretty efficient to pass by-copy (opens in a new tab). As a result, we will always pass Vec3
(and its aliases Point3
and Color
) by-copy in this tutorial.
Vec3 Utility Functions
The other part of Vec3
contains vector utility functions:
use std::fmt::{Display, Formatter, Result};
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub};
#[derive(Copy, Clone, Default)]
pub struct Vec3 {
e: [f64; 3],
}
impl Vec3 {
pub fn new(x: f64, y: f64, z: f64) -> Vec3 {
Vec3 { e: [x, y, z] }
}
pub fn x(&self) -> f64 {
self.e[0]
}
pub fn y(&self) -> f64 {
self.e[1]
}
pub fn z(&self) -> f64 {
self.e[2]
}
pub fn length(&self) -> f64 {
f64::sqrt(self.length_squared())
}
pub fn length_squared(&self) -> f64 {
self.e[0] * self.e[0] + self.e[1] * self.e[1] + self.e[2] * self.e[2]
}
}
// Type alias
pub type Point3 = Vec3;
// Output formatting
impl Display for Vec3 {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{} {} {}", self.e[0], self.e[1], self.e[2])
}
}
// -Vec3
impl Neg for Vec3 {
type Output = Vec3;
fn neg(self) -> Vec3 {
Vec3::new(-self.x(), -self.y(), -self.z())
}
}
// Vec3 += Vec3
impl AddAssign for Vec3 {
fn add_assign(&mut self, v: Vec3) {
*self = *self + v;
}
}
// Vec3 *= f64
impl MulAssign<f64> for Vec3 {
fn mul_assign(&mut self, t: f64) {
*self = *self * t;
}
}
// Vec3 /= f64
impl DivAssign<f64> for Vec3 {
fn div_assign(&mut self, t: f64) {
*self = *self / t;
}
}
// Vec3 + Vec3
impl Add for Vec3 {
type Output = Vec3;
fn add(self, v: Vec3) -> Vec3 {
Vec3::new(self.x() + v.x(), self.y() + v.y(), self.z() + v.z())
}
}
// Vec3 - Vec3
impl Sub for Vec3 {
type Output = Vec3;
fn sub(self, v: Vec3) -> Vec3 {
Vec3::new(self.x() - v.x(), self.y() - v.y(), self.z() - v.z())
}
}
// Vec3 * Vec3
impl Mul for Vec3 {
type Output = Vec3;
fn mul(self, v: Vec3) -> Vec3 {
Vec3::new(self.x() * v.x(), self.y() * v.y(), self.z() * v.z())
}
}
// f64 * Vec3
impl Mul<Vec3> for f64 {
type Output = Vec3;
fn mul(self, v: Vec3) -> Vec3 {
Vec3::new(self * v.x(), self * v.y(), self * v.z())
}
}
// Vec3 * f64
impl Mul<f64> for Vec3 {
type Output = Vec3;
fn mul(self, t: f64) -> Vec3 {
Vec3::new(self.x() * t, self.y() * t, self.z() * t)
}
}
// Vec3 / f64
impl Div<f64> for Vec3 {
type Output = Vec3;
fn div(self, t: f64) -> Vec3 {
Vec3::new(self.x() / t, self.y() / t, self.z() / t)
}
}
pub fn dot(u: Vec3, v: Vec3) -> f64 {
u.e[0] * v.e[0] + u.e[1] * v.e[1] + u.e[2] * v.e[2]
}
pub fn cross(u: Vec3, v: Vec3) -> Vec3 {
Vec3::new(
u.e[1] * v.e[2] - u.e[2] * v.e[1],
u.e[2] * v.e[0] - u.e[0] * v.e[2],
u.e[0] * v.e[1] - u.e[1] * v.e[0],
)
}
pub fn unit_vector(v: Vec3) -> Vec3 {
v / v.length()
}
Color Utility Functions
Using our new Color
(alias Vec3
), we’ll create a utility function to write a single pixel’s color out to the output stream.
use std::io::Write;
use crate::vec3::Vec3;
// Type alias
pub type Color = Vec3;
pub fn write_color(out: &mut impl Write, pixel_color: Color) {
// Write the translated [0, 255] value of each color component
let r = (255.999 * pixel_color.x()) as i32;
let g = (255.999 * pixel_color.y()) as i32;
let b = (255.999 * pixel_color.z()) as i32;
writeln!(out, "{} {} {}", r, g, b).expect("writing color");
}
Now we can change our main to use this:
mod color;
mod vec3;
use std::io;
use color::Color;
fn main() {
// Image
const IMAGE_WIDTH: i32 = 256;
const IMAGE_HEIGHT: i32 = 256;
// 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 r = i as f64 / (IMAGE_WIDTH - 1) as f64;
let g = j as f64 / (IMAGE_HEIGHT - 1) as f64;
let b = 0.25;
let pixel_color = Color::new(r, g, b);
color::write_color(&mut io::stdout(), pixel_color);
}
}
eprint!("\nDone.\n");
}