+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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