Index: issm/trunk-jpl/src/m/plot/webgl.js
===================================================================
--- issm/trunk-jpl/src/m/plot/webgl.js	(revision 21768)
+++ issm/trunk-jpl/src/m/plot/webgl.js	(revision 21911)
@@ -9,7 +9,9 @@
 		if (!isEmptyOrUndefined(canvas.animation) && canvas.animation.handler !== 0) { clearInterval(canvas.animation.handler); }
 		initWebGL(canvas, options);
-		initializeMarker(canvas);
 		draw(canvas);
 		canvas.initialized = true;
+		
+		//The onStart event triggers once per plotmodel call load after WebGL and canvas initialization are complete
+		canvas.selector.trigger('onStart', [canvas]);
 	}
 	return canvas;
@@ -34,7 +36,7 @@
 		mc.add(new Hammer.Pan({threshold: 0, pointers: 0}));
 		mc.add(new Hammer.Pinch({threshold: 0})).recognizeWith(mc.get('pan'));
-		mc.on('tap press', function (ev) {onTap(ev, canvas);});
+		mc.on('tap', function (ev) {onTap(ev, canvas);});
 		mc.on('panstart panmove', function (ev) {onPan(ev, canvas, displayview);});
-		mc.on('pinchstart pinchmove', function (ev) {onPinch(ev, canvas, displayview);});		
+		mc.on('pinchstart pinchmove', function (ev) {onPinch(ev, canvas, displayview);});
 		canvas.addEventListener('mousewheel', function (ev) {onZoom(ev, canvas, displayzoom)}, false);
 		canvas.addEventListener('DOMMouseScroll', function (ev) {onZoom(ev, canvas, displayzoom)}, false);
@@ -46,4 +48,5 @@
 		canvas.unitData = {};
 		canvas.unitMovieData = {};
+		
 		canvas.gl = gl;
 		canvas.rootPath = options.getfieldvalue('rootpath', '../../../js/');
@@ -57,5 +60,7 @@
 	
 	//Add context state variables
+	canvas.render = options.getfieldvalue('render', {});
 	canvas.controlSensitivity = options.getfieldvalue('controlsensitivity', 1);
+	canvas.overlayHandlers = {};
 	var backgroundcolor = new RGBColor(options.getfieldvalue('backgroundcolor', 'lightcyan'));
 	if (backgroundcolor.ok) { canvas.backgroundcolor = [backgroundcolor.r/255.0, backgroundcolor.g/255.0, backgroundcolor.b/255.0, 1.0]; }
@@ -63,72 +68,88 @@
 	
 	//Property intiialization, using values from options first, then from default values.
+	var atmosphere = options.getfieldvalue('atmosphere', {});
+	canvas.atmosphere = { 												//Default Values
+		wavelength_r: defaultFor(atmosphere.wavelength_r, 			0.65), 		//0.65		Red wavelength (micrometers)
+		wavelength_g: defaultFor(atmosphere.wavelength_g, 			0.57),		//0.57		Green wavelength (micrometers)
+		wavelength_b: defaultFor(atmosphere.wavelength_b, 			0.475),		//0.475		Green wavelength (micrometers)
+		eSun: 	defaultFor(atmosphere.eSun, 						100.0),		//20.0		Sun intensity	
+		kRayleigh: defaultFor(atmosphere.kRayleigh, 				0.0025),	//0.0025	Rayleigh scattering amount
+		kMie: defaultFor(atmosphere.kMie, 							0.000), 	//0.01		Mie scattering amount
+		g: defaultFor(atmosphere.g, 								-0.99),		//-0.99		Mie phase asymmetry/direction factor
+		hdr_exposure: defaultFor(atmosphere.hdr_exposure, 			0.8),		//0.8		High Dynamic Range Exposure
+		scaleHeight: defaultFor(atmosphere.scaleHeight, 			1.25), 		//1.025		Scale of height of atmosphere to earth radius.
+		scaleDepth: defaultFor(atmosphere.scaleDepth, 				0.25), 		//0.25		Percentage altitude at which the atmosphere's average density is found
+		a: defaultFor(atmosphere.a, 								-0.00287),	//-0.00287	Scaling constant a
+		b: defaultFor(atmosphere.b, 								0.459),		//0.459		Scaling constant b
+		c: defaultFor(atmosphere.c, 								3.83),		//3.83		Scaling constant c
+		d: defaultFor(atmosphere.d, 								-6.80),		//-6.80		Scaling constant d
+		e: defaultFor(atmosphere.e, 								3.6),		//5.25		Scaling constant e. Lower when increasing atmosphere scale.
+		attenuation: defaultFor(atmosphere.attenuation, 			0.5)		//0.5		Strength of atmospheric scattering on ground shading.
+	};
+	updateAtmosphereParameters(canvas);
 	var animation = options.getfieldvalue('movies', {});
 	canvas.animation = {
-		frame: defaultFor(animation.frame, 0),
-		play: defaultFor(animation.play, true),
-		increment: defaultFor(animation.increment, true),
-		fps: defaultFor(animation.fps, 4),
-		interval: defaultFor(animation.interval, 1000 / animation.fps),
-		loop: defaultFor(animation.loop, true),
-		handler: defaultFor(animation.handler, 0)
+		frame: defaultFor(animation.frame, 							0),
+		play: defaultFor(animation.play, 							true),
+		increment: defaultFor(animation.increment, 					true),
+		fps: defaultFor(animation.fps, 								4),
+		interval: defaultFor(animation.interval, 					1000 / defaultFor(animation.fps, 4)),
+		loop: defaultFor(animation.loop, 							true),
+		handler: defaultFor(animation.handler, 						0)
 	}
 	var brush = options.getfieldvalue('brush', {});
 	canvas.brush = {
-		enabled: defaultFor(brush.enabled, false),
-		strength: defaultFor(brush.strength, 0.075),
-		falloff: defaultFor(brush.falloff, 0.5),
-		hit: defaultFor(brush.hit, {})
+		enabled: defaultFor(brush.enabled, 							false),
+		strength: defaultFor(brush.strength, 						0.075),
+		falloff: defaultFor(brush.falloff, 							0.5),
+		hit: defaultFor(brush.hit, 									{})
 	};
 	var camera = options.getfieldvalue('camera', {});
 	canvas.camera = {
-		position: defaultFor(camera.position, vec3.create()),
-		rotation: defaultFor(camera.rotation, vec3.create()),
-		near: defaultFor(camera.near, 1e3),
-		far: defaultFor(camera.far, 1e10),
-		fov: defaultFor(camera.fov, 45),
-		vMatrix: defaultFor(camera.vMatrix, mat4.create()),
-		pMatrix: defaultFor(camera.pMatrix, mat4.create()),
-		vpMatrix: defaultFor(camera.vpMatrix, mat4.create()),
-		vInverseMatrix: defaultFor(camera.vInverseMatrix, mat4.create()),
-		pInverseMatrix: defaultFor(camera.pInverseMatrix, mat4.create()),
-		vpInverseMatrix: defaultFor(camera.vpInverseMatrix, mat4.create()),
-		ready: defaultFor(camera.ready, false)
-	};
-	var clouds = options.getfieldvalue('clouds', {});
-	canvas.clouds = {
-		enabled: defaultFor(clouds.enabled, false),
-		height: defaultFor(clouds.height, 7500),
-		quantity: defaultFor(clouds.quantity, 10),
-		hit: defaultFor(clouds.hit, {})
+		position: defaultFor(camera.position, 						vec3.create()),
+		rotation: defaultFor(camera.rotation, 						quat.create()),
+		relativePosition: defaultFor(camera.relativePosition, 		vec3.create()),
+		direction: defaultFor(camera.direction, 					vec3.create()),
+		near: defaultFor(camera.near, 								1e3),
+		far: defaultFor(camera.far, 								1e10),
+		fov: defaultFor(camera.fov, 								45),
+		vMatrix: defaultFor(camera.vMatrix, 						mat4.create()),
+		pMatrix: defaultFor(camera.pMatrix, 						mat4.create()),
+		vpMatrix: defaultFor(camera.vpMatrix, 						mat4.create()),
+		vInverseMatrix: defaultFor(camera.vInverseMatrix, 			mat4.create()),
+		pInverseMatrix: defaultFor(camera.pInverseMatrix, 			mat4.create()),
+		vpInverseMatrix: defaultFor(camera.vpInverseMatrix, 		mat4.create()),
+		ready: defaultFor(camera.ready, 							false)
 	};
 	var dataMarkers = options.getfieldvalue('datamarkers', {});
 	canvas.dataMarkers = {
-		enabled: defaultFor(dataMarkers.enabled, true),
-		values: defaultFor(dataMarkers.values, []),
-		image: defaultFor(dataMarkers.image, canvas.rootPath+'textures/data_marker.svg'),
-		size: defaultFor(dataMarkers.size, [32, 32]),
-		format: defaultFor(dataMarkers.format, ['X: %.2e<br>Y: %.2e<br>Z: %.2e<br>Value: %0.1f', 'x', 'y', 'z', 'value']),
-		animated: defaultFor(dataMarkers.animated, false),
-		labels: defaultFor(dataMarkers.labels, []),
-		font: defaultFor(dataMarkers.font, ''),
-		marker: defaultFor(dataMarkers.marker, document.getElementById('sim-data-marker-' + canvas.id)),
-		reposition: defaultFor(dataMarkers.reposition, true)
+		enabled: defaultFor(dataMarkers.enabled, 					true),
+		values: defaultFor(dataMarkers.values, 						[]),
+		image: defaultFor(dataMarkers.image, 						canvas.rootPath + 'textures/data_marker.svg'),
+		size: defaultFor(dataMarkers.size, 							[32, 32]),
+		format: defaultFor(dataMarkers.format, 						['X: %.2e<br>Y: %.2e<br>Z: %.2e<br>Value: %0.1f', 'x', 'y', 'z', 'value']),
+		animated: defaultFor(dataMarkers.animated, 					false),
+		labels: defaultFor(dataMarkers.labels, 						[]),
+		font: defaultFor(dataMarkers.font, 							''),
+		marker: defaultFor(dataMarkers.marker, 						document.getElementById('sim-data-marker-' + canvas.id)),
+		reposition: defaultFor(dataMarkers.reposition, 				true)
 	};
 	var draw = options.getfieldvalue('draw', {});
 	canvas.draw = {
-		ready: defaultFor(draw.ready, false),
-		handler: defaultFor(draw.handler, null)
+		ready: defaultFor(draw.ready, 								false),
+		handler: defaultFor(draw.handler, 							null)
 	};
 	var view = options.getfieldvalue('view', {});
 	canvas.view = {
-		position: defaultFor(view.position, [0.0, 0.0, 0.0]),
-		rotation: defaultFor(view.rotation, [0, 90]),
-		zoom: defaultFor(view.zoom, 1.0),
-		zoomLimits: defaultFor(view.zoomLimits, [0.001, 100.0]),
-		lastZoom: defaultFor(view.lastZoom, 1.0),
-		azimuthLimits: defaultFor(view.azimuthLimits, [0, 360]),
-		elevationLimits: defaultFor(view.elevationLimits, [-180, 180]),
-		panningEnabled: defaultFor(view.panningEnabled, false),
-		twod: defaultFor(view.twod, false)
+		position: defaultFor(view.position, 						[0.0, 0.0, 0.0]),
+		rotation: defaultFor(view.rotation, 						[0, 90]),
+		zoom: defaultFor(view.zoom, 								1.0),
+		zoomLimits: defaultFor(view.zoomLimits, 					[0.001, 100.0]),
+		lastZoom: defaultFor(view.lastZoom, 						1.0),
+		azimuthLimits: defaultFor(view.azimuthLimits, 				[0, 360]),
+		elevationLimits: defaultFor(view.elevationLimits, 			[-180, 180]),
+		panningEnabled: defaultFor(view.panningEnabled, 			false),
+		preventDefaultOnPan: defaultFor(view.preventDefaultOnPan, 	false),
+		twod: defaultFor(view.twod, 								false)
 	};
 
@@ -171,8 +192,9 @@
 	var shaders = {};
 	shaders.Colored = new GL.Shader.fromURL(rootPath+'shaders/Colored.vsh', rootPath+'shaders/Colored.fsh', null, gl);
+	shaders.ColoredDiffuse = new GL.Shader.fromURL(rootPath+'shaders/ColoredDiffuse.vsh', rootPath+'shaders/ColoredDiffuse.fsh', null, gl);
 	shaders.Textured = new GL.Shader.fromURL(rootPath+'shaders/Textured.vsh', rootPath+'shaders/Textured.fsh', null, gl);
+	shaders.TexturedDiffuse = new GL.Shader.fromURL(rootPath+'shaders/TexturedDiffuse.vsh', rootPath+'shaders/TexturedDiffuse.fsh', null, gl);
 	shaders.SkyFromSpace = new GL.Shader.fromURL(rootPath+'shaders/SkyFromSpace.vert', rootPath+'shaders/SkyFromSpace.frag', null, gl);
 	shaders.GroundFromSpace = new GL.Shader.fromURL(rootPath+'shaders/GroundFromSpace.vert', rootPath+'shaders/GroundFromSpace.frag', null, gl);
-	shaders.Clouds = new GL.Shader.fromURL(rootPath+'shaders/Clouds.vert', rootPath+'shaders/Clouds.frag', null, gl);
 	return shaders;
 } //}}}
@@ -184,4 +206,22 @@
 	return gl.textures[imageSource];
 } //}}}
+function updateAtmosphereParameters(canvas) {
+	//Precalculate derived atmosphere shader parameters
+	//TODO: Find a better way to structure this
+	var atm = canvas.atmosphere;
+	atm.inv_wavelength4 = [1.0 / Math.pow(atm.wavelength_r, 4), 1.0 / Math.pow(atm.wavelength_g, 4), 1.0 / Math.pow(atm.wavelength_b, 4)];
+	atm.innerRadius = 6.371e6;
+	atm.innerRadius2 = atm.innerRadius * atm.innerRadius;
+	atm.outerRadius = atm.innerRadius * atm.scaleHeight;
+	atm.outerRadius2 = atm.outerRadius * atm.outerRadius;
+	atm.krESun = atm.kRayleigh * atm.eSun;
+	atm.kmESun = atm.kMie * atm.eSun;
+	atm.kr4PI = atm.kRayleigh * 4 * Math.PI;
+	atm.km4PI = atm.kMie * 4 * Math.PI;
+	atm.scale = 1.0 / (atm.outerRadius - atm.innerRadius);
+	atm.scaleOverScaleDepth = atm.scale / atm.scaleDepth;
+	atm.g2 = atm.g * atm.g;
+	canvas.atmosphere = atm;
+}
 function clamp(value, min, max) { //{{{
 	return Math.max(min, Math.min(value, max));
@@ -253,47 +293,15 @@
 } //}}}
 //}}}
-//{{{ Interface Functions
+//{{{ Interaction Functions
 function onTap(ev, canvas) { //{{{
-	//Sets up a marker on a canvas that will track a point on the mesh. Can be dismissed by closing the display or clicking the marker.
 	ev.preventDefault();
 	
-	if (canvas.clouds.enabled) {
-		for (var i = 0; i < canvas.clouds.quantity; i++) {
-			raycast(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, canvas.nodes["clouds" + i]);
-		}
-	}
-	var hit = raycast(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, canvas.unitNode);
-	if ('cities' in canvas.nodes) {
-		var hitCities = raycast(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, canvas.nodes.cities);
-		canvas.nodes.cities.hit = hitCities;
-		updateCities(canvas);
-	}
-
-	canvas.brush.hit = hit;
-	
-	if (canvas.dataMarkers.enabled) {
-		canvas.dataMarkers.marker.selector.closed = false;
-		canvas.dataMarkers.marker.hit = hit;
-		updateMarker(canvas, true);
-	}
-	
-	brushModify(canvas);
+	var hit = raycastXY(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, canvas.unitNode);
+
+	//Trigger any handlers attatched to this canvas event.
+	canvas.selector.trigger('onTap', [ev, canvas, hit]);
 } //}}}
 function onPan(ev, canvas, displaylog) { //{{{
 	ev.preventDefault();
-	
-	if (canvas.dataMarkers.enabled) {
-		if (!isEmptyOrUndefined(canvas.unitNode)) {
-			canvas.brush.hit = raycast(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, canvas.unitNode);
-			brushModify(canvas);
-		}
-	}
-	
-	if (canvas.clouds.enabled) {
-		if (!isEmptyOrUndefined(canvas.nodes['overlay'])) {
-			canvas.clouds.hit = raycast(canvas, ev.srcEvent.layerX, ev.srcEvent.layerY, canvas.nodes['overlay']);
-			updateClouds(canvas);
-		}
-	}
 	
 	if (ev.type === 'panstart') {
@@ -301,55 +309,74 @@
 		canvas.lastDeltaY = 0;
 	}
-	if (ev.srcEvent.shiftKey || ev.pointers.length === 2) {
-		if (!canvas.view.panningEnabled) return;
-		var deltaX = (canvas.lastDeltaX - ev.deltaX) / canvas.clientWidth / canvas.view.zoom * 2 * canvas.controlSensitivity * 6.371e6;
-		var deltaY = (canvas.lastDeltaY - ev.deltaY) / canvas.clientHeight / canvas.view.zoom * 2 * canvas.controlSensitivity * 6.371e6;
-		
-		if (canvas.view.twod) {
-			canvas.view.position[0] += Math.cos(DEG2RAD * canvas.view.rotation[0]) * deltaX - Math.sin(DEG2RAD * 0) * deltaY;
-			canvas.view.position[2] += Math.sin(DEG2RAD * canvas.view.rotation[0]) * deltaX + Math.cos(DEG2RAD * 0) * deltaY;
+	
+	//Trigger any handlers attatched to this canvas event.
+	canvas.selector.trigger('onPan', [ev, canvas]);
+	
+	//If any onPan handler sets preventDefaultOnPan to true, skips default onPan camera behavior
+	if (!canvas.view.preventDefaultOnPan) {
+		//If panning with two fingers or shift key, translate camera center
+		if (ev.srcEvent.shiftKey || ev.pointers.length === 2) {
+			if (canvas.view.panningEnabled) {
+				var deltaX = (canvas.lastDeltaX - ev.deltaX) / canvas.clientWidth / canvas.view.zoom * 2 * canvas.controlSensitivity * 6.371e6;
+				var deltaY = (canvas.lastDeltaY - ev.deltaY) / canvas.clientHeight / canvas.view.zoom * 2 * canvas.controlSensitivity * 6.371e6;
+				
+				//TODO: convert canvas.view.rotation from az/el euler to quaternion
+				if (canvas.view.twod) {
+					canvas.view.position[0] += Math.cos(DEG2RAD * canvas.view.rotation[0]) * deltaX - Math.sin(DEG2RAD * 0) * deltaY;
+					canvas.view.position[2] += Math.sin(DEG2RAD * canvas.view.rotation[0]) * deltaX + Math.cos(DEG2RAD * 0) * deltaY;
+				}
+				else {
+					canvas.view.position[0] += Math.cos(DEG2RAD * canvas.view.rotation[0]) * deltaX - Math.sin(DEG2RAD * canvas.view.rotation[0]) * deltaY;
+					canvas.view.position[2] += Math.sin(DEG2RAD * canvas.view.rotation[0]) * deltaX + Math.cos(DEG2RAD * canvas.view.rotation[0]) * deltaY;
+				}
+			}
 		}
+		//Else, rotate around camera center
 		else {
-			canvas.view.position[0] += Math.cos(DEG2RAD * canvas.view.rotation[0]) * deltaX - Math.sin(DEG2RAD * canvas.view.rotation[0]) * deltaY;
-			canvas.view.position[2] += Math.sin(DEG2RAD * canvas.view.rotation[0]) * deltaX + Math.cos(DEG2RAD * canvas.view.rotation[0]) * deltaY;
+			canvas.view.rotation[0] += (canvas.lastDeltaX - ev.deltaX) / canvas.clientWidth * 2 * canvas.controlSensitivity * RAD2DEG;
+			canvas.view.rotation[1] += (canvas.lastDeltaY - ev.deltaY) / canvas.clientHeight * -2 * canvas.controlSensitivity * RAD2DEG;
+			
+			if (canvas.view.rotation[0] > 360) { canvas.view.rotation[0] -= 360; };
+			if (canvas.view.rotation[0] < -360) { canvas.view.rotation[0] += 360; };
+			if (canvas.view.rotation[1] > 180) { canvas.view.rotation[1] -= 360; };
+			if (canvas.view.rotation[1] < -180) { canvas.view.rotation[1] += 360; };
+			
+			canvas.view.rotation[0] = clamp(canvas.view.rotation[0], canvas.view.azimuthLimits[0], canvas.view.azimuthLimits[1]);
+			canvas.view.rotation[1] = clamp(canvas.view.rotation[1], canvas.view.elevationLimits[0], canvas.view.elevationLimits[1]);
+			
+			if (displaylog) { console.log(canvas.view.rotation); }
 		}
-	}
-	
-	else {
-		canvas.view.rotation[0] += (canvas.lastDeltaX - ev.deltaX) / canvas.clientWidth * 2 * canvas.controlSensitivity * RAD2DEG;
-		canvas.view.rotation[1] += (canvas.lastDeltaY - ev.deltaY) / canvas.clientHeight * -2 * canvas.controlSensitivity * RAD2DEG;
-		
-		if (canvas.view.rotation[0] > 360) { canvas.view.rotation[0] -= 360; };
-		if (canvas.view.rotation[0] < -360) { canvas.view.rotation[0] += 360; };
-		if (canvas.view.rotation[1] > 180) { canvas.view.rotation[1] -= 360; };
-		if (canvas.view.rotation[1] < -180) { canvas.view.rotation[1] += 360; };
-		
-		canvas.view.rotation[0] = clamp(canvas.view.rotation[0], canvas.view.azimuthLimits[0], canvas.view.azimuthLimits[1]);
-		canvas.view.rotation[1] = clamp(canvas.view.rotation[1], canvas.view.elevationLimits[0], canvas.view.elevationLimits[1])
-	}
+	}	
+	
+	canvas.view.preventDefaultOnPan = false;
 	canvas.lastDeltaX = ev.deltaX;
 	canvas.lastDeltaY = ev.deltaY;
-	
-	canvas.dataMarkers.reposition = true;
-	
-	if (displaylog) { console.log(canvas.view.rotation); }
 } //}}}
 function onPinch(ev, canvas, displaylog) { //{{{
 	ev.preventDefault();
 	if (ev.type === 'pinchstart') { canvas.view.lastZoom = canvas.view.zoom; }
-	else { modifyZoom(ev.scale * canvas.view.lastZoom, canvas, displaylog); }
+	else { modifyZoom(ev.scale * canvas.view.lastZoom, canvas, displaylog, ev, 0); }
 } //}}}
 function onZoom(ev, canvas, displaylog) { //{{{
 	ev.preventDefault();
-	var delta = clamp(ev.scale || ev.wheelDelta || -ev.detail, -1, 1) * canvas.controlSensitivity * canvas.view.zoom / 20;
-	modifyZoom(canvas.view.zoom + delta, canvas, displaylog);
-} //}}}
-function modifyZoom(value, canvas, displaylog) { //{{{
-	canvas.view.zoom = clamp(value, canvas.view.zoomLimits[0], canvas.view.zoomLimits[1]);
-	canvas.dataMarkers.reposition = true;
-	if (displaylog) { console.log(canvas.view.zoom); }
-} //}}}
-function modifyDataMarkersEnabled(value, canvas) { //{{{
-	canvas.dataMarkers.enabled = value;
+	var delta = clamp(ev.scale || ev.wheelDelta || -ev.detail, -1, 1) * canvas.controlSensitivity * canvas.view.zoom / 2;
+	modifyZoom(canvas.view.zoom + delta, canvas, displaylog, ev, 0);
+} //}}}
+function modifyZoom(value, canvas, displaylog, ev, duration) { //{{{
+	if (isEmptyOrUndefined(duration)) duration = 200;
+	var targetZoom = clamp(value, canvas.view.zoomLimits[0], canvas.view.zoomLimits[1]);
+	var currentZoom = canvas.view.zoom;
+	animateValue(
+		0,
+		1.0,
+		duration,
+		function(value, info) {
+			canvas.view.zoom = currentZoom * (1 - value) + targetZoom * value;
+			if (displaylog) { console.log(canvas.view.zoom); }
+			
+			//Trigger any handlers attatched to this canvas event.
+			canvas.selector.trigger('onZoom', [ev, canvas]);
+		}
+	);
 } //}}}
 function toggleMoviePlay(canvas) { //{{{
@@ -364,288 +391,39 @@
 	}
 } //}}}
-//}}}
-//{{{ Interaction Functions
-function raycast(canvas, x, y, node) { //{{{
-	//Performs raycast on canvas.unitNode.mesh using x/y screen coordinates. Returns hit objects with hit position, coords, and indicies of ray-triangle intersection.
+function screenToWorldPoint(canvas, x, y) { //{{{
+	var viewportX = (x - canvas.width / 2) / (canvas.width / 2);
+	var viewportY = (canvas.height / 2 - y) / (canvas.height / 2);
+	var origin = vec3.transformMat4(vec3.create(), [viewportX, viewportY, 0], canvas.camera.vpInverseMatrix);
+	return origin;
+} //}}}
+function screenToModelRay(canvas, x, y, node) { //{{{
+	var inverseMVPMatrix = mat4.invert(mat4.create(), mat4.multiply(mat4.create(), canvas.camera.vpMatrix, node.modelMatrix));
+	var viewportX = (x - canvas.width / 2) / (canvas.width / 2);
+	var viewportY = (canvas.height / 2 - y) / (canvas.height / 2);
+	var origin = vec3.transformMat4(vec3.create(), [viewportX, viewportY, 0], inverseMVPMatrix);
+	var far = vec3.transformMat4(vec3.create(), [viewportX, viewportY, 1.0], inverseMVPMatrix);
+	var direction = vec3.subtract(vec3.create(), far, origin);
+	return {'origin':origin, 'direction':direction};
+} //}}}
+function raycast(canvas, origin, direction, node) { //{{{
+	//Performs raycast on given node using ray origin and direction vectors.
+	//Returns hit objects with hit position, normals, barycentric coordinates, element number, and indices of ray-triangle intersection.
 	//TODO: Diagnose marker issues with orthographic views and slr-eustatic updates when switching between basins.
-	var inverseMVPMatrix = mat4.invert(mat4.create(), mat4.multiply(mat4.create(), canvas.camera.vpMatrix, node.modelMatrix));
-	var origin = vec3.transformMat4(vec3.create(), [(x - canvas.width / 2) / (canvas.width / 2), (canvas.height / 2 - y) / (canvas.height / 2), 0], inverseMVPMatrix);
-	var far = far || vec3.transformMat4(vec3.create(), [(x - canvas.width / 2) / (canvas.width / 2), (canvas.height / 2 - y) / (canvas.height / 2), 1.0], inverseMVPMatrix);
-	var ray = vec3.subtract(vec3.create(), far, origin);
-
-	var mesh = node.mesh;
-
-	if (!mesh) { return; }
-	if (!node.octree) { node.octree = new GL.Octree(mesh); }
-	
-	var hit = node.octree.testRay(origin, ray, 1e3, 1e10);
-	
-	if(!hit) { return; }
-
-	if (node.name.startsWith("clouds")) { canvas.clouds.selected = node.name; alert("New selected cloud: " + canvas.clouds.selected); }
+	if (!node.octree) { node.octree = new GL.Octree(node.mesh); }
+	
+	var hit = node.octree.testRay(origin, direction, 1e3, 1e10);
+	if (!hit) { return; }
 
 	hit.modelPos = vec3.copy(vec3.create(), hit.pos);
 	vec3.transformMat4(hit.pos, hit.pos, node.modelMatrix);
-	vec3.transformMat4(hit.normal, hit.normal, node.modelMatrix);
-
+	
 	return hit;
 } //}}}
-function updateCities(canvas) {
-	//Update selected city
-	var hit = canvas.nodes.cities.hit;
-	if (hit) {
-		citiesIndex = Math.floor(hit.indices[0] / ((ArrayMax(canvas.nodes.cities.mesh.getIndexBuffer('triangles').data) + 1) / cities.length));
-		cityName = cities[citiesIndex];
-		if (cityName !== $('#gfm-sim-controls-select-city').val()) {
-			$('#gfm-sim-controls-select-city').val(cityName).selectmenu("refresh");
-			changeCity(canvas);
-		}
-	}
-}
-function brushModify(canvas) { //{{{
-	//This function takes in the canvas and x/y coordinates, performing a raycast against the mesh, and modifies the mesh using a the canvas.brush.strength and canvas.brush.falloff properties.
-	//Currently the brush extends to the raycasted element and its immediate neighbors.
-	//TODO: Allow variable brush size/additional neighbors. Allow the function to work on multiple models (currently hardcoded to md model).
-	if (!canvas.unitNode || canvas.brush.enabled != 'on') { return; }
-	
-	var hit = canvas.brush.hit;
-
-	if (hit) {
-		var bufferVertices = canvas.unitNode.mesh.getBuffer('vertices');
-		var vertices = bufferVertices.data;
-		var bufferCoords = canvas.unitNode.mesh.getBuffer('coords');
-		var coords = bufferCoords.data;
-
-		//Query nearby elements and store indicies of affected vertices using pregenerated vertexconnectivity list (from NodeConnectivity)
-		var baseIndices = new Set(hit.indices);
-		var connectedIndices = new Set(hit.indices);
-		var connectedElement;
-		var indices;
-		var lengthIndex = md.mesh.vertexconnectivity[0].length - 1;
-		var length;
-		for (var i = 0; i < 3; i++) {
-			length = md.mesh.vertexconnectivity[hit.indices[i]][lengthIndex];
-			for (var j = 0; j < length; j++) {
-				//Shift elements down by one (matlab 1-based index to 0-based index)
-				connectedElement = md.mesh.vertexconnectivity[hit.indices[i]][j] - 1;
-				indices = md.mesh.elements[connectedElement];
-				connectedIndices.add(indices[0] - 1);
-				connectedIndices.add(indices[1] - 1);
-				connectedIndices.add(indices[2] - 1);
-			}
-		}
-
-		//Apply modifications to included vertices in mesh using brush strength and falloff.
-		var strength;
-		for (var index of connectedIndices) {
-			if (!baseIndices.has(index)) {
-				strength = canvas.brush.strength * canvas.brush.falloff;
-			}
-			else {
-				strength = canvas.brush.strength;
-			}
-			vertices[index*3+2] += strength * 100;
-			md.geometry.surface[index] += strength;	
-			md.geometry.thickness[index] += strength;
-			coords[index*2+1] += strength;
-			canvas.unitData[index] += strength;
-		}
-		
-		//Update mesh on GPU
-		bufferVertices.upload(canvas.gl.DYNAMIC_DRAW);
-		bufferCoords.upload(canvas.gl.DYNAMIC_DRAW);
-		canvas.unitNode.octree = new GL.Octree(canvas.unitNode.mesh);	
-	}
-} //}}}
-function updateClouds(canvas) {
-	//Update clouds if rendered
-	//TODO: Steven, the hit now queries the radaroverlay.
-	if (canvas.nodes[canvas.clouds.selected]) {
-		var v1 = vec3.fromValues(vertices[hit.indices[0] * 3], vertices[hit.indices[0] * 3 + 1], vertices[hit.indices[0] * 3 + 2]);
-		var v2 = vec3.fromValues(vertices[hit.indices[1] * 3], vertices[hit.indices[1] * 3 + 1], vertices[hit.indices[1] * 3 + 2]);
-		var v3 = vec3.fromValues(vertices[hit.indices[2] * 3], vertices[hit.indices[2] * 3 + 1], vertices[hit.indices[2] * 3 + 2]);
-		vec3.transformMat4(v1, v1, canvas.unitNode.modelMatrix);//move out of brushModify, perhaps onto onPan
-		vec3.transformMat4(v2, v2, canvas.unitNode.modelMatrix);
-		vec3.transformMat4(v3, v3, canvas.unitNode.modelMatrix);
-		var x  = (v1[0] + v2[0] + v3[0]) / 3;// + Math.floor((Math.random() * (1 + 10000 - (-10000)) + (-10000)));
-		var y  = (v1[1] + v2[1] + v3[1]) / 3;// + Math.floor((Math.random() * (1 + 10000 - (-10000)) + (-10000)));
-		var z  = (v1[2] + v2[2] + v3[2]) / 3;
-		canvas.nodes[canvas.clouds.selected].translation = [x, y + canvas.clouds.height, z];
-		updateModelMatrix(canvas.nodes[canvas.clouds.selected]);
-	}
-}
-function initializeMarker(canvas) { //{{{
-	//Initialize data marker and tooltip display once per page load
-	var marker = $('#' + canvas.dataMarkers.marker.id);
-	var size = canvas.dataMarkers.size;
-	if (!marker.hasClass('tooltipstered')) {
-		marker.css({
-			'position': 'absolute',
-			'left': -size[0] + 'px',
-			'top': -size[1] + '0px',
-			'width': size[0] + 'px',
-			'height': size[1] + 'px',
-			'pointer-events': 'all',
-			'cursor': 'pointer',
-			'display': 'none'
-		});
-		marker.tooltipster({
-			contentAsHTML: true,
-			maxWidth: 320,
-			maxHeight: 320,
-			zIndex: 1000,
-			trigger: 'custom',
-			triggerOpen: {
-				mouseenter: false,
-				originClick: true,
-				touchstart: false
-			},
-			triggerClose: {
-				mouseleave: false,
-				originClick: true,
-				touchleave: false
-			},
-		});
-		marker.on('click touch', function () {
-			marker.fadeOut(175);
-			marker.tooltipster('close');
-			marker.closed = true;
-		});
-		marker.closed = false;
-		canvas.dataMarkers.marker.selector = marker;
-	}
-	updateMarker(canvas, true);
-} //}}}
-function repositionMarker(canvas) { //{{{
-	//Mover marker to point to mouse position, offset in y by 1 to enable immediate clicking.
-	//Return if no marker hit exists, the camera is not rendering, or if no reposition has been scheduled.
-	if (isEmptyOrUndefined(canvas.dataMarkers.marker.hit) || !canvas.camera.ready || !canvas.dataMarkers.reposition) { return; }
-	var size = canvas.dataMarkers.size;
-	var screenPoint = vec3.transformMat4(vec3.create(), canvas.dataMarkers.marker.hit.pos, canvas.camera.vpMatrix);
-	var x = (screenPoint[0] + 1.0) * (canvas.width / 2) + canvas.selector.offset().left;
-	var y = (-screenPoint[1] + 1.0) * (canvas.height / 2) + canvas.selector.offset().top;
-	canvas.dataMarkers.marker.selector.css({
-		'left': (Math.round(x) - size[0] / 2) + 'px', 
-		'top': (Math.round(y) - size[1] + 1) + 'px'
-	});
-	if (canvas.dataMarkers.marker.selector.tooltipster('status').state != 'closed') { canvas.dataMarkers.marker.selector.tooltipster('reposition'); }
-	canvas.dataMarkers.reposition = false;
-} //}}}
-function updateMarker(canvas, reset) { //{{{
-	//Retrieve data value fields and plots them on data marker popup if a hit has been registered.
-	//TODO: Automatically pick up any field of size md.mesh.numberofelements
-	//If no marker has been placed, no update is needed. If canvas is resimulating and unitNode has not been set yet, wait and try again.
-	if (!canvas.dataMarkers.enabled || isEmptyOrUndefined(canvas.dataMarkers.marker.hit)) { return; }
-	if (isEmptyOrUndefined(canvas.unitNode) || isEmptyOrUndefined(canvas.unitNode.mesh)) { setTimeout( function(){ updateMarker(canvas, reset); }, 750); return; }
-	
-	var hit = canvas.dataMarkers.marker.hit;
-	
-	var coords = canvas.unitNode.mesh.vertexBuffers.coords.data;
-	var latitude = md.mesh.lat;
-	var longitude = md.mesh.long;
-	var thickness;
-	var velocity;
-	if (md.results[0]) {
-		thickness = md.results[canvas.animation.frame].Thickness;
-		velocity = md.results[canvas.animation.frame].Vel;
-	}
-	else {
-		thickness = md.geometry.thickness;
-		velocity = md.initialization.vel;
-	}
-	
-	//Construct new argument array of the data display format for sprintf using first first argument as the formatSpecifier string and the rest as the additional arguments.
-	var labels = canvas.dataMarkers.labels.slice();
-	for (var i = 0; i < labels.length; i++) {
-		if (labels[i].toLowerCase() === 'x') { labels[i] = hit.modelPos[0]; }
-		else if (labels[i].toLowerCase() === 'y') { labels[i] = hit.modelPos[1]; }
-		else if (labels[i].toLowerCase() === 'z') { labels[i] = hit.modelPos[2]; }
-		else if (labels[i].toLowerCase() === 'latitude') {
-			var hitLatitude = [latitude[hit.indices[0]], latitude[hit.indices[1]], latitude[hit.indices[2]]];
-			var valueLatitude = Math.abs(hitLatitude[0] * hit.uvw[0] + hitLatitude[1] * hit.uvw[1] + hitLatitude[2] * hit.uvw[2]);
-			labels[i] = valueLatitude;
-		}
-		else if (labels[i].toLowerCase() === 'longitude') {
-			var hitLongitude = [longitude[hit.indices[0]], longitude[hit.indices[1]], longitude[hit.indices[2]]];
-			var valueLongitude = Math.abs(hitLongitude[0] * hit.uvw[0] + hitLongitude[1] * hit.uvw[1] + hitLongitude[2] * hit.uvw[2]);
-			labels[i] = valueLongitude;
-		}
-		else if (labels[i].toLowerCase() === 'thickness') {
-			var hitThickness = [thickness[hit.indices[0]], thickness[hit.indices[1]], thickness[hit.indices[2]]];
-			var valueThickness = hitThickness[0] * hit.uvw[0] + hitThickness[1] * hit.uvw[1] + hitThickness[2] * hit.uvw[2];
-			labels[i] = valueThickness; 
-		}
-		else if (labels[i].toLowerCase() === 'velocity') {
-			var hitVelocity = [velocity[hit.indices[0]], velocity[hit.indices[1]], velocity[hit.indices[2]]];
-			var valueVelocity = hitVelocity[0] * hit.uvw[0] + hitVelocity[1] * hit.uvw[1] + hitVelocity[2] * hit.uvw[2];	
-			labels[i] = valueVelocity; 
-		}
-		else if (labels[i].toLowerCase() === 'value') {
-			var hitCoords = [coords[hit.indices[0]*2], coords[hit.indices[0]*2+1], coords[hit.indices[1]*2], coords[hit.indices[1]*2+1], coords[hit.indices[2]*2], coords[hit.indices[2]*2+1]];
-			var u = hitCoords[0] * hit.uvw[0] + hitCoords[2] * hit.uvw[1] + hitCoords[4] * hit.uvw[2];
-			var v = hitCoords[1] * hit.uvw[0] + hitCoords[3] * hit.uvw[1] + hitCoords[5] * hit.uvw[2];
-			var value = canvas.unitNode.caxis[0] * (1.0 - v) + canvas.unitNode.caxis[1] * v;
-			labels[i] = value; 
-		}
-	}
-	
-	//Apply changes to tooltip
-	$('#tooltip-content-data-marker-' + canvas.id).html(sprintf.apply(null, canvas.dataMarkers.format.concat(labels)));
-	$('#tooltip-content-data-marker-' + canvas.id).css({'font': canvas.dataMarkers.font});				
-	
-	//If animated, setup animation loop to update plot as movie plays.
-	if (canvas.dataMarkers.animated) {
-		var isEmpty = (canvas.dataMarkers.values.length === 0);
-		var lastUpdatedIndex = (canvas.dataMarkers.values.length - 1);
-		var newMovieFrame = (!isEmpty && canvas.dataMarkers.values[lastUpdatedIndex][0] != canvas.animation.frame);
-		//If new data marker has been placed, reinitialize plot. If not, push new value into plot value array.
-		if (reset) {
-			canvas.dataMarkers.values = [];
-			newMovieFrame = true;
-			for (var currentFrame = 0; currentFrame < (canvas.unitNode.movieLength); currentFrame++) {
-				coords = canvas.unitMovieData[currentFrame];
-				var hitCoords = [coords[hit.indices[0]*2], coords[hit.indices[0]*2+1], coords[hit.indices[1]*2], coords[hit.indices[1]*2+1], coords[hit.indices[2]*2], coords[hit.indices[2]*2+1]];
-				var u = hitCoords[0] * hit.uvw[0] + hitCoords[2] * hit.uvw[1] + hitCoords[4] * hit.uvw[2];
-				var v = hitCoords[1] * hit.uvw[0] + hitCoords[3] * hit.uvw[1] + hitCoords[5] * hit.uvw[2];
-				var value = canvas.unitNode.caxis[0] * (1.0 - v) + canvas.unitNode.caxis[1] * v;
-				canvas.dataMarkers.values.push([currentFrame, value]);
-			}
-		}
-		else {
-			if (canvas.animation.lastFrame > canvas.animation.frame) {
-				canvas.dataMarkers.values = [];
-			}
-			if (isEmpty || newMovieFrame) {
-				coords = canvas.unitMovieData[canvas.animation.frame];
-				var hitValues = [coords[hit.indices[0]], coords[hit.indices[1]], coords[hit.indices[2]]];
-				var value = hitValues[0] * hit.uvw[0] + hitValues[1] * hit.uvw[1] + hitValues[2] * hit.uvw[2];	
-				canvas.dataMarkers.values.push([canvas.animation.frame, value]);
-			}
-		}
-		
-		//Replot data marker popup using update data value array.
-		if (isEmpty || newMovieFrame) {
-			var dataLabels = {'latitude': valueLatitude, 'longitude': valueLongitude, 'thickness': valueThickness, 'velocity': valueVelocity, 'value': value};
-			var dataDisplay = canvas.dataMarkers.values.slice(0, canvas.animation.frame + 1);		
-			plot(
-				'id', '#sim-plot', 
-				'type', 'bar', 
-				'width', 400, 
-				'height', 300, 
-				'nticks', 25, 
-				'xlabel', 'Time', 
-				'ylabel', 'Value', 
-				'title', 'Changes Over Time', 
-				'datalabels', canvas.dataMarkers.labels,
-				'labelvalues', dataLabels,
-				'data', dataDisplay
-			);
-		}
-	}
-	canvas.dataMarkers.reposition = true;
-	if (reset && !canvas.dataMarkers.marker.selector.closed) {
-		canvas.dataMarkers.marker.selector.fadeIn(175);
-		canvas.dataMarkers.marker.selector.tooltipster('open');
-	}
+function raycastXY(canvas, x, y, node) { //{{{
+	//Performs raycast on given node using x and y screenspace coordinates.
+	//Returns hit objects with hit position, normals, barycentric coordinates, element number, and indices of ray-triangle intersection.
+	//TODO: Diagnose marker issues with orthographic views and slr-eustatic updates when switching between basins.
+	var ray = screenToModelRay(canvas, x, y, node);
+	return raycast(canvas, ray.origin, ray.direction, node);
 } //}}}
 function animateValue(current, target, duration, stepCallback, doneCallback) { //{{{
@@ -669,14 +447,15 @@
 	var elevationRotationMatrix = mat4.create();
 	var aspectRatio = canvas.clientWidth / canvas.clientHeight;
-	var cameraPosition = vec3.create();
-
-	if (canvas.view.twod) { mat4.ortho(pMatrix, -aspectRatio*6.371e6/canvas.view.zoom, aspectRatio*6.371e6/canvas.view.zoom, -6.371e6/canvas.view.zoom, 6.371e6/canvas.view.zoom, canvas.camera.near, canvas.camera.far); }
-	else { mat4.perspective(pMatrix, canvas.camera.fov * DEG2RAD, aspectRatio, canvas.camera.near, canvas.camera.far); }
+	var camera = canvas.camera;
+	var view = canvas.view;
+
+	if (view.twod) { mat4.ortho(pMatrix, -aspectRatio*6.371e6/view.zoom, aspectRatio*6.371e6/view.zoom, -6.371e6/view.zoom, 6.371e6/view.zoom, camera.near, camera.far); }
+	else { mat4.perspective(pMatrix, camera.fov * DEG2RAD, aspectRatio, camera.near, camera.far); }
 	
 	//Apply worldspace translation
-	mat4.translate(vMatrix, translateMatrix, vec3.negate(vec3.create(), canvas.view.position));
+	mat4.translate(vMatrix, translateMatrix, vec3.negate(vec3.create(), view.position));
 	
 	//Calculate rotation around camera focal point about worldspace origin
-	if (canvas.view.twod) {
+	if (view.twod) {
 		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, -DEG2RAD * 0, [0, 1, 0]);
 		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * 90, [1, 0, 0]);
@@ -684,7 +463,13 @@
 	}
 	else {
-		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, -DEG2RAD * (canvas.view.rotation[0] + 90), [0, 1, 0]);
-		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * canvas.view.rotation[1], [1, 0, 0]);
+		mat4.rotate(azimuthRotationMatrix, azimuthRotationMatrix, -DEG2RAD * (view.rotation[0] + 90), [0, 1, 0]);
+		mat4.rotate(elevationRotationMatrix, elevationRotationMatrix, DEG2RAD * view.rotation[1], [1, 0, 0]);
 		mat4.multiply(rotationMatrix, elevationRotationMatrix, azimuthRotationMatrix);
+		//var quaternionWorldX = Node.prototype.eulerToQuaternion(0, 0, DEG2RAD * (view.rotation[0]));
+		//var quaternionWorldY = Node.prototype.eulerToQuaternion(0, DEG2RAD * (view.rotation[1]), 0);
+		//var quaternionWorldZ = Node.prototype.eulerToQuaternion(DEG2RAD * (view.rotation[2]), 0, 0);
+		//var quaternionTemp = quat.multiply(quat.create(), quaternionWorldY, quaternionWorldX);
+		//quat.multiply(camera.rotation, quaternionWorldZ, quaternionTemp);
+		//mat4.fromQuat(rotationMatrix, camera.rotation);	
 	}
 
@@ -694,18 +479,20 @@
 	//Apply screenspace translation to emulate rotation around point
 	mat4.identity(translateMatrix);
-	mat4.translate(translateMatrix, translateMatrix, [0.0, 0.0, -6.371e6/canvas.view.zoom]);
+	mat4.translate(translateMatrix, translateMatrix, [0.0, 0.0, -6.371e6/view.zoom]);
 	mat4.multiply(vMatrix, translateMatrix, vMatrix);
 	
 	//Apply projection matrix to get camera matrix
-	mat4.copy(canvas.camera.vMatrix, vMatrix);
-	mat4.multiply(canvas.camera.vpMatrix, pMatrix, vMatrix);
+	mat4.copy(camera.vMatrix, vMatrix);
+	mat4.multiply(camera.vpMatrix, pMatrix, vMatrix);
 	
 	//Calculate inverse view matrix fields for lighting and raycasts
-	mat4.invert(canvas.camera.vInverseMatrix, canvas.camera.vMatrix);
-	mat4.invert(canvas.camera.vpInverseMatrix, canvas.camera.vpMatrix);
-	
-	vec3.transformMat4(canvas.camera.position, cameraPosition, canvas.camera.vpInverseMatrix);
-	canvas.camera.ready = true;
-	repositionMarker(canvas);
+	mat4.invert(camera.vInverseMatrix, camera.vMatrix);
+	mat4.invert(camera.vpInverseMatrix, camera.vpMatrix);
+	
+	vec3.transformMat4(camera.position, vec3.create(), camera.vInverseMatrix);
+	vec3.sub(camera.relativePosition, camera.position, view.position);
+	vec3.normalize(camera.direction, camera.relativePosition);
+	
+	camera.ready = true;
 }//}}}
 function drawSceneGraphNode(canvas, node) { //{{{
@@ -718,10 +505,9 @@
 	mat4.multiply(mvpMatrix, canvas.camera.vpMatrix, node.modelMatrix);
 	
-	var mvMatrix = mat4.create();
-	mat4.multiply(mvMatrix, canvas.camera.vMatrix, node.modelMatrix);
-	
-	var normalMatrix = mat4.create();
-	mat4.invert(normalMatrix, node.modelMatrix);
-	mat4.transpose(normalMatrix, normalMatrix);
+	var normalMatrix = mat3.create();
+	var tempMatrix = mat4.create();
+	mat4.invert(tempMatrix, node.modelMatrix);
+	mat4.transpose(tempMatrix, tempMatrix);
+	mat3.fromMat4(normalMatrix, tempMatrix);
 	
 	if (node.texture) { node.texture.bind(0); }
@@ -732,81 +518,56 @@
 	gl.lineWidth(node.lineWidth);
 	gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
-
+	
 	//Setup for light that originates from camera
-	var origin = vec3.fromValues(0, 0, 0);
-	var lightOrigin = vec3.fromValues(0, 0, 0);
-	var cameraPositionRelative = vec3.create();
-	vec3.transformMat4(origin, origin, canvas.camera.vInverseMatrix);
-	vec3.normalize(lightOrigin, lightOrigin);
-	vec3.sub(cameraPositionRelative, origin, node.translation);
-	cameraHeight = vec3.length(cameraPositionRelative);
-	
-	var atm = { 					//Default Values
-		wavelength_r: 0.65, 		//0.65		Red wavelength (micrometers)
-		wavelength_g: 0.57,			//0.57		Green wavelength (micrometers)
-		wavelength_b: 0.475,		//0.475		Green wavelength (micrometers)
-		eSun: 100.0,				//20.0		Sun intensity	
-		kRayleigh: 0.0025,			//0.0025	Rayleigh scattering amount
-		kMie: 0.000, 				//0.01		Mie scattering amount
-		g: -0.99,					//-0.99		Mie phase asymmetry/direction factor
-		hdr_exposure: 0.8,			//0.8		High Dynamic Range Exposure
-		scale: 1.25, 				//1.025		Scale of atmosphere. WARNING: Change atmosphereScale in applyoptions.js, and scaling constants.
-		scaleDepth: 0.25, 			//0.25		Percentage altitude at which the atmosphere's average density is found
-		a: -0.00287,				//-0.00287	Scaling constant a
-		b: 0.459,					//0.459		Scaling constant b
-		c: 3.83,					//3.83		Scaling constant c
-		d: -6.80,					//-6.80		Scaling constant d
-		e: 3.6,						//5.25		Scaling constant e. Lower when increasing atmosphere scale.
-		attenuation: 0.5			//0.5		Strength of atmospheric scattering on ground shading.
-	};
-			
-	var inv_wavelength4 = [1.0 / Math.pow(atm.wavelength_r, 4), 1.0 / Math.pow(atm.wavelength_g, 4), 1.0 / Math.pow(atm.wavelength_b, 4)];
-	var innerRadius = 6.371e6;
-	var outerRadius = innerRadius*atm.scale;
-	var scale = 1.0 / (outerRadius - innerRadius);
-	var scaleDepth = atm.scaleDepth;
+	var atm = canvas.atmosphere;
+	var lightOrigin = vec3.create();
 	
 	node.shader.uniforms({
-		m4MVP: mvpMatrix,
-		m4Normal: normalMatrix,
-		m4Model: node.modelMatrix,
-		u_lightPosition: lightOrigin,
-		//u_lightPosition: [1.0, 1.0, 1.0],
-		u_diffuseColor: node.diffuseColor,
-		u_texture: 0,
-		u_alpha: node.alpha,
-		u_maskZerosColor: node.maskZerosColor,
-		u_maskZerosEnabled: node.maskZerosEnabled,
-		u_maskZerosTolerance: node.maskZerosTolerance,
-		u_maskZerosZeroValue: node.maskZerosZeroValue,
-		u_maskEnabled: node.maskEnabled,
-		u_maskHeight: node.maskHeight,
-		u_maskColor: node.maskColor,
-		u_pointSize: node.pointSize,
-		v3CameraPosition: origin,
-		v3Translate: node.translation,
-		v3LightPos: lightOrigin,
-		v3InvWavelength: inv_wavelength4,
-		fOuterRadius: outerRadius,
-		fOuterRadius2: outerRadius * outerRadius,
-		fInnerRadius: innerRadius,
-		fInnerRadius2: innerRadius * innerRadius,
-		fKrESun: atm.kRayleigh * atm.eSun, 
-		fKmESun: atm.kMie * atm.eSun, 
-		fKr4PI: atm.kRayleigh * 4 * Math.PI, 
-		fKm4PI: atm.kMie * 4 * Math.PI,
-		fScale: scale, 
-		fScaleDepth: scaleDepth,
-		fScaleOverScaleDepth: scale/scaleDepth, 
-		v3LightPosFrag: lightOrigin,
-		fHdrExposure: atm.hdr_exposure,	
-		g: atm.g,			
-		g2: atm.g * atm.g,
-		a: atm.a,
-		b: atm.b,
-		c: atm.c,
-		d: atm.d,		
-		e: atm.e,
-		attenuation: atm.attenuation
+		m4MVP: 					mvpMatrix,
+		m3Normal: 				normalMatrix,
+		m4Model: 				node.modelMatrix,
+		u_alpha: 				node.alpha,
+		u_ambientColor: 		node.ambientColor,
+		u_cameraPosition: 		canvas.camera.position,
+		u_diffuseColor: 		node.diffuseColor,
+		u_lightDirection: 		canvas.camera.direction,
+		u_lightingBias: 		node.lightingBias,
+		u_maskZerosColor: 		node.maskZerosColor,
+		u_maskZerosEnabled: 	node.maskZerosEnabled,
+		u_maskZerosTolerance: 	node.maskZerosTolerance,
+		u_maskZerosZeroValue: 	node.maskZerosZeroValue,
+		u_maskEnabled: 			node.maskEnabled,
+		u_maskHeight: 			node.maskHeight,
+		u_maskColor: 			node.maskColor,
+		u_pointSize: 			node.pointSize,
+		u_specularColor: 		node.specularColor,
+		u_specularPower: 		node.specularPower,
+		u_specularStrength: 	node.specularStrength,
+		u_texture: 				0,
+		v3CameraPosition: 		canvas.camera.position,
+		v3Translate: 			node.translation,
+		v3LightPos: 			lightOrigin,
+		v3InvWavelength: 		atm.inv_wavelength4,
+		fOuterRadius: 			atm.outerRadius,
+		fOuterRadius2: 			atm.outerRadius2,
+		fInnerRadius: 			atm.innerRadius,
+		fInnerRadius2: 			atm.innerRadius2,
+		fKrESun: 				atm.krESun,
+		fKmESun: 				atm.kmESun,
+		fKr4PI: 				atm.kr4PI,
+		fKm4PI: 				atm.km4PI,
+		fScale: 				atm.scale, 
+		fScaleDepth: 			atm.scaleDepth,
+		fScaleOverScaleDepth: 	atm.scaleOverScaleDepth, 
+		v3LightPosFrag: 		lightOrigin,
+		fHdrExposure: 			atm.hdr_exposure,	
+		g: 						atm.g,			
+		g2: 					atm.g2,
+		a: 						atm.a,
+		b: 						atm.b,
+		c: 						atm.c,
+		d: 						atm.d,		
+		e: 						atm.e,
+		attenuation: 			atm.attenuation
 	}).draw(node.mesh, node.drawMode, 'triangles');
 	
@@ -814,4 +575,16 @@
 	gl.disable(gl.CULL_FACE);
 } //}}}
+function canvasResize(canvas) {
+	var rect = canvas.getBoundingClientRect();
+	canvas.width  = rect.width;
+	canvas.height = rect.height;
+	canvas.gl.viewport(0, 0, canvas.width, canvas.height);
+	
+	if (!isEmptyOrUndefined(canvas.overlaycanvas)) {
+		rect = canvas.overlaycanvas.getBoundingClientRect();
+		canvas.overlaycanvas.width  = rect.width;
+		canvas.overlaycanvas.height = rect.height;
+	}
+}
 function draw(canvas) { //{{{
 	//Ensure all nodes are ready to render
@@ -833,11 +606,9 @@
 	//Begin rendering nodes
 	if (canvas.draw.ready) {
-		var rect = canvas.getBoundingClientRect();
-		canvas.width  = rect.width;
-		canvas.height = rect.height;
+		//Handle canvas resizing and viewport/screenspace coordinate synchronization
+		canvasResize(canvas);	
 		
 		var gl = canvas.gl;
 		gl.makeCurrent(); //litegl function to handle switching between multiple canvases
-		gl.viewport(0, 0, canvas.width, canvas.height);
 		gl.clearColor(canvas.backgroundcolor[0], canvas.backgroundcolor[1], canvas.backgroundcolor[2], canvas.backgroundcolor[3]);
 		gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
@@ -845,6 +616,8 @@
 		updateCameraMatrix(canvas);
 		
-		if (canvas.textcanvas) { canvas.textcanvas.draw(canvas); }
-		if (canvas.overlaycanvas) { canvas.overlaycanvas.draw(canvas); }
+		//Trigger any handlers attatched to this canvas event.
+		canvas.selector.trigger('onPreRender', [canvas]);
+		
+		for (var handler in canvas.overlayHandlers) { canvas.overlayHandlers[handler](canvas); }
 		
 		var drawPassNumber = 3;
