+
+
+
\ No newline at end of file
diff --git a/dist/raytracing-finished.png b/dist/raytracing-finished.png
new file mode 100644
index 0000000..db85207
Binary files /dev/null and b/dist/raytracing-finished.png differ
diff --git a/dist/raytracing.html b/dist/raytracing.html
new file mode 100644
index 0000000..e39f5e2
--- /dev/null
+++ b/dist/raytracing.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ Graphische Datenverarbeitung - Raytracing - Basic Raytracing
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/04/bresenhamsimple.ts b/src/04/bresenhamsimple.ts
index e680b39..8a2d794 100644
--- a/src/04/bresenhamsimple.ts
+++ b/src/04/bresenhamsimple.ts
@@ -21,4 +21,23 @@ export function bresenhamSimple(data: Uint8ClampedArray, pointA: [number, number
// TODO: 1. Calculate dx and dy and set the start position x and y
// TODO: 2. Calculate the initial epsilon of the bresenham algorithm
// TODO: 3. Go from pointA[0] to pointB[0], and update epsilon in each step as given in the bresenham algorithm. Increase y when necessary.
+
+ var dX = pointB[0] - pointA[0];
+ var dY = pointB[1] - pointA[1];
+ var x = pointA[0];
+ var y = pointA[1];
+ var err = 2 * dY - dX;
+
+ while(x < pointB[0]) {
+ if(err <= 0) {
+ err += 2 * dY;
+ } else {
+ y = y + 1;
+ err += 2*dY - 2*dX;
+ }
+ setPixel(data, x, y, width, height);
+ x++;
+ }
+
+
}
diff --git a/src/04/ddasimple.ts b/src/04/ddasimple.ts
index 0d3898c..bbd1f10 100644
--- a/src/04/ddasimple.ts
+++ b/src/04/ddasimple.ts
@@ -25,4 +25,16 @@ export function ddaSimple(
// TODO: Calculcate the slope m for a line from pointA to pointB.
// TODO: In this example, the main direction of the line is the x-direction.
// TODO: Go from the x-coordinate of pointA (pointA[0]) to the x-coordinate of pointB (pointB[0]) and calculate the y-coordinate of the pixels in between.
+ pointA[0] = Math.round(pointA[0]);
+ pointA[1] = Math.round(pointA[1]);
+ pointB[0] = Math.round(pointB[0]);
+ pointB[1] = Math.round(pointB[1]);
+
+ var m = (pointB[1] + pointA[1]) / (pointB[0] + pointA[0]);
+ setPixel(data, pointA[0], pointA[1], width, height);
+ setPixel(data, pointB[0], pointB[1], width, height);
+
+ for(let i = 1; i < (pointB[0] - pointA[0]); i++) {
+ setPixel(data, pointA[0] + i, pointA[1] + Math.round( m * i), width, height);
+ }
}
diff --git a/src/05/camera.ts b/src/05/camera.ts
new file mode 100644
index 0000000..19557be
--- /dev/null
+++ b/src/05/camera.ts
@@ -0,0 +1,32 @@
+import Vector from '../05/vector';
+
+/**
+ * A class representing a camera
+ */
+export default class Camera {
+
+ public width: number;
+ public height: number;
+ public alpha: number;
+ public origin: Vector;
+
+ /**
+ * Creates a new camera with an image canvas, a field of view, and a position in world space.
+ * For now, the camera is always viewing along the negative z-axis.
+ * @param width The width of the canvas
+ * @param height The height of the canvas
+ * @param alpha The field of view in X dimension of the camera
+ * @param origin The origin of the camera in world coordinates
+ */
+ constructor(
+ width: number,
+ height: number,
+ alpha: number,
+ origin: Vector = new Vector(0, 0, 0, 1)
+ ) {
+ this.width = width;
+ this.height = height;
+ this.alpha = alpha;
+ this.origin = origin;
+ }
+}
diff --git a/src/05/intersection.ts b/src/05/intersection.ts
new file mode 100644
index 0000000..45ba158
--- /dev/null
+++ b/src/05/intersection.ts
@@ -0,0 +1,39 @@
+import Vector from './vector';
+
+/**
+ * Class representing a ray-sphere intersection in 3D space
+ */
+export default class Intersection {
+
+ public t: number;
+ public point: Vector;
+ public normal: Vector;
+
+ /**
+ * Create an Intersection
+ * @param t The distance on the ray
+ * @param point The intersection point
+ * @param normal The normal of the surface at the point of intersection
+ */
+ constructor(t: number = Infinity,
+ point: Vector = null,
+ normal: Vector = null) {
+
+ this.t = t;
+ this.point = point;
+ this.normal = normal;
+ }
+
+ /**
+ * Determines whether this intersection
+ * is closer than the other
+ * @param other The other Intersection
+ * @return The result
+ */
+ closerThan(other: Intersection): boolean {
+ if (this.t < other.t)
+ return true;
+ else
+ return false;
+ }
+}
diff --git a/src/05/manyspheres.ts b/src/05/manyspheres.ts
new file mode 100644
index 0000000..db895b2
--- /dev/null
+++ b/src/05/manyspheres.ts
@@ -0,0 +1,28 @@
+import Camera from './camera';
+import Sphere from './sphere';
+import Intersection from './intersection';
+import Vector from './vector';
+import Ray from './ray';
+
+/**
+ * Compute the color of the pixel (x, y) by raytracing
+ * using a given camera and multiple spheres.
+ *
+ * @param data The linearised pixel array
+ * @param camera The camera used for raytracing
+ * @param spheres The spheres to raytrace
+ * @param x The x coordinate of the pixel to convert
+ * @param y The y coordinate of the pixel to convert
+ * @param width The width of the canvas
+ * @param height The height of the canvas
+ */
+export function raytrace(data: Uint8ClampedArray,
+ camera: Camera,
+ spheres: Array,
+ x: number, y: number,
+ width: number, height: number) {
+
+ // TODO: Generate ray and perform intersection with every sphere.
+ // TODO: On intersection set pixel color to color of the sphere
+ // TODO: containing the closest intersection point.
+}
diff --git a/src/05/ray.ts b/src/05/ray.ts
new file mode 100644
index 0000000..ff38d30
--- /dev/null
+++ b/src/05/ray.ts
@@ -0,0 +1,38 @@
+import Vector from './vector';
+import Camera from './camera';
+
+/**
+ * Class representing a ray
+ */
+export default class Ray {
+
+ public origin: Vector = null;
+ public direction: Vector = null;
+
+ /**
+ * Creates a new ray with origin and direction
+ * @param origin The origin of the Ray
+ * @param direction The direction of the Ray
+ */
+ constructor(origin: Vector, direction: Vector) {
+
+ this.origin = origin;
+ this.direction = direction;
+ }
+
+ /**
+ * Creates a ray from the camera through the image plane.
+ * The image plane is positioned in direction of the negative z-axis.
+ * @param x The pixel's x-position in the canvas
+ * @param y The pixel's y-position in the canvas
+ * @param camera The Camera
+ * @return The resulting Ray
+ */
+ static makeRay(x: number, y: number, camera: Camera): Ray {
+ // TODO: Generate a ray from the camera origin through pixel (x, y)
+ // TODO: on the image plane. In addition to the coordinates (x, y), you will need the
+ // TODO: width and height of the camera (i.e. the width and height of the camera's
+ // TODO: image plane), and the angle alpha specifying the camera's field of view.
+ return null;
+ }
+}
diff --git a/src/05/raytracing.ts b/src/05/raytracing.ts
new file mode 100644
index 0000000..b05d695
--- /dev/null
+++ b/src/05/raytracing.ts
@@ -0,0 +1,28 @@
+import Camera from './camera';
+import Sphere from './sphere';
+import Ray from './ray';
+
+/**
+ * Compute the color of the pixel (x, y) by raytracing
+ * using a given camera and a sphere.
+ *
+ * @param data The linearised pixel array
+ * @param camera The camera used for raytracing
+ * @param sphere The sphere to raytrace
+ * @param x The x coordinate of the pixel to convert
+ * @param y The y coordinate of the pixel to convert
+ * @param width The width of the canvas
+ * @param height The height of the canvas
+ */
+
+export function raytrace(data: Uint8ClampedArray,
+ camera: Camera,
+ sphere: Sphere,
+ x: number, y: number,
+ width: number, height: number) {
+
+ // TODO: Create a ray from the camera's position through the pixel
+ // TODO: (x, y) in the camera's image plane, and perform intersection
+ // TODO: with the given sphere. Set color of pixel (x, y) in the data
+ // TODO: array to black, if the ray hits the sphere.
+}
diff --git a/src/05/setup-manyspheres.ts b/src/05/setup-manyspheres.ts
new file mode 100644
index 0000000..f2a68bd
--- /dev/null
+++ b/src/05/setup-manyspheres.ts
@@ -0,0 +1,51 @@
+import 'bootstrap';
+import 'bootstrap/scss/bootstrap.scss';
+import Sphere from './sphere';
+import Vector from './vector';
+import Camera from './camera';
+import { raytrace } from './manyspheres';
+
+window.addEventListener('load', evt => {
+
+ const canvas = document.getElementById("result") as HTMLCanvasElement;
+
+ if (canvas === null)
+ return;
+
+ const ctx = canvas.getContext("2d");
+ var pixel = ctx.createImageData(1, 1);
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const data = imageData.data;
+
+ const spheres: Sphere[] = [
+ new Sphere(
+ new Vector(0, 0, -10, 1),
+ 2.0,
+ new Vector(1, 0, 0, 1)
+ ),
+ new Sphere(
+ new Vector(2, 0, -12, 1),
+ 1.5,
+ new Vector(0, 1, 0, 1)
+ ),
+ new Sphere(
+ new Vector(-2, 0, -8, 1),
+ 1.0,
+ new Vector(0, 0, 1, 1)
+ )
+ ];
+
+ const camera = new Camera(canvas.width, canvas.height, Math.PI / 3);
+
+ for (let y = 0; y < canvas.height; y++) {
+ for (let x = 0; x < canvas.width; x++) {
+
+ raytrace(data, camera, spheres, x, y, canvas.width, canvas.height);
+
+ // update pixel in HTML context2d
+ for (let i = 0; i < 4; i ++)
+ pixel.data[i] = data[(x + y * canvas.width) * 4 + i];
+ ctx.putImageData(pixel, x, y);
+ }
+ }
+});
diff --git a/src/05/setup-raytracing.ts b/src/05/setup-raytracing.ts
new file mode 100644
index 0000000..891fbdc
--- /dev/null
+++ b/src/05/setup-raytracing.ts
@@ -0,0 +1,38 @@
+import 'bootstrap';
+import 'bootstrap/scss/bootstrap.scss';
+import Sphere from './sphere';
+import Vector from './vector';
+import Camera from './camera';
+import { raytrace } from './raytracing';
+
+window.addEventListener('load', evt => {
+
+ const canvas = document.getElementById("result") as HTMLCanvasElement;
+ if (canvas === null)
+ return;
+
+ const ctx = canvas.getContext("2d");
+ var pixel = ctx.createImageData(1, 1);
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const data = imageData.data;
+
+ const sphere = new Sphere(
+ new Vector(0, 0, -10, 1), // position
+ 4.0, // radius
+ new Vector(0, 0, 0, 1) // color
+ );
+
+ const camera = new Camera(canvas.width, canvas.height, Math.PI / 3);
+
+ for (let y = 0; y < canvas.height; y++) {
+ for (let x = 0; x < canvas.width; x++) {
+
+ raytrace(data, camera, sphere, x, y, canvas.width, canvas.height);
+
+ // update pixel in HTML context2d
+ for (let i = 0; i < 4; i ++)
+ pixel.data[i] = data[(x + y * canvas.width) * 4 + i];
+ ctx.putImageData(pixel, x, y);
+ }
+ }
+});
diff --git a/src/05/sphere.ts b/src/05/sphere.ts
new file mode 100644
index 0000000..bbf22cc
--- /dev/null
+++ b/src/05/sphere.ts
@@ -0,0 +1,49 @@
+import Vector from './vector';
+import Intersection from './intersection';
+import Ray from './ray';
+
+/**
+ * A class representing a sphere
+ */
+export default class Sphere {
+
+ public center: Vector;
+ public radius: number;
+ public color: Vector;
+
+ /**
+ * Creates a new Sphere with center and radius
+ * @param center The center of the Sphere
+ * @param radius The radius of the Sphere
+ * @param color The color of the Sphere
+ */
+ constructor(
+ center: Vector,
+ radius: number,
+ color: Vector
+ ) {
+ this.center = center;
+ this.radius = radius;
+ this.color = color;
+ }
+
+ /**
+ * Calculates the intersection of the sphere with the given ray
+ * @param ray The ray to intersect with
+ * @return The intersection if there is one, null if there is none
+ */
+ intersect(ray: Ray): Intersection | null {
+
+ // TODO: Calculate the quadratic equation for ray-sphere
+ // TODO: intersection. You will need the origin of your ray as x0,
+ // TODO: the ray direction, and the radius of the sphere.
+ // TODO: Don't forget to translate your ray's starting position with
+ // TODO: respect to the center of the sphere.
+ // TODO: Calculate the discriminant c, and distinguish between the 3
+ // TODO: possible outcomes: no hit, one hit, or two hits.
+ // TODO: Return an Intersection or null if there was no hit. In case
+ // TODO: of two hits, return the one closer to the start point of
+ // TODO: the ray.
+ return null;
+ }
+}
diff --git a/src/05/vector.ts b/src/05/vector.ts
new file mode 100644
index 0000000..9efa4b4
--- /dev/null
+++ b/src/05/vector.ts
@@ -0,0 +1,256 @@
+/**
+ * Class representing a vector in 4D space
+ */
+export default class Vector {
+ /**
+ * The variable to hold the vector data
+ */
+ data: [number, number, number, number];
+
+ /**
+ * Create a vector
+ * @param x The x component
+ * @param y The y component
+ * @param z The z component
+ * @param w The w component
+ */
+ constructor(x: number, y: number, z: number, w: number) {
+ // TODO: Set the data member components to the given values
+ }
+
+ /**
+ * Returns the x component of the vector
+ * @return The x component of the vector
+ */
+ get x(): number {
+ // TODO: Return actual value
+ return null;
+ }
+
+ /**
+ * Sets the x component of the vector to val
+ * @param val - The new value
+ */
+ set x(val: number) {
+ // TODO: Set actual value
+ }
+
+ /**
+ * Returns the first component of the vector
+ * @return The first component of the vector
+ */
+ get r(): number {
+ // TODO: Return actual value
+ return null;
+ }
+
+ /**
+ * Sets the first component of the vector to val
+ * @param val The new value
+ */
+ set r(val: number) {
+ // TODO: Set actual value
+ }
+
+ /**
+ * Returns the y component of the vector
+ * @return The y component of the vector
+ */
+ get y(): number {
+ // TODO: Return actual value
+ return null;
+ }
+
+ /**
+ * Sets the y component of the vector to val
+ * @param val The new value
+ */
+ set y(val: number) {
+ // TODO: Set actual value
+ }
+
+ /**
+ * Returns the second component of the vector
+ * @return The second component of the vector
+ */
+ get g(): number {
+ // TODO: Return actual value
+ return null;
+ }
+
+ /**
+ * Sets the second component of the vector to val
+ * @param val The new value
+ */
+ set g(val: number) {
+ // TODO: Set actual value
+ }
+
+ /**
+ * Returns the z component of the vector
+ * @return The z component of the vector
+ */
+ get z(): number {
+ // TODO: Return actual value
+ return null;
+ }
+
+ /**
+ * Sets the z component of the vector to val
+ * @param val The new value
+ */
+ set z(val: number) {
+ // TODO: Set actual value
+ }
+
+ /**
+ * Returns the third component of the vector
+ * @return The third component of the vector
+ */
+ get b(): number {
+ // TODO: Return actual value
+ return null;
+ }
+
+ /**
+ * Sets the third component of the vector to val
+ * @param val The new value
+ */
+ set b(val: number) {
+ // TODO: Set actual value
+ }
+
+ /**
+ * Returns the w component of the vector
+ * @return The w component of the vector
+ */
+ get w(): number {
+ // TODO: Return actual value
+ return null;
+ }
+
+ /**
+ * Sets the w component of the vector to val
+ * @param val The new value
+ */
+ set w(val: number) {
+ // TODO: Set actual value
+ }
+
+ /**
+ * Returns the fourth component of the vector
+ * @return The fourth component of the vector
+ */
+ get a(): number {
+ // TODO: Return actual value
+ return null;
+ }
+
+ /**
+ * Sets the fourth component of the vector to val
+ * @param val The new value
+ */
+ set a(val: number) {
+ // TODO: Set actual value
+ }
+
+ /**
+ * Creates a new vector with the vector added
+ * @param other The vector to add
+ * @return The new vector;
+ */
+ add(other: Vector): Vector {
+ // TODO: Return new vector with result
+ return null;
+ }
+
+ /**
+ * Creates a new vector with the vector subtracted
+ * @param other The vector to subtract
+ * @return The new vector
+ */
+ sub(other: Vector): Vector {
+ // TODO: Return new vector with result
+ return null;
+ }
+
+ /**
+ * Creates a new vector with the scalar multiplied
+ * @param other The scalar to multiply
+ * @return The new vector
+ */
+ mul(other: number): Vector {
+ // TODO: Return new vector with result
+ return null;
+ }
+
+ /**
+ * Creates a new vector with the scalar divided
+ * @param other The scalar to divide
+ * @return The new vector
+ */
+ div(other: number): Vector {
+ // TODO: Return new vector with result
+ return null;
+ }
+
+ /**
+ * Dot product
+ * @param other The vector to calculate the dot product with
+ * @return The result of the dot product
+ */
+ dot(other: Vector): number {
+ // TODO: Compute and return dot product
+ return 0;
+ }
+
+ /**
+ * Cross product
+ * Calculates the cross product using the first three components.
+ * @param other The vector to calculate the cross product with
+ * @return The result of the cross product as new Vector
+ */
+ cross(other: Vector): Vector {
+ // TODO: Return new vector with result
+ // TODO: The fourth component should be set to 0
+ return null;
+ }
+
+ /**
+ * Normalizes this vector in place
+ * @returns this vector for easier function chaining
+ */
+ normalize(): Vector {
+ // TODO: Normalize this vector and return it
+ return this;
+ }
+
+ /**
+ * Compares the vector to another vector.
+ * @param other The vector to compare to.
+ * @return True if the vectors carry equal numbers.
+ */
+ equals(other: Vector): boolean {
+ // TODO: Perform comparison and return result
+ // TODO: Respect inaccuracies: coordinates within 0.000001 of each other
+ // TODO: should be considered equal
+ return false;
+ }
+
+ /**
+ * Calculates the length of the vector
+ * @return The length of the vector
+ */
+ get length(): number {
+ // TODO: Calculate and return length
+ return 0;
+ }
+
+ /**
+ * Returns an array representation of the vector
+ * @return An array representation.
+ */
+ valueOf(): [number, number, number, number] {
+ return this.data;
+ }
+}
diff --git a/src/06/phong.ts b/src/06/phong.ts
new file mode 100644
index 0000000..87361f2
--- /dev/null
+++ b/src/06/phong.ts
@@ -0,0 +1,35 @@
+import Vector from '../05/vector';
+import Intersection from '../05/intersection';
+
+/**
+ * Calculate the color of an object at the intersection point according to the Phong Lighting model.
+ * @param color The color of the intersected object
+ * @param intersection The intersection information
+ * @param lightPositions The light positions
+ * @param shininess The shininess parameter of the Phong model
+ * @param cameraPosition The position of the camera
+ * @return The resulting color
+ */
+export function phong(
+ color: Vector,
+ intersection: Intersection,
+ lightPositions: Array,
+ shininess: number,
+ cameraPosition: Vector
+): Vector {
+
+ const lightColor = new Vector(0.8, 0.8, 0.8, 0);
+ const kA = 1.0;
+ const kD = 0.5;
+ const kS = 0.5;
+
+ // TODO: Compute light intensity according to phong reflection model.
+ // TODO: Compute diffuse lighting using light color, diffuse
+ // TODO: reflectivity, light positions and an intersection point.
+ // TODO: Compute specular reflection using light color, specular
+ // TODO: reflectivity, shininess, light positions, an intersection
+ // TODO: point, and eye (camera) position.
+ // TODO: Return complete phong emission using object color, ambient,
+ // TODO: diffuse and specular terms.
+ return color;
+}
diff --git a/src/06/raytracing.ts b/src/06/raytracing.ts
new file mode 100644
index 0000000..0c6040c
--- /dev/null
+++ b/src/06/raytracing.ts
@@ -0,0 +1,32 @@
+import Camera from '../05/camera';
+import Sphere from '../05/sphere';
+import Intersection from '../05/intersection';
+import Vector from '../05/vector';
+import Ray from '../05/ray';
+import { phong } from './phong';
+
+/**
+ * Compute the color of the pixel (x, y) by raytracing
+ * using a given camera and multiple spheres.
+ *
+ * @param data The linearised pixel array
+ * @param camera The camera used for raytracing
+ * @param spheres The spheres to raytrace
+ * @param x The x coordinate of the pixel to convert
+ * @param y The y coordinate of the pixel to convert
+ * @param width The width of the canvas
+ * @param height The height of the canvas
+ */
+export function raytracePhong(data: Uint8ClampedArray,
+ camera: Camera,
+ spheres: Array,
+ lightPositions: Array,
+ shininess: number,
+ x: number, y: number,
+ width: number, height: number) {
+
+ // TODO: Create ray from camera through image plane at position (x, y).
+ // TODO: Compute closest intersection with spheres in the scene.
+ // TODO: Compute emission at point of intersection using phong model.
+ // TODO: Set pixel color accordingly.
+}
diff --git a/src/06/setup-phong.ts b/src/06/setup-phong.ts
new file mode 100644
index 0000000..d07dc8a
--- /dev/null
+++ b/src/06/setup-phong.ts
@@ -0,0 +1,62 @@
+import 'bootstrap';
+import 'bootstrap/scss/bootstrap.scss';
+import Vector from '../05/vector';
+import Sphere from '../05/sphere';
+import Ray from '../05/ray';
+import Camera from '../05/camera';
+import Intersection from '../05/intersection';
+import { raytracePhong } from './raytracing';
+
+window.addEventListener('load', () => {
+
+ const canvas = document.getElementById("result") as HTMLCanvasElement;
+ if (canvas === null)
+ return;
+
+ const ctx = canvas.getContext("2d");
+ var pixel = ctx.createImageData(1, 1);
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const data = imageData.data;
+
+ const spheres: Sphere[] = [
+ new Sphere(new Vector(.5, -.2, -2, 1), 0.4, new Vector(.3, 0, 0, 1)),
+ new Sphere(new Vector(-.5, -.2, -1.7, 1), 0.2, new Vector(0, 0, .3, 1))
+ ];
+
+ const lightPositions = [
+ new Vector(1, 1, -1, 1)
+ ];
+
+ let shininess = 10;
+
+ const camera = new Camera(
+ canvas.width, canvas.height, Math.PI / 3, new Vector(0, 0, 0, 1)
+ );
+
+ function animate() {
+
+ for (let y = 0; y < canvas.height; y++) {
+ for (let x = 0; x < canvas.width; x++) {
+
+ raytracePhong(data, camera, spheres, lightPositions, shininess, x, y, canvas.width, canvas.height);
+
+ // update pixel in HTML context2d
+ for (let i = 0; i < 4; i ++)
+ pixel.data[i] = data[(x + y * canvas.width) * 4 + i];
+ ctx.putImageData(pixel, x, y);
+ }
+ }
+ }
+
+ const shininessElement =
+ document.getElementById("shininess") as HTMLInputElement;
+
+ shininessElement.onchange = function () {
+ shininess = Number(shininessElement.value);
+ window.requestAnimationFrame(animate);
+ }
+
+ shininess = Number(shininessElement.value);
+
+ window.requestAnimationFrame(animate);
+});
diff --git a/test/ray-spec.ts b/test/ray-spec.ts
new file mode 100644
index 0000000..321db33
--- /dev/null
+++ b/test/ray-spec.ts
@@ -0,0 +1,50 @@
+import Ray from "../src/05/ray";
+import Camera from "../src/05/camera";
+
+import { assert, expect } from 'chai';
+
+describe('Ray', () => {
+
+ it('can be initialized with two numbers and a camera', () => {
+ const r: Ray = Ray.makeRay(0, 0, new Camera(128, 128, 45));
+ expect(r).to.be.an('object');
+ });
+
+ it('the origin of the ray is initialized correctly', () => {
+ const r: Ray = Ray.makeRay(0, 0, new Camera(128, 128, 45));
+ expect(r).to.be.an('object');
+ expect(r.origin.x).to.equal(0);
+ expect(r.origin.y).to.equal(0);
+ expect(r.origin.z).to.equal(0);
+ expect(r.origin.w).to.equal(1);
+ });
+
+ it('the direction is normalized', () => {
+ const r: Ray = Ray.makeRay(64, 64, new Camera(129, 129, 45));
+ expect(r).to.be.an('object');
+ expect(r.direction.length).to.equal(1);
+ });
+
+ it('the direction is initialized correctly', () => {
+ const r: Ray = Ray.makeRay(64, 64, new Camera(129, 129, 45));
+ expect(r).to.be.an('object');
+ expect(r.direction.x).to.be.closeTo(0, 0.01);
+ expect(r.direction.y).to.be.closeTo(0, 0.01);
+ expect(r.direction.z).to.be.closeTo(-1, 0.01);
+ expect(r.direction.w).to.be.closeTo(0, 0.01);
+
+ const r2: Ray = Ray.makeRay(0, 0, new Camera(129, 129, 45));
+ expect(r2).to.be.an('object');
+ expect(r2.direction.x).to.be.closeTo(-0.435, 0.01);
+ expect(r2.direction.y).to.be.closeTo(0.435, 0.01);
+ expect(r2.direction.z).to.be.closeTo(-0.787, 0.01);
+ expect(r2.direction.w).to.be.closeTo(0, 0.01);
+
+ const r3: Ray = Ray.makeRay(10, 7, new Camera(64, 64, 90));
+ expect(r3).to.be.an('object');
+ expect(r3.direction.x).to.be.closeTo(-0.564, 0.01);
+ expect(r3.direction.y).to.be.closeTo(0.642, 0.01);
+ expect(r3.direction.z).to.be.closeTo(-0.518, 0.01);
+ expect(r3.direction.w).to.equal(0);
+ });
+});
diff --git a/test/sphere-spec.ts b/test/sphere-spec.ts
new file mode 100644
index 0000000..ea43650
--- /dev/null
+++ b/test/sphere-spec.ts
@@ -0,0 +1,78 @@
+import Sphere from "../src/05/sphere";
+import Intersection from "../src/05/intersection";
+
+import { assert, expect } from 'chai';
+import Vector from "../src/05/vector";
+import Ray from "../src/05/ray";
+
+describe('Sphere', () => {
+
+ it('can be initialized with center, radius and color', () => {
+ const s: Sphere = new Sphere(new Vector(0, 0, 0, 1), 1, new Vector(0, 0, 0, 0));
+ expect(s).to.be.an('object');
+ });
+
+ it('a sphere at origin and radius 1 can be intersected correctly with the x axis', () => {
+
+ const s: Sphere = new Sphere(new Vector(0, 0, 0, 1), 1, new Vector(0, 0, 0, 0));
+ const i: Intersection = s.intersect(new Ray(new Vector(-10, 0, 0, 1), new Vector(1, 0, 0, 0)));
+ expect(s).to.be.an('object');
+ expect(i).to.be.an('object');
+
+ expect(i.point.x).to.equal(-1);
+ expect(i.point.y).to.equal(0);
+ expect(i.point.z).to.equal(0);
+ });
+
+ it('a sphere at origin and radius 1 can be intersected correctly with the y axis', () => {
+
+ const s: Sphere = new Sphere(new Vector(0, 0, 0, 1), 1, new Vector(0, 0, 0, 0));
+ const i: Intersection = s.intersect(new Ray(new Vector(0, -10, 0, 1), new Vector(0, 1, 0, 0)));
+ expect(s).to.be.an('object');
+ expect(i).to.be.an('object');
+
+ expect(i.point.x).to.equal(0);
+ expect(i.point.y).to.equal(-1);
+ expect(i.point.z).to.equal(0);
+ });
+
+ it('a sphere at origin and radius 1 can be intersected correctly with the z axis', () => {
+
+ const s: Sphere = new Sphere(new Vector(0, 0, 0, 1), 1, new Vector(0, 0, 0, 0));
+ const i: Intersection = s.intersect(new Ray(new Vector(0, 0, -10, 1), new Vector(0, 0, 1, 0)));
+ expect(s).to.be.an('object');
+ expect(i).to.be.an('object');
+
+ expect(i.point.x).to.equal(0);
+ expect(i.point.y).to.equal(0);
+ expect(i.point.z).to.equal(-1);
+ });
+
+ it('intersection is correct when radius != 1', () => {
+
+ const s: Sphere = new Sphere(new Vector(0, 0, 0, 1), 2.5, new Vector(0, 0, 0, 0));
+ const i: Intersection = s.intersect(new Ray(new Vector(-10, 0, 0, 1), new Vector(1, 0, 0, 0)));
+ expect(s).to.be.an('object');
+ expect(i).to.be.an('object');
+
+ expect(i.point.x).to.equal(-2.5);
+ expect(i.point.y).to.equal(0);
+ expect(i.point.z).to.equal(0);
+ expect(i.t).to.equal(7.5);
+ });
+
+ it('intersection is correct when center != (0, 0, 0, 1)', () => {
+
+ const s: Sphere = new Sphere(new Vector(1, 0, 0, 1), 2.5, new Vector(0, 0, 0, 0));
+ const i: Intersection = s.intersect(new Ray(new Vector(-10, 0, 0, 1), new Vector(1, 0, 0, 0)));
+ expect(s).to.be.an('object');
+ expect(i).to.be.an('object');
+
+ expect(i.point.x).to.equal(-1.5);
+ expect(i.point.y).to.equal(0);
+ expect(i.point.z).to.equal(0);
+ expect(i.t).to.equal(8.5);
+ });
+});
+
+// TODO: Test normalization of normal
diff --git a/test/vector-spec.ts b/test/vector-spec.ts
new file mode 100644
index 0000000..6a74af3
--- /dev/null
+++ b/test/vector-spec.ts
@@ -0,0 +1,275 @@
+import Vector from "../src/05/vector";
+
+import { expect } from 'chai';
+
+describe('Vector', () => {
+
+ it('can be initialized with 4 numbers', () => {
+ const v = new Vector(1, 2, 3, 4);
+ expect(v).to.be.an('object');
+ expect(v.data[0]).to.be.a("number");
+ expect(v.data[0]).to.equal(1);
+ expect(v.data[1]).to.be.a("number");
+ expect(v.data[1]).to.equal(2);
+ expect(v.data[2]).to.be.a("number");
+ expect(v.data[2]).to.equal(3);
+ expect(v.data[3]).to.be.a("number");
+ expect(v.data[3]).to.equal(4);
+ });
+
+ it('x component can be set and retrieved', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).has.property('x');
+ v.x = 42;
+ expect(v.x).to.equal(42);
+ });
+
+ it('y component can be set and retrieved', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).has.property('y');
+ v.y = 42;
+ expect(v.y).to.equal(42);
+ });
+
+ it('z component can be set and retrieved', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).has.property('z');
+ v.z = 42;
+ expect(v.z).to.equal(42);
+ });
+
+ it('w component can be set and retrieved', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).has.property('w');
+ v.w = 42;
+ expect(v.w).to.equal(42);
+ });
+
+ it('r component can be set and retrieved', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).has.property('r');
+ v.r = 42;
+ expect(v.r).to.equal(42);
+ });
+
+ it('g component can be set and retrieved', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).has.property('g');
+ v.g = 42;
+ expect(v.g).to.equal(42);
+ });
+
+ it('b component can be set and retrieved', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).has.property('b');
+ v.b = 42;
+ expect(v.b).to.equal(42);
+ });
+
+ it('a component can be set and retrieved', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).has.property('a');
+ v.a = 42;
+ expect(v.a).to.equal(42);
+ });
+
+ it('method add exists', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).to.respondTo('add');
+ });
+
+ it('method sub exists', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).to.respondTo('sub');
+ });
+
+ it('method mul exists', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).to.respondTo('mul');
+ });
+
+ it('method div exists', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).to.respondTo('div');
+ });
+
+ it('method normalize exists', () => {
+ const v = new Vector(0, 0, 0, 0);
+ expect(v).to.respondTo('normalize');
+ });
+
+ it('length returns the correct result', () => {
+ const v = new Vector(0, 4, 3, 0);
+ expect(v).has.property('length');
+ expect(v.length).to.equal(5);
+ });
+
+ it('addition adds the other vector', () => {
+ const a = new Vector(1, 2, 3, 1);
+ const b = new Vector(1, -2, 0, 0);
+ expect(a).to.respondTo('add');
+
+ const c: Vector = a.add(b);
+ expect(c).to.not.be.null;
+ expect(c).to.be.an('object');
+
+ expect(c.x).to.equal(2);
+ expect(c.y).to.equal(0);
+ expect(c.z).to.equal(3);
+ expect(c.w).to.equal(1);
+ });
+
+ it('addition does not change the value of "this"', () => {
+
+ const a = new Vector(1, 2, 3, 1);
+ const b = new Vector(1, -2, 0, 0);
+ expect(a).to.respondTo('add');
+
+ const c: Vector = a.add(b);
+ expect(a).to.not.be.null;
+ expect(a).to.be.an('object');
+
+ expect(a.x).to.equal(1);
+ expect(a.y).to.equal(2);
+ expect(a.z).to.equal(3);
+ expect(a.w).to.equal(1);
+ });
+
+ it('subtraction subtracts the other vector', () => {
+ const a = new Vector(1, 3, 3, 0);
+ const b = new Vector(2, -2, 0, 0);
+ expect(a).to.respondTo('sub');
+
+ const c: Vector = a.sub(b);
+ expect(c).to.not.be.null;
+ expect(c).to.be.an('object');
+
+ expect(c.x).to.equal(-1);
+ expect(c.y).to.equal(5);
+ expect(c.z).to.equal(3);
+ expect(c.w).to.equal(0);
+ });
+
+ it('subtraction does not change the value of "this"', () => {
+
+ const a = new Vector(1, 2, 3, 1);
+ const b = new Vector(1, -2, 0, 0);
+ expect(a).to.respondTo('sub');
+
+ const c: Vector = a.sub(b);
+ expect(a).to.not.be.null;
+ expect(a).to.be.an('object');
+
+ expect(a.x).to.equal(1);
+ expect(a.y).to.equal(2);
+ expect(a.z).to.equal(3);
+ expect(a.w).to.equal(1);
+ });
+
+ it('multiplication with a scalar multiplies correctly', () => {
+ const a = new Vector(1, 3, 3, 0);
+ expect(a).to.respondTo('mul');
+
+ const c: Vector = a.mul(4);
+ expect(c.x).to.equal(4);
+ expect(c.y).to.equal(12);
+ expect(c.z).to.equal(12);
+ expect(c.w).to.equal(0);
+ });
+
+ it('multiplication does not change the value of "this"', () => {
+ const a = new Vector(1, 3, 3, 0);
+ expect(a).to.respondTo('mul');
+
+ const c: Vector = a.mul(4);
+ expect(a.x).to.equal(1);
+ expect(a.y).to.equal(3);
+ expect(a.z).to.equal(3);
+ expect(a.w).to.equal(0);
+ });
+
+
+ it('division by a scalar divides correctly', () => {
+ const a = new Vector(3, 12, 6, 0);
+ expect(a).to.respondTo('div');
+
+ const c: Vector = a.div(3);
+ expect(c.x).to.equal(1);
+ expect(c.y).to.equal(4);
+ expect(c.z).to.equal(2);
+ expect(c.w).to.equal(0);
+ });
+
+ it('division does not change the value of "this"', () => {
+ const a = new Vector(1, 3, 3, 0);
+ expect(a).to.respondTo('div');
+
+ const c: Vector = a.div(4);
+ expect(a.x).to.equal(1);
+ expect(a.y).to.equal(3);
+ expect(a.z).to.equal(3);
+ expect(a.w).to.equal(0);
+ });
+
+ it('cross product returns the correct result', () => {
+
+ const a = new Vector(1, 3, 3, 0);
+ const b = new Vector(2, -2, 0, 0);
+ expect(a).to.respondTo('cross');
+ const c = a.cross(b);
+ expect(c.x).to.equal(6);
+ expect(c.y).to.equal(6);
+ expect(c.z).to.equal(-8);
+ expect(c.w).to.equal(0);
+ });
+
+ it('cross product does not change the value of "this"', () => {
+
+ const a = new Vector(1, 3, 3, 0);
+ const b = new Vector(2, -2, 0, 0);
+ expect(a).to.respondTo('cross');
+ const c = a.cross(b);
+ expect(a.x).to.equal(1);
+ expect(a.y).to.equal(3);
+ expect(a.z).to.equal(3);
+ expect(a.w).to.equal(0);
+ });
+
+ it('dot product is correct', () => {
+
+ const a = new Vector(1, 3, 3, 0);
+ const b = new Vector(2, 2, 1, 0);
+ const c = new Vector(2, -2, 0, 0);
+ expect(a).to.respondTo('dot');
+ const d = a.dot(b);
+ const e = b.dot(c);
+ expect(d).to.equal(11);
+ expect(e).to.equal(0);
+ });
+
+ it('equality of different vectors returns false', () => {
+
+ const a = new Vector(1, 3, 3, 1);
+ const b = new Vector(2, -2, 0, 1);
+ expect(a).to.respondTo('equals');
+
+ expect(a.equals(b)).to.equal(false);
+ });
+
+ it('equality of equal vectors returns true', () => {
+
+ const a = new Vector(1, 3, 3, 1);
+ expect(a).to.respondTo('equals');
+
+ expect(a.equals(a)).to.equal(true);
+ });
+
+ it('vectors very close to each other are equal', () => {
+
+ const a = new Vector(1, 3, 3, 1);
+ const b = new Vector(1.0000000000001, 3.000000000001, 2.9999999999999, 1);
+ expect(a).to.respondTo('equals');
+
+ expect(a.equals(b)).to.equal(true);
+ });
+});