diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9a5620e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "mochaExplorer.require": "ts-node/register", + "mochaExplorer.files": "test/*.ts", +} \ No newline at end of file diff --git a/dist/manyspheres-finished.png b/dist/manyspheres-finished.png new file mode 100644 index 0000000..d6a2bcf Binary files /dev/null and b/dist/manyspheres-finished.png differ diff --git a/dist/manyspheres.html b/dist/manyspheres.html new file mode 100644 index 0000000..6b0ffbf --- /dev/null +++ b/dist/manyspheres.html @@ -0,0 +1,39 @@ + + + + + + + Graphische Datenverarbeitung - Raytracing - Many Spheres + + + + + + +
+
+
+
+ +
+ Implement a Raytracer by sending a ray into the scene for every pixel. Color the pixel with the color of the nearest sphere if the ray hits. +
+
+
+ +
Reference image
+
+
+
+ + + diff --git a/dist/phong-finished.png b/dist/phong-finished.png new file mode 100644 index 0000000..e88d4d8 Binary files /dev/null and b/dist/phong-finished.png differ diff --git a/dist/phong.html b/dist/phong.html new file mode 100644 index 0000000..3ad11a0 --- /dev/null +++ b/dist/phong.html @@ -0,0 +1,43 @@ + + + + + + + Graphische Datenverarbeitung - Raytracing - Phong + + + + + + +
+
+
+
+ +
+ Implement a Raytracer that uses the Phong Lighting model. +
+ + +
+
+
+
+ +
Reference image
+
+
+
+ + + \ 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 + + + + + + +
+
+
+
+ +
+ Implement a Raytracer by sending a ray into the scene for every pixel. Color the pixel black if the ray hits the given sphere. +
+
+
+ +
Reference image
+
+
+
+ + + 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); + }); +});