Browse Source

added exercises 07 & 08

master
fdai7303 11 months ago
parent
commit
272f442bb1
  1. BIN
      dist/matrix-finished.png
  2. 63
      dist/matrix.html
  3. BIN
      dist/scenegraph-finished.png
  4. 41
      dist/scenegraph.html
  5. 194
      src/07/matrix.ts
  6. 156
      src/07/setup-matrix.ts
  7. 54
      src/08/matrixcache.ts
  8. 86
      src/08/nodes.ts
  9. 52
      src/08/raytracing.ts
  10. 106
      src/08/setup-scenegraph.ts
  11. 100
      src/08/transformation.ts
  12. 54
      src/08/traversal.ts
  13. 190
      test/matrix-spec.ts
  14. 52
      test/nodes-spec.ts
  15. 69
      test/transformation-spec.ts
  16. 70
      test/traversal-spec.ts

BIN
dist/matrix-finished.png

After

Width: 500  |  Height: 500  |  Size: 18 KiB

63
dist/matrix.html

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Graphische Datenverarbeitung - Math - Matrix</title>
<script type="text/javascript" src="matrix.bundle.js"></script>
<link rel="icon" type="image/png" href="ai-logo.png">
</head>
<body>
<nav class="navbar navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="/index.html">
<img src="ai-logo.png" width="30" height="30" class="d-inline-block align-top" alt="">
Graphische Datenverarbeitung
</a>
<div class="navbar-brand">Matrix</div>
</div>
</nav>
<br>
<div class="container">
<div class="row">
<figure class="figure col-sm-8">
<canvas id="result" class="figure-img mx-auto d-block" width="500" height="500"></canvas>
<figcaption class="figure-caption text-center">
Implement the Matrix class. You may use the sliders to transform the sphere's midpoint
</figcaption>
</figure>
<div class="col-sm-4">
<div class="form-group text-left col-sm-12">
<input class="form-check-input" type="checkbox" id="userotation"></input>
<label class="form-check-label" for="rotation">Rotation</label>
<input class="form-control-range" type="range" min="0" max="6.28318" step="0.01" id="rotation"
disabled></input>
</div>
<div class="form-group text-left col-sm-12">
<input class="form-check-input" type="checkbox" id="usetranslation"></input>
<label class="form-check-label" for="translation">Translation X</label>
<input class="form-control-range" type="range" min="-1" max="1" step="0.02" id="translation"
disabled></input>
<p>
Translation is done before rotation. Therefore the translate direction might be different from
what you expect.
</p>
</div>
<div class="form-group text-left col-sm-12">
<input class="form-check-input" type="checkbox" id="usescale"></input>
<label class="form-check-label" for="scale">Scale</label>
<input class="form-control-range" type="range" min="1" max="2" step="0.01" id="scale"
disabled></input>
<p>
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.
</p>
</div>
</div>
</div>
</div>
</body>
</html>

BIN
dist/scenegraph-finished.png

After

Width: 500  |  Height: 500  |  Size: 22 KiB

41
dist/scenegraph.html

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Graphische Datenverarbeitung - Raytracing - Scenegraph</title>
<script type="text/javascript" src="scenegraph.bundle.js"></script>
<link rel="icon" type="image/png" href="ai-logo.png">
</head>
<body>
<nav class="navbar navbar-light bg-light">
<div class="container">
<a class="navbar-brand" href="/index.html">
<img src="ai-logo.png" width="30" height="30" class="d-inline-block align-top" alt="">
Graphische Datenverarbeitung
</a>
<div class="navbar-brand">Raytracing</div>
</div>
</nav>
<br>
<div class="container">
<div class="row">
<figure class="figure col-sm-6">
<canvas id="result" class="figure-img mx-auto d-block" width="500" height="500"></canvas>
<figcaption class="figure-caption text-center">
<p>Implement a Raytracer using a Scenegraph.</p>
<button id="startAnimationBtn" class="btn btn-dark">Start</button>
<button id="stopAnimationBtn" class="btn btn-dark">Stop</button>
</figcaption>
</figure>
<figure class="col-sm-6 figure">
<img class="figure-img mx-auto d-block" src="scenegraph-finished.png">
<figcaption class="figure-caption text-center">Reference image</figcaption>
</figure>
</div>
</div>
</body>
</html>

194
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<number>) {
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<number> {
var mat = new Array<number>(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)
);
}
}
}

156
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"));
}
}
});

54
src/08/matrixcache.ts

@ -0,0 +1,54 @@
import Matrix from "../07/matrix"
import { MatrixTransformation } from "./transformation";
import { Node } from "./nodes";
/**
* A map containing <Node, MatrixTransformation> 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<Node, MatrixTransformation>();
/**
* 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;
}
}

86
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<Node>;
/**
* 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;
}
}

52
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<Vector>,
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<Intersection>();
let intersectionObjects = new Array<SphereNode>();
// 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;
}
}

106
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));
});

100
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);
}
}

54
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<Intersection>, intersectionObjects: Array<SphereNode>) {
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
}
}
}

190
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
]);
});
});

52
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,
]);
});
});

69
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
]);
});
});

70
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<Intersection>();
let intersectionObjects = new Array<SphereNode>();
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<Intersection>();
intersectionObjects = new Array<SphereNode>();
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<Intersection>();
intersectionObjects = new Array<SphereNode>();
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);
});
});
Loading…
Cancel
Save