# Ray Tracing

## In this example below you will see how to do a Ray Tracing with some HTML / CSS and JavascriptMany errors has been corrected. Still in progress...

This awesome code was written by arcollector, you can see more from this user in the personal repository.
You can find the original code on Codepen.io
Copyright arcollector ©
• HTML
• CSS
• JavaScript
``````<!DOCTYPE html>
<html lang="en" >

<head>
<meta charset="UTF-8">
<title>Ray Tracing</title>

<link rel="stylesheet" href="css/style.css">

</head>

<body>

<div class="screen"></div>

<script  src="js/index.js"></script>

</body>

</html>
``````
``````
/*Downloaded from https://www.codeseek.co/arcollector/ray-tracing-gbRXPw */
html{
background:#000;
}
``````
``````
/*Downloaded from https://www.codeseek.co/arcollector/ray-tracing-gbRXPw */
Math.epsilon = 1e-6;
Math.sign2 = function(x) {
return Math.abs(x)<Math.epsilon?0:Math.sign(x);
};
Math.degToRad = function(angle) {
return angle/180*Math.PI;
};
Math.radToDeg = function(angle) {
return angle/Math.PI*180;
};
Math.round2 = function(x,padding) {
var sign = Math.sign2(x);
padding = padding || 0;
return parseInt(x+.5*sign)+padding*sign;
};
Math.floatCmp = function(a,b) {
return Math.sign2(Math.abs(a)-Math.abs(b)) === 0;
};
Math.five = function(x) {
x = Math.sign2(x)===0?0:x;
var tmp = x.toString().split( '.' );
return tmp[0] + '.' + (tmp[1] ? tmp[1].substring(0,5) : '');
};

// *******************************************
//
// *******************************************
var Vector = function( x,y,z,w ) {
this.x = x || 0;
this.y = y || 0;
this.z = z || 0;
this.w = typeof w === 'undefined' ? 1 : w;
return this;
};
Vector.prototype.length = function() {
var l = Math.sqrt( this.x*this.x + this.y*this.y + this.z*this.z );
return Math.sign2( l ) === 0 ? 0 : l;
};
Vector.prototype.normalize = function() {
var l = this.length();
if( l === 0 ) {
return this;
}
this.scalar( 1/l );
return this;
};
Vector.prototype.negate = function() {
this.scalar( -1 );
return this;
};
Vector.prototype.add = function( v ) {
this.x += v.x;
this.y += v.y;
this.z += v.z;
return this;
};
Vector.prototype.sub = function( v ) {
this.x -= v.x;
this.y -= v.y;
this.z -= v.z;
return this;
};
Vector.prototype.mul = function( v ) {
this.x *= v.x;
this.y *= v.y;
this.z *= v.z;
return this;
};
Vector.prototype.scalar = function( s ) {
this.x *= s;
this.y *= s;
this.z *= s;
return this;
};
Vector.prototype.dot = function( v ) {
return this.x*v.x + this.y*v.y + this.z*v.z;
};
Vector.prototype.cross = function( v ) {
var x = this.y*v.z - this.z*v.y;
var y = this.z*v.x - this.x*v.z;
var z = this.x*v.y - this.y*v.x;
return new Vector( x, y, z, 1 );
};
Vector.prototype.copy = function() {
return new Vector( this.x, this.y, this.z, this.w );
};
Vector.prototype.transform = function( m ) {
var x = this.x*m._00 + this.y*m._10 + this.z*m._20 + this.w*m._30;
var y = this.x*m._01 + this.y*m._11 + this.z*m._21 + this.w*m._31;
var z = this.x*m._02 + this.y*m._12 + this.z*m._22 + this.w*m._32;
var w = this.x*m._03 + this.y*m._13 + this.z*m._23 + this.w*m._33;

if( Math.sign2( w ) === 0 ) { // fuck this shit
w = Math.epsilon;
}

this.x = x/w;
this.y = y/w;
this.z = z/w;
this.w = 1;
return this;
};
Vector.prototype.equals = function( v ) {
return Math.floatCmp(this.x,v.x) && Math.floatCmp(this.y,v.y) && Math.floatCmp(this.z,v.z);
};
Vector.prototype.toString2 = function() {
return "Purple,Point[" +
"{" + ((Math.sign2(this.x)===0)?0:Math.five(this.x)) + "," + ((Math.sign2(this.y)===0)?0:Math.five(this.y)) + "," + ((Math.sign2(this.z)===0)?0:Math.five(this.z)) + "}" +
"],";
};
//
Vector.fromP1toP2 = function( v1, v2 ) {
var x = v2.x - v1.x;
var y = v2.y - v1.y;
var z = v2.z - v1.z;
return new Vector( x, y, z, 1 );
};
Vector.substraction = function(v1,v2) {
return new Vector(
v1.x - v2.x,
v1.y - v2.y,
v1.z - v2.z
);
};
Vector.scalarMultiplication = function( v, scalar ) {
return new Vector(
v.x*scalar,
v.y*scalar,
v.z*scalar
);
};
Vector.vectorMultiplication = function( v1, v2 ) {
return new Vector(
v1.x*v2.x,
v1.y*v2.y,
v1.z*v2.z
);
};
Vector.addition = function( v1, v2 ) {
return new Vector(
v1.x+v2.x,
v1.y+v2.y,
v1.z+v2.z
);
};
Vector.projectV1OntoV2 = function( v1, v2 ) {

var kvLength = v1.dot( v2 );
if( Math.sign2( kvLength ) === 0 ) { // v1 and v2 are normal vectors
return null;
}

// kv is the parallel vector to v2
var kvOrientation = v2.copy().normalize();
kvLength = kvLength/v2.length();
var kv = new Vector(
kvOrientation.x*kvLength,
kvOrientation.y*kvLength,
kvOrientation.z*kvLength
);

// u is the orthogonal vector of v2
var u = Vector.substraction( v1, kv );

console.assert(Math.sign2(u.dot(kv))==0,"u not normal to kv");
console.assert(Math.sign2(u.dot(v2))==0,"u not normal to v2");

return {
kv: kv,
u: u
};
};
Vector.prototype.angleWith = function(v2) {
var l1 = this.length();
var l2 = v2.length();
if( l1 === 0 || l2 === 0 ) {
return null;
}
return Math.acos( this.dot( v2 ) / ( l1*l2 ) );
};

// *******************************************
//
// *******************************************
var Matrix = function(v1,v2,v3,v4) { // rows vectors
v1 = v1 || new Vector(1,0,0,0);
v2 = v2 || new Vector(0,1,0,0);
v3 = v3 || new Vector(0,0,1,0);
v4 = v4 || new Vector(0,0,0,1);

this._00 = v1.x; this._01 = v1.y; this._02 = v1.z; this._03 = v1.w;
this._10 = v2.x; this._11 = v2.y; this._12 = v2.z; this._13 = v2.w;
this._20 = v3.x; this._21 = v3.y; this._22 = v3.z; this._23 = v3.w;
this._30 = v4.x; this._31 = v4.y; this._32 = v4.z; this._33 = v4.w;

return this;
};
Matrix.prototype.multiply = function(m) {
var _00 = this._00*m._00 + this._01*m._10 + this._02*m._20 + this._03*m._30;
var _01 = this._00*m._01 + this._01*m._11 + this._02*m._21 + this._03*m._31;
var _02 = this._00*m._02 + this._01*m._12 + this._02*m._22 + this._03*m._32;
var _03 = this._00*m._03 + this._01*m._13 + this._02*m._23 + this._03*m._33;

var _10 = this._10*m._00 + this._11*m._10 + this._12*m._20 + this._13*m._30;
var _11 = this._10*m._01 + this._11*m._11 + this._12*m._21 + this._13*m._31;
var _12 = this._10*m._02 + this._11*m._12 + this._12*m._22 + this._13*m._32;
var _13 = this._10*m._03 + this._11*m._13 + this._12*m._23 + this._13*m._33;

var _20 = this._20*m._00 + this._21*m._10 + this._22*m._20 + this._23*m._30;
var _21 = this._20*m._01 + this._21*m._11 + this._22*m._21 + this._23*m._31;
var _22 = this._20*m._02 + this._21*m._12 + this._22*m._22 + this._23*m._32;
var _23 = this._20*m._03 + this._21*m._13 + this._22*m._23 + this._23*m._33;

var _30 = this._30*m._00 + this._31*m._10 + this._32*m._20 + this._33*m._30;
var _31 = this._30*m._01 + this._31*m._11 + this._32*m._21 + this._33*m._31;
var _32 = this._30*m._02 + this._31*m._12 + this._32*m._22 + this._33*m._32;
var _33 = this._30*m._03 + this._31*m._13 + this._32*m._23 + this._33*m._33;

this._00 = _00; this._01 = _01; this._02 = _02; this._03 = _03;
this._10 = _10; this._11 = _11; this._12 = _12; this._13 = _13;
this._20 = _20; this._21 = _21; this._22 = _22; this._23 = _23;
this._30 = _30; this._31 = _31; this._32 = _32; this._33 = _33;

return this;
};
Matrix.prototype.transpose = function() {
return new Matrix(
new Vector(this._00,this._10,this._20,this._30),
new Vector(this._01,this._11,this._21,this._31),
new Vector(this._02,this._12,this._22,this._32),
new Vector(this._03,this._13,this._23,this._33)
);
};
Matrix.prototype.copy = function() {
return new Matrix(
new Vector(this._00,this._01,this._02,this._03),
new Vector(this._10,this._11,this._12,this._13),
new Vector(this._20,this._21,this._22,this._23),
new Vector(this._30,this._31,this._32,this._33)
);
};
Matrix.prototype.toString2 = function() {
return "mat={\n" +
"{" + Math.five(this._00) + "," + Math.five(this._01) + "," + Math.five(this._02) + "," + Math.five(this._03) + "},\n" +
"{" + Math.five(this._10) + "," + Math.five(this._11) + "," + Math.five(this._12) + "," + Math.five(this._13) + "},\n" +
"{" + Math.five(this._20) + "," + Math.five(this._21) + "," + Math.five(this._22) + "," + Math.five(this._23) + "},\n" +
"{" + Math.five(this._30) + "," + Math.five(this._31) + "," + Math.five(this._32) + "," + Math.five(this._33) + "}\n" +
"}";
};
// some constants
Matrix.TRANSLATION = 0;
Matrix.SCALING = 1;
Matrix.ROTATION = 2;

// *******************************************
// TRANSFORMATIONS
// *******************************************
var ProjectionWithXYPlane = function( centerOfProjection ) { // point must be in the negative z axis halfspace!!
this.centerOfProjection = centerOfProjection.copy();
this.viewerDirection = centerOfProjection.copy().negate().normalize();
this.m = new Matrix(
new Vector(-centerOfProjection.z,0,0,0),
new Vector(0,-centerOfProjection.z,0,0),
new Vector(centerOfProjection.x,centerOfProjection.y,1,1),
new Vector(0,0,0,-centerOfProjection.z)
);
this.inverse = new Matrix(
new Vector(1/-centerOfProjection.z,0,0,0),
new Vector(0,1/-centerOfProjection.z,0,0),
new Vector(centerOfProjection.x/centerOfProjection.z,centerOfProjection.y/centerOfProjection.z,1,1/centerOfProjection.z),
new Vector(0,0,0,1/-centerOfProjection.z)
);
return this;
};
var YawRotation = function( angleInDeg, point ) { //ccw rotation, y-axis as pivot, looking toward negative y axis in LHS
var rad = Math.degToRad(angleInDeg);
var cos = Math.cos(rad);
var sin = Math.sin(rad);

var r = new Matrix();
r._00 = cos;
r._20 = -sin;
r._02 = sin;
r._22 = cos;

var inverse = r.transpose();

if( point ) {
var t = new Translation(-point.x,-point.y,-point.z);
this.m = new Matrix();
this.m.multiply(t.m); this.m.multiply(r); this.m.multiply(t.inverse);
this.inverse = new Matrix();
this.inverse.multiply(t.inverse); this.inverse.multiply(inverse); this.inverse.multiply(t.m);
} else {
this.m = r;
this.inverse = inverse;
}

this.type = Matrix.ROTATION;
};
var PitchRotation = function( angleInDeg, point ) { //ccw rotation, x-axis as pivot, looking toward negative x axis in LHS
var rad = Math.degToRad(angleInDeg);
var cos = Math.cos(rad);
var sin = Math.sin(rad);

var r = new Matrix();
r._11 = cos;
r._21 = sin;
r._12 = -sin;
r._22 = cos;
var inverse = r.transpose();

if( point ) {
var t = new Translation(-point.x,-point.y,-point.z);
this.m = new Matrix();
this.m.multiply(t.m); this.m.multiply(r); this.m.multiply(t.inverse);
this.inverse = new Matrix();
this.inverse.multiply(t.inverse); this.inverse.multiply(inverse); this.inverse.multiply(t.m);
} else {
this.m = r;
this.inverse = inverse;
}

this.type = Matrix.ROTATION;
};
var RollRotation = function( angleInDeg, point) { // ccw rotation, z-axis as pivot, looking toward negative positive z axis in LHS
var rad = Math.degToRad(angleInDeg);
var cos = Math.cos(rad);
var sin = Math.sin(rad);

var r = new Matrix();
r._00 = cos;
r._10 = -sin;
r._01 = sin;
r._11 = cos;

var inverse = r.transpose();

if( point ) {
var t = new Translation(-point.x,-point.y,-point.z);
this.m = new Matrix();
this.m.multiply(t.m); this.m.multiply(r); this.m.multiply(t.inverse);
this.inverse = new Matrix();
this.inverse.multiply(t.inverse); this.inverse.multiply(inverse); this.inverse.multiply(t.m);
} else {
this.m = r;
this.inverse = inverse;
}

this.type = Matrix.ROTATION;
};
var Translation = function(tx,ty,tz) {
this.m = new Matrix();
this.m._30 = tx;
this.m._31 = ty;
this.m._32 = tz;

this.inverse = new Matrix();
this.inverse._30 = -tx;
this.inverse._31 = -ty;
this.inverse._32 = -tz;

this.type = Matrix.TRANSLATION;
};
var Scaling = function(sx,sy,sz,point) {
var s = new Matrix();
s._00 = sx;
s._11 = sy;
s._22 = sz;

var inverse = new Matrix();
inverse._00 = 1/sx;
inverse._11 = 1/sy;
inverse._22 = 1/sz;

if( point ) {
var t = new Translation(-point.x,-point.y,-point.z);
this.m = new Matrix();
this.m.multiply(t.m); this.m.multiply(s); this.m.multiply(t.inverse);
this.inverse = new Matrix();
this.inverse.multiply(t.inverse); this.inverse.multiply(inverse); this.inverse.multiply(t.m);
} else {
this.m = s;
this.inverse = inverse;
}

this.type = Matrix.SCALING;
};
var RotationAAAAxis = function( cosine, angle ) { // cosine must be normalize

var a = Math.sqrt(cosine.y*cosine.y + cosine.z*cosine.z);
if( a <= Math.epsilon ) {
var tmp = new PitchRotation( angle );
this.m = tmp.m;
this.inverse = tmp.inverse;
return;
}

// put axis onto the x-z plane
var cos = cosine.z/a;
var sin = cosine.y/a;
var rX = new Matrix(
new Vector(1,0,0,0),
new Vector(0,cos,sin,0),
new Vector(0,-sin,cos,0),
new Vector(0,0,0,1)
);
var rX_Inverse = rX.transpose();

// put axis onto +z axis direction
var cos = a;
var sin = cosine.x;
var rY = new Matrix(
new Vector(cos,0,sin,0),
new Vector(0,1,0,0),
new Vector(-sin,0,cos,0),
new Vector(0,0,0,1)
);
var rY_Inverse = rY.transpose();

var rZ = new RollRotation( angle ); // ccw rotation

this.m = new Matrix();
this.m.multiply( rX );
this.m.multiply( rY );
this.m.multiply( rZ.m );
this.m.multiply( rY_Inverse );
this.m.multiply( rX_Inverse );

this.inverse = this.m.transpose();

this.type = Matrix.ROTATION;
};
// *******************************************
// WINDOWING
// *******************************************
var calcWindowingMatrix = function( minS,maxS, minT,maxT ) {

var tmp = (maxS.x - minS.x);
var a = Math.sign2(tmp) === 0 ? 1 : (maxT.x - minT.x) / tmp;
tmp = (maxS.y - minS.y)
var b = Math.sign2(tmp) === 0 ? 1 : (maxT.y - minT.y) / tmp;
tmp = (maxS.z - minS.z);
var e = Math.sign2(tmp) === 0 ? 1 : (maxT.z - minT.z) / tmp;
var c = -minS.x*a+minT.x;
var d = -minS.y*b+minT.y;
var f = -minS.z*e+minT.z;

var m = new Matrix(
new Vector(a,0,0,0),
new Vector(0,b,0,0),
new Vector(0,0,e,0),
new Vector(c,d,f,1)
);

var inverse = new Matrix(
new Vector(1/a,0,0,0),
new Vector(0,1/b,0,0),
new Vector(0,0,1/e,0),
new Vector(-c/a,-d/b,-f/e,1)
);

return {
minS: minS.copy(), maxS: maxS.copy(),
minT: minT.copy(), maxT: maxT.copy(),
m: m,
inverse: inverse,
rectSource: {
left: minS.x,
right: maxS.x,
bottom: minS.y,
top: maxS.y,
size: Math.abs((minS.x-maxS.x)*(minS.y-maxS.y))
},
rectTarget: {
left: minT.x,
right: maxT.x,
bottom: minT.y,
top: maxT.y,
size: Math.abs((minT.x-maxT.x)*(minT.y-maxT.y))
}
};
};

// *******************************************
// CAMERA
// *******************************************
var Camera = function( vrp, vpn, vup, viewDistance ) { // I am in LHS!!

if( Math.sign2( vpn.length() ) === 0 ) {
console.error( 'bad view plane normal' );
}

if( Math.sign2( vup.x ) === 0 && Math.sign2( vup.y ) === 0 ) {
console.error( 'bad view up plane direction' );
}

this.vrp = vrp.copy();
this.vpn = vpn.copy().normalize();
this.vup = vup.copy();
this.viewDistance = viewDistance;

this.m = null;
this.inverse = null;
this._translationMatrix = null;
this._translationMatrixInverse = null;
this._rotationMatrix = null;
this._rotationMatrixInverse = null;

this.hasChange = true;
return this;
};
Camera.prototype.update = function() {

if( !this.hasChange ) {
this.m = new Matrix();
this.inverse = new Matrix();
this._translationMatrix = new Matrix();
this._translationMatrixInverse = new Matrix();
this._rotationMatrix = new Matrix();
this._rotationMatrixInverse = new Matrix();
return;
}
this.hasChange = false;

// translate view plane to the origin
/*
[ 1 0 0 0 ]
[ 0 1 0 0 ]
[ 0 0 1 0 ]
[ -vrp.x+vpn.x*viewDistance -vrp.y+vpn.y*viewDistance -vrp.z+vpn.z*viewDistance 1 ]
*/
var t = new Translation(
-this.vrp.x+this.vpn.x*this.viewDistance,
-this.vrp.y+this.vpn.y*this.viewDistance,
-this.vrp.z+this.vpn.z*this.viewDistance
);
// store this matrix for special uses cases
this._translationMatrix = t.m.copy();
this._translationMatrixInverse = t.inverse.copy();

var b = Math.sqrt( this.vpn.y*this.vpn.y + this.vpn.z*this.vpn.z );
var rX = new Matrix();
if( Math.sign2( b ) !== 0 ) { // check that vpn is not parallel to the x axis
// put the view plane normal onto the x-z plane
/*
[ 1 0 0 0 ]
[ 0 vpn.z/b vpn.y/b 0 ]
[ 0 -vpn.y/b vpn.z/b 0 ]
[ 0 0 0 1 ]
*/
var cos = this.vpn.z/b;
var sin = this.vpn.y/b;
rX._11 = cos;
rX._12 = sin;
rX._21 = -sin;
rX._22 = cos;
}

// align the view plane normal z component with the +z axis
/*
[ b 0 vpn.x 0 ]
[ 0 1 0 0 ]
[ -vpn.x 0 b 0 ]
[ 0 0 0 1 ]
*/
var rY = new Matrix();
rY._00 = b;
rY._02 = this.vpn.x;
rY._20 = -this.vpn.x;
rY._22 = b;

// apply rotation transformations to the vup vector
var vup = this.vup.copy();
vup.transform( rX );
vup.transform( rY );

// align vup y component with the +y axis using its projection onto x-y plane
var e = Math.sqrt( vup.x*vup.x + vup.y*vup.y );
if( Math.sign2( e ) === 0 ) {
console.log( 'view up direction along view plane normal' );
}
/*
[ vup.y/e vup.x/e 0 0 ]
[ -vup.x/e vup.y/e 0 0 ]
[ 0 0 1 0 ]
[ 0 0 0 1 ]
*/
var rZ = new Matrix();
var cos = vup.y/e;
var sin = vup.x/e;
rZ._00 = cos;
rZ._01 = sin;
rZ._10 = -sin;
rZ._11 = cos;

var rotationMatrix = new Matrix();
rotationMatrix.multiply( rX );
rotationMatrix.multiply( rY );
rotationMatrix.multiply( rZ );
var rotationMatrixInverse = rotationMatrix.transpose();

// build the camera matrix
this.m = new Matrix();
this.m.multiply( t.m );
this.m.multiply( rotationMatrix );

this.inverse = new Matrix();
this.inverse.multiply( rotationMatrixInverse );
this.inverse.multiply( t.inverse );

// store this matrix for special uses cases
this._rotationMatrix = rotationMatrix;
this._rotationMatrixInverse = rotationMatrixInverse;
};
Camera.prototype.getTranslationVector = function() {
return new Vector( this._translationMatrix._30, this._translationMatrix._31, this._translationMatrix._32 );
};
Camera.prototype.getTranslationMatrix = function() {
return { m: this._translationMatrix.copy(), inverse: this._translationMatrixInverse.copy(), type: Matrix.TRANSLATION };
};
Camera.prototype.getRotationMatrix = function() {
return { m: this._rotationMatrix.copy(), inverse: this._rotationMatrixInverse.copy(), type: Matrix.ROTATION };
};

// *******************************************
//
// *******************************************
var FrameBuffer = {
SCREEN_WIDTH: null,
SCREEN_HEIGHT: null,
SIZE: null,
pixels: null,
backgroundColor: null,

init: function( options ) {
this.\$table = document.createElement('table');
this.\$table.style.borderCollapse = 'collapse';
this.\$table.style.margin = '0 auto';
this.\$table.addEventListener( 'mousedown', this._cancelEvent, false );

this.SCREEN_WIDTH = options.width;
this.SCREEN_HEIGHT = options.height;
this.SIZE = options.width*options.height;

this.backgroundColor = new Vector( options.backgroundColor[0], options.backgroundColor[1], options.backgroundColor[2] );

this.pixels = [];

var pixelSize = options.pixelSize + 'px';

for(var y = 0; y < FrameBuffer.SCREEN_HEIGHT; y++) {
var \$tr = document.createElement( 'tr' );
for(var x = 0; x < FrameBuffer.SCREEN_WIDTH; x++) {
var \$td = document.createElement('td');
\$td.style.width = pixelSize;
\$td.style.height = pixelSize;
\$td.setAttribute( 'title','(x,y)'.replace( 'x',x ).replace( 'y',FrameBuffer.SCREEN_HEIGHT-1-y ) );
\$tr.appendChild( \$td );
this.pixels.push( \$td );
}
this.\$table.appendChild( \$tr );
}

this.\$container = document.querySelector( options.screenSelector );
this.\$container.appendChild( this.\$table );
this.\$container.style.margin = '0 auto';
this.\$container.style.border = '1px solid #ccc';
this.\$container.style.width = this.\$table.offsetWidth + 'px';
this.\$container.style.height = this.\$table.offsetHeight + 'px';

this.clear();
},

calcAddress: function( x, y ) {
return y*this.SCREEN_WIDTH+x;
},

hLine: function( xs, xe, y, color ) {
var address = this.calcAddress( xs, y );
for( ; xs <= xe; xs++ ) {
this.pixels[address].style.backgroundColor = color;
address++;
}
},

plot: function(x,y,color) {
this.pixels[this.calcAddress(x,y)].style.backgroundColor = color;
},

hide: function() {
//this.\$table.style.display = 'none';
},

show: function() {
this.\$table.style.display = 'show';
},

clear: function() {
var color = Illumination.vectorToRGB( this.backgroundColor );
for( var i = 0; i < this.SIZE; i++ ) {
this.pixels[i].style.backgroundColor = color;
}
},

_cancelEvent: function( e ) {
e.preventDefault();
},
};

// *******************************
// GEOMETRY
// *******************************
var _Geometry = function() {};
_Geometry.bindMembers = function( classObject ) {

};
// *******************************
// RAY
// *******************************
var Ray = function( start, direction, normalized ) {
this.start = start.copy();
this.direction = direction ? direction.copy() : null;
direction && normalized && this.direction.normalize();
};
Ray.prototype = {
calcDirection: function( p, normalized ) {
this.direction = new Vector(
p.x - this.start.x,
p.y - this.start.y,
p.z - this.start.z
);
normalized && this.direction.normalize();
},
getPointAt: function( t ) {
return new Vector(
this.start.x + this.direction.x*t,
this.start.y + this.direction.y*t,
this.start.z + this.direction.z*t
);
},
copy: function() {
return new Ray( this.start, this.direction );
},
transform: function( mPoint, mVector ) {
mPoint && this.start.transform( mPoint );
mVector && this.direction.transform( mVector );
},
addOffset: function( t ) {
this.start = this.getPointAt( t );
},
toString2: function( t ) {
var str = [];
var end = this.direction.copy().scalar( t || 1 ).add( this.start );
str.push( 'Red,Line[{{sx,sy,sz},{dx,dy,dz}}],'.replace( 'sx', Math.five(this.start.x) ).replace( 'sy', Math.five(this.start.y) ).replace( 'sz', Math.five(this.start.z) ).replace( 'dx', Math.five(end.x) ).replace( 'dy', Math.five(end.y) ).replace( 'dz', Math.five(end.z) ) );
return str.join( '' );
},
};
// *******************************
// SPHERE
// *******************************
var Sphere = function( options ) {
this.color = options.color || new Vector(255,255,255); // white
this.colorRGBStr = Illumination.intensityToRGB( this.color );

this.diffuseness = Illumination.rgbToIntensity( this.color );
this.specularity = typeof options.specularity === 'undefined' ? 0 : options.specularity; // akas: shininess
this.refraction = typeof options.refraction === 'undefined' ? 0 : options.refraction;

this.radius = options.radius || 1;
this.radiusSquare = this.radius*this.radius;
this.radiusInverse = 1/this.radius;

this.matrixStackPoint = [];
this.matrixStackVector = [];
};
Sphere.prototype = {
_buildTransformation: function() {
this.mPoint = new Matrix();
this.invPoint = new Matrix();
for( var l = this.matrixStackPoint.length, i = 0, j = l-1; i < l; i++, j-- ) {
this.mPoint.multiply( this.matrixStackPoint[i].m );
this.invPoint.multiply( this.matrixStackPoint[j].inverse );
}
this.mVector = new Matrix();
this.invVector = new Matrix();
for( var l = this.matrixStackVector.length, i = 0, j = l-1; i < l; i++, j-- ) {
this.mVector.multiply( this.matrixStackVector[i].m );
this.invVector.multiply( this.matrixStackVector[j].inverse );
}
},
transform: function( transformation ) {
if( transformation.type === Matrix.SCALING ) {
return; // avoid scaling, uses the radius property
}
if( transformation.type === Matrix.TRANSLATION ) {
this.matrixStackPoint.push( transformation );
} else {
this.matrixStackVector.push( transformation );
this.matrixStackPoint.push( transformation );
}
},
calcHitValues: function( ray ) {
ray = ray.copy();
ray.transform( this.invPoint, this.invVector ); // to unit sphere

var b = ray.start.dot( ray.direction );
var c = ray.start.dot( ray.start ) - this.radiusSquare;

var tmp = Math.pow(b,2) - c;
if( Math.sign2( tmp ) <= 0 ) {
return null;
}
tmp = Math.sqrt( tmp );
var tPlus = (-b + tmp);
var tMinus = (-b - tmp);
if( Math.sign2( tPlus ) <= 0 || Math.sign2( tMinus ) <= 0 ) {
return null;
}
return [ tMinus, tPlus ];
},
calcNormal: function( p, normalized ) {
var center = new Vector(0,0,0);
center.transform( this.mPoint );
var normal = Vector.fromP1toP2( center, p );
normalized && normal.scalar( this.radiusInverse );
return normal;
},

getDiffuseness: function( p ) {
return this.diffuseness;
},
};
_Geometry.bindMembers( Sphere );
// *******************************
// (CHECKER)BOARD
// *******************************
var Board = function( options ) {
this.tilesX = options.tilesX || 5;
this.tilesZ = options.tilesZ || 5;
this.tileSize = options.tileSize || 10;

this.scaleX = options.scaleX || 5;
this.scaleZ = options.scaleZ || 5;
this.titleSize = options.tileSize || 10;

this.colorOn = options.colorOn || new Vector(0,0,0); // black
this.colorOff = options.colorOff || new Vector(255,255,255); // white
this.colorOnIntensity = Illumination.rgbToIntensity( this.colorOn ); // diffuseness
this.colorOffIntensity = Illumination.rgbToIntensity( this.colorOff ); // diffuseness
this.specularity = typeof options.specularity === 'undefined' ? 0 : options.specularity; // akas: shininess
this.refraction = typeof options.refraction === 'undefined' ? 0 : options.refraction;

this.matrixStackPoint = [];
this.matrixStackVector = [];
};
Board.prototype = {
_buildTransformation: function() {
// from arbitrary board to unit board
var toArbitrary = new Scaling(this.tileSize,1,this.tileSize);
var toArbitrary = new Scaling(this.scaleX,1,this.scaleZ);
var toUnit = toArbitrary.inverse;
toArbitrary = toArbitrary.m;

this.mPoint = new Matrix();
this.invPoint = new Matrix();
// from unit board to arbitrary board
this.mPoint.multiply( toArbitrary );
for( var l = this.matrixStackPoint.length, i = 0, j = l-1; i < l; i++, j-- ) { // user transformations
this.mPoint.multiply( this.matrixStackPoint[i].m );
this.invPoint.multiply( this.matrixStackPoint[j].inverse );
}
// from arbitrary board to unit board
this.invPoint.multiply( toUnit );

// repeat process now for vector transformation matrix
this.mVector = new Matrix();
this.invVector = new Matrix();
this.mVector.multiply( toArbitrary );
for( var l = this.matrixStackVector.length, i = 0, j = l-1; i < l; i++, j-- ) {
this.mVector.multiply( this.matrixStackVector[i].m );
this.invVector.multiply( this.matrixStackVector[j].inverse );
}
this.invVector.multiply( toUnit );
},
transform: function( transformation ) {
if( transformation.type === Matrix.SCALING ) { // avoid scaling
return; // use tileSize property
}
if( transformation.type === Matrix.TRANSLATION ) {
this.matrixStackPoint.push( transformation );
} else {
this.matrixStackVector.push( transformation );
this.matrixStackPoint.push( transformation );
}
},
calcHitValues: function( ray ) {
ray = ray.copy();
ray.transform( this.invPoint, this.invVector ); // undo transformations

var n = new Vector(0,1,0);
var denominator = n.dot( ray.direction );
if( Math.sign2( denominator ) === 0 ) {
return null;
}
var numerator = -n.dot( ray.start );
var t = numerator/denominator;
if( Math.sign2( t ) <= 0 ) {
return null;
}
var p = ray.getPointAt( t ); // compute intersection
if( p.x < -1 || p.x > 1 || p.z < -1 || p.z > 1 ) { // outside
return null;
}
return [ t, t ];
},
calcNormal: function( p, normalized ) {
var normal = new Vector(0,1,0);
normal.transform( this.mVector );
normalized && normal.normalize();
return normal;
},
getDiffuseness: function( p ) {
p = p.copy();
p.transform( this.invPoint );
var tileZ = 1;
var tileX = 1;
var offsetX = this.tileSize/this.scaleX;
var offsetZ = this.tileSize/this.scaleZ;
var minX = -1;
var minZ = -1;
var maxX = minX+offsetX;
var maxZ = minZ+offsetZ;
var tilesInX = this.scaleX*2/this.tileSize;
var tilesInZ = this.scaleZ*2/this.tileSize;
for( var z = 0; z < tilesInZ; z++ ) {
for( var x = 0; x < tilesInX; x++ ) {
if( (p.x >= minX||Math.floatCmp(p.x,minX)) && (p.x <= maxX||Math.floatCmp(p.x,maxX)) && (p.z >= minZ||Math.floatCmp(p.z,minZ)) && (p.z <= maxZ||Math.floatCmp(p.z, maxZ)) ) {
if( tileZ % 2 === 1 ) { // is odd
if( tileX % 2 === 1 ) { // is odd
return this.colorOnIntensity;
} else { // is even
return this.colorOffIntensity;
}
} else { // is even
if( tileX % 2 === 0 ) { // is even
return this.colorOnIntensity;
} else { // is odd
return this.colorOffIntensity;
}
}
}
minX += offsetX;
maxX += offsetX;
tileX++;
}
minX = -1;
maxX = minX+offsetX;
minZ += offsetZ;
maxZ += offsetZ;
tileX = 1;
tileZ++;
}
throw Error( 'checkerboard missing color calculation' );
},
};
_Geometry.bindMembers( Board );
// *******************************
// BOX
// *******************************
var Box = function( options ) {
this.color = options.color || new Vector(255,255,255); // white
this.colorRGBStr = Illumination.intensityToRGB( this.color );

this.scaleX = options.scaleX || 1;
this.scaleY = options.scaleY || 1;
this.scaleZ = options.scaleZ || 1;

this.diffuseness = Illumination.rgbToIntensity( this.color );
this.specularity = typeof options.specularity === 'undefined' ? 0 : options.specularity; // akas: shininess
this.refraction = typeof options.refraction === 'undefined' ? 0 : options.refraction;

this.matrixStackPoint = [];
this.matrixStackVector = [];

this.normals = [
new Vector(0,0,-1), // front
new Vector(0,0,1), // back
new Vector(-1,0,0), // left
new Vector(1,0,0), // right
new Vector(0,1,0), // top
new Vector(0,-1,0), // bottom
];
};
Box.prototype = {
_buildTransformation: function() {
// from arbitrary box to unit box
var toArbitrary = new Scaling(this.scaleX,this.scaleY,this.scaleZ);
var toUnit = toArbitrary.inverse;
toArbitrary = toArbitrary.m;

this.mPoint = new Matrix();
this.invPoint = new Matrix();
// from unit box to arbitrary box
this.mPoint.multiply( toArbitrary );
for( var l = this.matrixStackPoint.length, i = 0, j = l-1; i < l; i++, j-- ) { // user transformations
this.mPoint.multiply( this.matrixStackPoint[i].m );
this.invPoint.multiply( this.matrixStackPoint[j].inverse );
}
// from arbitrary box to unit box
this.invPoint.multiply( toUnit );

// repeat process now for vector transformation matrix
this.mVector = new Matrix();
this.invVector = new Matrix();
this.mVector.multiply( toArbitrary );
for( var l = this.matrixStackVector.length, i = 0, j = l-1; i < l; i++, j-- ) {
this.mVector.multiply( this.matrixStackVector[i].m );
this.invVector.multiply( this.matrixStackVector[j].inverse );
}
this.invVector.multiply( toUnit );
},
transform: function( transformation ) {
if( transformation.type === Matrix.SCALING ) { // avoid scaling
return;
}
if( transformation.type === Matrix.TRANSLATION ) {
this.matrixStackPoint.push( transformation );
} else {
this.matrixStackVector.push( transformation );
this.matrixStackPoint.push( transformation );
}
},
calcHitValues: function( ray ) {
ray = ray.copy();
ray.transform( this.invPoint, this.invVector ); // undo transformations

var tOut = Number.POSITIVE_INFINITY;
var tIn = Number.NEGATIVE_INFINITY;
for( var i = 0; i < 6; i++ ) {
var n = this.normals[i];
var denominator = n.dot( ray.direction );
var denominatorSign = Math.sign2( denominator );
if( denominatorSign === 0 ) { // parallel
continue;
}
var numerator = -n.dot( ray.start ) +1;
var t = numerator/denominator;
if( denominatorSign > 0 ) {
tOut = Math.min( tOut, t );
} else {
tIn = Math.max( tIn, t );
}
}
if( Math.sign2( tOut ) <= 0 || Math.sign2( tIn ) <= 0 ) {
return null;
}
if( tOut < tIn ) {
return null;
}
var p = ray.getPointAt( tIn ); // compute intersection
if( p.x < -1.01 || p.x > 1.01 || p.y < -1.01 || p.y > 1.01 || p.z < -1.01 || p.z > 1.01 ) { // outside
return null;
}
return [ tIn, tOut ];
},
calcIntersection: function( hitValue, ray ) {
return ray.getPointAt( hitValue );
},
calcNormal: function( p, normalized ) {
p = p.copy();
p.transform( this.invPoint );
var normal;
for( var i = 0; i < 6; i++ ) {
var n = this.normals[i];
if( Math.sign2(n.dot( p )-1) === 0 ) {
normal = n.copy().transform( this.mVector );
break;
}
}
if( !normal ) {
throw Error( 'box cannot find normal at point' );
}
normalized && normal.normalize();
return normal;
},
getDiffuseness: function( p ) {
return this.diffuseness;
},
};

// *******************************
// RAY TRACING
// *******************************
var Scene = function( options ) {

this.theIllumination = options.theIllumination;
this.theCamera = options.theCamera;
this.centerOfProjection = options.centerOfProjection;
this.theWindowing = options.theWindowing;

this.backgroundColor = options.backgroundColor || new Vector(0,0,0);
this.backgroundColorRGB = Illumination.vectorToRGB( this.backgroundColor );
this.backgroundIntensity = Illumination.rgbToIntensity( this.backgroundColor );

this.ambientColor = options.ambientColor || new Vector(50,50,50); // gray
this.ambientColorRGB = Illumination.vectorToRGB( this.ambientColor );
this.ambientIntensity = Illumination.rgbToIntensity( this.ambientColor );

// TODO: clean up
this.highLightColor = new Vector(255,255,255);
this.highLightColorRGB = Illumination.vectorToRGB( this.highLightColor );
this.highLightColorIntensity = Illumination.rgbToIntensity( this.highLightColor );

this._objects = [];
this._lights = [];
};
Scene.prototype = {
addObject: function( geometry ) {
var id = this._objects.length;
this._objects.push( geometry );
return id;
},
removeObject: function( id ) {
this._objects.splice( id, 1 );
},
addLight: function( light ) {
var id = this._lights.length;
this._lights.push( light );
return id;
},
removeLight: function( id ) {
this._lights.splice( id, 1 );
},

_prepareObjects: function() {
for( var i = 0, l = this._objects.length; i < l; i++ ) {
var theObject = this._objects[i];
theObject.transform( this.theCamera.getTranslationMatrix() );
theObject.transform( this.theCamera.getRotationMatrix() );
theObject._buildTransformation();
}
},
_prepareLights: function() {
for( var i = 0, l = this._lights.length; i < l; i++ ) {
var theLightSource = this._lights[i];
theLightSource.transform( this.theCamera.m );
}
},

render: function() {
this.theCamera.update();
this._prepareObjects();
this._prepareLights();

this._start();
},

_calcReflectedVector: function( incidentVector, surfaceNormal ) {
var baseLength = -2*surfaceNormal.dot( incidentVector );
var baseVector = Vector.scalarMultiplication( surfaceNormal, baseLength );
var reflectedVector = baseVector.add( incidentVector );
return reflectedVector;
},

// N = surface normal
// L = light direction
// f = refraction ration
// R = -N*(f*L(dot)N) + Math.sqrt(1-f^2*(1-(L(dot)N)^2) + f*L
_calcRefractedVector: function( refractionRatio, surfaceNormal, incidentVector ) {
var cos = surfaceNormal.dot( incidentVector );
var radicand = 1 - refractionRatio*refractionRatio*( 1 - cos*cos );
if( Math.sign2( radicand ) < 0 ) { // total internal reflection
return this._calcReflectedVector( incidentVector, surfaceNormal );
}

var term1 = Vector.scalarMultiplication( surfaceNormal, refractionRatio*cos + Math.sqrt( radicand ) );
term1.negate();
var term2 = Vector.scalarMultiplication( incidentVector, refractionRatio );
var refractedVector = term1.add( term2 );
return refractedVector;
},

_calcRefractedRay: function( object, surfacePoint, surfaceNormal, incidentVector ) {
var incidentIndex = 1;
var refractiveIndex = 1.4;

// calc entry refracted vector
var ratio = incidentIndex/refractiveIndex;
var refractedVector = this._calcRefractedVector( ratio, surfaceNormal, incidentVector );
if( Math.sign2( refractedVector.dot( surfaceNormal ) ) >= 0 ) { // refracted vector is pointing outside
var outerRay = new Ray( surfacePoint, refractedVector );
outerRay.addOffset( .05 );
return outerRay;
}

var inverseRatio = 1/ratio;
var refractionsCounter = 20;
while( refractionsCounter-- > 0 ) {
// build the ray
var refractedRay = new Ray( surfacePoint, refractedVector );
refractedRay.addOffset( -.05 ); // back off a little to ensure to positive intersections
var hitValues = object.calcHitValues( refractedRay );
if( !hitValues ) {
throw Error( 'refraction ray error!! fail at calculating the hit values missing' );
}
surfacePoint = refractedRay.getPointAt( hitValues[1] ); // outer point

// calc outer refracted vector
surfaceNormal = object.calcNormal( surfacePoint, true ); // surface at outer point
surfaceNormal.negate(); // normal needs to point inside object

refractedVector = this._calcRefractedVector( inverseRatio, surfaceNormal, refractedVector );

// check if the outer refracted vector at exit point leaves the object
if( Math.sign2( refractedVector.dot( surfaceNormal ) ) <= 0 ) {
var outerRay = new Ray( surfacePoint, refractedVector );
outerRay.addOffset( .05 ); // back of a little from the surface
return outerRay;
}
// repeat the proccess
}

// calc the intersection follwing the refracted ray direction
var outerRay = new Ray( surfacePoint, refractedVector );
outerRay.addOffset( -.05 );
var hitValues = object.calcHitValues( refractedRay );
surfacePoint = refractedRay.getPointAt( hitValues[1] ); // outer point
outerRay = new Ray( surfacePoint, reflectedVector );
outerRay.addOffset( .05 );

return outerRay;
},

_calcBackgroundInten: function( ray ) {
if( this._curDepth === 1 ) {
var direction = new Vector(0,ray.direction.y,ray.direction.z);
direction.normalize();
var cosRayHorizontal = direction.dot( new Vector( 0,0,1 ) );
var color = Vector.scalarMultiplication( this.backgroundIntensity, cosRayHorizontal );
return color;
}
var factor = 1/this._curDepth;
return Vector.scalarMultiplication( this.backgroundIntensity, factor );
},

_calcBackgroundInten2: function( ray ) {
var lightSourcesContribution = new Vector();
var isIntersectionWithLight = false;
for( var i = 0, l = this._lights.length; i < l; i++ ) {
var theLightSource = this._lights[i];
var fromRayToLight = Vector.fromP1toP2( ray.start, theLightSource.pos ).normalize();
var cos = fromRayToLight.dot( ray.direction );
if( cos >= .99999 && cos <= 1 ) {
lightSourcesContribution.add( theLightSource.inten );
isIntersectionWithLight = true;
}
}
return isIntersectionWithLight ? lightSourcesContribution : this._calcBackgroundInten( ray );
},

_calcSpecularitySimply: function( lightDirection, reflectedVector ) {
var cos = lightDirection.dot( reflectedVector ); // reflected ray points to the light source
if( cos >= .99 && cos <= 1 ) {
return this.highLightColorIntensity;
}
return new Vector(0,0,0); // none contribution
},

_calcSpecularityPhong: function( lightDirection, reflectedVector, lightIntensity ) {
var cos = reflectedVector.dot( lightDirection ); // cos(alpha)
if( Math.sign2(cos) > 0 ) {
//return new Vector(1,1,1);
cos = Math.pow( cos, 8 ); // cos(alpha)^n
var specularityContribution = Vector.scalarMultiplication( lightIntensity, .3 ); // I_light*k_specularly
specularityContribution.scalar( cos ); // I_light*k_specularly*cos(alpha)^n
return specularityContribution;
}
return new Vector(0,0,0); // none contribution
},

_checkForShadow: function( rayToLight ) {
var isShadow = false;
for( var i = 0, l = this._objects.length; i < l; i++ ) {
var object = this._objects[i];
var hitValues = object.calcHitValues( rayToLight );
if( hitValues ) {
return true;
}
}
return false;
},

_calcInten: function( ray ) {
var specularity = new Vector(),
refraction = new Vector(),
dullness = new Vector();

this._curDepth++;
if( this._curDepth > Scene.MAX_DEPTH ) {
return this._calcBackgroundInten( ray );
}

var objectIntersected = null;
var minHit = Number.POSITIVE_INFINITY;
for( var i = 0, l = this._objects.length; i < l; i++ ) {
var object = this._objects[i];
var hitValues = object.calcHitValues( ray );
if( !hitValues ) {
continue;
}
if( minHit > hitValues[0] ) {
minHit = hitValues[0];
objectIntersected = object;
}
}
if( !objectIntersected ) {
var backgroundContribution;
if( this.showLights ) {
backgroundContribution = this._calcBackgroundInten2( ray );
} else {
backgroundContribution = this._calcBackgroundInten( ray );
}
return backgroundContribution;
}

var theIntersection = ray.getPointAt( minHit );
var surfaceNormal = objectIntersected.calcNormal( theIntersection, true ); // true for normalized
var surfaceDiffuseness = objectIntersected.getDiffuseness( theIntersection );

// ambient term
var ambientTerm = Vector.vectorMultiplication( surfaceDiffuseness, this.ambientIntensity ); // surface_diffuseness * ambient_intensity
dullness.add( ambientTerm );

for( var i = 0, l = this._lights.length; i < l; i++ ) {
var theLightSource = this._lights[i];
// calc light direction from intersection point to light source
var lightDirection = Vector.fromP1toP2( theIntersection, theLightSource.pos ).normalize();
var rayToLight = new Ray( theIntersection, lightDirection );
rayToLight.addOffset( .05 );

// facing the light source?
var NL = surfaceNormal.dot( lightDirection ); // N dot L
var isFacingLight = Math.sign2( NL ) > 0;
// check for shadows
var isShadow = isFacingLight && this._checkForShadow( rayToLight );

if( isFacingLight && !isShadow && objectIntersected.specularity > 0 ) {
var reflectedVector = this._calcReflectedVector( ray.direction, surfaceNormal );

var specularityContribution;
if( this.specularityModel === Scene.SPECULARITY_SIMPLY ) {
specularityContribution = this._calcSpecularitySimply( lightDirection, reflectedVector );
} else if( this.specularityModel === Scene.SPECULARITY_PHONG ) {
specularityContribution = this._calcSpecularityPhong( lightDirection, reflectedVector, theLightSource.inten );
} else {
specularityContribution = new Vector(0,0,0);
}
specularity.add( specularityContribution );

var reflectedRay = new Ray( theIntersection, reflectedVector );
reflectedRay.addOffset( .05 );
var reflectionIntensity = this._calcInten( reflectedRay ); // follow reflection ray
reflectionIntensity.scalar( objectIntersected.specularity );
specularity.add( reflectionIntensity );
}

// compute diffueseness term
if( isFacingLight && !isShadow ) {
var tmp = Vector.scalarMultiplication( theLightSource.inten, NL ); // light * (N dot L)
tmp = tmp.mul( surfaceDiffuseness ); // surface_diffuseness * ( light * (N dot L) )
dullness.add( tmp ); // I_light*(K_diffusse*N(dot)L)
}
}

if( objectIntersected.refraction > 0 ) {
var refractedRay = this._calcRefractedRay( objectIntersected, theIntersection, surfaceNormal, ray.direction );
var refractedIntensity = this._calcInten( refractedRay ); // follow refracted ray
refractedIntensity.scalar( objectIntersected.refraction );
refraction.add( refractedIntensity );
}

var intensity = Vector.addition( specularity, refraction );
intensity.add( dullness );
return intensity;
},

_rayTrace: function( x, y ) {
var point = new Vector( x, y ); // from absolute screen space to persp space (akas: screen space)
point.transform( this.theWindowing.inverse );
// build the ray
var ray = new Ray( this.centerOfProjection );
ray.calcDirection( point, true ); // true for normalize
// start ray tracing
this._curDepth = 0;
var inten = this._calcInten( ray );
return inten;
},

_start: function() {
for( var y = 0; y < FrameBuffer.SCREEN_HEIGHT; y++ ) {
for( var x = 0; x < FrameBuffer.SCREEN_WIDTH; x++ ) {
// do some stochastic super sampling anti aliasing
var inten1 = this._rayTrace( x+.2, y+.2 );
var inten2 = this._rayTrace( x+.8, y+.8 );
var rgb;
if( Scene.almostSameIntensity( inten1, inten2 ) ) {
// enough rays to compute the final color
rgb = Illumination.intensityToRGB( inten1 );
} else {
// shoot remaining rays
var color = new Vector();
color.add( inten1 );
color.add( inten2 );
var offsets = [ .8, .2, .5, .5, .2, .8 ];
for( var i = 0, l = offsets.length-1; i < l; i+=2 ) {
var offsetX = offsets[i];
var offsetY = offsets[i+1];
var inten = this._rayTrace( x+offsetX, y+offsetY );
color.add( inten );
}
color.scalar( .2 ); // average
rgb = Illumination.intensityToRGB( color );
}

FrameBuffer.plot( x, y, rgb );
}
}
}
};
Scene.SPECULARITY_SIMPLY = 1;
Scene.SPECULARITY_PHONG = 2;
Scene.MAX_DEPTH = 6;
Scene.almostSameIntensity = function( inten1, inten2 ) {
var r, g, b;
if( (Math.sign2(inten1.x) !== 0 && Math.sign2(inten2.x) !== 0) || Math.sign2(inten1.x+inten2.x) === 0 ) {
r = Math.abs(inten1.x-inten2.x) <= .01;
if( r && (Math.sign2(inten1.y) !== 0 && Math.sign2(inten2.y) !== 0 || Math.sign2(inten1.y+inten2.y) === 0) ) {
g = Math.abs(inten1.y-inten2.y) <= .01;
if( g && (Math.sign2(inten1.z) !== 0 && Math.sign2(inten2.z) !== 0 || Math.sign2(inten1.z+inten2.z) === 0) ) {
b = Math.abs(inten1.z-inten2.z) <= .01;
if( b ) {
return true;
}
}
}
}
return false;
};
// *******************************************
// LIGHT
// *******************************************
var Light = function( pos, inten ) {
this.pos = pos ? pos.copy() : null;
this.direction = this.pos ? this.pos.copy().normalize() : null;
this.inten = inten ? Illumination.rgbToIntensity( inten ) : null;
return this;
};
Light.prototype.copy = function() {
var light = new Light();
light.pos = this.pos.copy();
light.direction = this.direction.copy();
light.inten = this.inten.copy();
return light;
};
Light.prototype.transform = function( m ) {
this.pos.transform( m );
this.direction = this.pos.copy().normalize();
return this;
};
// *******************************************
// ILLUMINATION
// *******************************************
var Illumination = function() {
this._ambientInt = new Vector( .2,.2,.2 );
this._pointLights = [];
};
// CONSTANTS
Illumination.SHADING = {};
Illumination.SHADING.FACETED = 0;
Illumination.SHADING.PHONG = 1;
Illumination.SHADING.GOURAUD = 2;
Illumination._rgbReciprocal = 1/255;
// "static members"
Illumination.intensityToRGB = function( inten ) {
var red = Math.min(parseInt(inten.x*255),255);
var green = Math.min(parseInt(inten.y*255),255);
var blue = Math.min(parseInt(inten.z*255),255);
return 'rgb('+red+','+green+','+blue+')';
};
Illumination.vectorToRGB = function(v) {
return 'rgb('+Math.min(Math.round(v.x),255)+','+Math.min(Math.round(v.y),255)+','+Math.min(Math.round(v.z),255)+')';
};
Illumination.rgbToIntensity = function( v ) {
return v.copy().scalar( Illumination._rgbReciprocal );
};

// *******************************
// PROGRAM START HERE
// *******************************
FrameBuffer.init( {
width: 150*2,
height: 75*2,
pixelSize: 1,
backgroundColor: [0,0,0],
screenSelector: '.screen',
} );
// the camera viewing transformation (vrp,vpn,vup,viewDistance)
var theCamera = new Camera( new Vector(10,0,0), new Vector(0,-.1,1), new Vector(0,1,0), 0);
var centerOfProjection = new Vector(0,0,-100);
var theWindowing = calcWindowingMatrix(
new Vector(-20,-10,0), new Vector(20,10,0), //minS,maxS
new Vector(0,FrameBuffer.SCREEN_HEIGHT,0),new Vector(FrameBuffer.SCREEN_WIDTH,0,0) //minT,maxT
);
var theScene = new Scene( {
theCamera: theCamera,
centerOfProjection: centerOfProjection,
theWindowing: theWindowing,
backgroundColor: new Vector(0,191,255), // deepskyblue
ambientColor: new Vector(63,63,63), // gray
} );
var theSphere1 = new Sphere( {
color: new Vector(255,0,0),
specularity: 1,
refraction: 0,
radius: 4,
} );
theSphere1.transform( new Translation( 0,3,2 ) );
var theBoard1 = new Board( {
scaleX: 30,
scaleZ: 200,
tileSize: 10,
colorOn: new Vector(255,255,180), // yellow
colorOff: new Vector(220,20,60), // crimson
specularity: 0,
refraction: 0,
} );
theBoard1.transform( new Translation( 0,-10,0 ) );
var theBox1 = new Box( {
color: new Vector(50,205,50), // green
specularity: 0,
refraction: 0,
scaleX: 4,
scaleY: 4,
scaleZ: 4,
} );
theBox1.transform( new YawRotation(45) );
theBox1.transform( new RollRotation(10) );
theBox1.transform( new Translation(30,0,50) );
var theBox2 = new Box( {
color: new Vector(255,160,122), // light salmon
specularity: 0,
refraction: 0,
scaleX: 2,
scaleY: 2,
scaleZ: 1,
} );
theBox2.transform( new YawRotation(45) );
theBox2.transform( new RollRotation(-10) );
theBox2.transform( new Translation(-13,0,35) );
var theSphere2 = new Sphere( {
color: new Vector(0,255,255), // cyan
specularity: .5,
refraction: 0,
radius: 4,
} );
theSphere2.transform( new Translation( 10,-1,10 ) );
var theSphere3 = new Sphere( {
color: new Vector(65,105,225), // blue
specularity: .7,
refraction: .1,
radius: 6,
} );
theSphere3.transform( new Translation( 15,3,0 ) );

theScene.addObject( theSphere1 );
theScene.addObject( theBoard1 );
theScene.addObject( theBox1 );
theScene.addObject( theBox2 );
theScene.addObject( theSphere2 );
theScene.addObject( theSphere3 );

theScene.addLight( new Light( new Vector(0,50,-50), new Vector( 255,255,255 ) ) );
// *******************************
// DRAW
// *******************************
theScene.specularityModel = Scene.SPECULARITY_PHONG;
theScene.render();
``````