The vec3 Class 1
Almost all graphics programs have some class(es) for storing geometric vectors and colors. In many systems these vectors are 4D (3D position plus a homogeneous coordinate for geometry, or RGB plus an alpha transparency component for colors). For our purposes, three coordinates suffice. We’ll use the same class vec3 for colors, locations, directions, offsets, whatever. Some people don’t like this because it doesn’t prevent you from doing something silly, like subtracting a position from a color. They have a good point, but we’re going to always take the “less code” route when not obviously wrong. In spite of this, we do declare two aliases for vec3: point3 and color. Since these two types are just aliases for vec3, you won't get warnings if you pass a color to a function expecting a point3, and nothing is stopping you from adding a point3 to a color, but it makes the code a little bit easier to read and to understand.
We define the vec3 class in the top half of a new vec3.h header file, and define a set of useful vector utility functions in the bottom half:
use std::{
fmt::Display,
ops::{Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub},
};
#[derive(Debug, Default, Clone, Copy)]
pub struct Vec3 {
pub e: [f64; 3],
}
pub type Point3 = Vec3;
impl Vec3 {
pub fn new(e0: f64, e1: f64, e2: f64) -> Self {
Self { e: [e0, e1, e2] }
}
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]
}
}
impl Neg for Vec3 {
type Output = Self;
fn neg(self) -> Self::Output {
Self::Output {
e: self.e.map(|e| -e),
}
}
}
impl Index<usize> for Vec3 {
type Output = f64;
fn index(&self, index: usize) -> &Self::Output {
&self.e[index]
}
}
impl IndexMut<usize> for Vec3 {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.e[index]
}
}
impl AddAssign for Vec3 {
fn add_assign(&mut self, rhs: Self) {
self.e[0] += rhs.e[0];
self.e[1] += rhs.e[1];
self.e[2] += rhs.e[2];
}
}
impl MulAssign<f64> for Vec3 {
fn mul_assign(&mut self, rhs: f64) {
self.e[0] *= rhs;
self.e[1] *= rhs;
self.e[2] *= rhs;
}
}
impl DivAssign<f64> for Vec3 {
fn div_assign(&mut self, rhs: f64) {
self.mul_assign(1.0 / rhs);
}
}
impl Display for Vec3 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {} {}", self.e[0], self.e[1], self.e[2])
}
}
impl Add for Vec3 {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self::Output {
e: [
self.e[0] + rhs.e[0],
self.e[1] + rhs.e[1],
self.e[2] + rhs.e[2],
],
}
}
}
impl Sub for Vec3 {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self::Output {
e: [
self.e[0] - rhs.e[0],
self.e[1] - rhs.e[1],
self.e[2] - rhs.e[2],
],
}
}
}
impl Mul for Vec3 {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
Self::Output {
e: [
self.e[0] * rhs.e[0],
self.e[1] * rhs.e[1],
self.e[2] * rhs.e[2],
],
}
}
}
impl Mul<f64> for Vec3 {
type Output = Self;
fn mul(self, rhs: f64) -> Self::Output {
Self::Output {
e: [self.e[0] * rhs, self.e[1] * rhs, self.e[2] * rhs],
}
}
}
impl Mul<Vec3> for f64 {
type Output = Vec3;
fn mul(self, rhs: Vec3) -> Self::Output {
rhs.mul(self)
}
}
impl Div<f64> for Vec3 {
type Output = Self;
fn div(self, rhs: f64) -> Self::Output {
self * (1.0 / rhs)
}
}
#[inline]
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]
}
#[inline]
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],
)
}
#[inline]
pub fn unit_vector(v: Vec3) -> Vec3 {
v / v.length()
}
Listing 4: [vec3.rs] vec3 definitions and helper functions
We use double here, but some ray tracers use float. double has greater precision and range, but is twice the size compared to float. This increase in size may be important if you're programming in limited memory conditions (such as hardware shaders). Either one is fine — follow your own tastes.
-
There are no classes in Rust. They are replaced with structs. ↩