A Simple Ray Tracer in JavaScript

The code below implements a toy ray tracer in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.   Or, for an online version, visit http://thiscouldbebetter.neocities.org/raytracer.html.

It’s not exactly optimized, and there’s some stuff in there that isn’t fully implemented, like a subdivision surface algorithm and optimization through bounds checking.

RayTracer

<html>
<body>
<script type='text/javascript'>

// main

function main()
{
	var imageEyeball = ImageHelper.buildImageFromStrings
	(
		"Eyeball",
		1, // scaleMultiplier
		[
			"k","b","w","w","w","w","w","w","w","w"
		]
	);

	var imageRTBang = ImageHelper.buildImageFromStrings	
	(
		"RTBang",
		1, // scaleMultiplier
		[
			"RRRRRRRRRRRRRRRR",
			"RRcccccRcccccRcR",
			"RRcRRRcRRRcRRRcR",
			"RRcRRRcRRRcRRRcR",
			"RRcccccRRRcRRRcR",
			"RRcRRcRRRRcRRRRR",
			"RRcRRRcRRRcRRRcR",
			"RRRRRRRRRRRRRRRR",
		]
	);

	Globals.Instance.mediaHelper.loadImages
	(
		[ 
			imageEyeball,
			imageRTBang,
		],
		main2
	);
}

function main2()
{
	var mediaHelper = Globals.Instance.mediaHelper;

	var materialRTBang = new Material
	(
		"RTBang", 
		Color.Instances.White, 
		1, // diffuse
		1, // specular
		.2, // shininess
		10, // diffuse
		new Texture
		(
			"RTBang", 
			mediaHelper.images["RTBang"]
		)
	);

	var materialEyeball = new Material
	(
		"Eyeball", 
		Color.Instances.White, 
		1, // diffuse
		1, // specular
		.2, // shininess
		10, // diffuse
		new Texture
		(
			"Eyeball", 
			mediaHelper.images["Eyeball"]
		)
	);

	var meshMonolith = MeshHelper.transformMeshVertexPositions
	(
		MeshHelper.buildCubeUnit("Monolith", materialRTBang),
		new TransformMultiple
		([
			new TransformScale(new Coords(40, 10, 90)),
			new TransformTranslate(new Coords(0, 0, -90)),

		])
	);		

	var meshGround = new Mesh
	(
		"Ground",
		// vertices
		[
			new Vertex(new Coords(-1000, -1000, 0)),
			new Vertex(new Coords(1000, -1000, 0)),
			new Vertex(new Coords(1000, 1000, 0)),
			new Vertex(new Coords(-1000, 1000, 0)),
		],
		// vertexIndicesForFaces
		[
			[3, 2, 1, 0]
		],
		//materialsForFaces
		[
			Material.Instances.Green,
		],
		null, // textureUVsForFaceVertices
		null // normalsForFaceVertices
	);

	var sphereTest = new Sphere
	(
		"SphereTest", 
		materialEyeball,
		100, // radius
		new Coords(200, 200, -270),
		new Orientation
		(
			new Coords(1, 0, 0),
			new Coords(1, 1, 0) // down = SE
		)
	);
	
	var displaySize = new Coords(320, 240, 960);

	var scene = new Scene
	(
		"Scene0",
		Color.Instances.BlueDark,
		new Lighting
		(
			// lights
			[
				//new LightAmbient(.1),
				new LightPoint(30000, new Coords(-200, -200, -300)),
				new LightPoint(60000, new Coords(200, -200, -300)),
				new LightPoint(30000, new Coords(200, 200, -300)),
			]
		),
		new Camera
		(
			displaySize.clone(),
			200, // focalLength
			new Coords(-150, -300, -60), // pos
			new Orientation
			(
				new Coords(1, 2, 0), // forward
				new Coords(0, 0, 1) // down
			)
		),
		// collidables
		[
			sphereTest,
			meshMonolith,
			meshGround,
		]
	);

	Globals.Instance.initialize
	(
		displaySize,
		scene
	);
}

// classes

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
	this.minAndMax = [ this.min, this.max ];
	this.size = new Coords(0, 0, 0);

	this.recalculateDerivedValues();
}
{	
	Bounds.prototype.overlapsWith = function(other)
	{
		var returnValue = false;

		var bounds = [ this, other ];

		for (var b = 0; b < bounds.length; b++)
		{
			var boundsThis = bounds[b];
			var boundsOther = bounds[1 - b];			

			var doAllDimensionsOverlapSoFar = true;

			for (var d = 0; d < Coords.NumberOfDimensions; d++)
			{
				if 
				(
					boundsThis.max.dimension(d) <= boundsOther.min.dimension(d)
					|| boundsThis.min.dimension(d) >= boundsOther.max.dimension(d)
				)
				{
					doAllDimensionsOverlapSoFar = false;
					break;
				}
					}

			if (doAllDimensionsOverlapSoFar == true)
			{
				returnValue = true;
				break;
			}
		}

		return returnValue;
	}

	Bounds.prototype.recalculateDerivedValues = function()
	{
		this.size.overwriteWith(this.max).subtract(this.min);
	}
}

function Camera(viewSize, focalLength, pos, orientation)
{
	this.viewSize = viewSize;
	this.focalLength = focalLength;
	this.pos = pos;
	this.orientation = orientation;
}

function Cloneable()
{}
{
	Cloneable.cloneMany = function(cloneablesToClone)
	{
		var returnValues = [];

		for (var i = 0; i < cloneablesToClone.length; i++)
		{
			returnValues.push
			(
				cloneablesToClone[i].clone()
			);
		}

		return returnValues;
	}

	Cloneable.overwriteManyWithOthers = function(cloneablesToOverwrite, cloneablesToOverwriteWith)
	{
		for (var i = 0; i < cloneablesToClone.length; i++)
		{
			cloneablesToOverwrite[i].overwriteWith
			(
				cloneablesToOverwriteWith[i]
			);
		}		
	}
}

function Collision()
{	
	this.pos = new Coords(0, 0, 0);
	this.distanceToCollision = null;
	this.colliders = [];
}
{
	// instance methods

	Collision.prototype.rayAndFace = function(ray, face)
	{
		this.rayAndPlane
		(
			ray,
			face.plane
		);

		if (this.colliders["Plane"] != null)
		{
			if (this.isPosWithinFace(face) == false)
			{
				this.colliders["Face"] = null;
			}
			else
			{
				this.colliders["Face"] = face;
	
				var displacementFromVertex0ToCollision = new Coords(0, 0, 0);

				for (var t = 0; t < face.triangles.length; t++)
				{
					var triangle = face.triangles[t];
					if (this.isPosWithinFace(triangle) == true)
					{
						this.colliders["Triangle"] = triangle;
						break;
					}
				}
			}
		}

		return this;
	}

	Collision.prototype.rayAndPlane = function(ray, plane)
	{
		this.distanceToCollision = 
			(
				plane.distanceFromOrigin 
				- plane.normal.dotProduct(ray.startPos)
			)
			/ plane.normal.dotProduct(ray.direction);

		if (this.distanceToCollision >= 0)
		{
			this.pos.overwriteWith
			(
				ray.direction
			).multiplyScalar
			(
				this.distanceToCollision
			).add
			(
				ray.startPos
			);

			this.colliders["Plane"] = plane;
		}

		return this;
	}

	Collision.prototype.rayAndSphere = function(ray, sphere)
	{
		var rayDirection = ray.direction;
		var displacementFromSphereCenterToCamera = ray.startPos.clone().subtract
		(
			sphere.centerPos
		);
		var sphereRadius = sphere.radius;
		var sphereRadiusSquared = sphereRadius * sphereRadius;

		var a = rayDirection.dotProduct(rayDirection);

		var b = 2 * rayDirection.dotProduct
		(
			displacementFromSphereCenterToCamera
		);

		var c = displacementFromSphereCenterToCamera.dotProduct
		(
			displacementFromSphereCenterToCamera
		) - sphereRadiusSquared;

		var discriminant = (b * b) - 4 * a * c;

		if (discriminant >= 0)
		{
			var rootOfDiscriminant = Math.sqrt(discriminant);

			var distanceToCollision1 = 
				(rootOfDiscriminant - b) 
				/ (2 * a);

			var distanceToCollision2 = 
				(0 - rootOfDiscriminant - b) 
				/ (2 * a);

			if (distanceToCollision1 >= 0)
			{
				if (distanceToCollision2 >= 0 && distanceToCollision2 < distanceToCollision1)
				{
					this.distanceToCollision = distanceToCollision2;
				}
				else
				{
					this.distanceToCollision = distanceToCollision1;
				}
			}
			else
			{
				this.distanceToCollision = distanceToCollision2;				
			}
	
			this.pos.overwriteWith
			(
				ray.direction
			).multiplyScalar
			(
				this.distanceToCollision
			).add
			(
				ray.startPos
			);

			this.colliders["Sphere"] = sphere;
		}

		return this;
	}

	Collision.prototype.isPosWithinFace = function(face)
	{
		var displacementFromVertex0ToCollision = new Coords(0, 0);

		var isPosWithinAllEdgesOfFaceSoFar = true;

		var edges = face.edgesRectified;

		for (var e = 0; e < edges.length; e++)
		{
			var edgeFromFace = edges[e];

			displacementFromVertex0ToCollision.overwriteWith
			(
				this.pos
			).subtract
			(
				edgeFromFace.vertices[0].pos
			);
		
			// hack?
			var epsilon = .01;

			if (displacementFromVertex0ToCollision.dotProduct(edgeFromFace.transverse) >= epsilon)
			{
				isPosWithinAllEdgesOfFaceSoFar = false;
				break;
			}	
		}

		return isPosWithinAllEdgesOfFaceSoFar;
	}

}

function Color(name, codeChar, componentsRGBA)
{
	this.name = name;
	this.codeChar = codeChar;
	this.componentsRGBA = componentsRGBA;
}
{
	// constants

	Color.NumberOfComponentsRGBA = 4;

	// instances

	function Color_Instances()
	{
		this.Transparent = new Color("Transparent", ".", [0, 0, 0, 0]);

		this.Black 	= new Color("Black",	"k", [0, 0, 0, 1]);
		this.Blue 	= new Color("Blue", 	"b", [0, 0, 1, 1]);
		this.BlueDark 	= new Color("BlueDark", "B", [0, 0, .5, 1]);
		this.Cyan 	= new Color("Cyan", 	"c", [0, 1, 1, 1]);
		this.Gray 	= new Color("Gray", 	"a", [.5, .5, .5, 1]);
		this.Green 	= new Color("Green", 	"g", [0, 1, 0, 1]);
		this.GreenDark 	= new Color("GreenDark", "G", [0, .5, 0, 1]);
		this.Orange 	= new Color("Orange", 	"o", [1, .5, 0, 1]);
		this.OrangeDark	= new Color("OrangeDark", "O", [.5, .25, 0, 1]);
		this.Red 	= new Color("Red", 	"r", [1, 0, 0, 1]);
		this.RedDark 	= new Color("RedDark", 	"R", [.5, 0, 0, 1]);
		this.Violet 	= new Color("Violet", 	"v", [1, 0, 1, 1]);
		this.VioletDark	= new Color("VioletDark","V", [.5, 0, .5, 1]);
		this.White 	= new Color("White", 	"w", [1, 1, 1, 1]);
		this.Yellow 	= new Color("Yellow", 	"y", [1, 1, 0, 1]);
		this.YellowDark	= new Color("YellowDark", "Y", [.5, .5, 0, 1]);

		this._All = 
		[
			this.Transparent,

			this.Blue,
			this.BlueDark,
			this.Black,
			this.Cyan,
			this.Gray,
			this.Green,
			this.GreenDark,
			this.Orange,
			this.OrangeDark,
			this.Red,
			this.RedDark,
			this.Violet,
			this.VioletDark,
			this.White,
			this.Yellow,
			this.YellowDark,
		];

		for (var i = 0; i < this._All.length; i++)
		{
			var color = this._All[i];
			this._All[color.codeChar] = color;
		}
	}

	Color.Instances = new Color_Instances();

	// instance methods

	Color.prototype.clone = function()
	{
		return new Color
		(
			this.name, 
			this.codeChar, 
			[
				this.componentsRGBA[0],
				this.componentsRGBA[1],
				this.componentsRGBA[2],
				this.componentsRGBA[3],
			]
		);
	}

	Color.prototype.components = function(red, green, blue, alpha)
	{
		this.componentsRGBA[0] = red;
		this.componentsRGBA[1] = green;
		this.componentsRGBA[2] = blue;
		this.componentsRGBA[3] = alpha;
	}

	Color.prototype.multiply = function(scalar)
	{
		for (var i = 0; i < 3; i++)
		{
			this.componentsRGBA[i] *= scalar;
		}

		return this;
	}

	Color.prototype.overwriteWith = function(other)
	{
		this.name = other.name;
		this.codeChar = other.codeChar;
		for (var i = 0; i < this.componentsRGBA.length; i++)
		{
			this.componentsRGBA[i] = other.componentsRGBA[i];
		}

		return this;
	}

	Color.prototype.systemColor = function()
	{
		var returnValue = 
			"rgba(" 
			+ Math.round(255 * this.componentsRGBA[0]) + ", " 
			+ Math.round(255 * this.componentsRGBA[1]) + ", " 
			+ Math.round(255 * this.componentsRGBA[2]) + ", "
			+ this.componentsRGBA[3] 
			+ ")";	

		return returnValue;	
	}
}

function Constants()
{}
{
	Constants.DegreesPerCircle = 360;
	Constants.RadiansPerCircle = 2 * Math.PI;
	Constants.RadiansPerRightAngle = Math.PI / 2;
	Constants.RadiansPerDegree = 
		Constants.RadiansPerCircle
		/ Constants.DegreesPerCircle;
}

function Coords(x, y, z)
{
	this.x = x;
	this.y = y;
	this.z = z;
}
{
	// constants

	Coords.NumberOfDimensions = 3;

	// instance methods

	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		this.z += other.z;

		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y, this.z);
	}

	Coords.prototype.crossProduct = function(other)
	{
		this.overwriteWithXYZ
		(
			this.y * other.z - this.z * other.y,
			this.z * other.x - this.x * other.z,
			this.x * other.y - this.y * other.x
		);

		return this;
	}

	Coords.prototype.dimensionValues = function()
	{
		return [ this.x, this.y, this.z ];
	}

	Coords.prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;
		this.z /= other.z;

		return this;
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		this.z /= scalar;

		return this;
	}

	Coords.prototype.dotProduct = function(other)
	{
		var returnValue =
			this.x * other.x
			+ this.y * other.y
			+ this.z * other.z;

		return returnValue;
	}

	Coords.prototype.magnitude = function()
	{
		var returnValue = Math.sqrt
		(
			this.x * this.x
			+ this.y * this.y
			+ this.z * this.z
		);

		return returnValue;
	}

	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;
		this.z *= other.z;

		return this;
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		this.z *= scalar;

		return this;
	}

	Coords.prototype.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		this.z = other.z;

		return this;
	}

	Coords.prototype.overwriteWithXYZ = function(x, y, z)
	{
		this.x = x;
		this.y = y;
		this.z = z;

		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		this.z -= other.z;

		return this;
	}

	Coords.prototype.toString = function()
	{
		var returnValue = "(" + this.x + "," + this.y + "," + this.z + ")";

		return returnValue;
	}

	Coords.prototype.trimToRange = function(range)
	{
		if (this.x < 0)
		{
			this.x = 0;
		}
		else if (this.x > range.x)
		{
			this.x = range.x;
		}

		if (this.y < 0)
		{
			this.y = 0;
		}
		else if (this.y > range.y)
		{
			this.y = range.y;
		}

		if (this.z < 0)
		{
			this.z = 0;
		}
		else if (this.z > range.z)
		{
			this.z = range.z;
		}

		return this;
	}
}

function DisplayHelper()
{}
{
	// static variables

	DisplayHelper.Collisions = [];
	DisplayHelper.DirectionFromEyeToPixel = new Coords(0, 0, 0);
	DisplayHelper.DisplacementFromEyeToPixel = new Coords(0, 0, 0);
	DisplayHelper.Material = new Material("DisplayHelperMaterial", new Color("Color", "x", [0, 0, 0, 0]));
	DisplayHelper.PixelColor = new Color("PixelColor", "x", [0, 0, 0, 0]);
	DisplayHelper.TexelColor = new Color("TexelColor", "x", [0, 0, 0, 0]);
	DisplayHelper.TexelUV = new Coords(0, 0, 0);
	DisplayHelper.VertexWeightsAtSurfacePos = [];

	// instance methods

	DisplayHelper.prototype.drawScene = function(scene)
	{
		this.graphics.fillStyle = scene.backgroundColor.systemColor();
		this.graphics.fillRect
		(
			0, 0, 
			this.displaySize.x,
			this.displaySize.y
		)

		var displaySizeHalf = this.displaySizeHalf;

		var pixelPos = new Coords(0, 0);

		for (var y = 0 - displaySizeHalf.y; y < displaySizeHalf.y; y++)
		{
			pixelPos.y = y;

			for (var x = 0 - displaySizeHalf.x; x < this.displaySizeHalf.x; x++)
			{
				pixelPos.x = x;

				this.drawScene_Pixel
				(
					scene,
					pixelPos
				);
			}
		}
	}

	DisplayHelper.prototype.drawScene_Pixel = function
	(
		scene,
		pixelPos
	)
	{
		var collisionClosest = this.drawScene_Pixel_FindClosestCollision
		(
			scene,
			pixelPos
		);	

		if (collisionClosest != null)// && collisionClosest.colliders["Triangle"] != null)
		{	
			var collidable = collisionClosest.colliders["Collidable"];

			var surfaceColor = DisplayHelper.PixelColor;
			var surfaceNormal = new Coords(0, 0, 0);
			var surfaceMaterial = DisplayHelper.Material;

			collidable.surfaceMaterialColorAndNormalForCollision
			(
				scene,
				collisionClosest,
				surfaceMaterial,
				surfaceColor,
				surfaceNormal
			);				

			var intensityFromLightsAll = 0;

			var lights = scene.lighting.lights;

			for (var i = 0; i < lights.length; i++)
			{
				var light = lights[i];

				var intensity = light.intensityForCollisionMaterialNormalAndCamera
				(
					collisionClosest,
					surfaceMaterial,
					surfaceNormal,
					scene.camera
				);
	
				intensityFromLightsAll += intensity;						
			}
	
			surfaceColor.multiply
			(
				intensityFromLightsAll 
			);

			this.graphics.fillStyle = surfaceColor.systemColor();
			this.graphics.fillRect
			(
				pixelPos.x + this.displaySizeHalf.x, 
				pixelPos.y + this.displaySizeHalf.y, 
				1, 1
			);
		}			
	}

	DisplayHelper.prototype.drawScene_Pixel_FindClosestCollision = function
	(
		scene,
		pixelPos
	)
	{
		var camera = scene.camera;
		var cameraOrientation = camera.orientation;

		var displacementFromEyeToPixel = DisplayHelper.DisplacementFromEyeToPixel;
		var cameraOrientationTemp = Orientation.Instances.Camera;
		var cameraForward = cameraOrientationTemp.forward;
		var cameraRight = cameraOrientationTemp.right;
		var cameraDown = cameraOrientationTemp.down;

		displacementFromEyeToPixel.overwriteWith
		(
			cameraForward.overwriteWith
			(
				cameraOrientation.forward
			).multiplyScalar
			(
				camera.focalLength
			)
		).add
		(
			cameraRight.overwriteWith
			(
				cameraOrientation.right
			).multiplyScalar
			(
				pixelPos.x
			)
		).add
		(
			cameraDown.overwriteWith
			(
				cameraOrientation.down
			).multiplyScalar
			(
				pixelPos.y
			)
		);	

		var directionFromEyeToPixel = DisplayHelper.DirectionFromEyeToPixel;
		directionFromEyeToPixel.overwriteWith
		(
			displacementFromEyeToPixel
		).normalize();

		var rayFromEyeToPixel = new Ray
		(
			camera.pos,
			directionFromEyeToPixel
		);		

		var collisions = DisplayHelper.Collisions;
		collisions.length = 0;

		for (var i = 0; i < scene.collidables.length; i++)
		{
			var collidable = scene.collidables[i];

			collidable.addCollisionsWithRayToList
			(
				rayFromEyeToPixel,
				collisions
			);
		}

		var collisionClosest = null;

		if (collisions.length > 0)
		{
			collisionClosest = collisions[0];

			for (var c = 1; c < collisions.length; c++)
			{
				var collision = collisions[c];
				if (collision.distanceToCollision < collisionClosest.distanceToCollision)
				{
					collisionClosest = collision;
				}
			}
		}

		return collisionClosest;
	}

	DisplayHelper.prototype.initialize = function(displaySize)
	{
		this.displaySize = displaySize;
		this.displaySizeHalf = this.displaySize.clone().divideScalar(2);

		var canvas = document.createElement("canvas");
		canvas.width = displaySize.x;
		canvas.height = displaySize.y;
		document.body.appendChild(canvas);

		this.graphics = canvas.getContext("2d");
	}
}

function Edge(vertices)
{
	this.vertices = vertices;
	this.displacement = new Coords(0, 0, 0);
	this.direction = new Coords(0, 0, 0);
	this.transverse = new Coords(0, 0, 0);
	this.children = [];
}
{
	Edge.prototype.assignToVertices = function()
	{
		for (var i = 0; i < this.vertices.length; i++)
		{
			var vertex = this.vertices[i];

			var doesVertexAlreadyBelongToAnyEdgesMatchingThis = false;

			for (var e = 0; e < vertex.edges.length; e++)
			{
				var edge = vertex.edges[e];
				if (edge.equals(this) == true)
				{
					doesVertexAlreadyBelongToAnyEdgesMatchingThis = true;
					break;
				}
			}
	
			if (doesVertexAlreadyBelongToAnyEdgesMatchingThis == false)
			{
				vertex.edges.push(this);
			}
		}
	}

	Edge.prototype.center = function()
	{
		return this.vertices[0].pos.clone().add(this.vertices[1].pos).divideScalar(2);
	}

	Edge.prototype.recalculateDerivedValues = function(faceNormal)
	{
		this.displacement.overwriteWith
		(
			this.vertices[1].pos
		).subtract
		(
			this.vertices[0].pos
		);
		this.length = this.displacement.magnitude();
		this.direction.overwriteWith
		(
			this.displacement
		).divideScalar
		(
			this.length
		);

		this.transverse.overwriteWith(this.direction).crossProduct
		(
			faceNormal
		);

		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.recalculateDerivedValues(faceNormal);
		}
	}

	Edge.prototype.equals = function(other)
	{
		var returnValue = 
		(
			(
				this.vertices[0] == other.vertices[0]
				&& this.vertices[1] == other.vertices[1]
			)
			||
			(
				this.vertices[0] == other.vertices[1]
				&& this.vertices[1] == other.vertices[0]
			)
		);

		return returnValue;		
	}
}

function Face(material, vertices, textureUVsForVertices, normalsForVertices)
{
	this.material = material;
	this.vertices = vertices;
	this.textureUVsForVertices = textureUVsForVertices;
	this.normalsForVertices = normalsForVertices;

	if (this.normalsForVertices != null)
	{
		for (var i = 0; i < this.normalsForVertices.length; i++)
		{
			var normalForVertex = normalsForVertices[i];
			normalForVertex.normalize();
		}
	}

	this.plane = new Plane(Vertex.positionsForMany(this.vertices));
	
	if (this.edgesRectified == null)
	{
		this.edgesRectified = [];

		for (var v = 0; v < this.vertices.length; v++)
		{
			var vNext = v + 1;
			if (vNext >= this.vertices.length)
			{
				vNext = 0;
			}
		
			var vertex = this.vertices[v];
			var vertexNext = this.vertices[vNext];
		
			var edge = new Edge([ vertex, vertexNext ]);
		
			edge.recalculateDerivedValues(this.plane.normal);

			this.edgesRectified.push(edge);
		}	
	}

	this.buildTriangles();
}
{
	// static variables

	Face.DisplacementFromVertexNextToPos = new Coords(0, 0, 0)
	Face.VertexValueInterpolated = new Coords(0, 0, 0);
	Face.VertexValueWeighted = new Coords(0, 0, 0);

	Face.prototype.buildTriangles = function()
	{
		// instance variables
	
		if (this.vertices.length == 3)
		{
			this.triangles = [ this ];
		}
		else if (this.vertices.length == 4)
		{
			this.triangles = 
			[
				this.buildTriangleFromVertexIndices(0, 1, 2),
				this.buildTriangleFromVertexIndices(2, 3, 0),
			];
		}
		else
		{
			var errorMessage = "A Face may only have 3 or 4 vertices.";
			throw errorMessage;
		}
	}

	Face.prototype.buildTriangleFromVertexIndices = function(vertex0, vertex1, vertex2)
	{
		return new Face
		(
			this.material, 
			[
				this.vertices[vertex0],
				this.vertices[vertex1],
				this.vertices[vertex2],
			],
			(
				this.textureUVsForVertices == null
				? null
				:
				[
					this.textureUVsForVertices[vertex0],
					this.textureUVsForVertices[vertex1],
					this.textureUVsForVertices[vertex2],
				]
			),
			(
				this.normalsForVertices == null 
				? null
				:
				[
					this.normalsForVertices[vertex0],
					this.normalsForVertices[vertex1],
					this.normalsForVertices[vertex2],
				]
			)
		);
	}

	Face.prototype.center = function()
	{
		var returnValue = new Coords(0, 0, 0);

		var numberOfVertices = this.vertices.length;

		for (var v = 0; v < numberOfVertices; v++)
		{
			var vertexPos = this.vertices[v].pos;
		}

		returnValue.divideScalar(numberOfVertices);

		return returnValue;
	}

	Face.prototype.interpolateVertexValuesWeighted = function(vertexValues, weights)
	{
		var valueInterpolated = Face.VertexValueInterpolated.overwriteWith
		(
			vertexValues[0]
		).multiplyScalar
		(
			weights[0]
		)

		var vertexValueWeighted = Face.VertexValueWeighted;

		for (var i = 1; i < vertexValues.length; i++)
		{
			vertexValueWeighted.overwriteWith
			(
				vertexValues[i]
			).multiplyScalar
			(
				weights[i]
			);

			valueInterpolated.add(vertexValueWeighted);
		}		
	
		return valueInterpolated;
	}

	Face.prototype.normalForVertexWeights = function(vertexWeights)
	{
		if (this.normalsForVertices == null)
		{
			returnValue = this.plane.normal;
		}
		else
		{
			returnValue = this.interpolateVertexValuesForWeights
			(
				this.normalsForVertices,
				vertexWeights
			);
		}
		
		return returnValue;	
	}

	Face.prototype.recalculateDerivedValues = function()
	{
		this.plane.recalculateDerivedValues();

		for (var e = 0; e < this.edgesRectified.length; e++)
		{
			var edge = this.edgesRectified[e];
			edge.recalculateDerivedValues(this.plane.normal);
		}

		if (this.triangles.length > 1)
		{
			for (var t = 0; t < this.triangles.length; t++)
			{
				var triangle = this.triangles[t];
				triangle.recalculateDerivedValues();
			}
		}
	}

	Face.prototype.texelColorForVertexWeights = function(texture, vertexWeights)
	{
		var texelUV = this.interpolateVertexValuesWeighted
		(
			this.textureUVsForVertices,
			vertexWeights
		);	

		var texelColor = DisplayHelper.TexelColor;

		texture.colorSetFromUV(texelColor, texelUV);

		return texelColor;
	}

	Face.prototype.vertexWeightsAtSurfacePosAddToList = function(surfacePos, weights)
	{
		var vertices = this.vertices;
		var edges = this.edgesRectified;

		var areaOfFace = edges[0].displacement.clone().crossProduct
		(
			edges[1].displacement
		).magnitude() / 2;

		var displacementFromVertexNextToPos = Face.DisplacementFromVertexNextToPos;

		for (var v = 0; v < vertices.length; v++)
		{	
			var vNext = v + 1;
			if (vNext >= this.vertices.length)
			{
				vNext = 0;
			}
			var edgeNext = edges[vNext];
						
			displacementFromVertexNextToPos.overwriteWith
			(
				surfacePos
			).subtract
			(
				edgeNext.vertices[0].pos
			);

			var areaOfTriangleFormedByEdgeNextAndPos = edgeNext.displacement.clone().crossProduct
			(
				displacementFromVertexNextToPos
			).magnitude() / 2;
								
			var weightOfVertex = 
				areaOfTriangleFormedByEdgeNextAndPos
				/ areaOfFace;
			
			weights[v] = weightOfVertex;
		}

		return weights;
	}
}

function Globals()
{
	this.mediaHelper = new MediaHelper();

}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(displaySize, scene)
	{
		this.displayHelper = new DisplayHelper();
		this.displayHelper.initialize(displaySize);

		this.inputHelper = new InputHelper();
		this.inputHelper.initialize();

		this.scene = scene;

		this.displayHelper.drawScene(this.scene);
	}
}

function Image(name, systemImage)
{
	this.name = name;
	this.systemImage = systemImage;
	this.filePath = this.systemImage.src;
}

function ImageHelper()
{}
{
	// static methods

	ImageHelper.buildImageFromStrings = function
	(
		name, 
		scaleMultiplier, 
		stringsForPixels
	)
	{
		var sizeInPixels = new Coords
		(
			stringsForPixels[0].length, stringsForPixels.length
		);

		var canvas = document.createElement("canvas");
		canvas.width = sizeInPixels.x * scaleMultiplier;
		canvas.height = sizeInPixels.y * scaleMultiplier;

		var graphics = canvas.getContext("2d");

		var pixelPos = new Coords(0, 0);
		var colorForPixel = Color.Instances.Transparent;

		for (var y = 0; y < sizeInPixels.y; y++)
		{
			var stringForPixelRow = stringsForPixels[y];
			pixelPos.y = y * scaleMultiplier;

			for (var x = 0; x < sizeInPixels.x; x++)
			{
				var charForPixel = stringForPixelRow[x];
				pixelPos.x = x * scaleMultiplier;

				colorForPixel = Color.Instances._All[charForPixel];

				graphics.fillStyle = colorForPixel.systemColor();
				graphics.fillRect
				(
					pixelPos.x, pixelPos.y, 
					scaleMultiplier, scaleMultiplier
				);				
			}
		}

		var imageFromCanvasURL = canvas.toDataURL("image/png");
		var htmlImageFromCanvas = document.createElement("img");

		htmlImageFromCanvas.width = canvas.width;
		htmlImageFromCanvas.height = canvas.height;
		htmlImageFromCanvas.isLoaded = false;
		htmlImageFromCanvas.onload = function(event) 
		{ 
			event.target.isLoaded = true; 
		}
		htmlImageFromCanvas.src = imageFromCanvasURL;

		var returnValue = new Image(name, htmlImageFromCanvas);

		return returnValue;
	}
}

function InputHelper()
{
	// do nothing
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = Globals.Instance.inputHelper.processKeyDownEvent;
	}

	InputHelper.prototype.processKeyDownEvent = function(event)
	{
		var scene = Globals.Instance.scene
		var camera = scene.camera;

		var keyCode = event.keyCode;

		var distanceToMove = camera.focalLength / 10;
		var amountToTurn = .1;

		if (keyCode == 65) // A
		{
			// move left
			camera.pos.subtract
			(
				camera.orientation.right.clone().multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (keyCode == 68) // D
		{
			// move right
			camera.pos.add
			(
				camera.orientation.right.clone().multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (keyCode == 69) // E
		{
			// roll right
			camera.orientation.overwriteWithForwardDown
			(
				camera.orientation.forward,
				camera.orientation.down.add
				(
					camera.orientation.right.multiplyScalar
					(
						amountToTurn
					)
				)
			);

		}
		else if (keyCode == 70) // F
		{
			// fall
			camera.pos.add
			(
				camera.orientation.down.clone().multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (keyCode == 81) // Q
		{
			// roll left
			camera.orientation.overwriteWithForwardDown
			(
				camera.orientation.forward,
				camera.orientation.down.subtract
				(
					camera.orientation.right.multiplyScalar
					(
						amountToTurn
					)
				)
			);

		}
		else if (keyCode == 82) // R
		{
			// rise
			camera.pos.subtract
			(
				camera.orientation.down.clone().multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (keyCode == 83) // S
		{
			// move back
			camera.pos.subtract
			(
				camera.orientation.forward.clone().multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (keyCode == 87) // W
		{
			// move forward
			camera.pos.add
			(
				camera.orientation.forward.clone().multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (keyCode == 90) // Z
		{
			// turn left
			camera.orientation.overwriteWithForwardDown
			(
				camera.orientation.forward.subtract
				(
					camera.orientation.right.multiplyScalar
					(
						amountToTurn
					)
				),
				camera.orientation.down
			);
		}
		else if (keyCode == 67) // C
		{
			// turn right
			camera.orientation.overwriteWithForwardDown
			(
				camera.orientation.forward.add
				(
					camera.orientation.right.multiplyScalar
					(
						amountToTurn
					)
				),
				camera.orientation.down
			);
		}
		else if (keyCode == 88) // X 
		{
			// cancel roll
			camera.orientation.overwriteWithForwardDown
			(
				camera.orientation.forward,
				new Coords(0, 0, 1)
			);
		}

		Globals.Instance.displayHelper.drawScene
		(
			Globals.Instance.scene
		);
	}
}

function LightAmbient(intensity)
{
	this.intensity = intensity;
}
{
	LightAmbient.prototype.intensityForCollisionNormalAndCamera = function(collision, normal, camera)
	{
		return this.intensity;
	}
}

function LightDirectional(intensity, orientation)
{
	this.intensity = intensity;
	this.orientation = orientation;
}
{
	LightDirectional.prototype.intensityForCollisionMaterialNormalAndCamera = function
	(
		collision, material, normal, camera
	)
	{
		return 0; // todo
	}
}

function LightPoint(intensity, pos)
{
	this.intensity = intensity;
	this.pos = pos;
}
{
	LightPoint.prototype.intensityForCollisionMaterialNormalAndCamera = function
	(
		collision, material, normal, camera
	)
	{
		var displacementFromObjectToLight = Lighting.Temp;

		displacementFromObjectToLight.overwriteWith
		(
			this.pos
		).subtract
		(
			collision.pos
		);

		var distanceFromLightToObject = displacementFromObjectToLight.magnitude();
		var distanceFromLightToObjectSquared = Math.pow
		(
			distanceFromLightToObject, 2
		);

		var surfaceNormal = Lighting.Temp2.overwriteWith(normal);

		var directionFromObjectToLight = displacementFromObjectToLight.normalize();

		var directionFromObjectToLightDotSurfaceNormal = directionFromObjectToLight.dotProduct
		(
			surfaceNormal
		);

		var returnValue = 0;

		if (directionFromObjectToLightDotSurfaceNormal > 0)
		{
			var diffuseComponent = 
				material.diffuse
				* directionFromObjectToLightDotSurfaceNormal
				* this.intensity
				/ distanceFromLightToObjectSquared;

			var directionOfReflection = 
				surfaceNormal.multiplyScalar
				(
					2 * directionFromObjectToLightDotSurfaceNormal
				).subtract
				(
					directionFromObjectToLight
				);
	
			var directionFromObjectToViewer = Lighting.Temp3.overwriteWith
			(
				camera.pos
			).subtract
			(
				collision.pos
			).normalize();
	
			var specularComponent = 
				material.specular
				* Math.pow
				(
					directionOfReflection.dotProduct(directionFromObjectToViewer),
					material.shininess
				)
				* this.intensity
				/ distanceFromLightToObjectSquared;

			returnValue = diffuseComponent + specularComponent;
		}
	
		return returnValue;
	}
}

function Lighting(lights)
{
	this.lights = lights;
}
{
	Lighting.Temp = new Coords(0, 0, 0);
	Lighting.Temp2 = new Coords(0, 0, 0);
	Lighting.Temp3 = new Coords(0, 0, 0);
}

function Material(name, color, ambient, diffuse, specular, shininess, texture)
{
	this.name = name;
	this.color = color;
	this.ambient = ambient;
	this.diffuse = diffuse;
	this.specular = specular;
	this.shininess = shininess;
	this.texture = texture;
}
{	
	// instances

	function Material_Instances()
	{
		this.Green = new Material("Green", Color.Instances.Green, 1, 1, .2, 0);
		this.White = new Material("White", Color.Instances.White, 1, 1, .2, 0);
	}

	Material.Instances = new Material_Instances();
	
	// methods

	// cloneable

	Material.prototype.clone = function()
	{
		return new Material
		(
			this.name,
			this.color.clone(),
			this.ambient,
			this.diffuse,
			this.specular,
			this.shininess,
			this.texture
		);
	}

	Material.prototype.overwriteWith = function(other)
	{
		this.name = other.name;
		this.color.overwriteWith(other.color);
		this.ambient = other.ambient;
		this.diffuse = other.diffuse;
		this.specular = other.specular;
		this.shininess = other.shininess;
		this.texture = other.texture;
	}
}

function MediaHelper()
{
	this.images = [];
}
{
	MediaHelper.prototype.loadImages = function
	(
		imagesToLoad,
		methodToCallWhenAllImagesLoaded
	)
	{
		for (var i = 0; i < imagesToLoad.length; i++)
		{
			var imageToLoad = imagesToLoad[i];

			this.images.push(imageToLoad);
			this.images[imageToLoad.name] = imageToLoad;
		}

		this.methodToCallWhenAllImagesLoaded = methodToCallWhenAllImagesLoaded;	

		setTimeout
		(
			this.checkWhetherAllImagesAreLoaded, 
			100
		);
	}

	MediaHelper.prototype.checkWhetherAllImagesAreLoaded = function()
	{
		var mediaHelper = Globals.Instance.mediaHelper;

		var numberOfImagesLeftToLoad = 0;

		for (var i = 0; i < mediaHelper.images.length; i++)
		{
			var image = mediaHelper.images[i];
			if (image.systemImage.isLoaded == false)
			{
				numberOfImagesLeftToLoad++;
			}
		}	

		if (numberOfImagesLeftToLoad > 0)
		{
			setTimeout
			(
				mediaHelper.checkWhetherAllImagesAreLoaded, 
				100
			);
		}
		else
		{
			mediaHelper.methodToCallWhenAllImagesLoaded();
		}
	}
}

function Mesh
(
	name, 
	vertices, 
	vertexIndicesForFaces, 
	materialsForFaces,
	textureUVsForFaceVertices,
	normalsForFaceVertices
)
{
	this.name = name;
	this.vertices = vertices;
	this.vertexIndicesForFaces = vertexIndicesForFaces;
	this.materialsForFaces = materialsForFaces;

	if (textureUVsForFaceVertices == null)
	{
		textureUVsForFaceVertices = [];
	}

	if (normalsForFaceVertices == null)
	{
		normalsForFaceVertices = [];
	}

	this.faces = [];

	var numberOfFaces = this.vertexIndicesForFaces.length;

	for (var f = 0; f < numberOfFaces; f++)
	{
		var vertexIndicesForFace = this.vertexIndicesForFaces[f];

		var textureUVsForFace = textureUVsForFaceVertices[f];
		var normalsForFace = normalsForFaceVertices[f];

		var numberOfVerticesInFace = vertexIndicesForFace.length;

		var verticesForFace = [];

		for (var vi = 0; vi < numberOfVerticesInFace; vi++)
		{
			var vertexIndex = vertexIndicesForFace[vi];
			var vertex = this.vertices[vertexIndex];

			verticesForFace.push(vertex);
		}

		var face = new Face
		(
			this.materialsForFaces[f], 
			verticesForFace,
			textureUVsForFace,
			normalsForFace
		);

		this.faces.push(face);
	}

	this.adjacenciesBuild();
}
{
	// constants

	Mesh.VerticesInATriangle = 3;

	// methods

	Mesh.prototype.adjacenciesBuild = function()
	{
		this.edges = [];

		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];

			face.edges = [];

			for (var e = 0; e < face.edgesRectified.length; e++)
			{
				var edgeToAdd = face.edgesRectified[e];

				for (var i = 0; i < this.edges.length; i++)
				{
					var edgeAlreadyAdded = this.edges[i]; 
					if (edgeToAdd.equals(edgeAlreadyAdded) == true)
					{
						hasEdgeAlreadyBeenAdded = true;
						edgeAlreadyAdded.children.push(edgeToAdd);
						edgeToAdd.parent = edgeAlreadyAdded;
						break;
					}

					if (hasEdgeAlreadyBeenAdded == false)
					{
						var edgeParent = new Edge(edgeToAdd.vertices);
	
						edgeParent.children.push(edgeToAdd);
						edgeToAdd.parent = edgeParent;

						edgeParent.assignToVertices();
		
						this.edges.push(edgeParent);
						face.edges.push(edgeParent);
					}
				}				
			}

			for (var v = 0; v < face.vertices.length; v++)
			{
				var faceVertex = face.vertices[v];
				faceVertex.faces.push(face);
			}
		}
	}

	Mesh.prototype.clone = function()
	{
		var returnValue = new Mesh
		(
			this.name,
			Cloneable.cloneMany(vertices), 
			vertexIndicesForFaces, 
			materialsForFaces,
			textureUVsForFaceVertices,
			normalsForFaceVertices
		);

		return returnValue;
	}

	Mesh.prototype.overwriteWith = function(other)
	{
		Cloneable.overwriteManyWithOthers(this.vertices, other.vertices);
	}

	Mesh.prototype.recalculateDerivedValues = function()
	{
		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];
			face.recalculateDerivedValues();
		}

		for (var e = 0; e < this.edges.length; e++)
		{
			var edge = this.edges[e];
			edge.recalculateDerivedValues();
		}
	}

	// collidable

	Mesh.prototype.addCollisionsWithRayToList = function(ray, listToAddTo)
	{	
		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];
	
			if (face.plane.normal.dotProduct(ray.direction) < 0)
			{
				var collision = new Collision().rayAndFace
				(
					ray,
					face	
				);

				if (collision.colliders["Face"] != null)
				{
					collision.colliders["Collidable"] = this;
					listToAddTo.push(collision);
				}
			}
		}

		return listToAddTo;
	}

	Mesh.prototype.surfaceMaterialColorAndNormalForCollision = function
	(
		scene, 
		collisionClosest,
		surfaceMaterial,
		surfaceColor,
		surfaceNormal
	)
	{
		var face = collisionClosest.colliders["Triangle"];
		var surfacePos = collisionClosest.pos;

		var vertexWeightsAtSurfacePos = face.vertexWeightsAtSurfacePosAddToList
		(
			surfacePos,
			DisplayHelper.VertexWeightsAtSurfacePos
		);

		surfaceMaterial.overwriteWith(face.material);

		if (surfaceMaterial.texture == null)
		{
			surfaceColor.overwriteWith
			(
				surfaceMaterial.color
			);
		}
		else
		{
			var texelColor = face.texelColorForVertexWeights
			(
				face.material.texture, 
				vertexWeightsAtSurfacePos
			);
		
			if (texelColor != null)
			{			
				surfaceColor.overwriteWith(texelColor);
			}
		}

		surfaceNormal.overwriteWith
		(
			face.normalForVertexWeights
			(
				vertexWeightsAtSurfacePos
			)
		); 
			
		return surfaceColor;
	}
}

function MeshHelper()
{
	// static class
}
{
	MeshHelper.buildCubeUnit = function(name, material)
	{
		var returnValue = new Mesh
		(
			name, 
			// vertices
			[ 
				new Vertex(new Coords(-1, -1, -1)), // 0
				new Vertex(new Coords(1, -1, -1)), // 1
				new Vertex(new Coords(1, 1, -1)), // 2
				new Vertex(new Coords(-1, 1, -1)), // 3
		
				new Vertex(new Coords(-1, -1, 1)), // 4
				new Vertex(new Coords(1, -1, 1)), // 5
				new Vertex(new Coords(1, 1, 1)), // 6
				new Vertex(new Coords(-1, 1, 1)), // 7
			],
			// vertexIndicesForFaces
			[
				[3, 2, 1, 0], // top
				[4, 5, 6, 7], // bottom

				[0, 1, 5, 4], // north
				[2, 3, 7, 6], // south
	
				[1, 2, 6, 5], // east 
				[4, 7, 3, 0], // west				
			],
			// materialsForFaces
			[
				material,
				material,
				material,
				material,
				material,
				material,
			],
			// textureUVsForFaceVertices
			[
				[ new Coords(0, 1), new Coords(1, 1), new Coords(1, 0), new Coords(0, 0) ],
				[ new Coords(0, 0), new Coords(1, 0), new Coords(1, 1), new Coords(0, 1) ],
				[ new Coords(1, 0), new Coords(0, 0), new Coords(0, 1), new Coords(1, 1) ],
				[ new Coords(1, 0), new Coords(0, 0), new Coords(0, 1), new Coords(1, 1) ],
				[ new Coords(1, 0), new Coords(0, 0), new Coords(0, 1), new Coords(1, 1) ],
				[ new Coords(0, 1), new Coords(1, 1), new Coords(1, 0), new Coords(0, 0) ],
			],
			 // normalsForFaceVertices
			[
				null,
				null,
				null,
				null,
				null,
				null,
			]
		);

		return returnValue;
	}

	MeshHelper.subdivideMesh = function(mesh)
	{
		// Adapted from pseudocode found at the URL:
		//  https://en.wikipedia.org/wiki/Catmull%E2%80%93Clark_subdivision_surface

		// For each face, add a face point.

		var faceSplitPoints = [];

		for (var f = 0; f < this.faces.length; f++)
		{
			// Set each face point to be the average of all original points for the respective face.

			var face = this.faces[f];
			var faceSplitPoint = face.center();

			faceSplitPoints.push(faceSplitPoint);
			faceSplitPoints[face] = faceSplitPoints;
		}

		// For each edge, add an edge point.

		var edgeSplitPoints = [];

		for (var e = 0; e < this.edges.length; e++)
		{
			// Set each edge point to be the average of the two neighbouring face points and its two original endpoints.

			var edge = this.edges[e];
			edgeSplitPoint = edge.center();
			var faceSplitPointsAveraged = new Coords(0, 0, 0);
			for (var f = 0; f < edge.faces.length; f++)
			{
				var face = edge.faces[f];
				var faceSplitPoint = faceSplitPoints[null]; // todo
				faceSplitPoints.add(faceSplitPoint);
			}
			faceSplitPointsAveraged.divideScalar(2);
			edgeSplitPoint.add(faceSplitPointsAveraged).divideScalar(2);
			edgeSplitPoints.push(edgeSplitPoint);
			edgeSplitPoints[edge] = edgeSplitPoint;
		}

		var edgeEndpointPairsAfterSubdivision = [];

		// For each face point, add an edge for every edge of the face, 
		// connecting the face point to each edge point for the face.

		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];
			var faceSplitPoint = faceSplitPoints[face];

			for (var e = 0; e < face.edges.length; e++)
			{
				var edge = face.edges[e];
				var edgeSplitPoint = edgeSplitPoints[edge];
				var edgeEndpointPair = 
				[
					faceSplitPoint, 
					edgeSplitPoint
				];
				edgeEndpointPairsAfterSubdivision.push
				(
					edgeEndpointPair
				);
			}
		}	

		var vertexPositionsMoved = [];

		// For each original point P, 

		for (var v = 0; v < this.vertices.length; v++)
		{
			var vertex = this.vertices[v];

			// Take the average F of all n (recently created) face points 
			// for faces touching P.

			var faceSplitPointsAveraged = new Coords(0, 0, 0);
			var facesAdjacent = vertex.faces;
			var numberOfFacesAdjacent = facesAdjacent.length;
			for (var f = 0; f < numberOfFacesAdjacent; f++)
			{
				var faceAdjacent = facesAdjacent[f];
				var faceSplitPoint = faceSplitPoints[faceAdjacent];
				faceSplitPointsAveraged.add(faceSplitPoint);
			}
			faceSplitPointsAveraged.divideScalar(numberOfFacesAdjacent);

			// and take the average R of all n edge midpoints for edges touching P, 
			// where each edge midpoint is the average of its two endpoint vertices.

			var edgesAdjacent = vertex.edges;
			var numberOfEdgesAdjacent = edgesAdjacent.length;
			for (var e = 0; e < numberOfEdgesAdjacent; e++)
			{
				var edgeAdjacent = edgesAdjacent[e];
				var edgeMidpoint = edge.center();
				edgeMidpointsAveraged.add(edgeMidpoint);
			}
			edgeMidpointsAveraged.divideScalar(numberOfEdgesAdjacent);

			// Move each original point to the point:
			// (F + 2R + (n-3)P) / n
			// This is the barycenter of P, R and F with respective weights (n - 3), 2 and 1.

			var vertexPosMoved = vertex.pos.clone().multiplyScalar
			(
				numberOfFacesAdjacent - 3
			).overwriteWith
			(
				faceSplitPointsAverage
			).add
			(
				edgeMidpointsAverage
			).divideScalar
			(
				numberOfFacesAdjacent
			);

			vertexPositionsMoved.push(vertexPosMoved);

			// Connect each new vertex point to the new edge points 
			// of all original edges incident on the original vertex.

			for (var e = 0; e < numberOfEdgesAdjacent; e++)
			{
				var edgeAdjacent = edgesAdjacent[e];
				var edgeSplitPoint = edgeSplitPoints[edgeAdjacent];
				var edgeEndpointPair = 
				[
					vertexPosMoved,
					edgeSplitPoint
				];
				edgeEndpointPairsAfterSubdivision.push
				(
					edgeEndpointPair
				);
			}

		} // end for each original vertex

		var vertexPositionsAfterSubdivision = faceSplitPoints.concat
		(
			edgeSplitPoints
		).concat
		(
			vertexPositionsMoved
		);

		var verticesAfterSubdivision = Vertex.buildManyFromPositions
		(
			vertexPositionsAfterSubdivision
		);		

		// Define new faces as enclosed by edges.

		for (var e = 0; e < edgeEndpointPairsAfterSubdivision.length; e++)
		{
			var edgeEndpointPair = edgeEndpointPairsAfterSubdivision;
			// todo
		}

		// todo

		var returnValue = new Mesh
		(
			mesh.name,
			verticesAfterSubdivision,
			null, // vertexIndicesForFaces, 
			null, // materialsForFaces,
			null, // textureUVsForFaceVertices,
			null // normalsForFaceVertices			
		);

		return returnValue;
	}

	MeshHelper.transformMeshVertexPositions = function(mesh, transform)
	{
		for (var v = 0; v < mesh.vertices.length; v++)
		{
			var vertex = mesh.vertices[v];
			transform.transformCoords(vertex.pos);
		}

		mesh.recalculateDerivedValues();

		return mesh;
	}
}

function Orientation(forward, down)
{
	this.forward = new Coords();
	this.right = new Coords();
	this.down = new Coords();

	this.overwriteWithForwardDown(forward, down);
}
{
	// instance methods

	Orientation.prototype.clone = function()
	{
		return new Orientation
		(
			this.forward.clone(), 
			this.down.clone()
		);
	}

	Orientation.prototype.overwriteWithForwardDown = function(forward, down)
	{
		this.forward.overwriteWith(forward).normalize();
		this.right.overwriteWith(down).crossProduct(this.forward).normalize();
		this.down.overwriteWith(this.forward).crossProduct(this.right).normalize();
	}

	// instances

	function Orientation_Instances()
	{
		this.Camera = new Orientation
		(
			new Coords(1, 0, 0),
			new Coords(0, 0, 1)
		);

		this.ForwardXDownZ = new Orientation
		(
			new Coords(1, 0, 0),
			new Coords(0, 0, 1)
		);

		this.ForwardZDownX = new Orientation
		(
			new Coords(0, 0, 1),
			new Coords(1, 0, 0)
		);
	}

	Orientation.Instances = new Orientation_Instances();
}

function Plane(positionsOnPlane)
{
	this.positionsOnPlane = positionsOnPlane;
	this.normal = new Coords(0, 0, 0);
	this.recalculateDerivedValues();
}
{
	Plane.prototype.recalculateDerivedValues = function()
	{
		var pos0 = this.positionsOnPlane[0];
		var displacementFromPos0To1 = this.positionsOnPlane[1].clone().subtract(pos0);
		var displacementFromPos0To2 = this.positionsOnPlane[2].clone().subtract(pos0);
		this.normal.overwriteWith
		(
			displacementFromPos0To1
		).crossProduct
		(
			displacementFromPos0To2
		).normalize();

		this.distanceFromOrigin = this.normal.dotProduct(pos0);
	}
}

function Polar(azimuth, elevation, radius)
{
	this.azimuth = azimuth;
	this.elevation = elevation;
	this.radius = radius;
}
{
	Polar.prototype.fromCoords = function(coordsToConvert)
	{
		this.radius = coordsToConvert.magnitude();

		this.azimuth = Math.atan2
		(
			coordsToConvert.y,
			coordsToConvert.x
		) / Constants.RadiansPerCircle;

		if (this.azimuth < 0)
		{
			this.azimuth += 1;
		}

		this.elevation = Math.asin
		(
			coordsToConvert.z / this.radius
		) / Constants.RadiansPerRightAngle;
	
		return this;
	}

	Polar.prototype.toCoords = function(coordsToOverwrite)
	{
		var azimuthInRadians = this.azimuth * Constants.RadiansPerCircle;
		var elevationInRadians = this.elevation * Constants.RadiansPerRightAngle;
		var cosineOfElevation = Math.cos(elevationInRadians);

		coordsToOverwrite.overwriteWithXYZ
		(
			cosineOfElevation * Math.cos(this.azimuth),
			cosineOfElevation * Math.sin(this.azimuth),
			Math.sin(elevationInRadians)
		).multiplyScalar
		(
			this.radius
		);
	}
}

function Ray(startPos, direction)
{
	this.startPos = startPos;
	this.direction = direction;
}

function Scene(name, backgroundColor, lighting, camera, collidables)
{
	this.name = name;
	this.backgroundColor = backgroundColor;
	this.lighting = lighting;
	this.camera = camera;
	this.collidables = collidables;
}


function Sphere(name, material, radius, centerPos, orientation)
{
	this.name = name;
	this.material = material;
	this.radius = radius;
	this.centerPos = centerPos;
	this.orientation = orientation;
}
{
	// collidable

	Sphere.prototype.addCollisionsWithRayToList = function(ray, listToAddTo)
	{	
		var collision = new Collision().rayAndSphere
		(
			ray,
			this
		);

		if (collision.colliders["Sphere"] != null)
		{
			collision.colliders["Collidable"] = this;
			listToAddTo.push(collision);
		}

		return listToAddTo;
	}

	Sphere.prototype.surfaceMaterialColorAndNormalForCollision = function
	(
		scene, 
		collisionClosest,
		surfaceMaterial,
		surfaceColor,
		surfaceNormal
	)
	{
		var sphere = collisionClosest.colliders["Sphere"];
		var surfacePos = collisionClosest.pos;
		surfaceMaterial.overwriteWith(sphere.material);

		surfaceNormal.overwriteWith
		(
			surfacePos
		).subtract
		(
			sphere.centerPos
		).normalize();

		if (surfaceMaterial.texture == null)
		{
			surfaceColor.overwriteWith
			(
				surfaceMaterial.color
			);
		}
		else
		{
			var surfaceNormalInLocalCoords = new TransformOrient
			(
				this.orientation
			).transformCoords
			(
				surfaceNormal.clone()
			);

			var surfaceNormalInLocalCoordsAsPolar = new Polar().fromCoords
			(
				surfaceNormalInLocalCoords
			);

			var texelUV = DisplayHelper.TexelUV;
			texelUV.overwriteWithXYZ
			(
				surfaceNormalInLocalCoordsAsPolar.azimuth,
				(1 + surfaceNormalInLocalCoordsAsPolar.elevation) / 2
			); // todo

			surfaceMaterial.texture.colorSetFromUV
			(
				surfaceColor,
				texelUV
			);
		}
			
		return surfaceColor;
	}
}

function Texture(name, image)
{
	this.name = name;
	this.image = image;

	
	this.canvas = document.createElement("canvas");
	this.graphics = this.canvas.getContext("2d");
	this.graphics.drawImage(this.image.systemImage, 0, 0);
}
{
	Texture.prototype.colorSetFromUV = function(texelColor, texelUV)
	{
		var systemImage = this.image.systemImage;

		var texelColorComponents = this.graphics.getImageData
		(
			texelUV.x * systemImage.width, 
			texelUV.y * systemImage.height,
			1, 1
		).data;
	
		texelColor.components
		(
			texelColorComponents[0] / 255, 
			texelColorComponents[1] / 255, 
			texelColorComponents[2] / 255, 
			1 // alpha
		);
	}
}

function TransformMultiple(children)
{
	this.children = children;
}
{
	TransformMultiple.prototype.transformCoords = function(coordsToTransform)
	{
		for (var i = 0; i < this.children.length; i++)
		{
			this.children[i].transformCoords(coordsToTransform);
		}

		return coordsToTransform;
	}
}

function TransformOrient(orientation)
{
	this.orientation = orientation;
}
{
	TransformOrient.prototype.transformCoords = function(coordsToTransform)
	{
		coordsToTransform.overwriteWithXYZ
		(
			this.orientation.forward.dotProduct(coordsToTransform),
			this.orientation.right.dotProduct(coordsToTransform),
			this.orientation.down.dotProduct(coordsToTransform)
		);

		return coordsToTransform;
	}
}

function TransformScale(scaleFactors)
{
	this.scaleFactors = scaleFactors;
}
{
	TransformScale.prototype.transformCoords = function(coordsToTransform)
	{
		coordsToTransform.multiply(this.scaleFactors);

		return coordsToTransform;
	}

}

function TransformTranslate(offset)
{
	this.offset = offset;
}
{
	TransformTranslate.prototype.transformCoords = function(coordsToTransform)
	{
		coordsToTransform.add(this.offset);

		return coordsToTransform;
	}
}

function Vertex(pos)
{
	this.pos = pos;
	this.edges = [];
	this.faces = [];
}
{
	Vertex.prototype.clone = function()
	{
		return new Vertex(this.pos.clone());
	}

	Vertex.positionsForMany = function(vertices)
	{
		var returnValues = [];

		for (var i = 0; i < vertices.length; i++)
		{
			returnValues.push(vertices[i].pos);
		}

		return returnValues;
	}
}

// run

main();

</script>
</body>
</html>

Advertisements
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

3 Responses to A Simple Ray Tracer in JavaScript

  1. Mira says:

    Hey, nice peace of code, can i have your email?

    • Thanks, but I’d prefer not to put my email address on the Internet. If you think about it a while, I’m sure you’ll understand why : )

      If you have a question, though, feel free to ask here. There’s plenty of room in the comment section. Only one of the posts on this blog has ever gotten more than three comments, and that was before Google severely downgraded my page rank.

      If you’d like use this code somewhere, feel free to do that too. I’d say, “you not allowed to use this code for evil”, but I mean, how would I stop you? How would I even know? I mean, that’s pretty much between you and your chosen deity and/or moral basis. Besides, how much evil could anyone really do with this?

  2. Pingback: A Parallelization-Ready Ray Tracer in JavaScript | This Could Be Better

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s