fdai7303
1 year ago
16 changed files with 1287 additions and 0 deletions
-
BINdist/matrix-finished.png
-
63dist/matrix.html
-
BINdist/scenegraph-finished.png
-
41dist/scenegraph.html
-
194src/07/matrix.ts
-
156src/07/setup-matrix.ts
-
54src/08/matrixcache.ts
-
86src/08/nodes.ts
-
52src/08/raytracing.ts
-
106src/08/setup-scenegraph.ts
-
100src/08/transformation.ts
-
54src/08/traversal.ts
-
190test/matrix-spec.ts
-
52test/nodes-spec.ts
-
69test/transformation-spec.ts
-
70test/traversal-spec.ts
After Width: 500 | Height: 500 | Size: 18 KiB |
@ -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> |
After Width: 500 | Height: 500 | Size: 22 KiB |
@ -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> |
@ -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) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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")); |
||||
|
} |
||||
|
} |
||||
|
}); |
@ -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; |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -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)); |
||||
|
}); |
@ -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); |
||||
|
} |
||||
|
} |
@ -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
|
||||
|
} |
||||
|
} |
||||
|
} |
@ -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 |
||||
|
]); |
||||
|
}); |
||||
|
}); |
@ -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, |
||||
|
]); |
||||
|
}); |
||||
|
}); |
||||
|
|
@ -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 |
||||
|
]); |
||||
|
}); |
||||
|
}); |
@ -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); |
||||
|
}); |
||||
|
}); |
Write
Preview
Loading…
Cancel
Save
Reference in new issue