A Parallelization-Ready Ray Tracer in JavaScript

The JavaScript code below implements a ray tracer in JavaScript. It is based on an earlier version in a previous post, but has been cleaned up and refactored somewhat, with an eye towards eventually making splitting the view to be rendered into several pieces and rendering them in parallel using the “web workers” feature that major browser manufacturers have relatively recently impemented in JavaScript.

However, this is not feasible at the moment because the texturing depends on the GraphicsContext object, which cannot be used in a web worker, since it is part of the DOM. Specifically, my homegrown attempt to reproduce the functionality of the GetImageData() function met with less-than-attractive results. Some proposed solutions to using HTML5 graphics in web workers are currently under consideration by browser makers, and hopefully someday it may be possible to finish the parallelization effort.

It should be noted that the new version of the ray tracer is somewhat slower than the old. The refactored code, while easier to read and understand, lacks some of the performance optimizations of the older version.

RayTracer


<html>
<body>

<script type="text/javascript">

// main

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

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

	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)),
		],
		// faces
		[
			new Face(Material.Instances.Green.name, [3, 2, 1, 0], null, null)
		]
	);

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

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

	var materials = 
	[
		materialEyeball, 
		materialRTBang, 
		Material.Instances.Green,
	]; 

	var sphereEyeball = new Sphere
	(
		"Eyeball", 
		materialEyeball.name,
		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",
		materials,
		Color.Instances.BlueDark, // backgroundColor
		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
		[
			sphereEyeball,
			meshMonolith,
			meshGround,
		]
	);

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

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			this[key] = element;
		}

		return this;
	}
}

// classes

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

	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();
	this.distanceToCollision = null;
	this.colliders = [];
}
{
	// instance methods

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

		if (this.colliders["Plane"] != null)
		{
			if (this.isPosWithinFace(mesh, face) == false)
			{
				this.colliders["Face"] = null;
			}
			else
			{
				this.colliders["Face"] = face;
	
				for (var t = 0; t < face.triangles.length; t++)
				{
					var triangle = face.triangles[t];
					if (this.isPosWithinFace(mesh, 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(mesh, face)
	{
		var displacementFromVertex0ToCollision = new Coords();

		var isPosWithinAllEdgesOfFaceSoFar = true;

		var edges = face.edges;

		for (var i = 0; i < edges.length; i++)
		{
			var edge = edges[i];

			displacementFromVertex0ToCollision.overwriteWith
			(
				this.pos
			).subtract
			(
				edge.vertex(mesh, 0).pos
			);

			var edgeTransverse = edge.direction.clone().crossProduct
			(
				face.plane.normal
			);
					
			// hack?
			var epsilon = .01;

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

		return isPosWithinAllEdgesOfFaceSoFar;
	}

}

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

	Color.NumberOfComponentsRGBA = 4;
	Color.ComponentMax = 255;

	// 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,
		];

		this._All.addLookups("codeChar");

	}

	Color.Instances = new Color_Instances();

	// static methods

	Color.blank = function(name)
	{
		return new Color(name, "x", [0, 0, 0, 0]);
	}

	// 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(Color.ComponentMax * this.componentsRGBA[0]) + ", " 
			+ Math.round(Color.ComponentMax * this.componentsRGBA[1]) + ", " 
			+ Math.round(Color.ComponentMax * 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 Display()
{}
{
	// static variables

	Display.Collisions = [];
	Display.DirectionFromEyeToPixel = new Coords();
	Display.DisplacementFromEyeToPixel = new Coords();
	Display.Material = new Material("DisplayMaterial", Color.blank("MaterialColor"));
	Display.PixelColor = Color.blank("PixelColor");
	Display.SurfaceNormal = new Coords();
	Display.TexelColor = Color.blank("TexelColor");
	Display.TexelUV = new Coords();
	Display.VertexWeightsAtSurfacePos = [];

	// instance methods

	Display.prototype.drawScene = function(scene)
	{
		this.drawScene_Background(scene);

		var boundsForTiles = [];

		var sizeInTiles = new Coords(1, 1);
		var tileSizeInPixels = this.sizeInPixels.clone().divide
		(
			sizeInTiles
		);

		var tilePosInTiles = new Coords();
	 	var tileBounds = new Bounds
		(
			new Coords(),
			new Coords()
		);
		
		for (var y = 0; y < sizeInTiles.y; y++)
		{
			tilePosInTiles.y = y;

			for (var x = 0; x < sizeInTiles.x; x++)
			{
				tilePosInTiles.x = x;

				tileBounds.min.overwriteWith
				(
					tilePosInTiles
				).multiply
				(
					tileSizeInPixels
				);

				tileBounds.max.overwriteWith
				(
					tileBounds.min
				).add
				(
					tileSizeInPixels
				);

				this.drawScene_PixelsGetAndDrawForBounds
				(
					scene, tileBounds
				);
			}
		}
	}

	Display.prototype.drawScene_Background = function(scene)
	{
		this.graphics.fillStyle = scene.backgroundColor.systemColor();
		this.graphics.fillRect
		(
			0, 0, 
			this.sizeInPixels.x,
			this.sizeInPixels.y
		);
	}

	Display.prototype.drawScene_PixelsGetAndDrawForBounds = function(scene, bounds)
	{
		// todo
		// It's currently impossible to use DOM objects,
		// including Canvas and GraphicsContext objects,
		// within a web worker. Hopefully this will 
		// change in the future.

		var returnValues = [];
		
		var pixelPos = new Coords();
		var pixelColor = Display.PixelColor;

		var boundsMin = bounds.min;
		var boundsMax = bounds.max;

		var sceneBackgroundColor = scene.backgroundColor;

		for (var y = boundsMin.y; y < boundsMax.y; y++)
		{
			pixelPos.y = y;

			for (var x = boundsMin.x; x < boundsMax.x; x++)
			{
				pixelPos.x = x;

				var collisionForPixel = this.drawScene_ColorSetFromPixelAtPos
				(
					scene,
					pixelColor,
					pixelPos
				);

				if (collisionForPixel == null)
				{
					pixelColor.overwriteWith(sceneBackgroundColor);
				}

				this.graphics.fillStyle = 
					pixelColor.systemColor();

				this.graphics.fillRect
				(
					pixelPos.x, 
					pixelPos.y, 
					1, 1
				);
			}
		}
	}

	Display.prototype.drawScene_ColorSetFromPixelAtPos = function
	(
		scene,
		surfaceColor,
		pixelPos
	)
	{
		var collisionClosest = this.drawScene_Pixel_FindClosestCollision
		(
			scene,
			pixelPos
		);	

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

			var surfaceNormal = Display.SurfaceNormal;
			var surfaceMaterial = Display.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 
			);
		}	

		return collisionClosest;
	}

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

		var displacementFromEyeToPixel = Display.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 - this.sizeInPixelsHalf.x
			)
		).add
		(
			cameraDown.overwriteWith
			(
				cameraOrientation.down
			).multiplyScalar
			(
				pixelPos.y - this.sizeInPixelsHalf.y
			)
		);	

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

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

		var collisions = Display.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;
	}

	Display.prototype.initialize = function(sizeInPixels)
	{
		this.sizeInPixels = sizeInPixels;
		this.sizeInPixelsHalf = this.sizeInPixels.clone().divideScalar(2);

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

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

function Edge(vertexIndices)
{
	this.vertexIndices = vertexIndices;

	this.vertices = null;
	this.displacement = new Coords();
	this.direction = new Coords();
	this.transverse = new Coords();
}
{
	Edge.prototype.recalculateDerivedValues = function(mesh, face)
	{
		if (this.vertices == null)
		{
			this.vertices = [this.vertex(mesh, 0), this.vertex(mesh, 1)];
		}

		this.displacement.overwriteWith
		(
			this.vertices[1].pos
		).subtract
		(
			this.vertices[0].pos
		);

		this.direction.overwriteWith
		(
			this.displacement
		).normalize();

		this.transverse.overwriteWith
		(
			this.direction
		).crossProduct
		(
			face.plane.normal
		);
	}

	Edge.prototype.vertex = function(mesh, vertexIndexIndex)
	{
		var vertexIndex = this.vertexIndices[vertexIndexIndex];
		var vertex = mesh.vertices[vertexIndex];
		return vertex;
	}
}

function Face(materialName, vertexIndices, textureUVsForVertices, normalsForVertices)
{
	this.materialName = materialName;
	this.vertexIndices = vertexIndices;
	this.textureUVsForVertices = textureUVsForVertices;
	this.normalsForVertices = normalsForVertices;
}
{
	// static variables

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

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

	Face.prototype.buildTriangle = function(vertexIndexIndex0, vertexIndexIndex1, vertexIndexIndex2)
	{
		var vertexIndex0 = this.vertexIndices[vertexIndexIndex0];
		var vertexIndex1 = this.vertexIndices[vertexIndexIndex1];
		var vertexIndex2 = this.vertexIndices[vertexIndexIndex2];
		
		var returnValue = new Face
		(
			this.materialName, 
			[
				vertexIndex0,
				vertexIndex1,
				vertexIndex2,
			],
			(
				this.textureUVsForVertices == null
				? null
				:
				[
					this.textureUVsForVertices[vertexIndexIndex0],
					this.textureUVsForVertices[vertexIndexIndex1],
					this.textureUVsForVertices[vertexIndexIndex2],
				]
			),
			(
				this.normalsForVertices == null 
				? null
				:
				[
					this.normalsForVertices[vertexIndexIndex0],
					this.normalsForVertices[vertexIndexIndex1],
					this.normalsForVertices[vertexIndexIndex2],
				]
			)
		);

		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.material = function(scene)
	{
		return scene.materials[this.materialName];
	}

	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(mesh)
	{
		if (this.normalsForVertices != null)
		{
			for (var i = 0; i < this.normalsForVertices.length; i++)
			{
				var normalForVertex = normalsForVertices[i];
				normalForVertex.normalize();
			}
		}

		var vertices = this.vertices(mesh);

		if (this.plane == null)
		{
			this.plane = new Plane
			(
				Vertex.positionsForMany(vertices)
			);
		}
		else
		{
			this.plane.recalculateDerivedValues();
		}


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

		if (this.edges == null)
		{
			this.edges = [];

			for (var i = 0; i < this.vertexIndices.length; i++)
			{
				var iNext = NumberHelper.wrapValueToRange
				(
					i + 1, this.vertexIndices.length
				);

				var vertexIndex = this.vertexIndices[i];
				var vertexIndexNext = this.vertexIndices[iNext];

				var edge = new Edge([vertexIndex, vertexIndexNext]);
				
				this.edges.push(edge);
			}

		}

		for (var i = 0; i < this.edges.length; i++)
		{
			this.edges[i].recalculateDerivedValues(mesh, this);
		}	

		return this;
	}

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

		var texelColor = Display.TexelColor;

		texture.colorSetFromUV(texelColor, texelUV);

		return texelColor;
	}

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

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

		var displacementFromVertexNextToPos = Face.DisplacementFromVertexNextToPos;

		for (var i = 0; i < vertices.length; i++)
		{	
			var iNext = NumberHelper.wrapValueToRange(i + 1, vertices.length);

			var vertex = vertices[i];
			var vertexNext = vertices[iNext];
				
			displacementFromVertexNextToPos.overwriteWith
			(
				surfacePos
			).subtract
			(
				vertexNext.pos
			);

			var displacementOfEdgeNext = edges[iNext].displacement;

			var areaOfTriangleFormedByEdgeNextAndPos = displacementOfEdgeNext.clone().crossProduct
			(
				displacementFromVertexNextToPos
			).magnitude() / 2;
								
			var weightOfVertex = 
				areaOfTriangleFormedByEdgeNextAndPos
				/ areaOfFace;
			
			weights[i] = weightOfVertex;
		}

		return weights;
	}

	Face.prototype.vertex = function(mesh, vertexIndexIndex)
	{
		var vertexIndex = this.vertexIndices[vertexIndexIndex];
		var vertex = mesh.vertices[vertexIndex];
		return vertex;
	}

	Face.prototype.vertices = function(mesh)
	{
		var returnValues = [];

		for (var i = 0; i < this.vertexIndices.length; i++)
		{
			var vertexIndex = this.vertexIndices[i];
			var vertex = mesh.vertices[vertexIndex];
			returnValues.push(vertex);
		}

		return returnValues;
	}

	// cloneable

	Face.prototype.clone = function()
	{
		// todo - Deep clone.
		return new Face
		(
			this.materialName, 
			this.vertexIndices, 
			this.textureUVsForVertices, 
			this.normalsForVertices
		);
	}

	// strings

	Face.prototype.toString = function(mesh)
	{
		var returnValue = this.vertices(mesh).join("->");
		return returnValue;
	}

}

function Globals()
{
	// do nothing
}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(displaySize, scene)
	{
		this.display = new Display();
		this.display.initialize(displaySize);

		this.scene = scene;

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

function Image(name, sizeInPixels, imageData)
{
	this.name = name;
	this.sizeInPixels = sizeInPixels;
	this.imageData = imageData;
}
{
	Image.prototype.systemImage = function()
	{
		if (this._systemImage == null)
		{		
			var canvas = document.createElement("canvas");
			canvas.width = this.sizeInPixels.x;
			canvas.height = this.sizeInPixels.y;

			var graphics = canvas.getContext("2d");
			graphics.putImageData(this.imageData, 0, 0);

			var imageFromCanvasURL = canvas.toDataURL("image/png");

			var systemImage = document.createElement("img");
			systemImage.width = canvas.width;
			systemImage.height = canvas.height;
			systemImage.isLoaded = false;
			systemImage.onload = function(event) 
			{ 
				event.target.isLoaded = true; 
			}
			systemImage.src = imageFromCanvasURL;

			this._systemImage = systemImage;

		}		

		return this._systemImage;
		
	}
}

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 imageData = graphics.getImageData
		(
			0, 0, sizeInPixels.x, sizeInPixels.y
		);

		var returnValue = new Image
		(
			name, sizeInPixels, imageData
		);

		return returnValue;
	}
}

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();
	Lighting.Temp2 = new Coords();
	Lighting.Temp3 = new Coords();
}

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 Mesh
(
	name, 
	vertices, 
	faces
)
{
	this.name = name;
	this.vertices = vertices;
	this.faces = faces;
	this.recalculateDerivedValues();
}
{
	// constants

	Mesh.VerticesInATriangle = 3;

	// methods

	Mesh.prototype.clone = function()
	{
		var returnValue = new Mesh
		(
			this.name,
			Cloneable.cloneMany(this.vertices), 
			Cloneable.cloneMany(this.faces)
		);

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

	// 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,
					this, // mesh
					face	
				);

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

		return listToAddTo;
	}

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

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

		var vertexWeightsAtSurfacePos = face.vertexWeightsAtSurfacePosAddToList
		(
			this, // mesh
			surfacePos,
			Display.VertexWeightsAtSurfacePos
		);

		surfaceMaterial.overwriteWith(face.material(scene));

		if (surfaceMaterial.texture == null)
		{
			surfaceColor.overwriteWith
			(
				surfaceMaterial.color
			);
		}
		else
		{
			var texelColor = face.texelColorForVertexWeights
			(
				surfaceMaterial.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 materialName = material.name;

		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
			],
			// faces
			[
				new Face(materialName, [3, 2, 1, 0], [ new Coords(0, 1), new Coords(1, 1), new Coords(1, 0), new Coords(0, 0) ], null), // top
				new Face(materialName, [4, 5, 6, 7], [ new Coords(0, 0), new Coords(1, 0), new Coords(1, 1), new Coords(0, 1) ], null), // bottom

				new Face(materialName, [0, 1, 5, 4], [ new Coords(1, 0), new Coords(0, 0), new Coords(0, 1), new Coords(1, 1) ], null), // north
				new Face(materialName, [2, 3, 7, 6], [ new Coords(1, 0), new Coords(0, 0), new Coords(0, 1), new Coords(1, 1) ], null), // south
	
				new Face(materialName, [1, 2, 6, 5], [ new Coords(1, 0), new Coords(0, 0), new Coords(0, 1), new Coords(1, 1) ], null), // east 
				new Face(materialName, [4, 7, 3, 0], [ new Coords(0, 1), new Coords(1, 1), new Coords(1, 0), new Coords(0, 0) ], null), // west				
			]
		);

		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 NumberHelper()
{
	// static class
}
{
	NumberHelper.wrapValueToRange = function(valueToWrap, range)
	{
		while (valueToWrap < 0)
		{
			valueToWrap += range;
		}

		while (valueToWrap >= range)
		{
			valueToWrap -= range;
		}

		return valueToWrap;
	}
}

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, materials, backgroundColor, lighting, camera, collidables)
{
	this.name = name;
	this.materials = materials;
	this.backgroundColor = backgroundColor;
	this.lighting = lighting;
	this.camera = camera;
	this.collidables = collidables;

	this.materials.addLookups("name");
}

function Sphere(name, materialName, radius, centerPos, orientation)
{
	this.name = name;
	this.materialName = materialName;
	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.material = function(scene)
	{
		return scene.materials[this.materialName];
	}

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

		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 = Display.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;
	
	var canvas = document.createElement("canvas");
	this.graphics = canvas.getContext("2d");
	this.graphics.drawImage(this.image.systemImage(), 0, 0);
}
{
	Texture.prototype.colorSetFromUV = function(texelColor, texelUV)
	{
		var imageSizeInPixels = this.image.sizeInPixels;

		var texelColorComponents = this.graphics.getImageData
		(
			texelUV.x * imageSizeInPixels.x, 
			texelUV.y * imageSizeInPixels.y,
			1, 1
		).data;
	
		texelColor.components
		(
			texelColorComponents[0] / Color.ComponentMax, 
			texelColorComponents[1] / Color.ComponentMax, 
			texelColorComponents[2] / Color.ComponentMax, 
			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;
}
{
	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;
	}

	// strings

	Vertex.prototype.toString = function()
	{
		return this.pos.toString();
	}
}


// run

main();

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

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

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