Rendering 3D Shapes as Wireframes in JavaScript

The code shown below will render several three-dimensional shapes, or “meshes”, in various positions and orientations for the viewpoint of a particular camera. To see the code in action, copy it into an .html file and open that file in a web browser that has JavaScript enabled.

Note that no effort is made to handle hidden face removal at this time, which means that the shapes will render as “wireframes”. Changing the value of the “hideFacesNotPointedTowardCamera” variable will cause the program attempt to remove the non-visible faces of each shape, but as of this writing that functionality isn’t working as required.

UPDATE 2013/05/04: For a somewhat more advanced demonstration of 3D graphics in JavaScript, see a later post that uses WebGL.

Wireframes


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

function ProjectionTest()
{
	this.main = function()
	{
		var meshCube = Mesh.fromVertexPositions
		(
			"MeshCube",
			// vertexPositions
			[
				new Coords(-1, -1, -1), // 0 - tnw
				new Coords(1, -1, -1), // 1 - tne
				new Coords(1, 1, -1), // 2 - tse
				new Coords(-1, 1, -1), // 3 - tsw

				new Coords(-1, -1, 1), // 4 - bnw
				new Coords(1, -1, 1), // 5 - bne
				new Coords(1, 1, 1), // 6 - bse
				new Coords(-1, 1, 1)  // 7 - bsw
			],
			// vertexIndicesForFaces
			[
				// top
				[ 0, 1, 2, 3 ],
				// bottom
				[ 4, 7, 6, 5 ],
				// north
				[ 0, 1, 5, 4 ],
				// east
				[ 1, 2, 6, 5 ],
				// south
				[ 2, 3, 7, 6 ],
				// west
				[ 0, 3, 7, 4 ],
			]
		);

		var scaleFactor = 6;
		var scaleFactors = new Coords
		(
			scaleFactor, scaleFactor, scaleFactor
		);

		Transform.applyManyToCoordsMany
		(
			[
				new TransformScale(scaleFactors)
			],
			Vertex.getPositionsForMany(meshCube.vertices)
		);

		var scene = new Scene
		(
			"Scene 0",
			new Array
			(
				new Body
				(
					"Cube0",
					Color.Instances.Red,
					new Coords(150, 30, -30), // pos
					new Orientation
					(
						new Coords(1, 0, 0), 
						new Coords(0, 0, 1)
					), 				
					meshCube
				),

				new Body
				(
					"Cube1",
					Color.Instances.Green,
					new Coords(50, 0, 0), // pos
					new Orientation(new Coords(1, 0, 0), new Coords(0, 1, 1)), 	

			
					meshCube
				),

				new Body
				(
					"Cube2",
					Color.Instances.Cyan,
					new Coords(150, -50, 15), // pos
					new Orientation
					(
						new Coords(1, 0, 0), 
						new Coords(0, 0, 1)
					), 				
					meshCube
				)

			)
		);

		var camera = new Camera
		(
			"Camera 0",
			new Coords(0, 0, 0), // pos
			new Orientation
			(
				new Coords(1, 0, 0), 
				new Coords(0, 0, 1)
			),
			new Coords(320, 200, 800), // viewSize
			320 // distanceToViewPlane
		);

		camera.renderScene(scene);
	}
}

// classes

function Body(name, color, pos, orientation, meshBase)
{
	this.name = name;

	this.color = color;

	this.pos = pos;
	this.orientation = orientation;

	this.meshBase = meshBase;
}

function Camera(name, pos, orientation, viewSize, distanceToViewPlane)
{
	this.name = name;
	this.pos = pos;
	this.orientation = orientation;
	this.viewSize = viewSize;
	this.distanceToViewPlane = distanceToViewPlane;

	this.viewSizeHalf = this.viewSize.clone().divideScalar(2);
}
{
	// instance methods

	Camera.prototype.drawLine = function(color, startPos, endPos)
	{
		this.graphics.strokeStyle = color.systemColor;
		this.graphics.beginPath();
		this.graphics.moveTo(startPos.x, startPos.y);
		this.graphics.lineTo(endPos.x, endPos.y);
		this.graphics.stroke();
	}

	Camera.prototype.drawPixel = function(color, pos)
	{
		this.graphics.fillStyle = color.systemColor;
		this.graphics.fillRect(pos.x, pos.y, 1, 1);		
	}

	Camera.prototype.drawRectangle = function(color, pos, size)
	{
		this.graphics.fillStyle = color.systemColor;
		this.graphics.fillRect(pos.x, pos.y, size.x, size.y);
	}

	Camera.prototype.renderScene = function(sceneToRender)
	{
		var canvas		= document.createElement("canvas");
		canvas.id		= "cameraViewCanvas";
		canvas.width		= this.viewSize.x;
		canvas.height		= this.viewSize.y;
		canvas.style.position	= "absolute";
		canvas.style.cursor 	= "crosshair";
		canvas.style.background = Color.Instances.BlueHalf.systemColor;

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

		var drawPos = new Coords(0, 0, 0);

		var transformsForCamera = new Array
		(
			new TransformTranslate(this.pos.clone().multiplyScalar(-1)),
			new TransformOrient(this.orientation),
			new TransformPerspective(this.distanceToViewPlane),
			new TransformTranslate(this.viewSizeHalf)
		);

		var hideFacesNotPointedTowardCamera = false;

		var numberOfBodies = sceneToRender.bodies.length;

		for (var b = 0; b < numberOfBodies; b++)
		{
			var body = sceneToRender.bodies[b];
			var transformsForBody = new Array
			(
				new TransformTranslate(body.pos),
				new TransformOrient(body.orientation)
			);

			var mesh = body.meshBase.clone();

			var vertexPositions = Vertex.getPositionsForMany(mesh.vertices);

			Transform.applyManyToCoordsMany(transformsForBody, vertexPositions);
			Transform.applyManyToCoordsMany(transformsForCamera, vertexPositions);

			mesh.facesRecalculate();

			var numberOfFaces = mesh.faces.length;

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

				if 
				(
					hideFacesNotPointedTowardCamera == false 
					|| this.orientation.forward.dotProduct(face.plane.normal) 

< 0
				)
				{
					// the face is pointed toward the camera

					var numberOfVerticesInFace = face.vertices.length;

					for (var v = 0; v < numberOfVerticesInFace; v++)
					{				
						var vNext = v + 1;
						if (vNext >= numberOfVerticesInFace)
						{
							vNext = 0;
						}				

						var vertex = face.vertices[v];
						var vertexNext = face.vertices[vNext];

						// todo - clipping

						this.drawLine
						(
							body.color,
							vertex.pos,
							vertexNext.pos	
						);	
					}
				}
			}
		}

		document.body.appendChild(canvas);
	}
}

function Color(name, systemColor)
{
	this.name = name;
	this.systemColor = systemColor;
}
{
	Color.Instances = new (function()
	{
		this.Blue 	= new Color("Blue", "#0000ff");
		this.BlueHalf 	= new Color("BlueDark", "#000080");
		this.Cyan 	= new Color("Cyan", "#00ffff");
		this.Green 	= new Color("Green", "#00ff00");
		this.Red 	= new Color("Red", "#ff0000");

		this._All = new Array
		(
			this.Blue,
			this.BlueHalf,
			this.Cyan,
			this.Green,
			this.Red
		);
	})();
}

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

	Coords.NumberOfDimensions = 3;

	// instances

	Coords.Instances = new CoordsInstances();

	function CoordsInstances()
	{}
	{
		CoordsInstances.Ones = new Coords(1, 1, 1);
		CoordsInstances.Zeroes = new Coords(0, 0, 0);
		CoordsInstances.ZeroZeroOne = new Coords(0, 0, 1);
	}

	// instance methods

	var prototype = Coords.prototype;

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

		return this;
	}

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

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

		return this;
	}

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

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

		return this;
	}

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

		return returnValue;
	}

	prototype.isWithinRange = function(range)
	{
		var returnValue = true;

		var dimensionsOfThis = this.dimensions();
		var dimensionsOfRange = range.dimensions();

		for (var i = 0; i < Coords.NumberOfDimensions; i++)
		{
			var dimensionOfThis = dimensionsOfThis[i];
			var dimensionOfRange = dimensionsOfRange[i];

			if (dimensionOfThis < 0 || dimensionOfThis > dimensionOfRange)
			{
				returnValue = false;
				break;
			}
		}

		return returnValue;
	}

	prototype.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
	}

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

		return this;
	}

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

		return this;
	}

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

		return this;
	}

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

		return this;
	}

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

		return this;
	}

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

		return this;
	}
}

function Edge(vertex0, vertex1)
{
	this.vertices = new Array(vertex0, vertex1);
	this.displacementFromVertex0To1 = vertex1.pos.clone().subtract(vertex0.pos);
	this.length = this.displacementFromVertex0To1.magnitude();
	this.directionFromVertex0To1 = this.displacementFromVertex0To1.clone().divideScalar

(this.length);
}
{
	// instance methods

	Edge.prototype.findIntersectionXYWith = function(other)
	{
		var returnValue = null;

		var orientation = new Orientation(this.directionFromVertex0To1, new Coords(0, 0, 1));
		var transformOrient = new TransformOrient(orientation);

		var edgeOtherProjectedOntoThis = new Edge
		(
			new Array
			(
				new Vertex(transformOrient.applyToCoords(other.vertices[0].pos.clone

())),
				new Vertex(transformOrient.applyToCoords(other.vertices[1].pos.clone

()))
			)
		);

		var distanceToIntersectionAlongEdgeOtherProjected = 
			0 - edgeOtherProjectedOntoThis.vertices[0].pos.y 
			/ edgeOtherProjectedOntoThis.directionFromVertex0To1.y;

		if (NumberHelper.isNumberWithinRangeMax

(distanceToIntersectionAlongEdgeOtherProjected, edgeOtherProjectedOntoThis.length) == true)
		{
			var distanceToIntersectionAlongEdgeThis = 
				edgeOtherProjectedOntoThis.vertices[0].pos.x
				+ edgeOtherProjectedOntoThis.directionFromVertex0To1.x 
				* distanceToIntersectionAlongEdgeProjected;

			if (NumberHelper.isNumberWithinRangeMax(distanceToIntersectionAlongEdgeThis, 

this.length) == true)
			{
				returnValue = this.vertices[0].pos.clone().add
				(
					directionFromVertex0To1.multiplyScalar

(distanceToIntersectoinAlongEdgeThis)
				);
			}
		}

		return returnValue;		
	}
}

function Face(vertices)
{
	this.vertices = vertices;

	this.plane = Plane.fromPoints
	(
		this.vertices[0].pos,
		this.vertices[1].pos,
		this.vertices[2].pos
	);
}
{
	Face.VerticesPerFace = 4;
}

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

	this.facesRecalculate();	
}
{
	Mesh.fromVertexPositions = function(name, vertexPositions, vertexIndicesForFaces)
	{
		var vertices = new Array();

		for (var v = 0; v < vertexPositions.length; v++)
		{
			var vertex = new Vertex
			(
				vertexPositions[v]
			);

			vertices.push(vertex);
		}

		var returnValue = new Mesh(name, vertices, vertexIndicesForFaces);

		return returnValue;
	}

	// instance methods
	Mesh.prototype.facesRecalculate = function()
	{
		this.faces = new Array();

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

			var face = new Face
			(
				new Array
				(
					this.vertices[vertexIndicesForFace[0]],
					this.vertices[vertexIndicesForFace[1]],
					this.vertices[vertexIndicesForFace[2]],
					this.vertices[vertexIndicesForFace[3]]
				)
			);

			this.faces.push(face);
		}
	}

	Mesh.prototype.clone = function()
	{
		return new Mesh(this.name, Vertex.cloneMany(this.vertices), 

this.vertexIndicesForFaces);	
	}
}

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

function NumberHelper()
{}
{
	NumberHelper.isNumberWithinRangeMax = function(numberToCheck, max)
	{
		return (numberToCheck >= 0 && numberToCheck <= max);
	}
}

function Plane(normal, distanceFromOrigin)
{
	this.normal = normal;
	this.distanceFromOrigin = distanceFromOrigin;
}
{
	Plane.fromPoints = function(point0, point1, point2)
	{
		var displacementFromPoint0To1 = point1.clone().subtract(point0);
		var displacementFromPoint1To2 = point2.clone().subtract(point1);

		var normal = displacementFromPoint0To1.crossProduct

(displacementFromPoint1To2).normalize();
		var distanceFromOrigin = point0.clone().multiply(normal).magnitude();

		var returnValue = new Plane(normal, distanceFromOrigin);

		return returnValue;
	}
}

function Scene(name, bodies)
{
	this.name = name;
	this.bodies = bodies;
}

function Transform()
{}
{
	// static methods

	Transform.applyManyToCoordsMany = function(transformsToApply, coordsSetToApplyTo)
	{
		var numberOfTransforms = transformsToApply.length;
		var numberOfCoords = coordsSetToApplyTo.length;

		for (var t = 0; t < numberOfTransforms; t++)
		{
			for (var c = 0; c < numberOfCoords; c++)
			{
				transformsToApply[t].applyToCoords(coordsSetToApplyTo[c]);
			}
		}
	}
}

function TransformOrient(orientation)
{
	this.orientation = orientation;
}
{
	TransformOrient.prototype.applyToCoords = function(coordsToApplyTo)
	{		
		return coordsToApplyTo.overwriteWithDimensions
		(
			coordsToApplyTo.dotProduct(this.orientation.forward),
			coordsToApplyTo.dotProduct(this.orientation.right),
			coordsToApplyTo.dotProduct(this.orientation.down)
		);
	}
}

function TransformScale(scaleFactors)
{
	this.scaleFactors = scaleFactors;
}
{
	TransformScale.prototype.applyToCoords = function(coordsToApplyTo)
	{
		return coordsToApplyTo.multiply(this.scaleFactors);
	}
}

function TransformTranslate(displacement)
{
	this.displacement = displacement;
}
{
	TransformTranslate.prototype.applyToCoords = function(coordsToApplyTo)
	{
		return coordsToApplyTo.add(this.displacement);	
	}
}

function TransformPerspective(distanceToViewPlane)
{
	this.distanceToViewPlane = distanceToViewPlane;
}
{
	TransformPerspective.prototype.applyToCoords = function(coordsToApplyTo)
	{
		return coordsToApplyTo.overwriteWithDimensions
		(
			this.distanceToViewPlane * coordsToApplyTo.y / coordsToApplyTo.x,
			this.distanceToViewPlane * coordsToApplyTo.z / coordsToApplyTo.x,
			coordsToApplyTo.x
		);
	}
}

function Vertex(pos)
{
	this.pos = pos;
}
{
	// static methods

	Vertex.cloneMany = function(verticesToClone)
	{
		var returnArray = new Array();

		var numberOfVertices = verticesToClone.length;

		for (var i = 0; i < numberOfVertices; i++)
		{
			returnArray.push(verticesToClone[i].clone());
		}

		return returnArray;
	}

	Vertex.getPositionsForMany = function(verticesToGetPositionsFor)
	{
		var returnArray = new Array();

		var numberOfVertices = verticesToGetPositionsFor.length;

		for (var i = 0; i < numberOfVertices; i++)
		{
			returnArray.push(verticesToGetPositionsFor[i].pos);
		}

		return returnArray;
	}

	// instance methods

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

new ProjectionTest().main();

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

Advertisements
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