Skip to content

Commit

Permalink
Implement basic camera
Browse files Browse the repository at this point in the history
I made Vector almost-quality comparison more forgiving as I had some
precision problems, will probably need to revisit this in the future.
  • Loading branch information
jstasiak committed Jul 8, 2019
1 parent af936e7 commit ecbd9a6
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 5 deletions.
104 changes: 100 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::ops::{Add, Div, Mul, Sub};
use std::ops::{Add, Div, Mul, Neg, Sub};

#[derive(Copy, Clone, Debug)]
pub struct Vector {
Expand All @@ -9,7 +9,7 @@ pub struct Vector {

impl Vector {
pub fn almost_equal(&self, other: &Vector) -> bool {
self.almost_equal_with_epsilon(other, 0.00000001)
self.almost_equal_with_epsilon(other, 0.0000001)
}

pub fn almost_equal_with_epsilon(&self, other: &Vector, epsilon: f32) -> bool {
Expand All @@ -26,6 +26,30 @@ impl Vector {
}
}

pub fn unitx() -> Vector {
Vector {
x: 1.0,
y: 0.0,
z: 0.0,
}
}

pub fn unity() -> Vector {
Vector {
x: 0.0,
y: 1.0,
z: 0.0,
}
}

pub fn unitz() -> Vector {
Vector {
x: 0.0,
y: 0.0,
z: 1.0,
}
}

pub fn dot(&self, other: &Vector) -> f32 {
self.x * other.x + self.y * other.y + self.z * other.z
}
Expand All @@ -41,6 +65,15 @@ impl Vector {
pub fn len(&self) -> f32 {
(self.x.powf(2.0) + self.y.powf(2.0) + self.z.powf(2.0)).sqrt()
}

pub fn normalized(&self) -> Vector {
let len = self.len();
Vector {
x: self.x / len,
y: self.y / len,
z: self.z / len,
}
}
}

impl Add for Vector {
Expand Down Expand Up @@ -95,7 +128,19 @@ impl Div<f32> for Vector {
}
}

#[derive(Copy, Clone)]
impl Neg for Vector {
type Output = Vector;

fn neg(self) -> Vector {
Vector {
x: -self.x,
y: -self.y,
z: -self.z,
}
}
}

#[derive(Copy, Clone, Debug)]
pub struct Ray {
pub pos: Vector,
pub dir: Vector,
Expand All @@ -108,6 +153,10 @@ impl Ray {
dir: self.dir,
}
}

pub fn almost_equal(&self, other: &Ray) -> bool {
self.pos.almost_equal(&other.pos) && self.dir.almost_equal(&other.dir)
}
}

#[derive(Copy, Clone)]
Expand Down Expand Up @@ -177,9 +226,56 @@ impl Intersection {
}

pub fn almost_equal(a: f32, b: f32) -> bool {
almost_equal_with_epsilon(a, b, 0.00000001)
almost_equal_with_epsilon(a, b, 0.0000001)
}

pub fn almost_equal_with_epsilon(a: f32, b: f32, epsilon: f32) -> bool {
(a - b).abs() < epsilon
}

pub struct Camera {
pub position: Vector,
// The forward and up vectors have to be normalized
pub forward: Vector,
pub up: Vector,
pub aspect_ratio: f32,
pub fovx: Radians,
}

impl Camera {
pub fn screen_ray(&self, x: f32, y: f32) -> Ray {
// We assume that a screen lies 1 unit in front of the camera. The center (x: 0.5, y: 0.5) of the screen
// lies directly on the forward axis.
assert!(0.0 <= x && x <= 1.0);
assert!(0.0 <= y && y <= 1.0);
let right = self.forward.cross(&self.up);
// top left corner is x -1.0, y 1.0
let xunit = posunit_to_unit(x);
let yunit = -posunit_to_unit(y);
// The distance between a point on the screen and the center of the screen forms a right
// triangle with the distance between the camera and the center of the screen and the
// distance between the camera and the point. Since we know the maximum angle we can go in
// either direction (fovx/2 for x, fovy/2 for y) we first calculate the size of the screen
// 1 unit in front of the camera using tangent:
let screen_width = 2.0 * (self.fovx.0 / 2.0).tan();
let screen_height = screen_width / self.aspect_ratio;
// What's left now is to calculate the point at the screen we're looking at and a ray
// pointing to it:
let point_at_screen = self.position
+ self.forward
+ right * xunit * screen_width / 2.0
+ self.up * yunit * screen_height / 2.0;
let ray = Ray {
pos: self.position,
dir: (point_at_screen - self.position).normalized(),
};
ray
}
}

pub fn posunit_to_unit(value: f32) -> f32 {
// Convert value in range [0.0, 1.0] to value in range [-1.0, 1.0]
value * 2.0 - 1.0
}

pub struct Radians(pub f32);
72 changes: 71 additions & 1 deletion tests/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use raytracer::{almost_equal, Intersection, Ray, Sphere, Vector};
use raytracer::{almost_equal, Camera, Intersection, Radians, Ray, Sphere, Vector};

#[test]
fn test_add() {
Expand Down Expand Up @@ -123,6 +123,22 @@ fn test_vector_cross_product() {
assert!(got.almost_equal(&expected), "Got: {:?}", got);
}

#[test]
fn test_vector_normalization() {
let original = Vector {
x: 1.0,
y: 2.0,
z: 3.0,
};
let expected = Vector {
x: 0.2672612419124244,
y: 0.5345224838248488,
z: 0.8017837257372732,
};
let got = original.normalized();
assert!(got.almost_equal(&expected), "Got: {:?}", got);
}

#[test]
fn test_sphere_ray_intersection() {
let sphere = Sphere {
Expand Down Expand Up @@ -187,3 +203,57 @@ fn test_sphere_ray_intersection() {
intersection3
);
}

#[test]
fn test_camera_screen_ray() {
let camera = Camera {
position: Vector::zero(),
forward: -Vector::unitz(),
up: Vector::unity(),
aspect_ratio: 2.0 / 1.0,
fovx: Radians(90.0f32.to_radians()),
};

let expected_top_left_corner_ray = Ray {
pos: Vector::zero(),
dir: Vector {
x: -1.0,
y: 0.5,
z: -1.0,
}
.normalized(),
};
let got_top_left_corner_ray = camera.screen_ray(0.0, 0.0);
assert!(
got_top_left_corner_ray.almost_equal(&expected_top_left_corner_ray),
"Got: {:?}",
got_top_left_corner_ray
);

let expected_center_screen_ray = Ray {
pos: Vector::zero(),
dir: -Vector::unitz(),
};
let got_center_screen_ray = camera.screen_ray(0.5, 0.5);
assert!(
got_center_screen_ray.almost_equal(&expected_center_screen_ray),
"Got: {:?}",
got_center_screen_ray
);

let expected_quarter_screen_ray = Ray {
pos: Vector::zero(),
dir: Vector {
x: -0.5,
y: 0.25,
z: -1.0,
}
.normalized(),
};
let got_quarter_screen_ray = camera.screen_ray(0.25, 0.25);
assert!(
got_quarter_screen_ray.almost_equal(&expected_quarter_screen_ray),
"Got: {:?}",
got_quarter_screen_ray
);
}

0 comments on commit ecbd9a6

Please sign in to comment.