Rendering a Simple 3D Scene Using JavaScript

The code included below makes use of vector mathematics to render a simple three-dimensional scene from a particular viewpoint.

To run the code, copy it into an .html file and open it in a web browser capable of running JavaScript. To achieve the intended result, it is also necessary to include the image files SquareRed.png, SquareGreen.png, and SquareBlue.png in the same directory as the .html file.



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

function ProjectionTest()
{
	this.main = function()
	{
		var bodyDefns = new Array
		(
			new BodyDefn("Blue", 32, new Image("SquareBlue.png")),
			new BodyDefn("Green", 32, new Image("SquareGreen.png")),
			new BodyDefn("Red", 32, new Image("SquareRed.png"))
		);

		var scene = new Scene
		(
			"Scene 0",
			new Array
			(
				new Body("Body 0", bodyDefns[0], new Coords(100, 0, 0)),
				new Body("Body 1", bodyDefns[1], new Coords(100, 50, 0)),
				new Body("Body 2", bodyDefns[2], new Coords(200, 50, -25))
			)
		);

		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(640, 480, 1024), // viewSize
			320 // distanceToViewPlane
		);

		camera.renderScene(scene);
	}
}

// classes

function Body(name, defn, pos)
{
	this.name = name;
	this.defn = defn;
	this.pos = pos;	
}
{
	var prototype = Body.prototype;
}

function BodyDefn(name, sizeInPixels, image)
{
	this.name = name;
	this.sizeInPixels = sizeInPixels;
	this.image = image;
}

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);
}
{
	var prototype = Camera.prototype;

	prototype.renderScene = function(sceneToRender)
	{
		var htmlDivForViewport = document.createElement("div");
		htmlDivForViewport.style.position = "absolute";
		htmlDivForViewport.style.width = this.viewSize.x + "px";
		htmlDivForViewport.style.height = this.viewSize.y + "px";
		htmlDivForViewport.style.background = "#0000ff";
		document.body.appendChild(htmlDivForViewport);

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

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

		var numberOfBodies = sceneToRender.bodies.length;
		var numberOfTransforms = transformsToPerform.length;

		for (var i = 0; i < numberOfBodies; i++)
		{
			var body = sceneToRender.bodies[i];

			drawPos.overwriteWith(body.pos);

			for (var t = 0; t < numberOfTransforms; t++)
			{
				var transform = transformsToPerform[t];

				transform.applyToCoords(drawPos);	
			}

			if (drawPos.isWithinRange(this.viewSize) == true)
			{
				var htmlImageForBody = document.createElement("img");
				htmlImageForBody.src = body.defn.image.filePath;
				htmlImageForBody.style.width = (body.defn.sizeInPixels * this.distanceToViewPlane / drawPos.z) + "px";
				htmlImageForBody.style.height = (body.defn.sizeInPixels * this.distanceToViewPlane / drawPos.z) + "px";

				var htmlParagraphForBody = document.createElement("p");
				htmlParagraphForBody.innerHTML = body.name;

				var htmlDivForBody = document.createElement("div");
				htmlDivForBody.appendChild(htmlImageForBody);
				htmlDivForBody.appendChild(htmlParagraphForBody);
				htmlDivForBody.style.position = "absolute";
				htmlDivForBody.style.left = "" + drawPos.x + "px";
				htmlDivForBody.style.top = "" + drawPos.y + "px";
				htmlDivForViewport.appendChild(htmlDivForBody);			
			}
		}
	}
}

function Coords(x, y, z)
{
	this.x = x;
	this.y = y;
	this.z = z;
}
{
	Coords.NumberOfDimensions = 3;

	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.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		this.z *= scalar;

		return this;
	}

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

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

function Image(filePath)
{
	this.filePath = filePath;
}

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

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

function TransformOrient(orientation)
{
	this.orientation = orientation;
}
{
	var prototype = TransformOrient.prototype;

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

function TransformTranslate(displacement)
{
	this.displacement = displacement;
}
{
	var prototype = TransformTranslate.prototype;

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

function TransformPerspective(distanceToViewPlane)
{
	this.distanceToViewPlane = distanceToViewPlane;
}
{
	var prototype = TransformPerspective.prototype;

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

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