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.

UPDATE 2017/02/08 – I have cleaned up the code further, in preparation for hopefully adding shadow mapping in the future. Notably, I have incorporated a lot of mesh data into the Face object, rather than having several different arrays containing disparate information for each face. I also moved the code for the WebGL vertex and fragment shaders into HTML elements rather than building them up as giant strings from a bunch of single-line strings.

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 displaySize = new Coords(320, 240, 2000);

		var scene = new DemoData().scene(mediaHelper, displaySize);

		var display = new Display(displaySize);

		Globals.Instance.initialize
		(
			display,
			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;
	}

	Array.prototype.append = function(other)
	{
		for (var i = 0; i < other.length; i++)
		{
			var element = other[i];
			this.push(element);
		}

		return this;
	}
}

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

		].addLookups("codeChar");
	}

	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;

	// instances

	Coords.Instances = new Coords_Instances();

	function Coords_Instances()
	{
		this.OnesNegative = new Coords(-1, -1, -1);
	}

	// 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 Display(size)
{
	this.size = size;
}
{
	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.size.x;
		canvas.height = this.size.y;
		document.body.appendChild(canvas);

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

function Face(vertexIndices, material, textureUVs, vertexNormals)
{
	this.vertexIndices = vertexIndices;
	this.material = material;
	this.textureUVs = textureUVs;
	this.vertexNormals = vertexNormals;
}
{
	// constants

	Face.VertexIndexIndicesForQuad = [ [ 0, 1, 2 ], [ 0, 2, 3 ] ];
	Face.VertexIndexIndicesForTriangle = [ [ 0, 1, 2 ] ];

	// methods

	Face.prototype.plane = function(mesh)
	{
		var returnValue = new Plane(this.vertices(mesh));
		return returnValue;
	}

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

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

		return returnValues;
	}

	// WebGL

	Face.prototype.drawToWebGLContext = function
	(
		webGLContext, 
		scene, 
		mesh,
		vertexPositionsAsFloatArray,
		vertexColorsAsFloatArray,
		vertexNormalsAsFloatArray,
		vertexTextureUVsAsFloatArray,		
		numberOfTrianglesSoFarWrapped
	)
	{
		var face = this;
		var facePlane = face.plane(mesh);
		var faceNormal = facePlane.normal;
		var faceMaterial = face.material;
		var faceVertexNormals = face.vertexNormals;

		var numberOfVerticesInFace = face.vertexIndices.length;

		var vertexIndexIndicesForChildTriangles = 
			(numberOfVerticesInFace == 4) 
			? Face.VertexIndexIndicesForQuad
			: Face.VertexIndexIndicesForTriangle

		for (var t = 0; t < vertexIndexIndicesForChildTriangles.length; t++)
		{
			var vertexIndexIndicesForChildTriangle = 
				vertexIndexIndicesForChildTriangles[t];

			for (var vii = 0; vii < vertexIndexIndicesForChildTriangle.length; vii++)
			{
				var vertexIndexIndex = 
					vertexIndexIndicesForChildTriangle[vii];
				var vertexIndex = face.vertexIndices[vertexIndexIndex];
				var vertexPosition = mesh.vertexPositions[vertexIndex];

				vertexPositionsAsFloatArray.append
				(
					vertexPosition.dimensionValues()
				);

				vertexColorsAsFloatArray.append
				(
					faceMaterial.color.componentsRGBA
				);

				var vertexNormal = 
				(
					faceVertexNormals == null 
					? faceNormal
					: faceVertexNormals[vertexIndex]
				);

				vertexNormalsAsFloatArray.append
				(
					vertexNormal.dimensionValues()
				);

				var vertexTextureUV = 
				(
					face.textureUVs == null 
					? Coords.Instances.OnesNegative
					: face.textureUVs[vertexIndexIndex]
				);

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

		numberOfTrianglesSoFarWrapped.value 
			+= vertexIndexIndicesForChildTriangles.length;
	}
}

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

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

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

		this.scene = scene;

		this.scene.initialize(this.display.webGLContext);

		this.scene.drawToWebGLContext(this.display.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();
}
{
	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 key = event.key.toLowerCase();

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

		if (key == "a")
		{
			// move left
			camera.pos.subtract
			(
				this.tempCoords.overwriteWith
				(
					cameraOrientation.right
				).multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (key == "d")
		{
			// move right
			camera.pos.add
			(
				this.tempCoords.overwriteWith
				(
					cameraOrientation.right
				).multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (key == "e")
		{
			// roll right
			cameraOrientation.overwriteWithForwardDown
			(
				cameraOrientation.forward,
				cameraOrientation.down.add
				(
					cameraOrientation.right.multiplyScalar
					(
						amountToTurn
					)
				)
			);

		}
		else if (key == "f")
		{
			// fall
			camera.pos.subtract
			(
				this.tempCoords.overwriteWith
				(
					cameraOrientation.down
				).multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (key == "q")
		{
			// roll left
			cameraOrientation.overwriteWithForwardDown
			(
				cameraOrientation.forward,
				cameraOrientation.down.subtract
				(
					cameraOrientation.right.multiplyScalar
					(
						amountToTurn
					)
				)
			);

		}
		else if (key == "r")
		{
			// rise
			camera.pos.add
			(
				this.tempCoords.overwriteWith
				(
					cameraOrientation.down
				).multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (key == "s")
		{
			// move back
			camera.pos.subtract
			(
				this.tempCoords.overwriteWith
				(
					cameraOrientation.forward
				).multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (key == "w")
		{
			// move forward
			camera.pos.add
			(
				this.tempCoords.overwriteWith
				(
					cameraOrientation.forward
				).multiplyScalar
				(
					distanceToMove
				)
			);
		}
		else if (key == "z")
		{
			// turn left
			cameraOrientation.overwriteWithForwardDown
			(
				cameraOrientation.forward.subtract
				(
					cameraOrientation.right.multiplyScalar
					(
						amountToTurn
					)
				),
				cameraOrientation.down
			);
		}
		else if (key == "c")
		{
			// turn right
			cameraOrientation.overwriteWithForwardDown
			(
				cameraOrientation.forward.add
				(
					cameraOrientation.right.multiplyScalar
					(
						amountToTurn
					)
				),
				cameraOrientation.down
			);
		}
		else if (key == "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()
	{
		var colors = Color.Instances;

		this.Blue 	= new Material("Blue", 	colors.Blue);
		this.Cyan 	= new Material("Cyan", 	colors.Cyan);
		this.Green 	= new Material("Green", colors.Green);
		this.GreenDark 	= new Material("GreenDark", colors.GreenDark);
		this.Orange 	= new Material("Orange", colors.Orange);
		this.Red 	= new Material("Red", 	colors.Red);
		this.Violet 	= new Material("Violet", colors.Violet);
		this.Yellow 	= new Material("Yellow", colors.Yellow);
		this.White 	= new Material("White", colors.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, 
	texture,
	faces
)
{
	this.name = name;
	this.vertexPositions = vertexPositions;
	this.texture = texture;
	this.faces = faces;
}
{
	// 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 numberOfTrianglesSoFarWrapped = new NumberWrapper(0);

		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];
			face.drawToWebGLContext
			(
				webGLContext, 
				scene, 
				this, // mesh
				vertexPositionsAsFloatArray,
				vertexColorsAsFloatArray,
				vertexNormalsAsFloatArray,
				vertexTextureUVsAsFloatArray,
				numberOfTrianglesSoFarWrapped
			);
		}

		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, 
			numberOfTrianglesSoFarWrapped.value 
				* Mesh.VerticesInATriangle
		);
	}
}

function NumberWrapper(value)
{
	this.value = value;
}

function Orientation(forward, down)
{
	this.forward = new Coords();
	this.down = new Coords();
	this.right = new Coords();
	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);
}
{
	// helper variables

	Plane.TempCoords0 = new Coords();
	Plane.TempCoords1 = new Coords();
}

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

	// helper variables

	this.matrixBody = Matrix.buildZeroes();
	this.matrixCamera = Matrix.buildZeroes();
	this.matrixOrient = Matrix.buildZeroes();
	this.matrixPerspective = Matrix.buildZeroes();
	this.matrixTranslate = Matrix.buildZeroes();
	this.tempCoords = new Coords();
	this.tempMatrix0 = Matrix.buildZeroes();
	this.tempMatrix1 = Matrix.buildZeroes();
}
{
	Scene.prototype.draw = function()
	{
		this.drawToWebGLContext
		(
			Globals.Instance.display.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);
	}
}

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 colorBack = Color.Instances.Cyan;
		var colorBackComponentsRGBA = colorBack.componentsRGBA;
		gl.clearColor
		(
			colorBackComponentsRGBA[0], 
			colorBackComponentsRGBA[1], 
			colorBackComponentsRGBA[2], 
			colorBackComponentsRGBA[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 = document.getElementById
		(
			"divShaderCodeFragment"
		).childNodes[0].nodeValue;

		gl.shaderSource(fragmentShader, fragmentShaderCode);
		gl.compileShader(fragmentShader);

		return fragmentShader;
	}

	WebGLContext.prototype.buildShaderProgram_VertexShader = function(gl)
	{
		var vertexShader = gl.createShader(gl.VERTEX_SHADER);

		var vertexShaderCode = document.getElementById
		(
			"divShaderCodeVertex"
		).childNodes[0].nodeValue;

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

// demo

function DemoData()
{
	// do nothing
}
{
	DemoData.prototype.scene = function(mediaHelper, displaySize)
	{
		var imageTextGL = mediaHelper.images["ImageTextGL"];

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

		var materials = Material.Instances;

		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),
			],
			null, // texture
			// faces
			[
				new Face
				(
					[0, 1, 2, 3], // vertexIndices
					materials.GreenDark,
					null, // textureUVs
					null // vertexNormals - todo
				),
			]
		);

		var textureUVsForMeshFaces = 
		[ 
			new Coords(0, 1), 
			new Coords(1, 1), 
			new Coords(1, 0), 
			new Coords(0, 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),
			],
			textureTextGL,
			// faces
			[
				new Face
				(
					[5, 4, 7, 6], 
					materials.Red, 
					textureUVsForMeshFaces
				), // front

				new Face
				(
					[0, 1, 2, 3], 
					materials.Orange, 
					textureUVsForMeshFaces
				), // back

				new Face
				(
					[3, 0, 4, 7], 
					materials.Yellow, 
					textureUVsForMeshFaces
				), // left

				new Face
				(
					[5, 6, 2, 1], 
					materials.Green, 
					textureUVsForMeshFaces
				), // right

				new Face
				(
					[6, 7, 3, 2], 
					materials.Blue, 
					textureUVsForMeshFaces
				), // top

				new Face
				(
					[4, 5, 1, 0], 
					materials.Violet, 
					textureUVsForMeshFaces
				), // bottom

			]
		);

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

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

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

		return scene;
	}
}

// run

new WebGLTest().main();

</script>

<!-- WebGL shader programs -->

<div id="divShaderCodeFragment" style="display:none">

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

</div>

<div id="divShaderCodeVertex" style="display:none">

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


</div>


</body>
</html>

Advertisements
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