Phong's specularly

In this example below you will see how to do a Phong's specularly with some HTML / CSS and Javascript

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
  • JavaScript
<!DOCTYPE html>
<html lang="en" >

<head>
  <meta charset="UTF-8">
  <title>Phong's specularly</title>
  
  
  
  
  
</head>

<body>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js" type="text/javascript"></script>

<table class="frameBuffer"></table>

<div>
  <strong>Polygon vertices are:</strong>
  <pre class="vertices"></pre>
  <strong>Poly's normal is:</strong>
  <pre class="normal">  
  </pre>
  <strong>Viewer is located at the point<strong>
    <pre class="viewer"></pre>
  <strong>LHS is used, poly is in +z axis halspace</strong>
</div>
  
  

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




</body>

</html>

/*Downloaded from https://www.codeseek.co/arcollector/phongandaposs-specularly-azObXZ */
Math.sign2 = function(x) {
	return Math.abs(x)<1e-6?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;
};
// *******************************************
//
// *******************************************
var FrameBuffer = {
	SCREEN_WIDTH: null,
	SCREEN_HEIGHT: null,
	BACKGROUND_COLOR: null,
	pixels: null,
	
	init: function(selector,width,height,pixelSize,bkgColor) {
		var $table = document.querySelector(selector);
		$table.style.borderCollapse = 'collapse';
		$table.style.margin = '0 auto';
		
		this.SCREEN_WIDTH = width || 32;
		this.SCREEN_HEIGHT = height || 32;
		
		this.BACKGROUND_COLOR = bkgColor || new Vector(66,66,66);
		
		this.pixels = [];
		
		var $frameBuffer = document.querySelector('table');
		pixelSize = pixelSize ? pixelSize + 'px' : '10px';
	
		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);
			}
			$table.appendChild($tr);
		}
	},
	
	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;
	}
};
// *******************************************
//
// *******************************************
var Vector = function( x,y,z,w ) {
	this.x = x;
	this.y = y;
	this.z = z;
	this.w = typeof w === 'undefined' ? 1 : w;
	return this;
};

Vector.fromP1toP2 = function( v1, v2 ) {
	var x = v2.x - v1.x;
	var y = v2.y - v1.y;
	var z = v2.z - v1.z;
	var w = v2.w - v1.w;
	return new Vector(x,y,z,w);
};

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.x /= l;
	this.y /= l;
	this.z /= 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.substraction = function(v1,v2) {
	return new Vector(
		v1.x - v2.x,
		v1.y - v2.y,
		v1.z - v2.z
	);
};

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;
	
	this.x = x/w;
	this.y = y/w;
	this.z = z/w;
	this.w = 1;
	return this;
};

Vector.prototype.toString = function() {
	console.log( "Purple,Point[" +
		"{" + ((Math.abs(this.x)<=1e-6)?0:this.x) + "," + ((Math.abs(this.y)<=1e-6)?0:this.y) + "," + ((Math.abs(this.z)<=1e-6)?0:this.z) + "}" +
		"]," 
	);
};

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

Vector.scalarMultiplication = function(v,scalar) {
	return new Vector(
		v.x*scalar,
		v.y*scalar,
		v.z*scalar
	);
};
// *******************************************
//
// *******************************************
var Matrix4x4 = 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;
};

Matrix4x4.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;
};

Matrix4x4.prototype.transpose = function() {
	return new Matrix4x4(
		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)
	);
};

Matrix4x4.prototype.toString = function() {
	console.log( "mat={" +
		"{" + this._00 + "," + this._01 + "," + this._02 + "," + this._03 + "}," +
		"{" + this._10 + "," + this._11 + "," + this._12 + "," + this._13 + "}," +
		"{" + this._20 + "," + this._21 + "," + this._22 + "," + this._23 + "}," +
		"{" + this._30 + "," + this._31 + "," + this._32 + "," + this._33 + "}" +
	"}");
};
// *******************************************
//
// *******************************************
//ccw rotation, y-axis as pivot, looking toward negative y axis in LHS
var YawRotation = function( angleInDeg ) {
	var rad = Math.degToRad(angleInDeg);
	var cos = Math.cos(rad);
	var sin = Math.sin(rad);
	
	this.m = new Matrix4x4();
	this.m._00 = cos;
	this.m._20 = -sin;
	this.m._02 = sin;
	this.m._22 = cos;
	
	this.inverse = this.m.transpose();
};

//ccw rotation, x-axis as pivot, looking toward negative x axis in LHS
var PitchRotation = function( angleInDeg ) {
	var rad = Math.degToRad(angleInDeg);
	var cos = Math.cos(rad);
	var sin = Math.sin(rad);
	
	this.m = new Matrix4x4();
	this.m._11 = cos;
	this.m._21 = sin;
	this.m._12 = -sin;
	this.m._22 = cos;
	
	this.inverse = this.m.transpose();
};

// ccw rotation, z-axis as pivot, looking toward negative positive z axis in LHS
var RollRotation = function(angleInDeg) {
	var rad = Math.degToRad(angleInDeg);
	var cos = Math.cos(rad);
	var sin = Math.sin(rad);
	
	this.m = new Matrix4x4();
	this.m._00 = cos;
	this.m._10 = -sin;
	this.m._01 = sin;
	this.m._11 = cos;
	
	this.inverse = this.m.transpose();
};

var Translation = function(tx,ty,tz) {
	this.m = new Matrix4x4();
	this.m._30 = tx;
	this.m._31 = ty;
	this.m._32 = tz;
	
	this.inverse = new Matrix4x4();
	this.inverse._30 = -tx;
	this.inverse._31 = -ty;
	this.inverse._32 = -tz;
};

var Scaling = function(sx,sy,sz) {
	this.m = new Matrix4x4();
	this.m._00 = sx;
	this.m._11 = sy;
	this.m._22 = sz;
	
	this.inverse = new Matrix4x4();
	this.inverse._00 = 1/sx;
	this.inverse._11 = 1/sy;
	this.inverse._22 = 1/sz;
};
// *******************************************
//
// *******************************************
var Polygon = function() {
	this.vertices = [];
	this.centroid = new Vector(0,0,0);
	for( var i = 0, l = arguments.length-1; i < l; i++ ) {
		var v = arguments[i];
		this.vertices[i] = v;
		this.centroid.x += v.x;
		this.centroid.y += v.y;
		this.centroid.z += v.z;
		// associate every vertex with this plane
		v.plane = this;
	}
	var l = this.vertices.length;
	this.centroid.x /= l;
	this.centroid.y /= l;
	this.centroid.z /= l;
	
	this.color = arguments[arguments.length-1];
	
	this.projectedVertices = [];
	this.projectedCentroid = null;
	
	this.screenVertices = [];

	this.calcNormal();
};

Polygon.prototype.displayPlaneEquation = function() {
	console.log(
		this.A+"x" + 
		(Math.sign2(this.B)<0?"":"+")+this.B+"y" + 
		(Math.sign2(this.C)<0?"":"+")+this.C+"z" + 
		(Math.sign2(this.D)<0?"":"+")+this.D + "=0"
	);
};

Polygon.prototype.calcNormal = function( useProjectedVertices ) {
	var vertices;
	if( useProjectedVertices ) {
		vertices = this.projectedVertices;
	} else {
		vertices = this.vertices;
	}
	
	var p1 = vertices[0],
		p2 = vertices[1],	
		p3 = vertices[2];
	var v1 = Vector.fromP1toP2(p1,p2);
	var v2 = Vector.fromP1toP2(p1,p3);
	var normal = v1.cross(v2).normalize();
	var A = normal.x;
	var B = normal.y;
	var C = normal.z;
	var D = -(A*p1.x + B*p1.y + C*p1.z);
	
	if( useProjectedVertices ) {
		this.projectedNormal = normal;
		this.projectedA = A; this.projectedB = B; this.projectedC = C; this.projectedD = D;
	} else {
		this.normal = normal;
		this.A= A; this.B = B; this.C = C; this.D = D;
	}
};

Polygon.prototype.calcBoundingBox = function( useProjectedVertices ) {
	this.min = new Vector(0,0,0);
	this.max = new Vector(0,0,0);
	
	var vertices;
	if( useProjectedVertices ) {
		vertices = this.projectedVertices;
	} else {
		vertices = this.vertices;
	}
	
	for(var i = 0, l = vertices.length; i < l; i++ ) {
		var v = vertices[i];
		if( this.min.x > v.x ) {
			this.min.x = v.x;
		}
		if( this.max.x < v.x ) {
			this.max.x = v.x;
		}
		if( this.min.y > v.y ) {
			this.min.y = v.y;
		}
		if( this.max.y < v.y ) {
			this.max.y = v.y;
		}
		if( this.min.z > v.z ) {
			this.min.z = v.z;
		}
		if( this.max.z < v.z ) {
			this.max.z = v.z;
		}
	}
};

Polygon.prototype.transform = function(m) {
	for( var i = 0, l = this.vertices.length; i < l; i++ ) {
		this.vertices[i].transform(m);
	}
	this.centroid.transform(m);
	this.calcNormal();
};

Polygon.prototype.project = function(perspectiveTransformation) {
	for( var i = 0, l = this.vertices.length; i < l; i++ ) {
		var v = this.vertices[i].copy(); // copy vertex in object space
		v.transform(perspectiveTransformation); // transform it to perp space
		this.projectedVertices[i] = v; // save it into another array
	}
	this.projectedCentroid = this.centroid.copy();
	this.projectedCentroid.transform(perspectiveTransformation);
	this.calcNormal( true ); // calc normal with projected vertices
};

Polygon.prototype.clean = function() {
	this.projectedVertices = [];
	this.screenVertices = [];
	this.projectedCentroid = null;
	this.isVisible = false;
	this.projectedNormal = null;
	this.projectedA = null; this.projectedB = null; this.projectedC = null; this.projectedD = null;
};

Polygon.prototype.toString = function( useProjectedVertices ) {
	var str = [];
	var vertices = useProjectedVertices ? this.projectedVertices : this.vertices;
	str.push( "Red,Polygon[{\n" );
	for( var i = 0, l = vertices.length; i < l; i++ ) {
		var v = vertices[i];
		str.push( "{x,y,z}".replace("x",v.x).replace("y",v.y).replace("z",v.z) + (i!=l-1?",\n":"\n") );
	}
	str.push( "}],\n" );
	console.log( str.join("") );
};
// *******************************************
//
// *******************************************
var Geometry = function() {
};
Geometry.correctMeshPlanesOrientation = function(mesh) {
	// if centroid is on the positive side of the plane polygon
	// reverse normal polygon, so normal point out of the surface
	for( var i = 0, l = mesh.faces.length; i < l; i++ ) {
		var plane = mesh.faces[i];
		plane.calcNormal();
		var sign = plane.normal.dot(mesh.centroid) + plane.D;
		sign = Math.sign2(sign);
		if( sign != 0 && sign > 0 ) {
			// reverse that shit
			plane.normal.negate();
			plane.A = plane.normal.x;
			plane.B = plane.normal.y;
			plane.C = plane.normal.z;
			plane.D = -plane.D;
			
			// reverse vertices order also, so future normal calculations
			// be always correct
			plane.vertices.reverse();
		}
	}	
};
Geometry.calcMeshCentroid = function(mesh) {
	mesh.centroid = new Vector(0,0,0);
	for( var i = 0, l = mesh.faces.length; i < l; i++ ) {
		var poly = mesh.faces[i];
		mesh.centroid.x += poly.centroid.x;
		mesh.centroid.y += poly.centroid.y;
		mesh.centroid.z += poly.centroid.z;
	}
	mesh.centroid.x /= l;
	mesh.centroid.y /= l;
	mesh.centroid.z /= l;	
};
Geometry.meshMembersNames = [
	'transform','calcBoundingBox','project','clean','toString'
];
Geometry.registerMeshMembers = function(mesh) {
	for( var i = 0, l = Geometry.meshMembersNames.length; i < l; i++ ) {
		var meshMemberName = Geometry.meshMembersNames[i];
		mesh.prototype[meshMemberName] = Geometry[meshMemberName];
	}
};
Geometry.transform = function(m) {
	for( var i = 0, l = this.vertices.length; i < l; i++ ) {
		this.vertices[i].transform(m);
	}
	this.centroid.transform(m);
	for( var i = 0, l = this.faces.length; i < l; i++ ) {
		var plane = this.faces[i];
		plane.centroid.transform(m);
		plane.calcNormal();
	}
};
Geometry.calcBoundingBox = function( useProjectedVertices ) {
  	this.min = new Vector(0,0,0);
	this.max = new Vector(0,0,0);
 	for( var i = 0, l = this.faces.length; i < l; i++ ) {
		var plane = this.faces[i];
		plane.calcBoundingBox( useProjectedVertices );
 		if( this.min.x > plane.min.x ) {
			this.min.x = plane.min.x;
		}
		if( this.max.x < plane.max.x ) {
			this.max.x = plane.max.x;
		}
		if( this.min.y > plane.min.y ) {
			this.min.y = plane.min.y;
		}
		if( this.max.y < plane.max.y ) {
			this.max.y = plane.max.y;
		}
		if( this.min.z > plane.min.z ) {
			this.min.z = plane.min.z;
		}
		if( this.max.z < plane.max.z ) {
			this.max.z = plane.max.z;
		}
	}
};
Geometry.project = function(perspectiveTransformation,viewer) {
	for( var i = 0, l = this.faces.length; i < l; i++ ) {
		var plane = this.faces[i];
		// do backface culling
		if( l == 1 || Math.sign2(plane.normal.dot(viewer)) < 0 ) {
			plane.isVisible = true;
			var polyData = Scene.addVisibleObject( this, plane );
			plane.project(perspectiveTransformation); // object to persp
			// save plane equation in persp space
			polyData.A = plane.projectedA;
			polyData.B = plane.projectedB;
			polyData.C = plane.projectedC;
			polyData.D = plane.projectedD;
		} else {
			plane.isVisible = false;
		}
	}

	this.projectedCentroid = this.centroid.copy();
	this.projectedCentroid.transform(perspectiveTransformation);
};
Geometry.clean = function() {
	this.projectedCentroid = null;
	for( var i = 0, l = this.faces.length; i < l; i++ ) {
		var poly = this.faces[i];
		poly.clean();
	}
};
Geometry.toString = function( useProjectedVertices ) {
	for( var i = 0, l = this.faces.length; i < l; i++ ) {
		var poly = this.faces[i];
		poly.toString( useProjectedVertices );
	}
};
// *******************************************
//
// *******************************************
var Cube = function(color,specularlyFactor,specularlySpatialDistribution) {
	this.vertices = [
		new Vector(-1,-1,-1), // 0
		new Vector(1,-1,-1), // 1
		new Vector(1,1,-1), // 2
		new Vector(-1,1,-1), // 3
	
		new Vector(-1,-1,1), // 4
		new Vector(1,-1,1), // 5
		new Vector(1,1,1), // 6
		new Vector(-1,1,1) // 7
	];
	
	this.color = color || new Vector(1,0,0);
	
	this.faces = [
		// front face
		new Polygon(this.vertices[0],this.vertices[1],this.vertices[2],this.vertices[3],this.color),
		// back face
		new Polygon(this.vertices[4],this.vertices[5],this.vertices[6],this.vertices[7],this.color),
		// top face
		new Polygon(this.vertices[2],this.vertices[3],this.vertices[7],this.vertices[6],this.color),
		// bottom face
		new Polygon(this.vertices[0],this.vertices[1],this.vertices[5],this.vertices[4],this.color),
		// right face
		new Polygon(this.vertices[1],this.vertices[5],this.vertices[6],this.vertices[2],this.color),
		// left face
		new Polygon(this.vertices[0],this.vertices[4],this.vertices[7],this.vertices[3],this.color),
	];
	
	this.projectedCentroid = null;

	Geometry.calcMeshCentroid(this);
	Geometry.correctMeshPlanesOrientation(this);
	
	// shininess of the surface
	this.specularlyFactor = specularlyFactor || .1;
	// spatial distribution of the specularly reflected light
	this.specularlySpatialDistribution = specularlySpatialDistribution || 1;
};

Geometry.registerMeshMembers(Cube);
// *******************************************
//
// *******************************************
var Plane = function(color,specularlyFactor,specularlySpatialDistribution) {
	this.color = color || new Vector(1,1,0);
	
	this.vertices = [
		new Vector(1,-1,0),
		new Vector(-1,-1,0),
		new Vector(-1,1,0),
		new Vector(1,1,0)
	];
	
	this.faces = [
		new Polygon(this.vertices[0],this.vertices[1],this.vertices[2],this.vertices[3],this.color)
	];
	
	// mesh with one poly, its normal must point to the negative z axis, because geometry must be located in positive z axis halfspace
	var thePoly = this.faces[0];
	thePoly.calcNormal();
	var normal = thePoly.normal;
	if( Math.sign2(normal.dot(new Vector(0,0,1))) > 0 ) {
		normal.negate();
		thePoly.A = normal.x;
		thePoly.B = normal.y;
		thePoly.C = normal.z;
		thePoly.D = -thePoly.D;
		thePoly.vertices.reverse();
	}
	
	this.centroid = thePoly.centroid.copy();
	this.projectedCentroid = null;
	
	// shininess of the surface
	this.specularlyFactor = specularlyFactor || .1;
	// spatial distribution of the specularly reflected light
	this.specularlySpatialDistribution = specularlySpatialDistribution || 1;
};

Geometry.registerMeshMembers(Plane);
// *******************************************
//
// *******************************************
var Scene = {
	visiblePolys: null,
	meshes: null,
	centerOfProjection: null,
	viewerDirection: null,
	objectToPerpsTransformation: null,
	perpsToObjectTransformation: null,
	persToScreenTransformation: null,
	screenToPerspTransformation: null,
	
	init: function( centerOfProjection, 
		objectToPerpsTransformation, perpsToObjectTransformation,
		persToScreenTransformation, screenToPerspTransformation 
	) {
		this.visiblePolys = [];
		this.meshes = [];
		
		this.centerOfProjection = centerOfProjection;
		this.viewerDirection = this.centerOfProjection.copy().negate().normalize();
		
		this.objectToPerpsTransformation = objectToPerpsTransformation;
		this.perpsToObjectTransformation = perpsToObjectTransformation;
		
		this.persToScreenTransformation = persToScreenTransformation;
		this.screenToPerspTransformation = screenToPerspTransformation;
	},

	addObject: function( mesh ) {
		this.meshes.push(mesh);
	},
	
	addVisibleObject: function( mesh, thePoly ) { // poly is object space
		var polyData = { 
			thePoly: thePoly,
			normal: thePoly.normal.copy(), // for shading calc
			color: thePoly.color, // for shading calc
			specularlyFactor: mesh.specularlyFactor, // for shading calc
			specularlySpatialDistribution: mesh.specularlySpatialDistribution, // for shading calc
			flag: false, // for spanning scan line hidden surface algorithm
			A:null,B:null,C:null,D:null, // for spanning scan line hidden surface algorithm
		};
		this.visiblePolys.push(polyData);
		return polyData;
	},
	
	project: function() {
		for( var i = 0, l = this.meshes.length; i < l; i++ ) {
			var mesh = this.meshes[i];
			mesh.project(this.objectToPerpsTransformation,this.viewerDirection);
		}
	},

	toScreenSpace: function() {
		for( var i = 0, l = this.visiblePolys.length; i < l; i++ ) {
			var polyData = this.visiblePolys[i];
			var thePoly = polyData.thePoly;
			var projectedVertices = thePoly.projectedVertices;
			var screenVertices = thePoly.screenVertices;
			for( var j = 0, k = projectedVertices.length; j < k; j++ ) {
				var v = projectedVertices[j].copy();
				v.transform(this.persToScreenTransformation);
				screenVertices.push(v);
			}
		}
	},
	
	pointFromScreenToObjectSpace: function( point, polyData ) {
		point.transform(Scene.screenToPerspTransformation);
		point.z = (-polyData.A*point.x -polyData.B*point.y -polyData.D)/polyData.C;
		point.transform(Scene.perpsToObjectTransformation);
	},
	
	clean: function() {
		for( var i = 0, l = this.meshes.length; i < l; i++ ) {
			this.meshes[i].clean();
		}
		this.visiblePolys = [];
	},
	
	toString: function() {
		for( var i = 0, l = this.meshes; i < l; i++ ) {
			this.meshes[i].toString();
		}
	},
	
	toStringVisibles: function( useProjectedVertices ) {
		for( var i = 0, l = this.visiblePolys.length; i < l; i++ ) {
			var polyData = this.visiblePolys[i];
			var thePoly = polyData.thePoly;
			thePoly.toString( useProjectedVertices );
		}
	},
};
// *******************************************
//
// *******************************************
var Render = {
	
	init: function() {
	},

	setYBucketList: function() {
		this.YBucketList = [];
		for( var i = 0; i < FrameBuffer.SCREEN_HEIGHT; i++ ) {
			this.YBucketList[i] = {
				scanLine: i,
				polyList: []
			};
		}
		
		for( var i = 0; i < Scene.visiblePolys.length; i++ ) {
			var polyData = Scene.visiblePolys[i];
			var thePoly = polyData.thePoly;
			var vertices = thePoly.screenVertices;
			//console.log(vertices);throw Error();
			for( var j = 0, k = vertices.length; j < k; j++ ) {
				var j2 = j+1==k?0:j+1;
				var v1 = vertices[j];
				var v2 = vertices[j2];
				var x1 = v1.x, y1 = v1.y;
				var x2 = v2.x, y2 = v2.y;
				if( Math.abs(Math.round2(y1)-Math.round2(y2)) == 0 ) { // avoid horizontal edges
					continue;
				}
				var maxY = Math.max(y1,y2);
				var scanLine = parseInt(maxY-.5);
				var yBucket = this.YBucketList[scanLine];
				yBucket.polyList.push( { 
					polyIndex: i, 
					v1Index: j, 
					v2Index: j2 
				} );
			}
		}
	},

	draw: function() {
		this.setYBucketList();
		
		var activeEdgeList = [];
		for( var i = FrameBuffer.SCREEN_HEIGHT-1; i >= 0; i-- ) {
			var scanLine = i + .5;
			var yBucket = this.YBucketList[i];
			
			if( yBucket.polyList.length ) {
				var polyList = yBucket.polyList;
				for( var j = 0, k = polyList.length; j < k; j++ ) {
					var polyData = polyList[j];
					var polyIndex = polyData.polyIndex;
					var thePoly = Scene.visiblePolys[polyIndex].thePoly;
					var vertices = thePoly.screenVertices;
					var v1 = vertices[polyData.v1Index];
					var v2 = vertices[polyData.v2Index];
					var x1 = v1.x, y1 = v1.y;
					var x2 = v2.x, y2 = v2.y;
					activeEdgeList.push( {
						deltaY: parseInt(Math.max(y1,y2)-.5) - parseInt(Math.min(y1,y2)+.5),
						deltaX: -(x2-x1)/(y2-y1),
						xInt: (x2-x1)/(y2-y1)*(scanLine-y1)+x1,
						polyIndex: polyIndex
					} );
				}
			}
			
			if( activeEdgeList.length ) {
				activeEdgeList.sort( this.sortFunc );
			}
			
			var spanLeft = 0, spanRight;
			var spanLeftForDrawing = 0, spanRightForDrawing;
			var y = i;
			var polyCount = 0;
			var activePolygonList = {};
			/*console.log('proccessing scanline:',i);
			var str = "";
			for( var j = 0, k = activeEdgeList.length; j < k; j++ ) {
				str += 'xInt: ' + activeEdgeList[j].xInt + " polyIndex: " + activeEdgeList[j].polyIndex + " ,\n";
			}
			console.log(str);*/
			for( var j = 0, k = activeEdgeList.length; j < k; j++ ) {
				var ael = activeEdgeList[j];
				spanRight = ael.xInt;
				// do a very basic clipping routine
				if( Math.sign2(spanRight) < 0 ) {
					spanRight = 0;
					spanRightForDrawing = spanRight;
				} else if( spanRight >= FrameBuffer.SCREEN_WIDTH-1 ) {
					spanRight = FrameBuffer.SCREEN_WIDTH-1;
					spanRightForDrawing = spanRight;
				} else {
					spanRightForDrawing = parseInt(spanRight)-1;
				}
				if( polyCount == 0 ) {
					//console.log("drawing from", spanLeftForDrawing, "to", spanRightForDrawing, "y =", y, "poly index:", ael.polyIndex );
					this.line( spanLeftForDrawing, spanRightForDrawing, y, null, FrameBuffer.BACKGROUND_COLOR );
				} else if( polyCount == 1 ) {
					//console.log("drawing from", spanLeftForDrawing, "to", spanRightForDrawing, "y =", y, "poly index:", ael.polyIndex );
					var activePolyIndex = Object.keys(activePolygonList)[0];
					this.line( spanLeftForDrawing, spanRightForDrawing, y, Scene.visiblePolys[activePolyIndex] );
				} else {
					//console.log("drawing from", spanLeft, "to", spanRight, "y =", y );
					this.processSegment(spanLeft,spanRight,y,activePolygonList);
				}
				var polyIndex = ael.polyIndex;
				var curPoly = Scene.visiblePolys[polyIndex];
				curPoly.flag = !curPoly.flag;
				if( !curPoly.flag ) {
					polyCount--;
					delete activePolygonList[polyIndex];
				} else {
					polyCount++;
					activePolygonList[polyIndex] = true;
				}
				spanLeft = spanRight;
				spanLeftForDrawing = parseInt(spanLeft);
			}
			if( spanLeftForDrawing < FrameBuffer.SCREEN_WIDTH-1 ) {
				//console.log("drawing background from", spanLeftForDrawing, "to", SCREEN_WIDTH-1, "y =", y );
				this.line( spanLeftForDrawing, FrameBuffer.SCREEN_WIDTH-1, y, null, FrameBuffer.BACKGROUND_COLOR );
			}
			
			for( var j = 0, k = activeEdgeList.length; j < k; ) {
				var ael = activeEdgeList[j];
				ael.deltaY--;
				if( ael.deltaY < 0 ) {
					activeEdgeList.splice(j,1);
					k--;
				} else {
					ael.xInt += ael.deltaX;
					j++;
				}
			}
		}
	},
	
	sortFunc: function(a,b) {
		return a.xInt-b.xInt;
	},
	
	line: function( xs, xe, y, polyData, backgroundColor ) {
		if( xs-xe == 1 ) {
			xe++;
		}

		if( backgroundColor ) {
			var rgb = Illumination.vectorToRGB(backgroundColor);
			FrameBuffer.hLine(xs,xe,y,rgb);

		} else {
			for( ; xs <= xe; xs++ ) {
				var point = new Vector(xs,y,0);
				Scene.pointFromScreenToObjectSpace(point,polyData);
				// surface point, surface normal, surface diffuse reflectivity
				var rgb = Illumination.calcColor(
					point,
					polyData.normal,
					polyData.color,
					polyData.specularlyFactor, 
					polyData.specularlySpatialDistribution 
				);
				FrameBuffer.plot(xs,y,rgb);
			}
		}
	},

	processSegment: function(xs,xe,y,activePolygonList) {
		var zBuffer = [];
		var frameBuffer = [];

		xs = parseInt(xs);
		xe = parseInt(xe);
		
		for( var x = xs; x <= xe; x++ ) {
			zBuffer[x-xs] = 1; // perspective transformation normalize z coord to 0...1
		}
		
		for( var polyIndex in activePolygonList ) {
			var polyData = Scene.visiblePolys[polyIndex];
			for( var x = xs; x <= xe; x++ ) {
				var point = new Vector(x,y,0);
				Scene.pointFromScreenToObjectSpace(point,polyData);
				var index = x-xs;
				if( point.z < zBuffer[index] ) {
					zBuffer[index] = point.z;
					frameBuffer[index] = {
						point: point, // point is in object space
						polyData: polyData
					};
				}
			}
		}
		
		for(  var x = xs; x <= xe; x++ ) {
			var pixelData = frameBuffer[x-xs];
			var polyData = pixelData.polyData;
			var rgb = Illumination.calcColor( 
				pixelData.point, 
				polyData.normal, 
				polyData.color,
				polyData.specularlyFactor, 
				polyData.specularlySpatialDistribution 
			);
			FrameBuffer.plot(x,y,rgb);
		}
	},
};
// *******************************************
//
// *******************************************
var Illumination = {
	lightPos: new Vector(2,3,0),
	lightInt: new Vector(1,1,1),
	ambientInt: new Vector(.2,.2,.2),
	
	calcLightDirection: function(surfacePoint) {
		var lightDirection = Vector.fromP1toP2(surfacePoint,this.lightPos);
		lightDirection.normalize();
		return lightDirection;
	},
	
	calcAmbientLightTerm: function(surfaceDiffuseReflectivity) {
		var ambientLightTerm = Vector.vectorMultiplication(surfaceDiffuseReflectivity,this.ambientInt);
		return ambientLightTerm;
	},
	
	calcLightFactor: function(surfaceDiffuseReflectivity) {
		var lightFactor = Vector.vectorMultiplication(this.lightInt,surfaceDiffuseReflectivity);
		return lightFactor;
	},
	
	calcReflectedVector: function(lightDirection,surfaceNormal) {
		var projectionData = Vector.projectV1OntoV2(lightDirection,surfaceNormal);
		if( !projectionData ) {
			return lightDirection.copy().negate();
		}
		var kv = projectionData.kv;
		var u = projectionData.u;
		var reflectedVector = Vector.substraction(kv,u);
		reflectedVector.normalize();
		return reflectedVector;
	},
	
	calcLineOfSight: function(surfacePoint,viewerPosition) {
		var lineOfSight = Vector.fromP1toP2(surfacePoint,viewerPosition);
		lineOfSight.normalize();
		return lineOfSight;
	},
	
	calcReflectionFactor: function(reflectanceConstant) {
		var reflectionFactor = Vector.scalarMultiplication(this.lightInt,reflectanceConstant);
		return reflectionFactor;
	},
	
	calcColor: function(surfacePoint, surfaceNormal, surfaceDiffuseReflectivity, specularlyFactor, specularlySpatialDistribution ) {
		var ambientLightTerm = this.calcAmbientLightTerm(surfaceDiffuseReflectivity); // I_amb*k_surface
		
		var lightDirection = this.calcLightDirection(surfacePoint); // L
		var cosTheta = surfaceNormal.dot(lightDirection); // cos(theta)
		if( Math.sign2(cosTheta) < 0 ) { // light source is behind object
			// only background intensity
			return this.intensityToRGB(ambientLightTerm);
		}
		var lightFactor = this.calcLightFactor(surfaceDiffuseReflectivity); // I_light*k_surface
		var lightTerm = lightFactor.scalar(cosTheta); // I_light*k_surface*cos(theta)
		
		var reflectedVector = this.calcReflectedVector(lightDirection,surfaceNormal); // R
		var lineOfSight = this.calcLineOfSight(surfacePoint,Scene.centerOfProjection); // S
		var cosAlpha = reflectedVector.dot(lineOfSight); // cos(alpha)
		var reflectionTerm;
		//console.log(specularlyFactor,specularlySpatialDistribution); throw Error();
		if( Math.sign2(cosAlpha) > 0 ) {
			cosAlpha = Math.pow(cosAlpha,specularlySpatialDistribution); // cos(alpha)^n
			var reflectionFactor = this.calcReflectionFactor(specularlyFactor); // I_light*k_specularly
			reflectionTerm = reflectionFactor.scalar(cosAlpha); // I_light*k_specularly*cos(alpha)^n
		} else {
			reflectionTerm = new Vector(0,0,0);
		}
		
		var intensity = Vector.addition(ambientLightTerm,lightTerm); // I_amb*k_surface +  I_light*k_surface*cos(theta)
		intensity.add(reflectionTerm); //  I_amb*k_surface +  I_light*k_surface*cos(theta) + I_light*k_specularly*cos(alpha)^n
		
		return this.intensityToRGB(intensity);
	},
	
	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+')';
	},
	
	vectorToRGB: function(v) {
		return 'rgb('+v.x+','+v.y+','+v.z+')';
	}
};
// *******************************************
// GEOMETRY
// *******************************************
// objects
var cube1 = new Cube(new Vector(0,1,0));
var rotationY = new YawRotation( 25 );
var rotationX = new PitchRotation( 25 );
var rotationZ = new RollRotation( 25 );
var translationZ = new Translation(-1,0,5);
var trans = new Matrix4x4();
trans.multiply(rotationY.m);
trans.multiply(rotationX.m);
//trans.multiply(rotationZ.m);
trans.multiply(translationZ.m);
cube1.transform(trans);

var cube2 = new Cube(new Vector(1,0,0));
var trans = new Matrix4x4();
var rotationY = new YawRotation( -25 );
var translationZ = new Translation(2,0,10);
trans.multiply(rotationY.m);
trans.multiply(rotationX.m);
trans.multiply(translationZ.m);
cube2.transform(trans);

var plane1 = new Plane(new Vector(165/255,42/255,42/255));
plane1.toString()
var rotationX = new PitchRotation( -20 );
var scaleXY = new Scaling(5,10,1);
var translationZ = new Translation(0,0,5);
var trans = new Matrix4x4();
trans.multiply(scaleXY.m);
trans.multiply(translationZ.m);
trans.multiply(rotationX.m);
plane1.transform(trans);
//plane1.toString(); throw Error();
// *******************************************
// INIT
// *******************************************
FrameBuffer.init('.frameBuffer',32,32,10,new Vector(0,0,0));

// center of projection
var cop = new Vector(0,0,-10);
var objectToPerpsTransformation = new Matrix4x4(
	new Vector(-cop.z,0,0,0),
	new Vector(0,-cop.z,0,0),
	new Vector(cop.x,cop.y,1,1),
	new Vector(0,0,0,-cop.z)
);
var perpsToObjectTransformation = new Matrix4x4(
	new Vector(1/-cop.z,0,0,0),
	new Vector(0,1/-cop.z,0,0),
	new Vector(cop.x/cop.z,cop.y/cop.z,1,1/cop.z),
	new Vector(0,0,0,1/-cop.z)
);

// persp to screen transformation
var minX = -5;
var minY = -11;
var maxX = 5;
var maxY = 6;
var a = -minX;
var b = -minY;
var c = FrameBuffer.SCREEN_WIDTH/(maxX-minX);
var d = -FrameBuffer.SCREEN_HEIGHT/(maxY-minY);
var e = FrameBuffer.SCREEN_HEIGHT;
var persToScreenTransformation = new Matrix4x4(
	new Vector(c,0,0,0),
	new Vector(0,d,0,0),
	new Vector(0,0,1,0),
	new Vector(a*c,b*d+e,0,1)
);
var screenToPerspTransformation = new Matrix4x4(
	new Vector(1/c,0,0,0),
	new Vector(0,1/d,0,0),
	new Vector(0,0,1,0),
	new Vector(-a*c/c,-(b*d+e)/d,0,1)
);

Scene.init( cop, 
	objectToPerpsTransformation, perpsToObjectTransformation, 
	persToScreenTransformation, screenToPerspTransformation
);
Render.init();
Scene.addObject(plane1);
//Scene.addObject(cube1);
//Scene.addObject(cube2);
// *******************************************
// INFO
// *******************************************
!function() {
	var $pre = document.querySelector('.vertices');
	var str = [];
	for(var i=0,l=plane1.vertices.length;i<l;i++) {
		var v = plane1.vertices[i];
		str[i] = '(' + v.x + ',' + v.y + ',' + v.z + ')';
	}
	$pre.innerHTML = str.join('\n');
	$pre = document.querySelector('.normal');
	var normal = plane1.faces[0].normal;
	str = [ '(' + normal.x + ',' + normal.y + ',' + normal.z + ')' ];
	$pre.innerHTML = str.join('\n');
	$pre = document.querySelector('.viewer');
	var viewer = Scene.centerOfProjection;
	str = [ '(' + viewer.x + ',' + viewer.y + ',' + viewer.z + ')' ];
	$pre.innerHTML = str.join('\n');
}();
// *******************************************
// DRAW
// *******************************************
var draw = function( timeStamp ) {
	Scene.project();
	Scene.toScreenSpace();
	//Scene.toStringVisibles(true);throw Error();
	Render.draw();
	Scene.clean();
};
draw();
// *******************************************
// GUI
// *******************************************
var gui = new dat.GUI();
var f = gui.addFolder( 'light position' );
f.add( Illumination.lightPos, 'x', -10, 10 ).onChange( draw );
f.add( Illumination.lightPos, 'y', -10, 10 ).onChange( draw );
f.add( Illumination.lightPos, 'z', -10, 10 ).onChange( draw );
f.open();
f = gui.addFolder( 'poly speculary config' );
f.add( plane1, 'specularlyFactor', 0.1, 0.8 ).onChange( draw );
f.add( plane1, 'specularlySpatialDistribution', 1, 10 ).onChange( draw );
f.open();

Comments