Last active
May 30, 2022 08:29
-
-
Save mmzsource/86065dea8201c490340ccc3043e2760f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
mdlr('mmzsource:raytrace', m => { | |
// SHARED | |
function isEqualNumber(n1, n2, epsilon=Number.EPSILON) { | |
return Math.abs(n1-n2) < epsilon; | |
} | |
function dot(r,c){ | |
console.assert(r.length === c.length); | |
let sum = 0; | |
for (let i = 0; i < r.length; i++) { | |
sum += r[i] * c[i]; | |
} | |
return sum; | |
} | |
// COLOR STUFF | |
function color(r,g,b) {return [r,g,b];} | |
function getRed(color){return color[0];} | |
function getGreen(color){return color[1];} | |
function getBlue(color){return color[2];} | |
function isEqualColor(c1, c2, epsilon=Number.EPSILON) { | |
return isEqualNumber(c1[0], c2[0], epsilon) && | |
isEqualNumber(c1[1], c2[1], epsilon) && | |
isEqualNumber(c1[2], c2[2], epsilon); | |
} | |
function cadd(c1, c2) { | |
return color(c1[0]+c2[0], c1[1]+c2[1], c1[2]+c2[2]); | |
} | |
function csub(c1, c2) { | |
return color(c1[0]-c2[0], c1[1]-c2[1], c1[2]-c2[2]); | |
} | |
function cmul([r,g,b], s) { | |
return color(r*s, g*s, b*s); | |
} | |
function cprod(c1, c2) { | |
return color(c1[0]*c2[0], c1[1]*c2[1], c1[2]*c2[2]); | |
} | |
// TUPLE STUFF | |
function tuple(x, y, z, w) { | |
return [[Number(x)], [Number(y)], [Number(z)], [Number(w)]]; | |
} | |
function getX([[x],[y],[z],[w]]){return x;} | |
function getY([[x],[y],[z],[w]]){return y;} | |
function getZ([[x],[y],[z],[w]]){return z;} | |
function getW([[x],[y],[z],[w]]){return w;} | |
function point(x,y,z) { | |
return tuple(x,y,z,1.0); | |
} | |
function vector(x,y,z){ | |
return tuple(x,y,z,0.0); | |
} | |
function isTuple([[x],[y],[z],[w]]) { | |
return typeof x === 'number' && | |
typeof y === 'number' && | |
typeof z === 'number' && | |
typeof w === 'number' && | |
(isEqualNumber(w, 0.0) || | |
isEqualNumber(w, 1.0)); | |
} | |
function isPoint(tuple) { | |
return isTuple(tuple) && isEqualNumber(tuple[3][0], 1.0); | |
} | |
function isVector(tuple) { | |
return isTuple(tuple) && isEqualNumber(tuple[3][0], 0.0); | |
} | |
function isEqualTuple(t1, t2) { | |
return isEqualNumber(t1[0][0], t2[0][0]) && | |
isEqualNumber(t1[1][0], t2[1][0]) && | |
isEqualNumber(t1[2][0], t2[2][0]) && | |
isEqualNumber(t1[3][0], t2[3][0]); | |
} | |
function add(t1, t2) { | |
return tuple((t1[0][0] + t2[0][0]), (t1[1][0] + t2[1][0]), (t1[2][0] + t2[2][0]), (t1[3][0] + t2[3][0])); | |
} | |
function sub(t1, t2) { | |
return tuple((t1[0][0] - t2[0][0]), (t1[1][0] - t2[1][0]), (t1[2][0] - t2[2][0]), (t1[3][0] - t2[3][0])); | |
} | |
function neg([[x],[y],[z],[w]]) { | |
return tuple(-x, -y, -z, -w); | |
} | |
function tmul([[x],[y],[z],[w]], s) { | |
return tuple(x*s, y*s, z*s, w*s); | |
} | |
function div(t, s) { | |
return tmul(t, 1/s); | |
} | |
function mag([[x],[y],[z],[w]]) { | |
return Math.sqrt(x*x + y*y + z*z + w*w); | |
} | |
function norm(v) { | |
const m = mag(v); | |
const [[x],[y],[z],[w]] = v; | |
return tuple(x/m, y/m, z/m, w/m); | |
} | |
function cross(v1, v2) { | |
return vector(v1[1][0]*v2[2][0] - v1[2][0]*v2[1][0], v1[2][0]*v2[0][0] - v1[0][0]*v2[2][0], v1[0][0]*v2[1][0] - v1[1][0]*v2[0][0]); | |
} | |
// MATRIX STUFF | |
function sameLength(a, b) { | |
return a.length === a.length; | |
} | |
function sameValues(m1, m2, epsilon){ | |
let equalValues = true; | |
outer: | |
for (let r = 0; r < m1.length; r++) { | |
for (let c = 0; c < m1[r].length; c++) { | |
if (!isEqualNumber(m1[r][c], m2[r][c], epsilon)) { | |
equalValues = false; | |
break outer; | |
} | |
} | |
} | |
return equalValues; | |
} | |
function isEqual2DMatrix(m1,m2,epsilon=Number.EPSILON) { | |
return sameLength(m1,m2) && sameLength(m1[0],m2[0]) && sameValues(m1,m2,epsilon); | |
} | |
function cols(m) { | |
const rl = m.length; | |
const cl = m[0].length; | |
const cs = []; //cols | |
for(let c = 0; c < cl; c++) { | |
let col = new Array(); | |
for (let r = 0; r < rl; r++) { | |
col.push(m[r][c]); | |
} | |
cs.push(col); | |
} | |
return cs; | |
} | |
// Creates matrix with r rows and c cols | |
function matrix(r,c){ | |
let m = []; | |
for (let i=0; i < r; i++) { | |
let cols = Array.from({length: c}); | |
m.push(cols); | |
} | |
return m; | |
} | |
function identityMatrixFor(m) { | |
let cols = m[0].length; | |
let im = matrix(cols, cols); | |
for (let r = 0; r < cols; r++) { | |
for (let c = 0; c < cols; c++) { | |
r === c ? im[r][c] = 1 : im[r][c] = 0; | |
} | |
} | |
return im; | |
} | |
function mmul(m1,m2) { | |
const rs = m1.length; | |
const cs = cols(m2); | |
let m = matrix(rs, cs.length); | |
for (let r = 0; r < m.length; r++) { | |
for (let c = 0; c < m[0].length; c++) { | |
m[r][c] = dot(m1[r], cs[c]); | |
} | |
} | |
return m; | |
} | |
function transpose(m) { | |
const t = matrix(m[0].length, m.length); | |
for (let r = 0; r < t.length; r++) { | |
for (let c = 0; c < t[0].length; c++) { | |
t[r][c] = m[c][r]; | |
} | |
} | |
return t; | |
} | |
function determinant(m) { | |
let det = 0; | |
if (m.length == 2) { | |
det = m[0][0] * m[1][1] - m[0][1] * m[1][0]; | |
} | |
else { | |
for (let col=0 ; col < m.length; col++){ | |
det = det + m[0][col] * cofactor(m, 0, col); | |
} | |
} | |
return det; | |
} | |
function submatrix(m,r,c) { | |
let sm = matrix(m.length -1, m.length -1); | |
for(let row = 0; row < m.length; row++) { | |
for(let col = 0; col < m[0].length; col++) { | |
if (row !== r && col !== c) { | |
let smr = row > r ? row - 1 : row; | |
let smc = col > c ? col - 1 : col; | |
sm[smr][smc] = m[row][col]; | |
} | |
} | |
} | |
return sm; | |
} | |
function minor(m,r,c) { | |
const sm = submatrix(m,r,c); | |
return determinant(sm); | |
} | |
function cofactor(m,r,c){ | |
const min = minor(m,r,c); | |
return (r+c) % 2 !== 0 ? -1 * min : min; | |
} | |
function isInvertible(m) { | |
return determinant(m) !== 0; | |
} | |
function inverse(m) { | |
if (isInvertible(m) === false) {throw `this matrix is not invertible: ${m}`}; | |
const im = matrix(m.length, m.length); // square matrix | |
const det = determinant(m); | |
for (let row = 0; row < m.length; row++) { | |
for (let col = 0; col < m.length; col++) { | |
let c = cofactor(m,row,col); | |
im[col][row] = c / det; | |
} | |
} | |
return im; | |
} | |
function translation(x,y,z) { | |
let t = identityMatrixFor(matrix(4,4)); | |
t[0][3] = x; | |
t[1][3] = y; | |
t[2][3] = z; | |
return t; | |
} | |
function scaling(x,y,z) { | |
let s = identityMatrixFor(matrix(4,4)); | |
s[0][0] = x; | |
s[1][1] = y; | |
s[2][2] = z; | |
return s; | |
} | |
function rotationX(rad) { | |
let r = identityMatrixFor(matrix(4,4)); | |
r[1][1] = Math.cos(rad); | |
r[1][2] = -Math.sin(rad); | |
r[2][1] = Math.sin(rad); | |
r[2][2] = Math.cos(rad); | |
return r; | |
} | |
function rotationY(rad) { | |
let r = identityMatrixFor(matrix(4,4)); | |
r[0][0] = Math.cos(rad); | |
r[0][2] = Math.sin(rad); | |
r[2][0] = -Math.sin(rad); | |
r[2][2] = Math.cos(rad); | |
return r; | |
} | |
function rotationZ(rad) { | |
let r = identityMatrixFor(matrix(4,4)); | |
r[0][0] = Math.cos(rad); | |
r[0][1] = -Math.sin(rad); | |
r[1][0] = Math.sin(rad); | |
r[1][1] = Math.cos(rad); | |
return r; | |
} | |
function shearing(xy,xz,yx,yz,zx,zy) { | |
let r = identityMatrixFor(matrix(4,4)); | |
r[0][1] = xy; | |
r[0][2] = xz; | |
r[1][0] = yx; | |
r[1][2] = yz; | |
r[2][0] = zx; | |
r[2][1] = zy; | |
return r; | |
} | |
// RAYCAST STUFF | |
function ray(origin, direction){ | |
return {origin, direction}; | |
} | |
function sphere() { | |
return { | |
origin: point(0,0,0), | |
radius: 1, | |
transform: identityMatrixFor(matrix(4,4)), | |
material: material() | |
}; | |
} | |
function position(ray, t) { | |
return add(ray.origin, tmul(ray.direction, t)); | |
} | |
function discriminant(ray, sphere) { | |
const sphereToRay = sub(ray.origin, point(0,0,0)); | |
const a = dot(ray.direction, ray.direction); | |
const b = 2 * dot(ray.direction, sphereToRay); | |
const c = dot(sphereToRay, sphereToRay) - 1; | |
return {a: a, b: b, c: c, discriminant: b * b - 4 * a * c}; | |
} | |
function intersects(sphere, ray) { | |
const r = transform(ray, inverse(sphere.transform)); | |
const d = discriminant(r, sphere); | |
if (d.discriminant < 0) { | |
return []; | |
} | |
else { | |
const i1 = intersection((-d.b - Math.sqrt(d.discriminant)) / (2 * d.a), sphere); | |
const i2 = intersection((-d.b + Math.sqrt(d.discriminant)) / (2 * d.a), sphere); | |
return intersections(i1,i2); | |
} | |
} | |
function intersection(t, o) { | |
return {t: t, object: o}; | |
} | |
function intersections(...args) { | |
return args; | |
} | |
function hit(intersections) { | |
let smallestPositive = {t: Number.MAX_SAFE_INTEGER}; | |
for (let i = 0; i < intersections.length ; i++) { | |
const intersection = intersections[i]; | |
if (intersection.t > 0 && intersection.t < smallestPositive.t) { | |
smallestPositive = intersection; | |
} | |
} | |
if (smallestPositive.t !== Number.MAX_SAFE_INTEGER) { | |
return smallestPositive; | |
} | |
// return undefined if no smallestPositive is found | |
} | |
function transform(r, m) { | |
const o = mmul(m, r.origin); | |
const d = mmul(m, r.direction); | |
return ray(o, d); | |
} | |
function setTransform(s, t) { | |
s.transform = t; | |
return s; | |
} | |
function normalAt(sphere, worldPoint){ | |
const objectPoint = mmul(inverse(sphere.transform), worldPoint); | |
const objectNormal = sub(objectPoint, point(0,0,0)); | |
const worldNormal = mmul(transpose(inverse(sphere.transform)), objectNormal); | |
worldNormal[3][0] = 0; // hack | |
return norm(worldNormal); | |
} | |
function reflect(inVec, normVec){ | |
return sub(inVec, tmul(tmul(normVec, 2), dot(inVec, normVec))); | |
} | |
function pointLight(position, intensity){ | |
return {position, intensity}; | |
} | |
function material(){ | |
return { | |
color: color(1,1,1), | |
ambient: 0.1, | |
diffuse: 0.9, | |
specular: 0.9, | |
shininess: 200 | |
}; | |
} | |
function lighting(material, light, point, eyeV, normalV) { | |
const black = color(0,0,0); | |
const effectiveColor = cprod(material.color, light.intensity); | |
const lightV = norm(sub(light.position, point)); | |
const ambient = cmul(effectiveColor, material.ambient); | |
const lightDotNormal = dot(lightV, normalV); | |
let diffuse = null; | |
let specular = null; | |
if (lightDotNormal < 0) { | |
diffuse = black; | |
specular = black; | |
} else { | |
diffuse = cmul(effectiveColor, (material.diffuse * lightDotNormal)); | |
const reflectV = reflect(neg(lightV), normalV); | |
const reflectDotEye = dot(reflectV, eyeV); | |
if (reflectDotEye <= 0) { | |
specular = black; | |
} else { | |
const factor = Math.pow(reflectDotEye, material.shininess); | |
specular = cmul(light.intensity, (material.specular * factor)); | |
} | |
} | |
return cadd(ambient, cadd(diffuse, specular)); | |
} | |
return { color, getRed, getGreen, getBlue, isEqualColor, | |
cadd, csub, cmul, cprod, | |
tuple, point, vector, | |
isTuple, isPoint, isVector, isEqualNumber, isEqualTuple, | |
add, sub, neg, tmul, div, mag, norm, dot, cross, | |
getX, getY, getZ, getW, | |
isEqual2DMatrix, mmul, identityMatrixFor, transpose, | |
determinant, submatrix, minor, cofactor, isInvertible, | |
inverse, translation, scaling, rotationX, rotationY, rotationZ, | |
shearing, | |
ray, sphere, position, intersects, intersection, intersections, hit, | |
transform, setTransform, normalAt, reflect, pointLight, material, | |
lighting}; | |
}); | |
mdlr('[test]mmzsource:raytrace', m => { | |
const test = m.test; | |
const expect = test.expect; | |
const { color, getRed, getGreen, getBlue, isEqualColor, | |
cadd, csub, cmul, cprod, | |
tuple, point, vector, | |
isTuple, isPoint, isVector, isEqualNumber, isEqualTuple, | |
add, sub, neg, tmul, div, mag, norm, dot, cross, | |
getX, getY, getZ, getW, | |
isEqual2DMatrix, mmul, identityMatrixFor, transpose, | |
determinant, submatrix, minor, cofactor, isInvertible, | |
inverse, translation, scaling, rotationX, rotationY, rotationZ, | |
shearing, | |
ray, sphere, position, intersects, intersection, intersections, hit, | |
transform, setTransform, normalAt, reflect, pointLight, material, | |
lighting } = m.require('mmzsource:raytrace'); | |
// COLOR TESTS | |
test.it('should create a color', done => { | |
const c = color(-0.5,0.4,1.7); | |
expect(getRed(c)).to.eql(-0.5); | |
expect(getGreen(c)).to.eql(0.4); | |
expect(getBlue(c)).to.eql(1.7); | |
done(); | |
}); | |
test.it('should add colors', done => { | |
const c1 = color(0.9, 0.6, 0.75); | |
const c2 = color(0.7, 0.1, 0.25); | |
expect(isEqualColor(cadd(c1,c2), color(1.6, 0.7, 1.0))).to.be.true(); | |
done(); | |
}); | |
test.it('should subtract colors', done => { | |
const c1 = color(0.9, 0.6, 0.75); | |
const c2 = color(0.7, 0.1, 0.25); | |
expect(isEqualColor(csub(c1,c2), color(0.2, 0.5, 0.5))).to.be.true(); | |
done(); | |
}); | |
test.it('should multiply a color by a scalar', done => { | |
expect(isEqualColor(cmul(color(0.2, 0.3, 0.4), 2), color(0.4, 0.6, 0.8))).to.be.true(); | |
done(); | |
}); | |
test.it('should calculate the hadamard product of 2 colors', done => { | |
const c1 = color(0.2, 0.3, 0.4); | |
const c2 = color(0.5, 0.6, 0.7); | |
expect(isEqualColor(cprod(c1,c2), color(0.10, 0.18, 0.28))).to.be.true(); | |
done(); | |
}) | |
// TUPLE TESTS | |
test.it('should create a point when tuple.w = 1.0', done => { | |
const t = tuple(4.3, -4.2, 3.1, 1.0); | |
expect(isPoint(t)).to.be.true(); | |
expect(isVector(t)).to.be.false(); | |
done(); | |
}); | |
test.it('should create a vector when tuple.w = 0.0', done => { | |
const t = tuple(4.3, -4.2, 3.1, 0.0); | |
expect(isVector(t)).to.be.true(); | |
expect(isPoint(t)).to.be.false(); | |
done(); | |
}); | |
test.it('should create a tuple with w = 1.0', done => { | |
const p = point(4, -4, 3); | |
expect(isTuple(p) && isEqualNumber(getW(p), 1.0)).to.be.true(); | |
done(); | |
}); | |
test.it('should create a tuple with w = 0.0', done => { | |
const v = vector(4, -4, 3); | |
expect(isTuple(v) && isEqualNumber(getW(v), 0.0)).to.be.true(); | |
done(); | |
}); | |
test.it('should add 2 tuples', done => { | |
const t1 = tuple(3, -2, 5, 1.0); | |
const t2 = tuple(-2, 3, 1, 0); | |
expect(isEqualTuple(add(t1, t2), tuple(1, 1, 6, 1))).to.be.true(); | |
done(); | |
}); | |
test.it('should add a point and a vector', done => { | |
const p = point(1, 2, 3); | |
const v = vector(5, 10, 20); | |
const expected = tuple(6, 12, 23, 1.0); | |
expect(isEqualTuple(add(p, v), expected)).to.be.true(); | |
expect(isEqualTuple(add(v, p), expected)).to.be.true(); | |
expect(isPoint(add(p, v))).to.be.true(); | |
done(); | |
}); | |
test.it('should add 2 vectors', done => { | |
const v1 = vector(-1, -2, -3); | |
const v2 = vector(-10, -20, -30); | |
const expected = tuple(-11, -22, -33, 0.0); | |
expect(isEqualTuple(add(v1, v2), expected)).to.be.true(); | |
expect(isEqualTuple(add(v2, v1), expected)).to.be.true(); | |
expect(isVector(add(v1, v2))).to.be.true(); | |
done(); | |
}); | |
// No real meaning in the domain | |
test.it('should add 2 points', done => { | |
const p1 = point(0, 0, 0); | |
const p2 = point(-100, 42, 1024); | |
const expected = tuple(-100, 42, 1024, 2.0); | |
expect(isEqualTuple(add(p1, p2), expected)).to.be.true(); | |
expect(isEqualTuple(add(p2, p1), expected)).to.be.true(); | |
expect(isPoint(add(p1, p2))).to.be.false(); | |
expect(isVector(add(p1, p2))).to.be.false(); | |
done(); | |
}); | |
test.it('should subtract 2 tuples', done => { | |
const t1 = tuple(3, -2, 5, 1.0); | |
const t2 = tuple(-20, 3, 1, 0); | |
expect(isEqualTuple(sub(t1, t2), tuple(23, -5, 4, 1.0))).to.be.true(); | |
done(); | |
}); | |
test.it('should subtract a point and a vector', done => { | |
const p = point(1, 2, 3); | |
const v = vector(5, 10, 20); | |
expect(isEqualTuple(sub(p, v), tuple(-4, -8, -17, 1.0))).to.be.true(); | |
expect(isEqualTuple(sub(v, p), tuple(4, 8, 17, -1.0))).to.be.true(); | |
expect(isPoint(sub(p, v))).to.be.true(); | |
done(); | |
}); | |
test.it('should subtract 2 vectors', done => { | |
const v1 = vector(-1, -2, -3); | |
const v2 = vector(10, 20, 30); | |
expect(isEqualTuple(sub(v1, v2), tuple(-11, -22, -33, 0.0))).to.be.true(); | |
expect(isEqualTuple(sub(v2, v1), tuple(11, 22, 33, 0.0))).to.be.true(); | |
expect(isVector(sub(v1, v2))).to.be.true(); | |
done(); | |
}); | |
test.it('should subtract 2 points', done => { | |
const p1 = point(0, 0, 0); | |
const p2 = point(100, 42, 1024); | |
expect(isEqualTuple(sub(p1, p2), tuple(-100, -42, -1024, 0.0))).to.be.true(); | |
expect(isEqualTuple(sub(p2, p1), tuple(100, 42, 1024, 0.0))).to.be.true(); | |
expect(isVector(sub(p1, p2))); | |
done(); | |
}); | |
test.it('should negate a tuple', done => { | |
expect(isEqualTuple(neg(tuple(1, 2, 3, 4)), tuple(-1, -2, -3, -4))).to.be.true(); | |
done(); | |
}); | |
test.it('should multiply a tuple by a scalar', done => { | |
const t = tuple(22, 33, 44, 55); | |
expect(isEqualTuple(tmul(t, 4), tuple(88, 132, 176, 220))).to.be.true(); | |
expect(isEqualTuple(tmul(t, 0.2), tuple(4.4, 6.6000000000000005, 8.8, 11))).to.be.true(); | |
done(); | |
}); | |
test.it('should divide a tuple by a scalar', done => { | |
const t = tuple(22, 33, 44, 55); | |
expect(isEqualTuple(div(t, 0.25), tuple(88, 132, 176, 220))).to.be.true(); | |
expect(isEqualTuple(div(t, 5), tuple(4.4, 6.6000000000000005, 8.8, 11))).to.be.true(); | |
done(); | |
}); | |
test.it('should calculate the magnitude of a vector', done => { | |
expect(mag(vector(1, 0, 0))).to.eql(1); | |
expect(mag(vector(0, 1, 0))).to.eql(1); | |
expect(mag(vector(0, 0, 1))).to.eql(1); | |
expect(mag(vector(-1, -2, -3))).to.eql(Math.sqrt(14)); | |
done(); | |
}); | |
test.it('should normalize a vector', done => { | |
expect(isEqualTuple(norm(vector(4, 0, 0)), vector(1, 0, 0))).to.be.true(); | |
const n = norm(vector(1, 2, 3)); | |
expect(isEqualTuple(n, vector(0.2672612419124244, 0.5345224838248488, 0.8017837257372732, 0))).to.be.true(); | |
expect(isEqualNumber(mag(n), 1)).to.be.true(); | |
expect(mag(n)).to.eql(1); | |
done(); | |
}); | |
test.it('should calculate the dot product of 2 tuples', done => { | |
const v1 = vector(1, 2, 3); | |
const v2 = vector(2, 3, 4); | |
expect(dot(v1, v2)).to.eql(20); | |
done(); | |
}); | |
test.it('should calculate the cross product of 2 vectors', done => { | |
const v1 = vector(1, 2, 3); | |
const v2 = vector(2, 3, 4); | |
expect(isEqualTuple(cross(v1, v2), vector(-1, 2, -1))).to.be.true(); | |
expect(isEqualTuple(cross(v2, v1), vector(1, -2, 1))).to.be.true(); | |
const x = vector(1, 0, 0); | |
const y = vector(0, 1, 0); | |
const z = vector(0, 0, 1); | |
expect(isEqualTuple(cross(x, y), z)).to.be.true(); | |
expect(isEqualTuple(cross(z, x), y)).to.be.true(); | |
expect(isEqualTuple(cross(y, z), x)).to.be.true(); | |
done(); | |
}); | |
// MATRIX TESTS | |
test.it('should create zero based matrices, lookup via r,c', done => { | |
const m = [[1,2,3,4],[5.5,6.5,7.5,8.5],[9,10,11,12],[13.5,14.5,15.5,16.5]]; | |
expect(m[0][0]).to.eql(1); | |
expect(m[0][3]).to.eql(4); | |
expect(m[1][0]).to.eql(5.5); | |
expect(m[1][2]).to.eql(7.5); | |
expect(m[2][2]).to.eql(11); | |
expect(m[3][0]).to.eql(13.5); | |
expect(m[3][2]).to.eql(15.5); | |
done(); | |
}); | |
test.it('should check for equality given some EPSILON', done => { | |
const m1 = [[1,2,3],[4,5,6],[7,8,9]]; | |
const m2 = [[1,2,3],[4,5,6],[7,8,9]]; | |
const m3 = [[0,0,0],[1,1,1],[2,2,2]]; | |
const m4 = [[0,0][1,1]]; | |
expect(isEqual2DMatrix(m1,m1)).to.be.true(); | |
expect(isEqual2DMatrix(m1,m2)).to.be.true(); | |
expect(isEqual2DMatrix(m1,m3)).to.be.false(); | |
expect(isEqual2DMatrix(m3,m4)).to.be.false(); | |
done(); | |
}); | |
test.it('should multiply matrices', done => { | |
const m1 = [[0,1],[2,3]]; | |
const m2 = [[5,6],[7,8]]; | |
expect(isEqual2DMatrix(mmul(m1,m2),[[7,8],[31,36]])).to.be.true(); | |
const m3 = [[1,2,3],[4,5,6]]; | |
const m4 = [[7,8],[9,10],[11,12]]; | |
expect(isEqual2DMatrix(mmul(m3,m4), [[58,64],[139,154]])).to.be.true(); | |
const m5 = [[1,2,3]]; | |
const m6 = [[4],[5],[6]]; | |
expect(isEqual2DMatrix(mmul(m5,m6), [[32]])).to.be.true(); | |
const m7 = [[1,2,3,4],[2,4,4,2],[8,6,4,1],[0,0,0,1]]; | |
const m8 = [[1],[2],[3],[1]]; // tuple in matrix format | |
expect(isEqual2DMatrix(mmul(m7,m8), [[18],[24],[33],[1]])).to.be.true(); | |
done(); | |
}); | |
test.it('should create identity matrices', done => { | |
const m1 = [[1,2],[3,4]]; | |
const im1 = identityMatrixFor(m1); | |
expect(isEqual2DMatrix(im1, [[1,0],[0,1]])).to.be.true(); | |
expect(isEqual2DMatrix(mmul(m1,im1), m1)).to.be.true(); | |
expect(isEqual2DMatrix(mmul(im1,m1), m1)).to.be.true(); | |
const m2 = [[1,2,3],[4,5,6],[7,8,9]]; | |
const im2 = identityMatrixFor(m2); | |
expect(isEqual2DMatrix(im2, [[1,0,0],[0,1,0],[0,0,1]])).to.be.true(); | |
expect(isEqual2DMatrix(mmul(m2,im2), m2)).to.be.true(); | |
expect(isEqual2DMatrix(mmul(im2,m2), m2)).to.be.true(); | |
done(); | |
}); | |
test.it('should transpose matrices', done => { | |
const m1 = [[0,9,3,0],[9,8,0,8],[1,8,5,3],[0,0,5,8]]; | |
const t1 = [[0,9,1,0],[9,8,8,0],[3,0,5,5],[0,8,3,8]]; | |
expect(isEqual2DMatrix(transpose(m1),t1)).to.be.true(); | |
expect(isEqual2DMatrix(transpose(transpose(m1)), m1)).to.be.true(); | |
const m2 = [[1,2],[3,4],[5,6]]; | |
const t2 = [[1,3,5],[2,4,6]]; | |
expect(isEqual2DMatrix(transpose(m2), t2)).to.be.true(); | |
expect(isEqual2DMatrix(transpose(transpose(m2)), m2)).to.be.true(); | |
const im = identityMatrixFor([[11,22],[33,44]]); | |
expect(isEqual2DMatrix(transpose(im), im)).to.be.true(); | |
done(); | |
}); | |
test.it('should calculate determinant of 2x2 matrix', done => { | |
const m = [[1,5],[-3,2]]; | |
expect(determinant(m)).to.eql(17); | |
done(); | |
}); | |
test.it('should calculate submatrix of 3x3 matrix', done => { | |
const m = [[1,5,0],[-3,2,7],[0,6,-3]]; | |
const sm = submatrix(m,0,2); | |
const expected = [[-3,2],[0,6]]; | |
expect(isEqual2DMatrix(sm, expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should calculate submatrix of 4x4 matrix', done => { | |
const m = [[-6,1,1,6],[-8,5,8,6],[-1,0,8,2],[-7,1,-1,1]]; | |
const sm = submatrix(m,2,1); | |
const expected = [[-6,1,6],[-8,8,6],[-7,-1,1]]; | |
expect(isEqual2DMatrix(sm, expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should calculate minor in a 3x3 matrix', done => { | |
const m = [[3,5,0],[2,-1,-7],[6,-1,5]]; | |
expect(minor(m,1,0)).to.eql(25); | |
done(); | |
}); | |
test.it('should calculate cofactor of 3x3 matrix', done => { | |
const m = [[3,5,0],[2,-1,-7],[6,-1,5]]; | |
expect(minor(m,0,0)).to.eql(-12); | |
expect(cofactor(m,0,0)).to.eql(-12); | |
expect(minor(m,1,0)).to.eql(25); | |
expect(cofactor(m,1,0)).to.eql(-25); | |
done(); | |
}); | |
test.it('should calculate determinant of a 3x3 matrix', done => { | |
const m = [[1,2,6],[-5,8,-4],[2,6,4]]; | |
expect(cofactor(m,0,0)).to.eql(56); | |
expect(cofactor(m,0,1)).to.eql(12); | |
expect(cofactor(m,0,2)).to.eql(-46); | |
expect(determinant(m)).to.eql(-196); | |
done(); | |
}); | |
test.it('should calculate determinant of a 4x4 matrix', done => { | |
const m = [[-2,-8,3,5],[-3,1,7,3],[1,2,-9,6],[-6,7,7,-9]]; | |
expect(cofactor(m,0,0)).to.eql(690); | |
expect(cofactor(m,0,1)).to.eql(447); | |
expect(cofactor(m,0,2)).to.eql(210); | |
expect(cofactor(m,0,3)).to.eql(51); | |
expect(determinant(m)).to.eql(-4071); | |
done(); | |
}); | |
test.it('should determine invertibility of a matrix', done => { | |
const m1 = [[6,4,4,4],[5,5,7,6],[4,-9,3,-7],[9,1,7,-6]]; | |
expect(determinant(m1)).to.eql(-2120); | |
expect(isInvertible(m1)).to.be.true(); | |
const m2 = [[-4,2,-2,-3],[9,6,2,6],[0,-5,1,-5],[0,0,0,0]]; | |
expect(determinant(m2)).to.eql(0); | |
expect(isInvertible(m2)).to.be.false(); | |
done(); | |
}); | |
test.it('should calculate inverse of a matrix part 1', done => { | |
const m = [[-5,2,6,-8],[1,-5,1,8],[7,7,-6,-7],[1,-3,7,4]]; | |
const i = inverse(m); | |
const expected = [[ 0.21805, 0.45113, 0.24060, -0.04511], | |
[-0.80827, -1.45677, -0.44361, 0.52068], | |
[-0.07895, -0.22368, -0.05263, 0.19737], | |
[-0.52256, -0.81391, -0.30075, 0.30639]] | |
expect(determinant(m)).to.eql(532); | |
expect(cofactor(m,2,3)).to.eql(-160); | |
expect(i[3][2]).to.eql(-160/532); | |
expect(cofactor(m,3,2)).to.eql(105); | |
expect(i[2][3]).to.eql(105/532); | |
expect(isEqual2DMatrix(i, expected, 0.0001)).to.be.true(); | |
done(); | |
}); | |
test.it('should calculate inverse of a matrix part 2', done => { | |
const m = [[8,-5,9,2],[7,5,6,1],[-6,0,9,6],[-3,0,-9,-4]]; | |
const i = inverse(m); | |
const expected = [[-0.15385, -0.15385, -0.28205, -0.53846], | |
[-0.07692, 0.12308, 0.02564, 0.03077], | |
[ 0.35897, 0.35897, 0.43590, 0.92308], | |
[-0.69231, -0.69231, -0.76923, -1.92308]] | |
expect(isEqual2DMatrix(i, expected, 0.0001)).to.be.true(); | |
done(); | |
}); | |
test.it('should calculate inverse of a matrix part 3', done => { | |
const m = [[9,3,0,9],[-5,-2,-6,-3],[-4,9,6,4],[-7,6,6,2]]; | |
const i = inverse(m); | |
const expected = [[-0.04074, -0.07778, 0.14444, -0.22222], | |
[-0.07778, 0.03333, 0.36667, -0.33333], | |
[-0.02901, -0.14630, -0.10926, 0.12963], | |
[ 0.17778, 0.06667, -0.26667, 0.33333]] | |
expect(isEqual2DMatrix(i, expected, 0.0001)).to.be.true(); | |
done(); | |
}); | |
test.it('should return original matrix when multiplying product by its inverse', done => { | |
const a = [[3,-9,7,3],[3,-8,2,-9],[-4,4,4,1],[-6,5,-1,1]]; | |
const b = [[8,2,2,2],[3,-1,7,0],[7,0,5,4],[6,-2,0,5]]; | |
const c = mmul(a,b); | |
expect(isEqual2DMatrix(a, mmul(c, inverse(b)), 0.0001)).to.be.true(); | |
done(); | |
}); | |
test.it('should create translation matrices', done => { | |
const x=3, y=4, z=5; | |
const expected = [[1,0,0,x],[0,1,0,y],[0,0,1,z],[0,0,0,1]]; | |
expect(isEqual2DMatrix(translation(x,y,z), expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should multiply point with a translation matrix', done => { | |
const t = translation(5,-3,2); | |
const p = point(-3,4,5); | |
const expected = point(2,1,7); | |
expect(isEqual2DMatrix(mmul(t,p), expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should multiply points with inverse of a translation matrix', done => { | |
const t = translation(5,-3,2); | |
const inv = inverse(t); | |
const p = point(-3,4,5); | |
const expected = point(-8,7,3); | |
expect(isEqual2DMatrix(mmul(inv,p), expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should leave vectors as is', done => { | |
const t = translation(5,-3,2); | |
const v = vector(-3,4,5); | |
expect(isEqual2DMatrix(mmul(t,v), v)).to.be.true(); | |
done(); | |
}); | |
test.it('should create scaling matrices', done => { | |
const x=3, y=4, z=5; | |
const expected = [[x,0,0,0],[0,y,0,0],[0,0,z,0],[0,0,0,1]]; | |
expect(isEqual2DMatrix(scaling(x,y,z), expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should apply scaling to a point', done => { | |
const s = scaling(2,3,4); | |
const p = point(-4,6,8); | |
const expected = point(-8,18,32); | |
expect(isEqual2DMatrix(mmul(s,p), expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should apply scaling to a vector', done => { | |
const s = scaling(2,3,4); | |
const p = point(-4,6,8); | |
const expected = point(-8,18,32); | |
expect(isEqual2DMatrix(mmul(s,p), expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should multiply with the inverse of a scaling matrix', done => { | |
const s = scaling(2,3,4); | |
const inv = inverse(s); | |
const v = vector(-4,6,8); | |
const expected = vector(-2,2,2); | |
expect(isEqual2DMatrix(mmul(inv, v), expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should reflect by scaling by a negative value', done => { | |
const s = scaling(-1,1,1); | |
const p = point(2,3,4); | |
const expected = point(-2,3,4); | |
expect(isEqual2DMatrix(mmul(s,p), expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should create x-axis rotation matrix', done => { | |
const r = Math.PI/4; | |
const a = Math.cos(r), b = -Math.sin(r), c = Math.sin(r), d = Math.cos(r); | |
const halfQuarter = rotationX(r); | |
const expected = [[1,0,0,0],[0,a,b,0],[0,c,d,0],[0,0,0,1]]; | |
expect(isEqual2DMatrix(halfQuarter, expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should handle x rotations', done => { | |
const p = point(0,1,0); | |
const halfQuarter = rotationX(Math.PI/4); | |
const fullQuarter = rotationX(Math.PI/2); | |
const hqExpected = point(0,Math.sqrt(2)/2,Math.sqrt(2)/2); | |
const fqExpected = point(0,0,1); | |
expect(isEqual2DMatrix(mmul(halfQuarter, p), hqExpected)).to.be.true(); | |
expect(isEqual2DMatrix(mmul(fullQuarter, p), fqExpected)).to.be.true(); | |
done(); | |
}); | |
test.it('should handle inverse x rotations', done => { | |
const p = point(0,1,0); | |
const halfQuarter = rotationX(Math.PI/4); | |
const inv = inverse(halfQuarter); | |
const expected = point(0,Math.sqrt(2)/2,-Math.sqrt(2)/2); | |
expect(isEqual2DMatrix(mmul(inv, p), expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should create y-axis rotation matrix', done => { | |
const r = Math.PI/4; | |
const a = Math.cos(r), b = Math.sin(r), c = -Math.sin(r), d = Math.cos(r); | |
const halfQuarter = rotationY(r); | |
const expected = [[a,0,b,0],[0,1,0,0],[c,0,d,0],[0,0,0,1]]; | |
expect(isEqual2DMatrix(halfQuarter, expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should handle y rotations', done => { | |
const p = point(0,0,1); | |
const halfQuarter = rotationY(Math.PI/4); | |
const fullQuarter = rotationY(Math.PI/2); | |
const hqExpected = point(Math.sqrt(2)/2,0,Math.sqrt(2)/2); | |
const fqExpected = point(1,0,0); | |
expect(isEqual2DMatrix(mmul(halfQuarter, p), hqExpected)).to.be.true(); | |
expect(isEqual2DMatrix(mmul(fullQuarter, p), fqExpected)).to.be.true(); | |
done(); | |
}); | |
test.it('should create z-axis rotation matrix', done => { | |
const r = Math.PI/4; | |
const a = Math.cos(r), b = -Math.sin(r), c = Math.sin(r), d = Math.cos(r); | |
const halfQuarter = rotationZ(r); | |
const expected = [[a,b,0,0],[c,d,0,0],[0,0,1,0],[0,0,0,1]]; | |
expect(isEqual2DMatrix(halfQuarter, expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should handle z rotations', done => { | |
const p = point(0,1,0); | |
const halfQuarter = rotationZ(Math.PI/4); | |
const fullQuarter = rotationZ(Math.PI/2); | |
const hqExpected = point(-Math.sqrt(2)/2,Math.sqrt(2)/2,0); | |
const fqExpected = point(-1,0,0); | |
expect(isEqual2DMatrix(mmul(halfQuarter, p), hqExpected)).to.be.true(); | |
expect(isEqual2DMatrix(mmul(fullQuarter, p), fqExpected)).to.be.true(); | |
done(); | |
}); | |
test.it('should create shearing matrix', done => { | |
const xy = 2, xz = 3, yx = 4, yz = 5, zx = 6, zy = 7; | |
const expected = [[1,xy,xz,0],[yx,1,yz,0],[zx,zy,1,0],[0,0,0,1]]; | |
expect(isEqual2DMatrix(shearing(xy,xz,yx,yz,zx,zy), expected)).to.be.true(); | |
done(); | |
}); | |
test.it('should apply shearing transformation of x in proportion to y', done => { | |
const s = shearing(1,0,0,0,0,0); | |
const p = point(2,3,4); | |
expect(isEqual2DMatrix(mmul(s,p), point(5,3,4))).to.be.true(); | |
done(); | |
}); | |
test.it('should apply shearing transformation of x in proportion to z', done => { | |
const s = shearing(0,1,0,0,0,0); | |
const p = point(2,3,4); | |
expect(isEqual2DMatrix(mmul(s,p), point(6,3,4))).to.be.true(); | |
done(); | |
}); | |
test.it('should apply shearing transformation of y in proportion to x', done => { | |
const s = shearing(0,0,1,0,0,0); | |
const p = point(2,3,4); | |
expect(isEqual2DMatrix(mmul(s,p), point(2,5,4))).to.be.true(); | |
done(); | |
}); | |
test.it('should apply shearing transformation of y in proportion to z', done => { | |
const s = shearing(0,0,0,1,0,0); | |
const p = point(2,3,4) | |
expect(isEqual2DMatrix(mmul(s,p), point(2,7,4))).to.be.true(); | |
done(); | |
}); | |
test.it('should apply shearing transformation of z in proportion to x', done => { | |
const s = shearing(0,0,0,0,1,0); | |
const p = point(2,3,4); | |
expect(isEqual2DMatrix(mmul(s,p), point(2,3,6))).to.be.true(); | |
done(); | |
}); | |
test.it('should apply shearing transformation of z in proportion to y', done => { | |
const s = shearing(0,0,0,0,0,1); | |
const p = point(2,3,4); | |
expect(isEqual2DMatrix(mmul(s,p), point(2,3,7))).to.be.true(); | |
done(); | |
}); | |
test.it('should chain transformation in reverse order', done => { | |
const p = point(1,0,1); | |
const a = rotationX(Math.PI/2); | |
const b = scaling(5,5,5); | |
const c = translation(10,5,7); | |
const expected = point(15,0,7); | |
const p2 = mmul(a,p); // rotate | |
expect(isEqual2DMatrix(p2, point(1,-1,0), 0.0001)).to.be.true(); | |
const p3 = mmul(b,p2); // then scale | |
expect(isEqual2DMatrix(p3, point(5,-5,0), 0.0001)).to.be.true(); | |
const p4 = mmul(c,p3); // then apply translation | |
expect(isEqual2DMatrix(p4, expected, 0.0001)).to.be.true(); | |
// now chain them: | |
const ct = mmul(c, mmul(b,a)); | |
const p5 = mmul(ct, p); | |
expect(isEqual2DMatrix(p5, expected, 0.0001)).to.be.true(); | |
done(); | |
}); | |
// RAY TESTS | |
test.it('should create rays', done => { | |
const r = ray(point(1,2,3), vector(4,5,6)); | |
expect(isEqual2DMatrix(r.origin, point(1,2,3))).to.be.true(); | |
expect(isEqual2DMatrix(r.direction, vector(4,5,6))).to.be.true(); | |
done(); | |
}); | |
test.it('should compute a point from a distance', done => { | |
const r = ray(point(2,3,4), vector(1,0,0)); | |
expect(isEqual2DMatrix(position(r, 0), point(2,3,4))).to.be.true(); | |
expect(isEqual2DMatrix(position(r,1), point(3,3,4))).to.be.true(); | |
expect(isEqual2DMatrix(position(r,-1), point(1,3,4))).to.be.true(); | |
expect(isEqual2DMatrix(position(r,2.5), point(4.5,3,4))).to.be.true(); | |
done(); | |
}); | |
test.it('should compute intersection points', done => { | |
const r = ray(point(0,0,-5), vector(0,0,1)); | |
const s = sphere(); | |
const xs = intersects(s,r); | |
expect(xs.length).to.eql(2); | |
expect(xs[0].t).to.eql(4); | |
expect(xs[1].t).to.eql(6); | |
done(); | |
}); | |
test.it('should compute intersection points at tangent', done => { | |
const r = ray(point(0,1,-5), vector(0,0,1)); | |
const s = sphere(); | |
const xs = intersects(s,r); | |
expect(xs.length).to.eql(2); | |
expect(xs[0].t).to.eql(5); | |
expect(xs[1].t).to.eql(5); | |
done(); | |
}); | |
test.it('should compute no intersection points when ray misses sphere', done => { | |
const r = ray(point(0,2,-5), vector(0,0,1)); | |
const s = sphere(); | |
const xs = intersects(s,r); | |
expect(xs.length).to.eql(0); | |
done(); | |
}); | |
test.it('should compute intersection points when ray originates inside sphere', done => { | |
const r = ray(point(0,0,0), vector(0,0,1)); | |
const s = sphere(); | |
const xs = intersects(s,r); | |
expect(xs.length).to.eql(2); | |
expect(xs[0].t).to.eql(-1); | |
expect(xs[1].t).to.eql(1); | |
done(); | |
}); | |
test.it('should compute intersection points when sphere is behind ray', done => { | |
const r = ray(point(0,0,5), vector(0,0,1)); | |
const s = sphere(); | |
const xs = intersects(s,r); | |
expect(xs.length).to.eql(2); | |
expect(xs[0].t).to.eql(-6); | |
expect(xs[1].t).to.eql(-4); | |
done(); | |
}); | |
test.it('should have datastructure to encapsulate intersection', done => { | |
const s = sphere(); | |
const i = intersection(3.5, s); | |
expect(i.t).to.eql(3.5); | |
expect(i.object).to.eql(s); | |
done(); | |
}); | |
test.it('should aggregate intersections', done => { | |
const s = sphere(); | |
const i1 = intersection(1, s); | |
const i2 = intersection(2, s); | |
const xs = intersections(i1, i2); | |
expect(xs.length).to.eql(2); | |
expect(xs[0].t).to.eql(1); | |
expect(xs[1].t).to.eql(2); | |
done(); | |
}); | |
test.it('should add the objects to the intersections', done => { | |
const r = ray(point(0,0,-5), vector(0,0,1)); | |
const s = sphere(); | |
const xs = intersects(s,r); | |
expect(xs.length).to.eql(2); | |
expect(xs[0].object).to.eql(s); | |
expect(xs[1].object).to.eql(s); | |
done(); | |
}); | |
test.it('should detect hit in positive intersections', done => { | |
const s = sphere(); | |
const i1 = intersection(1, s); | |
const i2 = intersection(2, s); | |
const xs = intersections(i2, i1); | |
const h = hit(xs); | |
expect(h).to.eql(i1); | |
expect(h.t).to.eql(i1.t); | |
done(); | |
}); | |
test.it('should filter negative intersections when determining hit', done => { | |
const s = sphere(); | |
const i1 = intersection(-1, s); | |
const i2 = intersection(1, s); | |
const xs = intersections(i2, i1); | |
const h = hit(xs); | |
expect(h).to.eql(i2); | |
expect(h.t).to.eql(i2.t); | |
done(); | |
}); | |
test.it('should return no hit when all intersections have a negative t value', done => { | |
const s = sphere(); | |
const i1 = intersection(-2, s); | |
const i2 = intersection(-1, s); | |
const xs = intersections(i2, i1); | |
const h = hit(xs); | |
expect(h).to.eql(undefined); | |
done(); | |
}); | |
test.it('should return the lowest nonnegative intersection', done => { | |
const s = sphere(); | |
const i1 = intersection(5, s); | |
const i2 = intersection(7, s); | |
const i3 = intersection(-3, s); | |
const i4 = intersection(2,s); | |
const xs = intersections(i1, i2, i3, i4); | |
const h = hit(xs); | |
expect(h).to.eql(i4); | |
expect(h.t).to.eql(i4.t); | |
done(); | |
}); | |
test.it('should translate a ray', done => { | |
const r1 = ray(point(1,2,3), vector(0,1,0)); | |
const m = translation(3,4,5); | |
const r2 = transform(r1,m); | |
expect(isEqualTuple(r2.origin, point(4,6,8))).to.be.true(); | |
expect(isEqualTuple(r2.direction, vector(0,1,0))).to.be.true(); | |
done(); | |
}); | |
test.it('should scale a ray', done => { | |
const r1 = ray(point(1,2,3), vector(0,1,0)); | |
const m = scaling(2,3,4); | |
const r2 = transform(r1,m); | |
expect(isEqualTuple(r2.origin, point(2,6,12))).to.be.true(); | |
expect(isEqualTuple(r2.direction, vector(0,3,0))).to.be.true(); | |
done(); | |
}); | |
test.it('should provide a sphere with a default transformation', done => { | |
const s = sphere(); | |
expect(s.transform).to.eql([[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]); | |
done(); | |
}); | |
test.it('should change a spheres transformation', done => { | |
let s1 = sphere(); | |
const t = translation(2,3,4); | |
s1 = setTransform(s1, t); | |
expect(s1.transform).to.eql(t); | |
done(); | |
}); | |
test.it('should intersect a scaled sphere with a ray', done => { | |
const r = ray(point(0,0,-5), vector(0,0,1)); | |
let s = sphere(); | |
s = setTransform(s, scaling(2,2,2)); | |
const xs = intersects(s,r); | |
expect(xs.length).to.eql(2); | |
expect(xs[0].t).to.eql(3); | |
expect(xs[1].t).to.eql(7); | |
done(); | |
}); | |
test.it('should intersect a translated sphere with a ray', done => { | |
const r = ray(point(0,0,-5), vector(0,0,1)); | |
let s = sphere(); | |
s = setTransform(s, translation(5,0,0)); | |
const xs = intersects(s,r); | |
expect(xs.length).to.eql(0); | |
done(); | |
}); | |
test.it('should return the normal on a sphere at a point on the x-axis', done => { | |
const s = sphere(); | |
const n = normalAt(s, point(1,0,0)); | |
expect(n).to.eql(vector(1,0,0)); | |
done(); | |
}); | |
test.it('should return the normal on a sphere at a point on the y-axis', done => { | |
const s = sphere(); | |
const n = normalAt(s, point(0,1,0)); | |
expect(n).to.eql(vector(0,1,0)); | |
done(); | |
}); | |
test.it('should return the normal on a sphere at a point on the z-axis', done => { | |
const s = sphere(); | |
const n = normalAt(s, point(0,0,1)); | |
expect(n).to.eql(vector(0,0,1)); | |
done(); | |
}); | |
test.it('should return a normalized vector', done => { | |
const s = sphere(); | |
const x = Math.sqrt(3) / 3; | |
const n = normalAt(s, point(x,x,x)); | |
expect(n).to.eql(norm(n)); | |
done(); | |
}); | |
test.it('should compute the normal on a translated sphere', done => { | |
let s = sphere(); | |
s = setTransform(s, translation(0,1,0)); | |
const n = normalAt(s, point(0, 1.70711, -0.70711)); | |
expect(isEqual2DMatrix(n, vector(0, 0.70711, -0.70711), 0.0001)).to.be.true(); | |
done(); | |
}); | |
test.it('should compute the normal on a transformed sphere', done => { | |
let s = sphere(); | |
let m = mmul(scaling(1, 0.5, 1), rotationX(Math.PI/5)); | |
s = setTransform(s, m); | |
const x = Math.sqrt(2) / 2; | |
const n = normalAt(s, point(0, x, -x)); | |
expect(isEqual2DMatrix(n, vector(0, 0.97014, -0.24254), 0.0001)).to.be.true(); | |
done(); | |
}); | |
test.it('should reflect a vector approaching at 45 degrees', done => { | |
const v = vector(1,-1,0); | |
const n = vector(0,1,0); | |
const r = reflect(v,n); | |
expect(r).to.eql(vector(1,1,0)); | |
done(); | |
}); | |
test.it('should reflect a vector off a slanted surface', done => { | |
const v = vector(0,-1,0); | |
const x = Math.sqrt(2) / 2; | |
const n = vector(x,x,0); | |
const r = reflect(v,n); | |
expect(isEqual2DMatrix(r, vector(1,0,0), 0.0001)).to.be.true(); | |
done(); | |
}); | |
test.it('should create light with position and intensity', done => { | |
const intensity = color(1,1,1); | |
const position = point(0,0,0); | |
const light = pointLight(position, intensity); | |
expect(light.position).to.eql(position); | |
expect(light.intensity).to.eql(intensity); | |
done(); | |
}); | |
test.it('should create a default material', done => { | |
const m = material(); | |
expect(m.color).to.eql(color(1,1,1)); | |
expect(m.ambient).to.eql(0.1); | |
expect(m.diffuse).to.eql(0.9); | |
expect(m.specular).to.eql(0.9); | |
expect(m.shininess).to.eql(200); | |
done(); | |
}); | |
test.it('should add default material to a sphere', done => { | |
const s = sphere(); | |
expect(s.material).to.eql(material()); | |
done(); | |
}); | |
test.it('should assign a material to a sphere', done => { | |
const s = sphere(); | |
const m = material(); | |
m.ambient = 1; | |
s.material = m; | |
expect(s.material.ambient).to.eql(1); | |
done(); | |
}); | |
test.it('should return lighting at full strength', done => { | |
const m = material(); | |
const position = point(0,0,0); | |
const eyeV = vector(0,0,-1); | |
const normalV = vector(0,0,-1); | |
const light = pointLight(point(0,0,-10), color(1,1,1)); | |
const result = lighting(m, light, position, eyeV, normalV); | |
expect(isEqualColor(result, color(1.9, 1.9, 1.9), 0.0001)).to.be.true(); | |
done(); | |
}); | |
test.it('should return low specular value; eye offset = 45 degrees', done => { | |
const x = Math.sqrt(2) / 2; | |
const m = material(); | |
const position = point(0,0,0); | |
const eyeV = vector(0, x, x); | |
const normalV = vector(0,0,-1); | |
const light = pointLight(point(0,0,-10), color(1,1,1)); | |
const result = lighting(m, light, position, eyeV, normalV); | |
expect(isEqualColor(result, color(1.0, 1.0, 1.0), 0.0001)).to.be.true(); | |
done(); | |
}); | |
test.it('should return lower specular & diffuse values; light offset = 45 degrees', done => { | |
const m = material(); | |
const position = point(0,0,0); | |
const eyeV = vector(0, 0, -1); | |
const normalV = vector(0,0,-1); | |
const light = pointLight(point(0,10,-10), color(1,1,1)); | |
const result = lighting(m, light, position, eyeV, normalV); | |
expect(isEqualColor(result, color(0.7364, 0.7364, 0.7364), 0.0001)).to.be.true(); | |
done(); | |
}); | |
test.it('should return ambient component when light is behind surface', done => { | |
const m = material(); | |
const position = point(0,0,0); | |
const eyeV = vector(0,0,-1); | |
const normalV = vector(0,0,-1); | |
const light = pointLight(point(0,0,10), color(1,1,1)); | |
const result = lighting(m, light, position, eyeV, normalV); | |
expect(isEqualColor(result, color(0.1,0.1,0.1), 0.0001)).to.be.true(); | |
done(); | |
}); | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
mdlr('mmzsource:rt-h5', m => { | |
const style = `width:100vw; height:100vh;`; | |
const doc = document.body; | |
doc.innerHTML = `<canvas width=500 height=500 style="${style}"></canvas>`; | |
doc.style.margin = "0"; | |
doc.style.overflow = "hidden"; | |
const canvas = doc.querySelector('canvas'); | |
const { width, height } = canvas; | |
const ctx = canvas.getContext('2d'); | |
const image = ctx.getImageData(0, 0, width, height); | |
const { color, getRed, getGreen, getBlue } = m.require('mmzsource:canvas'); | |
const shapeColor = color(0,0,1); | |
const rt = m.require('mmzsource:raytrace'); | |
const rayOrigin = rt.point(0,0,-5); | |
const wallZ = 10; | |
const wallSize = 7; | |
const pixelSize = wallSize / width; | |
const half = wallSize * 0.5; | |
const shape = rt.sphere(); | |
function animate() { | |
for (let y = 0; y < height; y++) { | |
let worldY = half - pixelSize * y; | |
for (let x = 0; x < width; x++) { | |
const worldX = -half + pixelSize * x; | |
const position = rt.point(worldX, worldY, wallZ); | |
const r = rt.ray(rayOrigin, rt.norm(rt.sub(position, rayOrigin))); | |
const xs = rt.intersects(shape, r); | |
if (rt.hit(xs)) { | |
const pixel = (x*4)+(y*4*width); | |
image.data[pixel + 0] = getRed(shapeColor) * 255; | |
image.data[pixel + 1] = getGreen(shapeColor) * 255; | |
image.data[pixel + 2] = getBlue(shapeColor) * 255; | |
image.data[pixel + 3] = 255; | |
} | |
} | |
} | |
ctx.putImageData(image, 0, 0); | |
} | |
requestAnimationFrame(animate); | |
// CREATE PPM FILE CONTENTS | |
let ppm = `P3\n${width} ${height}\n255`; | |
for (let x = 0; x < width; x++) { | |
ppm += `\n`; | |
for (let y = 0; y < height; y++) { | |
let pixel = (x*4) + (y*4*width); | |
ppm += `${image.data[pixel + 0]} ${image.data[pixel + 1]} ${image.data[pixel + 2]} ` | |
} | |
} | |
ppm += `\n`; | |
console.log(ppm); | |
}) | |
mdlr('mmzsource:rt-h5'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
mdlr('mmzsource:rt-h6', m => { | |
const style = `width:100vw; height:100vh;`; | |
const doc = document.body; | |
doc.innerHTML = `<canvas width=200 height=200 style="${style}"></canvas>`; | |
doc.style.margin = "0"; | |
doc.style.overflow = "hidden"; | |
const canvas = doc.querySelector('canvas'); | |
const { width, height } = canvas; | |
const ctx = canvas.getContext('2d'); | |
const image = ctx.getImageData(0, 0, width, height); | |
// module import problem ... moved stuff to raytrace (temporarily?) | |
// const { color, getRed, getGreen, getBlue } = m.require('mmzsource:canvas'); | |
const rt = m.require('mmzsource:raytrace'); | |
const rayOrigin = rt.point(0,0,-5); | |
const wallZ = 10; | |
const wallSize = 7; | |
const pixelSize = wallSize / width; // assuming width === height | |
const half = wallSize * 0.5 // assuming width === height | |
let material = rt.material(); | |
let shape = rt.sphere(); | |
material.color = rt.color(1,0.2,1); | |
shape.material = material; | |
const lightPosition = rt.point(-10,10,-10); | |
const lightColor = rt.color(1,1,1); | |
const light = rt.pointLight(lightPosition, lightColor); | |
function animate() { | |
for (let y = 0; y < height; y++) { | |
let worldY = half - pixelSize * y; | |
for (let x = 0; x < width; x++) { | |
const worldX = -half + pixelSize * x; | |
const position = rt.point(worldX, worldY, wallZ); | |
const r = rt.ray(rayOrigin, rt.norm(rt.sub(position, rayOrigin))); | |
const xs = rt.intersects(shape, r); | |
const h = rt.hit(xs); | |
if (h) { | |
const point = rt.position(r,h.t); | |
const normal = rt.normalAt(h.object, point); | |
const eye = rt.neg(r.direction); | |
const color = rt.lighting(h.object.material, light, point, eye, normal); | |
const pixel = (x*4)+(y*4*width); | |
image.data[pixel + 0] = rt.getRed(color) * 255; | |
image.data[pixel + 1] = rt.getGreen(color) * 255; | |
image.data[pixel + 2] = rt.getBlue(color) * 255; | |
image.data[pixel + 3] = 255; | |
} | |
} | |
} | |
ctx.putImageData(image, 0, 0); | |
// CREATE PPM FILE CONTENTS | |
let ppm = `P3\n${width} ${height}\n255`; | |
for (let x = 0; x < width; x++) { | |
ppm += `\n`; | |
for (let y = 0; y < height; y++) { | |
let pixel = (x*4) + (y*4*width); | |
ppm += `${image.data[pixel + 0]} ${image.data[pixel + 1]} ${image.data[pixel + 2]} ` | |
} | |
} | |
ppm += `\n`; | |
console.log(ppm); | |
} | |
requestAnimationFrame(animate); | |
}) | |
mdlr('mmzsource:rt-h6'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment