Ray Tracing

In this example below you will see how to do a Ray Tracing with some HTML / CSS and Javascript

Many errors has been corrected. Still in progress...

Thumbnail
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();

Comments