diff --git a/dist/matrix-finished.png b/dist/matrix-finished.png new file mode 100644 index 0000000..029dd8c Binary files /dev/null and b/dist/matrix-finished.png differ diff --git a/dist/matrix.html b/dist/matrix.html new file mode 100644 index 0000000..5fbd8fa --- /dev/null +++ b/dist/matrix.html @@ -0,0 +1,63 @@ + + + + + + + Graphische Datenverarbeitung - Math - Matrix + + + + + + +
+
+
+
+ +
+ Implement the Matrix class. You may use the sliders to transform the sphere's midpoint +
+
+
+
+ + + +
+
+ + + +

+ Translation is done before rotation. Therefore the translate direction might be different from + what you expect. +

+
+
+ + + +

+ We only scale the sphere midpoint positions. The slider is from 1 to 2. The camera is in the + origin. Scaling to 2 will move the spheres further away from the camera. +

+
+
+
+
+ + + \ No newline at end of file diff --git a/dist/scenegraph-finished.png b/dist/scenegraph-finished.png new file mode 100644 index 0000000..d6bbe00 Binary files /dev/null and b/dist/scenegraph-finished.png differ diff --git a/dist/scenegraph.html b/dist/scenegraph.html new file mode 100644 index 0000000..fe21bd9 --- /dev/null +++ b/dist/scenegraph.html @@ -0,0 +1,41 @@ + + + + + + + Graphische Datenverarbeitung - Raytracing - Scenegraph + + + + + + +
+
+
+
+ +
+

Implement a Raytracer using a Scenegraph.

+ + +
+
+
+ +
Reference image
+
+
+
+ + + diff --git a/src/07/matrix.ts b/src/07/matrix.ts new file mode 100644 index 0000000..2ada48a --- /dev/null +++ b/src/07/matrix.ts @@ -0,0 +1,194 @@ +import Vector from '../05/vector'; + +/** + * Class representing a 4x4 Matrix + */ +export default class Matrix { + + /** + * Data representing the matrix values + */ + data: Float32Array; + + /** + * Constructor of the matrix. Expects an array in row-major layout. Saves the data as column major internally. + * @param mat Matrix values row major + */ + constructor(mat: Array) { + this.data = new Float32Array(16); + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + this.data[row * 4 + col] = mat[col * 4 + row]; + } + } + } + + /** + * Returns the values of the matrix in row-major layout. + * @return The values of the matrix + */ + getVals(): Array { + + var mat = new Array(16); + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + mat[row * 4 + col] = this.data[col * 4 + row]; + } + } + return mat; + } + + /** + * Returns the value of the matrix at position row, col + * @param row The value's row + * @param col The value's column + * @return The requested value + */ + getVal(row: number, col: number): number { + return this.data[col * 4 + row]; + } + + /** + * Sets the value of the matrix at position row, col + * @param row The value's row + * @param val The value to set to + * @param col The value's column + */ + setVal(row: number, col: number, val: number) { + this.data[col * 4 + row] = val; + } + + /** + * Returns a matrix that represents a translation + * @param translation The translation vector that shall be expressed by the matrix + * @return The resulting translation matrix + */ + static translation(translation: Vector): Matrix { + + // TODO: Return a new Matrix that is translated according to the given Vector + return Matrix.identity(); + } + + /** + * Returns a matrix that represents a rotation. The rotation axis is either the x, y or z axis (either x, y, z is 1). + * @param axis The axis to rotate around + * @param angle The angle to rotate + * @return The resulting rotation matrix + */ + static rotation(axis: Vector, angle: number): Matrix { + + // TODO: Return a new rotation matrix, distinguish the different + // TODO: axis of rotation. You can use the axis vector + // TODO: (1, 0, 0, 0) to specify the x-axis + // TODO: (0, 1, 0, 0) to specify the y-axis + // TODO: (0, 0, 1, 0) to specify the z-axis + return Matrix.identity(); + } + + /** + * Returns a matrix that represents a scaling + * @param scale The amount to scale in each direction + * @return The resulting scaling matrix + */ + static scaling(scale: Vector): Matrix { + // TODO: Return a new scaling Matrix with the scaling components of the Vector scale + return Matrix.identity(); + } + + + /** + * Returns the identity matrix + * @return A new identity matrix + */ + static identity(): Matrix { + return new Matrix([ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]); + } + + /** + * Matrix multiplication + * @param other The matrix to multiply with + * @return The result of the multiplication this*other + */ + mul(other: Matrix): Matrix { + // TODO: Return a new Matrix mat with mat = this * other + return Matrix.identity(); + } + + /** + * Matrix-vector multiplication + * @param other The vector to multiply with + * @return The result of the multiplication this * other + */ + mulVec(other: Vector): Vector { + // TODO: Return a new Vector vec with vec = this * other + return new Vector(0, 0, 0, 0); + } + + /** + * Returns the transpose of this matrix + * @return A new matrix that is the transposed of this + */ + transpose(): Matrix { + // TODO: Return a new matrix that is the transposed of this + return Matrix.identity(); + } + + /** + * Constructs a lookat matrix + * @param eye The position of the viewer + * @param center The position to look at + * @param up The up direction + * @return The resulting lookat matrix + */ + static lookat(eye: Vector, center: Vector, up: Vector): Matrix { + // TODO: Return a new lookat Matrix + return Matrix.identity(); + } + + /** + * Constructs a new matrix that represents a projection normalisation transformation + * @param left Camera-space left value of lower near point + * @param right Camera-space right value of upper right far point + * @param bottom Camera-space bottom value of lower lower near point + * @param top Camera-space top value of upper right far point + * @param near Camera-space near value of lower lower near point + * @param far Camera-space far value of upper right far point + * @return The rotation matrix + */ + static frustum(left: number, right: number, bottom: number, top: number, near: number, far: number): Matrix { + // TODO: Return a new frustum Matrix + return Matrix.identity(); + } + + /** + * Constructs a new matrix that represents a projection normalisation transformation. + * @param fovy Field of view in y-direction + * @param aspect Aspect ratio between width and height + * @param near Camera-space distance to near plane + * @param far Camera-space distance to far plane + * @return The resulting matrix + */ + static perspective(fovy: number, aspect: number, near: number, far: number): Matrix { + // TODO: Return a new perspective Matrix, possibly reuse + // TODO: Matrix.frustum + return Matrix.identity(); + } + + /** + * Debug print to console + */ + print() { + for (let row = 0; row < 4; row++) { + console.log("> " + this.getVal(row, 0) + + "\t" + this.getVal(row, 1) + + "\t" + this.getVal(row, 2) + + "\t" + this.getVal(row, 3) + ); + } + } +} diff --git a/src/07/setup-matrix.ts b/src/07/setup-matrix.ts new file mode 100644 index 0000000..2152392 --- /dev/null +++ b/src/07/setup-matrix.ts @@ -0,0 +1,156 @@ +import 'bootstrap'; +import 'bootstrap/scss/bootstrap.scss'; +import Ray from '../05/ray'; +import { phong } from '../06/phong'; +import Sphere from '../05/sphere'; +import Vector from '../05/vector'; +import Matrix from './matrix'; + +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 lightPositions = [ + new Vector(1, 1, -1, 1) + ]; + + const shininess = 10; + + const camera = { + origin: new Vector(0, 0, 0, 1), + width: canvas.width, + height: canvas.height, + alpha: Math.PI / 3 + } + + function setPixel(x: number, y: number, color: Vector) { + data[4 * (canvas.width * y + x) + 0] = Math.min(255, color.r * 255); + data[4 * (canvas.width * y + x) + 1] = Math.min(255, color.g * 255); + data[4 * (canvas.width * y + x) + 2] = Math.min(255, color.b * 255); + data[4 * (canvas.width * y + x) + 3] = 255; + } + + let rotation = Matrix.identity(); + let translation = Matrix.identity(); + let scale = Matrix.identity(); + + function animate() { + + let matrix = Matrix.identity(); + + if (useRotationElement.checked) { + matrix = matrix.mul(rotation); + } + + if (useTranslationElement.checked) { + matrix = matrix.mul(translation); + } + + if (useScaleElement.checked) { + matrix = matrix.mul(scale); + } + + const blank = new Vector(1, 1, 1, 0); + + const sphere = new Sphere(matrix.mulVec(new Vector(0.1, 0, -1.5, 1)), 0.4, new Vector(.3, 0, 0, 1)); + for (let y = 0; y < canvas.height; y++) { + for (let x = 0; x < canvas.width; x++) { + + const ray = Ray.makeRay(x, y, camera); + const intersection = sphere.intersect(ray); + if (intersection) { + const color = phong(sphere.color, intersection, lightPositions, shininess, camera.origin); + setPixel(x, y, color); + + // 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); + } else { + setPixel(x, y, blank); + + // 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); + } + } + } + } + window.requestAnimationFrame(animate); + + const useRotationElement = + document.getElementById("userotation") as HTMLInputElement; + + useRotationElement.onchange = () => { + let range = document.getElementById("rotation") as HTMLInputElement; + if (useRotationElement.checked) { + range.value = "0"; + range.oninput = () => { + rotation = Matrix.rotation(new Vector(0, 0, 1, 0), + Number(range.value)); + window.requestAnimationFrame(animate); + } + range.disabled = false; + range.oninput(new Event("click")); + } else { + range.disabled = true; + rotation = Matrix.identity(); + } + window.requestAnimationFrame(animate); + } + + const useTranslationElement = document.getElementById("usetranslation") as HTMLInputElement; + useTranslationElement.onchange = () => { + let range = document.getElementById("translation") as HTMLInputElement; + if (useTranslationElement.checked) { + range.value = "0"; + range.oninput = () => { + translation = Matrix.translation(new Vector(Number(range.value), 0, 0, 0)); + window.requestAnimationFrame(animate); + } + range.disabled = false; + range.oninput(new Event("click")); + } else { + range.disabled = true; + translation = Matrix.identity(); + } + window.requestAnimationFrame(animate); + } + + const useScaleElement = document.getElementById("usescale") as HTMLInputElement; + useScaleElement.onchange = () => { + let range = document.getElementById("scale") as HTMLInputElement; + if (useScaleElement.checked) { + range.value = "1"; + range.oninput = () => { + scale = Matrix.scaling(new Vector( + Number(range.value), + Number(range.value), + Number(range.value), 0)); + window.requestAnimationFrame(animate); + } + range.disabled = false; + range.oninput(new Event("click")); + } else { + range.disabled = true; + scale = Matrix.identity(); + } + window.requestAnimationFrame(animate); + } + + const sliders = ["rotation", "translation", "scale"]; + for (let t of sliders) { + const elem = document.getElementById("use" + t) as HTMLInputElement; + if (elem.checked) { + elem.onchange(new Event("click")); + } + } +}); diff --git a/src/08/matrixcache.ts b/src/08/matrixcache.ts new file mode 100644 index 0000000..f9fbc9d --- /dev/null +++ b/src/08/matrixcache.ts @@ -0,0 +1,54 @@ +import Matrix from "../07/matrix" +import { MatrixTransformation } from "./transformation"; +import { Node } from "./nodes"; + +/** + * A map containing pairs. Used to cache + * the MatrixTransformation of a Node that has already been traversed. + * The map needs to be updated when Transformations are changed in the scenegraph. + */ +export class MatrixCache { + + static matrices = new Map(); + + /** + * Clear the cache + */ + static clear() { + + MatrixCache.matrices.clear(); + } + + /** + * Clear the cached transformation of a given node. + * @param node the node to clear + */ + static delete(node: Node) { + + MatrixCache.matrices.delete(node); + } + + /** + * Add a transformation of a given node to the cache. + * @param node the node to cache + * @param transform the transformation of the node to cache + */ + static add(node: Node, transform: MatrixTransformation) { + + MatrixCache.matrices.set(node, transform); + } + + /** + * Try to retrieve a cached transformation from the cache. + * @param node the node + * @returns the cached transformation, null if no transformation is cached for this node + */ + static get(node: Node): MatrixTransformation { + + if (MatrixCache.matrices.has(node)) { + return MatrixCache.matrices.get(node); + } + + return null; + } +} \ No newline at end of file diff --git a/src/08/nodes.ts b/src/08/nodes.ts new file mode 100644 index 0000000..779ea55 --- /dev/null +++ b/src/08/nodes.ts @@ -0,0 +1,86 @@ +import Vector from '../05/vector'; +import Sphere from '../05/sphere'; +import Ray from '../05/ray'; +import Intersection from '../05/intersection'; + +import { MatrixTransformation, Transformation } from './transformation'; + +/** + * Class representing a Node in a Scenegraph + * A Node holds a transformation. + */ +export class Node { + + public transform: Transformation = null; + + constructor(transform: Transformation) { + this.transform = transform; + } +} + +/** + * Class representing a GroupNode in the Scenegraph. + * A GroupNode holds a transformation and is able + * to have child nodes attached to it. + * @extends Node + */ +export class GroupNode extends Node { + + /** + * The children of the group node + */ + children: Array; + + /** + * Constructor + * @param transform The node's transformation + */ + constructor(transform: Transformation) { + + super(transform); + this.children = []; + } + + /** + * Adds a child node + * @param childNode The child node to add + */ + add(childNode: Node) { + // TODO: Add the childNode to the list of children + } +} + +/** + * Class representing a Sphere in the Scenegraph + * @extends Node + */ +export class SphereNode extends Node { + + public static unit_sphere: Sphere = new Sphere(new Vector(0.0, 0.0, 0.0, 1.0), 1.0, new Vector(0.0, 0.0, 0.0, 0.0)); + + public color: Vector = null; + + /** + * Creates a new Sphere. + * The sphere is defined around the origin + * with radius 1. + * @param color The color of the Sphere + */ + constructor(transform: Transformation, color: Vector) { + + super(transform); + this.color = color; + } + + /** + * Calculate the intersection of the ray with unit_sphere + * @param ray The ray to intersect with + * @returns An Intersection or null if there is no intersection + */ + public intersect(ray: Ray): Intersection { + + // TODO: Intersect this ray with the unit_sphere and return the Intersection + // TODO: Reuse Sphere.intersect that you have already implemented + return null; + } +} diff --git a/src/08/raytracing.ts b/src/08/raytracing.ts new file mode 100644 index 0000000..7b143a1 --- /dev/null +++ b/src/08/raytracing.ts @@ -0,0 +1,52 @@ +import Camera from '../05/camera'; +import Intersection from '../05/intersection'; +import Vector from '../05/vector'; +import Ray from '../05/ray'; +import { phong } from '../06/phong'; +import { Traversal } from './traversal'; +import { Node, SphereNode } from './nodes'; + +/** + * Compute the color of the pixel (x, y) by raytracing + * using a given camera and a given scenegraph. + * + * @param data The linearised pixel array + * @param camera The camera used for raytracing + * @param scenegraph The root node of the scene to raytrace + * @param lightPositions The light positions + * @param shininess The shininess parameter of the Phong model + * @param width The width of the canvas + * @param height The height of the canvas + */ +export function raytracePhong(data: Uint8ClampedArray, + camera: Camera, + scenegraph: Node, + lightPositions: Array, + shininess: number, + x: number, y: number, + width: number, height: number) { + + let index = (x + y * width) * 4; + + // Create ray from camera through image plane at position (x, y). + const ray = Ray.makeRay(x, y, camera); + + let intersections = new Array(); + let intersectionObjects = new Array(); + + // Compute all the intersections by traversing the scenegraph + // using Traversal.traverse. + + Traversal.traverse(scenegraph, ray, scenegraph.transform, intersections, intersectionObjects); + + // TODO: Find the closest intersection from the intersections array. + // TODO: Compute emission at point of intersection using phong model. + // TODO: Set pixel color accordingly. + // If there are no intersections, set pixel color to white + if (intersections.length == 0) { + data[index + 0] = 255; + data[index + 1] = 255; + data[index + 2] = 255; + data[index + 3] = 255; + } +} diff --git a/src/08/setup-scenegraph.ts b/src/08/setup-scenegraph.ts new file mode 100644 index 0000000..c3e9a73 --- /dev/null +++ b/src/08/setup-scenegraph.ts @@ -0,0 +1,106 @@ +import 'bootstrap'; +import 'bootstrap/scss/bootstrap.scss'; + +import Vector from '../05/vector'; +import { GroupNode, SphereNode } from './nodes'; +import { Rotation, Scaling, Translation } from './transformation'; +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); + let imageData: ImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + const lightPositions = [ + new Vector(0, 0, 0, 1) + ]; + + const camera = { + origin: new Vector(0, 0, 0, 1), + width: canvas.width, + height: canvas.height, + alpha: Math.PI / 3 + } + + const root = new GroupNode(new Translation(new Vector(0, 0, -5, 0))); + let moonRotation = new Rotation(new Vector(0.0, 1.0, 0.0, 0.0), 0.0); + + // TODO: Create a SceneGraph looking like this: + // TODO: + // TODO: o root GroupNode with Translation (0, 0, -5) + // TODO: / \ + // TODO: / \ + // TODO: SphereNode o o GroupNode with moonRotation + // TODO: \ + // TODO: o GroupNode with Translation (2, 0, 0) + // TODO: \ + // TODO: o GroupNode with Scaling (0.2, 0.2, 0.2) + // TODO: \ + // TODO: o SphereNode + // TODO: + + let animationHandle: number; + + let lastTimestamp = 0; + let animationTime = 0; + let animationHasStarted = true; + + function animate(timestamp: number) { + + console.log("animate"); + let deltaT = timestamp - lastTimestamp; + + if (animationHasStarted) { + deltaT = 0; + animationHasStarted = false; + } + + animationTime += deltaT; + lastTimestamp = timestamp; + + let data = imageData.data; + data.fill(0); + + const width = imageData.width; + const height = imageData.height; + + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + + raytracePhong(data, camera, root, lightPositions, 20, x, y, width, height); + + for (let i = 0; i < 4; i ++) + pixel.data[i] = data[(x + y * canvas.width) * 4 + i]; + ctx.putImageData(pixel, x, y); + } + } + + moonRotation.angle = -animationTime / 5000; + } + + function startAnimation() { + if (animationHandle) { + window.cancelAnimationFrame(animationHandle); + } + + animationHasStarted = true; + + function animation(t: number) { + animate(t); + animationHandle = window.requestAnimationFrame(animation); + } + + animationHandle = window.requestAnimationFrame(animation); + } + animate(0); + + document.getElementById("startAnimationBtn").addEventListener( + "click", startAnimation); + document.getElementById("stopAnimationBtn").addEventListener( + "click", () => cancelAnimationFrame(animationHandle)); +}); diff --git a/src/08/transformation.ts b/src/08/transformation.ts new file mode 100644 index 0000000..f4c6bdc --- /dev/null +++ b/src/08/transformation.ts @@ -0,0 +1,100 @@ +import Matrix from "../07/matrix"; +import Vector from "../05/vector"; + +export interface Transformation { + getMatrix(): Matrix; + getInverseMatrix(): Matrix; +} + +// TODO: constructors do not compile without super call. + +/** + * The MatrixTransformation class holds a transformation as well as its + * inverse using matrices. + */ +export class MatrixTransformation implements Transformation { + matrix: Matrix; + inverse: Matrix; + + constructor(matrix: Matrix, inverse: Matrix) { + this.matrix = matrix; + this.inverse = inverse; + } + + getMatrix(): Matrix { + return this.matrix; + } + + getInverseMatrix(): Matrix { + return this.inverse; + } +} + +/** + * Translation holds a matrix for the translation, + * and a matrix for the inverse translation. + */ +export class Translation extends MatrixTransformation { + + constructor(translation: Vector) { + + // TODO: Create 2 matrices, one for the translation and + // TODO: one for its inverse. + // TODO: Call the constructor of the super class with the two matrices. + // TODO: "super" has to be the first call in the constructor, so you have to put + // TODO: everything into a single line + super(null, null); + } +} + +/** + * Rotation holds a matrix for the rotation, + * and a matrix for the inverse rotation. + */ +export class Rotation extends MatrixTransformation { + + private _axis: Vector; + private _angle: number; + + constructor(axis: Vector, angle: number) { + + // TODO: Create 2 matrices, one for the rotation and + // TODO: one for its inverse. + // TODO: Call the constructor of the super class with the two matrices. + // TODO: "super" has to be the first call in the constructor, so you have to put + // TODO: everything into a single line. + // TODO: Store the axis and angle for later recalculation when the angle is changed. + super(null, null); + } + + set axis(axis: Vector) { + this._axis = axis; + this.recalculate(); + } + + set angle(angle: number) { + this._angle = angle; + this.recalculate(); + } + + private recalculate() { + // TODO: Calculate a new rotation matrix and inverse + // TODO: from this._axis and this._angle + } +} + +/** + * Scaling holds a matrix for the scaling, + * and a matrix for the inverse scaling. + */ +export class Scaling extends MatrixTransformation { + + constructor(scale: Vector) { + // TODO: Create 2 matrices, one for the scaling and + // TODO: one for its inverse. + // TODO: Call the constructor of the super class with the two matrices. + // TODO: "super" has to be the first call in the constructor, so you have to put + // TODO: everything into a single line. + super(null, null); + } +} diff --git a/src/08/traversal.ts b/src/08/traversal.ts new file mode 100644 index 0000000..bc37d16 --- /dev/null +++ b/src/08/traversal.ts @@ -0,0 +1,54 @@ +import Camera from "../05/camera"; +import Ray from "../05/ray"; +import Intersection from "../05/intersection"; + +import { Node, GroupNode, SphereNode } from "./nodes"; +import { Transformation, MatrixTransformation } from "./transformation"; + +export class Traversal { + + /** + * Traverse through the scenegraph while intersecting all the SphereNodes + * in the graph. + * If an intersection between ray and a SphereNode occurs, add the + * intersection to the Array of Intersections, and add the SphereNode to the + * Array of Nodes. + * + * @param node The node in the scenegraph to traverse + * @param ray The current ray with which to raytrace + * @param transformation The current world transformation during traversal + * @param intersections An Array of intersections that needs to be filled + * @param intersectionObjects An Array of intersected Nodes that needs to be filled + */ + public static traverse(node: Node, ray: Ray, transformation: Transformation, + intersections: Array, intersectionObjects: Array) { + + if (node instanceof GroupNode) { + + // TODO: Recurse through the list of child nodes: + // TODO: Calculate a new world matrix = + // TODO: current transformation Matrix * the child transformation matrix + // TODO: And the inverse world matrix = + // TODO: child inverse * current inverse transformation Matrix + } + + if (node instanceof SphereNode) { + + // TODO: Calculate a new Ray for intersection testing. + // TODO: If you passed the correct matrices during traversal of + // TODO: the GroupNodes, "transformation" is currently + // TODO: in world coordinates. + // TODO: 1. Transform the ray's origin and direction vector to + // TODO: the object coordinate system by multiplying with + // TODO: the inverse transformation matrix. + // TODO: 2. Perform the intersection by reusing sphere.intersect + // TODO: 3. If there is an intersection, transform the resulting + // TODO: intersection point and intersection normal back to + // TODO: world coordinates, by multiplying with the current + // TODO: transformation matrix. Re-calculate "t" from the + // TODO: transformed intersection point in world coordinates. + // TODO: 4. Push the intersection and intersection object to + // TODO: "intersections" and "intersectionObjects", respectively + } + } +} diff --git a/test/matrix-spec.ts b/test/matrix-spec.ts new file mode 100644 index 0000000..cc79c63 --- /dev/null +++ b/test/matrix-spec.ts @@ -0,0 +1,190 @@ +import Vector from "../src/05/vector"; +import Matrix from "../src/07/matrix"; + +import { assert, expect } from 'chai'; +import { SingleEntryPlugin } from "webpack"; + +describe('Matrix', () => { + + it('can be initialized with an array consisting of 16 numbers', () => { + const m = new Matrix([ + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 1 + ]); + expect(m).to.be.an('object'); + }); + + it('single values can be set and retrieved', () => { + const m = Matrix.identity(); + + expect(m).has.property('setVal'); + expect(m).has.property('getVal'); + m.setVal(1, 2, 342); + expect(m.getVal(1, 2)).to.equal(342); + }); + + it('all values can be retrieved', () => { + + const m = new Matrix([ + 0, 1, 2, 3, + 4, 5, 6, 7, + 8, 9, 10, 11, + 12, 13, 14, 15 + ]); + + expect(m).has.property('getVals'); + var mat = m.getVals(); + assert.deepEqual(mat, [ + 0, 1, 2, 3, + 4, 5, 6, 7, + 8, 9, 10, 11, + 12, 13, 14, 15 + ]); + }); + + it('translation works', () => { + + expect(Matrix).has.property('translation'); + + const t = Matrix.translation(new Vector(1, 2, 3, 0)); + + expect(t).to.not.be.null; + expect(t).to.be.an('object'); + + assert.deepEqual(t.getVals(), [ + 1, 0, 0, 1, + 0, 1, 0, 2, + 0, 0, 1, 3, + 0, 0, 0, 1 + ]); + }); + + it('rotation around x-axis works', () => { + + expect(Matrix).has.property('rotation'); + + const x = Matrix.rotation(new Vector(1, 0, 0, 0), Math.PI / 4); + + expect(x).to.not.be.null; + expect(x).to.be.an('object'); + assert.deepEqual(x.getVals(), [ + 1, 0, 0, 0, + 0, 0.7071067690849304, -0.7071067690849304, 0, + 0, 0.7071067690849304, 0.7071067690849304, 0, + 0, 0, 0, 1 + ]); + }); + + it('rotation around y-axis works', () => { + + expect(Matrix).has.property('rotation'); + + const y = Matrix.rotation(new Vector(0, 1, 0, 0), -Math.PI / 4); + expect(y).to.not.be.null; + expect(y).to.be.an('object'); + assert.deepEqual(y.getVals(), [ + 0.7071067690849304, 0, -0.7071067690849304, 0, + 0, 1, 0, 0, + 0.7071067690849304, 0, 0.7071067690849304, 0, + 0, 0, 0, 1 + ]); + }); + + it('rotation around z-axis works', () => { + + expect(Matrix).has.property('rotation'); + + const z = Matrix.rotation(new Vector(0, 0, 1, 0), -Math.PI / 4); + + expect(z).to.not.be.null; + expect(z).to.be.an('object'); + assert.deepEqual(z.getVals(), [ + 0.7071067690849304, 0.7071067690849304, 0, 0, + -0.7071067690849304, 0.7071067690849304, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]); + }); + + it('scaling matrix works', () => { + + expect(Matrix).has.property('scaling'); + + const m = Matrix.scaling(new Vector(2, 3, 4, 0)); + + expect(m).to.not.be.null; + expect(m).to.be.an('object'); + assert.deepEqual(m.getVals(), [ + 2, 0, 0, 0, + 0, 3, 0, 0, + 0, 0, 4, 0, + 0, 0, 0, 1 + ]); + }); + + it('matrix vector multiplication works', () => { + + let m1: Matrix = new Matrix([ + -1, 0, 0, 1, + 0, -1, 0, 2, + 0, 0, 1, 3, + 0, 0, 0, 1 ]); + + let a = new Vector(0, 1, 0, 1); + let b = new Vector(-1, 1, 0, 1); + + expect(m1).to.not.be.null; + expect(m1).to.be.an('object'); + + expect(m1).has.property('mulVec'); + + let at = m1.mulVec(a); + + assert.deepEqual(at.valueOf(), [ + 1, 1, 3, 1 + ]); + + let m2: Matrix = new Matrix([ + 1, 0, 0, 0, + 0, 0, -1, 0, + 0, 1, 0, 0, + 0, 0, 0, 1 ]); + + let bt = m2.mulVec(b); + + assert.deepEqual(bt.valueOf(), [ + -1, 0, 1, 1 + ]); + }); + + it('matrix matrix multiplication works', () => { + + let m1: Matrix = new Matrix([ + -1, 0, 0, 1, + 0, -1, 0, 2, + 0, 0, 1, 3, + 0, 0, 0, 1 ]); + + expect(m1).to.not.be.null; + expect(m1).to.be.an('object'); + + expect(m1).has.property('mul'); + + let m2: Matrix = new Matrix([ + 1, 0, 0, 0, + 0, 0, -1, 0, + 0, 1, 0, 0, + 0, 0, 0, 1 ]); + + let m = m1.mul(m2); + + assert.deepEqual(m.getVals(), [ + -1, 0, 0, 1, + 0, 0, 1, 2, + 0, 1, 0, 3, + 0, 0, 0, 1 + ]); + }); +}); \ No newline at end of file diff --git a/test/nodes-spec.ts b/test/nodes-spec.ts new file mode 100644 index 0000000..6ea849d --- /dev/null +++ b/test/nodes-spec.ts @@ -0,0 +1,52 @@ +import { assert, expect } from 'chai'; +import Intersection from '../src/05/intersection'; +import Ray from '../src/05/ray'; + +import Vector from "../src/05/vector"; +import Matrix from '../src/07/matrix'; + +import { GroupNode, SphereNode } from '../src/08/nodes'; +import { Translation, Rotation, Scaling, MatrixTransformation, Transformation } from "../src/08/transformation"; + +describe('GroupNode', () => { + + it('can be correctly initialized with a Transformation', () => { + + let t: Transformation = new MatrixTransformation(Matrix.identity(), Matrix.identity()); + let g: GroupNode = new GroupNode(t); + expect(g).to.be.an('object'); + }); + + it('can have children', () => { + + let t: Transformation = new MatrixTransformation(Matrix.identity(), Matrix.identity()); + let g: GroupNode = new GroupNode(t); + let c: GroupNode = new GroupNode(t); + + expect(g).to.be.an('object'); + expect(c).to.be.an('object'); + + g.add(c); + + expect(g.children).to.be.an('Array'); + expect(g.children.length).to.equal(1); + }); +}); + +describe('SphereNode', () => { + it('can be intersected in Origin', () => { + + let t: Transformation = new MatrixTransformation(Matrix.identity(), Matrix.identity()); + let s: SphereNode = new SphereNode(t, new Vector(0.0, 0.0, 0.0, 1.0)); + + expect(s).to.be.an('object'); + + let r: Ray = new Ray(new Vector(0.0, 0.0, 10, 1.0), new Vector(0.0, 0.0, -1.0, 0.0)); + let i: Intersection = s.intersect(r); + + assert.deepEqual(i.point.data, [ + 0, 0, 1, 1, + ]); + }); +}); + diff --git a/test/transformation-spec.ts b/test/transformation-spec.ts new file mode 100644 index 0000000..621a38c --- /dev/null +++ b/test/transformation-spec.ts @@ -0,0 +1,69 @@ +import { assert, expect } from 'chai'; + +import { Translation, Rotation, Scaling } from "../src/08/transformation"; +import Vector from "../src/05/vector"; + +describe('Translation', () => { + + it('can be correctly initialized with a Vector', () => { + + const t: Translation = new Translation(new Vector(1.0, 2.0, 3.0, 0.0)); + expect(t).to.be.an('object'); + assert.deepEqual(t.getMatrix().getVals(), [ + 1, 0, 0, 1, + 0, 1, 0, 2, + 0, 0, 1, 3, + 0, 0, 0, 1 + ]); + assert.deepEqual(t.getInverseMatrix().getVals(), [ + 1, 0, 0, -1, + 0, 1, 0, -2, + 0, 0, 1, -3, + 0, 0, 0, 1 + ]); + }); +}); + +describe('Rotation', () => { + + it('can be correctly initialized with an axis and an angle', () => { + + const r: Rotation = new Rotation(new Vector(0.0, 0.0, 1.0, 0.0), Math.PI / 4); + console.log(r.matrix); + expect(r).to.be.an('object'); + assert.deepEqual(r.getMatrix().getVals(), [ + 0.7071067690849304, -0.7071067690849304, 0, 0, + 0.7071067690849304, 0.7071067690849304, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]); + assert.deepEqual(r.getInverseMatrix().getVals(), [ + 0.7071067690849304, 0.7071067690849304, 0, 0, + -0.7071067690849304, 0.7071067690849304, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]); + + }); +}); + +describe('Scaling', () => { + + it('can be correctly initialized with a Vector', () => { + + const s: Scaling = new Scaling(new Vector(1.0, 2.0, 4.0, 0.0)); + expect(s).to.be.an('object'); + assert.deepEqual(s.getMatrix().getVals(), [ + 1, 0, 0, 0, + 0, 2, 0, 0, + 0, 0, 4, 0, + 0, 0, 0, 1 + ]); + assert.deepEqual(s.getInverseMatrix().getVals(), [ + 1, 0, 0, 0, + 0, 1/2, 0, 0, + 0, 0, 1/4, 0, + 0, 0, 0, 1 + ]); + }); +}); diff --git a/test/traversal-spec.ts b/test/traversal-spec.ts new file mode 100644 index 0000000..d0049e5 --- /dev/null +++ b/test/traversal-spec.ts @@ -0,0 +1,70 @@ +import Ray from "../src/05/ray"; +import Camera from "../src/05/camera"; + +import { assert, expect } from 'chai'; +import { GroupNode, SphereNode } from "../src/08/nodes"; +import { Translation, Rotation } from "../src/08/transformation"; +import Vector from "../src/05/vector"; +import Sphere from "../src/05/sphere"; +import Intersection from "../src/05/intersection"; +import { Traversal } from "../src/08/traversal"; + +describe('Traversal', () => { + + it('Moved & rotated SphereNode can be intersected correctly', () => { + let t: GroupNode = new GroupNode(new Translation(new Vector(0.0, 0.0, -5.0, 0.0))); + let r: GroupNode = new GroupNode(new Rotation(new Vector(0.0, 1.0, 0.0, 0.0), Math.PI / 180 * 90)); + t.add(r); + + let s: SphereNode = new SphereNode(new Translation(new Vector(0.0, 0.0, 0.0, 0.0)), new Vector(0.0, 0.0, 0.0, 0.0)); + r.add(s); + + let ray: Ray = new Ray(new Vector(0.0, 0.0, 0.0, 1.0), new Vector(0.0, 0.0, -1.0, 0.0)); + + let intersection = new Array(); + let intersectionObjects = new Array(); + Traversal.traverse(t, ray, t.transform, intersection, intersectionObjects); + + // rotation should not move sphere + expect(intersection.length).to.equal(1); + expect(intersection[0].point.x).to.be.closeTo(0, 0.00001); + expect(intersection[0].point.y).to.be.closeTo(0, 0.00001); + expect(intersection[0].point.z).to.be.closeTo(-4, 0.00001); + expect(intersection[0].point.w).to.be.closeTo(1, 0.00001); + + r.children.pop(); + let s1: SphereNode = new SphereNode(new Translation(new Vector(1.0, 0.0, 0.0, 0.0)), new Vector(0.0, 0.0, 0.0, 0.0)); + r.add(s1); + intersection = new Array(); + intersectionObjects = new Array(); + Traversal.traverse(t, ray, t.transform, intersection, intersectionObjects); + + // rotating 90° around y and translating in x direction should move in z direction + expect(intersection.length).to.equal(1); + expect(intersection[0].point.x).to.be.closeTo(0, 0.00001); + expect(intersection[0].point.y).to.be.closeTo(0, 0.00001); + expect(intersection[0].point.z).to.be.closeTo(-5, 0.00001); + expect(intersection[0].point.w).to.be.closeTo(1, 0.00001); + + t.children.pop(); + r = new GroupNode(new Rotation(new Vector(0.0, 0.0, 1.0, 0.0), Math.PI / 180 * 90)); + t.add(r); + + let s2 = new SphereNode(new Translation(new Vector(0.999999999999, 0.0, 0.0, 0.0)), new Vector(0.0, 0.0, 0.0, 0.0)); + r.add(s2); + intersection = new Array(); + intersectionObjects = new Array(); + Traversal.traverse(t, ray, t.transform, intersection, intersectionObjects); + + // rotating 90° around z and translating in x direction should move in y direction + expect(intersection.length).to.equal(1); + expect(intersection[0].point.x).to.be.closeTo(0, 0.00001); + expect(intersection[0].point.y).to.be.closeTo(0, 0.00001); + expect(intersection[0].point.z).to.be.closeTo(-5, 0.00001); + expect(intersection[0].point.w).to.be.closeTo(1, 0.00001); + expect(intersection[0].normal.x).to.be.closeTo(0, 0.00001); + expect(intersection[0].normal.y).to.be.closeTo(-1, 0.00001); + expect(intersection[0].normal.z).to.be.closeTo(0, 0.00001); + expect(intersection[0].normal.w).to.be.closeTo(0, 0.00001); + }); +}); \ No newline at end of file