From 272f442bb1bd29e5258356b96db3a2ea51f9ff43 Mon Sep 17 00:00:00 2001 From: fdai7303 Date: Thu, 22 Jun 2023 08:08:03 +0200 Subject: [PATCH] added exercises 07 & 08 --- dist/matrix-finished.png | Bin 0 -> 18006 bytes dist/matrix.html | 63 ++++++++++++ dist/scenegraph-finished.png | Bin 0 -> 22814 bytes dist/scenegraph.html | 41 ++++++++ src/07/matrix.ts | 194 +++++++++++++++++++++++++++++++++++ src/07/setup-matrix.ts | 156 ++++++++++++++++++++++++++++ src/08/matrixcache.ts | 54 ++++++++++ src/08/nodes.ts | 86 ++++++++++++++++ src/08/raytracing.ts | 52 ++++++++++ src/08/setup-scenegraph.ts | 106 +++++++++++++++++++ src/08/transformation.ts | 100 ++++++++++++++++++ src/08/traversal.ts | 54 ++++++++++ test/matrix-spec.ts | 190 ++++++++++++++++++++++++++++++++++ test/nodes-spec.ts | 52 ++++++++++ test/transformation-spec.ts | 69 +++++++++++++ test/traversal-spec.ts | 70 +++++++++++++ 16 files changed, 1287 insertions(+) create mode 100644 dist/matrix-finished.png create mode 100644 dist/matrix.html create mode 100644 dist/scenegraph-finished.png create mode 100644 dist/scenegraph.html create mode 100644 src/07/matrix.ts create mode 100644 src/07/setup-matrix.ts create mode 100644 src/08/matrixcache.ts create mode 100644 src/08/nodes.ts create mode 100644 src/08/raytracing.ts create mode 100644 src/08/setup-scenegraph.ts create mode 100644 src/08/transformation.ts create mode 100644 src/08/traversal.ts create mode 100644 test/matrix-spec.ts create mode 100644 test/nodes-spec.ts create mode 100644 test/transformation-spec.ts create mode 100644 test/traversal-spec.ts diff --git a/dist/matrix-finished.png b/dist/matrix-finished.png new file mode 100644 index 0000000000000000000000000000000000000000..029dd8c94f00659557e5a0266b1b7c20c4ada71b GIT binary patch literal 18006 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 + + + + + + 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 0000000000000000000000000000000000000000..d6bbe005d36a148723974e21c8cf7ea59fbdf38e GIT binary patch literal 22814 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 literal 0 HcmV?d00001 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