Drawing a 3D Scene in JavaScript Using WebGL

The code included below uses HTML5’s experimental WebGL functionality to draw a 3D scene containing a few textured rectangular boxes with some simple lighting.

To see the code 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/webgl.html.  You can move the camera around with the keys “Q, W, E, A, S, D, X, C, V, R, F”. You’ll figure it out.

This code was originally inspired by similar demonstrations found at “http://learningwebgl.com“. It’s mutated pretty profoundly since then, though.  Some of the matrices in it are a little sketchy at this point.  Specifically, I don’t think that the orientation transformation is working quite right, but it works for this particular scene, and hopefully I’ll figure out how to repair it later.

UPDATE 2014/10/29 – I have cleaned this code up somewhat, with a mind to reducing the number of new objects that have to be allocated every time the scene is rendered. I also worked with the matrices a little. They’re still kind of “magical” at the moment, but it’s a kind of magic that I feel I understand better.

WebGLTest

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

// main

function WebGLTest()
{
	this.main = function()
	{
		var imageTextGL = ImageHelper.buildImageFromStrings
		(
			"ImageTextGL",
			8, // scaleMultiplier
			[
				"RRRRRRRRRRRRRRRR",
				"RRcccccRcRRRRRcR",
				"RRcRRRRRcRRRRRcR",
				"RRcRRRRRcRRRRRcR",
				"RRcRcccRcRRRRRcR",
				"RRcRRRcRcRRRRRRR",
				"RRcccccRcccccRcR",
				"RRRRRRRRRRRRRRRR",
			]
		);

		Globals.Instance.mediaHelper.loadImages
		(
			[ imageTextGL ],
			this.main2
		);
	}

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

		var imageTextGL = mediaHelper.images["ImageTextGL"];

		var textureTextGL = new Texture
		(
			"TextureTextGL",
			imageTextGL
		);

		var meshGround = new Mesh
		(
			"Ground",
			// vertexPositions
			[
				new Coords(1000, -1000, 0),
				new Coords(1000, 1000, 0),
				new Coords(-1000, 1000, 0), 
				new Coords(-1000, -1000, 0),
			],
			// vertexIndicesForFaces
			[
				[0, 1, 2, 3],
			],
			null, // vertexNormalsForFaceVertices
			// materialsForFaces
			[
				Material.Instances.GreenDark,
			],
			null, // texture
			null // textureUVsForFaceVertices
			/*
			[
				[ new Coords(0, 0), new Coords(0, 1), new Coords(1, 1), new Coords(1, 0) ],
			]
			*/
		);

		var meshRainbowMonolith = new Mesh
		(
			"RainbowMonolith",
			// vertexPositions
			[
				// back 
				new Coords(-40, 0, -10), 
				new Coords(40, 0, -10),
				new Coords(40, -180, -10),
				new Coords(-40, -180, -10),

				// front
				new Coords(-40, 0, 10),
				new Coords(40, 0, 10),
				new Coords(40, -180, 10),
				new Coords(-40, -180, 10),
			],
			// vertexIndicesForFaces
			[
				[5, 4, 7, 6], // front
				[0, 1, 2, 3], // back

				[3, 0, 4, 7], // left
				[5, 6, 2, 1], // right

				[6, 7, 3, 2], // top
				[4, 5, 1, 0], // bottom

			],
			// vertexNormalsForFaceVertices
			null,
			/*
			[
				[ new Coords(0, 0, 1), new Coords(1, 0, 0), new Coords(0, 1, 0), new Coords(0, 1, 0) ],
				[ new Coords(0, 0, 1), new Coords(1, 0, 0), new Coords(0, 1, 0), new Coords(0, 1, 0) ],
				[ new Coords(0, 0, 1), new Coords(1, 0, 0), new Coords(0, 1, 0), new Coords(0, 1, 0) ],
				[ new Coords(0, 0, 1), new Coords(1, 0, 0), new Coords(0, 1, 0), new Coords(0, 1, 0) ],
				[ new Coords(0, 0, 1), new Coords(1, 0, 0), new Coords(0, 1, 0), new Coords(0, 1, 0) ],				
			],
			*/ 
			// materialsForFaces
			[
				Material.Instances.Red,
				Material.Instances.Orange,
				Material.Instances.Yellow,
				Material.Instances.Green,
				Material.Instances.Blue,
				Material.Instances.Violet,	
			],
			textureTextGL,
			// textureUVsForFaceVertices
			[
				[ 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(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(1, 0), new Coords(0, 0) ],
			]
		);

		var bodyDefnGround = new BodyDefn
		(
			"Ground",
			meshGround
		);

		var bodyDefnRainbowMonolith = new BodyDefn
		(
			"RainbowMonolith",
			meshRainbowMonolith
		);		

		var displaySize = new Coords(320, 240, 2000);

		var scene = new Scene
		(
			"Scene0",
			new Lighting
			(
				.5, // ambientIntensity
				new Coords(-1, -1, -1), // direction
				2 // directionalIntensity
			),
			new Camera
			(
				displaySize.clone(),
				10, // focalLength
				new Coords(-200, 0, -100), // pos
				new Orientation
				(
					new Coords(1, 0, 0), // forward
					new Coords(0, 0, 1) // down
				)
			),
			// textures
			[
				textureTextGL,
			],
			// bodies
			[
				new Body
				(
					"Ground0",
					bodyDefnGround,
					new Coords(0, 0, 0), // pos
					new Orientation
					(
						new Coords(0, 0, -1), // forward
						new Coords(1, 0, 0) // down
					)
				),

				new Body
				(
					"RainbowMonolith0",
					bodyDefnRainbowMonolith,
					new Coords(0, 0, 0), // pos
					new Orientation
					(
						new Coords(1, 0, 0), // forward
						new Coords(0, 0, 1) // down
					)
				),

				new Body
				(
					"RainbowMonolith1",
					bodyDefnRainbowMonolith,
					new Coords(100, 0, 0), // pos
					new Orientation
					(
						new Coords(1, 0, 0), // forward
						new Coords(0, 0, 1) // down
					)
				),

				new Body
				(
					"RainbowMonolith2",
					bodyDefnRainbowMonolith,
					new Coords(0, 100, 0), // pos
					new Orientation
					(
						new Coords(1, 1, 0), // forward
						new Coords(0, 0, 1) // down
					)
				),
			]
		);

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

// classes

function Body(name, defn, pos, orientation)
{
	this.name = name;
	this.defn = defn;
	this.pos = pos;
	this.orientation = orientation;
}
{
	Body.prototype.drawToWebGLContext = function(webGLContext, scene)
	{
		this.defn.mesh.drawToWebGLContext(webGLContext, scene);
	}
}

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

function Camera(viewSize, focalLength, pos, orientation)
{
	this.viewSize = viewSize;
	this.focalLength = focalLength;
	this.pos = pos;
	this.orientation = orientation;
}
{
	Camera.prototype.toString = function()
	{
		var returnValue = "<Camera "
			+ "pos='" + this.pos.toString() + "' "
			+ ">" 
			+ this.orientation.toString()
			+ "</Camera>";

		return returnValue;
	}
}

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

	Color.NumberOfComponentsRGBA = 4;

	// instances

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

		this.Black 	= new Color("Black",	"k", "Black", [0, 0, 0, 1]);
		this.Blue 	= new Color("Blue", 	"b", "Blue", [0, 0, 1, 1]);
		this.Cyan 	= new Color("Cyan", 	"c", "Cyan", [0, 1, 1, 1]);
		this.Gray 	= new Color("Gray", 	"a", "Gray", [.5, .5, .5, 1]);
		this.Green 	= new Color("Green", 	"g", "Green", [0, 1, 0, 1]);
		this.GreenDark 	= new Color("GreenDark", "G", "#008000", [0, .5, 0, 1]);
		this.Orange 	= new Color("Orange", 	"o", "Orange", [1, .5, 0, 1]);
		this.Red 	= new Color("Red", 	"r", "Red", [1, 0, 0, 1]);
		this.RedDark 	= new Color("RedDark", 	"R", "#800000", [.5, 0, 0, 1]);
		this.Violet 	= new Color("Violet", 	"v", "Violet", [1, 0, 1, 1]);
		this.White 	= new Color("White", 	"w", "White", [1, 1, 1, 1]);
		this.Yellow 	= new Color("Yellow", 	"y", "Yellow", [1, 1, 0, 1]);

		this._All = 
		[
			this.Transparent,

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

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

	Color.Instances = new Color_Instances();

}

function Constants()
{}
{
	Constants.DegreesPerCircle = 360;
	Constants.RadiansPerCircle = 2 * Math.PI;
	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)
	{
		return 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
		);

	}

	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.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.toWebGLArray = function()
	{
		var returnValues = new Float32Array(this.dimensionValues());

		return returnValues;
	}
}

function DisplayHelper()
{}
{
	DisplayHelper.prototype.initialize = function(displaySize)
	{
		var canvas = document.createElement("canvas");
		canvas.width = displaySize.x;
		canvas.height = displaySize.y;
		document.body.appendChild(canvas);

		this.webGLContext = new WebGLContext(canvas);
	}
}

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

	this.plane = new Plane(this.vertices);
}

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.scene.initialize(this.displayHelper.webGLContext);

		this.scene.drawToWebGLContext(this.displayHelper.webGLContext);
	}
}

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

		// hack
		// WebGL doesn't support images from DataURLs?
		returnValue.canvas = canvas;

		return returnValue;
	}
}

function InputHelper()
{
	this.tempCoords = new Coords(0, 0, 0);
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.processKeyDownEvent.bind(this);
	}

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

		var keyCode = event.keyCode;

		var distanceToMove = camera.focalLength;
		var amountToTurn = .1;
		var cameraOrientation = camera.orientation;

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

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

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

		scene.draw();
	}
}

function Lighting(ambientIntensity, direction, directionalIntensity)
{
	this.ambientIntensity = ambientIntensity;
	this.direction = direction.clone().normalize();
	this.directionalIntensity = directionalIntensity;
}

function Material(name, color)
{
	this.name = name;
	this.color = color;
}
{	
	function Material_Instances()
	{
		this.Blue 	= new Material("Blue", 	Color.Instances.Blue);
		this.Cyan 	= new Material("Cyan", 	Color.Instances.Cyan);
		this.Green 	= new Material("Green", Color.Instances.Green);
		this.GreenDark 	= new Material("GreenDark", Color.Instances.GreenDark);
		this.Orange 	= new Material("Orange", Color.Instances.Orange);
		this.Red 	= new Material("Red", 	Color.Instances.Red);
		this.Violet 	= new Material("Violet", Color.Instances.Violet);
		this.Yellow 	= new Material("Yellow", Color.Instances.Yellow);
		this.White 	= new Material("White", Color.Instances.White);
	}

	Material.Instances = new Material_Instances();
}

function Matrix(values)
{
	this.values = values;
}
{
	// static methods

	Matrix.buildZeroes = function()
	{
		var returnValue = new Matrix
		([
			0, 0, 0, 0,
			0, 0, 0, 0,
			0, 0, 0, 0,
			0, 0, 0, 0,
		]);

		return returnValue;
	}

	// instance methods

	Matrix.prototype.clone = function()
	{
		var valuesCloned = [];

		for (var i = 0; i < this.values.length; i++)
		{
			valuesCloned[i] = this.values[i];
		}

		var returnValue = new Matrix(valuesCloned);

		return returnValue;
	}

	Matrix.prototype.divideScalar = function(scalar)
	{
		for (var i = 0; i < this.values.length; i++)
		{
			this.values[i] /= scalar;
		}

		return this;
	}

	Matrix.prototype.multiply = function(other)
	{
		// hack
		// Instantiates a new matrix.

		var valuesMultiplied = [];

		for (var y = 0; y < 4; y++)
		{
			for (var x = 0; x < 4; x++)
			{
				var valueSoFar = 0;

				for (var i = 0; i < 4; i++)
				{
					// This appears backwards,
					// but the other way doesn't work?
					valueSoFar += 
						other.values[y * 4 + i] 
						* this.values[i * 4 + x];
				}

				valuesMultiplied[y * 4 + x] = valueSoFar;
			}
		}

		this.overwriteWithValues(valuesMultiplied);

		return this;
	}

	Matrix.prototype.multiplyScalar = function(scalar)
	{
		for (var i = 0; i < this.values.length; i++)
		{
			this.values[i] *= scalar;
		}

		return this;
	}

	Matrix.prototype.overwriteWith = function(other)
	{
		for (var i = 0; i < this.values.length; i++)
		{
			this.values[i] = other.values[i];
		}

		return this;
	}

	Matrix.prototype.overwriteWithOrientationBody = function(orientation)
	{
		var forward = orientation.forward;
		var right = orientation.right;
		var down = orientation.down;

		this.overwriteWithValues
		([
			right.x, 	down.x, 	forward.x, 	0,
			right.y, 	down.y,		forward.y, 	0,
			right.z, 	down.z, 	forward.z, 	0,
			0, 		0, 		0, 		1,
		]);


		return this;
	}

	Matrix.prototype.overwriteWithOrientationCamera = function(orientation)
	{
		var forward = orientation.forward;
		var right = orientation.right;
		var down = orientation.down;

		this.overwriteWithValues
		([

			right.x, 	right.y, 	right.z, 	0,
			0-down.x, 	0-down.y,	0-down.z, 	0,
			0-forward.x, 	0-forward.y, 	0-forward.z, 	0,
			0, 		0, 		0, 		1,
		]);
	

		return this;
	}

	Matrix.prototype.overwriteWithPerspectiveForCamera = function(camera) 
	{
		var viewSize = camera.viewSize;
		var clipDistanceNear = camera.focalLength;
		var clipDistanceFar = camera.viewSize.z;

		var scaleFactorY = 1.0 / Math.tan(viewSize.y / 2);
		var scaleFactorX = scaleFactorY * viewSize.y / viewSize.x;

		var clipRange = clipDistanceNear - clipDistanceFar;
		var distanceFromFocusToClipPlaneFar = clipDistanceFar + clipDistanceNear;  
		var clipDistanceSumOverDifference = distanceFromFocusToClipPlaneFar / clipRange;
		var clipDistanceProductOverDifference = (clipDistanceFar * clipDistanceNear) / clipRange;

		this.overwriteWithValues
		([
			scaleFactorX, 	0,		0,				0,
			0,		scaleFactorY,	0, 				0,
			0, 		0,		clipDistanceSumOverDifference, 	2 * clipDistanceProductOverDifference,
			0,		0,		-1,				0,
		]);

		return this;
	}

	Matrix.prototype.overwriteWithRotate = function(axisToRotateAround, radiansToRotate)
	{
		var x = axisToRotateAround.x;
		var y = axisToRotateAround.y;
		var z = axisToRotateAround.z;

		var cosine = Math.cos(radiansToRotate);
		var sine = Math.sin(radiansToRotate);
		var cosineReversed = 1 - cosine;

		var xSine = x * sine;
		var ySine = y * sine;
		var zSine = z * sine;

		var xCosineReversed = x * cosineReversed;
		var yCosineReversed = y * cosineReversed;
		var zCosineReversed = z * cosineReversed;

		var xyCosineReversed = x * yCosineReversed;
		var xzCosineReversed = x * zCosineReversed;
		var yzCosineReversed = y * zCosineReversed;

		this.overwriteWithValues
		([
			(x * xCosineReversed + cosine), 	(xyCosineReversed + z * sine), 		(xzCosineReversed - ySine), 	0,
			(xyCosineReversed - zSine), 		(y * yCosineReversed + cosine), 	(yzCosineReversed + xSine), 	0,
			(xzCosineReversed + ySine), 		(yzCosineReversed - xSine), 		(z * zCosineReversed + cosine), 0,
			0,					0, 					0, 				1,
		]);

		return this;
	}

	Matrix.prototype.overwriteWithScale = function(scaleFactors)
	{
		this.overwriteWithValues
		([
			scaleFactors.x,	0, 		0, 		0,
			0, 		scaleFactors.y, 0, 		0,
			0, 		0, 		scaleFactors.z, 0,
			0, 		0, 		0, 		1,

		]);

		return this;
	}

	Matrix.prototype.overwriteWithTranslate = function(displacement)
	{
		this.overwriteWithValues
		([
			1, 0, 0, displacement.x,
			0, 1, 0, displacement.y,
			0, 0, 1, displacement.z,
			0, 0, 0, 1,

		]);

		return this;
	}

	Matrix.prototype.overwriteWithValues = function(otherValues)
	{
		for (var i = 0; i < this.values.length; i++)
		{
			this.values[i] = otherValues[i];
		}

		return this;
	}

	Matrix.prototype.toWebGLArray = function()
	{
		var returnValues = [];

		for (var x = 0; x < 4; x++)
		{
			for (var y = 0; y < 4; y++)
			{
				returnValues.push(this.values[(y * 4 + x)]);
			}
		}

		var returnValues = new Float32Array(returnValues);

		return returnValues;
	}
}

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, 
	vertexPositions, 
	vertexIndicesForFaces, 
	vertexNormalsForFaceVertices, 
	materialsForFaces,
	texture,
	textureUVsForFaceVertices
)
{
	this.name = name;
	this.vertexPositions = vertexPositions;
	this.vertexIndicesForFaces = vertexIndicesForFaces;
	this.vertexNormalsForFaceVertices = vertexNormalsForFaceVertices;
	this.materialsForFaces = materialsForFaces;
	this.texture = texture;
	this.textureUVsForFaceVertices = textureUVsForFaceVertices;

	this.faces = [];

	var numberOfFaces = this.vertexIndicesForFaces.length;

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

		var numberOfVerticesInFace = vertexIndicesForFace.length;

		var verticesForFace = [];

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

			verticesForFace.push(vertexPos);			
		}

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

		this.faces.push(face);
	}
}
{
	// constants

	Mesh.VerticesInATriangle = 3;

	// instance methods

	Mesh.prototype.drawToWebGLContext = function(webGLContext, scene)
	{
		var gl = webGLContext.gl;

		var shaderProgram = webGLContext.shaderProgram;

		var vertexPositionsAsFloatArray = [];
		var vertexColorsAsFloatArray = [];
		var vertexNormalsAsFloatArray = [];
		var vertexTextureUVsAsFloatArray = [];

		var numberOfTrianglesSoFar = 0;

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

			var vertexIndicesForTriangles = 
			[
				[0, 1, 2]
			];

			var numberOfVerticesInFace = face.vertices.length;

			if (numberOfVerticesInFace == 4)
			{
				vertexIndicesForTriangles.push([0, 2, 3]);
			}

			for (var t = 0; t < vertexIndicesForTriangles.length; t++)
			{
				var vertexIndicesForTriangle = vertexIndicesForTriangles[t];

				for (var vi = 0; vi < vertexIndicesForTriangle.length; vi++)
				{
					var vertexIndex = vertexIndicesForTriangle[vi];
					var vertexPosition = face.vertices[vertexIndex];

					vertexPositionsAsFloatArray = vertexPositionsAsFloatArray.concat
					(
						vertexPosition.dimensionValues()
					);

					vertexColorsAsFloatArray = vertexColorsAsFloatArray.concat
					(
						face.material.color.componentsRGBA
					);

					var vertexNormal = 
					(
						this.vertexNormalsForFaceVertices == null 
						? faceNormal
						: this.vertexNormalsForFaceVertices[f][vertexIndex]
					);

					vertexNormalsAsFloatArray = vertexNormalsAsFloatArray.concat
					(
						vertexNormal.dimensionValues()
					);

					var vertexTextureUV = 
					(
						this.textureUVsForFaceVertices == null 
						? new Coords(-1, -1)
						: this.textureUVsForFaceVertices[f] == null
						? new Coords(-1, -1)
						: this.textureUVsForFaceVertices[f][vertexIndex]
					);

					vertexTextureUVsAsFloatArray = vertexTextureUVsAsFloatArray.concat
					(
						[
							vertexTextureUV.x,
							vertexTextureUV.y
						]
					);
				}
			}

			numberOfTrianglesSoFar += vertexIndicesForTriangles.length;
		}

		var colorBuffer = gl.createBuffer();
		gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
		gl.bufferData
		(
			gl.ARRAY_BUFFER, 
			new Float32Array(vertexColorsAsFloatArray), 
			gl.STATIC_DRAW
		);
		gl.vertexAttribPointer
		(
			shaderProgram.vertexColorAttribute, 
			Color.NumberOfComponentsRGBA, 
			gl.FLOAT, 
			false, 
			0, 
			0
		);

		var normalBuffer = gl.createBuffer();
		gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
		gl.bufferData
		(
			gl.ARRAY_BUFFER, 
			new Float32Array(vertexNormalsAsFloatArray), 
			gl.STATIC_DRAW
		);		
		gl.vertexAttribPointer
		(
			shaderProgram.vertexNormalAttribute, 
			Coords.NumberOfDimensions, 
			gl.FLOAT, 
			false, 
			0, 
			0
		);

		var positionBuffer = gl.createBuffer();
		gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
		gl.bufferData
		(
			gl.ARRAY_BUFFER, 
			new Float32Array(vertexPositionsAsFloatArray), 
			gl.STATIC_DRAW
		);
		gl.vertexAttribPointer
		(
			shaderProgram.vertexPositionAttribute, 
			Coords.NumberOfDimensions, 
			gl.FLOAT, 
			false, 
			0, 
			0
		);

		if (this.texture != null)
		{
			gl.activeTexture(gl.TEXTURE0);
			gl.bindTexture(gl.TEXTURE_2D, this.texture.systemTexture);
		}

		gl.uniform1i(shaderProgram.samplerUniform, 0);

		var textureBuffer = gl.createBuffer();
		gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
		gl.bufferData
		(
			gl.ARRAY_BUFFER, 
			new Float32Array(vertexTextureUVsAsFloatArray), 
			gl.STATIC_DRAW
		);
		gl.vertexAttribPointer
		(
			shaderProgram.vertexTextureUVAttribute, 
			2, 
			gl.FLOAT, 
			false, 
			0, 
			0
		);

		gl.drawArrays
		(
			gl.TRIANGLES,
			0, 
			numberOfTrianglesSoFar * Mesh.VerticesInATriangle
		);
	}
}

function WebGLContext(canvas)
{
	this.gl = this.initGL(canvas);
	this.shaderProgram = this.buildShaderProgram(this.gl);

}
{
	WebGLContext.prototype.initGL = function(canvas)
	{
		var gl = canvas.getContext("experimental-webgl");
		gl.viewportWidth = canvas.width;
		gl.viewportHeight = canvas.height;

		var colorBlackComponentsRGBA = Color.Instances.Cyan.componentsRGBA;
		gl.clearColor
		(
			colorBlackComponentsRGBA[0], 
			colorBlackComponentsRGBA[1], 
			colorBlackComponentsRGBA[2], 
			colorBlackComponentsRGBA[3]
		);

		gl.enable(gl.DEPTH_TEST);

		return gl;
	}

	WebGLContext.prototype.buildShaderProgram = function(gl)
	{
		var shaderProgram = this.buildShaderProgram_Compile
		(
			gl,
			this.buildShaderProgram_FragmentShader(gl),
			this.buildShaderProgram_VertexShader(gl)
		);

		this.buildShaderProgram_SetUpInputVariables(gl, shaderProgram);

		return shaderProgram;
	}

	WebGLContext.prototype.buildShaderProgram_FragmentShader = function(gl)
	{
		var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
		var fragmentShaderCode = 
			"precision mediump float;"
			+ "uniform sampler2D uSampler;"
			+ "varying vec4 vColor;"
			+ "varying vec3 vLight;"
			+ "varying vec2 vTextureUV;"
			+ "void main(void) {"
			+ "    if (vTextureUV.x < 0.0) {"
			+ "        gl_FragColor = vColor;"
			+ "    } else {"
			+ "        vec4 textureColor = texture2D(uSampler, vec2(vTextureUV.s, vTextureUV.t));"
			+ "        gl_FragColor = vec4(vLight * textureColor.rgb, textureColor.a);"
			+ "    }"
			+ "}";
		gl.shaderSource(fragmentShader, fragmentShaderCode);
		gl.compileShader(fragmentShader);

		return fragmentShader;
	}

	WebGLContext.prototype.buildShaderProgram_VertexShader = function(gl)
	{
		var vertexShader = gl.createShader(gl.VERTEX_SHADER);
		var vertexShaderCode = 
			"attribute vec4 aVertexColor;"
			+ "attribute vec3 aVertexNormal;"
			+ "attribute vec3 aVertexPosition;"
			+ "attribute vec2 aVertexTextureUV;"
			+ "uniform mat4 uBodyMatrix;"
			+ "uniform mat4 uCameraMatrix;"
			+ "uniform float uLightAmbientIntensity;"
			+ "uniform vec3 uLightDirection;"
			+ "uniform float uLightDirectionalIntensity;"
			+ "uniform mat4 uNormalMatrix;"
			+ "varying vec4 vColor;"
			+ "varying vec3 vLight;"
			+ "varying vec2 vTextureUV;"
			+ "void main(void) {"
			+ "    vColor = aVertexColor;"
			+ "    vec4 vertexNormal4 = vec4(aVertexNormal, 0.0);"
			+ "    vec4 transformedNormal4 = uNormalMatrix * vertexNormal4;"
			+ "    vec3 transformedNormal = vec3(transformedNormal4.xyz) * -1.0;"
			+ "    float lightMagnitude = uLightAmbientIntensity;"
			+ "    lightMagnitude += uLightDirectionalIntensity * max(dot(transformedNormal, uLightDirection), 0.0);"
			+ "    vLight = vec3(1.0, 1.0, 1.0) * lightMagnitude;"
			+ "    vTextureUV = aVertexTextureUV;"
			+ "    vec4 vertexPos = vec4(aVertexPosition, 1.0);"
			+ "    gl_Position = uCameraMatrix * uBodyMatrix * vertexPos;"
    			+ "}";
		gl.shaderSource(vertexShader, vertexShaderCode);
		gl.compileShader(vertexShader);

		return vertexShader;
	}

	WebGLContext.prototype.buildShaderProgram_Compile = function
	(
		gl, fragmentShader, vertexShader
	)
	{
		var shaderProgram = gl.createProgram();
		gl.attachShader(shaderProgram, vertexShader);
		gl.attachShader(shaderProgram, fragmentShader);
		gl.linkProgram(shaderProgram);
		gl.useProgram(shaderProgram);

		return shaderProgram;
	}

	WebGLContext.prototype.buildShaderProgram_SetUpInputVariables = function
	(
		gl, shaderProgram
	)
	{
		shaderProgram.vertexColorAttribute = gl.getAttribLocation
		(
			shaderProgram,
			"aVertexColor"
		);
		gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);

		shaderProgram.vertexNormalAttribute = gl.getAttribLocation
		(
			shaderProgram,
			"aVertexNormal"
		);
		gl.enableVertexAttribArray(shaderProgram.vertexNormalAttribute);

		shaderProgram.vertexPositionAttribute = gl.getAttribLocation
		(
			shaderProgram,
			"aVertexPosition"
		);
		gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);

		shaderProgram.vertexTextureUVAttribute = gl.getAttribLocation
		(
			shaderProgram,
			"aVertexTextureUV"
		);
		gl.enableVertexAttribArray(shaderProgram.vertexTextureUVAttribute);

		shaderProgram.bodyMatrix = gl.getUniformLocation
		(
			shaderProgram, 
			"uBodyMatrix"
		);

		shaderProgram.cameraMatrix = gl.getUniformLocation
		(
			shaderProgram, 
			"uCameraMatrix"
		);

		shaderProgram.lightAmbientIntensity = gl.getUniformLocation
		(
			shaderProgram,
			"uLightAmbientIntensity"
		);

		shaderProgram.lightDirection = gl.getUniformLocation
		(
			shaderProgram, 
			"uLightDirection"
		);

		shaderProgram.lightDirectionalIntensity = gl.getUniformLocation
		(
			shaderProgram,
			"uLightDirectionalIntensity"
		);

		shaderProgram.normalMatrix = gl.getUniformLocation
		(
			shaderProgram, 
			"uNormalMatrix"
		);
	}
}

function Orientation(forward, down)
{
	this.forward = new Coords(0, 0, 0);
	this.down = new Coords(0, 0, 0);
	this.right = new Coords(0, 0, 0);
	this.overwriteWithForwardDown(forward, down);
}
{
	// instance methods

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

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

	Orientation.prototype.toString = function()
	{
		var returnValue = "<Orientation "
			+ "forward='" + this.forward.toString() + "' "
			+ "right='" + this.right.toString() + "' "
			+ "down='" + this.down.toString() + "' "
			+ " />";

		return returnValue;			
	}
}

function Plane(positionsOnPlane)
{
	var pos0 = positionsOnPlane[0];
	var displacementFromPos0To1 = Plane.TempCoords0.overwriteWith
	(
		positionsOnPlane[1]
	).subtract
	(
		pos0
	);
	var displacementFromPos0To2 = Plane.TempCoords1.overwriteWith
	(
		positionsOnPlane[2]
	).subtract
	(
		pos0
	);
	var normal = displacementFromPos0To1.clone().crossProduct
	(
		displacementFromPos0To2
	).normalize();

	this.normal = normal;
	this.distanceFromOrigin = this.normal.dotProduct(pos0);
}
{
	Plane.TempCoords0 = new Coords(0, 0, 0);
	Plane.TempCoords1 = new Coords(0, 0, 0);
}

function Scene(name, lighting, camera, textures, bodies)
{
	this.name = name;
	this.lighting = lighting;
	this.camera = camera;
	this.textures = textures;
	this.bodies = bodies;

	// temps

	this.matrixBody = Matrix.buildZeroes();
	this.matrixCamera = Matrix.buildZeroes();
	this.matrixOrient = Matrix.buildZeroes();
	this.matrixPerspective = Matrix.buildZeroes();
	this.matrixTranslate = Matrix.buildZeroes();
	this.tempCoords = new Coords(0, 0, 0);
	this.tempMatrix0 = Matrix.buildZeroes();
	this.tempMatrix1 = Matrix.buildZeroes();
}
{
	Scene.prototype.draw = function()
	{
		this.drawToWebGLContext
		(
			Globals.Instance.displayHelper.webGLContext
		);
	}

	Scene.prototype.drawToWebGLContext = function(webGLContext)
	{
		var gl = webGLContext.gl;
		var shaderProgram = webGLContext.shaderProgram;

		gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
		gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

		var camera = this.camera;

		var cameraMatrix = this.matrixCamera.overwriteWithTranslate
		(
			this.tempCoords.overwriteWith(camera.pos).multiplyScalar(-1)
		).multiply
		(
			this.matrixOrient.overwriteWithOrientationCamera
			(
				camera.orientation
			)
		).multiply
		(
			this.matrixPerspective.overwriteWithPerspectiveForCamera
			(
				camera
			)
		)

		gl.uniform1f
		(
			shaderProgram.lightAmbientIntensity,
			this.lighting.ambientIntensity
		);

		gl.uniform3fv
		(
			shaderProgram.lightDirection, 
			this.lighting.direction.toWebGLArray()
		);

		gl.uniform1f
		(
			shaderProgram.lightDirectionalIntensity,
			this.lighting.directionalIntensity
		);

		gl.uniformMatrix4fv
		(
			shaderProgram.cameraMatrix, 
			false, // transpose
			cameraMatrix.toWebGLArray()
		);

		for (var b = 0; b < this.bodies.length; b++)
		{
			var body = this.bodies[b];

			var normalMatrix = this.matrixOrient.overwriteWithOrientationBody
			(
				body.orientation
			);

			var bodyMatrix = this.matrixBody.overwriteWith
			(
				normalMatrix
			).multiply
			(
				this.matrixTranslate.overwriteWithTranslate
				(
					body.pos
				)
			);

			gl.uniformMatrix4fv
			(
				shaderProgram.bodyMatrix, 
				false, // transpose
				bodyMatrix.toWebGLArray()
			);

			gl.uniformMatrix4fv
			(
				shaderProgram.normalMatrix, 
				false, // transpose
				normalMatrix.multiplyScalar(-1).toWebGLArray()
			);

			body.drawToWebGLContext(webGLContext, this);
		}
	}

	Scene.prototype.initialize = function(webGLContext)
	{
		for (var t = 0; t < this.textures.length; t++)
		{
			var texture = this.textures[t];
			texture.initialize(webGLContext);
		}
	}
}

function Texture(name, image)
{
	this.name = name;
	this.image = image;
}
{
	Texture.prototype.initialize = function(webGLContext)
	{
		var gl = webGLContext.gl;

		this.systemTexture = gl.createTexture();

		gl.bindTexture(gl.TEXTURE_2D, this.systemTexture);
		//gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
		gl.texImage2D
		(
			gl.TEXTURE_2D, 
			0, 
			gl.RGBA, 
			gl.RGBA, 
			gl.UNSIGNED_BYTE, 
			this.image.systemImage
		);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
		gl.bindTexture(gl.TEXTURE_2D, null);
	}
}

// run

new WebGLTest().main();

</script>
</body>
</html>
This entry was posted in Uncategorized and tagged , , , , . Bookmark the permalink.

One Response to Drawing a 3D Scene in JavaScript Using WebGL

  1. xtmpxg says:

    Reblogged this on xtmpxg and commented:
    test

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