From 753d44cb4cb68a41f92dcaa83e0acda6c7698250 Mon Sep 17 00:00:00 2001
From: Seiji Emery <seijiemery@gmail.com>
Date: Tue, 25 Aug 2015 12:53:46 -0700
Subject: [PATCH 01/21] platform.js

---
 examples/example/entities/platform.js | 1057 +++++++++++++++++++++++++
 1 file changed, 1057 insertions(+)
 create mode 100644 examples/example/entities/platform.js

diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js
new file mode 100644
index 0000000000..4f20614f65
--- /dev/null
+++ b/examples/example/entities/platform.js
@@ -0,0 +1,1057 @@
+//
+// platform.js
+//
+// Created by Seiji Emery on 8/19/15
+// Copyright 2015 High Fidelity, Inc.
+//
+// Entity stress test / procedural demo.
+// Spawns a platform under your avatar made up of random cubes or spheres.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+var SCRIPT_NAME = "platform.js";
+
+Script.include("../../libraries/uiwidgets.js");
+// Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js');
+
+// Platform script
+(function () {
+	
+var SCRIPT_NAME = "platform.js";
+var USE_DEBUG_LOG = false;
+var LOG_ENTITY_CREATION_MESSAGES = false;
+var LOG_UPDATE_STATUS_MESSAGES = false;
+
+var MAX_UPDATE_INTERVAL = 0.2;	// restrict to 5 updates / sec
+
+var AVATAR_HEIGHT_OFFSET = 1.5;
+
+// Initial state
+var NUM_PLATFORM_ENTITIES = 400;
+var RADIUS = 5.0;
+
+// Limits for the user controls on platform radius, density, and dimensions (width, height, depth).
+// Color limits are hardcoded at [0, 255].
+var UI_RADIUS_RANGE = [ 1.0, 15.0 ];
+var UI_DENSITY_RANGE = [ 0.0, 35.0 ];	// do NOT increase this above 40! (actually, just don't go above 20k entities... >.>)
+var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ];
+
+// Used to detect teleportation. If user moves faster than this over one time step (times dt),
+// then we trigger a complete rebuild at their new height.
+var MAX_ACCELERATION_THRESHOLD = 20.0;
+
+// Utils
+(function () {
+	if (typeof(Math.randRange) === 'undefined') {
+		Math.randRange = function (min, max) {
+			return Math.random() * (max - min) + min;
+		}
+	}
+	if (typeof(Math.randInt) === 'undefined') {
+		Math.randInt = function (n) {
+			return Math.floor(Math.random() * n) | 0;
+		}
+	}
+	function fromComponents (r, g, b, a) {
+		this.red   = r;
+		this.green = g;
+		this.blue  = b;
+		this.alpha = a || 1.0;
+	}
+	function fromHex (c) {
+		this.red   = parseInt(c[1] + c[2], 16);
+		this.green = parseInt(c[3] + c[4], 16);
+		this.blue  = parseInt(c[5] + c[6], 16);
+	}
+	var Color = this.Color = function () {
+		if (arguments.length >= 3) {
+			fromComponents.apply(this, arguments);
+		} else if (arguments.length == 1 && arguments[0].length == 7 && arguments[0][0] == '#') {
+			fromHex.apply(this, arguments);
+		} else {
+			throw new Error("Invalid arguments to new Color(): " + JSON.stringify(arguments));
+		}
+	}
+	Color.prototype.toString = function () {
+		return "[Color: " + JSON.stringify(this) + "]";
+	}
+})();
+
+// RNG models
+(function () {
+	/// Encapsulates a simple color model that generates colors using a linear, pseudo-random color distribution.
+	var RandomColorModel = this.RandomColorModel = function () {
+		this.shadeRange = 0;	// = 200;
+		this.minColor = 55;		// = 100;
+		this.redRange = 255;	// = 200;
+		this.greenRange = 0; 	// = 10;
+		this.blueRange = 0;		// = 0;
+	};
+	/// Generates 4 numbers in [0, 1] corresponding to each color attribute (uniform shade and additive red, green, blue).
+	/// This is done in a separate step from actually generating the colors, since it allows us to either A) completely
+	/// rebuild / re-randomize the color values, or B) reuse the RNG values but with different color parameters, which
+	/// enables us to do realtime color editing on the same visuals (awesome!).
+	RandomColorModel.prototype.generateSeed = function () {
+		return [ Math.random(), Math.random(), Math.random(), Math.random() ];
+	};
+	/// Takes a random 'seed' (4 floats from this.generateSeed()) and calculates a pseudo-random
+	/// color by combining that with the color model's current parameters.
+	RandomColorModel.prototype.getRandom = function (r) {
+		// logMessage("color seed values " + JSON.stringify(r));
+		var shade = Math.min(255, this.minColor + r[0] * this.shadeRange);
+
+		// No clamping on the color components, so they may overflow. 
+		// However, this creates some pretty interesting visuals, so we're not "fixing" this.
+		var color = {
+			red:   shade + r[1] * this.redRange,
+			green: shade + r[2] * this.greenRange,
+			blue:  shade + r[3] * this.blueRange
+		};
+		// logMessage("this: " + JSON.stringify(this));
+		// logMessage("color: " + JSON.stringify(color), COLORS.RED);
+		return color;
+	};
+	/// Custom property iterator used to setup UI (sliders, etc)
+	RandomColorModel.prototype.setupUI = function (callback) {
+		var _this = this;
+		[ 
+			['shadeRange', 'shade range'], 
+			['minColor', 'shade min'], 
+			['redRange', 'red (additive)'],
+			['greenRange', 'green (additive)'], 
+			['blueRange', 'blue (additive)'] 
+		].forEach(function (v) {
+			//       name, value, min, max, onValueChanged
+			callback(v[1], _this[v[0]], 0, 255, function (value) { _this[v[0]] = value });
+		});
+	}
+
+	/// Generates pseudo-random dimensions for our cubes / shapes.
+	var RandomShapeModel = this.RandomShapeModel = function () {
+		this.widthRange = [ 0.3, 0.7 ];
+		this.depthRange = [ 0.5, 0.8 ];
+		this.heightRange = [ 0.01, 0.08 ];
+	};
+	/// Generates 3 seed numbers in [0, 1]
+	RandomShapeModel.prototype.generateSeed = function () {
+		return [ Math.random(), Math.random(), Math.random() ];
+	}
+	/// Combines seed values with width/height/depth ranges to produce vec3 dimensions for a cube / sphere.
+	RandomShapeModel.prototype.getRandom = function (r) {
+		return {
+			x: r[0] * (this.widthRange[1] - this.widthRange[0]) + this.widthRange[0],
+			y: r[1] * (this.heightRange[1] - this.heightRange[0]) + this.heightRange[0],
+			z: r[2] * (this.depthRange[1] - this.depthRange[0]) + this.depthRange[0]
+		};
+	}
+	/// Custom property iterator used to setup UI (sliders, etc)
+	RandomShapeModel.prototype.setupUI = function (callback) {
+		var _this = this;
+		var dimensionsMin = UI_SHAPE_DIMENSIONS_RANGE[0];
+		var dimensionsMax = UI_SHAPE_DIMENSIONS_RANGE[1];
+		[
+			['widthRange', 'width'], 
+			['depthRange', 'depth'], 
+			['heightRange', 'height'] 
+		].forEach(function (v) {
+			//       name, value, min, max, onValueChanged
+			callback(v[1], _this[v[0]], dimensionsMin, dimensionsMax, function (value) { _this[v[0]] = value });
+		});
+	}
+
+	/// Combines color + shape PRNG models and hides their implementation details.
+	var RandomAttribModel = this.RandomAttribModel = function () {
+		this.colorModel = new RandomColorModel();
+		this.shapeModel = new RandomShapeModel();
+	}
+	/// Completely re-randomizes obj's `color` and `dimensions` parameters based on the current model params.
+	RandomAttribModel.prototype.randomizeShapeAndColor = function (obj) {
+		// logMessage("randomizing " + JSON.stringify(obj));
+		obj._colorSeed = this.colorModel.generateSeed();
+		obj._shapeSeed = this.shapeModel.generateSeed();
+		this.updateShapeAndColor(obj);
+		// logMessage("color seed: " + JSON.stringify(obj._colorSeed), COLORS.RED);
+		// logMessage("randomized color: " + JSON.stringify(obj.color), COLORS.RED);
+		// logMessage("randomized: " + JSON.stringify(obj));
+		return obj;
+	}
+	/// Updates obj's `color` and `dimensions` params to use the current model params.
+	/// Reuses hidden seed attribs; _must_ have called randomizeShapeAndColor(obj) at some point before
+	/// calling this.
+	RandomAttribModel.prototype.updateShapeAndColor = function (obj) {
+		try {
+			// logMessage("update shape and color: " + this.colorModel);
+			obj.color      = this.colorModel.getRandom(obj._colorSeed);
+			obj.dimensions = this.shapeModel.getRandom(obj._shapeSeed); 
+		} catch (e) {
+			logMessage("update shape / color failed", COLORS.RED);
+			logMessage('' + e, COLORS.RED);
+			logMessage("obj._colorSeed = " + JSON.stringify(obj._colorSeed));
+			logMessage("obj._shapeSeed = " + JSON.stringify(obj._shapeSeed));
+			// logMessage("obj = " + JSON.stringify(obj));
+			throw e;
+		}
+		return obj;
+	}
+})();
+
+// Status / logging UI (ignore this)
+(function () {
+	var COLORS = this.COLORS = {
+		'GREEN': new Color("#2D870C"),
+		'RED': new Color("#AF1E07"),
+		'LIGHT_GRAY': new Color("#CCCCCC"),
+		'DARK_GRAY': new Color("#4E4E4E")
+	};
+	function buildDebugLog () {
+		var LINE_WIDTH = 400;
+		var LINE_HEIGHT = 20;
+	
+		var lines = [];
+		var lineIndex = 0;
+		for (var i = 0; i < 20; ++i) {
+			lines.push(new UI.Label({ 
+				text: " ", visible: false, 
+				width: LINE_WIDTH, height: LINE_HEIGHT,
+		 	}));
+		}
+		var title = new UI.Label({
+			text: SCRIPT_NAME, visible: true,
+			width: LINE_WIDTH, height: LINE_HEIGHT,
+		});
+	
+		var overlay = new UI.Box({
+			visible: true,
+			width: LINE_WIDTH, height: 0,
+			backgroundColor: COLORS.DARK_GRAY,
+			backgroundAlpha: 0.3
+		});
+		overlay.setPosition(280, 10);
+		relayoutFrom(0);
+		UI.updateLayout();
+	
+		function relayoutFrom (n) {
+			var layoutPos = {
+				x: overlay.position.x,
+				y: overlay.position.y
+			};
+	
+			title.setPosition(layoutPos.x, layoutPos.y);
+			layoutPos.y += LINE_HEIGHT;
+	
+			// for (var i = n; i >= 0; --i) {
+			for (var i = n + 1; i < lines.length; ++i) {
+				if (lines[i].visible) {
+					lines[i].setPosition(layoutPos.x, layoutPos.y);
+					layoutPos.y += LINE_HEIGHT;
+				}
+			}
+			// for (var i = lines.length - 1; i > n; --i) {
+			for (var i = 0; i <= n; ++i) {
+				if (lines[i].visible) {
+					lines[i].setPosition(layoutPos.x, layoutPos.y);
+					layoutPos.y += LINE_HEIGHT;
+				}
+			}
+			overlay.height = (layoutPos.y - overlay.position.y + 10);
+			overlay.getOverlay().update({
+				height: overlay.height
+			});
+		}
+		this.logMessage = function (text, color, alpha) {
+			lines[lineIndex].setVisible(true);
+			relayoutFrom(lineIndex);
+	
+			lines[lineIndex].getOverlay().update({
+				text: text,
+				visible: true,
+				color: color || COLORS.LIGHT_GRAY,
+				alpha: alpha !== undefined ? alpha : 1.0,
+				x: lines[lineIndex].position.x,
+				y: lines[lineIndex].position.y
+			});
+			lineIndex = (lineIndex + 1) % lines.length;
+			UI.updateLayout();
+		}
+	}
+	if (USE_DEBUG_LOG) {
+		buildDebugLog();
+	} else {
+		this.logMessage = function (msg) {
+			print(SCRIPT_NAME + ": " + msg);
+		}
+	}
+})();
+
+// Utils (ignore)
+(function () {
+	// Utility function
+	this.withDefaults = function (properties, defaults) {
+		// logMessage("withDefaults: " + JSON.stringify(properties) + JSON.stringify(defaults));
+		properties = properties || {};
+		if (defaults) {
+			for (var k in defaults) {
+				properties[k] = defaults[k];
+			}
+		}
+		return properties;
+	}
+
+	// Math utils
+	if (typeof(Math.randRange) === 'undefined') {
+		Math.randRange = function (min, max) {
+			return Math.random() * (max - min) + min;
+		}
+	}
+	if (typeof(Math.randInt) === 'undefined') {
+		Math.randInt = function (n) {
+			return Math.floor(Math.random() * n) | 0;
+		}
+	}
+
+	/// Random distrib: Get a random point within a circle on the xz plane with radius r, center p.
+	this.randomCirclePoint = function (r, pos) {
+		var a = Math.random(), b = Math.random();
+		if (b < a) {
+			var tmp = b;
+			b = a;
+			a = tmp;
+		}
+		var point = {
+			x: pos.x + b * r * Math.cos(2 * Math.PI * a / b),
+			y: pos.y,
+			z: pos.z + b * r * Math.sin(2 * Math.PI * a / b)
+		};
+		if (LOG_ENTITY_CREATION_MESSAGES) {
+			// logMessage("input params: " + JSON.stringify({ radius: r, position: pos }), COLORS.GREEN);
+			// logMessage("a = " + a + ", b = " + b);
+			logMessage("generated point: " + JSON.stringify(point), COLORS.RED);
+		}
+		return point;
+	}
+
+	// Entity utils. NOT using overlayManager for... reasons >.>
+	var makeEntity = this.makeEntity = function (properties) {
+		if (LOG_ENTITY_CREATION_MESSAGES) {
+			logMessage("Creating entity: " + JSON.stringify(properties));
+		}
+		var entity = Entities.addEntity(properties);
+		return {
+			update: function (properties) {
+				Entities.editEntity(entity, properties);
+			},
+			destroy: function () {
+				Entities.deleteEntity(entity)
+			}
+		};
+	}
+	// this.makeLight = function (properties) {
+	// 	return makeEntity(withDefaults(properties, {
+	// 		type: "Light",
+	// 		isSpotlight: false,
+	// 		diffuseColor: { red: 255, green: 100, blue: 100 },
+	// 		ambientColor: { red: 200, green: 80, blue: 80 }
+	// 	}));
+	// }
+	this.makeBox = function (properties) {
+		// logMessage("Creating box: " + JSON.stringify(properties));
+		return makeEntity(withDefaults(properties, {
+			type: "Box"
+		}));
+	}
+})();
+
+// Platform
+(function () {
+	/// Encapsulates a platform 'piece'. Owns an entity (`box`), and handles destruction and some other state.
+	var PlatformComponent = this.PlatformComponent = function (properties) {
+		// logMessage("Platform component initialized with " + Object.keys(properties), COLORS.GREEN);
+		this.position 	= properties.position || null;
+		this.color 		= properties.color    || null;
+		this.dimensions = properties.dimensions || null;
+
+		if (properties._colorSeed)
+			this._colorSeed = properties._colorSeed;
+		if (properties._shapeSeed)
+			this._shapeSeed = properties._shapeSeed;
+
+		// logMessage("dimensions: " + JSON.stringify(this.dimensions));
+		// logMessage("color: " + JSON.stringify(this.color));
+		this.box = makeBox({
+			dimensions: this.dimensions,
+			color: this.color,
+			position: this.position,
+			alpha: 0.5
+		});
+	};
+	/// Updates platform to be at position p, and calls <entity>.update() with the current
+	/// position, color, and dimensions parameters.
+	PlatformComponent.prototype.update = function (position) {
+		if (position)
+			this.position = position;
+		// logMessage("updating with " + JSON.stringify(this));
+		this.box.update(this);
+	}
+	function swap (a, b) {
+		var tmp = a;
+		a = b;
+		b = tmp;
+	}
+	/// Swap state with another component
+	PlatformComponent.prototype.swap = function (other) {
+		swap(this.position, other.position);
+		swap(this.dimensions, other.dimensions);
+		swap(this.color, other.color);
+	}
+	PlatformComponent.prototype.destroy = function () {
+		if (this.box) {
+			this.box.destroy();
+			this.box = null;
+		}
+	}
+
+	// util
+	function inRange (p1, p2, radius) {
+		return Vec3.distance(p1, p2) < Math.abs(radius);
+	}
+
+	/// Encapsulates a moving platform that follows the avatar around (mostly).
+	/// Owns a _large_ amount of entities, 
+	var DynamicPlatform = this.DynamicPlatform = function (n, position, radius) {
+		this.position = position;
+		this.radius   = radius;
+		this.randomizer = new RandomAttribModel();
+
+		var boxes = this.boxes = [];
+		// logMessage("Spawning " + n + " entities", COLORS.GREEN);
+		while (n > 0) {
+			var properties = { position: this.randomPoint() };
+			this.randomizer.randomizeShapeAndColor(properties);
+			// logMessage("properties: " + JSON.stringify(properties));
+			boxes.push(new PlatformComponent(properties));
+			--n;
+		}
+		this.targetDensity = this.getEntityDensity();
+		this.pendingUpdates = {};
+		this.updateTimer = 0.0;
+
+		this.platformHeight = position.y;
+		this.oldPos    = { x: position.x, y: position.y, z: position.z };
+		this.oldRadius = radius;
+	}
+	DynamicPlatform.prototype.toString = function () {
+		return "[DynamicPlatform (" + this.boxes.length + " entities)]";
+	}
+	DynamicPlatform.prototype.updateEntityAttribs = function () {
+		var _this = this;
+		this.setPendingUpdate('updateEntityAttribs', function () {
+			// logMessage("updating model", COLORS.GREEN);
+			_this.boxes.forEach(function (box) {
+				this.randomizer.updateShapeAndColor(box);
+				box.update();
+			}, _this);
+		});
+	}
+
+	/// Queue impl that uses the update loop to limit potentially expensive updates to only execute every x seconds (default: 200 ms).
+	/// This is to prevent UI code from running full entity updates every 10 ms (or whatever).
+	DynamicPlatform.prototype.setPendingUpdate = function (name, callback) {
+		if (!this.pendingUpdates[name]) {
+			this.pendingUpdates[name] = {
+				callback: callback,
+				timer: 0.0,
+				skippedUpdates: 0
+			}
+		} else {
+			this.pendingUpdates[name].callback = callback;
+			this.pendingUpdates[name].skippedUpdates++;
+			// logMessage("scheduling update for \"" + name + "\" to run in " + this.pendingUpdates[name].timer + " seconds");
+		}
+	}
+	/// Runs all queued updates as soon as they can execute (each one has a cooldown timer).
+	DynamicPlatform.prototype.processPendingUpdates = function (dt) {
+		for (var k in this.pendingUpdates) {
+			if (this.pendingUpdates[k].timer >= 0.0)
+				this.pendingUpdates[k].timer -= dt;
+
+			if (this.pendingUpdates[k].callback && this.pendingUpdates[k].timer < 0.0) {
+				// logMessage("Running update for \"" + k + "\" (skipped " + this.pendingUpdates[k].skippedUpdates + ")");
+				try {
+					this.pendingUpdates[k].callback();
+				} catch (e) {
+					logMessage("update for \"" + k + "\" failed: " + e, COLORS.RED);
+				}
+				this.pendingUpdates[k].timer = MAX_UPDATE_INTERVAL;
+				this.pendingUpdates[k].skippedUpdates = 0;
+				this.pendingUpdates[k].callback = null;
+			}
+		}
+	}
+
+	/// Updates the platform based on the avatar's current position (spawning / despawning entities as needed),
+	/// and calls processPendingUpdates() once this is done.
+	/// Does NOT have any update interval limits (it just updates every time it gets run), but these are not full
+	/// updates (they're incremental), so the network will not get flooded so long as the avatar is moving at a
+	/// normal walking / flying speed.
+	DynamicPlatform.prototype.update = function (dt, position) {
+		// logMessage("updating " + this);
+		position.y = this.platformHeight;
+		this.position = position;
+
+		var toUpdate = [];
+		this.boxes.forEach(function (box, i) {
+			// if (Math.abs(box.position.y - position.y) > HEIGHT_TOLERANCE || !inRange(box, position, radius)) {
+			if (!inRange(box.position, this.position, this.radius)) {
+				toUpdate.push(i);
+			}
+		}, this);
+
+		var MAX_TRIES = toUpdate.length * 8;
+		var tries = MAX_TRIES;
+		var moved = 0;
+		var recalcs = 0;
+		toUpdate.forEach(function (index) {
+			if ((index % 2 == 0) || tries > 0) {
+				do {
+					var randomPoint = this.randomPoint(this.position, this.radius);
+					++recalcs
+				} while (--tries > 0 && inRange(randomPoint, this.oldPos, this.oldRadiuss));
+	
+				if (LOG_UPDATE_STATUS_MESSAGES && tries <= 0) {
+					logMessage("updatePlatform() gave up after " + MAX_TRIES + " iterations (" + moved + " / " + toUpdate.length + " successful updates)", COLORS.RED);
+					logMessage("old pos: " + JSON.stringify(this.oldPos) + ", old radius: " + this.oldRadius);
+					logMessage("new pos: " + JSON.stringify(this.position) + ", new radius: " + this.radius);
+				}
+			} else {
+				var randomPoint = this.randomPoint(position, this.radius);
+			}
+
+			this.randomizer.randomizeShapeAndColor(this.boxes[index]);
+			this.boxes[index].update(randomPoint);
+			// this.boxes[index].setValues({
+			// 	position: randomPoint,
+			// 	// dimensions: this.randomDimensions(),
+			// 	// color: this.randomColor()
+			// });
+			++moved;
+		}, this);
+		recalcs = recalcs - toUpdate.length;
+
+		this.oldPos    = position;
+		this.oldRadius = this.radius;
+		if (LOG_UPDATE_STATUS_MESSAGES && toUpdate.length > 0) {
+			logMessage("updated " + toUpdate.length + " entities w/ " + recalcs + " recalcs");
+		}
+
+		this.processPendingUpdates(dt);
+	}
+	DynamicPlatform.prototype.getEntityCount = function () {
+		return this.boxes.length;
+	}
+
+	/// Sets the entity count to n. Don't call this directly -- use setRadius / density instead.
+	DynamicPlatform.prototype.setEntityCount = function (n) {
+		if (n > this.boxes.length) {
+			// logMessage("Setting entity count to " + n + " (adding " + (n - this.boxes.length) + " entities)", COLORS.GREEN);
+
+			// Spawn new boxes
+			n = n - this.boxes.length;
+			for (; n > 0; --n) {
+				var properties = { position: this.randomPoint() };
+				this.randomizer.randomizeShapeAndColor(properties);
+				this.boxes.push(new PlatformComponent(properties));
+			}
+		} else if (n < this.boxes.length) {
+			// logMessage("Setting entity count to " + n + " (removing " + (this.boxes.length - n) + " entities)", COLORS.GREEN);
+
+			// Destroy random boxes (technically, the most recent ones, but it should be sorta random)
+			n = this.boxes.length - n;
+			for (; n > 0; --n) {
+				this.boxes.pop().destroy();
+			}
+		}
+	}
+	/// Calculate the entity density based on radial surface area.
+	DynamicPlatform.prototype.getEntityDensity = function () {
+		return (this.boxes.length * 1.0) / (Math.PI * this.radius * this.radius);
+	}
+	/// Queues a setDensity update. This is expensive, so we don't call it directly from UI.
+	DynamicPlatform.prototype.setDensityOnNextUpdate = function (density) {
+		var _this = this;
+		this.targetDensity = density;
+		this.setPendingUpdate('density', function () {
+			_this.updateEntityDensity(density);
+		});
+	}
+	DynamicPlatform.prototype.updateEntityDensity = function (density) {
+		this.setEntityCount(Math.floor(density * Math.PI * this.radius * this.radius));
+	}
+	DynamicPlatform.prototype.getRadius = function () {
+		return this.radius;
+	}
+	/// Queues a setRadius update. This is expensive, so we don't call it directly from UI.
+	DynamicPlatform.prototype.setRadiusOnNextUpdate = function (radius) {
+		var _this = this;
+		this.setPendingUpdate('radius', function () {
+			_this.setRadius(radius);
+		});
+	}
+	var DEBUG_RADIUS_RECALC = false;
+	DynamicPlatform.prototype.setRadius = function (radius) {
+		if (radius < this.radius) {	// Reduce case
+			// logMessage("Setting radius to " + radius + " (shrink by " + (this.radius - radius) + ")", COLORS.GREEN );
+			this.radius = radius;
+
+			// Remove all entities outside of current bounds. Requires swapping, since we want to maintain a contiguous array.
+			// Algorithm: two pointers at front and back. We traverse fwd and back, swapping elems so that all entities in bounds
+			// are at the front of the array, and all entities out of bounds are at the back. We then pop + destroy all entities
+			// at the back to reduce the entity count.
+			var count = this.boxes.length;
+			var toDelete = 0;
+			var swapList = [];
+			if (DEBUG_RADIUS_RECALC) {
+				logMessage("starting at i = 0, j = " + (count - 1));
+			}
+			for (var i = 0, j = count - 1; i < j; ) {
+				// Find first elem outside of bounds that we can move to the back
+				while (inRange(this.boxes[i].position, this.position, this.radius) && i < j) {
+					++i;
+				}
+				// Find first elem in bounds that we can move to the front
+				while (!inRange(this.boxes[j].position, this.position, this.radius) && i < j) {
+					--j; ++toDelete;
+				}
+				if (i < j) {
+					// swapList.push([i, j]);
+					if (DEBUG_RADIUS_RECALC) {
+						logMessage("swapping " + i + ", " + j);	
+					}
+					this.boxes[i].swap(this.boxes[j]);
+					++i, --j; ++toDelete;
+				} else {
+					if (DEBUG_RADIUS_RECALC) {
+						logMessage("terminated at i = " + i + ", j = " + j, COLORS.RED);
+					}
+				}
+			}
+			if (DEBUG_RADIUS_RECALC) {
+				logMessage("toDelete = " + toDelete, COLORS.RED);
+			}
+			// Sanity check
+			if (toDelete > this.boxes.length) {
+				logMessage("Error: toDelete " + toDelete + " > entity count " + this.boxes.length + " (setRadius algorithm)", COLORS.RED);
+				toDelete = this.boxes.length;
+			}
+			if (toDelete > 0) {
+				// logMessage("Deleting " + toDelete + " entities as part of radius resize", COLORS.GREEN);
+			}
+			// Delete cleared boxes
+			for (; toDelete > 0; --toDelete) {
+				this.boxes.pop().destroy();
+			}
+			// fix entity density (just in case -- we may have uneven entity distribution)
+			this.updateEntityDensity(this.targetDensity);
+		} else if (radius > this.radius) {
+			// Grow case (much simpler)
+			// logMessage("Setting radius to " + radius + " (grow by " + (radius - this.radius) + ")", COLORS.GREEN);
+
+			// Add entities based on entity density
+			// var density = this.getEntityDensity();
+			var density = this.targetDensity;
+			var oldArea = Math.PI * this.radius * this.radius;
+			var n = Math.floor(density * Math.PI * (radius * radius - this.radius * this.radius));
+
+			if (n > 0) {
+				// logMessage("Adding " + n + " entities", COLORS.GREEN);
+
+				// Add entities (we use a slightly different algorithm to place them in the area between two concentric circles.
+				// This is *slightly* less uniform (the reason we're not using this everywhere is entities would be tightly clustered
+				// at the platform center and become spread out as the radius increases), but the use-case here is just incremental 
+				// radius resizes and the user's not likely to notice the difference).
+				for (; n > 0; --n) {
+					var theta = Math.randRange(0.0, Math.PI * 2.0);
+					var r = Math.randRange(this.radius, radius);
+					// logMessage("theta = " + theta + ", r = " + r);
+					var pos = {
+						x: Math.cos(theta) * r + this.position.x,
+						y: this.position.y,
+						z: Math.sin(theta) * r + this.position.y
+					};
+
+					var properties = { position: pos };
+					this.randomizer.randomizeShapeAndColor(properties);
+					this.boxes.push(new PlatformComponent(properties));
+				}
+			}
+			this.radius = radius;
+		}
+	}
+	DynamicPlatform.prototype.updateHeight = function (height) {
+		this.platformHeight = height;
+
+		// Invalidate current boxes to trigger a rebuild
+		this.boxes.forEach(function (box) {
+			box.position.x += this.oldRadius * 100;
+		});
+		// this.update(dt, position, radius);
+	}
+	/// Gets a random point within the platform bounds.
+	/// Should maybe get moved to the RandomAttribModel (would be much cleaner), but this works for now.
+	DynamicPlatform.prototype.randomPoint = function (position, radius) {
+		position = position || this.position;
+		radius = radius !== undefined ? radius : this.radius;
+		return randomCirclePoint(radius, position);
+	}
+	/// Old. The RandomAttribModel replaces this and enables realtime editing of the *****_RANGE params.
+	// DynamicPlatform.prototype.randomDimensions = function () {
+	// 	return {
+	// 		x: Math.randRange(WIDTH_RANGE[0], WIDTH_RANGE[1]),
+	// 		y: Math.randRange(HEIGHT_RANGE[0], HEIGHT_RANGE[1]),
+	// 		z: Math.randRange(DEPTH_RANGE[0], DEPTH_RANGE[1])
+	// 	};
+	// }
+	// DynamicPlatform.prototype.randomColor = function () {
+	// 	var shade = Math.randRange(SHADE_RANGE[0], SHADE_RANGE[1]);
+	// 	// var h = HUE_RANGE;
+	// 	return {
+	// 		red: 	shade + Math.randRange(RED_RANGE[0], RED_RANGE[1]) | 0,
+	// 		green: 	shade + Math.randRange(GREEN_RANGE[0], GREEN_RANGE[1]) | 0,
+	// 		blue: 	shade + Math.randRange(BLUE_RANGE[0], BLUE_RANGE[1]) | 0
+	// 	}
+	// 	// return COLORS[Math.randInt(COLORS.length)]
+	// }
+
+	/// Cleanup.
+	DynamicPlatform.prototype.destroy = function () {
+		this.boxes.forEach(function (box) {
+			box.destroy();
+		});
+		this.boxes = [];
+	}
+})();
+
+// UI
+(function () {
+	var CATCH_SETUP_ERRORS = true;
+
+	// Util functions for setting up widgets (the widget library is intended to be used like this)
+	function makePanel (dir, properties) {
+		return new UI.WidgetStack(withDefaults(properties, {
+			dir: dir
+		}));
+	}
+	function addSpacing (parent, width, height) {
+		parent.add(new UI.Box({
+			backgroundAlpha: 0.0,
+			width: width, height: height
+		}));
+	}
+	function addLabel (parent, text) {
+		return parent.add(new UI.Label({
+			text: text,
+			width: 200,
+			height: 20
+		}));
+	}
+	function addSlider (parent, label, min, max, getValue, onValueChanged) {
+		try {
+			var layout = parent.add(new UI.WidgetStack({ dir: "+x" }));
+			var textLabel = layout.add(new UI.Label({
+				text: label,
+				width: 130,
+				height: 20
+			}));
+			var valueLabel = layout.add(new UI.Label({
+				text: "" + (+getValue().toFixed(1)),
+				width: 60,
+				height: 20
+			}));
+			var slider = layout.add(new UI.Slider({
+				value: getValue(), minValue: min, maxValue: max,
+				width: 300, height: 20,
+				slider: {
+					width: 30,
+					height: 18
+				},
+				onValueChanged: function (value) {
+					valueLabel.setText("" + (+value.toFixed(1)));
+					onValueChanged(value, slider);
+					UI.updateLayout();
+				}
+			}));
+			return slider;
+		} catch (e) {
+			logMessage("" + e, COLORS.RED);
+			logMessage("parent: " + parent, COLORS.RED);
+			logMessage("label:  " + label, COLORS.RED);
+			logMessage("min:    " + min, COLORS.RED);
+			logMessage("max:    " + max, COLORS.RED);
+			logMessage("getValue: " + getValue, COLORS.RED);
+			logMessage("onValueChanged: " + onValueChanged, COLORS.RED);
+			throw e;
+		}
+	}
+	function addPushButton (parent, label, onClicked, setActive) {
+		var button = parent.add(new UI.Box({
+			text: label,
+			width: 120,
+			height: 20
+		}));
+	}
+	function moveToBottomLeftScreenCorner (widget) {
+		var border = 5;
+		var pos = {
+			x: border,
+			y: Controller.getViewportDimensions().y - widget.getHeight() - border
+		};
+		if (widget.position.x != pos.x || widget.position.y != pos.y) {
+			widget.setPosition(pos.x, pos.y);
+			UI.updateLayout();
+		}
+	}
+	var _export = this;
+
+	/// Setup the UI. Creates a bunch of sliders for setting the platform radius, density, and entity color / shape properties.
+	/// The entityCount slider is readonly.
+	function _setupUI (platform) {
+		var layoutContainer = makePanel("+y", { visible: true });
+		// layoutContainer.setPosition(10, 280);
+		// makeDraggable(layoutContainer);
+		_export.onScreenResize = function () {
+			moveToBottomLeftScreenCorner(layoutContainer);
+		}
+		var topSection = layoutContainer.add(makePanel("+x")); addSpacing(layoutContainer, 1, 5);
+		var btmSection = layoutContainer.add(makePanel("+x"));
+
+		var controls = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1);
+		var buttons  = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1);
+
+		var colorControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1);
+		var shapeControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1);
+
+		// Top controls
+		addLabel(controls, "Platform (platform.js)");
+		controls.radiusSlider = addSlider(controls, "radius", UI_RADIUS_RANGE[0], UI_RADIUS_RANGE[1], function () { return platform.getRadius() },
+			function (value) { 
+				platform.setRadiusOnNextUpdate(value);
+				controls.entityCountSlider.setValue(platform.getEntityCount());
+			});
+		addSpacing(controls, 1, 2);
+		controls.densitySlider = addSlider(controls, "entity density", UI_DENSITY_RANGE[0], UI_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, 
+			function (value) {
+				platform.setDensityOnNextUpdate(value);
+				controls.entityCountSlider.setValue(platform.getEntityCount());
+			});
+		addSpacing(controls, 1, 2);
+
+		var minEntities = Math.PI * UI_RADIUS_RANGE[0] * UI_RADIUS_RANGE[0] * UI_DENSITY_RANGE[0];
+		var maxEntities = Math.PI * UI_RADIUS_RANGE[1] * UI_RADIUS_RANGE[1] * UI_DENSITY_RANGE[1];
+		controls.entityCountSlider = addSlider(controls, "entity count", minEntities, maxEntities, function () { return platform.getEntityCount() },
+			function (value) {});
+		controls.entityCountSlider.actions = {}; // hack: make this slider readonly (clears all attached actions)
+		controls.entityCountSlider.slider.actions = {};
+		
+		// Bottom controls
+
+		// Iterate over controls (making sliders) for the RNG shape / dimensions model
+		platform.randomizer.shapeModel.setupUI(function (name, value, min, max, setValue) {
+			// logMessage("platform.randomizer.shapeModel." + name + " = " + value);
+			var internal = {
+				avg: (value[0] + value[1]) * 0.5,
+				range: Math.abs(value[0] - value[1])
+			};
+			// logMessage(JSON.stringify(internal), COLORS.GREEN);
+			addSlider(shapeControls, name + ' avg', min, max, function () { return internal.avg; }, function (value) {
+				internal.avg = value;
+				setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]);
+				platform.updateEntityAttribs();
+			});
+			addSpacing(shapeControls, 1, 2);
+			addSlider(shapeControls, name + ' range', min, max, function () { return internal.range }, function (value) {
+				internal.range = value;
+				setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]);
+				platform.updateEntityAttribs();
+			});
+			addSpacing(shapeControls, 1, 2);
+		});
+		// Do the same for the color model
+		platform.randomizer.colorModel.setupUI(function (name, value, min, max, setValue) {
+			// logMessage("platform.randomizer.colorModel." + name + " = " + value);
+			addSlider(colorControls, name, min, max, function () { return value; }, function (value) {
+				setValue(value);
+				platform.updateEntityAttribs();
+			});
+			addSpacing(shapeControls, 1, 2);
+		});
+		
+		moveToBottomLeftScreenCorner(layoutContainer);
+		layoutContainer.setVisible(true);
+	}
+	this.setupUI = function (platform) {
+		if (CATCH_SETUP_ERRORS) {
+			try {
+				_setupUI(platform);
+			} catch (e) {
+				logMessage("Error setting up ui: " + e, COLORS.RED);
+			}
+		} else {
+			_setupUI(platform);
+		}
+	}
+})();
+
+// Error handling w/ explicit try / catch blocks. Good for catching unexpected errors with the onscreen debugLog; bad
+// for detailed debugging since you lose the file and line num even if the error gets rethrown.
+
+// Catch errors from init
+var CATCH_INIT_ERRORS = true;
+
+// Catch errors from everything (technically, Script and Controller signals that runs platform / ui code)
+var CATCH_ERRORS_FROM_EVENT_UPDATES = false;
+
+// Setup everything
+(function () {
+	var doLater = null;
+	if (CATCH_ERRORS_FROM_EVENT_UPDATES) {
+		function catchErrors (fcn) {
+			return function () {
+				try {
+					fcn.apply(this, arguments);
+				} catch (e) {
+					logMessage('' + e, COLORS.RED);
+					logMessage("while calling " + fcn);
+					logMessage("Called by: " + arguments.callee.caller);
+				}
+			}
+		}
+		// We need to do this after the functions are actually registered...
+		doLater = function () {
+			// Intercept errors from functions called by Script.update and Script.ScriptEnding.
+			[ 'teardown', 'startup', 'update', 'initPlatform', 'setupUI' ].forEach(function (fcn) {
+				this[fcn] = catchErrors(this[fcn]);
+			});
+		};
+		// These need to be wrapped first though:
+
+		// Intercept errors from UI functions called by Controller.****Event.
+		[ 'handleMousePress', 'handleMouseMove', 'handleMouseRelease' ].forEach(function (fcn) {
+			UI[fcn] = catchErrors(UI[fcn]);
+		});
+	}
+
+	function getTargetPlatformPosition () {
+		var pos = MyAvatar.position;
+		pos.y -= AVATAR_HEIGHT_OFFSET;
+		return pos;
+	}
+
+	var platform = this.platform = null;
+	var lastHeight = null;
+
+	this.initPlatform = function () {
+		platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS);
+		lastHeight = getTargetPlatformPosition().y;
+	}
+
+	// this.init = function () {
+	// 	function _init () {
+	
+	// 		platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS);
+	// 		lastHeight = getTargetPlatformPosition().y;
+	// 		// Script.update.connect(update);
+	// 		// setupUI(platform);	
+	// 	}
+	// 	if (CATCH_INIT_ERRORS) {
+	// 		try {
+	// 			_init();
+	// 		} catch (e) {
+	// 			logMessage("error while initializing: " + e, COLORS.RED);
+	// 		}
+	// 	} else {
+	// 		_init();
+	// 	}
+	// }
+	var lastDimensions = Controller.getViewportDimensions();
+	function checkScreenDimensions () {
+		var dimensions = Controller.getViewportDimensions();
+		if (dimensions.x != lastDimensions.x || dimensions.y != lastDimensions.y) {
+			onScreenResize(dimensions.x, dimensions.y);
+		}
+		lastDimensions = dimensions;
+	}
+
+	this.update = function (dt) {
+		try {
+			checkScreenDimensions();
+
+			var pos = getTargetPlatformPosition();
+			if (Math.abs(pos.y - lastHeight) * dt > MAX_ACCELERATION_THRESHOLD) {
+				// User likely teleported
+				logMessage("Height rebuild (" +
+					"(" + Math.abs(pos.y - lastHeight) + " * " + dt + " = " + (Math.abs(pos.y - lastHeight) * dt) + ")" +
+					" > " + MAX_ACCELERATION_THRESHOLD + ")");
+				platform.updateHeight(pos.y);
+			}
+			platform.update(dt, getTargetPlatformPosition(), platform.getRadius());
+			lastHeight = pos.y;
+		} catch (e) {
+			logMessage("" + e, COLORS.RED);
+		}
+	}
+	this.teardown = function () {
+		try {
+			platform.destroy();
+			UI.teardown();
+
+			Controller.mousePressEvent.disconnect(UI.handleMousePress);
+			Controller.mouseMoveEvent.disconnect(UI.handleMouseMove);
+			Controller.mouseReleaseEvent.disconnect(UI.handleMouseRelease);
+		} catch (e) {
+			logMessage("" + e, COLORS.RED);
+		}
+	}
+
+	if (doLater) {
+		doLater();
+	}
+	this.startup = function () {
+		if (Entities.canAdjustLocks() && Entities.canRez()) {
+			Script.update.disconnect(this.startup);
+
+			function init () {
+				logMessage("initializing...");
+
+				this.initPlatform();
+	
+				Script.update.connect(this.update);
+				Script.scriptEnding.connect(this.teardown);
+	
+				this.setupUI(platform);
+	
+				logMessage("finished initializing.", COLORS.GREEN);
+			}
+			if (CATCH_INIT_ERRORS) {
+				try {
+					init();
+				} catch (error) {
+					logMessage("" + error, COLORS.RED);
+				}
+			} else {
+				init();
+			}
+
+			Controller.mousePressEvent.connect(UI.handleMousePress);
+			Controller.mouseMoveEvent.connect(UI.handleMouseMove);
+			Controller.mouseReleaseEvent.connect(UI.handleMouseRelease);
+		}
+	}
+})();
+Script.update.connect(startup);
+
+})();
+
+
+
+

From d57c7de316d82967b079a79bf1273a603baba144 Mon Sep 17 00:00:00 2001
From: Seiji Emery <seijiemery@gmail.com>
Date: Thu, 27 Aug 2015 11:51:36 -0700
Subject: [PATCH 02/21] Added rebuild + toggle entity type

---
 examples/example/entities/platform.js | 258 +++++++++++++++++---------
 1 file changed, 171 insertions(+), 87 deletions(-)

diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js
index 4f20614f65..66438daf9e 100644
--- a/examples/example/entities/platform.js
+++ b/examples/example/entities/platform.js
@@ -5,42 +5,35 @@
 // Copyright 2015 High Fidelity, Inc.
 //
 // Entity stress test / procedural demo.
-// Spawns a platform under your avatar made up of random cubes or spheres.
+// Spawns a platform under your avatar made up of random cubes or spheres. The platform follows your avatar
+// around, and has a 
 //
 // Distributed under the Apache License, Version 2.0.
 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
 
-var SCRIPT_NAME = "platform.js";
-
 Script.include("../../libraries/uiwidgets.js");
-// Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js');
 
 // Platform script
 (function () {
-	
 var SCRIPT_NAME = "platform.js";
-var USE_DEBUG_LOG = false;
-var LOG_ENTITY_CREATION_MESSAGES = false;
-var LOG_UPDATE_STATUS_MESSAGES = false;
+var USE_DEBUG_LOG = true;	// Turns on the 2dOverlay-based debug log. If false, just redirects to print.
+var NUM_DEBUG_LOG_LINES = 10;
+var LOG_ENTITY_CREATION_MESSAGES = false;	// detailed debugging (init)
+var LOG_UPDATE_STATUS_MESSAGES = false;		// detailed debugging (startup)
 
 var MAX_UPDATE_INTERVAL = 0.2;	// restrict to 5 updates / sec
-
-var AVATAR_HEIGHT_OFFSET = 1.5;
+var AVATAR_HEIGHT_OFFSET = 1.5; // offset to make the platform spawn under your feet. Might need to be adjusted for unusually proportioned avatars.
 
 // Initial state
 var NUM_PLATFORM_ENTITIES = 400;
 var RADIUS = 5.0;
 
-// Limits for the user controls on platform radius, density, and dimensions (width, height, depth).
+// Defines min/max for onscreen platform radius, density, and entity width/height/depth sliders.
 // Color limits are hardcoded at [0, 255].
 var UI_RADIUS_RANGE = [ 1.0, 15.0 ];
-var UI_DENSITY_RANGE = [ 0.0, 35.0 ];	// do NOT increase this above 40! (actually, just don't go above 20k entities... >.>)
-var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ];
-
-// Used to detect teleportation. If user moves faster than this over one time step (times dt),
-// then we trigger a complete rebuild at their new height.
-var MAX_ACCELERATION_THRESHOLD = 20.0;
+var UI_DENSITY_RANGE = [ 0.0, 35.0 ];	// do NOT increase this above 40! (~20k limit). Entity count = Math.PI * radius * radius * density.
+var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension limits
 
 // Utils
 (function () {
@@ -211,7 +204,7 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 	
 		var lines = [];
 		var lineIndex = 0;
-		for (var i = 0; i < 20; ++i) {
+		for (var i = 0; i < NUM_DEBUG_LOG_LINES; ++i) {
 			lines.push(new UI.Label({ 
 				text: " ", visible: false, 
 				width: LINE_WIDTH, height: LINE_HEIGHT,
@@ -288,7 +281,7 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 // Utils (ignore)
 (function () {
 	// Utility function
-	this.withDefaults = function (properties, defaults) {
+	var withDefaults = this.withDefaults = function (properties, defaults) {
 		// logMessage("withDefaults: " + JSON.stringify(properties) + JSON.stringify(defaults));
 		properties = properties || {};
 		if (defaults) {
@@ -298,6 +291,13 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 		}
 		return properties;
 	}
+	var withReadonlyProp = this.withReadonlyProp = function (propname, value, obj) {
+		Object.defineProperty(obj, propname, {
+			value: value,
+			writable: false
+		});
+		return obj;
+	}
 
 	// Math utils
 	if (typeof(Math.randRange) === 'undefined') {
@@ -338,14 +338,14 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 			logMessage("Creating entity: " + JSON.stringify(properties));
 		}
 		var entity = Entities.addEntity(properties);
-		return {
+		return withReadonlyProp("type", properties.type, {
 			update: function (properties) {
 				Entities.editEntity(entity, properties);
 			},
 			destroy: function () {
 				Entities.deleteEntity(entity)
-			}
-		};
+			},
+		});
 	}
 	// this.makeLight = function (properties) {
 	// 	return makeEntity(withDefaults(properties, {
@@ -371,6 +371,9 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 		this.position 	= properties.position || null;
 		this.color 		= properties.color    || null;
 		this.dimensions = properties.dimensions || null;
+		this.entityType = properties.type || "Box";
+
+		// logMessage("Spawning with type: '" + this.entityType + "' (properties.type = '" + properties.type + "')", COLORS.GREEN);
 
 		if (properties._colorSeed)
 			this._colorSeed = properties._colorSeed;
@@ -379,10 +382,13 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 
 		// logMessage("dimensions: " + JSON.stringify(this.dimensions));
 		// logMessage("color: " + JSON.stringify(this.color));
-		this.box = makeBox({
+
+		this.cachedEntity = null;
+		this.activeEntity = makeEntity({
+			type: this.entityType,
+			position: this.position,	
 			dimensions: this.dimensions,
 			color: this.color,
-			position: this.position,
 			alpha: 0.5
 		});
 	};
@@ -392,23 +398,65 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 		if (position)
 			this.position = position;
 		// logMessage("updating with " + JSON.stringify(this));
-		this.box.update(this);
+		this.activeEntity.update(this);
 	}
 	function swap (a, b) {
 		var tmp = a;
 		a = b;
 		b = tmp;
 	}
+	PlatformComponent.prototype.swapEntityType = function (newType) {
+		if (this.entityType !== newType) {
+			this.entityType = newType;
+
+			// logMessage("Destroying active entity and rebuilding it (newtype = '" + newType + "')");
+
+			// if (this.activeEntity) {
+			// 	this.activeEntity.destroy();
+			// }
+			// this.activeEntity = makeEntity({
+			// 	type: this.entityType,
+			// 	position: this.position,
+			// 	dimensions: this.dimensions,
+			// 	color: this.color,
+			// 	alpha: 0.5
+			// });
+			if (this.cachedEntity && this.cachedEntity.type == newType) {
+				this.cachedEntity.update({ visible: true });
+				this.activeEntity.update({ visible: false });
+				swap(this.cachedEntity, this.activeEntity);
+				this.update(this.position);
+			} else {
+				this.activeEntity.update({ visible: false });
+				this.cachedEntity = this.activeEntity;
+				this.activeEntity = makeEntity({
+					type: newType,
+					dimensions: this.dimensions,
+					color: this.color,
+					position: this.position,
+					alpha: 0.5
+				});
+			}
+		}
+	}
 	/// Swap state with another component
 	PlatformComponent.prototype.swap = function (other) {
 		swap(this.position, other.position);
 		swap(this.dimensions, other.dimensions);
 		swap(this.color, other.color);
+		swap(this.entityType, other.entityType);
+		swap(this.activeEntity, other.activeEntity);
+		swap(this._colorSeed, other._colorSeed);
+		swap(this._shapeSeed, other._shapeSeed);
 	}
 	PlatformComponent.prototype.destroy = function () {
-		if (this.box) {
-			this.box.destroy();
-			this.box = null;
+		if (this.activeEntity) {
+			this.activeEntity.destroy();
+			this.activeEntity = null;
+		}
+		if (this.cachedEntity) {
+			this.cachedEntity.destroy();
+			this.cachedEntity = null;
 		}
 	}
 
@@ -423,14 +471,13 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 		this.position = position;
 		this.radius   = radius;
 		this.randomizer = new RandomAttribModel();
+		this.boxType = "Sphere";
+		this.boxTypes = [ "Box", "Sphere" ];
 
-		var boxes = this.boxes = [];
 		// logMessage("Spawning " + n + " entities", COLORS.GREEN);
+		var boxes = this.boxes = [];
 		while (n > 0) {
-			var properties = { position: this.randomPoint() };
-			this.randomizer.randomizeShapeAndColor(properties);
-			// logMessage("properties: " + JSON.stringify(properties));
-			boxes.push(new PlatformComponent(properties));
+			boxes.push(this.spawnEntity());
 			--n;
 		}
 		this.targetDensity = this.getEntityDensity();
@@ -444,6 +491,12 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 	DynamicPlatform.prototype.toString = function () {
 		return "[DynamicPlatform (" + this.boxes.length + " entities)]";
 	}
+	DynamicPlatform.prototype.spawnEntity = function () {
+		// logMessage("Called spawn entity. this.boxType = '" + this.boxType + "'")
+		var properties = { position: this.randomPoint(), type: this.boxType };
+		this.randomizer.randomizeShapeAndColor(properties);
+		return new PlatformComponent(properties);
+	}
 	DynamicPlatform.prototype.updateEntityAttribs = function () {
 		var _this = this;
 		this.setPendingUpdate('updateEntityAttribs', function () {
@@ -454,6 +507,26 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 			}, _this);
 		});
 	}
+	DynamicPlatform.prototype.toggleBoxType = function () {
+		var _this = this;
+		this.setPendingUpdate('toggleBoxType', function () {
+			// Swap / cycle through types: find index of current type and set next type to idx+1
+			for (var idx = 0; idx < _this.boxTypes.length; ++idx) {
+				if (_this.boxTypes[idx] === _this.boxType) {
+					var nextIndex = (idx + 1) % _this.boxTypes.length;
+					logMessage("swapping box type from '" + _this.boxType + "' to '" + _this.boxTypes[nextIndex] + "'", COLORS.GREEN);
+					_this.boxType = _this.boxTypes[nextIndex];
+					break;
+				}
+			}
+			_this.boxes.forEach(function (box) {
+				box.swapEntityType(_this.boxType);
+			}, _this);
+		});
+	}
+	DynamicPlatform.prototype.getBoxType = function () {
+		return this.boxType;
+	}
 
 	/// Queue impl that uses the update loop to limit potentially expensive updates to only execute every x seconds (default: 200 ms).
 	/// This is to prevent UI code from running full entity updates every 10 ms (or whatever).
@@ -550,6 +623,18 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 	DynamicPlatform.prototype.getEntityCount = function () {
 		return this.boxes.length;
 	}
+	DynamicPlatform.prototype.getEntityCountWithRadius = function (radius) {
+		var est = Math.floor((radius * radius) / (this.radius * this.radius) * this.getEntityCount());
+		var actual = Math.floor(Math.PI * radius * radius * this.getEntityDensity());
+
+		if (est != actual) {
+			logMessage("assert failed: getEntityCountWithRadius() -- est " + est + " != actual " + actual);
+		}
+		return est;
+	}
+	DynamicPlatform.prototype.getEntityCountWithDensity = function (density) {
+		return Math.floor(Math.PI * this.radius * this.radius * density);
+	}
 
 	/// Sets the entity count to n. Don't call this directly -- use setRadius / density instead.
 	DynamicPlatform.prototype.setEntityCount = function (n) {
@@ -559,9 +644,10 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 			// Spawn new boxes
 			n = n - this.boxes.length;
 			for (; n > 0; --n) {
-				var properties = { position: this.randomPoint() };
-				this.randomizer.randomizeShapeAndColor(properties);
-				this.boxes.push(new PlatformComponent(properties));
+				// var properties = { position: this.randomPoint() };
+				// this.randomizer.randomizeShapeAndColor(properties);
+				// this.boxes.push(new PlatformComponent(properties));
+				this.boxes.push(this.spawnEntity());
 			}
 		} else if (n < this.boxes.length) {
 			// logMessage("Setting entity count to " + n + " (removing " + (this.boxes.length - n) + " entities)", COLORS.GREEN);
@@ -680,15 +766,17 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 						z: Math.sin(theta) * r + this.position.y
 					};
 
-					var properties = { position: pos };
-					this.randomizer.randomizeShapeAndColor(properties);
-					this.boxes.push(new PlatformComponent(properties));
+					// var properties = { position: pos };
+					// this.randomizer.randomizeShapeAndColor(properties);
+					// this.boxes.push(new PlatformComponent(properties));
+					this.boxes.push(this.spawnEntity());
 				}
 			}
 			this.radius = radius;
 		}
 	}
 	DynamicPlatform.prototype.updateHeight = function (height) {
+		logMessage("Setting platform height to " + height);
 		this.platformHeight = height;
 
 		// Invalidate current boxes to trigger a rebuild
@@ -730,7 +818,7 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 		});
 		this.boxes = [];
 	}
-})();
+}).call(this);
 
 // UI
 (function () {
@@ -793,12 +881,16 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 			throw e;
 		}
 	}
-	function addPushButton (parent, label, onClicked, setActive) {
+	function addButton (parent, label, onClicked) {
 		var button = parent.add(new UI.Box({
 			text: label,
-			width: 120,
-			height: 20
+			width: 160,
+			height: 26,
+			leftMargin: 8,
+			topMargin: 3
 		}));
+		button.addAction('onClick', onClicked);
+		return button;
 	}
 	function moveToBottomLeftScreenCorner (widget) {
 		var border = 5;
@@ -836,13 +928,13 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 		controls.radiusSlider = addSlider(controls, "radius", UI_RADIUS_RANGE[0], UI_RADIUS_RANGE[1], function () { return platform.getRadius() },
 			function (value) { 
 				platform.setRadiusOnNextUpdate(value);
-				controls.entityCountSlider.setValue(platform.getEntityCount());
+				controls.entityCountSlider.setValue(platform.getEntityCountWithRadius(value));
 			});
 		addSpacing(controls, 1, 2);
 		controls.densitySlider = addSlider(controls, "entity density", UI_DENSITY_RANGE[0], UI_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, 
 			function (value) {
 				platform.setDensityOnNextUpdate(value);
-				controls.entityCountSlider.setValue(platform.getEntityCount());
+				controls.entityCountSlider.setValue(platform.getEntityCountWithDensity(value));
 			});
 		addSpacing(controls, 1, 2);
 
@@ -852,6 +944,16 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 			function (value) {});
 		controls.entityCountSlider.actions = {}; // hack: make this slider readonly (clears all attached actions)
 		controls.entityCountSlider.slider.actions = {};
+
+		// Buttons
+		addSpacing(buttons, 1, 22);
+		addButton(buttons, 'rebuild', function () {
+			platform.updateHeight(MyAvatar.position.y - AVATAR_HEIGHT_OFFSET);
+		});
+		addSpacing(buttons, 1, 2);
+		addButton(buttons, 'toggle entity type', function () {
+			platform.toggleBoxType();
+		});
 		
 		// Bottom controls
 
@@ -902,8 +1004,8 @@ var MAX_ACCELERATION_THRESHOLD = 20.0;
 	}
 })();
 
-// Error handling w/ explicit try / catch blocks. Good for catching unexpected errors with the onscreen debugLog; bad
-// for detailed debugging since you lose the file and line num even if the error gets rethrown.
+// Error handling w/ explicit try / catch blocks. Good for catching unexpected errors with the onscreen debugLog 
+// (if it's enabled); bad for detailed debugging since you lose the file and line num even if the error gets rethrown.
 
 // Catch errors from init
 var CATCH_INIT_ERRORS = true;
@@ -915,6 +1017,7 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false;
 (function () {
 	var doLater = null;
 	if (CATCH_ERRORS_FROM_EVENT_UPDATES) {
+		// Decorates a function w/ explicit error catching + printing to the debug log.
 		function catchErrors (fcn) {
 			return function () {
 				try {
@@ -926,7 +1029,7 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false;
 				}
 			}
 		}
-		// We need to do this after the functions are actually registered...
+		// We need to do this after the functions are registered...
 		doLater = function () {
 			// Intercept errors from functions called by Script.update and Script.ScriptEnding.
 			[ 'teardown', 'startup', 'update', 'initPlatform', 'setupUI' ].forEach(function (fcn) {
@@ -947,32 +1050,17 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false;
 		return pos;
 	}
 
+	// Program state
 	var platform = this.platform = null;
 	var lastHeight = null;
 
+	// Init
 	this.initPlatform = function () {
 		platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS);
 		lastHeight = getTargetPlatformPosition().y;
 	}
 
-	// this.init = function () {
-	// 	function _init () {
-	
-	// 		platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS);
-	// 		lastHeight = getTargetPlatformPosition().y;
-	// 		// Script.update.connect(update);
-	// 		// setupUI(platform);	
-	// 	}
-	// 	if (CATCH_INIT_ERRORS) {
-	// 		try {
-	// 			_init();
-	// 		} catch (e) {
-	// 			logMessage("error while initializing: " + e, COLORS.RED);
-	// 		}
-	// 	} else {
-	// 		_init();
-	// 	}
-	// }
+	// Handle relative screen positioning (UI)
 	var lastDimensions = Controller.getViewportDimensions();
 	function checkScreenDimensions () {
 		var dimensions = Controller.getViewportDimensions();
@@ -982,24 +1070,22 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false;
 		lastDimensions = dimensions;
 	}
 
+	// Update
 	this.update = function (dt) {
-		try {
-			checkScreenDimensions();
+		checkScreenDimensions();
 
-			var pos = getTargetPlatformPosition();
-			if (Math.abs(pos.y - lastHeight) * dt > MAX_ACCELERATION_THRESHOLD) {
-				// User likely teleported
-				logMessage("Height rebuild (" +
-					"(" + Math.abs(pos.y - lastHeight) + " * " + dt + " = " + (Math.abs(pos.y - lastHeight) * dt) + ")" +
-					" > " + MAX_ACCELERATION_THRESHOLD + ")");
-				platform.updateHeight(pos.y);
-			}
-			platform.update(dt, getTargetPlatformPosition(), platform.getRadius());
-			lastHeight = pos.y;
-		} catch (e) {
-			logMessage("" + e, COLORS.RED);
-		}
+		var pos = getTargetPlatformPosition();
+		// if (Math.abs(pos.y - lastHeight) * dt > MAX_ACCELERATION_THRESHOLD) {
+		// 	// User likely teleported
+		// 	logMessage("Height rebuild (" +
+		// 		"(" + Math.abs(pos.y - lastHeight) + " * " + dt + " = " + (Math.abs(pos.y - lastHeight) * dt) + ")" +
+		// 		" > " + MAX_ACCELERATION_THRESHOLD + ")");
+		// 	platform.updateHeight(pos.y);
+		// }
+		platform.update(dt, getTargetPlatformPosition(), platform.getRadius());
 	}
+
+	// Teardown
 	this.teardown = function () {
 		try {
 			platform.destroy();
@@ -1016,6 +1102,8 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false;
 	if (doLater) {
 		doLater();
 	}
+
+	// Delays startup until / if entities can be spawned.
 	this.startup = function () {
 		if (Entities.canAdjustLocks() && Entities.canRez()) {
 			Script.update.disconnect(this.startup);
@@ -1047,11 +1135,7 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false;
 			Controller.mouseReleaseEvent.connect(UI.handleMouseRelease);
 		}
 	}
-})();
-Script.update.connect(startup);
-
+	Script.update.connect(this.startup);
 })();
 
-
-
-
+})();

From 62a6a9d45efead58813935d7c955621353fd393e Mon Sep 17 00:00:00 2001
From: Seiji Emery <seijiemery@gmail.com>
Date: Thu, 27 Aug 2015 11:54:47 -0700
Subject: [PATCH 03/21] Description

---
 examples/example/entities/platform.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js
index 66438daf9e..ac41dc593b 100644
--- a/examples/example/entities/platform.js
+++ b/examples/example/entities/platform.js
@@ -5,8 +5,8 @@
 // Copyright 2015 High Fidelity, Inc.
 //
 // Entity stress test / procedural demo.
-// Spawns a platform under your avatar made up of random cubes or spheres. The platform follows your avatar
-// around, and has a 
+// Spawns a platform under your avatar made up of randomly sized and colored boxes or spheres. The platform follows your avatar
+// around, and comes with a UI to update the platform's properties (radius, entity density, color distribution, etc) in real time.
 //
 // Distributed under the Apache License, Version 2.0.
 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html

From fbf6d0efe8b21947307d1014595e142da52161c3 Mon Sep 17 00:00:00 2001
From: Seiji Emery <seijiemery@gmail.com>
Date: Thu, 27 Aug 2015 11:57:53 -0700
Subject: [PATCH 04/21] Added failsafe so dependencies don't break

---
 examples/example/entities/platform.js | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js
index ac41dc593b..9cd6687a10 100644
--- a/examples/example/entities/platform.js
+++ b/examples/example/entities/platform.js
@@ -12,7 +12,12 @@
 // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
 
+
+// UI and debug console implemented using uiwidgets / 2d overlays
 Script.include("../../libraries/uiwidgets.js");
+if (typeof('UI') === 'undefined') {	// backup link in case the user downloaded this somewhere
+	Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js');
+}
 
 // Platform script
 (function () {

From 6583ea179136416c6654e1b920673322cdb2b8e7 Mon Sep 17 00:00:00 2001
From: Seiji Emery <seijiemery@gmail.com>
Date: Thu, 27 Aug 2015 13:08:19 -0700
Subject: [PATCH 05/21] Reverted default entity type back to spheres

---
 examples/example/entities/platform.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js
index 9cd6687a10..0aeb80e98f 100644
--- a/examples/example/entities/platform.js
+++ b/examples/example/entities/platform.js
@@ -476,7 +476,7 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 		this.position = position;
 		this.radius   = radius;
 		this.randomizer = new RandomAttribModel();
-		this.boxType = "Sphere";
+		this.boxType = "Box";
 		this.boxTypes = [ "Box", "Sphere" ];
 
 		// logMessage("Spawning " + n + " entities", COLORS.GREEN);

From 2bea436e12a7a32282a693de412ae135c93ea58d Mon Sep 17 00:00:00 2001
From: Seiji Emery <seijiemery@gmail.com>
Date: Thu, 27 Aug 2015 18:34:31 -0700
Subject: [PATCH 06/21] Added timeouts

if USE_ENTITY_TIMEOUTS is enabled, entities will now clean up after
themselves if not touched within X seconds (ie. if the script and/or
client crashes and scriptEnding isn't called then the entities will
timeout; otherwise, they get cleaned up normally when the script ends).
---
 examples/example/entities/platform.js | 139 +++++++++++++++++++-------
 1 file changed, 103 insertions(+), 36 deletions(-)

diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js
index 0aeb80e98f..ea9d2fd85d 100644
--- a/examples/example/entities/platform.js
+++ b/examples/example/entities/platform.js
@@ -30,6 +30,10 @@ var LOG_UPDATE_STATUS_MESSAGES = false;		// detailed debugging (startup)
 var MAX_UPDATE_INTERVAL = 0.2;	// restrict to 5 updates / sec
 var AVATAR_HEIGHT_OFFSET = 1.5; // offset to make the platform spawn under your feet. Might need to be adjusted for unusually proportioned avatars.
 
+var USE_ENTITY_TIMEOUTS = true;
+var ENTITY_TIMEOUT_DURATION = 30.0;	// kill entities in 30 secs if they don't get any updates
+var ENTITY_REFRESH_INTERVAL = 10.0;	// poke the entities every 10s so they don't die until we're done with them
+
 // Initial state
 var NUM_PLATFORM_ENTITIES = 400;
 var RADIUS = 5.0;
@@ -350,6 +354,9 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 			destroy: function () {
 				Entities.deleteEntity(entity)
 			},
+			getId: function () {
+				return entity;
+			}
 		});
 	}
 	// this.makeLight = function (properties) {
@@ -389,14 +396,27 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 		// logMessage("color: " + JSON.stringify(this.color));
 
 		this.cachedEntity = null;
-		this.activeEntity = makeEntity({
-			type: this.entityType,
-			position: this.position,	
+		this.activeEntity = this.spawnEntity(this.entityType);
+	};
+	PlatformComponent.prototype.spawnEntity = function (type) {
+		return makeEntity({
+			type: type,
+			position: this.position,
 			dimensions: this.dimensions,
 			color: this.color,
+			lifetime: USE_ENTITY_TIMEOUTS ? ENTITY_TIMEOUT_DURATION : -1.0,
 			alpha: 0.5
 		});
-	};
+	}
+	if (USE_ENTITY_TIMEOUTS) {
+		PlatformComponent.prototype.pokeEntity = function () {
+			// Kinda inefficient, but there's no way to get around this :/
+			var age = Entities.getEntityProperties(this.activeEntity.getId()).age;
+			this.activeEntity.update({ lifetime: ENTITY_TIMEOUT_DURATION + age });
+		}
+	} else {
+		PlatformComponent.prototype.pokeEntity = function () {}
+	}
 	/// Updates platform to be at position p, and calls <entity>.update() with the current
 	/// position, color, and dimensions parameters.
 	PlatformComponent.prototype.update = function (position) {
@@ -409,39 +429,25 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 		var tmp = a;
 		a = b;
 		b = tmp;
-	}
+	}	
 	PlatformComponent.prototype.swapEntityType = function (newType) {
 		if (this.entityType !== newType) {
 			this.entityType = newType;
-
 			// logMessage("Destroying active entity and rebuilding it (newtype = '" + newType + "')");
-
-			// if (this.activeEntity) {
-			// 	this.activeEntity.destroy();
-			// }
-			// this.activeEntity = makeEntity({
-			// 	type: this.entityType,
-			// 	position: this.position,
-			// 	dimensions: this.dimensions,
-			// 	color: this.color,
-			// 	alpha: 0.5
-			// });
-			if (this.cachedEntity && this.cachedEntity.type == newType) {
-				this.cachedEntity.update({ visible: true });
-				this.activeEntity.update({ visible: false });
-				swap(this.cachedEntity, this.activeEntity);
-				this.update(this.position);
-			} else {
-				this.activeEntity.update({ visible: false });
-				this.cachedEntity = this.activeEntity;
-				this.activeEntity = makeEntity({
-					type: newType,
-					dimensions: this.dimensions,
-					color: this.color,
-					position: this.position,
-					alpha: 0.5
-				});
+			if (this.activeEntity) {
+				this.activeEntity.destroy();
 			}
+			this.activeEntity = spawnEntity(newType);
+			// if (this.cachedEntity && this.cachedEntity.type == newType) {
+			// 	this.cachedEntity.update({ visible: true });
+			// 	this.activeEntity.update({ visible: false });
+			// 	swap(this.cachedEntity, this.activeEntity);
+			// 	this.update(this.position);
+			// } else {
+			// 	this.activeEntity.update({ visible: false });
+			// 	this.cachedEntity = this.activeEntity;
+			// 	this.activeEntity = spawnEntity(newType);
+			// }
 		}
 	}
 	/// Swap state with another component
@@ -471,7 +477,6 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 	}
 
 	/// Encapsulates a moving platform that follows the avatar around (mostly).
-	/// Owns a _large_ amount of entities, 
 	var DynamicPlatform = this.DynamicPlatform = function (n, position, radius) {
 		this.position = position;
 		this.radius   = radius;
@@ -479,7 +484,7 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 		this.boxType = "Box";
 		this.boxTypes = [ "Box", "Sphere" ];
 
-		// logMessage("Spawning " + n + " entities", COLORS.GREEN);
+		logMessage("Spawning " + n + " entities", COLORS.GREEN);
 		var boxes = this.boxes = [];
 		while (n > 0) {
 			boxes.push(this.spawnEntity());
@@ -492,6 +497,8 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 		this.platformHeight = position.y;
 		this.oldPos    = { x: position.x, y: position.y, z: position.z };
 		this.oldRadius = radius;
+
+		// this.sendPokes();
 	}
 	DynamicPlatform.prototype.toString = function () {
 		return "[DynamicPlatform (" + this.boxes.length + " entities)]";
@@ -533,16 +540,45 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 		return this.boxType;
 	}
 
+	// if (USE_ENTITY_TIMEOUTS) {
+	// 	DynamicPlatform.prototype.sendPokes = function () {
+	// 		var _this = this;
+	// 		function poke () {
+	// 			logMessage("Poking entities so they don't die", COLORS.GREEN);
+	// 			_this.boxes.forEach(function (box) {
+	// 				box.pokeEntity();
+	// 			}, _this);
+
+
+	// 			if (_this.pendingUpdates['keepalive']) {
+	// 				logMessage("previous timer: " + _this.pendingUpdates['keepalive'].timer + "; new timer: " + ENTITY_REFRESH_INTERVAL)
+	// 			}
+	// 			_this.pendingUpdates['keepalive'] = {
+	// 				callback: poke,
+	// 				timer: ENTITY_REFRESH_INTERVAL,
+	// 				skippedUpdates: 0
+	// 			};
+	// 			// _this.setPendingUpdate('keepalive', poke);
+	// 			// _this.pendingUpdates['keepalive'].timer = ENTITY_REFRESH_INTERVAL;
+	// 		}
+	// 		poke();
+	// 	}
+	// } else {
+	// 	DynamicPlatform.prototype.sendPokes = function () {};
+	// }
+
 	/// Queue impl that uses the update loop to limit potentially expensive updates to only execute every x seconds (default: 200 ms).
 	/// This is to prevent UI code from running full entity updates every 10 ms (or whatever).
 	DynamicPlatform.prototype.setPendingUpdate = function (name, callback) {
 		if (!this.pendingUpdates[name]) {
+			// logMessage("Queued update for " + name, COLORS.GREEN);
 			this.pendingUpdates[name] = {
 				callback: callback,
 				timer: 0.0,
 				skippedUpdates: 0
 			}
 		} else {
+			// logMessage("Deferred update for " + name, COLORS.GREEN);
 			this.pendingUpdates[name].callback = callback;
 			this.pendingUpdates[name].skippedUpdates++;
 			// logMessage("scheduling update for \"" + name + "\" to run in " + this.pendingUpdates[name].timer + " seconds");
@@ -555,7 +591,7 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 				this.pendingUpdates[k].timer -= dt;
 
 			if (this.pendingUpdates[k].callback && this.pendingUpdates[k].timer < 0.0) {
-				// logMessage("Running update for \"" + k + "\" (skipped " + this.pendingUpdates[k].skippedUpdates + ")");
+				// logMessage("Dispatching update for " + k);
 				try {
 					this.pendingUpdates[k].callback();
 				} catch (e) {
@@ -564,6 +600,8 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 				this.pendingUpdates[k].timer = MAX_UPDATE_INTERVAL;
 				this.pendingUpdates[k].skippedUpdates = 0;
 				this.pendingUpdates[k].callback = null;
+			} else {
+				// logMessage("Deferred update for " + k + " for " + this.pendingUpdates[k].timer + " seconds");
 			}
 		}
 	}
@@ -573,7 +611,7 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 	/// Does NOT have any update interval limits (it just updates every time it gets run), but these are not full
 	/// updates (they're incremental), so the network will not get flooded so long as the avatar is moving at a
 	/// normal walking / flying speed.
-	DynamicPlatform.prototype.update = function (dt, position) {
+	DynamicPlatform.prototype.updatePosition = function (dt, position) {
 		// logMessage("updating " + this);
 		position.y = this.platformHeight;
 		this.position = position;
@@ -622,8 +660,37 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 		if (LOG_UPDATE_STATUS_MESSAGES && toUpdate.length > 0) {
 			logMessage("updated " + toUpdate.length + " entities w/ " + recalcs + " recalcs");
 		}
+	}
 
+	DynamicPlatform.prototype.update = function (dt, position) {
+		this.updatePosition(dt, position);
 		this.processPendingUpdates(dt);
+		this.sendPokes(dt);
+	}
+
+	if (USE_ENTITY_TIMEOUTS) {
+		DynamicPlatform.prototype.sendPokes = function (dt) {
+			logMessage("starting keepalive", COLORS.GREEN);
+			// logMessage("dt = " + dt, COLORS.RED);
+			// var original = this.sendPokes;
+			var pokeTimer = 0.0;
+			this.sendPokes = function (dt) {
+				// logMessage("dt = " + dt);
+				if ((pokeTimer -= dt) < 0.0) {
+					// logMessage("Poking entities so they don't die", COLORS.GREEN);
+					this.boxes.forEach(function (box) {
+						box.pokeEntity();
+					}, this);
+					pokeTimer = ENTITY_REFRESH_INTERVAL;
+				} else {
+					// logMessage("Poking entities in " + pokeTimer + " seconds");
+				}
+			}
+			// logMessage("this.sendPokes === past this.sendPokes? " + (this.sendPokes === original), COLORS.GREEN);
+			this.sendPokes(dt);
+		}
+	} else {
+		DynamicPlatform.prototype.sendPokes = function () {};
 	}
 	DynamicPlatform.prototype.getEntityCount = function () {
 		return this.boxes.length;

From c12f192c3ab405d10a6b11ddfdd6aa1c7f597f91 Mon Sep 17 00:00:00 2001
From: Seiji Emery <seijiemery@gmail.com>
Date: Thu, 27 Aug 2015 21:44:55 -0700
Subject: [PATCH 07/21] bugfix...

Apparently I didn't test that -_-
---
 examples/example/entities/platform.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js
index ea9d2fd85d..3d6744b364 100644
--- a/examples/example/entities/platform.js
+++ b/examples/example/entities/platform.js
@@ -15,7 +15,7 @@
 
 // UI and debug console implemented using uiwidgets / 2d overlays
 Script.include("../../libraries/uiwidgets.js");
-if (typeof('UI') === 'undefined') {	// backup link in case the user downloaded this somewhere
+if (typeof(UI) === 'undefined') {	// backup link in case the user downloaded this somewhere
 	Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js');
 }
 

From 202f68be58b3242bfde846786c26ca19903a8288 Mon Sep 17 00:00:00 2001
From: Seiji Emery <seijiemery@gmail.com>
Date: Fri, 28 Aug 2015 11:38:59 -0700
Subject: [PATCH 08/21] Fixed sphere entities + renamed user settings

---
 examples/example/entities/platform.js | 22 +++++++++++-----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js
index 3d6744b364..bee04d8950 100644
--- a/examples/example/entities/platform.js
+++ b/examples/example/entities/platform.js
@@ -40,9 +40,9 @@ var RADIUS = 5.0;
 
 // Defines min/max for onscreen platform radius, density, and entity width/height/depth sliders.
 // Color limits are hardcoded at [0, 255].
-var UI_RADIUS_RANGE = [ 1.0, 15.0 ];
-var UI_DENSITY_RANGE = [ 0.0, 35.0 ];	// do NOT increase this above 40! (~20k limit). Entity count = Math.PI * radius * radius * density.
-var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension limits
+var PLATFORM_RADIUS_RANGE  = [ 1.0, 15.0 ];
+var PLATFORM_DENSITY_RANGE = [ 0.0, 35.0 ];	// do NOT increase this above 40! (~20k limit). Entity count = Math.PI * radius * radius * density.
+var PLATFORM_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension limits
 
 // Utils
 (function () {
@@ -151,8 +151,8 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 	/// Custom property iterator used to setup UI (sliders, etc)
 	RandomShapeModel.prototype.setupUI = function (callback) {
 		var _this = this;
-		var dimensionsMin = UI_SHAPE_DIMENSIONS_RANGE[0];
-		var dimensionsMax = UI_SHAPE_DIMENSIONS_RANGE[1];
+		var dimensionsMin = PLATFORM_SHAPE_DIMENSIONS_RANGE[0];
+		var dimensionsMax = PLATFORM_SHAPE_DIMENSIONS_RANGE[1];
 		[
 			['widthRange', 'width'], 
 			['depthRange', 'depth'], 
@@ -437,7 +437,7 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 			if (this.activeEntity) {
 				this.activeEntity.destroy();
 			}
-			this.activeEntity = spawnEntity(newType);
+			this.activeEntity = this.spawnEntity(newType);
 			// if (this.cachedEntity && this.cachedEntity.type == newType) {
 			// 	this.cachedEntity.update({ visible: true });
 			// 	this.activeEntity.update({ visible: false });
@@ -997,21 +997,21 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 
 		// Top controls
 		addLabel(controls, "Platform (platform.js)");
-		controls.radiusSlider = addSlider(controls, "radius", UI_RADIUS_RANGE[0], UI_RADIUS_RANGE[1], function () { return platform.getRadius() },
+		controls.radiusSlider = addSlider(controls, "radius", PLATFORM_RADIUS_RANGE[0], PLATFORM_RADIUS_RANGE[1], function () { return platform.getRadius() },
 			function (value) { 
 				platform.setRadiusOnNextUpdate(value);
 				controls.entityCountSlider.setValue(platform.getEntityCountWithRadius(value));
 			});
 		addSpacing(controls, 1, 2);
-		controls.densitySlider = addSlider(controls, "entity density", UI_DENSITY_RANGE[0], UI_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, 
+		controls.densitySlider = addSlider(controls, "entity density", PLATFORM_DENSITY_RANGE[0], PLATFORM_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, 
 			function (value) {
 				platform.setDensityOnNextUpdate(value);
 				controls.entityCountSlider.setValue(platform.getEntityCountWithDensity(value));
 			});
 		addSpacing(controls, 1, 2);
 
-		var minEntities = Math.PI * UI_RADIUS_RANGE[0] * UI_RADIUS_RANGE[0] * UI_DENSITY_RANGE[0];
-		var maxEntities = Math.PI * UI_RADIUS_RANGE[1] * UI_RADIUS_RANGE[1] * UI_DENSITY_RANGE[1];
+		var minEntities = Math.PI * PLATFORM_RADIUS_RANGE[0] * PLATFORM_RADIUS_RANGE[0] * PLATFORM_DENSITY_RANGE[0];
+		var maxEntities = Math.PI * PLATFORM_RADIUS_RANGE[1] * PLATFORM_RADIUS_RANGE[1] * PLATFORM_DENSITY_RANGE[1];
 		controls.entityCountSlider = addSlider(controls, "entity count", minEntities, maxEntities, function () { return platform.getEntityCount() },
 			function (value) {});
 		controls.entityCountSlider.actions = {}; // hack: make this slider readonly (clears all attached actions)
@@ -1057,7 +1057,7 @@ var UI_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension
 				setValue(value);
 				platform.updateEntityAttribs();
 			});
-			addSpacing(shapeControls, 1, 2);
+			addSpacing(colorControls, 1, 2);
 		});
 		
 		moveToBottomLeftScreenCorner(layoutContainer);

From d9ea2ae27be1ac76b64adf116fd07c5b6a488ac4 Mon Sep 17 00:00:00 2001
From: Seiji Emery <seijiemery@gmail.com>
Date: Fri, 28 Aug 2015 15:43:55 -0700
Subject: [PATCH 09/21] Fixed sliders

---
 examples/example/entities/platform.js | 19 +++--
 examples/libraries/uiwidgets.js       | 99 +++++++++++++++------------
 2 files changed, 62 insertions(+), 56 deletions(-)

diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js
index bee04d8950..d4fe7d0f68 100644
--- a/examples/example/entities/platform.js
+++ b/examples/example/entities/platform.js
@@ -16,7 +16,12 @@
 // UI and debug console implemented using uiwidgets / 2d overlays
 Script.include("../../libraries/uiwidgets.js");
 if (typeof(UI) === 'undefined') {	// backup link in case the user downloaded this somewhere
+	print("Missing library script -- loading from public.highfidelity.io");
 	Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js');
+	if (typeof(UI) === 'undefined') {
+		print("Cannot load UIWidgets library -- check your internet connection", COLORS.RED);
+		throw new Error("Could not load uiwidgets.js");
+	}
 }
 
 // Platform script
@@ -890,7 +895,7 @@ var PLATFORM_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dim
 		});
 		this.boxes = [];
 	}
-}).call(this);
+})();
 
 // UI
 (function () {
@@ -1145,15 +1150,7 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false;
 	// Update
 	this.update = function (dt) {
 		checkScreenDimensions();
-
 		var pos = getTargetPlatformPosition();
-		// if (Math.abs(pos.y - lastHeight) * dt > MAX_ACCELERATION_THRESHOLD) {
-		// 	// User likely teleported
-		// 	logMessage("Height rebuild (" +
-		// 		"(" + Math.abs(pos.y - lastHeight) + " * " + dt + " = " + (Math.abs(pos.y - lastHeight) * dt) + ")" +
-		// 		" > " + MAX_ACCELERATION_THRESHOLD + ")");
-		// 	platform.updateHeight(pos.y);
-		// }
 		platform.update(dt, getTargetPlatformPosition(), platform.getRadius());
 	}
 
@@ -1182,9 +1179,9 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false;
 
 			function init () {
 				logMessage("initializing...");
-
+				
 				this.initPlatform();
-	
+
 				Script.update.connect(this.update);
 				Script.scriptEnding.connect(this.teardown);
 	
diff --git a/examples/libraries/uiwidgets.js b/examples/libraries/uiwidgets.js
index 70eda8e5f2..479acd2ce8 100644
--- a/examples/libraries/uiwidgets.js
+++ b/examples/libraries/uiwidgets.js
@@ -28,13 +28,10 @@ if (this.Vec2 == undefined) {
         return new Vec2(v.x, v.y);
     }
 } else if (this.Vec2.clone == undefined) {
-    print("Vec2 exists; adding Vec2.clone");
     this.Vec2.clone = function (v) {
         return { 'x': v.x || 0.0, 'y': v.y || 0.0 };
     }
-} else {
-    print("Vec2...?");
-}
+} else {}
 })();
 
 var Rect = function (xmin, ymin, xmax, ymax) {
@@ -566,46 +563,51 @@ var Slider = UI.Slider = function (properties) {
     this.slider = new Box(properties.slider);
     this.slider.parent = this;
 
-    var updateSliderPos = function (event, widget) {
-        var rx = Math.max(event.x * 1.0 - widget.position.x - widget.slider.width * 0.5, 0.0);
+    var clickOffset = { x: 0.0, y: 0.0 };   // offset relative to slider knob
+    var widget = this;
+    var updateDrag = function (event) {
+        var rx = Math.max(event.x * 1.0 - widget.position.x - clickOffset.x, 0.0);
         var width = Math.max(widget.width - widget.slider.width - widget.padding.x * 2.0, 0.0);
         var v = Math.min(rx, width) / (width || 1);
 
-        widget.value = widget.minValue + (
-            widget.maxValue - widget.minValue) * v;
+        // print("dragging slider: rx = " + rx + ", width = " + width + ", v = " + v);
+
+        widget.value = widget.minValue + (widget.maxValue - widget.minValue) * v;
         widget.onValueChanged(widget.value);
         UI.updateLayout();
     }
+    var startDrag = function (event) {
+        // calculate position of slider knob
+        var x0 = widget.position.x + widget.padding.x;
+        var width = (widget.width - widget.slider.width - widget.padding.x * 2.0);
+        var normalizedValue = (widget.value - widget.minValue) / (widget.maxValue - widget.minValue)
 
-    var widget = this;
-    this.addAction('onMouseDown', function (event) {
-        sliderRel.x = sliderRel.y = 0.0;
-        // sliderRel.x = widget.slider.width * 0.5;
-        // sliderRel.y = widget.slider.height * 0.5;
-        updateSliderPos(event, widget);
+        var sliderX = x0 + normalizedValue * width;
+        var sliderWidth = widget.slider.width;
 
-        // hack
-        ui.clickedWidget = ui.draggedWidget = widget.slider;
-    });
+        if (event.x >= sliderX && event.x <= sliderX + sliderWidth) {
+            // print("Start drag -- on slider knob");
+            clickOffset.x = event.x - sliderX;
+        } else if (event.x >= x0 && event.x <= x0 + width) {
+            // print("Start drag -- on slider bar");
+            clickOffset.x = sliderWidth * 0.5;
+        } else {
+            clickOffset.x = 0.0;
+            // print("Start drag -- out of bounds!");
+            // print("event.x = " + event.x);
+            // print("x0 = " + x0 + ", x1 = " + (x0 + width) + " (width = " + width + ")");
+            // print("s0 = " + sliderX + ", s1 = " + (sliderX + sliderWidth) + "(slider width = " + sliderWidth + ")");
+            // print("widget = " + widget);
+            // print("widget.slider = " + widget.slider);
+            // print("widget.width = " + widget.width + ", widget.slider.width = " + widget.slider.width);
+        }
+        updateDrag(event);
+    }
 
-    var sliderRel = {};
-    this.slider.addAction('onMouseDown', function (event) {
-        sliderRel.x = widget.slider.position.x - event.x;
-        sliderRel.y = widget.slider.position.y - event.y;
-        event.x += sliderRel.x;
-        event.y += sliderRel.y;
-        updateSliderPos(event, widget);
-    });
-    this.slider.addAction('onDragBegin', function (event) {
-        event.x += sliderRel.x;
-        event.y += sliderRel.y;
-        updateSliderPos(event, widget);
-    })
-    this.slider.addAction('onDragUpdate', function (event) {
-        event.x += sliderRel.x;
-        event.y += sliderRel.y;
-        updateSliderPos(event, widget);
-    })
+    this.addAction('onMouseDown', startDrag);
+    this.addAction('onDragBegin', updateDrag);
+    this.addAction('onDragUpdate', updateDrag);
+    this.slider.actions = this.actions;
 };
 Slider.prototype = new Box();
 Slider.prototype.constructor = Slider;
@@ -947,16 +949,25 @@ var dispatchEvent = function (action, event, widget) {
     }
 }
 
+function hasAction (widget, action) {
+    // print("widget = " + widget);
+    // print("action = " + action);
+    // if (widget) {
+        // print("widget.actions[<action>] = " + widget.actions[action]);
+        // print("widget.parent = " + widget.parent);
+    // }
+    return widget && (widget.actions[action] || hasAction(widget.parent, action));
+}
+
 UI.handleMouseMove = function (event, canStartDrag) {
-    if (canStartDrag === undefined)
+    // if (canStartDrag === undefined)
+    if (arguments.length < 2)
         canStartDrag = true;
 
     // print("mouse moved x = " + event.x + ", y = " + event.y);
     var focused = getFocusedWidget(event);
 
-    // print("got focus: " + focused);
-
-    if (canStartDrag && !ui.draggedWidget && ui.clickedWidget && ui.clickedWidget.actions['onDragBegin']) {
+    if (!ui.draggedWidget && ui.clickedWidget && hasAction(ui.clickedWidget, 'onDragBegin')) {
         ui.draggedWidget = ui.clickedWidget;
         dispatchEvent('onDragBegin', event, ui.draggedWidget);
     } else if (ui.draggedWidget) {
@@ -980,26 +991,24 @@ UI.handleMousePress = function (event) {
 }
 
 UI.handleMouseDoublePress = function (event) {
-    // print("DOUBLE CLICK!");
     var focused = getFocusedWidget(event);
     UI.handleMouseMove(event);
     if (focused) {
-        // print("dispatched onDoubleClick");
         dispatchEvent('onDoubleClick', event, focused);
     }
 }
 
 UI.handleMouseRelease = function (event) {
-    // print("Mouse released");
-
     if (ui.draggedWidget) {
         dispatchEvent('onDragEnd', event, ui.draggedWidget);
     } else {
-        UI.handleMouseMove(event, false);
+        var clicked = ui.clickedWidget;
+        ui.clickedWidget = null;
+        UI.handleMouseMove(event);
         if (ui.focusedWidget) {
             dispatchEvent('onMouseUp', event, ui.focusedWidget);
 
-            if (ui.clickedWidget == ui.focusedWidget) {
+            if (clicked == ui.focusedWidget) {
                 dispatchEvent('onClick', event, ui.focusedWidget);
             }
         }

From e723a5c0fd62c862a280cc6bd6a67a0234e4d1fc Mon Sep 17 00:00:00 2001
From: Seiji Emery <seijiemery@gmail.com>
Date: Fri, 18 Sep 2015 11:18:33 -0700
Subject: [PATCH 10/21] whitespace + "waiting for entity server"

No functionality changes, but now prints "waiting for entity server"
once if waiting for more than X seconds in startup().
---
 examples/example/entities/platform.js | 2163 +++++++++++++------------
 1 file changed, 1087 insertions(+), 1076 deletions(-)

diff --git a/examples/example/entities/platform.js b/examples/example/entities/platform.js
index d4fe7d0f68..1748198cce 100644
--- a/examples/example/entities/platform.js
+++ b/examples/example/entities/platform.js
@@ -15,29 +15,29 @@
 
 // UI and debug console implemented using uiwidgets / 2d overlays
 Script.include("../../libraries/uiwidgets.js");
-if (typeof(UI) === 'undefined') {	// backup link in case the user downloaded this somewhere
-	print("Missing library script -- loading from public.highfidelity.io");
-	Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js');
-	if (typeof(UI) === 'undefined') {
-		print("Cannot load UIWidgets library -- check your internet connection", COLORS.RED);
-		throw new Error("Could not load uiwidgets.js");
-	}
+if (typeof(UI) === 'undefined') {   // backup link in case the user downloaded this somewhere
+    print("Missing library script -- loading from public.highfidelity.io");
+    Script.include('http://public.highfidelity.io/scripts/libraries/uiwidgets.js');
+    if (typeof(UI) === 'undefined') {
+        print("Cannot load UIWidgets library -- check your internet connection", COLORS.RED);
+        throw new Error("Could not load uiwidgets.js");
+    }
 }
 
 // Platform script
 (function () {
 var SCRIPT_NAME = "platform.js";
-var USE_DEBUG_LOG = true;	// Turns on the 2dOverlay-based debug log. If false, just redirects to print.
+var USE_DEBUG_LOG = true;   // Turns on the 2dOverlay-based debug log. If false, just redirects to print.
 var NUM_DEBUG_LOG_LINES = 10;
-var LOG_ENTITY_CREATION_MESSAGES = false;	// detailed debugging (init)
-var LOG_UPDATE_STATUS_MESSAGES = false;		// detailed debugging (startup)
+var LOG_ENTITY_CREATION_MESSAGES = false;   // detailed debugging (init)
+var LOG_UPDATE_STATUS_MESSAGES = false;     // detailed debugging (startup)
 
-var MAX_UPDATE_INTERVAL = 0.2;	// restrict to 5 updates / sec
+var MAX_UPDATE_INTERVAL = 0.2;  // restrict to 5 updates / sec
 var AVATAR_HEIGHT_OFFSET = 1.5; // offset to make the platform spawn under your feet. Might need to be adjusted for unusually proportioned avatars.
 
 var USE_ENTITY_TIMEOUTS = true;
-var ENTITY_TIMEOUT_DURATION = 30.0;	// kill entities in 30 secs if they don't get any updates
-var ENTITY_REFRESH_INTERVAL = 10.0;	// poke the entities every 10s so they don't die until we're done with them
+var ENTITY_TIMEOUT_DURATION = 30.0; // kill entities in 30 secs if they don't get any updates
+var ENTITY_REFRESH_INTERVAL = 10.0; // poke the entities every 10s so they don't die until we're done with them
 
 // Initial state
 var NUM_PLATFORM_ENTITIES = 400;
@@ -46,1039 +46,1039 @@ var RADIUS = 5.0;
 // Defines min/max for onscreen platform radius, density, and entity width/height/depth sliders.
 // Color limits are hardcoded at [0, 255].
 var PLATFORM_RADIUS_RANGE  = [ 1.0, 15.0 ];
-var PLATFORM_DENSITY_RANGE = [ 0.0, 35.0 ];	// do NOT increase this above 40! (~20k limit). Entity count = Math.PI * radius * radius * density.
+var PLATFORM_DENSITY_RANGE = [ 0.0, 35.0 ]; // do NOT increase this above 40! (~20k limit). Entity count = Math.PI * radius * radius * density.
 var PLATFORM_SHAPE_DIMENSIONS_RANGE = [ 0.001, 2.0 ]; // axis-aligned entity dimension limits
 
 // Utils
 (function () {
-	if (typeof(Math.randRange) === 'undefined') {
-		Math.randRange = function (min, max) {
-			return Math.random() * (max - min) + min;
-		}
-	}
-	if (typeof(Math.randInt) === 'undefined') {
-		Math.randInt = function (n) {
-			return Math.floor(Math.random() * n) | 0;
-		}
-	}
-	function fromComponents (r, g, b, a) {
-		this.red   = r;
-		this.green = g;
-		this.blue  = b;
-		this.alpha = a || 1.0;
-	}
-	function fromHex (c) {
-		this.red   = parseInt(c[1] + c[2], 16);
-		this.green = parseInt(c[3] + c[4], 16);
-		this.blue  = parseInt(c[5] + c[6], 16);
-	}
-	var Color = this.Color = function () {
-		if (arguments.length >= 3) {
-			fromComponents.apply(this, arguments);
-		} else if (arguments.length == 1 && arguments[0].length == 7 && arguments[0][0] == '#') {
-			fromHex.apply(this, arguments);
-		} else {
-			throw new Error("Invalid arguments to new Color(): " + JSON.stringify(arguments));
-		}
-	}
-	Color.prototype.toString = function () {
-		return "[Color: " + JSON.stringify(this) + "]";
-	}
+    if (typeof(Math.randRange) === 'undefined') {
+        Math.randRange = function (min, max) {
+            return Math.random() * (max - min) + min;
+        }
+    }
+    if (typeof(Math.randInt) === 'undefined') {
+        Math.randInt = function (n) {
+            return Math.floor(Math.random() * n) | 0;
+        }
+    }
+    function fromComponents (r, g, b, a) {
+        this.red   = r;
+        this.green = g;
+        this.blue  = b;
+        this.alpha = a || 1.0;
+    }
+    function fromHex (c) {
+        this.red   = parseInt(c[1] + c[2], 16);
+        this.green = parseInt(c[3] + c[4], 16);
+        this.blue  = parseInt(c[5] + c[6], 16);
+    }
+    var Color = this.Color = function () {
+        if (arguments.length >= 3) {
+            fromComponents.apply(this, arguments);
+        } else if (arguments.length == 1 && arguments[0].length == 7 && arguments[0][0] == '#') {
+            fromHex.apply(this, arguments);
+        } else {
+            throw new Error("Invalid arguments to new Color(): " + JSON.stringify(arguments));
+        }
+    }
+    Color.prototype.toString = function () {
+        return "[Color: " + JSON.stringify(this) + "]";
+    }
 })();
 
 // RNG models
 (function () {
-	/// Encapsulates a simple color model that generates colors using a linear, pseudo-random color distribution.
-	var RandomColorModel = this.RandomColorModel = function () {
-		this.shadeRange = 0;	// = 200;
-		this.minColor = 55;		// = 100;
-		this.redRange = 255;	// = 200;
-		this.greenRange = 0; 	// = 10;
-		this.blueRange = 0;		// = 0;
-	};
-	/// Generates 4 numbers in [0, 1] corresponding to each color attribute (uniform shade and additive red, green, blue).
-	/// This is done in a separate step from actually generating the colors, since it allows us to either A) completely
-	/// rebuild / re-randomize the color values, or B) reuse the RNG values but with different color parameters, which
-	/// enables us to do realtime color editing on the same visuals (awesome!).
-	RandomColorModel.prototype.generateSeed = function () {
-		return [ Math.random(), Math.random(), Math.random(), Math.random() ];
-	};
-	/// Takes a random 'seed' (4 floats from this.generateSeed()) and calculates a pseudo-random
-	/// color by combining that with the color model's current parameters.
-	RandomColorModel.prototype.getRandom = function (r) {
-		// logMessage("color seed values " + JSON.stringify(r));
-		var shade = Math.min(255, this.minColor + r[0] * this.shadeRange);
+    /// Encapsulates a simple color model that generates colors using a linear, pseudo-random color distribution.
+    var RandomColorModel = this.RandomColorModel = function () {
+        this.shadeRange = 0;    // = 200;
+        this.minColor = 55;     // = 100;
+        this.redRange = 255;    // = 200;
+        this.greenRange = 0;    // = 10;
+        this.blueRange = 0;     // = 0;
+    };
+    /// Generates 4 numbers in [0, 1] corresponding to each color attribute (uniform shade and additive red, green, blue).
+    /// This is done in a separate step from actually generating the colors, since it allows us to either A) completely
+    /// rebuild / re-randomize the color values, or B) reuse the RNG values but with different color parameters, which
+    /// enables us to do realtime color editing on the same visuals (awesome!).
+    RandomColorModel.prototype.generateSeed = function () {
+        return [ Math.random(), Math.random(), Math.random(), Math.random() ];
+    };
+    /// Takes a random 'seed' (4 floats from this.generateSeed()) and calculates a pseudo-random
+    /// color by combining that with the color model's current parameters.
+    RandomColorModel.prototype.getRandom = function (r) {
+        // logMessage("color seed values " + JSON.stringify(r));
+        var shade = Math.min(255, this.minColor + r[0] * this.shadeRange);
 
-		// No clamping on the color components, so they may overflow. 
-		// However, this creates some pretty interesting visuals, so we're not "fixing" this.
-		var color = {
-			red:   shade + r[1] * this.redRange,
-			green: shade + r[2] * this.greenRange,
-			blue:  shade + r[3] * this.blueRange
-		};
-		// logMessage("this: " + JSON.stringify(this));
-		// logMessage("color: " + JSON.stringify(color), COLORS.RED);
-		return color;
-	};
-	/// Custom property iterator used to setup UI (sliders, etc)
-	RandomColorModel.prototype.setupUI = function (callback) {
-		var _this = this;
-		[ 
-			['shadeRange', 'shade range'], 
-			['minColor', 'shade min'], 
-			['redRange', 'red (additive)'],
-			['greenRange', 'green (additive)'], 
-			['blueRange', 'blue (additive)'] 
-		].forEach(function (v) {
-			//       name, value, min, max, onValueChanged
-			callback(v[1], _this[v[0]], 0, 255, function (value) { _this[v[0]] = value });
-		});
-	}
+        // No clamping on the color components, so they may overflow. 
+        // However, this creates some pretty interesting visuals, so we're not "fixing" this.
+        var color = {
+            red:   shade + r[1] * this.redRange,
+            green: shade + r[2] * this.greenRange,
+            blue:  shade + r[3] * this.blueRange
+        };
+        // logMessage("this: " + JSON.stringify(this));
+        // logMessage("color: " + JSON.stringify(color), COLORS.RED);
+        return color;
+    };
+    /// Custom property iterator used to setup UI (sliders, etc)
+    RandomColorModel.prototype.setupUI = function (callback) {
+        var _this = this;
+        [ 
+            ['shadeRange', 'shade range'], 
+            ['minColor', 'shade min'], 
+            ['redRange', 'red (additive)'],
+            ['greenRange', 'green (additive)'], 
+            ['blueRange', 'blue (additive)'] 
+        ].forEach(function (v) {
+            //       name, value, min, max, onValueChanged
+            callback(v[1], _this[v[0]], 0, 255, function (value) { _this[v[0]] = value });
+        });
+    }
 
-	/// Generates pseudo-random dimensions for our cubes / shapes.
-	var RandomShapeModel = this.RandomShapeModel = function () {
-		this.widthRange = [ 0.3, 0.7 ];
-		this.depthRange = [ 0.5, 0.8 ];
-		this.heightRange = [ 0.01, 0.08 ];
-	};
-	/// Generates 3 seed numbers in [0, 1]
-	RandomShapeModel.prototype.generateSeed = function () {
-		return [ Math.random(), Math.random(), Math.random() ];
-	}
-	/// Combines seed values with width/height/depth ranges to produce vec3 dimensions for a cube / sphere.
-	RandomShapeModel.prototype.getRandom = function (r) {
-		return {
-			x: r[0] * (this.widthRange[1] - this.widthRange[0]) + this.widthRange[0],
-			y: r[1] * (this.heightRange[1] - this.heightRange[0]) + this.heightRange[0],
-			z: r[2] * (this.depthRange[1] - this.depthRange[0]) + this.depthRange[0]
-		};
-	}
-	/// Custom property iterator used to setup UI (sliders, etc)
-	RandomShapeModel.prototype.setupUI = function (callback) {
-		var _this = this;
-		var dimensionsMin = PLATFORM_SHAPE_DIMENSIONS_RANGE[0];
-		var dimensionsMax = PLATFORM_SHAPE_DIMENSIONS_RANGE[1];
-		[
-			['widthRange', 'width'], 
-			['depthRange', 'depth'], 
-			['heightRange', 'height'] 
-		].forEach(function (v) {
-			//       name, value, min, max, onValueChanged
-			callback(v[1], _this[v[0]], dimensionsMin, dimensionsMax, function (value) { _this[v[0]] = value });
-		});
-	}
+    /// Generates pseudo-random dimensions for our cubes / shapes.
+    var RandomShapeModel = this.RandomShapeModel = function () {
+        this.widthRange = [ 0.3, 0.7 ];
+        this.depthRange = [ 0.5, 0.8 ];
+        this.heightRange = [ 0.01, 0.08 ];
+    };
+    /// Generates 3 seed numbers in [0, 1]
+    RandomShapeModel.prototype.generateSeed = function () {
+        return [ Math.random(), Math.random(), Math.random() ];
+    }
+    /// Combines seed values with width/height/depth ranges to produce vec3 dimensions for a cube / sphere.
+    RandomShapeModel.prototype.getRandom = function (r) {
+        return {
+            x: r[0] * (this.widthRange[1] - this.widthRange[0]) + this.widthRange[0],
+            y: r[1] * (this.heightRange[1] - this.heightRange[0]) + this.heightRange[0],
+            z: r[2] * (this.depthRange[1] - this.depthRange[0]) + this.depthRange[0]
+        };
+    }
+    /// Custom property iterator used to setup UI (sliders, etc)
+    RandomShapeModel.prototype.setupUI = function (callback) {
+        var _this = this;
+        var dimensionsMin = PLATFORM_SHAPE_DIMENSIONS_RANGE[0];
+        var dimensionsMax = PLATFORM_SHAPE_DIMENSIONS_RANGE[1];
+        [
+            ['widthRange', 'width'], 
+            ['depthRange', 'depth'], 
+            ['heightRange', 'height'] 
+        ].forEach(function (v) {
+            //       name, value, min, max, onValueChanged
+            callback(v[1], _this[v[0]], dimensionsMin, dimensionsMax, function (value) { _this[v[0]] = value });
+        });
+    }
 
-	/// Combines color + shape PRNG models and hides their implementation details.
-	var RandomAttribModel = this.RandomAttribModel = function () {
-		this.colorModel = new RandomColorModel();
-		this.shapeModel = new RandomShapeModel();
-	}
-	/// Completely re-randomizes obj's `color` and `dimensions` parameters based on the current model params.
-	RandomAttribModel.prototype.randomizeShapeAndColor = function (obj) {
-		// logMessage("randomizing " + JSON.stringify(obj));
-		obj._colorSeed = this.colorModel.generateSeed();
-		obj._shapeSeed = this.shapeModel.generateSeed();
-		this.updateShapeAndColor(obj);
-		// logMessage("color seed: " + JSON.stringify(obj._colorSeed), COLORS.RED);
-		// logMessage("randomized color: " + JSON.stringify(obj.color), COLORS.RED);
-		// logMessage("randomized: " + JSON.stringify(obj));
-		return obj;
-	}
-	/// Updates obj's `color` and `dimensions` params to use the current model params.
-	/// Reuses hidden seed attribs; _must_ have called randomizeShapeAndColor(obj) at some point before
-	/// calling this.
-	RandomAttribModel.prototype.updateShapeAndColor = function (obj) {
-		try {
-			// logMessage("update shape and color: " + this.colorModel);
-			obj.color      = this.colorModel.getRandom(obj._colorSeed);
-			obj.dimensions = this.shapeModel.getRandom(obj._shapeSeed); 
-		} catch (e) {
-			logMessage("update shape / color failed", COLORS.RED);
-			logMessage('' + e, COLORS.RED);
-			logMessage("obj._colorSeed = " + JSON.stringify(obj._colorSeed));
-			logMessage("obj._shapeSeed = " + JSON.stringify(obj._shapeSeed));
-			// logMessage("obj = " + JSON.stringify(obj));
-			throw e;
-		}
-		return obj;
-	}
+    /// Combines color + shape PRNG models and hides their implementation details.
+    var RandomAttribModel = this.RandomAttribModel = function () {
+        this.colorModel = new RandomColorModel();
+        this.shapeModel = new RandomShapeModel();
+    }
+    /// Completely re-randomizes obj's `color` and `dimensions` parameters based on the current model params.
+    RandomAttribModel.prototype.randomizeShapeAndColor = function (obj) {
+        // logMessage("randomizing " + JSON.stringify(obj));
+        obj._colorSeed = this.colorModel.generateSeed();
+        obj._shapeSeed = this.shapeModel.generateSeed();
+        this.updateShapeAndColor(obj);
+        // logMessage("color seed: " + JSON.stringify(obj._colorSeed), COLORS.RED);
+        // logMessage("randomized color: " + JSON.stringify(obj.color), COLORS.RED);
+        // logMessage("randomized: " + JSON.stringify(obj));
+        return obj;
+    }
+    /// Updates obj's `color` and `dimensions` params to use the current model params.
+    /// Reuses hidden seed attribs; _must_ have called randomizeShapeAndColor(obj) at some point before
+    /// calling this.
+    RandomAttribModel.prototype.updateShapeAndColor = function (obj) {
+        try {
+            // logMessage("update shape and color: " + this.colorModel);
+            obj.color      = this.colorModel.getRandom(obj._colorSeed);
+            obj.dimensions = this.shapeModel.getRandom(obj._shapeSeed); 
+        } catch (e) {
+            logMessage("update shape / color failed", COLORS.RED);
+            logMessage('' + e, COLORS.RED);
+            logMessage("obj._colorSeed = " + JSON.stringify(obj._colorSeed));
+            logMessage("obj._shapeSeed = " + JSON.stringify(obj._shapeSeed));
+            // logMessage("obj = " + JSON.stringify(obj));
+            throw e;
+        }
+        return obj;
+    }
 })();
 
 // Status / logging UI (ignore this)
 (function () {
-	var COLORS = this.COLORS = {
-		'GREEN': new Color("#2D870C"),
-		'RED': new Color("#AF1E07"),
-		'LIGHT_GRAY': new Color("#CCCCCC"),
-		'DARK_GRAY': new Color("#4E4E4E")
-	};
-	function buildDebugLog () {
-		var LINE_WIDTH = 400;
-		var LINE_HEIGHT = 20;
-	
-		var lines = [];
-		var lineIndex = 0;
-		for (var i = 0; i < NUM_DEBUG_LOG_LINES; ++i) {
-			lines.push(new UI.Label({ 
-				text: " ", visible: false, 
-				width: LINE_WIDTH, height: LINE_HEIGHT,
-		 	}));
-		}
-		var title = new UI.Label({
-			text: SCRIPT_NAME, visible: true,
-			width: LINE_WIDTH, height: LINE_HEIGHT,
-		});
-	
-		var overlay = new UI.Box({
-			visible: true,
-			width: LINE_WIDTH, height: 0,
-			backgroundColor: COLORS.DARK_GRAY,
-			backgroundAlpha: 0.3
-		});
-		overlay.setPosition(280, 10);
-		relayoutFrom(0);
-		UI.updateLayout();
-	
-		function relayoutFrom (n) {
-			var layoutPos = {
-				x: overlay.position.x,
-				y: overlay.position.y
-			};
-	
-			title.setPosition(layoutPos.x, layoutPos.y);
-			layoutPos.y += LINE_HEIGHT;
-	
-			// for (var i = n; i >= 0; --i) {
-			for (var i = n + 1; i < lines.length; ++i) {
-				if (lines[i].visible) {
-					lines[i].setPosition(layoutPos.x, layoutPos.y);
-					layoutPos.y += LINE_HEIGHT;
-				}
-			}
-			// for (var i = lines.length - 1; i > n; --i) {
-			for (var i = 0; i <= n; ++i) {
-				if (lines[i].visible) {
-					lines[i].setPosition(layoutPos.x, layoutPos.y);
-					layoutPos.y += LINE_HEIGHT;
-				}
-			}
-			overlay.height = (layoutPos.y - overlay.position.y + 10);
-			overlay.getOverlay().update({
-				height: overlay.height
-			});
-		}
-		this.logMessage = function (text, color, alpha) {
-			lines[lineIndex].setVisible(true);
-			relayoutFrom(lineIndex);
-	
-			lines[lineIndex].getOverlay().update({
-				text: text,
-				visible: true,
-				color: color || COLORS.LIGHT_GRAY,
-				alpha: alpha !== undefined ? alpha : 1.0,
-				x: lines[lineIndex].position.x,
-				y: lines[lineIndex].position.y
-			});
-			lineIndex = (lineIndex + 1) % lines.length;
-			UI.updateLayout();
-		}
-	}
-	if (USE_DEBUG_LOG) {
-		buildDebugLog();
-	} else {
-		this.logMessage = function (msg) {
-			print(SCRIPT_NAME + ": " + msg);
-		}
-	}
+    var COLORS = this.COLORS = {
+        'GREEN': new Color("#2D870C"),
+        'RED': new Color("#AF1E07"),
+        'LIGHT_GRAY': new Color("#CCCCCC"),
+        'DARK_GRAY': new Color("#4E4E4E")
+    };
+    function buildDebugLog () {
+        var LINE_WIDTH = 400;
+        var LINE_HEIGHT = 20;
+    
+        var lines = [];
+        var lineIndex = 0;
+        for (var i = 0; i < NUM_DEBUG_LOG_LINES; ++i) {
+            lines.push(new UI.Label({ 
+                text: " ", visible: false, 
+                width: LINE_WIDTH, height: LINE_HEIGHT,
+            }));
+        }
+        var title = new UI.Label({
+            text: SCRIPT_NAME, visible: true,
+            width: LINE_WIDTH, height: LINE_HEIGHT,
+        });
+    
+        var overlay = new UI.Box({
+            visible: true,
+            width: LINE_WIDTH, height: 0,
+            backgroundColor: COLORS.DARK_GRAY,
+            backgroundAlpha: 0.3
+        });
+        overlay.setPosition(280, 10);
+        relayoutFrom(0);
+        UI.updateLayout();
+    
+        function relayoutFrom (n) {
+            var layoutPos = {
+                x: overlay.position.x,
+                y: overlay.position.y
+            };
+    
+            title.setPosition(layoutPos.x, layoutPos.y);
+            layoutPos.y += LINE_HEIGHT;
+    
+            // for (var i = n; i >= 0; --i) {
+            for (var i = n + 1; i < lines.length; ++i) {
+                if (lines[i].visible) {
+                    lines[i].setPosition(layoutPos.x, layoutPos.y);
+                    layoutPos.y += LINE_HEIGHT;
+                }
+            }
+            // for (var i = lines.length - 1; i > n; --i) {
+            for (var i = 0; i <= n; ++i) {
+                if (lines[i].visible) {
+                    lines[i].setPosition(layoutPos.x, layoutPos.y);
+                    layoutPos.y += LINE_HEIGHT;
+                }
+            }
+            overlay.height = (layoutPos.y - overlay.position.y + 10);
+            overlay.getOverlay().update({
+                height: overlay.height
+            });
+        }
+        this.logMessage = function (text, color, alpha) {
+            lines[lineIndex].setVisible(true);
+            relayoutFrom(lineIndex);
+    
+            lines[lineIndex].getOverlay().update({
+                text: text,
+                visible: true,
+                color: color || COLORS.LIGHT_GRAY,
+                alpha: alpha !== undefined ? alpha : 1.0,
+                x: lines[lineIndex].position.x,
+                y: lines[lineIndex].position.y
+            });
+            lineIndex = (lineIndex + 1) % lines.length;
+            UI.updateLayout();
+        }
+    }
+    if (USE_DEBUG_LOG) {
+        buildDebugLog();
+    } else {
+        this.logMessage = function (msg) {
+            print(SCRIPT_NAME + ": " + msg);
+        }
+    }
 })();
 
 // Utils (ignore)
 (function () {
-	// Utility function
-	var withDefaults = this.withDefaults = function (properties, defaults) {
-		// logMessage("withDefaults: " + JSON.stringify(properties) + JSON.stringify(defaults));
-		properties = properties || {};
-		if (defaults) {
-			for (var k in defaults) {
-				properties[k] = defaults[k];
-			}
-		}
-		return properties;
-	}
-	var withReadonlyProp = this.withReadonlyProp = function (propname, value, obj) {
-		Object.defineProperty(obj, propname, {
-			value: value,
-			writable: false
-		});
-		return obj;
-	}
+    // Utility function
+    var withDefaults = this.withDefaults = function (properties, defaults) {
+        // logMessage("withDefaults: " + JSON.stringify(properties) + JSON.stringify(defaults));
+        properties = properties || {};
+        if (defaults) {
+            for (var k in defaults) {
+                properties[k] = defaults[k];
+            }
+        }
+        return properties;
+    }
+    var withReadonlyProp = this.withReadonlyProp = function (propname, value, obj) {
+        Object.defineProperty(obj, propname, {
+            value: value,
+            writable: false
+        });
+        return obj;
+    }
 
-	// Math utils
-	if (typeof(Math.randRange) === 'undefined') {
-		Math.randRange = function (min, max) {
-			return Math.random() * (max - min) + min;
-		}
-	}
-	if (typeof(Math.randInt) === 'undefined') {
-		Math.randInt = function (n) {
-			return Math.floor(Math.random() * n) | 0;
-		}
-	}
+    // Math utils
+    if (typeof(Math.randRange) === 'undefined') {
+        Math.randRange = function (min, max) {
+            return Math.random() * (max - min) + min;
+        }
+    }
+    if (typeof(Math.randInt) === 'undefined') {
+        Math.randInt = function (n) {
+            return Math.floor(Math.random() * n) | 0;
+        }
+    }
 
-	/// Random distrib: Get a random point within a circle on the xz plane with radius r, center p.
-	this.randomCirclePoint = function (r, pos) {
-		var a = Math.random(), b = Math.random();
-		if (b < a) {
-			var tmp = b;
-			b = a;
-			a = tmp;
-		}
-		var point = {
-			x: pos.x + b * r * Math.cos(2 * Math.PI * a / b),
-			y: pos.y,
-			z: pos.z + b * r * Math.sin(2 * Math.PI * a / b)
-		};
-		if (LOG_ENTITY_CREATION_MESSAGES) {
-			// logMessage("input params: " + JSON.stringify({ radius: r, position: pos }), COLORS.GREEN);
-			// logMessage("a = " + a + ", b = " + b);
-			logMessage("generated point: " + JSON.stringify(point), COLORS.RED);
-		}
-		return point;
-	}
+    /// Random distrib: Get a random point within a circle on the xz plane with radius r, center p.
+    this.randomCirclePoint = function (r, pos) {
+        var a = Math.random(), b = Math.random();
+        if (b < a) {
+            var tmp = b;
+            b = a;
+            a = tmp;
+        }
+        var point = {
+            x: pos.x + b * r * Math.cos(2 * Math.PI * a / b),
+            y: pos.y,
+            z: pos.z + b * r * Math.sin(2 * Math.PI * a / b)
+        };
+        if (LOG_ENTITY_CREATION_MESSAGES) {
+            // logMessage("input params: " + JSON.stringify({ radius: r, position: pos }), COLORS.GREEN);
+            // logMessage("a = " + a + ", b = " + b);
+            logMessage("generated point: " + JSON.stringify(point), COLORS.RED);
+        }
+        return point;
+    }
 
-	// Entity utils. NOT using overlayManager for... reasons >.>
-	var makeEntity = this.makeEntity = function (properties) {
-		if (LOG_ENTITY_CREATION_MESSAGES) {
-			logMessage("Creating entity: " + JSON.stringify(properties));
-		}
-		var entity = Entities.addEntity(properties);
-		return withReadonlyProp("type", properties.type, {
-			update: function (properties) {
-				Entities.editEntity(entity, properties);
-			},
-			destroy: function () {
-				Entities.deleteEntity(entity)
-			},
-			getId: function () {
-				return entity;
-			}
-		});
-	}
-	// this.makeLight = function (properties) {
-	// 	return makeEntity(withDefaults(properties, {
-	// 		type: "Light",
-	// 		isSpotlight: false,
-	// 		diffuseColor: { red: 255, green: 100, blue: 100 },
-	// 		ambientColor: { red: 200, green: 80, blue: 80 }
-	// 	}));
-	// }
-	this.makeBox = function (properties) {
-		// logMessage("Creating box: " + JSON.stringify(properties));
-		return makeEntity(withDefaults(properties, {
-			type: "Box"
-		}));
-	}
+    // Entity utils. NOT using overlayManager for... reasons >.>
+    var makeEntity = this.makeEntity = function (properties) {
+        if (LOG_ENTITY_CREATION_MESSAGES) {
+            logMessage("Creating entity: " + JSON.stringify(properties));
+        }
+        var entity = Entities.addEntity(properties);
+        return withReadonlyProp("type", properties.type, {
+            update: function (properties) {
+                Entities.editEntity(entity, properties);
+            },
+            destroy: function () {
+                Entities.deleteEntity(entity)
+            },
+            getId: function () {
+                return entity;
+            }
+        });
+    }
+    // this.makeLight = function (properties) {
+    //  return makeEntity(withDefaults(properties, {
+    //      type: "Light",
+    //      isSpotlight: false,
+    //      diffuseColor: { red: 255, green: 100, blue: 100 },
+    //      ambientColor: { red: 200, green: 80, blue: 80 }
+    //  }));
+    // }
+    this.makeBox = function (properties) {
+        // logMessage("Creating box: " + JSON.stringify(properties));
+        return makeEntity(withDefaults(properties, {
+            type: "Box"
+        }));
+    }
 })();
 
 // Platform
 (function () {
-	/// Encapsulates a platform 'piece'. Owns an entity (`box`), and handles destruction and some other state.
-	var PlatformComponent = this.PlatformComponent = function (properties) {
-		// logMessage("Platform component initialized with " + Object.keys(properties), COLORS.GREEN);
-		this.position 	= properties.position || null;
-		this.color 		= properties.color    || null;
-		this.dimensions = properties.dimensions || null;
-		this.entityType = properties.type || "Box";
+    /// Encapsulates a platform 'piece'. Owns an entity (`box`), and handles destruction and some other state.
+    var PlatformComponent = this.PlatformComponent = function (properties) {
+        // logMessage("Platform component initialized with " + Object.keys(properties), COLORS.GREEN);
+        this.position   = properties.position || null;
+        this.color      = properties.color    || null;
+        this.dimensions = properties.dimensions || null;
+        this.entityType = properties.type || "Box";
 
-		// logMessage("Spawning with type: '" + this.entityType + "' (properties.type = '" + properties.type + "')", COLORS.GREEN);
+        // logMessage("Spawning with type: '" + this.entityType + "' (properties.type = '" + properties.type + "')", COLORS.GREEN);
 
-		if (properties._colorSeed)
-			this._colorSeed = properties._colorSeed;
-		if (properties._shapeSeed)
-			this._shapeSeed = properties._shapeSeed;
+        if (properties._colorSeed)
+            this._colorSeed = properties._colorSeed;
+        if (properties._shapeSeed)
+            this._shapeSeed = properties._shapeSeed;
 
-		// logMessage("dimensions: " + JSON.stringify(this.dimensions));
-		// logMessage("color: " + JSON.stringify(this.color));
+        // logMessage("dimensions: " + JSON.stringify(this.dimensions));
+        // logMessage("color: " + JSON.stringify(this.color));
 
-		this.cachedEntity = null;
-		this.activeEntity = this.spawnEntity(this.entityType);
-	};
-	PlatformComponent.prototype.spawnEntity = function (type) {
-		return makeEntity({
-			type: type,
-			position: this.position,
-			dimensions: this.dimensions,
-			color: this.color,
-			lifetime: USE_ENTITY_TIMEOUTS ? ENTITY_TIMEOUT_DURATION : -1.0,
-			alpha: 0.5
-		});
-	}
-	if (USE_ENTITY_TIMEOUTS) {
-		PlatformComponent.prototype.pokeEntity = function () {
-			// Kinda inefficient, but there's no way to get around this :/
-			var age = Entities.getEntityProperties(this.activeEntity.getId()).age;
-			this.activeEntity.update({ lifetime: ENTITY_TIMEOUT_DURATION + age });
-		}
-	} else {
-		PlatformComponent.prototype.pokeEntity = function () {}
-	}
-	/// Updates platform to be at position p, and calls <entity>.update() with the current
-	/// position, color, and dimensions parameters.
-	PlatformComponent.prototype.update = function (position) {
-		if (position)
-			this.position = position;
-		// logMessage("updating with " + JSON.stringify(this));
-		this.activeEntity.update(this);
-	}
-	function swap (a, b) {
-		var tmp = a;
-		a = b;
-		b = tmp;
-	}	
-	PlatformComponent.prototype.swapEntityType = function (newType) {
-		if (this.entityType !== newType) {
-			this.entityType = newType;
-			// logMessage("Destroying active entity and rebuilding it (newtype = '" + newType + "')");
-			if (this.activeEntity) {
-				this.activeEntity.destroy();
-			}
-			this.activeEntity = this.spawnEntity(newType);
-			// if (this.cachedEntity && this.cachedEntity.type == newType) {
-			// 	this.cachedEntity.update({ visible: true });
-			// 	this.activeEntity.update({ visible: false });
-			// 	swap(this.cachedEntity, this.activeEntity);
-			// 	this.update(this.position);
-			// } else {
-			// 	this.activeEntity.update({ visible: false });
-			// 	this.cachedEntity = this.activeEntity;
-			// 	this.activeEntity = spawnEntity(newType);
-			// }
-		}
-	}
-	/// Swap state with another component
-	PlatformComponent.prototype.swap = function (other) {
-		swap(this.position, other.position);
-		swap(this.dimensions, other.dimensions);
-		swap(this.color, other.color);
-		swap(this.entityType, other.entityType);
-		swap(this.activeEntity, other.activeEntity);
-		swap(this._colorSeed, other._colorSeed);
-		swap(this._shapeSeed, other._shapeSeed);
-	}
-	PlatformComponent.prototype.destroy = function () {
-		if (this.activeEntity) {
-			this.activeEntity.destroy();
-			this.activeEntity = null;
-		}
-		if (this.cachedEntity) {
-			this.cachedEntity.destroy();
-			this.cachedEntity = null;
-		}
-	}
+        this.cachedEntity = null;
+        this.activeEntity = this.spawnEntity(this.entityType);
+    };
+    PlatformComponent.prototype.spawnEntity = function (type) {
+        return makeEntity({
+            type: type,
+            position: this.position,
+            dimensions: this.dimensions,
+            color: this.color,
+            lifetime: USE_ENTITY_TIMEOUTS ? ENTITY_TIMEOUT_DURATION : -1.0,
+            alpha: 0.5
+        });
+    }
+    if (USE_ENTITY_TIMEOUTS) {
+        PlatformComponent.prototype.pokeEntity = function () {
+            // Kinda inefficient, but there's no way to get around this :/
+            var age = Entities.getEntityProperties(this.activeEntity.getId()).age;
+            this.activeEntity.update({ lifetime: ENTITY_TIMEOUT_DURATION + age });
+        }
+    } else {
+        PlatformComponent.prototype.pokeEntity = function () {}
+    }
+    /// Updates platform to be at position p, and calls <entity>.update() with the current
+    /// position, color, and dimensions parameters.
+    PlatformComponent.prototype.update = function (position) {
+        if (position)
+            this.position = position;
+        // logMessage("updating with " + JSON.stringify(this));
+        this.activeEntity.update(this);
+    }
+    function swap (a, b) {
+        var tmp = a;
+        a = b;
+        b = tmp;
+    }   
+    PlatformComponent.prototype.swapEntityType = function (newType) {
+        if (this.entityType !== newType) {
+            this.entityType = newType;
+            // logMessage("Destroying active entity and rebuilding it (newtype = '" + newType + "')");
+            if (this.activeEntity) {
+                this.activeEntity.destroy();
+            }
+            this.activeEntity = this.spawnEntity(newType);
+            // if (this.cachedEntity && this.cachedEntity.type == newType) {
+            //  this.cachedEntity.update({ visible: true });
+            //  this.activeEntity.update({ visible: false });
+            //  swap(this.cachedEntity, this.activeEntity);
+            //  this.update(this.position);
+            // } else {
+            //  this.activeEntity.update({ visible: false });
+            //  this.cachedEntity = this.activeEntity;
+            //  this.activeEntity = spawnEntity(newType);
+            // }
+        }
+    }
+    /// Swap state with another component
+    PlatformComponent.prototype.swap = function (other) {
+        swap(this.position, other.position);
+        swap(this.dimensions, other.dimensions);
+        swap(this.color, other.color);
+        swap(this.entityType, other.entityType);
+        swap(this.activeEntity, other.activeEntity);
+        swap(this._colorSeed, other._colorSeed);
+        swap(this._shapeSeed, other._shapeSeed);
+    }
+    PlatformComponent.prototype.destroy = function () {
+        if (this.activeEntity) {
+            this.activeEntity.destroy();
+            this.activeEntity = null;
+        }
+        if (this.cachedEntity) {
+            this.cachedEntity.destroy();
+            this.cachedEntity = null;
+        }
+    }
 
-	// util
-	function inRange (p1, p2, radius) {
-		return Vec3.distance(p1, p2) < Math.abs(radius);
-	}
+    // util
+    function inRange (p1, p2, radius) {
+        return Vec3.distance(p1, p2) < Math.abs(radius);
+    }
 
-	/// Encapsulates a moving platform that follows the avatar around (mostly).
-	var DynamicPlatform = this.DynamicPlatform = function (n, position, radius) {
-		this.position = position;
-		this.radius   = radius;
-		this.randomizer = new RandomAttribModel();
-		this.boxType = "Box";
-		this.boxTypes = [ "Box", "Sphere" ];
+    /// Encapsulates a moving platform that follows the avatar around (mostly).
+    var DynamicPlatform = this.DynamicPlatform = function (n, position, radius) {
+        this.position = position;
+        this.radius   = radius;
+        this.randomizer = new RandomAttribModel();
+        this.boxType = "Box";
+        this.boxTypes = [ "Box", "Sphere" ];
 
-		logMessage("Spawning " + n + " entities", COLORS.GREEN);
-		var boxes = this.boxes = [];
-		while (n > 0) {
-			boxes.push(this.spawnEntity());
-			--n;
-		}
-		this.targetDensity = this.getEntityDensity();
-		this.pendingUpdates = {};
-		this.updateTimer = 0.0;
+        logMessage("Spawning " + n + " entities", COLORS.GREEN);
+        var boxes = this.boxes = [];
+        while (n > 0) {
+            boxes.push(this.spawnEntity());
+            --n;
+        }
+        this.targetDensity = this.getEntityDensity();
+        this.pendingUpdates = {};
+        this.updateTimer = 0.0;
 
-		this.platformHeight = position.y;
-		this.oldPos    = { x: position.x, y: position.y, z: position.z };
-		this.oldRadius = radius;
+        this.platformHeight = position.y;
+        this.oldPos    = { x: position.x, y: position.y, z: position.z };
+        this.oldRadius = radius;
 
-		// this.sendPokes();
-	}
-	DynamicPlatform.prototype.toString = function () {
-		return "[DynamicPlatform (" + this.boxes.length + " entities)]";
-	}
-	DynamicPlatform.prototype.spawnEntity = function () {
-		// logMessage("Called spawn entity. this.boxType = '" + this.boxType + "'")
-		var properties = { position: this.randomPoint(), type: this.boxType };
-		this.randomizer.randomizeShapeAndColor(properties);
-		return new PlatformComponent(properties);
-	}
-	DynamicPlatform.prototype.updateEntityAttribs = function () {
-		var _this = this;
-		this.setPendingUpdate('updateEntityAttribs', function () {
-			// logMessage("updating model", COLORS.GREEN);
-			_this.boxes.forEach(function (box) {
-				this.randomizer.updateShapeAndColor(box);
-				box.update();
-			}, _this);
-		});
-	}
-	DynamicPlatform.prototype.toggleBoxType = function () {
-		var _this = this;
-		this.setPendingUpdate('toggleBoxType', function () {
-			// Swap / cycle through types: find index of current type and set next type to idx+1
-			for (var idx = 0; idx < _this.boxTypes.length; ++idx) {
-				if (_this.boxTypes[idx] === _this.boxType) {
-					var nextIndex = (idx + 1) % _this.boxTypes.length;
-					logMessage("swapping box type from '" + _this.boxType + "' to '" + _this.boxTypes[nextIndex] + "'", COLORS.GREEN);
-					_this.boxType = _this.boxTypes[nextIndex];
-					break;
-				}
-			}
-			_this.boxes.forEach(function (box) {
-				box.swapEntityType(_this.boxType);
-			}, _this);
-		});
-	}
-	DynamicPlatform.prototype.getBoxType = function () {
-		return this.boxType;
-	}
+        // this.sendPokes();
+    }
+    DynamicPlatform.prototype.toString = function () {
+        return "[DynamicPlatform (" + this.boxes.length + " entities)]";
+    }
+    DynamicPlatform.prototype.spawnEntity = function () {
+        // logMessage("Called spawn entity. this.boxType = '" + this.boxType + "'")
+        var properties = { position: this.randomPoint(), type: this.boxType };
+        this.randomizer.randomizeShapeAndColor(properties);
+        return new PlatformComponent(properties);
+    }
+    DynamicPlatform.prototype.updateEntityAttribs = function () {
+        var _this = this;
+        this.setPendingUpdate('updateEntityAttribs', function () {
+            // logMessage("updating model", COLORS.GREEN);
+            _this.boxes.forEach(function (box) {
+                this.randomizer.updateShapeAndColor(box);
+                box.update();
+            }, _this);
+        });
+    }
+    DynamicPlatform.prototype.toggleBoxType = function () {
+        var _this = this;
+        this.setPendingUpdate('toggleBoxType', function () {
+            // Swap / cycle through types: find index of current type and set next type to idx+1
+            for (var idx = 0; idx < _this.boxTypes.length; ++idx) {
+                if (_this.boxTypes[idx] === _this.boxType) {
+                    var nextIndex = (idx + 1) % _this.boxTypes.length;
+                    logMessage("swapping box type from '" + _this.boxType + "' to '" + _this.boxTypes[nextIndex] + "'", COLORS.GREEN);
+                    _this.boxType = _this.boxTypes[nextIndex];
+                    break;
+                }
+            }
+            _this.boxes.forEach(function (box) {
+                box.swapEntityType(_this.boxType);
+            }, _this);
+        });
+    }
+    DynamicPlatform.prototype.getBoxType = function () {
+        return this.boxType;
+    }
 
-	// if (USE_ENTITY_TIMEOUTS) {
-	// 	DynamicPlatform.prototype.sendPokes = function () {
-	// 		var _this = this;
-	// 		function poke () {
-	// 			logMessage("Poking entities so they don't die", COLORS.GREEN);
-	// 			_this.boxes.forEach(function (box) {
-	// 				box.pokeEntity();
-	// 			}, _this);
+    // if (USE_ENTITY_TIMEOUTS) {
+    //  DynamicPlatform.prototype.sendPokes = function () {
+    //      var _this = this;
+    //      function poke () {
+    //          logMessage("Poking entities so they don't die", COLORS.GREEN);
+    //          _this.boxes.forEach(function (box) {
+    //              box.pokeEntity();
+    //          }, _this);
 
 
-	// 			if (_this.pendingUpdates['keepalive']) {
-	// 				logMessage("previous timer: " + _this.pendingUpdates['keepalive'].timer + "; new timer: " + ENTITY_REFRESH_INTERVAL)
-	// 			}
-	// 			_this.pendingUpdates['keepalive'] = {
-	// 				callback: poke,
-	// 				timer: ENTITY_REFRESH_INTERVAL,
-	// 				skippedUpdates: 0
-	// 			};
-	// 			// _this.setPendingUpdate('keepalive', poke);
-	// 			// _this.pendingUpdates['keepalive'].timer = ENTITY_REFRESH_INTERVAL;
-	// 		}
-	// 		poke();
-	// 	}
-	// } else {
-	// 	DynamicPlatform.prototype.sendPokes = function () {};
-	// }
+    //          if (_this.pendingUpdates['keepalive']) {
+    //              logMessage("previous timer: " + _this.pendingUpdates['keepalive'].timer + "; new timer: " + ENTITY_REFRESH_INTERVAL)
+    //          }
+    //          _this.pendingUpdates['keepalive'] = {
+    //              callback: poke,
+    //              timer: ENTITY_REFRESH_INTERVAL,
+    //              skippedUpdates: 0
+    //          };
+    //          // _this.setPendingUpdate('keepalive', poke);
+    //          // _this.pendingUpdates['keepalive'].timer = ENTITY_REFRESH_INTERVAL;
+    //      }
+    //      poke();
+    //  }
+    // } else {
+    //  DynamicPlatform.prototype.sendPokes = function () {};
+    // }
 
-	/// Queue impl that uses the update loop to limit potentially expensive updates to only execute every x seconds (default: 200 ms).
-	/// This is to prevent UI code from running full entity updates every 10 ms (or whatever).
-	DynamicPlatform.prototype.setPendingUpdate = function (name, callback) {
-		if (!this.pendingUpdates[name]) {
-			// logMessage("Queued update for " + name, COLORS.GREEN);
-			this.pendingUpdates[name] = {
-				callback: callback,
-				timer: 0.0,
-				skippedUpdates: 0
-			}
-		} else {
-			// logMessage("Deferred update for " + name, COLORS.GREEN);
-			this.pendingUpdates[name].callback = callback;
-			this.pendingUpdates[name].skippedUpdates++;
-			// logMessage("scheduling update for \"" + name + "\" to run in " + this.pendingUpdates[name].timer + " seconds");
-		}
-	}
-	/// Runs all queued updates as soon as they can execute (each one has a cooldown timer).
-	DynamicPlatform.prototype.processPendingUpdates = function (dt) {
-		for (var k in this.pendingUpdates) {
-			if (this.pendingUpdates[k].timer >= 0.0)
-				this.pendingUpdates[k].timer -= dt;
+    /// Queue impl that uses the update loop to limit potentially expensive updates to only execute every x seconds (default: 200 ms).
+    /// This is to prevent UI code from running full entity updates every 10 ms (or whatever).
+    DynamicPlatform.prototype.setPendingUpdate = function (name, callback) {
+        if (!this.pendingUpdates[name]) {
+            // logMessage("Queued update for " + name, COLORS.GREEN);
+            this.pendingUpdates[name] = {
+                callback: callback,
+                timer: 0.0,
+                skippedUpdates: 0
+            }
+        } else {
+            // logMessage("Deferred update for " + name, COLORS.GREEN);
+            this.pendingUpdates[name].callback = callback;
+            this.pendingUpdates[name].skippedUpdates++;
+            // logMessage("scheduling update for \"" + name + "\" to run in " + this.pendingUpdates[name].timer + " seconds");
+        }
+    }
+    /// Runs all queued updates as soon as they can execute (each one has a cooldown timer).
+    DynamicPlatform.prototype.processPendingUpdates = function (dt) {
+        for (var k in this.pendingUpdates) {
+            if (this.pendingUpdates[k].timer >= 0.0)
+                this.pendingUpdates[k].timer -= dt;
 
-			if (this.pendingUpdates[k].callback && this.pendingUpdates[k].timer < 0.0) {
-				// logMessage("Dispatching update for " + k);
-				try {
-					this.pendingUpdates[k].callback();
-				} catch (e) {
-					logMessage("update for \"" + k + "\" failed: " + e, COLORS.RED);
-				}
-				this.pendingUpdates[k].timer = MAX_UPDATE_INTERVAL;
-				this.pendingUpdates[k].skippedUpdates = 0;
-				this.pendingUpdates[k].callback = null;
-			} else {
-				// logMessage("Deferred update for " + k + " for " + this.pendingUpdates[k].timer + " seconds");
-			}
-		}
-	}
+            if (this.pendingUpdates[k].callback && this.pendingUpdates[k].timer < 0.0) {
+                // logMessage("Dispatching update for " + k);
+                try {
+                    this.pendingUpdates[k].callback();
+                } catch (e) {
+                    logMessage("update for \"" + k + "\" failed: " + e, COLORS.RED);
+                }
+                this.pendingUpdates[k].timer = MAX_UPDATE_INTERVAL;
+                this.pendingUpdates[k].skippedUpdates = 0;
+                this.pendingUpdates[k].callback = null;
+            } else {
+                // logMessage("Deferred update for " + k + " for " + this.pendingUpdates[k].timer + " seconds");
+            }
+        }
+    }
 
-	/// Updates the platform based on the avatar's current position (spawning / despawning entities as needed),
-	/// and calls processPendingUpdates() once this is done.
-	/// Does NOT have any update interval limits (it just updates every time it gets run), but these are not full
-	/// updates (they're incremental), so the network will not get flooded so long as the avatar is moving at a
-	/// normal walking / flying speed.
-	DynamicPlatform.prototype.updatePosition = function (dt, position) {
-		// logMessage("updating " + this);
-		position.y = this.platformHeight;
-		this.position = position;
+    /// Updates the platform based on the avatar's current position (spawning / despawning entities as needed),
+    /// and calls processPendingUpdates() once this is done.
+    /// Does NOT have any update interval limits (it just updates every time it gets run), but these are not full
+    /// updates (they're incremental), so the network will not get flooded so long as the avatar is moving at a
+    /// normal walking / flying speed.
+    DynamicPlatform.prototype.updatePosition = function (dt, position) {
+        // logMessage("updating " + this);
+        position.y = this.platformHeight;
+        this.position = position;
 
-		var toUpdate = [];
-		this.boxes.forEach(function (box, i) {
-			// if (Math.abs(box.position.y - position.y) > HEIGHT_TOLERANCE || !inRange(box, position, radius)) {
-			if (!inRange(box.position, this.position, this.radius)) {
-				toUpdate.push(i);
-			}
-		}, this);
+        var toUpdate = [];
+        this.boxes.forEach(function (box, i) {
+            // if (Math.abs(box.position.y - position.y) > HEIGHT_TOLERANCE || !inRange(box, position, radius)) {
+            if (!inRange(box.position, this.position, this.radius)) {
+                toUpdate.push(i);
+            }
+        }, this);
 
-		var MAX_TRIES = toUpdate.length * 8;
-		var tries = MAX_TRIES;
-		var moved = 0;
-		var recalcs = 0;
-		toUpdate.forEach(function (index) {
-			if ((index % 2 == 0) || tries > 0) {
-				do {
-					var randomPoint = this.randomPoint(this.position, this.radius);
-					++recalcs
-				} while (--tries > 0 && inRange(randomPoint, this.oldPos, this.oldRadiuss));
-	
-				if (LOG_UPDATE_STATUS_MESSAGES && tries <= 0) {
-					logMessage("updatePlatform() gave up after " + MAX_TRIES + " iterations (" + moved + " / " + toUpdate.length + " successful updates)", COLORS.RED);
-					logMessage("old pos: " + JSON.stringify(this.oldPos) + ", old radius: " + this.oldRadius);
-					logMessage("new pos: " + JSON.stringify(this.position) + ", new radius: " + this.radius);
-				}
-			} else {
-				var randomPoint = this.randomPoint(position, this.radius);
-			}
+        var MAX_TRIES = toUpdate.length * 8;
+        var tries = MAX_TRIES;
+        var moved = 0;
+        var recalcs = 0;
+        toUpdate.forEach(function (index) {
+            if ((index % 2 == 0) || tries > 0) {
+                do {
+                    var randomPoint = this.randomPoint(this.position, this.radius);
+                    ++recalcs
+                } while (--tries > 0 && inRange(randomPoint, this.oldPos, this.oldRadiuss));
+    
+                if (LOG_UPDATE_STATUS_MESSAGES && tries <= 0) {
+                    logMessage("updatePlatform() gave up after " + MAX_TRIES + " iterations (" + moved + " / " + toUpdate.length + " successful updates)", COLORS.RED);
+                    logMessage("old pos: " + JSON.stringify(this.oldPos) + ", old radius: " + this.oldRadius);
+                    logMessage("new pos: " + JSON.stringify(this.position) + ", new radius: " + this.radius);
+                }
+            } else {
+                var randomPoint = this.randomPoint(position, this.radius);
+            }
 
-			this.randomizer.randomizeShapeAndColor(this.boxes[index]);
-			this.boxes[index].update(randomPoint);
-			// this.boxes[index].setValues({
-			// 	position: randomPoint,
-			// 	// dimensions: this.randomDimensions(),
-			// 	// color: this.randomColor()
-			// });
-			++moved;
-		}, this);
-		recalcs = recalcs - toUpdate.length;
+            this.randomizer.randomizeShapeAndColor(this.boxes[index]);
+            this.boxes[index].update(randomPoint);
+            // this.boxes[index].setValues({
+            //  position: randomPoint,
+            //  // dimensions: this.randomDimensions(),
+            //  // color: this.randomColor()
+            // });
+            ++moved;
+        }, this);
+        recalcs = recalcs - toUpdate.length;
 
-		this.oldPos    = position;
-		this.oldRadius = this.radius;
-		if (LOG_UPDATE_STATUS_MESSAGES && toUpdate.length > 0) {
-			logMessage("updated " + toUpdate.length + " entities w/ " + recalcs + " recalcs");
-		}
-	}
+        this.oldPos    = position;
+        this.oldRadius = this.radius;
+        if (LOG_UPDATE_STATUS_MESSAGES && toUpdate.length > 0) {
+            logMessage("updated " + toUpdate.length + " entities w/ " + recalcs + " recalcs");
+        }
+    }
 
-	DynamicPlatform.prototype.update = function (dt, position) {
-		this.updatePosition(dt, position);
-		this.processPendingUpdates(dt);
-		this.sendPokes(dt);
-	}
+    DynamicPlatform.prototype.update = function (dt, position) {
+        this.updatePosition(dt, position);
+        this.processPendingUpdates(dt);
+        this.sendPokes(dt);
+    }
 
-	if (USE_ENTITY_TIMEOUTS) {
-		DynamicPlatform.prototype.sendPokes = function (dt) {
-			logMessage("starting keepalive", COLORS.GREEN);
-			// logMessage("dt = " + dt, COLORS.RED);
-			// var original = this.sendPokes;
-			var pokeTimer = 0.0;
-			this.sendPokes = function (dt) {
-				// logMessage("dt = " + dt);
-				if ((pokeTimer -= dt) < 0.0) {
-					// logMessage("Poking entities so they don't die", COLORS.GREEN);
-					this.boxes.forEach(function (box) {
-						box.pokeEntity();
-					}, this);
-					pokeTimer = ENTITY_REFRESH_INTERVAL;
-				} else {
-					// logMessage("Poking entities in " + pokeTimer + " seconds");
-				}
-			}
-			// logMessage("this.sendPokes === past this.sendPokes? " + (this.sendPokes === original), COLORS.GREEN);
-			this.sendPokes(dt);
-		}
-	} else {
-		DynamicPlatform.prototype.sendPokes = function () {};
-	}
-	DynamicPlatform.prototype.getEntityCount = function () {
-		return this.boxes.length;
-	}
-	DynamicPlatform.prototype.getEntityCountWithRadius = function (radius) {
-		var est = Math.floor((radius * radius) / (this.radius * this.radius) * this.getEntityCount());
-		var actual = Math.floor(Math.PI * radius * radius * this.getEntityDensity());
+    if (USE_ENTITY_TIMEOUTS) {
+        DynamicPlatform.prototype.sendPokes = function (dt) {
+            logMessage("starting keepalive", COLORS.GREEN);
+            // logMessage("dt = " + dt, COLORS.RED);
+            // var original = this.sendPokes;
+            var pokeTimer = 0.0;
+            this.sendPokes = function (dt) {
+                // logMessage("dt = " + dt);
+                if ((pokeTimer -= dt) < 0.0) {
+                    // logMessage("Poking entities so they don't die", COLORS.GREEN);
+                    this.boxes.forEach(function (box) {
+                        box.pokeEntity();
+                    }, this);
+                    pokeTimer = ENTITY_REFRESH_INTERVAL;
+                } else {
+                    // logMessage("Poking entities in " + pokeTimer + " seconds");
+                }
+            }
+            // logMessage("this.sendPokes === past this.sendPokes? " + (this.sendPokes === original), COLORS.GREEN);
+            this.sendPokes(dt);
+        }
+    } else {
+        DynamicPlatform.prototype.sendPokes = function () {};
+    }
+    DynamicPlatform.prototype.getEntityCount = function () {
+        return this.boxes.length;
+    }
+    DynamicPlatform.prototype.getEntityCountWithRadius = function (radius) {
+        var est = Math.floor((radius * radius) / (this.radius * this.radius) * this.getEntityCount());
+        var actual = Math.floor(Math.PI * radius * radius * this.getEntityDensity());
 
-		if (est != actual) {
-			logMessage("assert failed: getEntityCountWithRadius() -- est " + est + " != actual " + actual);
-		}
-		return est;
-	}
-	DynamicPlatform.prototype.getEntityCountWithDensity = function (density) {
-		return Math.floor(Math.PI * this.radius * this.radius * density);
-	}
+        if (est != actual) {
+            logMessage("assert failed: getEntityCountWithRadius() -- est " + est + " != actual " + actual);
+        }
+        return est;
+    }
+    DynamicPlatform.prototype.getEntityCountWithDensity = function (density) {
+        return Math.floor(Math.PI * this.radius * this.radius * density);
+    }
 
-	/// Sets the entity count to n. Don't call this directly -- use setRadius / density instead.
-	DynamicPlatform.prototype.setEntityCount = function (n) {
-		if (n > this.boxes.length) {
-			// logMessage("Setting entity count to " + n + " (adding " + (n - this.boxes.length) + " entities)", COLORS.GREEN);
+    /// Sets the entity count to n. Don't call this directly -- use setRadius / density instead.
+    DynamicPlatform.prototype.setEntityCount = function (n) {
+        if (n > this.boxes.length) {
+            // logMessage("Setting entity count to " + n + " (adding " + (n - this.boxes.length) + " entities)", COLORS.GREEN);
 
-			// Spawn new boxes
-			n = n - this.boxes.length;
-			for (; n > 0; --n) {
-				// var properties = { position: this.randomPoint() };
-				// this.randomizer.randomizeShapeAndColor(properties);
-				// this.boxes.push(new PlatformComponent(properties));
-				this.boxes.push(this.spawnEntity());
-			}
-		} else if (n < this.boxes.length) {
-			// logMessage("Setting entity count to " + n + " (removing " + (this.boxes.length - n) + " entities)", COLORS.GREEN);
+            // Spawn new boxes
+            n = n - this.boxes.length;
+            for (; n > 0; --n) {
+                // var properties = { position: this.randomPoint() };
+                // this.randomizer.randomizeShapeAndColor(properties);
+                // this.boxes.push(new PlatformComponent(properties));
+                this.boxes.push(this.spawnEntity());
+            }
+        } else if (n < this.boxes.length) {
+            // logMessage("Setting entity count to " + n + " (removing " + (this.boxes.length - n) + " entities)", COLORS.GREEN);
 
-			// Destroy random boxes (technically, the most recent ones, but it should be sorta random)
-			n = this.boxes.length - n;
-			for (; n > 0; --n) {
-				this.boxes.pop().destroy();
-			}
-		}
-	}
-	/// Calculate the entity density based on radial surface area.
-	DynamicPlatform.prototype.getEntityDensity = function () {
-		return (this.boxes.length * 1.0) / (Math.PI * this.radius * this.radius);
-	}
-	/// Queues a setDensity update. This is expensive, so we don't call it directly from UI.
-	DynamicPlatform.prototype.setDensityOnNextUpdate = function (density) {
-		var _this = this;
-		this.targetDensity = density;
-		this.setPendingUpdate('density', function () {
-			_this.updateEntityDensity(density);
-		});
-	}
-	DynamicPlatform.prototype.updateEntityDensity = function (density) {
-		this.setEntityCount(Math.floor(density * Math.PI * this.radius * this.radius));
-	}
-	DynamicPlatform.prototype.getRadius = function () {
-		return this.radius;
-	}
-	/// Queues a setRadius update. This is expensive, so we don't call it directly from UI.
-	DynamicPlatform.prototype.setRadiusOnNextUpdate = function (radius) {
-		var _this = this;
-		this.setPendingUpdate('radius', function () {
-			_this.setRadius(radius);
-		});
-	}
-	var DEBUG_RADIUS_RECALC = false;
-	DynamicPlatform.prototype.setRadius = function (radius) {
-		if (radius < this.radius) {	// Reduce case
-			// logMessage("Setting radius to " + radius + " (shrink by " + (this.radius - radius) + ")", COLORS.GREEN );
-			this.radius = radius;
+            // Destroy random boxes (technically, the most recent ones, but it should be sorta random)
+            n = this.boxes.length - n;
+            for (; n > 0; --n) {
+                this.boxes.pop().destroy();
+            }
+        }
+    }
+    /// Calculate the entity density based on radial surface area.
+    DynamicPlatform.prototype.getEntityDensity = function () {
+        return (this.boxes.length * 1.0) / (Math.PI * this.radius * this.radius);
+    }
+    /// Queues a setDensity update. This is expensive, so we don't call it directly from UI.
+    DynamicPlatform.prototype.setDensityOnNextUpdate = function (density) {
+        var _this = this;
+        this.targetDensity = density;
+        this.setPendingUpdate('density', function () {
+            _this.updateEntityDensity(density);
+        });
+    }
+    DynamicPlatform.prototype.updateEntityDensity = function (density) {
+        this.setEntityCount(Math.floor(density * Math.PI * this.radius * this.radius));
+    }
+    DynamicPlatform.prototype.getRadius = function () {
+        return this.radius;
+    }
+    /// Queues a setRadius update. This is expensive, so we don't call it directly from UI.
+    DynamicPlatform.prototype.setRadiusOnNextUpdate = function (radius) {
+        var _this = this;
+        this.setPendingUpdate('radius', function () {
+            _this.setRadius(radius);
+        });
+    }
+    var DEBUG_RADIUS_RECALC = false;
+    DynamicPlatform.prototype.setRadius = function (radius) {
+        if (radius < this.radius) { // Reduce case
+            // logMessage("Setting radius to " + radius + " (shrink by " + (this.radius - radius) + ")", COLORS.GREEN );
+            this.radius = radius;
 
-			// Remove all entities outside of current bounds. Requires swapping, since we want to maintain a contiguous array.
-			// Algorithm: two pointers at front and back. We traverse fwd and back, swapping elems so that all entities in bounds
-			// are at the front of the array, and all entities out of bounds are at the back. We then pop + destroy all entities
-			// at the back to reduce the entity count.
-			var count = this.boxes.length;
-			var toDelete = 0;
-			var swapList = [];
-			if (DEBUG_RADIUS_RECALC) {
-				logMessage("starting at i = 0, j = " + (count - 1));
-			}
-			for (var i = 0, j = count - 1; i < j; ) {
-				// Find first elem outside of bounds that we can move to the back
-				while (inRange(this.boxes[i].position, this.position, this.radius) && i < j) {
-					++i;
-				}
-				// Find first elem in bounds that we can move to the front
-				while (!inRange(this.boxes[j].position, this.position, this.radius) && i < j) {
-					--j; ++toDelete;
-				}
-				if (i < j) {
-					// swapList.push([i, j]);
-					if (DEBUG_RADIUS_RECALC) {
-						logMessage("swapping " + i + ", " + j);	
-					}
-					this.boxes[i].swap(this.boxes[j]);
-					++i, --j; ++toDelete;
-				} else {
-					if (DEBUG_RADIUS_RECALC) {
-						logMessage("terminated at i = " + i + ", j = " + j, COLORS.RED);
-					}
-				}
-			}
-			if (DEBUG_RADIUS_RECALC) {
-				logMessage("toDelete = " + toDelete, COLORS.RED);
-			}
-			// Sanity check
-			if (toDelete > this.boxes.length) {
-				logMessage("Error: toDelete " + toDelete + " > entity count " + this.boxes.length + " (setRadius algorithm)", COLORS.RED);
-				toDelete = this.boxes.length;
-			}
-			if (toDelete > 0) {
-				// logMessage("Deleting " + toDelete + " entities as part of radius resize", COLORS.GREEN);
-			}
-			// Delete cleared boxes
-			for (; toDelete > 0; --toDelete) {
-				this.boxes.pop().destroy();
-			}
-			// fix entity density (just in case -- we may have uneven entity distribution)
-			this.updateEntityDensity(this.targetDensity);
-		} else if (radius > this.radius) {
-			// Grow case (much simpler)
-			// logMessage("Setting radius to " + radius + " (grow by " + (radius - this.radius) + ")", COLORS.GREEN);
+            // Remove all entities outside of current bounds. Requires swapping, since we want to maintain a contiguous array.
+            // Algorithm: two pointers at front and back. We traverse fwd and back, swapping elems so that all entities in bounds
+            // are at the front of the array, and all entities out of bounds are at the back. We then pop + destroy all entities
+            // at the back to reduce the entity count.
+            var count = this.boxes.length;
+            var toDelete = 0;
+            var swapList = [];
+            if (DEBUG_RADIUS_RECALC) {
+                logMessage("starting at i = 0, j = " + (count - 1));
+            }
+            for (var i = 0, j = count - 1; i < j; ) {
+                // Find first elem outside of bounds that we can move to the back
+                while (inRange(this.boxes[i].position, this.position, this.radius) && i < j) {
+                    ++i;
+                }
+                // Find first elem in bounds that we can move to the front
+                while (!inRange(this.boxes[j].position, this.position, this.radius) && i < j) {
+                    --j; ++toDelete;
+                }
+                if (i < j) {
+                    // swapList.push([i, j]);
+                    if (DEBUG_RADIUS_RECALC) {
+                        logMessage("swapping " + i + ", " + j); 
+                    }
+                    this.boxes[i].swap(this.boxes[j]);
+                    ++i, --j; ++toDelete;
+                } else {
+                    if (DEBUG_RADIUS_RECALC) {
+                        logMessage("terminated at i = " + i + ", j = " + j, COLORS.RED);
+                    }
+                }
+            }
+            if (DEBUG_RADIUS_RECALC) {
+                logMessage("toDelete = " + toDelete, COLORS.RED);
+            }
+            // Sanity check
+            if (toDelete > this.boxes.length) {
+                logMessage("Error: toDelete " + toDelete + " > entity count " + this.boxes.length + " (setRadius algorithm)", COLORS.RED);
+                toDelete = this.boxes.length;
+            }
+            if (toDelete > 0) {
+                // logMessage("Deleting " + toDelete + " entities as part of radius resize", COLORS.GREEN);
+            }
+            // Delete cleared boxes
+            for (; toDelete > 0; --toDelete) {
+                this.boxes.pop().destroy();
+            }
+            // fix entity density (just in case -- we may have uneven entity distribution)
+            this.updateEntityDensity(this.targetDensity);
+        } else if (radius > this.radius) {
+            // Grow case (much simpler)
+            // logMessage("Setting radius to " + radius + " (grow by " + (radius - this.radius) + ")", COLORS.GREEN);
 
-			// Add entities based on entity density
-			// var density = this.getEntityDensity();
-			var density = this.targetDensity;
-			var oldArea = Math.PI * this.radius * this.radius;
-			var n = Math.floor(density * Math.PI * (radius * radius - this.radius * this.radius));
+            // Add entities based on entity density
+            // var density = this.getEntityDensity();
+            var density = this.targetDensity;
+            var oldArea = Math.PI * this.radius * this.radius;
+            var n = Math.floor(density * Math.PI * (radius * radius - this.radius * this.radius));
 
-			if (n > 0) {
-				// logMessage("Adding " + n + " entities", COLORS.GREEN);
+            if (n > 0) {
+                // logMessage("Adding " + n + " entities", COLORS.GREEN);
 
-				// Add entities (we use a slightly different algorithm to place them in the area between two concentric circles.
-				// This is *slightly* less uniform (the reason we're not using this everywhere is entities would be tightly clustered
-				// at the platform center and become spread out as the radius increases), but the use-case here is just incremental 
-				// radius resizes and the user's not likely to notice the difference).
-				for (; n > 0; --n) {
-					var theta = Math.randRange(0.0, Math.PI * 2.0);
-					var r = Math.randRange(this.radius, radius);
-					// logMessage("theta = " + theta + ", r = " + r);
-					var pos = {
-						x: Math.cos(theta) * r + this.position.x,
-						y: this.position.y,
-						z: Math.sin(theta) * r + this.position.y
-					};
+                // Add entities (we use a slightly different algorithm to place them in the area between two concentric circles.
+                // This is *slightly* less uniform (the reason we're not using this everywhere is entities would be tightly clustered
+                // at the platform center and become spread out as the radius increases), but the use-case here is just incremental 
+                // radius resizes and the user's not likely to notice the difference).
+                for (; n > 0; --n) {
+                    var theta = Math.randRange(0.0, Math.PI * 2.0);
+                    var r = Math.randRange(this.radius, radius);
+                    // logMessage("theta = " + theta + ", r = " + r);
+                    var pos = {
+                        x: Math.cos(theta) * r + this.position.x,
+                        y: this.position.y,
+                        z: Math.sin(theta) * r + this.position.y
+                    };
 
-					// var properties = { position: pos };
-					// this.randomizer.randomizeShapeAndColor(properties);
-					// this.boxes.push(new PlatformComponent(properties));
-					this.boxes.push(this.spawnEntity());
-				}
-			}
-			this.radius = radius;
-		}
-	}
-	DynamicPlatform.prototype.updateHeight = function (height) {
-		logMessage("Setting platform height to " + height);
-		this.platformHeight = height;
+                    // var properties = { position: pos };
+                    // this.randomizer.randomizeShapeAndColor(properties);
+                    // this.boxes.push(new PlatformComponent(properties));
+                    this.boxes.push(this.spawnEntity());
+                }
+            }
+            this.radius = radius;
+        }
+    }
+    DynamicPlatform.prototype.updateHeight = function (height) {
+        logMessage("Setting platform height to " + height);
+        this.platformHeight = height;
 
-		// Invalidate current boxes to trigger a rebuild
-		this.boxes.forEach(function (box) {
-			box.position.x += this.oldRadius * 100;
-		});
-		// this.update(dt, position, radius);
-	}
-	/// Gets a random point within the platform bounds.
-	/// Should maybe get moved to the RandomAttribModel (would be much cleaner), but this works for now.
-	DynamicPlatform.prototype.randomPoint = function (position, radius) {
-		position = position || this.position;
-		radius = radius !== undefined ? radius : this.radius;
-		return randomCirclePoint(radius, position);
-	}
-	/// Old. The RandomAttribModel replaces this and enables realtime editing of the *****_RANGE params.
-	// DynamicPlatform.prototype.randomDimensions = function () {
-	// 	return {
-	// 		x: Math.randRange(WIDTH_RANGE[0], WIDTH_RANGE[1]),
-	// 		y: Math.randRange(HEIGHT_RANGE[0], HEIGHT_RANGE[1]),
-	// 		z: Math.randRange(DEPTH_RANGE[0], DEPTH_RANGE[1])
-	// 	};
-	// }
-	// DynamicPlatform.prototype.randomColor = function () {
-	// 	var shade = Math.randRange(SHADE_RANGE[0], SHADE_RANGE[1]);
-	// 	// var h = HUE_RANGE;
-	// 	return {
-	// 		red: 	shade + Math.randRange(RED_RANGE[0], RED_RANGE[1]) | 0,
-	// 		green: 	shade + Math.randRange(GREEN_RANGE[0], GREEN_RANGE[1]) | 0,
-	// 		blue: 	shade + Math.randRange(BLUE_RANGE[0], BLUE_RANGE[1]) | 0
-	// 	}
-	// 	// return COLORS[Math.randInt(COLORS.length)]
-	// }
+        // Invalidate current boxes to trigger a rebuild
+        this.boxes.forEach(function (box) {
+            box.position.x += this.oldRadius * 100;
+        });
+        // this.update(dt, position, radius);
+    }
+    /// Gets a random point within the platform bounds.
+    /// Should maybe get moved to the RandomAttribModel (would be much cleaner), but this works for now.
+    DynamicPlatform.prototype.randomPoint = function (position, radius) {
+        position = position || this.position;
+        radius = radius !== undefined ? radius : this.radius;
+        return randomCirclePoint(radius, position);
+    }
+    /// Old. The RandomAttribModel replaces this and enables realtime editing of the *****_RANGE params.
+    // DynamicPlatform.prototype.randomDimensions = function () {
+    //  return {
+    //      x: Math.randRange(WIDTH_RANGE[0], WIDTH_RANGE[1]),
+    //      y: Math.randRange(HEIGHT_RANGE[0], HEIGHT_RANGE[1]),
+    //      z: Math.randRange(DEPTH_RANGE[0], DEPTH_RANGE[1])
+    //  };
+    // }
+    // DynamicPlatform.prototype.randomColor = function () {
+    //  var shade = Math.randRange(SHADE_RANGE[0], SHADE_RANGE[1]);
+    //  // var h = HUE_RANGE;
+    //  return {
+    //      red:    shade + Math.randRange(RED_RANGE[0], RED_RANGE[1]) | 0,
+    //      green:  shade + Math.randRange(GREEN_RANGE[0], GREEN_RANGE[1]) | 0,
+    //      blue:   shade + Math.randRange(BLUE_RANGE[0], BLUE_RANGE[1]) | 0
+    //  }
+    //  // return COLORS[Math.randInt(COLORS.length)]
+    // }
 
-	/// Cleanup.
-	DynamicPlatform.prototype.destroy = function () {
-		this.boxes.forEach(function (box) {
-			box.destroy();
-		});
-		this.boxes = [];
-	}
+    /// Cleanup.
+    DynamicPlatform.prototype.destroy = function () {
+        this.boxes.forEach(function (box) {
+            box.destroy();
+        });
+        this.boxes = [];
+    }
 })();
 
 // UI
 (function () {
-	var CATCH_SETUP_ERRORS = true;
+    var CATCH_SETUP_ERRORS = true;
 
-	// Util functions for setting up widgets (the widget library is intended to be used like this)
-	function makePanel (dir, properties) {
-		return new UI.WidgetStack(withDefaults(properties, {
-			dir: dir
-		}));
-	}
-	function addSpacing (parent, width, height) {
-		parent.add(new UI.Box({
-			backgroundAlpha: 0.0,
-			width: width, height: height
-		}));
-	}
-	function addLabel (parent, text) {
-		return parent.add(new UI.Label({
-			text: text,
-			width: 200,
-			height: 20
-		}));
-	}
-	function addSlider (parent, label, min, max, getValue, onValueChanged) {
-		try {
-			var layout = parent.add(new UI.WidgetStack({ dir: "+x" }));
-			var textLabel = layout.add(new UI.Label({
-				text: label,
-				width: 130,
-				height: 20
-			}));
-			var valueLabel = layout.add(new UI.Label({
-				text: "" + (+getValue().toFixed(1)),
-				width: 60,
-				height: 20
-			}));
-			var slider = layout.add(new UI.Slider({
-				value: getValue(), minValue: min, maxValue: max,
-				width: 300, height: 20,
-				slider: {
-					width: 30,
-					height: 18
-				},
-				onValueChanged: function (value) {
-					valueLabel.setText("" + (+value.toFixed(1)));
-					onValueChanged(value, slider);
-					UI.updateLayout();
-				}
-			}));
-			return slider;
-		} catch (e) {
-			logMessage("" + e, COLORS.RED);
-			logMessage("parent: " + parent, COLORS.RED);
-			logMessage("label:  " + label, COLORS.RED);
-			logMessage("min:    " + min, COLORS.RED);
-			logMessage("max:    " + max, COLORS.RED);
-			logMessage("getValue: " + getValue, COLORS.RED);
-			logMessage("onValueChanged: " + onValueChanged, COLORS.RED);
-			throw e;
-		}
-	}
-	function addButton (parent, label, onClicked) {
-		var button = parent.add(new UI.Box({
-			text: label,
-			width: 160,
-			height: 26,
-			leftMargin: 8,
-			topMargin: 3
-		}));
-		button.addAction('onClick', onClicked);
-		return button;
-	}
-	function moveToBottomLeftScreenCorner (widget) {
-		var border = 5;
-		var pos = {
-			x: border,
-			y: Controller.getViewportDimensions().y - widget.getHeight() - border
-		};
-		if (widget.position.x != pos.x || widget.position.y != pos.y) {
-			widget.setPosition(pos.x, pos.y);
-			UI.updateLayout();
-		}
-	}
-	var _export = this;
+    // Util functions for setting up widgets (the widget library is intended to be used like this)
+    function makePanel (dir, properties) {
+        return new UI.WidgetStack(withDefaults(properties, {
+            dir: dir
+        }));
+    }
+    function addSpacing (parent, width, height) {
+        parent.add(new UI.Box({
+            backgroundAlpha: 0.0,
+            width: width, height: height
+        }));
+    }
+    function addLabel (parent, text) {
+        return parent.add(new UI.Label({
+            text: text,
+            width: 200,
+            height: 20
+        }));
+    }
+    function addSlider (parent, label, min, max, getValue, onValueChanged) {
+        try {
+            var layout = parent.add(new UI.WidgetStack({ dir: "+x" }));
+            var textLabel = layout.add(new UI.Label({
+                text: label,
+                width: 130,
+                height: 20
+            }));
+            var valueLabel = layout.add(new UI.Label({
+                text: "" + (+getValue().toFixed(1)),
+                width: 60,
+                height: 20
+            }));
+            var slider = layout.add(new UI.Slider({
+                value: getValue(), minValue: min, maxValue: max,
+                width: 300, height: 20,
+                slider: {
+                    width: 30,
+                    height: 18
+                },
+                onValueChanged: function (value) {
+                    valueLabel.setText("" + (+value.toFixed(1)));
+                    onValueChanged(value, slider);
+                    UI.updateLayout();
+                }
+            }));
+            return slider;
+        } catch (e) {
+            logMessage("" + e, COLORS.RED);
+            logMessage("parent: " + parent, COLORS.RED);
+            logMessage("label:  " + label, COLORS.RED);
+            logMessage("min:    " + min, COLORS.RED);
+            logMessage("max:    " + max, COLORS.RED);
+            logMessage("getValue: " + getValue, COLORS.RED);
+            logMessage("onValueChanged: " + onValueChanged, COLORS.RED);
+            throw e;
+        }
+    }
+    function addButton (parent, label, onClicked) {
+        var button = parent.add(new UI.Box({
+            text: label,
+            width: 160,
+            height: 26,
+            leftMargin: 8,
+            topMargin: 3
+        }));
+        button.addAction('onClick', onClicked);
+        return button;
+    }
+    function moveToBottomLeftScreenCorner (widget) {
+        var border = 5;
+        var pos = {
+            x: border,
+            y: Controller.getViewportDimensions().y - widget.getHeight() - border
+        };
+        if (widget.position.x != pos.x || widget.position.y != pos.y) {
+            widget.setPosition(pos.x, pos.y);
+            UI.updateLayout();
+        }
+    }
+    var _export = this;
 
-	/// Setup the UI. Creates a bunch of sliders for setting the platform radius, density, and entity color / shape properties.
-	/// The entityCount slider is readonly.
-	function _setupUI (platform) {
-		var layoutContainer = makePanel("+y", { visible: true });
-		// layoutContainer.setPosition(10, 280);
-		// makeDraggable(layoutContainer);
-		_export.onScreenResize = function () {
-			moveToBottomLeftScreenCorner(layoutContainer);
-		}
-		var topSection = layoutContainer.add(makePanel("+x")); addSpacing(layoutContainer, 1, 5);
-		var btmSection = layoutContainer.add(makePanel("+x"));
+    /// Setup the UI. Creates a bunch of sliders for setting the platform radius, density, and entity color / shape properties.
+    /// The entityCount slider is readonly.
+    function _setupUI (platform) {
+        var layoutContainer = makePanel("+y", { visible: false });
+        // layoutContainer.setPosition(10, 280);
+        // makeDraggable(layoutContainer);
+        _export.onScreenResize = function () {
+            moveToBottomLeftScreenCorner(layoutContainer);
+        }
+        var topSection = layoutContainer.add(makePanel("+x")); addSpacing(layoutContainer, 1, 5);
+        var btmSection = layoutContainer.add(makePanel("+x"));
 
-		var controls = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1);
-		var buttons  = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1);
+        var controls = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1);
+        var buttons  = topSection.add(makePanel("+y")); addSpacing(topSection, 20, 1);
 
-		var colorControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1);
-		var shapeControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1);
+        var colorControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1);
+        var shapeControls = btmSection.add(makePanel("+y")); addSpacing(btmSection, 20, 1);
 
-		// Top controls
-		addLabel(controls, "Platform (platform.js)");
-		controls.radiusSlider = addSlider(controls, "radius", PLATFORM_RADIUS_RANGE[0], PLATFORM_RADIUS_RANGE[1], function () { return platform.getRadius() },
-			function (value) { 
-				platform.setRadiusOnNextUpdate(value);
-				controls.entityCountSlider.setValue(platform.getEntityCountWithRadius(value));
-			});
-		addSpacing(controls, 1, 2);
-		controls.densitySlider = addSlider(controls, "entity density", PLATFORM_DENSITY_RANGE[0], PLATFORM_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, 
-			function (value) {
-				platform.setDensityOnNextUpdate(value);
-				controls.entityCountSlider.setValue(platform.getEntityCountWithDensity(value));
-			});
-		addSpacing(controls, 1, 2);
+        // Top controls
+        addLabel(controls, "Platform (platform.js)");
+        controls.radiusSlider = addSlider(controls, "radius", PLATFORM_RADIUS_RANGE[0], PLATFORM_RADIUS_RANGE[1], function () { return platform.getRadius() },
+            function (value) { 
+                platform.setRadiusOnNextUpdate(value);
+                controls.entityCountSlider.setValue(platform.getEntityCountWithRadius(value));
+            });
+        addSpacing(controls, 1, 2);
+        controls.densitySlider = addSlider(controls, "entity density", PLATFORM_DENSITY_RANGE[0], PLATFORM_DENSITY_RANGE[1], function () { return platform.getEntityDensity() }, 
+            function (value) {
+                platform.setDensityOnNextUpdate(value);
+                controls.entityCountSlider.setValue(platform.getEntityCountWithDensity(value));
+            });
+        addSpacing(controls, 1, 2);
 
-		var minEntities = Math.PI * PLATFORM_RADIUS_RANGE[0] * PLATFORM_RADIUS_RANGE[0] * PLATFORM_DENSITY_RANGE[0];
-		var maxEntities = Math.PI * PLATFORM_RADIUS_RANGE[1] * PLATFORM_RADIUS_RANGE[1] * PLATFORM_DENSITY_RANGE[1];
-		controls.entityCountSlider = addSlider(controls, "entity count", minEntities, maxEntities, function () { return platform.getEntityCount() },
-			function (value) {});
-		controls.entityCountSlider.actions = {}; // hack: make this slider readonly (clears all attached actions)
-		controls.entityCountSlider.slider.actions = {};
+        var minEntities = Math.PI * PLATFORM_RADIUS_RANGE[0] * PLATFORM_RADIUS_RANGE[0] * PLATFORM_DENSITY_RANGE[0];
+        var maxEntities = Math.PI * PLATFORM_RADIUS_RANGE[1] * PLATFORM_RADIUS_RANGE[1] * PLATFORM_DENSITY_RANGE[1];
+        controls.entityCountSlider = addSlider(controls, "entity count", minEntities, maxEntities, function () { return platform.getEntityCount() },
+            function (value) {});
+        controls.entityCountSlider.actions = {}; // hack: make this slider readonly (clears all attached actions)
+        controls.entityCountSlider.slider.actions = {};
 
-		// Buttons
-		addSpacing(buttons, 1, 22);
-		addButton(buttons, 'rebuild', function () {
-			platform.updateHeight(MyAvatar.position.y - AVATAR_HEIGHT_OFFSET);
-		});
-		addSpacing(buttons, 1, 2);
-		addButton(buttons, 'toggle entity type', function () {
-			platform.toggleBoxType();
-		});
-		
-		// Bottom controls
+        // Buttons
+        addSpacing(buttons, 1, 22);
+        addButton(buttons, 'rebuild', function () {
+            platform.updateHeight(MyAvatar.position.y - AVATAR_HEIGHT_OFFSET);
+        });
+        addSpacing(buttons, 1, 2);
+        addButton(buttons, 'toggle entity type', function () {
+            platform.toggleBoxType();
+        });
+        
+        // Bottom controls
 
-		// Iterate over controls (making sliders) for the RNG shape / dimensions model
-		platform.randomizer.shapeModel.setupUI(function (name, value, min, max, setValue) {
-			// logMessage("platform.randomizer.shapeModel." + name + " = " + value);
-			var internal = {
-				avg: (value[0] + value[1]) * 0.5,
-				range: Math.abs(value[0] - value[1])
-			};
-			// logMessage(JSON.stringify(internal), COLORS.GREEN);
-			addSlider(shapeControls, name + ' avg', min, max, function () { return internal.avg; }, function (value) {
-				internal.avg = value;
-				setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]);
-				platform.updateEntityAttribs();
-			});
-			addSpacing(shapeControls, 1, 2);
-			addSlider(shapeControls, name + ' range', min, max, function () { return internal.range }, function (value) {
-				internal.range = value;
-				setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]);
-				platform.updateEntityAttribs();
-			});
-			addSpacing(shapeControls, 1, 2);
-		});
-		// Do the same for the color model
-		platform.randomizer.colorModel.setupUI(function (name, value, min, max, setValue) {
-			// logMessage("platform.randomizer.colorModel." + name + " = " + value);
-			addSlider(colorControls, name, min, max, function () { return value; }, function (value) {
-				setValue(value);
-				platform.updateEntityAttribs();
-			});
-			addSpacing(colorControls, 1, 2);
-		});
-		
-		moveToBottomLeftScreenCorner(layoutContainer);
-		layoutContainer.setVisible(true);
-	}
-	this.setupUI = function (platform) {
-		if (CATCH_SETUP_ERRORS) {
-			try {
-				_setupUI(platform);
-			} catch (e) {
-				logMessage("Error setting up ui: " + e, COLORS.RED);
-			}
-		} else {
-			_setupUI(platform);
-		}
-	}
+        // Iterate over controls (making sliders) for the RNG shape / dimensions model
+        platform.randomizer.shapeModel.setupUI(function (name, value, min, max, setValue) {
+            // logMessage("platform.randomizer.shapeModel." + name + " = " + value);
+            var internal = {
+                avg: (value[0] + value[1]) * 0.5,
+                range: Math.abs(value[0] - value[1])
+            };
+            // logMessage(JSON.stringify(internal), COLORS.GREEN);
+            addSlider(shapeControls, name + ' avg', min, max, function () { return internal.avg; }, function (value) {
+                internal.avg = value;
+                setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]);
+                platform.updateEntityAttribs();
+            });
+            addSpacing(shapeControls, 1, 2);
+            addSlider(shapeControls, name + ' range', min, max, function () { return internal.range }, function (value) {
+                internal.range = value;
+                setValue([ internal.avg - internal.range * 0.5, internal.avg + internal.range * 0.5 ]);
+                platform.updateEntityAttribs();
+            });
+            addSpacing(shapeControls, 1, 2);
+        });
+        // Do the same for the color model
+        platform.randomizer.colorModel.setupUI(function (name, value, min, max, setValue) {
+            // logMessage("platform.randomizer.colorModel." + name + " = " + value);
+            addSlider(colorControls, name, min, max, function () { return value; }, function (value) {
+                setValue(value);
+                platform.updateEntityAttribs();
+            });
+            addSpacing(colorControls, 1, 2);
+        });
+        
+        moveToBottomLeftScreenCorner(layoutContainer);
+        layoutContainer.setVisible(true);
+    }
+    this.setupUI = function (platform) {
+        if (CATCH_SETUP_ERRORS) {
+            try {
+                _setupUI(platform);
+            } catch (e) {
+                logMessage("Error setting up ui: " + e, COLORS.RED);
+            }
+        } else {
+            _setupUI(platform);
+        }
+    }
 })();
 
 // Error handling w/ explicit try / catch blocks. Good for catching unexpected errors with the onscreen debugLog 
@@ -1092,119 +1092,130 @@ var CATCH_ERRORS_FROM_EVENT_UPDATES = false;
 
 // Setup everything
 (function () {
-	var doLater = null;
-	if (CATCH_ERRORS_FROM_EVENT_UPDATES) {
-		// Decorates a function w/ explicit error catching + printing to the debug log.
-		function catchErrors (fcn) {
-			return function () {
-				try {
-					fcn.apply(this, arguments);
-				} catch (e) {
-					logMessage('' + e, COLORS.RED);
-					logMessage("while calling " + fcn);
-					logMessage("Called by: " + arguments.callee.caller);
-				}
-			}
-		}
-		// We need to do this after the functions are registered...
-		doLater = function () {
-			// Intercept errors from functions called by Script.update and Script.ScriptEnding.
-			[ 'teardown', 'startup', 'update', 'initPlatform', 'setupUI' ].forEach(function (fcn) {
-				this[fcn] = catchErrors(this[fcn]);
-			});
-		};
-		// These need to be wrapped first though:
+    var doLater = null;
+    if (CATCH_ERRORS_FROM_EVENT_UPDATES) {
+        // Decorates a function w/ explicit error catching + printing to the debug log.
+        function catchErrors (fcn) {
+            return function () {
+                try {
+                    fcn.apply(this, arguments);
+                } catch (e) {
+                    logMessage('' + e, COLORS.RED);
+                    logMessage("while calling " + fcn);
+                    logMessage("Called by: " + arguments.callee.caller);
+                }
+            }
+        }
+        // We need to do this after the functions are registered...
+        doLater = function () {
+            // Intercept errors from functions called by Script.update and Script.ScriptEnding.
+            [ 'teardown', 'startup', 'update', 'initPlatform', 'setupUI' ].forEach(function (fcn) {
+                this[fcn] = catchErrors(this[fcn]);
+            });
+        };
+        // These need to be wrapped first though:
 
-		// Intercept errors from UI functions called by Controller.****Event.
-		[ 'handleMousePress', 'handleMouseMove', 'handleMouseRelease' ].forEach(function (fcn) {
-			UI[fcn] = catchErrors(UI[fcn]);
-		});
-	}
+        // Intercept errors from UI functions called by Controller.****Event.
+        [ 'handleMousePress', 'handleMouseMove', 'handleMouseRelease' ].forEach(function (fcn) {
+            UI[fcn] = catchErrors(UI[fcn]);
+        });
+    }
 
-	function getTargetPlatformPosition () {
-		var pos = MyAvatar.position;
-		pos.y -= AVATAR_HEIGHT_OFFSET;
-		return pos;
-	}
+    function getTargetPlatformPosition () {
+        var pos = MyAvatar.position;
+        pos.y -= AVATAR_HEIGHT_OFFSET;
+        return pos;
+    }
 
-	// Program state
-	var platform = this.platform = null;
-	var lastHeight = null;
+    // Program state
+    var platform = this.platform = null;
+    var lastHeight = null;
 
-	// Init
-	this.initPlatform = function () {
-		platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS);
-		lastHeight = getTargetPlatformPosition().y;
-	}
+    // Init
+    this.initPlatform = function () {
+        platform = new DynamicPlatform(NUM_PLATFORM_ENTITIES, getTargetPlatformPosition(), RADIUS);
+        lastHeight = getTargetPlatformPosition().y;
+    }
 
-	// Handle relative screen positioning (UI)
-	var lastDimensions = Controller.getViewportDimensions();
-	function checkScreenDimensions () {
-		var dimensions = Controller.getViewportDimensions();
-		if (dimensions.x != lastDimensions.x || dimensions.y != lastDimensions.y) {
-			onScreenResize(dimensions.x, dimensions.y);
-		}
-		lastDimensions = dimensions;
-	}
+    // Handle relative screen positioning (UI)
+    var lastDimensions = Controller.getViewportDimensions();
+    function checkScreenDimensions () {
+        var dimensions = Controller.getViewportDimensions();
+        if (dimensions.x != lastDimensions.x || dimensions.y != lastDimensions.y) {
+            onScreenResize(dimensions.x, dimensions.y);
+        }
+        lastDimensions = dimensions;
+    }
 
-	// Update
-	this.update = function (dt) {
-		checkScreenDimensions();
-		var pos = getTargetPlatformPosition();
-		platform.update(dt, getTargetPlatformPosition(), platform.getRadius());
-	}
+    // Update
+    this.update = function (dt) {
+        checkScreenDimensions();
+        var pos = getTargetPlatformPosition();
+        platform.update(dt, getTargetPlatformPosition(), platform.getRadius());
+    }
 
-	// Teardown
-	this.teardown = function () {
-		try {
-			platform.destroy();
-			UI.teardown();
+    // Teardown
+    this.teardown = function () {
+        try {
+            platform.destroy();
+            UI.teardown();
 
-			Controller.mousePressEvent.disconnect(UI.handleMousePress);
-			Controller.mouseMoveEvent.disconnect(UI.handleMouseMove);
-			Controller.mouseReleaseEvent.disconnect(UI.handleMouseRelease);
-		} catch (e) {
-			logMessage("" + e, COLORS.RED);
-		}
-	}
+            Controller.mousePressEvent.disconnect(UI.handleMousePress);
+            Controller.mouseMoveEvent.disconnect(UI.handleMouseMove);
+            Controller.mouseReleaseEvent.disconnect(UI.handleMouseRelease);
+        } catch (e) {
+            logMessage("" + e, COLORS.RED);
+        }
+    }
 
-	if (doLater) {
-		doLater();
-	}
+    if (doLater) {
+        doLater();
+    }
 
-	// Delays startup until / if entities can be spawned.
-	this.startup = function () {
-		if (Entities.canAdjustLocks() && Entities.canRez()) {
-			Script.update.disconnect(this.startup);
+    // Delays startup until / if entities can be spawned.
+    this.startup = function (dt) {
+        if (Entities.canAdjustLocks() && Entities.canRez()) {
+            Script.update.disconnect(this.startup);
 
-			function init () {
-				logMessage("initializing...");
-				
-				this.initPlatform();
+            function init () {
+                logMessage("initializing...");
+                
+                this.initPlatform();
 
-				Script.update.connect(this.update);
-				Script.scriptEnding.connect(this.teardown);
-	
-				this.setupUI(platform);
-	
-				logMessage("finished initializing.", COLORS.GREEN);
-			}
-			if (CATCH_INIT_ERRORS) {
-				try {
-					init();
-				} catch (error) {
-					logMessage("" + error, COLORS.RED);
-				}
-			} else {
-				init();
-			}
+                Script.update.connect(this.update);
+                Script.scriptEnding.connect(this.teardown);
+    
+                this.setupUI(platform);
+    
+                logMessage("finished initializing.", COLORS.GREEN);
+            }
+            if (CATCH_INIT_ERRORS) {
+                try {
+                    init();
+                } catch (error) {
+                    logMessage("" + error, COLORS.RED);
+                }
+            } else {
+                init();
+            }
 
-			Controller.mousePressEvent.connect(UI.handleMousePress);
-			Controller.mouseMoveEvent.connect(UI.handleMouseMove);
-			Controller.mouseReleaseEvent.connect(UI.handleMouseRelease);
-		}
-	}
-	Script.update.connect(this.startup);
+            Controller.mousePressEvent.connect(UI.handleMousePress);
+            Controller.mouseMoveEvent.connect(UI.handleMouseMove);
+            Controller.mouseReleaseEvent.connect(UI.handleMouseRelease);
+        } else {
+            if (!startup.printedWarnMsg) {
+                startup.timer = startup.timer || startup.ENTITY_SERVER_WAIT_TIME;
+                if ((startup.timer -= dt) < 0.0) {
+                    logMessage("Waiting for entity server");
+                    startup.printedWarnMsg = true;
+                }
+
+            }
+        }
+    }
+    startup.ENTITY_SERVER_WAIT_TIME = 0.2;   // print "waiting for entity server" if more than this time has elapsed in startup()
+
+    Script.update.connect(this.startup);
 })();
 
 })();

From b88c8e507b56985d34da33774839c88691563d3f Mon Sep 17 00:00:00 2001
From: Brad Hefta-Gaub <brad@highfidelity.io>
Date: Mon, 21 Sep 2015 11:35:26 -0700
Subject: [PATCH 11/21] fix a couple of properties

---
 libraries/entities/src/EntityItemProperties.cpp | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp
index 65b41b5734..13138e5c10 100644
--- a/libraries/entities/src/EntityItemProperties.cpp
+++ b/libraries/entities/src/EntityItemProperties.cpp
@@ -498,18 +498,21 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
     // Models only
     if (_type == EntityTypes::Model) {
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MODEL_URL, modelURL);
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_URL, animationURL);
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures);
     }
 
-
     if (_type == EntityTypes::Model || _type == EntityTypes::Zone || _type == EntityTypes::ParticleEffect) {
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, getShapeTypeAsString());
     }
 
+    // FIXME - it seems like ParticleEffect should also support this
+    if (_type == EntityTypes::Model || _type == EntityTypes::Zone) {
+        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL);
+    }
+
     // Models & Particles
     if (_type == EntityTypes::Model || _type == EntityTypes::ParticleEffect) {
+        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_PLAYING, animationIsPlaying);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_FPS, animationFPS);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_FRAME_INDEX, animationFrameIndex);

From dc9c774eb541439ea5064843abb39d8979987ab2 Mon Sep 17 00:00:00 2001
From: Brad Hefta-Gaub <brad@highfidelity.io>
Date: Mon, 21 Sep 2015 13:09:43 -0700
Subject: [PATCH 12/21] merge model part quads and triangles together to reduce
 the number of draw calls

---
 interface/resources/qml/Stats.qml    |  2 +-
 interface/src/ui/Stats.cpp           |  1 -
 libraries/fbx/src/FBXReader.cpp      | 53 ++++++++++++++++--------
 libraries/fbx/src/FBXReader.h        | 10 ++---
 libraries/render-utils/src/Model.cpp | 61 ++--------------------------
 libraries/render-utils/src/Model.h   |  4 --
 libraries/shared/src/RenderArgs.h    |  1 -
 7 files changed, 46 insertions(+), 86 deletions(-)

diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml
index c3829c7be2..eb39fbc70f 100644
--- a/interface/resources/qml/Stats.qml
+++ b/interface/resources/qml/Stats.qml
@@ -168,7 +168,7 @@ Item {
                         color: root.fontColor;
                         font.pixelSize: root.fontSize
                         text: "Triangles: " + root.triangles +
-                            " / Quads: " + root.quads + " / Material Switches: " + root.materialSwitches
+                            " / Material Switches: " + root.materialSwitches
                     }
                     Text {
                         color: root.fontColor;
diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp
index 8ddb767537..4dd552210c 100644
--- a/interface/src/ui/Stats.cpp
+++ b/interface/src/ui/Stats.cpp
@@ -337,7 +337,6 @@ void Stats::updateStats() {
 
 void Stats::setRenderDetails(const RenderDetails& details) {
     STAT_UPDATE(triangles, details._trianglesRendered);
-    STAT_UPDATE(quads, details._quadsRendered);
     STAT_UPDATE(materialSwitches, details._materialSwitches);
     if (_expanded) {
         STAT_UPDATE(meshOpaque, details._opaque._rendered);
diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp
index 6f69e8befc..2fbca10915 100644
--- a/libraries/fbx/src/FBXReader.cpp
+++ b/libraries/fbx/src/FBXReader.cpp
@@ -829,24 +829,30 @@ public:
     std::vector<AttributeData> attributes;
 };
 
-gpu::BufferPointer FBXMeshPart::getTrianglesForQuads() const {
+gpu::BufferPointer FBXMeshPart::getMergedTriangles() const {
     // if we've been asked for our triangulation of the original quads, but we don't yet have them
     // then create them now.
-    if (!trianglesForQuadsAvailable) {
-        trianglesForQuadsAvailable = true;
+    if (!mergedTrianglesAvailable) {
+        mergedTrianglesAvailable = true;
 
-        quadsAsTrianglesIndicesBuffer = std::make_shared<gpu::Buffer>();
+        mergedTrianglesIndicesBuffer = std::make_shared<gpu::Buffer>();
 
         // QVector<int> quadIndices; // original indices from the FBX mesh
-         QVector<quint32> quadsAsTrianglesIndices; // triangle versions of quads converted when first needed
+        QVector<quint32> mergedTrianglesIndices; // triangle versions of quads converted when first needed
+        const int INDICES_PER_ORIGINAL_TRIANGLE = 3;
         const int INDICES_PER_ORIGINAL_QUAD = 4;
         const int INDICES_PER_TRIANGULATED_QUAD = 6;
         int numberOfQuads = quadIndices.size() / INDICES_PER_ORIGINAL_QUAD;
-        
-        quadsAsTrianglesIndices.resize(numberOfQuads * INDICES_PER_TRIANGULATED_QUAD);
+        int numberOfTriangles = triangleIndices.size() / INDICES_PER_ORIGINAL_TRIANGLE;
+        int mergedNumberOfIndices = (numberOfQuads * INDICES_PER_TRIANGULATED_QUAD) + triangleIndices.size();
+
+        // resized our merged indices to be enough room for our triangulated quads and our original triangles        
+        mergedTrianglesIndices.resize(mergedNumberOfIndices);
         
         int originalIndex = 0;
         int triangulatedIndex = 0;
+
+        // triangulate our quads
         for (int fromQuad = 0; fromQuad < numberOfQuads; fromQuad++) {
             int i0 = quadIndices[originalIndex + 0];
             int i1 = quadIndices[originalIndex + 1];
@@ -860,23 +866,38 @@ gpu::BufferPointer FBXMeshPart::getTrianglesForQuads() const {
             // Triangle tri1 = { v0, v1, v2 };
             // Triangle tri2 = { v2, v3, v0 };
             
-            quadsAsTrianglesIndices[triangulatedIndex + 0] = i0;
-            quadsAsTrianglesIndices[triangulatedIndex + 1] = i1;
-            quadsAsTrianglesIndices[triangulatedIndex + 2] = i3;
+            mergedTrianglesIndices[triangulatedIndex + 0] = i0;
+            mergedTrianglesIndices[triangulatedIndex + 1] = i1;
+            mergedTrianglesIndices[triangulatedIndex + 2] = i3;
 
-            quadsAsTrianglesIndices[triangulatedIndex + 3] = i1;
-            quadsAsTrianglesIndices[triangulatedIndex + 4] = i2;
-            quadsAsTrianglesIndices[triangulatedIndex + 5] = i3;
+            mergedTrianglesIndices[triangulatedIndex + 3] = i1;
+            mergedTrianglesIndices[triangulatedIndex + 4] = i2;
+            mergedTrianglesIndices[triangulatedIndex + 5] = i3;
             
             originalIndex += INDICES_PER_ORIGINAL_QUAD;
             triangulatedIndex += INDICES_PER_TRIANGULATED_QUAD;
         }
 
-        trianglesForQuadsIndicesCount = INDICES_PER_TRIANGULATED_QUAD * numberOfQuads;
-        quadsAsTrianglesIndicesBuffer->append(quadsAsTrianglesIndices.size() * sizeof(quint32), (gpu::Byte*)quadsAsTrianglesIndices.data());
+        // add our original triangs
+        originalIndex = 0;
+        for (int fromTriangle = 0; fromTriangle < numberOfTriangles; fromTriangle++) {
+            int i0 = triangleIndices[originalIndex + 0];
+            int i1 = triangleIndices[originalIndex + 1];
+            int i2 = triangleIndices[originalIndex + 2];
+
+            mergedTrianglesIndices[triangulatedIndex + 0] = i0;
+            mergedTrianglesIndices[triangulatedIndex + 1] = i1;
+            mergedTrianglesIndices[triangulatedIndex + 2] = i2;
+
+            originalIndex += INDICES_PER_ORIGINAL_TRIANGLE;
+            triangulatedIndex += INDICES_PER_ORIGINAL_TRIANGLE;
+        }
+
+        mergedTrianglesIndicesCount = mergedNumberOfIndices;
+        mergedTrianglesIndicesBuffer->append(mergedNumberOfIndices * sizeof(quint32), (gpu::Byte*)mergedTrianglesIndices.data());
 
     }
-    return quadsAsTrianglesIndicesBuffer;
+    return mergedTrianglesIndicesBuffer;
 }
 
 void appendIndex(MeshData& data, QVector<int>& indices, int index) {
diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h
index cff22676c8..f2fd9fdd15 100644
--- a/libraries/fbx/src/FBXReader.h
+++ b/libraries/fbx/src/FBXReader.h
@@ -117,10 +117,10 @@ public:
 /// A single part of a mesh (with the same material).
 class FBXMeshPart {
 public:
-    
+
     QVector<int> quadIndices; // original indices from the FBX mesh
     QVector<int> triangleIndices; // original indices from the FBX mesh
-    mutable gpu::BufferPointer quadsAsTrianglesIndicesBuffer;
+    mutable gpu::BufferPointer mergedTrianglesIndicesBuffer; // both the quads and the triangles merged into a single set of triangles
 
     glm::vec3 diffuseColor;
     glm::vec3 specularColor;
@@ -136,10 +136,10 @@ public:
 
     QString materialID;
     model::MaterialPointer _material;
-    mutable bool trianglesForQuadsAvailable = false;
-    mutable int trianglesForQuadsIndicesCount = 0;
+    mutable bool mergedTrianglesAvailable = false;
+    mutable int mergedTrianglesIndicesCount = 0;
 
-    gpu::BufferPointer getTrianglesForQuads() const;
+    gpu::BufferPointer getMergedTriangles() const;
 };
 
 /// A single mesh (with optional blendshapes) extracted from an FBX document.
diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp
index a4dc1b1de3..a7910a7857 100644
--- a/libraries/render-utils/src/Model.cpp
+++ b/libraries/render-utils/src/Model.cpp
@@ -75,7 +75,6 @@ Model::Model(RigPointer rig, QObject* parent) :
     _isVisible(true),
     _blendNumber(0),
     _appliedBlendNumber(0),
-    _calculatedMeshPartOffsetValid(false),
     _calculatedMeshPartBoxesValid(false),
     _calculatedMeshBoxesValid(false),
     _calculatedMeshTrianglesValid(false),
@@ -601,25 +600,6 @@ bool Model::convexHullContains(glm::vec3 point) {
     return false;
 }
 
-void Model::recalculateMeshPartOffsets() {
-    if (!_calculatedMeshPartOffsetValid) {
-        const FBXGeometry& geometry = _geometry->getFBXGeometry();
-        int numberOfMeshes = geometry.meshes.size();
-        _calculatedMeshPartOffset.clear();
-        for (int i = 0; i < numberOfMeshes; i++) {
-            const FBXMesh& mesh = geometry.meshes.at(i);
-            qint64 partOffset = 0;
-            for (int j = 0; j < mesh.parts.size(); j++) {
-                const FBXMeshPart& part = mesh.parts.at(j);
-                _calculatedMeshPartOffset[QPair<int,int>(i, j)] = partOffset;
-                partOffset += part.quadIndices.size() * sizeof(int);
-                partOffset += part.triangleIndices.size() * sizeof(int);
-
-            }
-        }
-        _calculatedMeshPartOffsetValid = true;
-    }
-}
 // TODO: we seem to call this too often when things haven't actually changed... look into optimizing this
 // Any script might trigger findRayIntersectionAgainstSubMeshes (and maybe convexHullContains), so these
 // can occur multiple times. In addition, rendering does it's own ray picking in order to decide which
@@ -636,8 +616,6 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) {
         _calculatedMeshTriangles.clear();
         _calculatedMeshTriangles.resize(numberOfMeshes);
         _calculatedMeshPartBoxes.clear();
-        _calculatedMeshPartOffset.clear();
-        _calculatedMeshPartOffsetValid = false;
         for (int i = 0; i < numberOfMeshes; i++) {
             const FBXMesh& mesh = geometry.meshes.at(i);
             Extents scaledMeshExtents = calculateScaledOffsetExtents(mesh.meshExtents);
@@ -646,7 +624,6 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) {
 
             if (pickAgainstTriangles) {
                 QVector<Triangle> thisMeshTriangles;
-                qint64 partOffset = 0;
                 for (int j = 0; j < mesh.parts.size(); j++) {
                     const FBXMeshPart& part = mesh.parts.at(j);
 
@@ -732,15 +709,9 @@ void Model::recalculateMeshBoxes(bool pickAgainstTriangles) {
                         }
                     }
                     _calculatedMeshPartBoxes[QPair<int,int>(i, j)] = thisPartBounds;
-                    _calculatedMeshPartOffset[QPair<int,int>(i, j)] = partOffset;
-
-                    partOffset += part.quadIndices.size() * sizeof(int);
-                    partOffset += part.triangleIndices.size() * sizeof(int);
-
                 }
                 _calculatedMeshTriangles[i] = thisMeshTriangles;
                 _calculatedMeshPartBoxesValid = true;
-                _calculatedMeshPartOffsetValid = true;
             }
         }
         _calculatedMeshBoxesValid = true;
@@ -1480,12 +1451,6 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran
         return; // bail asap
     }
 
-    // We need to make sure we have valid offsets calculated before we can render
-    if (!_calculatedMeshPartOffsetValid) {
-        _mutex.lock();
-        recalculateMeshPartOffsets();
-        _mutex.unlock();
-    }
     auto textureCache = DependencyManager::get<TextureCache>();
 
     gpu::Batch& batch = *(args->_batch);
@@ -1703,32 +1668,12 @@ void Model::renderPart(RenderArgs* args, int meshIndex, int partIndex, bool tran
         }
     }
 
-    qint64 offset;
-    {
-        // FIXME_STUTTER: We should n't have any lock here
-        _mutex.lock();
-        offset = _calculatedMeshPartOffset[QPair<int,int>(meshIndex, partIndex)];
-        _mutex.unlock();
-    }
-
-    if (part.quadIndices.size() > 0) {
-        batch.setIndexBuffer(gpu::UINT32, part.getTrianglesForQuads(), 0);
-        batch.drawIndexed(gpu::TRIANGLES, part.trianglesForQuadsIndicesCount, 0);
-
-        offset += part.quadIndices.size() * sizeof(int);
-        batch.setIndexBuffer(gpu::UINT32, (networkMesh._indexBuffer), 0); // restore this in case there are triangles too
-    }
-
-    if (part.triangleIndices.size() > 0) {
-        batch.drawIndexed(gpu::TRIANGLES, part.triangleIndices.size(), offset);
-        offset += part.triangleIndices.size() * sizeof(int);
-    }
+    batch.setIndexBuffer(gpu::UINT32, part.getMergedTriangles(), 0);
+    batch.drawIndexed(gpu::TRIANGLES, part.mergedTrianglesIndicesCount, 0);
 
     if (args) {
         const int INDICES_PER_TRIANGLE = 3;
-        const int INDICES_PER_QUAD = 4;
-        args->_details._trianglesRendered += part.triangleIndices.size() / INDICES_PER_TRIANGLE;
-        args->_details._quadsRendered += part.quadIndices.size() / INDICES_PER_QUAD;
+        args->_details._trianglesRendered += part.mergedTrianglesIndicesCount / INDICES_PER_TRIANGLE;
     }
 }
 
diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h
index 93b98da8b5..fd9a9e6353 100644
--- a/libraries/render-utils/src/Model.h
+++ b/libraries/render-utils/src/Model.h
@@ -352,9 +352,6 @@ private:
     };
 
     QHash<QPair<int,int>, AABox> _calculatedMeshPartBoxes; // world coordinate AABoxes for all sub mesh part boxes
-    QHash<QPair<int,int>, qint64> _calculatedMeshPartOffset;
-    bool _calculatedMeshPartOffsetValid;
-
 
     bool _calculatedMeshPartBoxesValid;
     QVector<AABox> _calculatedMeshBoxes; // world coordinate AABoxes for all sub mesh boxes
@@ -365,7 +362,6 @@ private:
     QMutex _mutex;
 
     void recalculateMeshBoxes(bool pickAgainstTriangles = false);
-    void recalculateMeshPartOffsets();
 
     void segregateMeshGroups(); // used to calculate our list of translucent vs opaque meshes
 
diff --git a/libraries/shared/src/RenderArgs.h b/libraries/shared/src/RenderArgs.h
index 7a5daf7a17..25eed96490 100644
--- a/libraries/shared/src/RenderArgs.h
+++ b/libraries/shared/src/RenderArgs.h
@@ -43,7 +43,6 @@ public:
     
     int _materialSwitches = 0;
     int _trianglesRendered = 0;
-    int _quadsRendered = 0;
     
     Item _opaque;
     Item _translucent;

From e024d23366fa7e818f392c41afaebf5e03d0d7e3 Mon Sep 17 00:00:00 2001
From: Bradley Austin Davis <bdavis@saintandreas.org>
Date: Fri, 18 Sep 2015 18:51:44 -0700
Subject: [PATCH 13/21] Instancing work, second pass

---
 examples/cubePerfTest.js                      |   4 +-
 interface/src/Stars.cpp                       |   2 +-
 interface/src/Util.cpp                        |  28 +-
 interface/src/avatar/Avatar.cpp               |  44 +-
 interface/src/avatar/Hand.cpp                 |  13 +-
 interface/src/avatar/Head.cpp                 |   7 +-
 interface/src/avatar/SkeletonModel.cpp        |  22 +-
 interface/src/ui/overlays/Cube3DOverlay.cpp   |   7 +-
 interface/src/ui/overlays/Sphere3DOverlay.cpp |  10 +-
 .../src/RenderableBoxEntityItem.cpp           |   4 +-
 .../src/RenderableDebugableEntityItem.cpp     |   9 +-
 .../src/RenderableSphereEntityItem.cpp        |  17 +-
 .../src/RenderableZoneEntityItem.cpp          |  12 +-
 libraries/fbx/src/FBXReader.cpp               |   4 +-
 libraries/gpu/src/gpu/Batch.cpp               |   8 +-
 libraries/gpu/src/gpu/Batch.h                 |   3 +
 libraries/gpu/src/gpu/Format.cpp              |  21 +
 libraries/gpu/src/gpu/Format.h                |  19 +-
 libraries/gpu/src/gpu/GLBackend.cpp           |   9 +-
 libraries/gpu/src/gpu/GLBackendShared.h       |   3 +-
 libraries/gpu/src/gpu/GLBackendTexture.cpp    |   4 +-
 libraries/gpu/src/gpu/Resource.cpp            |   5 -
 libraries/gpu/src/gpu/Resource.h              |   5 +
 libraries/gpu/src/gpu/Stream.cpp              |  44 +
 libraries/gpu/src/gpu/Stream.h                |  30 +-
 .../src/DeferredLightingEffect.cpp            |  95 +-
 .../render-utils/src/DeferredLightingEffect.h |  28 +-
 libraries/render-utils/src/Environment.cpp    |   6 +-
 libraries/render-utils/src/GeometryCache.cpp  | 983 +++++++++---------
 libraries/render-utils/src/GeometryCache.h    |  92 +-
 tests/gpu-test/src/main.cpp                   | 312 ++----
 tests/gpu-test/src/{simple.slf => unlit.slf}  |   1 -
 tests/gpu-test/src/{simple.slv => unlit.slv}  |  11 +-
 33 files changed, 960 insertions(+), 902 deletions(-)
 create mode 100644 libraries/gpu/src/gpu/Format.cpp
 rename tests/gpu-test/src/{simple.slf => unlit.slf} (94%)
 rename tests/gpu-test/src/{simple.slv => unlit.slv} (64%)

diff --git a/examples/cubePerfTest.js b/examples/cubePerfTest.js
index 699472edd9..f2f4d48b22 100644
--- a/examples/cubePerfTest.js
+++ b/examples/cubePerfTest.js
@@ -36,7 +36,7 @@ for (var x = 0; x < SIDE_SIZE; x++) {
             var position = Vec3.sum(MyAvatar.position, { x: x * 0.2, y: y * 0.2, z: z * 0.2});
             var radius = Math.random() * 0.1;
             boxes.push(Entities.addEntity({ 
-                type: cube ? "Box" : "Box",
+                type: cube ? "Box" : "Sphere",
                 name: "PerfTest",
                 position: position,  
                 dimensions: { x: radius, y: radius, z: radius }, 
@@ -52,7 +52,7 @@ for (var x = 0; x < SIDE_SIZE; x++) {
 
 function scriptEnding() {
     for (var i = 0; i < boxes.length; i++) {
-        //Entities.deleteEntity(boxes[i]);
+        Entities.deleteEntity(boxes[i]);
     }
 }
 Script.scriptEnding.connect(scriptEnding);
diff --git a/interface/src/Stars.cpp b/interface/src/Stars.cpp
index 119b9ed1a2..6bc160d5eb 100644
--- a/interface/src/Stars.cpp
+++ b/interface/src/Stars.cpp
@@ -204,7 +204,7 @@ void Stars::render(RenderArgs* renderArgs, float alpha) {
     float msecs = (float)(usecTimestampNow() - start) / (float)USECS_PER_MSEC;
     float secs = msecs / (float)MSECS_PER_SECOND;
     batch._glUniform1f(_timeSlot, secs);
-    geometryCache->renderUnitCube(batch);
+    geometryCache->renderCube(batch);
 
     static const size_t VERTEX_STRIDE = sizeof(StarVertex);
     size_t offset = offsetof(StarVertex, position);
diff --git a/interface/src/Util.cpp b/interface/src/Util.cpp
index dad34e9243..d09dd41999 100644
--- a/interface/src/Util.cpp
+++ b/interface/src/Util.cpp
@@ -23,6 +23,7 @@
 
 #include <ByteCountCoding.h>
 #include <SharedUtil.h>
+#include <DeferredLightingEffect.h>
 
 #include "world.h"
 #include "Application.h"
@@ -93,29 +94,28 @@ void renderWorldBox(gpu::Batch& batch) {
     geometryCache->renderLine(batch, glm::vec3(-HALF_TREE_SCALE, 0.0f, HALF_TREE_SCALE),
                               glm::vec3(HALF_TREE_SCALE, 0.0f, HALF_TREE_SCALE), GREY);
 
-    geometryCache->renderWireCube(batch, TREE_SCALE, GREY4);
+    auto deferredLighting = DependencyManager::get<DeferredLightingEffect>();
+
+    deferredLighting->renderWireCubeInstance(batch, Transform(), GREY4);
 
     //  Draw meter markers along the 3 axis to help with measuring things
     const float MARKER_DISTANCE = 1.0f;
     const float MARKER_RADIUS = 0.05f;
 
-    geometryCache->renderSphere(batch, MARKER_RADIUS, 10, 10, RED);
+    transform = Transform().setScale(MARKER_RADIUS);
+    deferredLighting->renderSolidSphereInstance(batch, transform, RED);
 
-    transform.setTranslation(glm::vec3(MARKER_DISTANCE, 0.0f, 0.0f));
-    batch.setModelTransform(transform);
-    geometryCache->renderSphere(batch, MARKER_RADIUS, 10, 10, RED);
+    transform = Transform().setTranslation(glm::vec3(MARKER_DISTANCE, 0.0f, 0.0f)).setScale(MARKER_RADIUS);
+    deferredLighting->renderSolidSphereInstance(batch, transform, RED);
 
-    transform.setTranslation(glm::vec3(0.0f, MARKER_DISTANCE, 0.0f));
-    batch.setModelTransform(transform);
-    geometryCache->renderSphere(batch, MARKER_RADIUS, 10, 10, GREEN);
+    transform = Transform().setTranslation(glm::vec3(0.0f, MARKER_DISTANCE, 0.0f)).setScale(MARKER_RADIUS);
+    deferredLighting->renderSolidSphereInstance(batch, transform, GREEN);
 
-    transform.setTranslation(glm::vec3(0.0f, 0.0f, MARKER_DISTANCE));
-    batch.setModelTransform(transform);
-    geometryCache->renderSphere(batch, MARKER_RADIUS, 10, 10, BLUE);
+    transform = Transform().setTranslation(glm::vec3(0.0f, 0.0f, MARKER_DISTANCE)).setScale(MARKER_RADIUS);
+    deferredLighting->renderSolidSphereInstance(batch, transform, BLUE);
 
-    transform.setTranslation(glm::vec3(MARKER_DISTANCE, 0.0f, MARKER_DISTANCE));
-    batch.setModelTransform(transform);
-    geometryCache->renderSphere(batch, MARKER_RADIUS, 10, 10, GREY);
+    transform = Transform().setTranslation(glm::vec3(MARKER_DISTANCE, 0.0f, MARKER_DISTANCE)).setScale(MARKER_RADIUS);
+    deferredLighting->renderSolidSphereInstance(batch, transform, GREY);
 }
 
 //  Return a random vector of average length 1
diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp
index 5ac830baf3..0a934e1ed3 100644
--- a/interface/src/avatar/Avatar.cpp
+++ b/interface/src/avatar/Avatar.cpp
@@ -448,15 +448,14 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) {
 
         // If this is the avatar being looked at, render a little ball above their head
         if (_isLookAtTarget && Menu::getInstance()->isOptionChecked(MenuOption::RenderFocusIndicator)) {
-            const float INDICATOR_OFFSET = 0.22f;
-            const float INDICATOR_RADIUS = 0.03f;
-            const glm::vec4 LOOK_AT_INDICATOR_COLOR = { 0.8f, 0.0f, 0.0f, 0.75f };
+            static const float INDICATOR_OFFSET = 0.22f;
+            static const float INDICATOR_RADIUS = 0.03f;
+            static const glm::vec4 LOOK_AT_INDICATOR_COLOR = { 0.8f, 0.0f, 0.0f, 0.75f };
             glm::vec3 position = glm::vec3(_position.x, getDisplayNamePosition().y + INDICATOR_OFFSET, _position.z);
             Transform transform;
             transform.setTranslation(position);
-            batch.setModelTransform(transform);
-            DependencyManager::get<DeferredLightingEffect>()->renderSolidSphere(batch, INDICATOR_RADIUS,
-                15, 15, LOOK_AT_INDICATOR_COLOR);
+            transform.postScale(INDICATOR_RADIUS);
+            DependencyManager::get<DeferredLightingEffect>()->renderSolidSphereInstance(batch, transform, LOOK_AT_INDICATOR_COLOR);
         }
 
         // If the avatar is looking at me, indicate that they are
@@ -473,27 +472,29 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) {
                 if (geometry && geometry->isLoaded()) {
                     const float DEFAULT_EYE_DIAMETER = 0.048f;  // Typical human eye
                     const float RADIUS_INCREMENT = 0.005f;
-                    Transform transform;
+                    batch.setModelTransform(Transform());
 
                     glm::vec3 position = getHead()->getLeftEyePosition();
+                    Transform transform;
                     transform.setTranslation(position);
-                    batch.setModelTransform(transform);
                     float eyeDiameter = geometry->getFBXGeometry().leftEyeSize;
                     if (eyeDiameter == 0.0f) {
                         eyeDiameter = DEFAULT_EYE_DIAMETER;
                     }
-                    DependencyManager::get<DeferredLightingEffect>()->renderSolidSphere(batch,
-                        eyeDiameter * _scale / 2.0f + RADIUS_INCREMENT, 15, 15, glm::vec4(LOOKING_AT_ME_COLOR, alpha));
+
+                    DependencyManager::get<DeferredLightingEffect>()->renderSolidSphereInstance(batch, 
+                        Transform(transform).postScale(eyeDiameter * _scale / 2.0f + RADIUS_INCREMENT), 
+                        glm::vec4(LOOKING_AT_ME_COLOR, alpha));
 
                     position = getHead()->getRightEyePosition();
                     transform.setTranslation(position);
-                    batch.setModelTransform(transform);
                     eyeDiameter = geometry->getFBXGeometry().rightEyeSize;
                     if (eyeDiameter == 0.0f) {
                         eyeDiameter = DEFAULT_EYE_DIAMETER;
                     }
-                    DependencyManager::get<DeferredLightingEffect>()->renderSolidSphere(batch,
-                        eyeDiameter * _scale / 2.0f + RADIUS_INCREMENT, 15, 15, glm::vec4(LOOKING_AT_ME_COLOR, alpha));
+                    DependencyManager::get<DeferredLightingEffect>()->renderSolidSphereInstance(batch,
+                        Transform(transform).postScale(eyeDiameter * _scale / 2.0f + RADIUS_INCREMENT), 
+                        glm::vec4(LOOKING_AT_ME_COLOR, alpha));
 
                 }
             }
@@ -518,19 +519,16 @@ void Avatar::render(RenderArgs* renderArgs, const glm::vec3& cameraPosition) {
 
             if (renderArgs->_renderMode == RenderArgs::DEFAULT_RENDER_MODE && (sphereRadius > MIN_SPHERE_SIZE) &&
                     (angle < MAX_SPHERE_ANGLE) && (angle > MIN_SPHERE_ANGLE)) {
+                batch.setModelTransform(Transform());
+
+
                 Transform transform;
                 transform.setTranslation(_position);
                 transform.setScale(height);
-                batch.setModelTransform(transform);
-
-                if (_voiceSphereID == GeometryCache::UNKNOWN_ID) {
-                    _voiceSphereID = DependencyManager::get<GeometryCache>()->allocateID();
-                }
-
-                DependencyManager::get<DeferredLightingEffect>()->bindSimpleProgram(batch);
-                DependencyManager::get<GeometryCache>()->renderSphere(batch, sphereRadius, 15, 15,
-                    glm::vec4(SPHERE_COLOR[0], SPHERE_COLOR[1], SPHERE_COLOR[2], 1.0f - angle / MAX_SPHERE_ANGLE), true,
-                    _voiceSphereID);
+                transform.postScale(sphereRadius);
+                DependencyManager::get<DeferredLightingEffect>()->renderSolidSphereInstance(batch,
+                    transform,
+                    glm::vec4(SPHERE_COLOR[0], SPHERE_COLOR[1], SPHERE_COLOR[2], 1.0f - angle / MAX_SPHERE_ANGLE));
             }
         }
     }
diff --git a/interface/src/avatar/Hand.cpp b/interface/src/avatar/Hand.cpp
index a0125ca736..a144661f8b 100644
--- a/interface/src/avatar/Hand.cpp
+++ b/interface/src/avatar/Hand.cpp
@@ -14,6 +14,7 @@
 
 #include <GeometryUtil.h>
 #include <RenderArgs.h>
+#include <DeferredLightingEffect.h>
 
 #include "Avatar.h"
 #include "AvatarManager.h"
@@ -65,16 +66,16 @@ void Hand::renderHandTargets(RenderArgs* renderArgs, bool isMine) {
             Transform transform = Transform();
             transform.setTranslation(position);
             transform.setRotation(palm.getRotation());
-            batch.setModelTransform(transform);
-            DependencyManager::get<GeometryCache>()->renderSphere(batch, SPHERE_RADIUS,
-                    NUM_FACETS, NUM_FACETS, grayColor);
+            transform.postScale(SPHERE_RADIUS);
+            DependencyManager::get<DeferredLightingEffect>()->renderSolidSphereInstance(batch, transform, grayColor);
     
             // draw a green sphere at the old "finger tip"
+            transform = Transform();
             position = palm.getTipPosition();
             transform.setTranslation(position);
-            batch.setModelTransform(transform);
-            DependencyManager::get<GeometryCache>()->renderSphere(batch, SPHERE_RADIUS,
-                    NUM_FACETS, NUM_FACETS, greenColor, false);
+            transform.setRotation(palm.getRotation());
+            transform.postScale(SPHERE_RADIUS);
+            DependencyManager::get<DeferredLightingEffect>()->renderSolidSphereInstance(batch, transform, greenColor);
         }
     }
     
diff --git a/interface/src/avatar/Head.cpp b/interface/src/avatar/Head.cpp
index a514eb4e8d..96c55dfa93 100644
--- a/interface/src/avatar/Head.cpp
+++ b/interface/src/avatar/Head.cpp
@@ -462,13 +462,10 @@ void Head::renderLookatTarget(RenderArgs* renderArgs, glm::vec3 lookatPosition)
     auto& batch = *renderArgs->_batch;
     auto transform = Transform{};
     transform.setTranslation(lookatPosition);
-    batch.setModelTransform(transform);
 
     auto deferredLighting = DependencyManager::get<DeferredLightingEffect>();
-    deferredLighting->bindSimpleProgram(batch);
-
-    auto geometryCache = DependencyManager::get<GeometryCache>();
     const float LOOK_AT_TARGET_RADIUS = 0.075f;
+    transform.postScale(LOOK_AT_TARGET_RADIUS);
     const glm::vec4 LOOK_AT_TARGET_COLOR = { 0.8f, 0.0f, 0.0f, 0.75f };
-    geometryCache->renderSphere(batch, LOOK_AT_TARGET_RADIUS, 15, 15, LOOK_AT_TARGET_COLOR, true);
+    deferredLighting->renderSolidSphereInstance(batch, transform, LOOK_AT_TARGET_COLOR);
 }
diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp
index 27e61175eb..ff8cde3df8 100644
--- a/interface/src/avatar/SkeletonModel.cpp
+++ b/interface/src/avatar/SkeletonModel.cpp
@@ -639,27 +639,25 @@ void SkeletonModel::renderBoundingCollisionShapes(gpu::Batch& batch, float alpha
 
     auto geometryCache = DependencyManager::get<GeometryCache>();
     auto deferredLighting = DependencyManager::get<DeferredLightingEffect>();
-    Transform transform; // = Transform();
-
     // draw a blue sphere at the capsule top point
     glm::vec3 topPoint = _translation + _boundingCapsuleLocalOffset + (0.5f * _boundingCapsuleHeight) * glm::vec3(0.0f, 1.0f, 0.0f);
-    transform.setTranslation(topPoint);
-    batch.setModelTransform(transform);
-    deferredLighting->bindSimpleProgram(batch);
-    geometryCache->renderSphere(batch, _boundingCapsuleRadius, BALL_SUBDIVISIONS, BALL_SUBDIVISIONS,
-                                glm::vec4(0.6f, 0.6f, 0.8f, alpha));
+
+    deferredLighting->renderSolidSphereInstance(batch, 
+        Transform().setTranslation(topPoint).postScale(_boundingCapsuleRadius * 2.0),
+    	glm::vec4(0.6f, 0.6f, 0.8f, alpha));
 
     // draw a yellow sphere at the capsule bottom point
     glm::vec3 bottomPoint = topPoint - glm::vec3(0.0f, _boundingCapsuleHeight, 0.0f);
     glm::vec3 axis = topPoint - bottomPoint;
-    transform.setTranslation(bottomPoint);
-    batch.setModelTransform(transform);
-    deferredLighting->bindSimpleProgram(batch);
-    geometryCache->renderSphere(batch, _boundingCapsuleRadius, BALL_SUBDIVISIONS, BALL_SUBDIVISIONS,
-                                glm::vec4(0.8f, 0.8f, 0.6f, alpha));
+
+    deferredLighting->renderSolidSphereInstance(batch, 
+        Transform().setTranslation(bottomPoint).postScale(_boundingCapsuleRadius * 2.0),
+        glm::vec4(0.8f, 0.8f, 0.6f, alpha));
 
     // draw a green cylinder between the two points
     glm::vec3 origin(0.0f);
+    batch.setModelTransform(Transform().setTranslation(bottomPoint));
+    deferredLighting->bindSimpleProgram(batch);
     Avatar::renderJointConnectingCone(batch, origin, axis, _boundingCapsuleRadius, _boundingCapsuleRadius,
                                       glm::vec4(0.6f, 0.8f, 0.6f, alpha));
 }
diff --git a/interface/src/ui/overlays/Cube3DOverlay.cpp b/interface/src/ui/overlays/Cube3DOverlay.cpp
index a306c7c86d..3ad887ca65 100644
--- a/interface/src/ui/overlays/Cube3DOverlay.cpp
+++ b/interface/src/ui/overlays/Cube3DOverlay.cpp
@@ -61,8 +61,7 @@ void Cube3DOverlay::render(RenderArgs* args) {
             // }
 
             transform.setScale(dimensions);
-            batch->setModelTransform(transform);
-            DependencyManager::get<GeometryCache>()->renderSolidCube(*batch, 1.0f, cubeColor);
+            DependencyManager::get<DeferredLightingEffect>()->renderSolidCubeInstance(*batch, transform, cubeColor);
         } else {
 
             if (getIsDashedLine()) {
@@ -98,9 +97,9 @@ void Cube3DOverlay::render(RenderArgs* args) {
                 geometryCache->renderDashedLine(*batch, bottomRightFar, topRightFar, cubeColor);
 
             } else {
+                batch->setModelTransform(Transform());
                 transform.setScale(dimensions);
-                batch->setModelTransform(transform);
-                DependencyManager::get<DeferredLightingEffect>()->renderWireCube(*batch, 1.0f, cubeColor);
+                DependencyManager::get<DeferredLightingEffect>()->renderWireCubeInstance(*batch, transform, cubeColor);
             }
         }
     }
diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp
index 9712375209..3b503e87e8 100644
--- a/interface/src/ui/overlays/Sphere3DOverlay.cpp
+++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp
@@ -12,6 +12,7 @@
 
 #include <DependencyManager.h>
 #include <GeometryCache.h>
+#include <DeferredLightingEffect.h>
 #include <gpu/Batch.h>
 #include <SharedUtil.h>
 
@@ -36,10 +37,15 @@ void Sphere3DOverlay::render(RenderArgs* args) {
     auto batch = args->_batch;
 
     if (batch) {
+        batch->setModelTransform(Transform());
+
         Transform transform = _transform;
         transform.postScale(getDimensions());
-        batch->setModelTransform(transform);
-        DependencyManager::get<GeometryCache>()->renderSphere(*batch, 1.0f, SLICES, SLICES, sphereColor, _isSolid);
+        if (_isSolid) {
+            DependencyManager::get<DeferredLightingEffect>()->renderSolidSphereInstance(*batch, transform, sphereColor);
+        } else {
+            DependencyManager::get<DeferredLightingEffect>()->renderWireSphereInstance(*batch, transform, sphereColor);
+        }
     }
 
 }
diff --git a/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp b/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp
index b9ff69af52..187b25e75a 100644
--- a/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableBoxEntityItem.cpp
@@ -57,7 +57,9 @@ void RenderableBoxEntityItem::render(RenderArgs* args) {
     if (_procedural->ready()) {
         batch.setModelTransform(getTransformToCenter()); // we want to include the scale as well
         _procedural->prepare(batch, this->getDimensions());
-        DependencyManager::get<GeometryCache>()->renderSolidCube(batch, 1.0f, _procedural->getColor(cubeColor));
+        auto color = _procedural->getColor(cubeColor);
+        batch._glColor4f(color.r, color.g, color.b, color.a);
+        DependencyManager::get<GeometryCache>()->renderCube(batch);
     } else {
         DependencyManager::get<DeferredLightingEffect>()->renderSolidCubeInstance(batch, getTransformToCenter(), cubeColor);
     }
diff --git a/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp b/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp
index 2770c4edc2..6986025133 100644
--- a/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp
@@ -23,8 +23,13 @@ void RenderableDebugableEntityItem::renderBoundingBox(EntityItem* entity, Render
                                                       float puffedOut, glm::vec4& color) {
     Q_ASSERT(args->_batch);
     gpu::Batch& batch = *args->_batch;
-    batch.setModelTransform(entity->getTransformToCenter()); // we want to include the scale as well
-    DependencyManager::get<DeferredLightingEffect>()->renderWireCube(batch, 1.0f + puffedOut, color);
+
+    auto xfm = entity->getTransformToCenter();
+    if (puffedOut != 0.0) {
+        xfm.postScale(1.0 + puffedOut);
+    }
+    batch.setModelTransform(Transform()); // we want to include the scale as well
+    DependencyManager::get<DeferredLightingEffect>()->renderWireCubeInstance(batch, xfm, color);
 }
 
 void RenderableDebugableEntityItem::render(EntityItem* entity, RenderArgs* args) {
diff --git a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp
index 82257c67fb..63fbfff9cb 100644
--- a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp
@@ -39,15 +39,7 @@ void RenderableSphereEntityItem::render(RenderArgs* args) {
     PerformanceTimer perfTimer("RenderableSphereEntityItem::render");
     Q_ASSERT(getType() == EntityTypes::Sphere);
     Q_ASSERT(args->_batch);
-    gpu::Batch& batch = *args->_batch;
-    batch.setModelTransform(getTransformToCenter()); // use a transform with scale, rotation, registration point and translation
 
-    // TODO: it would be cool to select different slices/stacks geometry based on the size of the sphere
-    // and the distance to the viewer. This would allow us to reduce the triangle count for smaller spheres
-    // that aren't close enough to see the tessellation and use larger triangle count for spheres that would
-    // expose that effect
-    static const int SLICES = 15, STACKS = 15;
-    
     if (!_procedural) {
         _procedural.reset(new Procedural(getUserData()));
         _procedural->_vertexSource = simple_vert;
@@ -59,12 +51,17 @@ void RenderableSphereEntityItem::render(RenderArgs* args) {
             gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
     }
 
+    gpu::Batch& batch = *args->_batch;
     glm::vec4 sphereColor(toGlm(getXColor()), getLocalRenderAlpha());
     if (_procedural->ready()) {
+        batch.setModelTransform(getTransformToCenter()); // use a transform with scale, rotation, registration point and translation
         _procedural->prepare(batch, getDimensions());
-        DependencyManager::get<GeometryCache>()->renderSphere(batch, 0.5f, SLICES, STACKS, _procedural->getColor(sphereColor));
+        auto color = _procedural->getColor(sphereColor);
+        batch._glColor4f(color.r, color.g, color.b, color.a);
+        DependencyManager::get<GeometryCache>()->renderSphere(batch);
     } else {
-        DependencyManager::get<DeferredLightingEffect>()->renderSolidSphere(batch, 0.5f, SLICES, STACKS, sphereColor);
+        batch.setModelTransform(Transform());
+        DependencyManager::get<DeferredLightingEffect>()->renderSolidSphereInstance(batch, getTransformToCenter(), sphereColor);
     }
 
 
diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
index 930a684617..90aff03f55 100644
--- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
@@ -121,15 +121,15 @@ void RenderableZoneEntityItem::render(RenderArgs* args) {
                 
                 Q_ASSERT(args->_batch);
                 gpu::Batch& batch = *args->_batch;
-                batch.setModelTransform(getTransformToCenter());
-                
+                batch.setModelTransform(Transform());
+
+                auto xfm = getTransformToCenter();
                 auto deferredLightingEffect = DependencyManager::get<DeferredLightingEffect>();
-                
                 if (getShapeType() == SHAPE_TYPE_SPHERE) {
-                    const int SLICES = 15, STACKS = 15;
-                    deferredLightingEffect->renderWireSphere(batch, 0.5f, SLICES, STACKS, DEFAULT_COLOR);
+                    xfm.postScale(0.5);
+                    deferredLightingEffect->renderWireSphereInstance(batch, xfm, DEFAULT_COLOR);
                 } else {
-                    deferredLightingEffect->renderWireCube(batch, 1.0f, DEFAULT_COLOR);
+                    deferredLightingEffect->renderWireCubeInstance(batch, xfm, DEFAULT_COLOR);
                 }
                 break;
             }
diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp
index 6f69e8befc..0864414e5e 100644
--- a/libraries/fbx/src/FBXReader.cpp
+++ b/libraries/fbx/src/FBXReader.cpp
@@ -1436,12 +1436,12 @@ void buildModelMesh(ExtractedMesh& extracted, const QString& url) {
     if (clusterIndicesSize) {
         mesh.addAttribute(gpu::Stream::SKIN_CLUSTER_INDEX,
                           model::BufferView(attribBuffer, clusterIndicesOffset, clusterIndicesSize,
-                                            gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)));
+                                            gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)));
     }
     if (clusterWeightsSize) {
         mesh.addAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT,
                           model::BufferView(attribBuffer, clusterWeightsOffset, clusterWeightsSize,
-                                            gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW)));
+                                            gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW)));
     }
 
 
diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp
index e5ec8525b6..e6e176be88 100644
--- a/libraries/gpu/src/gpu/Batch.cpp
+++ b/libraries/gpu/src/gpu/Batch.cpp
@@ -304,12 +304,16 @@ bool Batch::isSkyboxEnabled() const {
     return _enableSkybox;
 }
 
-void Batch::setupNamedCalls(const std::string& instanceName, NamedBatchData::Function function) {
+void Batch::setupNamedCalls(const std::string& instanceName, size_t count, NamedBatchData::Function function) {
     NamedBatchData& instance = _namedData[instanceName];
-    ++instance._count;
+    instance._count += count;
     instance._function = function;
 }
 
+void Batch::setupNamedCalls(const std::string& instanceName, NamedBatchData::Function function) {
+    setupNamedCalls(instanceName, 1, function);
+}
+
 BufferPointer Batch::getNamedBuffer(const std::string& instanceName, uint8_t index) {
     NamedBatchData& instance = _namedData[instanceName];
     if (instance._buffers.size() <= index) {
diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h
index c3bf6250c5..ec6fb26c34 100644
--- a/libraries/gpu/src/gpu/Batch.h
+++ b/libraries/gpu/src/gpu/Batch.h
@@ -62,8 +62,10 @@ public:
         Function _function;
 
         void process(Batch& batch) {
+            if (_function) {
             _function(batch, *this);
         }
+        }
     };
 
     using NamedBatchDataMap = std::map<std::string, NamedBatchData>;
@@ -96,6 +98,7 @@ public:
     void drawIndexedInstanced(uint32 nbInstances, Primitive primitiveType, uint32 nbIndices, uint32 startIndex = 0, uint32 startInstance = 0);
 
 
+    void setupNamedCalls(const std::string& instanceName, size_t count, NamedBatchData::Function function);
     void setupNamedCalls(const std::string& instanceName, NamedBatchData::Function function);
     BufferPointer getNamedBuffer(const std::string& instanceName, uint8_t index = 0);
     
diff --git a/libraries/gpu/src/gpu/Format.cpp b/libraries/gpu/src/gpu/Format.cpp
new file mode 100644
index 0000000000..a66fc19458
--- /dev/null
+++ b/libraries/gpu/src/gpu/Format.cpp
@@ -0,0 +1,21 @@
+//
+//  Created by Bradley Austin Davis on 2015/09/20
+//  Copyright 2013-2015 High Fidelity, Inc.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#include "Format.h"
+
+using namespace gpu;
+
+const Element Element::COLOR_RGBA_32{ VEC4, NUINT8, RGBA };
+const Element Element::COLOR_RGBA{ VEC4, FLOAT, RGBA };
+const Element Element::VEC2F_UV{ VEC2, FLOAT, UV };
+const Element Element::VEC2F_XY{ VEC2, FLOAT, XY };
+const Element Element::VEC3F_XYZ{ VEC3, FLOAT, XYZ };
+const Element Element::VEC4F_XYZW{ VEC4, FLOAT, XYZW };
+const Element Element::INDEX_UINT16{ SCALAR, UINT16, INDEX };
+const Element Element::PART_DRAWCALL{ VEC4, UINT32, PART };
+
diff --git a/libraries/gpu/src/gpu/Format.h b/libraries/gpu/src/gpu/Format.h
index e16256574b..530db084a3 100644
--- a/libraries/gpu/src/gpu/Format.h
+++ b/libraries/gpu/src/gpu/Format.h
@@ -56,10 +56,8 @@ enum Type {
     INT8,
     UINT8,
 
-    NFLOAT,
     NINT32,
     NUINT32,
-    NHALF,
     NINT16,
     NUINT16,
     NINT8,
@@ -68,6 +66,7 @@ enum Type {
     NUM_TYPES,
 
     BOOL = UINT8,
+    NORMALIZED_START = NINT32,
 };
 // Array providing the size in bytes for a given scalar type
 static const int TYPE_SIZE[NUM_TYPES] = {
@@ -79,10 +78,10 @@ static const int TYPE_SIZE[NUM_TYPES] = {
     2,
     1,
     1,
+
+    // normalized values
     4,
     4,
-    4,
-    2,
     2,
     2,
     1,
@@ -99,10 +98,9 @@ static const bool TYPE_IS_INTEGER[NUM_TYPES] = {
     true,
     true,
 
-    false,
+    // Normalized values
     true,
     true,
-    false,
     true,
     true,
     true,
@@ -151,6 +149,7 @@ enum Semantic {
     RGB,
     RGBA,
     BGRA,
+    XY,
     XYZ,
     XYZW,
     QUAT,
@@ -199,7 +198,7 @@ public:
     uint8 getLocationCount() const { return  LOCATION_COUNT[(Dimension)_dimension]; }
 
     Type getType() const { return (Type)_type; }
-    bool isNormalized() const { return (getType() >= NFLOAT); }
+    bool isNormalized() const { return (getType() >= NORMALIZED_START); }
     bool isInteger() const { return TYPE_IS_INTEGER[getType()]; }
 
     uint32 getSize() const { return DIMENSION_COUNT[_dimension] * TYPE_SIZE[_type]; }
@@ -215,10 +214,14 @@ public:
     }
 
     static const Element COLOR_RGBA_32;
+    static const Element COLOR_RGBA;
+    static const Element VEC2F_UV;
+    static const Element VEC2F_XY;
     static const Element VEC3F_XYZ;
+    static const Element VEC4F_XYZW;
     static const Element INDEX_UINT16;
     static const Element PART_DRAWCALL;
-
+    
  protected:
     uint8 _semantic;
     uint8 _dimension : 4;
diff --git a/libraries/gpu/src/gpu/GLBackend.cpp b/libraries/gpu/src/gpu/GLBackend.cpp
index 43c01d0337..62508f273c 100644
--- a/libraries/gpu/src/gpu/GLBackend.cpp
+++ b/libraries/gpu/src/gpu/GLBackend.cpp
@@ -127,7 +127,12 @@ void GLBackend::renderPassTransfer(Batch& batch) {
     const size_t numCommands = batch.getCommands().size();
     const Batch::Commands::value_type* command = batch.getCommands().data();
     const Batch::CommandOffsets::value_type* offset = batch.getCommandOffsets().data();
-
+    
+    for (auto& cached : batch._buffers._items) {
+        if (cached._data) {
+            syncGPUObject(*cached._data);
+        }
+    }
     // Reset the transform buffers
     _transform._cameras.resize(0);
     _transform._cameraOffsets.clear();
@@ -330,7 +335,7 @@ void GLBackend::do_drawIndexedInstanced(Batch& batch, uint32 paramOffset) {
     uint32 startInstance = batch._params[paramOffset + 0]._uint;
     GLenum glType = _elementTypeToGLType[_input._indexBufferType];
 
-    glDrawElementsInstanced(mode, numIndices, glType, nullptr, numInstances);
+    glDrawElementsInstanced(mode, numIndices, glType, reinterpret_cast<GLvoid*>(startIndex + _input._indexBufferOffset), numInstances);
     (void)CHECK_GL_ERROR();
 }
 
diff --git a/libraries/gpu/src/gpu/GLBackendShared.h b/libraries/gpu/src/gpu/GLBackendShared.h
index 7ce54665be..21bd10a33a 100644
--- a/libraries/gpu/src/gpu/GLBackendShared.h
+++ b/libraries/gpu/src/gpu/GLBackendShared.h
@@ -34,10 +34,9 @@ static const GLenum _elementTypeToGLType[gpu::NUM_TYPES] = {
     GL_UNSIGNED_SHORT,
     GL_BYTE,
     GL_UNSIGNED_BYTE,
-    GL_FLOAT,
+    // Normalized values
     GL_INT,
     GL_UNSIGNED_INT,
-    GL_HALF_FLOAT,
     GL_SHORT,
     GL_UNSIGNED_SHORT,
     GL_BYTE,
diff --git a/libraries/gpu/src/gpu/GLBackendTexture.cpp b/libraries/gpu/src/gpu/GLBackendTexture.cpp
index d4e1db125d..dce5236868 100755
--- a/libraries/gpu/src/gpu/GLBackendTexture.cpp
+++ b/libraries/gpu/src/gpu/GLBackendTexture.cpp
@@ -156,7 +156,6 @@ public:
                         texel.internalFormat = GL_DEPTH_COMPONENT32;
                         break;
                         }
-                    case gpu::NFLOAT:
                     case gpu::FLOAT: {
                         texel.internalFormat = GL_DEPTH_COMPONENT32F;
                         break;
@@ -165,8 +164,7 @@ public:
                     case gpu::INT16:
                     case gpu::NUINT16:
                     case gpu::NINT16:
-                    case gpu::HALF:
-                    case gpu::NHALF: {
+                    case gpu::HALF: {
                         texel.internalFormat = GL_DEPTH_COMPONENT16;
                         break;
                         }
diff --git a/libraries/gpu/src/gpu/Resource.cpp b/libraries/gpu/src/gpu/Resource.cpp
index 4cdbe8203a..c162b38b93 100644
--- a/libraries/gpu/src/gpu/Resource.cpp
+++ b/libraries/gpu/src/gpu/Resource.cpp
@@ -14,11 +14,6 @@
 
 using namespace gpu;
 
-const Element Element::COLOR_RGBA_32 = Element(VEC4, NUINT8, RGBA);
-const Element Element::VEC3F_XYZ = Element(VEC3, FLOAT, XYZ);
-const Element Element::INDEX_UINT16 = Element(SCALAR, UINT16, INDEX);
-const Element Element::PART_DRAWCALL = Element(VEC4, UINT32, PART);
-
 Resource::Size Resource::Sysmem::allocateMemory(Byte** dataAllocated, Size size) {
     if ( !dataAllocated ) { 
         qWarning() << "Buffer::Sysmem::allocateMemory() : Must have a valid dataAllocated pointer.";
diff --git a/libraries/gpu/src/gpu/Resource.h b/libraries/gpu/src/gpu/Resource.h
index de5e4a7242..f330b0fd07 100644
--- a/libraries/gpu/src/gpu/Resource.h
+++ b/libraries/gpu/src/gpu/Resource.h
@@ -144,6 +144,11 @@ public:
         return append(sizeof(t), reinterpret_cast<const Byte*>(&t));
     }
 
+    template <typename T>
+    Size append(const std::vector<T>& t) {
+        return append(sizeof(T) * t.size(), reinterpret_cast<const Byte*>(&t[0]));
+    }
+
     // Access the sysmem object.
     const Sysmem& getSysmem() const { assert(_sysmem); return (*_sysmem); }
     Sysmem& editSysmem() { assert(_sysmem); return (*_sysmem); }
diff --git a/libraries/gpu/src/gpu/Stream.cpp b/libraries/gpu/src/gpu/Stream.cpp
index 634545b4dd..98d23ac266 100644
--- a/libraries/gpu/src/gpu/Stream.cpp
+++ b/libraries/gpu/src/gpu/Stream.cpp
@@ -15,6 +15,37 @@
 
 using namespace gpu;
 
+using ElementArray = std::array<Element, Stream::NUM_INPUT_SLOTS>;
+
+const ElementArray& getDefaultElements() {
+    static ElementArray defaultElements{
+        //POSITION = 0,
+        Element::VEC3F_XYZ,
+        //NORMAL = 1,
+        Element::VEC3F_XYZ,
+        //COLOR = 2,
+        Element::COLOR_RGBA_32,
+        //TEXCOORD0 = 3,
+        Element::VEC2F_UV,
+        //TANGENT = 4,
+        Element::VEC3F_XYZ,
+        //SKIN_CLUSTER_INDEX = 5,
+        Element::VEC4F_XYZW,
+        //SKIN_CLUSTER_WEIGHT = 6,
+        Element::VEC4F_XYZW,
+        //TEXCOORD1 = 7,
+        Element::VEC2F_UV,
+        //INSTANCE_SCALE = 8,
+        Element::VEC3F_XYZ,
+        //INSTANCE_TRANSLATE = 9,
+        Element::VEC3F_XYZ,
+        //INSTANCE_XFM = 10, 
+        // FIXME make a matrix element
+        Element::VEC4F_XYZW
+    };
+    return defaultElements;
+}
+
 void Stream::Format::evaluateCache() {
     _channels.clear();
     _elementTotalSize = 0;
@@ -34,6 +65,19 @@ bool Stream::Format::setAttribute(Slot slot, Slot channel, Element element, Offs
     return true;
 }
 
+bool Stream::Format::setAttribute(Slot slot, Frequency frequency) {
+    _attributes[slot] = Attribute((InputSlot)slot, slot, getDefaultElements()[slot], 0, frequency);
+    evaluateCache();
+    return true;
+}
+
+bool Stream::Format::setAttribute(Slot slot, Slot channel, Frequency frequency) {
+    _attributes[slot] = Attribute((InputSlot)slot, channel, getDefaultElements()[slot], 0, frequency);
+    evaluateCache();
+    return true;
+}
+
+
 BufferStream::BufferStream() :
     _buffers(),
     _offsets(),
diff --git a/libraries/gpu/src/gpu/Stream.h b/libraries/gpu/src/gpu/Stream.h
index c0ad1ebe46..420aa50f72 100644
--- a/libraries/gpu/src/gpu/Stream.h
+++ b/libraries/gpu/src/gpu/Stream.h
@@ -11,12 +11,14 @@
 #ifndef hifi_gpu_Stream_h
 #define hifi_gpu_Stream_h
 
+#include <vector>
+#include <map>
+#include <array>
+
 #include <assert.h>
 
 #include "Resource.h"
 #include "Format.h"
-#include <vector>
-#include <map>
 
 namespace gpu {
 
@@ -55,6 +57,8 @@ public:
     // Every thing that is needed to detail a stream attribute and how to interpret it
     class Attribute {
     public:
+        Attribute() {}
+
         Attribute(Slot slot, Slot channel, Element element, Offset offset = 0, Frequency frequency = PER_VERTEX) :
             _slot(slot),
             _channel(channel),
@@ -62,21 +66,12 @@ public:
             _offset(offset),
             _frequency(frequency)
         {}
-        Attribute() :
-            _slot(POSITION),
-            _channel(0),
-            _element(),
-            _offset(0),
-            _frequency(PER_VERTEX)
-        {}
 
-
-        Slot _slot; // Logical slot assigned to the attribute
-        Slot _channel; // index of the channel where to get the data from
-        Element _element;
-
-        Offset _offset;
-        uint32 _frequency;
+        Slot _slot{ POSITION }; // Logical slot assigned to the attribute
+        Slot _channel{ POSITION }; // index of the channel where to get the data from
+        Element _element{ Element::VEC3F_XYZ };
+        Offset _offset{ 0 };
+        uint32 _frequency{ PER_VERTEX };
 
         // Size of the 
         uint32 getSize() const { return _element.getSize(); }
@@ -113,6 +108,9 @@ public:
         uint32 getElementTotalSize() const { return _elementTotalSize; }
 
         bool setAttribute(Slot slot, Slot channel, Element element, Offset offset = 0, Frequency frequency = PER_VERTEX);
+        bool setAttribute(Slot slot, Frequency frequency = PER_VERTEX);
+        bool setAttribute(Slot slot, Slot channel, Frequency frequency = PER_VERTEX);
+
 
     protected:
         AttributeMap _attributes;
diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp
index 6c7310509a..1db26eae3b 100644
--- a/libraries/render-utils/src/DeferredLightingEffect.cpp
+++ b/libraries/render-utils/src/DeferredLightingEffect.cpp
@@ -187,15 +187,8 @@ gpu::PipelinePointer DeferredLightingEffect::bindSimpleProgram(gpu::Batch& batch
     return pipeline;
 }
 
+void DeferredLightingEffect::renderWireSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) {
 
-void DeferredLightingEffect::renderSolidSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color) {
-    bindSimpleProgram(batch);
-    DependencyManager::get<GeometryCache>()->renderSphere(batch, radius, slices, stacks, color);
-}
-
-void DeferredLightingEffect::renderWireSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color) {
-    bindSimpleProgram(batch);
-    DependencyManager::get<GeometryCache>()->renderSphere(batch, radius, slices, stacks, color, false);
 }
 
 uint32_t toCompactColor(const glm::vec4& color) {
@@ -206,39 +199,89 @@ uint32_t toCompactColor(const glm::vec4& color) {
     return compactColor;
 }
 
-void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) {
-    static const std::string INSTANCE_NAME = __FUNCTION__;
-    static const size_t TRANSFORM_BUFFER = 0;
-    static const size_t COLOR_BUFFER = 1;
+static const size_t INSTANCE_TRANSFORM_BUFFER = 0;
+static const size_t INSTANCE_COLOR_BUFFER = 1;
+
+template <typename F>
+void renderInstances(const std::string& name, gpu::Batch& batch, const Transform& xfm, const glm::vec4& color, F f) {
     {
-        gpu::BufferPointer instanceTransformBuffer = batch.getNamedBuffer(INSTANCE_NAME, TRANSFORM_BUFFER);
+        gpu::BufferPointer instanceTransformBuffer = batch.getNamedBuffer(name, INSTANCE_TRANSFORM_BUFFER);
         glm::mat4 xfmMat4;
         instanceTransformBuffer->append(xfm.getMatrix(xfmMat4));
 
-        gpu::BufferPointer instanceColorBuffer = batch.getNamedBuffer(INSTANCE_NAME, COLOR_BUFFER);
+        gpu::BufferPointer instanceColorBuffer = batch.getNamedBuffer(name, INSTANCE_COLOR_BUFFER);
         auto compactColor = toCompactColor(color);
         instanceColorBuffer->append(compactColor);
     }
 
-    batch.setupNamedCalls(INSTANCE_NAME, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
-        auto pipeline = bindSimpleProgram(batch);
+    auto that = DependencyManager::get<DeferredLightingEffect>();
+    batch.setupNamedCalls(name, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
+        auto pipeline = that->bindSimpleProgram(batch);
         auto location = pipeline->getProgram()->getUniforms().findLocation("Instanced");
 
         batch._glUniform1i(location, 1);
-        DependencyManager::get<GeometryCache>()->renderSolidCubeInstances(batch, data._count,
-            data._buffers[TRANSFORM_BUFFER], data._buffers[COLOR_BUFFER]);
+        f(batch, data);
         batch._glUniform1i(location, 0);
     });
 }
 
-void DeferredLightingEffect::renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color) {
-    bindSimpleProgram(batch);
-    DependencyManager::get<GeometryCache>()->renderSolidCube(batch, size, color);
+void DeferredLightingEffect::renderSolidSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) {
+    static const std::string INSTANCE_NAME = __FUNCTION__;
+    renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
+        DependencyManager::get<GeometryCache>()->renderSphereInstances(batch, data._count,
+            data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]);
+    });
 }
 
-void DeferredLightingEffect::renderWireCube(gpu::Batch& batch, float size, const glm::vec4& color) {
-    bindSimpleProgram(batch);
-    DependencyManager::get<GeometryCache>()->renderWireCube(batch, size, color);
+static auto startTime = usecTimestampNow();
+
+void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) {
+    static const std::string INSTANCE_NAME = __FUNCTION__;
+
+#ifdef DEBUG_SHAPES
+    renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
+
+        auto usecs = usecTimestampNow();
+        usecs -= startTime;
+        auto msecs = usecs / USECS_PER_MSEC;
+        float seconds = msecs;
+        seconds /= MSECS_PER_SECOND;
+        float fractionalSeconds = seconds - floor(seconds);
+        int shapeIndex = (int)seconds;
+
+        GeometryCache::Shape shapes[] = {
+            GeometryCache::Cube,
+            GeometryCache::Tetrahedron,
+            GeometryCache::Sphere,
+            GeometryCache::Icosahedron,
+            GeometryCache::Line,
+        };
+
+        shapeIndex %= 5;
+        GeometryCache::Shape shape = shapes[shapeIndex];
+
+        if (fractionalSeconds > 0.5f) {
+            DependencyManager::get<GeometryCache>()->renderShapeInstances(batch, shape, data._count,
+                data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]);
+        } else {
+            DependencyManager::get<GeometryCache>()->renderWireShapeInstances(batch, shape, data._count,
+                data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]);
+        }
+    });
+#else
+    renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
+        DependencyManager::get<GeometryCache>()->renderCubeInstances(batch, data._count,
+            data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]);
+    });
+#endif
+}
+
+void DeferredLightingEffect::renderWireCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) {
+    static const std::string INSTANCE_NAME = __FUNCTION__;
+    renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
+        DependencyManager::get<GeometryCache>()->renderWireCubeInstances(batch, data._count,
+            data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]);
+    });
 }
 
 void DeferredLightingEffect::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, const glm::vec3& maxCorner,
@@ -546,8 +589,10 @@ void DeferredLightingEffect::render(RenderArgs* args) {
                 } else {
                     Transform model;
                     model.setTranslation(glm::vec3(light->getPosition().x, light->getPosition().y, light->getPosition().z));
+                    model.postScale(expandedRadius);
                     batch.setModelTransform(model);
-                    geometryCache->renderSphere(batch, expandedRadius, 32, 32, glm::vec4(1.0f, 1.0f, 1.0f, 1.0f));
+                    batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
+                    geometryCache->renderSphere(batch);
                 }
             }
         }
diff --git a/libraries/render-utils/src/DeferredLightingEffect.h b/libraries/render-utils/src/DeferredLightingEffect.h
index 83bb4c215f..9c4809a82e 100644
--- a/libraries/render-utils/src/DeferredLightingEffect.h
+++ b/libraries/render-utils/src/DeferredLightingEffect.h
@@ -40,24 +40,26 @@ public:
     gpu::PipelinePointer bindSimpleProgram(gpu::Batch& batch, bool textured = false, bool culled = true,
                            bool emmisive = false, bool depthBias = false);
 
-    /// Sets up the state necessary to render static untextured geometry with the simple program.
-    void bindInstanceProgram(gpu::Batch& batch, bool textured = false, bool culled = true,
-        bool emmisive = false, bool depthBias = false);
+    void renderSolidSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color);
+    void renderSolidSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec3& color) { 
+        renderSolidSphereInstance(batch, xfm, glm::vec4(color, 1.0));
+    }
 
-    //// Renders a solid sphere with the simple program.
-    void renderSolidSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color);
+    void renderWireSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color);
+    void renderWireSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec3& color) {
+        renderWireSphereInstance(batch, xfm, glm::vec4(color, 1.0));
+    }
 
-    //// Renders a wireframe sphere with the simple program.
-    void renderWireSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color);
-    
-    //// Renders a solid cube using instancing.  Transform should include scaling.
     void renderSolidCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color);
+    void renderSolidCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec3& color) {
+        renderSolidCubeInstance(batch, xfm, glm::vec4(color, 1.0));
+    }
 
-    //// Renders a solid cube with the simple program.
-    void renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color);
+    void renderWireCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color);
+    void renderWireCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec3& color) {
+        renderWireCubeInstance(batch, xfm, glm::vec4(color, 1.0));
+    }
 
-    //// Renders a wireframe cube with the simple program.
-    void renderWireCube(gpu::Batch& batch, float size, const glm::vec4& color);
     
     //// Renders a quad with the simple program.
     void renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, const glm::vec3& maxCorner, const glm::vec4& color);
diff --git a/libraries/render-utils/src/Environment.cpp b/libraries/render-utils/src/Environment.cpp
index 605f67f957..bffac32b0c 100644
--- a/libraries/render-utils/src/Environment.cpp
+++ b/libraries/render-utils/src/Environment.cpp
@@ -197,6 +197,9 @@ bool Environment::findCapsulePenetration(const glm::vec3& start, const glm::vec3
 }
 
 void Environment::renderAtmosphere(gpu::Batch& batch, ViewFrustum& viewFrustum, const EnvironmentData& data) {
+    // FIXME atmosphere rendering is broken in some way, 
+    // should probably be replaced by a procedual skybox and put on the marketplace
+    return;
 
     glm::vec3 center = data.getAtmosphereCenter();
     
@@ -252,5 +255,6 @@ void Environment::renderAtmosphere(gpu::Batch& batch, ViewFrustum& viewFrustum,
     batch._glUniform1f(locations[G_LOCATION], -0.990f);
     batch._glUniform1f(locations[G2_LOCATION], -0.990f * -0.990f);
 
-    DependencyManager::get<GeometryCache>()->renderSphere(batch,1.0f, 100, 50, glm::vec4(1.0f, 0.0f, 0.0f, 0.5f)); //Draw a unit sphere
+    batch._glColor4f(1.0f, 0.0f, 0.0f, 0.5f);
+    DependencyManager::get<GeometryCache>()->renderSphere(batch); //Draw a unit sphere
 }
diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp
index ea05df84ef..53eb8a454b 100644
--- a/libraries/render-utils/src/GeometryCache.cpp
+++ b/libraries/render-utils/src/GeometryCache.cpp
@@ -33,11 +33,432 @@
 
 const int GeometryCache::UNKNOWN_ID = -1;
 
-GeometryCache::GeometryCache() :
-    _nextID(0)
-{
+static const uint FLOATS_PER_VERTEX = 3;
+static const uint VERTICES_PER_TRIANGLE = 3;
+static const uint TRIANGLES_PER_QUAD = 2;
+static const uint CUBE_FACES = 6;
+static const uint CUBE_VERTICES_PER_FACE = 4;
+static const uint CUBE_VERTICES = CUBE_FACES * CUBE_VERTICES_PER_FACE;
+static const uint CUBE_VERTEX_POINTS = CUBE_VERTICES * FLOATS_PER_VERTEX;
+static const uint CUBE_INDICES = CUBE_FACES * TRIANGLES_PER_QUAD * VERTICES_PER_TRIANGLE;
+static const uint SPHERE_LATITUDES = 24;
+static const uint SPHERE_MERIDIANS = SPHERE_LATITUDES * 2;
+static const uint SPHERE_INDICES = SPHERE_MERIDIANS * (SPHERE_LATITUDES - 1) * TRIANGLES_PER_QUAD * VERTICES_PER_TRIANGLE;
+
+static const gpu::Element POSITION_ELEMENT{ gpu::VEC3, gpu::FLOAT, gpu::XYZ };
+static const gpu::Element NORMAL_ELEMENT{ gpu::VEC3, gpu::FLOAT, gpu::XYZ };
+static const gpu::Element COLOR_ELEMENT{ gpu::VEC4, gpu::NUINT8, gpu::RGBA };
+static const gpu::Element TRANSFORM_ELEMENT{ gpu::MAT4, gpu::FLOAT, gpu::XYZW };
+
+static gpu::Stream::FormatPointer SOLID_STREAM_FORMAT;
+static gpu::Stream::FormatPointer INSTANCED_SOLID_STREAM_FORMAT;
+
+static const uint SHAPE_VERTEX_STRIDE = sizeof(glm::vec3) * 2; // vertices and normals
+static const uint SHAPE_NORMALS_OFFSET = sizeof(glm::vec3);
+
+
+void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, const VVertex& vertices) {
+    vertexBuffer->append(vertices);
+
+    _positionView = gpu::BufferView(vertexBuffer, 0,
+        vertexBuffer->getSize(), SHAPE_VERTEX_STRIDE, POSITION_ELEMENT);
+    _normalView = gpu::BufferView(vertexBuffer, SHAPE_NORMALS_OFFSET,
+        vertexBuffer->getSize(), SHAPE_VERTEX_STRIDE, NORMAL_ELEMENT);
+}
+
+void GeometryCache::ShapeData::setupIndices(gpu::BufferPointer& indexBuffer, const VIndex& indices, const VIndex& wireIndices) {
+    _indices = indexBuffer;
+    if (!indices.empty()) {
+        _indexOffset = indexBuffer->getSize();
+        _indexCount = indices.size();
+        indexBuffer->append(indices);
+    }
+
+    if (!wireIndices.empty()) {
+        _wireIndexOffset = indexBuffer->getSize();
+        _wireIndexCount = wireIndices.size();
+        indexBuffer->append(wireIndices);
+    }
+}
+
+void GeometryCache::ShapeData::setupBatch(gpu::Batch& batch) const {
+    batch.setInputBuffer(gpu::Stream::POSITION, _positionView);
+    batch.setInputBuffer(gpu::Stream::NORMAL, _normalView);
+}
+
+void GeometryCache::ShapeData::draw(gpu::Batch& batch) const {
+    if (_indexCount) {
+        setupBatch(batch);
+        batch.setIndexBuffer(gpu::UINT16, _indices, _indexOffset);
+        batch.drawIndexed(gpu::TRIANGLES, _indexCount);
+    }
+}
+
+void GeometryCache::ShapeData::drawWire(gpu::Batch& batch) const {
+    if (_wireIndexCount) {
+        setupBatch(batch);
+        batch.setIndexBuffer(gpu::UINT16, _indices, _wireIndexOffset);
+        batch.drawIndexed(gpu::LINES, _wireIndexCount);
+    }
+}
+
+void GeometryCache::ShapeData::drawInstances(gpu::Batch& batch, size_t count) const {
+    if (_indexCount) {
+        setupBatch(batch);
+        batch.setIndexBuffer(gpu::UINT16, _indices, _indexOffset);
+        batch.drawIndexedInstanced(count, gpu::TRIANGLES, _indexCount);
+    }
+}
+
+void GeometryCache::ShapeData::drawWireInstances(gpu::Batch& batch, size_t count) const {
+    if (_wireIndexCount) {
+        setupBatch(batch);
+        batch.setIndexBuffer(gpu::UINT16, _indices, _wireIndexOffset);
+        batch.drawIndexedInstanced(count, gpu::LINES, _wireIndexCount);
+    }
+}
+
+const VVertex& icosahedronVertices() {
+    static const float phi = (1.0 + sqrt(5.0)) / 2.0;
+    static const float a = 0.5;
+    static const float b = 1.0 / (2.0 * phi);
+
+    static const VVertex vertices{ //
+        vec3(0, b, -a), vec3(-b, a, 0), vec3(b, a, 0), // 
+        vec3(0, b, a), vec3(b, a, 0), vec3(-b, a, 0), //
+        vec3(0, b, a), vec3(-a, 0, b), vec3(0, -b, a), //
+        vec3(0, b, a), vec3(0, -b, a), vec3(a, 0, b),  //
+        vec3(0, b, -a), vec3(a, 0, -b), vec3(0, -b, -a),// 
+        vec3(0, b, -a), vec3(0, -b, -a), vec3(-a, 0, -b), //
+        vec3(0, -b, a), vec3(-b, -a, 0), vec3(b, -a, 0), //
+        vec3(0, -b, -a), vec3(b, -a, 0), vec3(-b, -a, 0), //
+        vec3(-b, a, 0), vec3(-a, 0, -b),  vec3(-a, 0, b), //
+        vec3(-b, -a, 0), vec3(-a, 0, b),  vec3(-a, 0, -b), //
+        vec3(b, a, 0), vec3(a, 0, b), vec3(a, 0, -b),   //
+        vec3(b, -a, 0), vec3(a, 0, -b), vec3(a, 0, b),  //
+        vec3(0, b, a), vec3(-b, a, 0),  vec3(-a, 0, b), //
+        vec3(0, b, a), vec3(a, 0, b), vec3(b, a, 0), //
+        vec3(0, b, -a), vec3(-a, 0, -b), vec3(-b, a, 0), // 
+        vec3(0, b, -a), vec3(b, a, 0),  vec3(a, 0, -b), //
+        vec3(0, -b, -a), vec3(-b, -a, 0), vec3(-a, 0, -b), // 
+        vec3(0, -b, -a), vec3(a, 0, -b), vec3(b, -a, 0), //
+        vec3(0, -b, a), vec3(-a, 0, b),  vec3(-b, -a, 0), //
+        vec3(0, -b, a), vec3(b, -a, 0), vec3(a, 0, b)
+    }; //
+    return vertices;
+}
+
+const VVertex& tetrahedronVertices() {
+    static const float a = 1.0f / sqrt(2.0f);
+    static const auto A = vec3(0, 1, a);
+    static const auto B = vec3(0, -1, a);
+    static const auto C = vec3(1, 0, -a);
+    static const auto D = vec3(-1, 0, -a);
+    static const VVertex vertices{
+        A, B, C,
+        D, B, A,
+        C, D, A,
+        C, B, D,
+    };
+    return vertices;
+}
+
+VVertex tesselate(const VVertex& startingTriangles, int count) {
+    VVertex triangles = startingTriangles;
+    if (0 != (triangles.size() % 3)) {
+        throw std::runtime_error("Bad number of vertices for tesselation");
+    }
+
+    for (size_t i = 0; i < triangles.size(); ++i) {
+        triangles[i] = glm::normalize(triangles[i]);
+    }
+
+    VVertex newTriangles;
+    while (count) {
+        newTriangles.clear();
+        newTriangles.reserve(triangles.size() * 4);
+        for (size_t i = 0; i < triangles.size(); i += 3) {
+            const vec3& a = triangles[i];
+            const vec3& b = triangles[i + 1];
+            const vec3& c = triangles[i + 2];
+            vec3 ab = glm::normalize(a + b);
+            vec3 bc = glm::normalize(b + c);
+            vec3 ca = glm::normalize(c + a);
+
+            newTriangles.push_back(a);
+            newTriangles.push_back(ab);
+            newTriangles.push_back(ca);
+
+            newTriangles.push_back(b);
+            newTriangles.push_back(bc);
+            newTriangles.push_back(ab);
+
+            newTriangles.push_back(c);
+            newTriangles.push_back(ca);
+            newTriangles.push_back(bc);
+
+            newTriangles.push_back(ab);
+            newTriangles.push_back(bc);
+            newTriangles.push_back(ca);
+        }
+        triangles.swap(newTriangles);
+        --count;
+    }
+    return triangles;
+}
+
+// FIXME solids need per-face vertices, but smooth shaded
+// components do not.  Find a way to support using draw elements
+// or draw arrays as appropriate
+// Maybe special case cone and cylinder since they combine flat
+// and smooth shading
+void GeometryCache::buildShapes() {
+    auto vertexBuffer = std::make_shared<gpu::Buffer>();
+    auto indexBuffer = std::make_shared<gpu::Buffer>();
+    uint16_t startingIndex = 0;
+    
+    // Cube 
+    startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE;
+    {
+        ShapeData& shapeData = _shapes[Cube];
+        VVertex vertices;
+        // front
+        vertices.push_back(vec3(1, 1, 1));
+        vertices.push_back(vec3(0, 0, 1));
+        vertices.push_back(vec3(-1, 1, 1));
+        vertices.push_back(vec3(0, 0, 1));
+        vertices.push_back(vec3(-1, -1, 1));
+        vertices.push_back(vec3(0, 0, 1));
+        vertices.push_back(vec3(1, -1, 1));
+        vertices.push_back(vec3(0, 0, 1));
+
+        // right
+        vertices.push_back(vec3(1, 1, 1));
+        vertices.push_back(vec3(1, 0, 0));
+        vertices.push_back(vec3(1, -1, 1));
+        vertices.push_back(vec3(1, 0, 0));
+        vertices.push_back(vec3(1, -1, -1));
+        vertices.push_back(vec3(1, 0, 0));
+        vertices.push_back(vec3(1, 1, -1));
+        vertices.push_back(vec3(1, 0, 0));
+
+        // top
+        vertices.push_back(vec3(1, 1, 1));
+        vertices.push_back(vec3(0, 1, 0));
+        vertices.push_back(vec3(1, 1, -1));
+        vertices.push_back(vec3(0, 1, 0));
+        vertices.push_back(vec3(-1, 1, -1));
+        vertices.push_back(vec3(0, 1, 0));
+        vertices.push_back(vec3(-1, 1, 1));
+        vertices.push_back(vec3(0, 1, 0));
+
+        // left
+        vertices.push_back(vec3(-1, 1, 1));
+        vertices.push_back(vec3(-1, 0, 0));
+        vertices.push_back(vec3(-1, 1, -1));
+        vertices.push_back(vec3(-1, 0, 0));
+        vertices.push_back(vec3(-1, -1, -1));
+        vertices.push_back(vec3(-1, 0, 0));
+        vertices.push_back(vec3(-1, -1, 1));
+        vertices.push_back(vec3(-1, 0, 0));
+
+        // bottom
+        vertices.push_back(vec3(-1, -1, -1));
+        vertices.push_back(vec3(0, -1, 0));
+        vertices.push_back(vec3(1, -1, -1));
+        vertices.push_back(vec3(0, -1, 0));
+        vertices.push_back(vec3(1, -1, 1));
+        vertices.push_back(vec3(0, -1, 0));
+        vertices.push_back(vec3(-1, -1, 1));
+        vertices.push_back(vec3(0, -1, 0));
+
+        // back
+        vertices.push_back(vec3(1, -1, -1));
+        vertices.push_back(vec3(0, 0, -1));
+        vertices.push_back(vec3(-1, -1, -1));
+        vertices.push_back(vec3(0, 0, -1));
+        vertices.push_back(vec3(-1, 1, -1));
+        vertices.push_back(vec3(0, 0, -1));
+        vertices.push_back(vec3(1, 1, -1));
+        vertices.push_back(vec3(0, 0, -1));
+
+        for (size_t i = 0; i < vertices.size(); ++i) {
+            if (0 == i % 2) {
+                vertices[i] *= 0.5f;
+            }
+        }
+        shapeData.setupVertices(_shapeVertices, vertices);
+
+        VIndex indices{
+            0, 1, 2, 2, 3, 0, // front
+            4, 5, 6, 6, 7, 4, // right
+            8, 9, 10, 10, 11, 8, // top
+            12, 13, 14, 14, 15, 12, // left
+            16, 17, 18, 18, 19, 16, // bottom
+            20, 21, 22, 22, 23, 20  // back
+        };
+        for (int i = 0; i < indices.size(); ++i) {
+            indices[i] += startingIndex;
+        }
+
+        VIndex wireIndices{
+            0, 1, 1, 2, 2, 3, 3, 0, // front
+            20, 21, 21, 22, 22, 23, 23, 20, // back
+            0, 23, 1, 22, 2, 21, 3, 20 // sides
+        };
+        for (int i = 0; i < wireIndices.size(); ++i) {
+            indices[i] += startingIndex;
+        }
+
+        shapeData.setupIndices(_shapeIndices, indices, wireIndices);
+    }
+
+    // Tetrahedron
+    startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE;
+    {
+        ShapeData& shapeData = _shapes[Tetrahedron];
+        size_t vertexCount = 4;
+        VVertex vertices;
+        {
+            VVertex originalVertices = tetrahedronVertices();
+            vertexCount = originalVertices.size();
+            vertices.reserve(originalVertices.size() * 2);
+            for (size_t i = 0; i < originalVertices.size(); i += 3) {
+                vec3 faceNormal;
+                for (size_t j = 0; j < 3; ++j) {
+                    faceNormal += originalVertices[i + j];
+                }
+                faceNormal = glm::normalize(faceNormal);
+                for (size_t j = 0; j < 3; ++j) {
+                    vertices.push_back(glm::normalize(originalVertices[i + j]) * 0.5f);
+                    vertices.push_back(faceNormal);
+                }
+            }
+        }
+        shapeData.setupVertices(_shapeVertices, vertices);
+
+        VIndex indices;
+        for (size_t i = 0; i < vertexCount; i += 3) {
+            for (size_t j = 0; j < 3; ++j) {
+                indices.push_back(i + j + startingIndex);
+            }
+        }
+
+        VIndex wireIndices{
+            0, 1, 1, 2, 2, 0,
+            0, 3, 1, 3, 2, 3,
+        };
+
+        for (int i = 0; i < wireIndices.size(); ++i) {
+            wireIndices[i] += startingIndex;
+        }
+
+        shapeData.setupIndices(_shapeIndices, indices, wireIndices);
+    }
+
+    // Sphere
+    // FIXME this uses way more vertices than required.  Should find a way to calculate the indices
+    // using shared vertices for better vertex caching
+    startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE;
+    {
+        ShapeData& shapeData = _shapes[Sphere];
+        VVertex vertices;
+        VIndex indices;
+        {
+            VVertex originalVertices = tesselate(icosahedronVertices(), 3);
+            vertices.reserve(originalVertices.size() * 2);
+            for (size_t i = 0; i < originalVertices.size(); i += 3) {
+                for (int j = 0; j < 3; ++j) {
+                    vertices.push_back(originalVertices[i + j] * 0.5f);
+                    vertices.push_back(originalVertices[i + j]);
+                    indices.push_back(i + j + startingIndex);
+                }
+            }
+        }
+        
+        shapeData.setupVertices(_shapeVertices, vertices);
+        // FIXME don't use solid indices for wire drawing.  
+        shapeData.setupIndices(_shapeIndices, indices, indices);
+    }
+
+    // Icosahedron
+    startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE;
+    {
+        ShapeData& shapeData = _shapes[Icosahedron];
+
+        VVertex vertices;
+        VIndex indices;
+        {
+            const VVertex& originalVertices = icosahedronVertices();
+            vertices.reserve(originalVertices.size() * 2);
+            for (size_t i = 0; i < originalVertices.size(); i += 3) {
+                vec3 faceNormal;
+                for (size_t j = 0; j < 3; ++j) {
+                    faceNormal += originalVertices[i + j];
+                }
+                faceNormal = glm::normalize(faceNormal);
+                for (int j = 0; j < 3; ++j) {
+                    vertices.push_back(glm::normalize(originalVertices[i + j]) * 0.5f);
+                    vertices.push_back(faceNormal);
+                    indices.push_back(i + j + startingIndex);
+                }
+            }
+        }
+
+        shapeData.setupVertices(_shapeVertices, vertices);
+        // FIXME don't use solid indices for wire drawing.  
+        shapeData.setupIndices(_shapeIndices, indices, indices);
+    }
+
+    //Triangle,
+    //Quad,
+    //Circle,
+    //Octahetron,
+    //Dodecahedron,
+    //Torus,
+    //Cone,
+    //Cylinder,
+    // Line
+    startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE;
+    {
+        ShapeData& shapeData = _shapes[Line];
+        shapeData.setupVertices(_shapeVertices, VVertex{
+            vec3(-0.5, 0, 0), vec3(-0.5, 0, 0),
+            vec3(0.5f, 0, 0), vec3(0.5f, 0, 0)
+        });
+        VIndex wireIndices;
+        wireIndices.push_back(0 + startingIndex);
+        wireIndices.push_back(1 + startingIndex);
+
+        shapeData.setupIndices(_shapeIndices, VIndex(), wireIndices);
+    }
+}
+
+gpu::Stream::FormatPointer& getSolidStreamFormat() {
+    if (!SOLID_STREAM_FORMAT) {
+        SOLID_STREAM_FORMAT = std::make_shared<gpu::Stream::Format>(); // 1 for everyone
+        SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, POSITION_ELEMENT);
+        SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT);
+    }
+    return SOLID_STREAM_FORMAT;
+}
+
+gpu::Stream::FormatPointer& getInstancedSolidStreamFormat() {
+    if (!INSTANCED_SOLID_STREAM_FORMAT) {
+        INSTANCED_SOLID_STREAM_FORMAT = std::make_shared<gpu::Stream::Format>(); // 1 for everyone
+        INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, POSITION_ELEMENT);
+        INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT);
+        INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
+        INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::INSTANCE_XFM, gpu::Stream::INSTANCE_XFM, TRANSFORM_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
+    }
+    return INSTANCED_SOLID_STREAM_FORMAT;
+}
+
+
+GeometryCache::GeometryCache() {
     const qint64 GEOMETRY_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE;
     setUnusedResourceCacheSize(GEOMETRY_DEFAULT_UNUSED_MAX_SIZE);
+    buildShapes();
 }
 
 GeometryCache::~GeometryCache() {
@@ -56,255 +477,64 @@ QSharedPointer<Resource> GeometryCache::createResource(const QUrl& url, const QS
     return QSharedPointer<Resource>();
 }
 
-const int NUM_VERTICES_PER_TRIANGLE = 3;
-const int NUM_TRIANGLES_PER_QUAD = 2;
-const int NUM_VERTICES_PER_TRIANGULATED_QUAD = NUM_VERTICES_PER_TRIANGLE * NUM_TRIANGLES_PER_QUAD;
-const int NUM_COORDS_PER_VERTEX = 3;
-
-void GeometryCache::renderSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color, bool solid, int id) {
-    bool registered = (id != UNKNOWN_ID);
-
-    Vec2Pair radiusKey(glm::vec2(radius, slices), glm::vec2(stacks, 0));
-    IntPair slicesStacksKey(slices, stacks);
-    Vec3Pair colorKey(glm::vec3(color.x, color.y, slices), glm::vec3(color.z, color.w, stacks));
-
-    int vertices = slices * (stacks - 1) + 2;    
-    int indices = slices * (stacks - 1) * NUM_VERTICES_PER_TRIANGULATED_QUAD;
-
-    if ((registered && (!_registeredSphereVertices.contains(id) || _lastRegisteredSphereVertices[id] != radiusKey))
-        || (!registered && !_sphereVertices.contains(radiusKey))) {
-
-        if (registered && _registeredSphereVertices.contains(id)) {
-            _registeredSphereVertices[id].reset();
-            #ifdef WANT_DEBUG
-                qCDebug(renderutils) << "renderSphere()... RELEASING REGISTERED VERTICES BUFFER";
-            #endif
-        }
-
-        auto verticesBuffer = std::make_shared<gpu::Buffer>();
-        if (registered) {
-            _registeredSphereVertices[id] = verticesBuffer;
-            _lastRegisteredSphereVertices[id] = radiusKey;
-        } else {
-            _sphereVertices[radiusKey] = verticesBuffer;
-        }
-
-        GLfloat* vertexData = new GLfloat[vertices * NUM_COORDS_PER_VERTEX];
-        GLfloat* vertex = vertexData;
-
-        // south pole
-        *(vertex++) = 0.0f;
-        *(vertex++) = 0.0f;
-        *(vertex++) = -1.0f * radius;
-
-        //add stacks vertices climbing up Y axis
-        for (int i = 1; i < stacks; i++) {
-            float phi = PI * (float)i / (float)(stacks) - PI_OVER_TWO;
-            float z = sinf(phi) * radius;
-            float stackRadius = cosf(phi) * radius;
-            
-            for (int j = 0; j < slices; j++) {
-                float theta = TWO_PI * (float)j / (float)slices;
-
-                *(vertex++) = sinf(theta) * stackRadius;
-                *(vertex++) = cosf(theta) * stackRadius;
-                *(vertex++) = z;
-            }
-        }
-
-        // north pole
-        *(vertex++) = 0.0f;
-        *(vertex++) = 0.0f;
-        *(vertex++) = 1.0f * radius;
-
-        verticesBuffer->append(sizeof(GLfloat) * vertices * NUM_COORDS_PER_VERTEX, (gpu::Byte*) vertexData);
-        delete[] vertexData;
-
-        #ifdef WANT_DEBUG
-            qCDebug(renderutils) << "GeometryCache::renderSphere()... --- CREATING VERTICES BUFFER";
-            qCDebug(renderutils) << "    radius:" << radius;
-            qCDebug(renderutils) << "    slices:" << slices;
-            qCDebug(renderutils) << "    stacks:" << stacks;
-
-            qCDebug(renderutils) << "    _sphereVertices.size():" << _sphereVertices.size();
-        #endif
-    }
-    #ifdef WANT_DEBUG
-    else if (registered) {
-        qCDebug(renderutils) << "renderSphere()... REUSING PREVIOUSLY REGISTERED VERTICES BUFFER";
-    }
-    #endif
-
-    if ((registered && (!_registeredSphereIndices.contains(id) || _lastRegisteredSphereIndices[id] != slicesStacksKey))
-        || (!registered && !_sphereIndices.contains(slicesStacksKey))) {
-
-        if (registered && _registeredSphereIndices.contains(id)) {
-            _registeredSphereIndices[id].reset();
-            #ifdef WANT_DEBUG
-                qCDebug(renderutils) << "renderSphere()... RELEASING REGISTERED INDICES BUFFER";
-            #endif
-        }
-
-        auto indicesBuffer = std::make_shared<gpu::Buffer>();
-        if (registered) {
-            _registeredSphereIndices[id] = indicesBuffer;
-            _lastRegisteredSphereIndices[id] = slicesStacksKey;
-        } else {
-            _sphereIndices[slicesStacksKey] = indicesBuffer;
-        }
-
-        GLushort* indexData = new GLushort[indices];
-        GLushort* index = indexData;
-        
-        int indexCount = 0;
-
-        // South cap
-        GLushort bottom = 0;
-        GLushort top = 1;
-        for (int i = 0; i < slices; i++) {    
-            *(index++) = bottom;
-            *(index++) = top + i;
-            *(index++) = top + (i + 1) % slices;
-            
-            indexCount += 3;
-        }
-
-        // (stacks - 2) ribbons
-        for (int i = 0; i < stacks - 2; i++) {
-            bottom = i * slices + 1;
-            top = bottom + slices;
-            for (int j = 0; j < slices; j++) {
-                int next = (j + 1) % slices;
-                
-                *(index++) = top + next;
-                *(index++) = bottom + j;
-                *(index++) = top + j;
-
-                indexCount += 3;
-                
-                *(index++) = bottom + next;
-                *(index++) = bottom + j;
-                *(index++) = top + next;
-
-                indexCount += 3;
-
-            }
-        }
-        
-        // north cap
-        bottom = (stacks - 2) * slices + 1;
-        top = bottom + slices;
-        for (int i = 0; i < slices; i++) {    
-            *(index++) = bottom + (i + 1) % slices;
-            *(index++) = bottom + i;
-            *(index++) = top;
-    
-            indexCount += 3;
-
-        }
-        indicesBuffer->append(sizeof(GLushort) * indices, (gpu::Byte*) indexData);
-        delete[] indexData;
-        
-        #ifdef WANT_DEBUG
-            qCDebug(renderutils) << "GeometryCache::renderSphere()... --- CREATING INDICES BUFFER";
-            qCDebug(renderutils) << "    radius:" << radius;
-            qCDebug(renderutils) << "    slices:" << slices;
-            qCDebug(renderutils) << "    stacks:" << stacks;
-            qCDebug(renderutils) << "indexCount:" << indexCount;
-            qCDebug(renderutils) << "   indices:" << indices;
-            qCDebug(renderutils) << "    _sphereIndices.size():" << _sphereIndices.size();
-        #endif
-    }
-    #ifdef WANT_DEBUG
-    else if (registered) {
-        qCDebug(renderutils) << "renderSphere()... REUSING PREVIOUSLY REGISTERED INDICES BUFFER";
-    }
-    #endif
-
-    if ((registered && (!_registeredSphereColors.contains(id) || _lastRegisteredSphereColors[id] != colorKey)) 
-        || (!registered && !_sphereColors.contains(colorKey))) {
-
-        if (registered && _registeredSphereColors.contains(id)) {
-            _registeredSphereColors[id].reset();
-            #ifdef WANT_DEBUG
-                qCDebug(renderutils) << "renderSphere()... RELEASING REGISTERED COLORS BUFFER";
-            #endif
-        }
-        
-        auto colorBuffer = std::make_shared<gpu::Buffer>();
-        if (registered) {
-            _registeredSphereColors[id] = colorBuffer;
-            _lastRegisteredSphereColors[id] = colorKey;
-        } else {
-            _sphereColors[colorKey] = colorBuffer;
-        }
-
-        int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
-                            ((int(color.y * 255.0f) & 0xFF) << 8) |
-                            ((int(color.z * 255.0f) & 0xFF) << 16) |
-                            ((int(color.w * 255.0f) & 0xFF) << 24);
-
-        int* colorData = new int[vertices];
-        int* colorDataAt = colorData;
-
-        for(int v = 0; v < vertices; v++) {
-            *(colorDataAt++) = compactColor;
-        }
-
-        colorBuffer->append(sizeof(int) * vertices, (gpu::Byte*) colorData);
-        delete[] colorData;
-
-        #ifdef WANT_DEBUG
-            qCDebug(renderutils) << "GeometryCache::renderSphere()... --- CREATING COLORS BUFFER";
-            qCDebug(renderutils) << "    vertices:" << vertices;
-            qCDebug(renderutils) << "    color:" << color;
-            qCDebug(renderutils) << "    slices:" << slices;
-            qCDebug(renderutils) << "    stacks:" << stacks;
-            qCDebug(renderutils) << "    _sphereColors.size():" << _sphereColors.size();
-        #endif
-    }
-    #ifdef WANT_DEBUG
-    else if (registered) {
-        qCDebug(renderutils) << "renderSphere()... REUSING PREVIOUSLY REGISTERED COLORS BUFFER";
-    }
-    #endif
-
-    gpu::BufferPointer verticesBuffer = registered ? _registeredSphereVertices[id] : _sphereVertices[radiusKey];
-    gpu::BufferPointer indicesBuffer = registered ? _registeredSphereIndices[id] : _sphereIndices[slicesStacksKey];
-    gpu::BufferPointer colorBuffer = registered ? _registeredSphereColors[id] : _sphereColors[colorKey];
-    
-    const int VERTICES_SLOT = 0;
-    const int NORMALS_SLOT = 1;
-    const int COLOR_SLOT = 2;
-    static gpu::Stream::FormatPointer streamFormat;
-    static gpu::Element positionElement, normalElement, colorElement;
-    if (!streamFormat) {
-        streamFormat = std::make_shared<gpu::Stream::Format>(); // 1 for everyone
-        streamFormat->setAttribute(gpu::Stream::POSITION, VERTICES_SLOT, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0);
-        streamFormat->setAttribute(gpu::Stream::NORMAL, NORMALS_SLOT, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
-        streamFormat->setAttribute(gpu::Stream::COLOR, COLOR_SLOT, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
-        positionElement = streamFormat->getAttributes().at(gpu::Stream::POSITION)._element;
-        normalElement = streamFormat->getAttributes().at(gpu::Stream::NORMAL)._element;
-        colorElement = streamFormat->getAttributes().at(gpu::Stream::COLOR)._element;
-    }
-
-    gpu::BufferView verticesView(verticesBuffer, positionElement);
-    gpu::BufferView normalsView(verticesBuffer, normalElement);
-    gpu::BufferView colorView(colorBuffer, colorElement);
-    
-    batch.setInputFormat(streamFormat);
-    batch.setInputBuffer(VERTICES_SLOT, verticesView);
-    batch.setInputBuffer(NORMALS_SLOT, normalsView);
-    batch.setInputBuffer(COLOR_SLOT, colorView);
-    batch.setIndexBuffer(gpu::UINT16, indicesBuffer, 0);
-
-    if (solid) {
-        batch.drawIndexed(gpu::TRIANGLES, indices);
-    } else {
-        batch.drawIndexed(gpu::LINES, indices);
-    }
+void setupBatchInstance(gpu::Batch& batch, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer) {
+    gpu::BufferView colorView(colorBuffer, COLOR_ELEMENT);
+    batch.setInputBuffer(gpu::Stream::COLOR, colorView);
+    gpu::BufferView instanceXfmView(transformBuffer, 0, transformBuffer->getSize(), TRANSFORM_ELEMENT);
+    batch.setInputBuffer(gpu::Stream::INSTANCE_XFM, instanceXfmView);
 }
 
+void GeometryCache::renderShape(gpu::Batch& batch, Shape shape) {
+    batch.setInputFormat(getSolidStreamFormat());
+    _shapes[shape].draw(batch);
+}
+
+void GeometryCache::renderWireShape(gpu::Batch& batch, Shape shape) {
+    batch.setInputFormat(getSolidStreamFormat());
+    _shapes[shape].drawWire(batch);
+}
+
+void GeometryCache::renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& transformBuffer, gpu::BufferPointer& colorBuffer) {
+    batch.setInputFormat(getInstancedSolidStreamFormat());
+    setupBatchInstance(batch, transformBuffer, colorBuffer);
+    _shapes[shape].drawInstances(batch, count);
+}
+
+void GeometryCache::renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& transformBuffer, gpu::BufferPointer& colorBuffer) {
+    batch.setInputFormat(getInstancedSolidStreamFormat());
+    setupBatchInstance(batch, transformBuffer, colorBuffer);
+    _shapes[shape].drawWireInstances(batch, count);
+}
+
+void GeometryCache::renderCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer) {
+    renderShapeInstances(batch, Cube, count, transformBuffer, colorBuffer);
+}
+
+void GeometryCache::renderWireCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer) {
+    renderWireShapeInstances(batch, Cube, count, transformBuffer, colorBuffer);
+}
+
+void GeometryCache::renderCube(gpu::Batch& batch) {
+    renderShape(batch, Cube);
+}
+
+void GeometryCache::renderWireCube(gpu::Batch& batch) {
+    renderWireShape(batch, Cube);
+}
+
+void GeometryCache::renderSphereInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer) {
+    renderShapeInstances(batch, Sphere, count, transformBuffer, colorBuffer);
+}
+
+void GeometryCache::renderSphere(gpu::Batch& batch) {
+    renderShape(batch, Sphere);
+}
+
+void GeometryCache::renderWireSphere(gpu::Batch& batch) {
+    renderWireShape(batch, Sphere);
+}
+
+
 void GeometryCache::renderGrid(gpu::Batch& batch, int xDivisions, int yDivisions, const glm::vec4& color) {
     IntPair key(xDivisions, yDivisions);
     Vec3Pair colorKey(glm::vec3(color.x, color.y, yDivisions), glm::vec3(color.z, color.y, xDivisions));
@@ -689,243 +919,6 @@ void GeometryCache::renderVertices(gpu::Batch& batch, gpu::Primitive primitiveTy
     }
 }
 
-static const int FLOATS_PER_VERTEX = 3;
-static const int VERTICES_PER_TRIANGLE = 3;
-
-static const int CUBE_NUMBER_OF_FACES = 6;
-static const int CUBE_VERTICES_PER_FACE = 4;
-static const int CUBE_TRIANGLES_PER_FACE = 2;
-static const int CUBE_VERTICES = CUBE_NUMBER_OF_FACES * CUBE_VERTICES_PER_FACE;
-static const int CUBE_VERTEX_POINTS = CUBE_VERTICES * FLOATS_PER_VERTEX;
-static const int CUBE_INDICES = CUBE_NUMBER_OF_FACES * CUBE_TRIANGLES_PER_FACE * VERTICES_PER_TRIANGLE;
-
-static const gpu::Element CUBE_POSITION_ELEMENT{ gpu::VEC3, gpu::FLOAT, gpu::XYZ };
-static const gpu::Element CUBE_NORMAL_ELEMENT{ gpu::VEC3, gpu::FLOAT, gpu::XYZ };
-static const gpu::Element CUBE_COLOR_ELEMENT{ gpu::VEC4, gpu::NUINT8, gpu::RGBA };
-static const gpu::Element INSTANCE_XFM_ELEMENT{ gpu::MAT4, gpu::FLOAT, gpu::XYZW };
-
-gpu::BufferPointer GeometryCache::getCubeVertices(float size) {
-    if (!_solidCubeVertices.contains(size)) {
-        auto verticesBuffer = std::make_shared<gpu::Buffer>();
-        _solidCubeVertices[size] = verticesBuffer;
-
-        GLfloat* vertexData = new GLfloat[CUBE_VERTEX_POINTS * 2]; // vertices and normals
-        GLfloat* vertex = vertexData;
-        float halfSize = size / 2.0f;
-
-        static GLfloat cannonicalVertices[CUBE_VERTEX_POINTS] = 
-                                    { 1, 1, 1,  -1, 1, 1,  -1,-1, 1,   1,-1, 1,   // v0,v1,v2,v3 (front)
-                                      1, 1, 1,   1,-1, 1,   1,-1,-1,   1, 1,-1,   // v0,v3,v4,v5 (right)
-                                      1, 1, 1,   1, 1,-1,  -1, 1,-1,  -1, 1, 1,   // v0,v5,v6,v1 (top)
-                                     -1, 1, 1,  -1, 1,-1,  -1,-1,-1,  -1,-1, 1,   // v1,v6,v7,v2 (left)
-                                     -1,-1,-1,   1,-1,-1,   1,-1, 1,  -1,-1, 1,   // v7,v4,v3,v2 (bottom)
-                                      1,-1,-1,  -1,-1,-1,  -1, 1,-1,   1, 1,-1 }; // v4,v7,v6,v5 (back)
-
-        // normal array
-        static GLfloat cannonicalNormals[CUBE_VERTEX_POINTS] =
-                                  { 0, 0, 1,   0, 0, 1,   0, 0, 1,   0, 0, 1,   // v0,v1,v2,v3 (front)
-                                    1, 0, 0,   1, 0, 0,   1, 0, 0,   1, 0, 0,   // v0,v3,v4,v5 (right)
-                                    0, 1, 0,   0, 1, 0,   0, 1, 0,   0, 1, 0,   // v0,v5,v6,v1 (top)
-                                   -1, 0, 0,  -1, 0, 0,  -1, 0, 0,  -1, 0, 0,   // v1,v6,v7,v2 (left)
-                                    0,-1, 0,   0,-1, 0,   0,-1, 0,   0,-1, 0,   // v7,v4,v3,v2 (bottom)
-                                    0, 0,-1,   0, 0,-1,   0, 0,-1,   0, 0,-1 }; // v4,v7,v6,v5 (back)
-
-
-        GLfloat* cannonicalVertex = &cannonicalVertices[0];
-        GLfloat* cannonicalNormal = &cannonicalNormals[0];
-
-        for (int i = 0; i < CUBE_VERTICES; i++) {
-            // vertices
-            *(vertex++) = halfSize * *cannonicalVertex++;
-            *(vertex++) = halfSize * *cannonicalVertex++;
-            *(vertex++) = halfSize * *cannonicalVertex++;
-
-            //normals
-            *(vertex++) = *cannonicalNormal++;
-            *(vertex++) = *cannonicalNormal++;
-            *(vertex++) = *cannonicalNormal++;
-        }
-        verticesBuffer->append(sizeof(GLfloat) * CUBE_VERTEX_POINTS * 2, (gpu::Byte*) vertexData);
-    }
-
-    return _solidCubeVertices[size];
-}
-
-gpu::BufferPointer GeometryCache::getSolidCubeIndices() {
-    if (!_solidCubeIndexBuffer) {
-        static GLubyte cannonicalIndices[CUBE_INDICES] =                                     { 0, 1, 2,   2, 3, 0,      // front
-                                      4, 5, 6,   6, 7, 4,      // right
-                                      8, 9,10,  10,11, 8,      // top
-                                     12,13,14,  14,15,12,      // left
-                                     16,17,18,  18,19,16,      // bottom
-                                     20,21,22,  22,23,20 };    // back
-
-        auto indexBuffer = std::make_shared<gpu::Buffer>();
-        _solidCubeIndexBuffer = indexBuffer;
-
-        _solidCubeIndexBuffer->append(sizeof(cannonicalIndices), (gpu::Byte*) cannonicalIndices);
-    }
-    return _solidCubeIndexBuffer;
-}
-
-
-void GeometryCache::setupCubeVertices(gpu::Batch& batch, gpu::BufferPointer& verticesBuffer) {
-    static const int VERTEX_STRIDE = sizeof(GLfloat) * FLOATS_PER_VERTEX * 2; // vertices and normals
-    static const int NORMALS_OFFSET = sizeof(GLfloat) * FLOATS_PER_VERTEX;
-
-    gpu::BufferView verticesView(verticesBuffer, 0, verticesBuffer->getSize(), VERTEX_STRIDE, CUBE_POSITION_ELEMENT);
-    gpu::BufferView normalsView(verticesBuffer, NORMALS_OFFSET, verticesBuffer->getSize(), VERTEX_STRIDE, CUBE_NORMAL_ELEMENT);
-    batch.setInputBuffer(gpu::Stream::POSITION, verticesView);
-    batch.setInputBuffer(gpu::Stream::NORMAL, normalsView);
-}
-
-void GeometryCache::renderSolidCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer) {
-    static gpu::Stream::FormatPointer streamFormat;
-    if (!streamFormat) {
-        streamFormat = std::make_shared<gpu::Stream::Format>(); // 1 for everyone
-        streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, CUBE_POSITION_ELEMENT, 0);
-        streamFormat->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, CUBE_NORMAL_ELEMENT);
-        streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, CUBE_COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
-        streamFormat->setAttribute(gpu::Stream::INSTANCE_XFM, gpu::Stream::INSTANCE_XFM, INSTANCE_XFM_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
-    }
-    batch.setInputFormat(streamFormat);
-
-    gpu::BufferView colorView(colorBuffer, CUBE_COLOR_ELEMENT);
-    batch.setInputBuffer(gpu::Stream::COLOR, colorView);
-
-    gpu::BufferView instanceXfmView(transformBuffer, 0, transformBuffer->getSize(), INSTANCE_XFM_ELEMENT);
-    batch.setInputBuffer(gpu::Stream::INSTANCE_XFM, instanceXfmView);
-
-    gpu::BufferPointer verticesBuffer = getCubeVertices(1.0);
-    setupCubeVertices(batch, verticesBuffer);
-    batch.setIndexBuffer(gpu::UINT8, getSolidCubeIndices(), 0);
-    batch.drawIndexedInstanced(count, gpu::TRIANGLES, CUBE_INDICES);
-}
-
-
-void GeometryCache::renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color) {
-    Vec2Pair colorKey(glm::vec2(color.x, color.y), glm::vec2(color.z, color.y));
-    if (!_solidCubeColors.contains(colorKey)) {
-        auto colorBuffer = std::make_shared<gpu::Buffer>();
-        _solidCubeColors[colorKey] = colorBuffer;
-
-        int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
-                            ((int(color.y * 255.0f) & 0xFF) << 8) |
-                            ((int(color.z * 255.0f) & 0xFF) << 16) |
-                            ((int(color.w * 255.0f) & 0xFF) << 24);
-        int colors[CUBE_VERTICES] = { 
-            compactColor, compactColor, compactColor, compactColor,
-            compactColor, compactColor, compactColor, compactColor,
-            compactColor, compactColor, compactColor, compactColor,
-            compactColor, compactColor, compactColor, compactColor,
-            compactColor, compactColor, compactColor, compactColor,
-            compactColor, compactColor, compactColor, compactColor 
-        };
-        colorBuffer->append(sizeof(colors), (gpu::Byte*) colors);
-    }
-    gpu::BufferPointer colorBuffer = _solidCubeColors[colorKey];
-
-    static gpu::Stream::FormatPointer streamFormat;
-    if (!streamFormat) {
-        streamFormat = std::make_shared<gpu::Stream::Format>(); // 1 for everyone
-        streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, CUBE_POSITION_ELEMENT, 0);
-        streamFormat->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, CUBE_NORMAL_ELEMENT);
-        streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, CUBE_COLOR_ELEMENT);
-    }
-    batch.setInputFormat(streamFormat);
-
-    gpu::BufferView colorView(colorBuffer, CUBE_COLOR_ELEMENT);
-    batch.setInputBuffer(gpu::Stream::COLOR, colorView);
-
-    gpu::BufferPointer verticesBuffer = getCubeVertices(size);
-    setupCubeVertices(batch, verticesBuffer);
-
-    batch.setIndexBuffer(gpu::UINT8, getSolidCubeIndices(), 0);
-    batch.drawIndexed(gpu::TRIANGLES, CUBE_INDICES);
-}
-
-
-void GeometryCache::renderWireCube(gpu::Batch& batch, float size, const glm::vec4& color) {
-    Vec2Pair colorKey(glm::vec2(color.x, color.y),glm::vec2(color.z, color.y));
-    static const int WIRE_CUBE_VERTICES_PER_EDGE = 2;
-    static const int WIRE_CUBE_TOP_EDGES = 4;
-    static const int WIRE_CUBE_BOTTOM_EDGES = 4;
-    static const int WIRE_CUBE_SIDE_EDGES = 4;
-    static const int WIRE_CUBE_VERTICES = 8;
-    static const int WIRE_CUBE_INDICES = (WIRE_CUBE_TOP_EDGES + WIRE_CUBE_BOTTOM_EDGES + WIRE_CUBE_SIDE_EDGES) * WIRE_CUBE_VERTICES_PER_EDGE;
-
-    if (!_cubeVerticies.contains(size)) {
-        auto verticesBuffer = std::make_shared<gpu::Buffer>();
-        _cubeVerticies[size] = verticesBuffer;
-
-        static const int WIRE_CUBE_VERTEX_POINTS = WIRE_CUBE_VERTICES * FLOATS_PER_VERTEX;
-        GLfloat* vertexData = new GLfloat[WIRE_CUBE_VERTEX_POINTS]; // only vertices, no normals because we're a wire cube
-        GLfloat* vertex = vertexData;
-        float halfSize = size / 2.0f;
-        
-        static GLfloat cannonicalVertices[] = 
-                                    { 1, 1, 1,   1, 1,-1,  -1, 1,-1,  -1, 1, 1,   // v0, v1, v2, v3 (top)
-                                      1,-1, 1,   1,-1,-1,  -1,-1,-1,  -1,-1, 1    // v4, v5, v6, v7 (bottom)
-                                    };
-
-        for (int i = 0; i < WIRE_CUBE_VERTEX_POINTS; i++) {
-            vertex[i] = cannonicalVertices[i] * halfSize;
-        }
-
-        verticesBuffer->append(sizeof(GLfloat) * WIRE_CUBE_VERTEX_POINTS, (gpu::Byte*) vertexData); // I'm skeptical that this is right
-    }
-
-    if (!_wireCubeIndexBuffer) {
-        static GLubyte cannonicalIndices[WIRE_CUBE_INDICES] = {
-                                      0, 1,  1, 2,  2, 3,  3, 0, // (top)
-                                      4, 5,  5, 6,  6, 7,  7, 4, // (bottom)
-                                      0, 4,  1, 5,  2, 6,  3, 7, // (side edges)
-                                    };
-        
-        auto indexBuffer = std::make_shared<gpu::Buffer>();
-        _wireCubeIndexBuffer = indexBuffer;
-    
-        _wireCubeIndexBuffer->append(sizeof(cannonicalIndices), (gpu::Byte*) cannonicalIndices);
-    }
-
-    if (!_cubeColors.contains(colorKey)) {
-        auto colorBuffer = std::make_shared<gpu::Buffer>();
-        _cubeColors[colorKey] = colorBuffer;
-
-        const int NUM_COLOR_SCALARS_PER_CUBE = 8;
-        int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
-                            ((int(color.y * 255.0f) & 0xFF) << 8) |
-                            ((int(color.z * 255.0f) & 0xFF) << 16) |
-                            ((int(color.w * 255.0f) & 0xFF) << 24);
-        int colors[NUM_COLOR_SCALARS_PER_CUBE] = { compactColor, compactColor, compactColor, compactColor,
-                                                   compactColor, compactColor, compactColor, compactColor };
-
-        colorBuffer->append(sizeof(colors), (gpu::Byte*) colors);
-    }
-    gpu::BufferPointer verticesBuffer = _cubeVerticies[size];
-    gpu::BufferPointer colorBuffer = _cubeColors[colorKey];
-
-    const int VERTICES_SLOT = 0;
-    const int COLOR_SLOT = 1;
-    static gpu::Stream::FormatPointer streamFormat;
-    static gpu::Element positionElement, colorElement;
-    if (!streamFormat) {
-        streamFormat = std::make_shared<gpu::Stream::Format>(); // 1 for everyone
-        streamFormat->setAttribute(gpu::Stream::POSITION, VERTICES_SLOT, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0);
-        streamFormat->setAttribute(gpu::Stream::COLOR, COLOR_SLOT, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
-        positionElement = streamFormat->getAttributes().at(gpu::Stream::POSITION)._element;
-        colorElement = streamFormat->getAttributes().at(gpu::Stream::COLOR)._element;
-    }
-   
-    gpu::BufferView verticesView(verticesBuffer, positionElement);
-    gpu::BufferView colorView(colorBuffer, colorElement);
-    
-    batch.setInputFormat(streamFormat);
-    batch.setInputBuffer(VERTICES_SLOT, verticesView);
-    batch.setInputBuffer(COLOR_SLOT, colorView);
-    batch.setIndexBuffer(gpu::UINT8, _wireCubeIndexBuffer, 0);
-    batch.drawIndexed(gpu::LINES, WIRE_CUBE_INDICES);
-}
 
 void GeometryCache::renderBevelCornersRect(gpu::Batch& batch, int x, int y, int width, int height, int bevelDistance, const glm::vec4& color, int id) {
     bool registered = (id != UNKNOWN_ID);
@@ -1099,10 +1092,10 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co
     batch.draw(gpu::TRIANGLE_STRIP, 4, 0);
 }
 
-void GeometryCache::renderUnitCube(gpu::Batch& batch) {
-    static const glm::vec4 color(1);
-    renderSolidCube(batch, 1, color);
-}
+//void GeometryCache::renderUnitCube(gpu::Batch& batch) {
+//    static const glm::vec4 color(1);
+//    renderSolidCube(batch, 1, color);
+//}
 
 void GeometryCache::renderUnitQuad(gpu::Batch& batch, const glm::vec4& color, int id) {
     static const glm::vec2 topLeft(-1, 1);
@@ -2043,8 +2036,8 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas
                 // need lightmap texcoord UV but doesn't have uv#1 so just reuse the same channel
                 networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, channelNum - 1, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV));
             }
-            if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW));
-            if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW));
+            if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW));
+            if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW));
         }
         else {
             int colorsOffset = mesh.tangents.size() * sizeof(glm::vec3);
@@ -2076,8 +2069,8 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas
             if (mesh.tangents.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
             if (mesh.colors.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB));
             if (mesh.texCoords.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV));
-            if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW));
-            if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::NFLOAT, gpu::XYZW));
+            if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW));
+            if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW));
         }
     }
 
diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h
index 9ba2658a9c..d911629001 100644
--- a/libraries/render-utils/src/GeometryCache.h
+++ b/libraries/render-utils/src/GeometryCache.h
@@ -12,6 +12,8 @@
 #ifndef hifi_GeometryCache_h
 #define hifi_GeometryCache_h
 
+#include <array>
+
 #include <QMap>
 #include <QRunnable>
 
@@ -119,37 +121,59 @@ inline uint qHash(const Vec4PairVec4Pair& v, uint seed) {
                 seed);
 }
 
+using VVertex = std::vector<glm::vec3>;
+using VIndex = std::vector<uint16_t>;
+
 /// Stores cached geometry.
 class GeometryCache : public ResourceCache, public Dependency {
     Q_OBJECT
     SINGLETON_DEPENDENCY
 
 public:
+    enum Shape {
+        Line,
+        Triangle,
+        Quad,
+        Circle,
+        Cube,
+        Sphere,
+        Tetrahedron,
+        Octahetron,
+        Dodecahedron,
+        Icosahedron,
+        Torus,
+        Cone,
+        Cylinder,
+
+        NUM_SHAPES,
+    };
+
     int allocateID() { return _nextID++; }
     static const int UNKNOWN_ID;
 
     virtual QSharedPointer<Resource> createResource(const QUrl& url, const QSharedPointer<Resource>& fallback,
                                                     bool delayLoad, const void* extra);
 
-    gpu::BufferPointer getCubeVertices(float size);
-    void setupCubeVertices(gpu::Batch& batch, gpu::BufferPointer& verticesBuffer);
+    void renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& transformBuffer, gpu::BufferPointer& colorBuffer);
+    void renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& transformBuffer, gpu::BufferPointer& colorBuffer);
+    void renderShape(gpu::Batch& batch, Shape shape);
+    void renderWireShape(gpu::Batch& batch, Shape shape);
 
-    gpu::BufferPointer getSolidCubeIndices();
+    void renderCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer);
+    void renderWireCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer);
+    void renderCube(gpu::Batch& batch);
+    void renderWireCube(gpu::Batch& batch);
 
-    void renderSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec3& color, bool solid = true, int id = UNKNOWN_ID) 
-                { renderSphere(batch, radius, slices, stacks, glm::vec4(color, 1.0f), solid, id); }
-                
-    void renderSphere(gpu::Batch& batch, float radius, int slices, int stacks, const glm::vec4& color, bool solid = true, int id = UNKNOWN_ID);
+    void renderSphereInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer);
+    void renderWireSphereInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer);
+    void renderSphere(gpu::Batch& batch);
+    void renderWireSphere(gpu::Batch& batch);
 
     void renderGrid(gpu::Batch& batch, int xDivisions, int yDivisions, const glm::vec4& color);
     void renderGrid(gpu::Batch& batch, int x, int y, int width, int height, int rows, int cols, const glm::vec4& color, int id = UNKNOWN_ID);
 
-    void renderSolidCubeInstances(gpu::Batch& batch, size_t count, gpu::BufferPointer transformBuffer, gpu::BufferPointer colorBuffer);
-    void renderSolidCube(gpu::Batch& batch, float size, const glm::vec4& color);
-    void renderWireCube(gpu::Batch& batch, float size, const glm::vec4& color);
     void renderBevelCornersRect(gpu::Batch& batch, int x, int y, int width, int height, int bevelDistance, const glm::vec4& color, int id = UNKNOWN_ID);
 
-    void renderUnitCube(gpu::Batch& batch);
     void renderUnitQuad(gpu::Batch& batch, const glm::vec4& color = glm::vec4(1), int id = UNKNOWN_ID);
 
     void renderQuad(gpu::Batch& batch, int x, int y, int width, int height, const glm::vec4& color, int id = UNKNOWN_ID)
@@ -223,19 +247,41 @@ public:
 private:
     GeometryCache();
     virtual ~GeometryCache();
+    void buildShapes();
 
     typedef QPair<int, int> IntPair;
     typedef QPair<unsigned int, unsigned int> VerticesIndices;
 
+    struct ShapeData {
+        size_t _indexOffset{ 0 };
+        size_t _indexCount{ 0 };
+        size_t _wireIndexOffset{ 0 };
+        size_t _wireIndexCount{ 0 };
+
+        gpu::BufferView _positionView;
+        gpu::BufferView _normalView;
+        gpu::BufferPointer _indices;
+
+        void setupVertices(gpu::BufferPointer& vertexBuffer, const VVertex& vertices);
+        void setupIndices(gpu::BufferPointer& indexBuffer, const VIndex& indices, const VIndex& wireIndices);
+        void setupBatch(gpu::Batch& batch) const;
+        void draw(gpu::Batch& batch) const;
+        void drawWire(gpu::Batch& batch) const;
+        void drawInstances(gpu::Batch& batch, size_t count) const;
+        void drawWireInstances(gpu::Batch& batch, size_t count) const;
+    };
+
+    using VShape = std::array<ShapeData, NUM_SHAPES>;
+
+    VShape _shapes;
+
+
+
     gpu::PipelinePointer _standardDrawPipeline;
     gpu::PipelinePointer _standardDrawPipelineNoBlend;
-    QHash<float, gpu::BufferPointer> _cubeVerticies;
-    QHash<Vec2Pair, gpu::BufferPointer> _cubeColors;
-    gpu::BufferPointer _wireCubeIndexBuffer;
 
-    QHash<float, gpu::BufferPointer> _solidCubeVertices;
-    QHash<Vec2Pair, gpu::BufferPointer> _solidCubeColors;
-    gpu::BufferPointer _solidCubeIndexBuffer;
+    gpu::BufferPointer _shapeVertices{ std::make_shared<gpu::Buffer>() };
+    gpu::BufferPointer _shapeIndices{ std::make_shared<gpu::Buffer>() };
 
     class BatchItemDetails {
     public:
@@ -257,7 +303,7 @@ private:
 
     QHash<IntPair, VerticesIndices> _coneVBOs;
 
-    int _nextID;
+    int _nextID{ 0 };
 
     QHash<int, Vec3PairVec4Pair> _lastRegisteredQuad3DTexture;
     QHash<Vec3PairVec4Pair, BatchItemDetails> _quad3DTextures;
@@ -299,16 +345,6 @@ private:
     QHash<int, Vec3Pair> _lastRegisteredAlternateGridBuffers;
     QHash<Vec3Pair, gpu::BufferPointer> _gridColors;
 
-    QHash<Vec2Pair, gpu::BufferPointer> _sphereVertices;
-    QHash<int, gpu::BufferPointer> _registeredSphereVertices;
-    QHash<int, Vec2Pair> _lastRegisteredSphereVertices;
-    QHash<IntPair, gpu::BufferPointer> _sphereIndices;
-    QHash<int, gpu::BufferPointer> _registeredSphereIndices;
-    QHash<int, IntPair> _lastRegisteredSphereIndices;
-    QHash<Vec3Pair, gpu::BufferPointer> _sphereColors;
-    QHash<int, gpu::BufferPointer> _registeredSphereColors;
-    QHash<int, Vec3Pair> _lastRegisteredSphereColors;
-
     QHash<QUrl, QWeakPointer<NetworkGeometry> > _networkGeometry;
 };
 
diff --git a/tests/gpu-test/src/main.cpp b/tests/gpu-test/src/main.cpp
index b27d10e312..ad9ed9bb4a 100644
--- a/tests/gpu-test/src/main.cpp
+++ b/tests/gpu-test/src/main.cpp
@@ -10,15 +10,22 @@
 
 #include <unordered_map>
 #include <memory>
+#include <cstdio>
 
 #include <glm/glm.hpp>
+#include <glm/gtc/matrix_transform.hpp>
 
-#include <QApplication>
-#include <QDir>
-#include <QElapsedTimer>
-#include <QFile>
-#include <QImage>
-#include <QLoggingCategory>
+#include <QtCore/QTime>
+#include <QtCore/QTimer>
+#include <QtCore/QDir>
+#include <QtCore/QElapsedTimer>
+#include <QtCore/QFile>
+#include <QtCore/QLoggingCategory>
+
+#include <QtGui/QResizeEvent>
+#include <QtGui/QWindow>
+#include <QtGui/QGuiApplication>
+#include <QtGui/QImage>
 
 #include <gpu/Context.h>
 #include <gpu/Batch.h>
@@ -26,17 +33,17 @@
 #include <gpu/StandardShaderLib.h>
 #include <gpu/GLBackend.h>
 
-#include <QOpenGLContext>
-#include <QResizeEvent>
-#include <QTime>
-#include <QTimer>
-#include <QWindow>
-#include <cstdio>
+// Must come after GL headers
+#include <QtGui/QOpenGLContext>
+
+#include <GLMHelpers.h>
 #include <PathUtils.h>
 #include <GeometryCache.h>
+#include <DeferredLightingEffect.h>
+#include <NumericalConstants.h>
 
-#include "simple_frag.h"
-#include "simple_vert.h"
+#include "unlit_frag.h"
+#include "unlit_vert.h"
 
 class RateCounter {
     std::vector<float> times;
@@ -74,173 +81,7 @@ public:
     }
 };
 
-#define MOVE_PARAM(name) decltype(name) && name
-
-struct BasicModel {
-    gpu::PipelinePointer pipeline;
-//    gpu::BufferPointer vertexBuffer;
-//    gpu::BufferPointer indexBuffer;
-//    gpu::BufferPointer normalBuffer;
-    
-    gpu::BufferView vertices;
-    gpu::BufferView normals;
-    gpu::BufferPointer indices;
-
-    gpu::Stream::FormatPointer format;
-    
-    BasicModel (MOVE_PARAM(pipeline), MOVE_PARAM(vertices), MOVE_PARAM(normals), MOVE_PARAM(indices), MOVE_PARAM(format))
-        : pipeline(pipeline), vertices(vertices), normals(normals), indices(indices), format(format) {}
-    
-//    BasicModel (gpu::PipelinePointer && pipeline, gpu::BufferPointer && buffer, gpu::Stream::FormatPointer && format)
-//        : pipeline(pipeline), buffer(buffer), format(format) {}
-};
-typedef std::shared_ptr<BasicModel> BasicModelPointer;
-#undef MOVE_PARAM
-
-BasicModelPointer makeCube () {
-    // Axis-aligned cube, facing the user at +z
-    // coords == binary mapping of each index, with z inverted (front face faces camera,
-    // instead of away from the camera)
-    //
-    //           -x,+y,-z ----------- +x,+y,-z
-    //        ___--- |           ___---   |
-    //   -x,+y,+z --------- +x,+y,+z      |
-    //       |       |          |         |
-    //       |       |          |         |
-    //       |       |          |         |
-    //       |       |          |         |
-    //       |   -x,-y,-z ------|---- +x,-y,-z
-    //       | ___---           | ___----
-    //   -x,-y,+z --------- +x,-y,+z
-    //
-    float s = 1.0f;
-    const glm::vec3 raw_verts[8] = {
-    //     x,  y,  z
-        { -s, -s, +s }, // 0b000    0x0
-        { +s, -s, +s }, // 0b001    0x1
-        { -s, +s, +s }, // 0b010    0x2
-        { +s, +s, +s }, // 0b011    0x3
-        { -s, -s, -s }, // 0b100    0x4
-        { +s, -s, -s }, // 0b101    0x5
-        { -s, +s, -s }, // 0b110    0x6
-        { +s, +s, -s }  // 0b111    0x7
-    };
-    const glm::vec3 raw_normals[6] = {
-        { 0.0f, 0.0f, +1.0f },  // x > 0:   1, 3, 5, 7      (N 0)
-        { 0.0f, 0.0f, -1.0f },  // x < 0:   0, 2, 4, 6      (N 1)
-        { 0.0f, +1.0f, 0.0f },  // y > 0:   2, 3, 6, 7      (N 2)
-        { 0.0f, -1.0f, 0.0f },  // y < 0:   0, 1, 4, 5      (N 3)
-        { +1.0f, 0.0f, 0.0f },  // z > 0:   0, 1, 2, 3      (N 4)
-        { -1.0f, 0.0f, 0.0f }   // z < 0:   4, 5, 6, 7      (N 5)
-    };
-    
-    const glm::vec3 cube_verts[24] = {
-        raw_verts[1], raw_verts[3], raw_verts[5], raw_verts[7],
-        raw_verts[0], raw_verts[2], raw_verts[4], raw_verts[6],
-        raw_verts[2], raw_verts[3], raw_verts[6], raw_verts[7],
-        raw_verts[0], raw_verts[1], raw_verts[4], raw_verts[5],
-        raw_verts[0], raw_verts[1], raw_verts[2], raw_verts[3],
-        raw_verts[4], raw_verts[5], raw_verts[6], raw_verts[7]
-    };
-    const glm::vec3 cube_normals[24] = {
-        raw_normals[0], raw_normals[0], raw_normals[0], raw_normals[0],
-        raw_normals[1], raw_normals[1], raw_normals[1], raw_normals[1],
-        raw_normals[2], raw_normals[2], raw_normals[2], raw_normals[2],
-        raw_normals[3], raw_normals[3], raw_normals[3], raw_normals[3],
-        raw_normals[4], raw_normals[4], raw_normals[4], raw_normals[4],
-        raw_normals[5], raw_normals[5], raw_normals[5], raw_normals[5]
-    };
-    
-    int16_t cube_indices_tris[36];
-    for (int i = 0, k = 0; i < 36; k += 4) {
-        cube_indices_tris[i++] = k + 0;
-        cube_indices_tris[i++] = k + 3;
-        cube_indices_tris[i++] = k + 1;
-        cube_indices_tris[i++] = k + 0;
-        cube_indices_tris[i++] = k + 2;
-        cube_indices_tris[i++] = k + 3;
-    }
-    
-//  const int16_t cube_indices_tris[36] {
-//      0, 3, 1,    0, 2, 3,
-//  };
-
-//    const glm::vec3 cube_normals[] = {
-//        { 0.0f, 0.0f, 1.0f },
-//        { 0.0f, 0.0f, 1.0f },
-//        { 0.0f, 0.0f, 1.0f },
-//        { 0.0f, 0.0f, 1.0f },
-//        { -1.0f, 0.0f, 0.0f },
-//        { -1.0f, 0.0f, 0.0f },
-//        { -1.0f, 0.0f, 0.0f },
-//        { -1.0f, 0.0f, 0.0f },
-//    };
-//    const int16_t cube_indices[] = {
-//        3, 1, 0,    2, 3, 0,
-//        6, 2, 0,    4, 6, 0,
-//    };
-    
-    gpu::Stream::FormatPointer format = std::make_shared<gpu::Stream::Format>();
-    
-    assert(gpu::Stream::POSITION == 0 && gpu::Stream::NORMAL == 1);
-    const int BUFFER_SLOT = 0;
-    
-    format->setAttribute(gpu::Stream::POSITION, BUFFER_SLOT, gpu::Element::VEC3F_XYZ);
-    format->setAttribute(gpu::Stream::NORMAL, BUFFER_SLOT, gpu::Element::VEC3F_XYZ);
-    
-    auto vertexBuffer = std::make_shared<gpu::Buffer>(24 * sizeof(glm::vec3), (gpu::Byte*)cube_verts);
-    auto normalBuffer = std::make_shared<gpu::Buffer>(24 * sizeof(glm::vec3), (gpu::Byte*)cube_normals);
-    gpu::BufferPointer indexBuffer  = std::make_shared<gpu::Buffer>(36 * sizeof(int16_t), (gpu::Byte*)cube_indices_tris);
-    
-    auto positionElement = format->getAttributes().at(gpu::Stream::POSITION)._element;
-    auto normalElement   = format->getAttributes().at(gpu::Stream::NORMAL)._element;
-    
-    gpu::BufferView vertexView { vertexBuffer, positionElement };
-    gpu::BufferView normalView { normalBuffer, normalElement };
-    
-    // Create shaders
-    auto vs = gpu::ShaderPointer(gpu::Shader::createVertex({ simple_vert }));
-    auto fs = gpu::ShaderPointer(gpu::Shader::createPixel({ simple_frag }));
-    auto shader = gpu::ShaderPointer(gpu::Shader::createProgram(vs, fs));
-    
-    gpu::Shader::BindingSet bindings;
-    bindings.insert({ "lightPosition", 1 });
-    
-    if (!gpu::Shader::makeProgram(*shader, bindings)) {
-        printf("Could not compile shader\n");
-        if (!vs)
-            printf("bad vertex shader\n");
-        if (!fs)
-            printf("bad fragment shader\n");
-        if (!shader)
-            printf("bad shader program\n");
-        exit(-1);
-    }
-    
-    auto state = std::make_shared<gpu::State>();
-//    state->setAntialiasedLineEnable(true);
-    state->setMultisampleEnable(true);
-    state->setDepthTest({ true });
-    auto pipeline = gpu::PipelinePointer(gpu::Pipeline::create(shader, state));
-
-    return std::make_shared<BasicModel>(
-        std::move(pipeline),
-        std::move(vertexView),
-        std::move(normalView),
-        std::move(indexBuffer),
-        std::move(format)
-    );
-}
-void renderCube(gpu::Batch & batch, const BasicModel & cube) {
-    
-    batch.setPipeline(cube.pipeline);
-    batch.setInputFormat(cube.format);
-    batch.setInputBuffer(gpu::Stream::POSITION, cube.vertices);
-    batch.setInputBuffer(gpu::Stream::NORMAL, cube.normals);
-    batch.setIndexBuffer(gpu::INT16, cube.indices, 0);
-//    batch.drawIndexed(gpu::TRIANGLES, 12);
-    batch.draw(gpu::TRIANGLES, 24);
-}
+uint32_t toCompactColor(const glm::vec4& color);
 
 gpu::ShaderPointer makeShader(const std::string & vertexShaderSrc, const std::string & fragmentShaderSrc, const gpu::Shader::BindingSet & bindings) {
     auto vs = gpu::ShaderPointer(gpu::Shader::createVertex(vertexShaderSrc));
@@ -253,6 +94,14 @@ gpu::ShaderPointer makeShader(const std::string & vertexShaderSrc, const std::st
     return shader;
 }
 
+float getSeconds(quint64 start = 0) {
+    auto usecs = usecTimestampNow() - start;
+    auto msecs = usecs / USECS_PER_MSEC;
+    float seconds = (float)msecs / MSECS_PER_SECOND;
+    return seconds;
+}
+
+
 
 // Creates an OpenGL window that renders a simple unlit scene using the gpu library and GeometryCache
 // Should eventually get refactored into something that supports multiple gpu backends.
@@ -265,9 +114,9 @@ class QTestWindow : public QWindow {
     gpu::ContextPointer _context;
     gpu::PipelinePointer _pipeline;
     glm::mat4 _projectionMatrix;
-//    BasicModelPointer _cubeModel;
     RateCounter fps;
     QTime _time;
+    int _instanceLocation{ -1 };
 
 protected:
     void renderText();
@@ -288,6 +137,7 @@ public:
         format.setVersion(4, 1);
         format.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);
         format.setOption(QSurfaceFormat::DebugContext);
+        format.setSwapInterval(0);
 
         setFormat(format);
 
@@ -301,23 +151,21 @@ public:
         gpu::Context::init<gpu::GLBackend>();
         _context = std::make_shared<gpu::Context>();
         
-        auto shader = makeShader(simple_vert, simple_frag, gpu::Shader::BindingSet {});
+        auto shader = makeShader(unlit_vert, unlit_frag, gpu::Shader::BindingSet{});
         auto state = std::make_shared<gpu::State>();
         state->setMultisampleEnable(true);
         state->setDepthTest(gpu::State::DepthTest { true });
         _pipeline = gpu::PipelinePointer(gpu::Pipeline::create(shader, state));
-        
+        _instanceLocation = _pipeline->getProgram()->getUniforms().findLocation("Instanced");
         
         // Clear screen
         gpu::Batch batch;
         batch.clearColorFramebuffer(gpu::Framebuffer::BUFFER_COLORS, { 1.0, 0.0, 0.5, 1.0 });
         _context->render(batch);
         
-//        _cubeModel = makeCube();
-        
         DependencyManager::set<GeometryCache>();
+        DependencyManager::set<DeferredLightingEffect>();
 
-        setFramePosition(QPoint(-1000, 0));
         resize(QSize(800, 600));
         
         _time.start();
@@ -327,6 +175,8 @@ public:
     }
 
     void draw() {
+        static auto startTime = usecTimestampNow();
+
         if (!isVisible()) {
             return;
         }
@@ -342,37 +192,81 @@ public:
         glm::vec3 unitscale { 1.0f };
         glm::vec3 up { 0.0f, 1.0f, 0.0f };
 
-        glm::vec3 cam_pos { 1.5f * sinf(t), 0.0f, 2.0f };
-//        glm::vec3 camera_focus { 5.0f * cosf(t * 0.1f), 0.0f, 0.0f };
-        glm::vec3 camera_focus { 0.0f, 0.0f, 0.0f };
-        glm::quat cam_rotation;
-        //    glm::quat cam_rotation = glm::quat_cast(glm::lookAt(cam_pos, camera_focus, up));
-        //    cam_rotation.w = -cam_rotation.w;
-        //    printf("cam rotation: %f %f %f %f\n", cam_rotation.x, cam_rotation.y, cam_rotation.z, cam_rotation.w);
-        Transform cam_transform { cam_rotation, unitscale, cam_pos };
-        
-        batch.setViewTransform(cam_transform);
+        glm::vec3 camera_position { 1.5f * sinf(t), 0.0f, 1.5f * cos(t) };
+
+        static const vec3 camera_focus(0);
+        static const vec3 camera_up(0, 1, 0);
+        glm::mat4 camera = glm::inverse(glm::lookAt(camera_position, camera_focus, up));
+        batch.setViewTransform(camera);
         batch.setPipeline(_pipeline);
-        
+        batch.setModelTransform(Transform());
+
         auto geometryCache = DependencyManager::get<GeometryCache>();
         
         // Render grid on xz plane (not the optimal way to do things, but w/e)
         // Note: GeometryCache::renderGrid will *not* work, as it is apparenly unaffected by batch rotations and renders xy only
-        batch.setModelTransform(Transform());
+        static const std::string GRID_INSTANCE = "Grid";
+        static auto compactColor1 = toCompactColor(vec4{ 0.35f, 0.25f, 0.15f, 1.0f });
+        static auto compactColor2 = toCompactColor(vec4{ 0.15f, 0.25f, 0.35f, 1.0f });
+        auto transformBuffer = batch.getNamedBuffer(GRID_INSTANCE, 0);
+        auto colorBuffer = batch.getNamedBuffer(GRID_INSTANCE, 1);
         for (int i = 0; i < 100; ++i) {
-            geometryCache->renderLine(batch, { -100.0f, -1.0f, -50.0f + float(i) }, { 100.0f, -1.0f, -50.0f + float(i) }, { 0.35f, 0.25f, 0.15f, 1.0f });
+            {
+                glm::mat4 transform = glm::translate(mat4(), vec3(0, -1, -50 + i));
+                transform = glm::scale(transform, vec3(100, 1, 1));
+                transformBuffer->append(transform);
+                colorBuffer->append(compactColor1);
+            }
+
+            {
+                glm::mat4 transform = glm::mat4_cast(quat(vec3(0, PI / 2.0f, 0)));
+                transform = glm::translate(transform, vec3(0, -1, -50 + i));
+                transform = glm::scale(transform, vec3(100, 1, 1));
+                transformBuffer->append(transform);
+                colorBuffer->append(compactColor2);
+            }
         }
-        for (int i = 0; i < 100; ++i) {
-            geometryCache->renderLine(batch, { -50.0f + float(i), -1.0f, -100.0f}, { -50.0f + float(i), -1.0f, 100.0f }, { 0.15f, 0.25f, 0.35f, 1.0f });
-        }
-        
+
+        batch.setupNamedCalls(GRID_INSTANCE, 200, [=](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
+            batch.setViewTransform(camera);
+            batch.setModelTransform(Transform());
+            batch.setPipeline(_pipeline);
+            auto& xfm = data._buffers[0];
+            auto& color = data._buffers[1];
+            batch._glUniform1i(_instanceLocation, 1);
+            geometryCache->renderWireShapeInstances(batch, GeometryCache::Line, data._count, xfm, color);
+            batch._glUniform1i(_instanceLocation, 0);
+        });
+
+
+
         // Render unlit cube + sphere
-        geometryCache->renderUnitCube(batch);
-        geometryCache->renderWireCube(batch, 1.0f, { 0.4f, 0.4f, 0.7f, 1.0f });
-        
-        batch.setModelTransform(Transform().setTranslation({ 1.5f, -0.5f, -0.5f }));
-        geometryCache->renderSphere(batch, 0.5f, 50, 50, { 0.8f, 0.25f, 0.25f });
+
+        static GeometryCache::Shape SHAPE[] = {
+            GeometryCache::Cube,
+            GeometryCache::Sphere,
+            GeometryCache::Tetrahedron,
+            GeometryCache::Icosahedron,
+        };
+
+        static auto startUsecs = usecTimestampNow(); 
+        float seconds = getSeconds(startUsecs);
+        seconds /= 4.0;
+        int shapeIndex = ((int)seconds) % 4;
+        bool wire = seconds - floor(seconds) > 0.5f;
+        batch.setModelTransform(Transform());
+        batch._glColor4f(0.8f, 0.25f, 0.25f, 1.0f);
+
+        if (wire) {
+            geometryCache->renderWireShape(batch, SHAPE[shapeIndex]);
+        } else {
+            geometryCache->renderShape(batch, SHAPE[shapeIndex]);
+        }
         
+        batch.setModelTransform(Transform().setScale(1.05f));
+        batch._glColor4f(1, 1, 1, 1);
+        geometryCache->renderWireCube(batch);
+
         _context->render(batch);
         _qGlContext->swapBuffers(this);
         
diff --git a/tests/gpu-test/src/simple.slf b/tests/gpu-test/src/unlit.slf
similarity index 94%
rename from tests/gpu-test/src/simple.slf
rename to tests/gpu-test/src/unlit.slf
index 31d33a73e4..350190180a 100644
--- a/tests/gpu-test/src/simple.slf
+++ b/tests/gpu-test/src/unlit.slf
@@ -20,7 +20,6 @@ in vec3 _normal;
 in vec3 _color;
 
 void main(void) {
-    Material material = getMaterial();
     packDeferredFragment(
         normalize(_normal.xyz), 
         glowIntensity,
diff --git a/tests/gpu-test/src/simple.slv b/tests/gpu-test/src/unlit.slv
similarity index 64%
rename from tests/gpu-test/src/simple.slv
rename to tests/gpu-test/src/unlit.slv
index 99f404eaec..3271d0ee90 100644
--- a/tests/gpu-test/src/simple.slv
+++ b/tests/gpu-test/src/unlit.slv
@@ -19,6 +19,7 @@
 <$declareStandardTransform()$>
 
 // the interpolated normal
+uniform bool Instanced = false;
 
 out vec3 _normal;
 out vec3 _color;
@@ -31,6 +32,12 @@ void main(void) {
     // standard transform
     TransformCamera cam = getTransformCamera();
     TransformObject obj = getTransformObject();
-    <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$>
-    <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$>
+    if (Instanced) {
+        <$transformInstancedModelToClipPos(cam, obj, inPosition, gl_Position)$>
+        <$transformInstancedModelToEyeDir(cam, obj, inNormal.xyz, _normal)$>
+    } else {
+        <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$>
+        <$transformModelToEyeDir(cam, obj, inNormal.xyz, _normal)$>
+    }
+    _normal = vec3(0.0, 0.0, 1.0);
 }
\ No newline at end of file

From cee296e70c3b76cc363244a9d123d2e4e16b3ba2 Mon Sep 17 00:00:00 2001
From: Seth Alves <seth@highfidelity.io>
Date: Mon, 21 Sep 2015 13:50:42 -0700
Subject: [PATCH 14/21] keep object from rotating during local grab

---
 examples/controllers/handControllerGrab.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/examples/controllers/handControllerGrab.js b/examples/controllers/handControllerGrab.js
index 56b710c5dd..f57e79e974 100644
--- a/examples/controllers/handControllerGrab.js
+++ b/examples/controllers/handControllerGrab.js
@@ -305,7 +305,7 @@ function controller(hand, triggerAction) {
 
         this.activateEntity(this.grabbedEntity);
 
-        var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, "position");
+        var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, ["position", "rotation"]);
 
         var handRotation = this.getHandRotation();
         var handPosition = this.getHandPosition();

From e12e4ece340a2f781a96e773f412118b09beff83 Mon Sep 17 00:00:00 2001
From: Bradley Austin Davis <bdavis@saintandreas.org>
Date: Mon, 21 Sep 2015 13:51:08 -0700
Subject: [PATCH 15/21] Fixing lighting and atmosphere

---
 interface/src/avatar/SkeletonModel.cpp                    | 4 ++--
 interface/src/ui/overlays/Sphere3DOverlay.cpp             | 2 +-
 .../entities-renderer/src/RenderableSphereEntityItem.cpp  | 8 +++++---
 libraries/render-utils/src/DeferredLightingEffect.cpp     | 1 -
 libraries/render-utils/src/Environment.cpp                | 2 +-
 libraries/render-utils/src/GeometryCache.cpp              | 6 +++---
 6 files changed, 12 insertions(+), 11 deletions(-)

diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp
index ff8cde3df8..6b56e92d80 100644
--- a/interface/src/avatar/SkeletonModel.cpp
+++ b/interface/src/avatar/SkeletonModel.cpp
@@ -643,7 +643,7 @@ void SkeletonModel::renderBoundingCollisionShapes(gpu::Batch& batch, float alpha
     glm::vec3 topPoint = _translation + _boundingCapsuleLocalOffset + (0.5f * _boundingCapsuleHeight) * glm::vec3(0.0f, 1.0f, 0.0f);
 
     deferredLighting->renderSolidSphereInstance(batch, 
-        Transform().setTranslation(topPoint).postScale(_boundingCapsuleRadius * 2.0),
+        Transform().setTranslation(topPoint).postScale(_boundingCapsuleRadius),
     	glm::vec4(0.6f, 0.6f, 0.8f, alpha));
 
     // draw a yellow sphere at the capsule bottom point
@@ -651,7 +651,7 @@ void SkeletonModel::renderBoundingCollisionShapes(gpu::Batch& batch, float alpha
     glm::vec3 axis = topPoint - bottomPoint;
 
     deferredLighting->renderSolidSphereInstance(batch, 
-        Transform().setTranslation(bottomPoint).postScale(_boundingCapsuleRadius * 2.0),
+        Transform().setTranslation(bottomPoint).postScale(_boundingCapsuleRadius),
         glm::vec4(0.8f, 0.8f, 0.6f, alpha));
 
     // draw a green cylinder between the two points
diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp
index 3b503e87e8..c22748b214 100644
--- a/interface/src/ui/overlays/Sphere3DOverlay.cpp
+++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp
@@ -40,7 +40,7 @@ void Sphere3DOverlay::render(RenderArgs* args) {
         batch->setModelTransform(Transform());
 
         Transform transform = _transform;
-        transform.postScale(getDimensions());
+        transform.postScale(getDimensions() * 0.5f);
         if (_isSolid) {
             DependencyManager::get<DeferredLightingEffect>()->renderSolidSphereInstance(*batch, transform, sphereColor);
         } else {
diff --git a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp
index 63fbfff9cb..1ff8dcbbbd 100644
--- a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp
@@ -53,15 +53,17 @@ void RenderableSphereEntityItem::render(RenderArgs* args) {
 
     gpu::Batch& batch = *args->_batch;
     glm::vec4 sphereColor(toGlm(getXColor()), getLocalRenderAlpha());
+    Transform modelTransform = getTransformToCenter();
+    modelTransform.postScale(0.5f);
     if (_procedural->ready()) {
-        batch.setModelTransform(getTransformToCenter()); // use a transform with scale, rotation, registration point and translation
-        _procedural->prepare(batch, getDimensions());
+        batch.setModelTransform(modelTransform); // use a transform with scale, rotation, registration point and translation
+        _procedural->prepare(batch, getDimensions() / 2.0f);
         auto color = _procedural->getColor(sphereColor);
         batch._glColor4f(color.r, color.g, color.b, color.a);
         DependencyManager::get<GeometryCache>()->renderSphere(batch);
     } else {
         batch.setModelTransform(Transform());
-        DependencyManager::get<DeferredLightingEffect>()->renderSolidSphereInstance(batch, getTransformToCenter(), sphereColor);
+        DependencyManager::get<DeferredLightingEffect>()->renderSolidSphereInstance(batch, modelTransform, sphereColor);
     }
 
 
diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp
index 1db26eae3b..59dd4e1d5a 100644
--- a/libraries/render-utils/src/DeferredLightingEffect.cpp
+++ b/libraries/render-utils/src/DeferredLightingEffect.cpp
@@ -589,7 +589,6 @@ void DeferredLightingEffect::render(RenderArgs* args) {
                 } else {
                     Transform model;
                     model.setTranslation(glm::vec3(light->getPosition().x, light->getPosition().y, light->getPosition().z));
-                    model.postScale(expandedRadius);
                     batch.setModelTransform(model);
                     batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
                     geometryCache->renderSphere(batch);
diff --git a/libraries/render-utils/src/Environment.cpp b/libraries/render-utils/src/Environment.cpp
index bffac32b0c..8a4e0a55a6 100644
--- a/libraries/render-utils/src/Environment.cpp
+++ b/libraries/render-utils/src/Environment.cpp
@@ -199,7 +199,7 @@ bool Environment::findCapsulePenetration(const glm::vec3& start, const glm::vec3
 void Environment::renderAtmosphere(gpu::Batch& batch, ViewFrustum& viewFrustum, const EnvironmentData& data) {
     // FIXME atmosphere rendering is broken in some way, 
     // should probably be replaced by a procedual skybox and put on the marketplace
-    return;
+    //return;
 
     glm::vec3 center = data.getAtmosphereCenter();
     
diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp
index 53eb8a454b..55575022a5 100644
--- a/libraries/render-utils/src/GeometryCache.cpp
+++ b/libraries/render-utils/src/GeometryCache.cpp
@@ -330,7 +330,7 @@ void GeometryCache::buildShapes() {
                 }
                 faceNormal = glm::normalize(faceNormal);
                 for (size_t j = 0; j < 3; ++j) {
-                    vertices.push_back(glm::normalize(originalVertices[i + j]) * 0.5f);
+                    vertices.push_back(glm::normalize(originalVertices[i + j]));
                     vertices.push_back(faceNormal);
                 }
             }
@@ -369,7 +369,7 @@ void GeometryCache::buildShapes() {
             vertices.reserve(originalVertices.size() * 2);
             for (size_t i = 0; i < originalVertices.size(); i += 3) {
                 for (int j = 0; j < 3; ++j) {
-                    vertices.push_back(originalVertices[i + j] * 0.5f);
+                    vertices.push_back(originalVertices[i + j]);
                     vertices.push_back(originalVertices[i + j]);
                     indices.push_back(i + j + startingIndex);
                 }
@@ -398,7 +398,7 @@ void GeometryCache::buildShapes() {
                 }
                 faceNormal = glm::normalize(faceNormal);
                 for (int j = 0; j < 3; ++j) {
-                    vertices.push_back(glm::normalize(originalVertices[i + j]) * 0.5f);
+                    vertices.push_back(glm::normalize(originalVertices[i + j]));
                     vertices.push_back(faceNormal);
                     indices.push_back(i + j + startingIndex);
                 }

From 7ffde27224a3da36da45653bd78abbab1f902442 Mon Sep 17 00:00:00 2001
From: David Rowe <david@ctrlaltstudio.com>
Date: Mon, 21 Sep 2015 14:04:32 -0700
Subject: [PATCH 16/21] Fix HMD fullscreen mirror view

---
 interface/src/Application.cpp | 25 +++++++++++++++++++------
 1 file changed, 19 insertions(+), 6 deletions(-)

diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 1b6be53e83..6540153c9b 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -1149,14 +1149,27 @@ void Application::paintGL() {
         }
 
     } else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) {
-        _myCamera.setRotation(_myAvatar->getWorldAlignedOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)));
-        _myCamera.setPosition(_myAvatar->getDefaultEyePosition() +
-                              glm::vec3(0, _raiseMirror * _myAvatar->getScale(), 0) +
-                              (_myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) *
-                               glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror);
+        if (isHMDMode()) {
+            glm::quat hmdRotation = extractRotation(_myAvatar->getHMDSensorMatrix());
+            _myCamera.setRotation(_myAvatar->getWorldAlignedOrientation() 
+                * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)) * hmdRotation);
+            glm::vec3 hmdOffset = extractTranslation(_myAvatar->getHMDSensorMatrix());
+            _myCamera.setPosition(_myAvatar->getDefaultEyePosition() 
+                + glm::vec3(0, _raiseMirror * _myAvatar->getScale(), 0) 
+                + (_myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) *
+                glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror 
+                + (_myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f))) * hmdOffset);
+        } else {
+            _myCamera.setRotation(_myAvatar->getWorldAlignedOrientation() 
+                * glm::quat(glm::vec3(0.0f, PI + _rotateMirror, 0.0f)));
+            _myCamera.setPosition(_myAvatar->getDefaultEyePosition() 
+                + glm::vec3(0, _raiseMirror * _myAvatar->getScale(), 0) 
+                + (_myAvatar->getOrientation() * glm::quat(glm::vec3(0.0f, _rotateMirror, 0.0f))) *
+                glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror);
+        }
         renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE;
     }
-    // Update camera position
+    // Update camera position 
     if (!isHMDMode()) {
         _myCamera.update(1.0f / _fps);
     }

From 0c0af812c27f1a6ce36599a37a74f6ef3a8e23ff Mon Sep 17 00:00:00 2001
From: Bradley Austin Davis <bdavis@saintandreas.org>
Date: Mon, 21 Sep 2015 14:16:56 -0700
Subject: [PATCH 17/21] CR comments

---
 interface/src/ui/overlays/Sphere3DOverlay.cpp            | 6 +++++-
 .../entities-renderer/src/RenderableSphereEntityItem.cpp | 9 +++++++--
 libraries/render-utils/src/Environment.cpp               | 4 ----
 3 files changed, 12 insertions(+), 7 deletions(-)

diff --git a/interface/src/ui/overlays/Sphere3DOverlay.cpp b/interface/src/ui/overlays/Sphere3DOverlay.cpp
index c22748b214..0df09d25f6 100644
--- a/interface/src/ui/overlays/Sphere3DOverlay.cpp
+++ b/interface/src/ui/overlays/Sphere3DOverlay.cpp
@@ -18,6 +18,10 @@
 
 QString const Sphere3DOverlay::TYPE = "sphere";
 
+// Sphere overlays should fit inside a cube of the specified dimensions, hence it needs to be a half unit sphere.
+// However, the geometry cache renders a UNIT sphere, so we need to scale down.
+static const float SPHERE_OVERLAY_SCALE = 0.5f;
+
 Sphere3DOverlay::Sphere3DOverlay(const Sphere3DOverlay* Sphere3DOverlay) :
     Volume3DOverlay(Sphere3DOverlay)
 {
@@ -40,7 +44,7 @@ void Sphere3DOverlay::render(RenderArgs* args) {
         batch->setModelTransform(Transform());
 
         Transform transform = _transform;
-        transform.postScale(getDimensions() * 0.5f);
+        transform.postScale(getDimensions() * SPHERE_OVERLAY_SCALE);
         if (_isSolid) {
             DependencyManager::get<DeferredLightingEffect>()->renderSolidSphereInstance(*batch, transform, sphereColor);
         } else {
diff --git a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp
index 1ff8dcbbbd..3cfc18046a 100644
--- a/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableSphereEntityItem.cpp
@@ -24,6 +24,11 @@
 #include "../render-utils/simple_vert.h"
 #include "../render-utils/simple_frag.h"
 
+// Sphere entities should fit inside a cube entity of the same size, so a sphere that has dimensions 1x1x1 
+// is a half unit sphere.  However, the geometry cache renders a UNIT sphere, so we need to scale down.
+static const float SPHERE_ENTITY_SCALE = 0.5f;
+
+
 EntityItemPointer RenderableSphereEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) {
     return std::make_shared<RenderableSphereEntityItem>(entityID, properties);
 }
@@ -54,10 +59,10 @@ void RenderableSphereEntityItem::render(RenderArgs* args) {
     gpu::Batch& batch = *args->_batch;
     glm::vec4 sphereColor(toGlm(getXColor()), getLocalRenderAlpha());
     Transform modelTransform = getTransformToCenter();
-    modelTransform.postScale(0.5f);
+    modelTransform.postScale(SPHERE_ENTITY_SCALE);
     if (_procedural->ready()) {
         batch.setModelTransform(modelTransform); // use a transform with scale, rotation, registration point and translation
-        _procedural->prepare(batch, getDimensions() / 2.0f);
+        _procedural->prepare(batch, getDimensions());
         auto color = _procedural->getColor(sphereColor);
         batch._glColor4f(color.r, color.g, color.b, color.a);
         DependencyManager::get<GeometryCache>()->renderSphere(batch);
diff --git a/libraries/render-utils/src/Environment.cpp b/libraries/render-utils/src/Environment.cpp
index 8a4e0a55a6..7fbd89acc1 100644
--- a/libraries/render-utils/src/Environment.cpp
+++ b/libraries/render-utils/src/Environment.cpp
@@ -197,10 +197,6 @@ bool Environment::findCapsulePenetration(const glm::vec3& start, const glm::vec3
 }
 
 void Environment::renderAtmosphere(gpu::Batch& batch, ViewFrustum& viewFrustum, const EnvironmentData& data) {
-    // FIXME atmosphere rendering is broken in some way, 
-    // should probably be replaced by a procedual skybox and put on the marketplace
-    //return;
-
     glm::vec3 center = data.getAtmosphereCenter();
     
     // transform the model transform to the center of our atmosphere

From 46524632fe01615a99fa0c83498aabf3787140bc Mon Sep 17 00:00:00 2001
From: David Rowe <david@ctrlaltstudio.com>
Date: Mon, 21 Sep 2015 14:18:33 -0700
Subject: [PATCH 18/21] Fix eyeball directions in fullscreen HMD view

---
 interface/src/Application.cpp | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 6540153c9b..ee98ce4c25 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -2611,11 +2611,7 @@ void Application::updateMyAvatarLookAtPosition() {
     if (_myCamera.getMode() == CAMERA_MODE_MIRROR) {
         //  When I am in mirror mode, just look right at the camera (myself); don't switch gaze points because when physically
         //  looking in a mirror one's eyes appear steady.
-        if (!isHMD) {
-            lookAtSpot = _myCamera.getPosition();
-        } else {
-            lookAtSpot = _myCamera.getPosition() + transformPoint(_myAvatar->getSensorToWorldMatrix(), extractTranslation(getHMDSensorPose()));
-        }
+        lookAtSpot = _myCamera.getPosition();
     } else if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) {
         //  Look at the point that the user is looking at.
         if (isHMD) {

From 84cea1ffd4ae13cd03c8c29db3721495d39f5063 Mon Sep 17 00:00:00 2001
From: Bradley Austin Davis <bdavis@saintandreas.org>
Date: Mon, 21 Sep 2015 14:33:32 -0700
Subject: [PATCH 19/21] More CR comments

---
 .../entities-renderer/src/RenderableZoneEntityItem.cpp      | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
index 90aff03f55..a102c19512 100644
--- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
@@ -19,6 +19,10 @@
 #include <GeometryCache.h>
 #include <PerfStat.h>
 
+// Sphere entities should fit inside a cube entity of the same size, so a sphere that has dimensions 1x1x1
+// is a half unit sphere.  However, the geometry cache renders a UNIT sphere, so we need to scale down.
+static const float SPHERE_ENTITY_SCALE = 0.5f;
+
 EntityItemPointer RenderableZoneEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) {
     return std::make_shared<RenderableZoneEntityItem>(entityID, properties);
 }
@@ -126,7 +130,7 @@ void RenderableZoneEntityItem::render(RenderArgs* args) {
                 auto xfm = getTransformToCenter();
                 auto deferredLightingEffect = DependencyManager::get<DeferredLightingEffect>();
                 if (getShapeType() == SHAPE_TYPE_SPHERE) {
-                    xfm.postScale(0.5);
+                    xfm.postScale(SPHERE_ENTITY_SCALE);
                     deferredLightingEffect->renderWireSphereInstance(batch, xfm, DEFAULT_COLOR);
                 } else {
                     deferredLightingEffect->renderWireCubeInstance(batch, xfm, DEFAULT_COLOR);

From b3aeaba5f4b7e6d0951aa7513d3a753510f30ab4 Mon Sep 17 00:00:00 2001
From: Bradley Austin Davis <bdavis@saintandreas.org>
Date: Mon, 21 Sep 2015 15:44:47 -0700
Subject: [PATCH 20/21] CR feedback

---
 .../src/RenderableDebugableEntityItem.cpp     |   6 +-
 .../src/RenderableZoneEntityItem.cpp          |   8 +-
 .../src/DeferredLightingEffect.cpp            |  46 +--
 libraries/render-utils/src/GeometryCache.cpp  | 264 ++++++++++++------
 libraries/render-utils/src/GeometryCache.h    |   8 +-
 5 files changed, 210 insertions(+), 122 deletions(-)

diff --git a/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp b/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp
index 6986025133..a13ed5b06d 100644
--- a/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableDebugableEntityItem.cpp
@@ -24,12 +24,12 @@ void RenderableDebugableEntityItem::renderBoundingBox(EntityItem* entity, Render
     Q_ASSERT(args->_batch);
     gpu::Batch& batch = *args->_batch;
 
-    auto xfm = entity->getTransformToCenter();
+    auto shapeTransform = entity->getTransformToCenter();
     if (puffedOut != 0.0) {
-        xfm.postScale(1.0 + puffedOut);
+        shapeTransform.postScale(1.0 + puffedOut);
     }
     batch.setModelTransform(Transform()); // we want to include the scale as well
-    DependencyManager::get<DeferredLightingEffect>()->renderWireCubeInstance(batch, xfm, color);
+    DependencyManager::get<DeferredLightingEffect>()->renderWireCubeInstance(batch, shapeTransform, color);
 }
 
 void RenderableDebugableEntityItem::render(EntityItem* entity, RenderArgs* args) {
diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
index a102c19512..c8088b7406 100644
--- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
@@ -127,13 +127,13 @@ void RenderableZoneEntityItem::render(RenderArgs* args) {
                 gpu::Batch& batch = *args->_batch;
                 batch.setModelTransform(Transform());
 
-                auto xfm = getTransformToCenter();
+                auto shapeTransform = getTransformToCenter();
                 auto deferredLightingEffect = DependencyManager::get<DeferredLightingEffect>();
                 if (getShapeType() == SHAPE_TYPE_SPHERE) {
-                    xfm.postScale(SPHERE_ENTITY_SCALE);
-                    deferredLightingEffect->renderWireSphereInstance(batch, xfm, DEFAULT_COLOR);
+                    shapeTransform.postScale(SPHERE_ENTITY_SCALE);
+                    deferredLightingEffect->renderWireSphereInstance(batch, shapeTransform, DEFAULT_COLOR);
                 } else {
-                    deferredLightingEffect->renderWireCubeInstance(batch, xfm, DEFAULT_COLOR);
+                    deferredLightingEffect->renderWireCubeInstance(batch, shapeTransform, DEFAULT_COLOR);
                 }
                 break;
             }
diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp
index 59dd4e1d5a..690cc8c6bd 100644
--- a/libraries/render-utils/src/DeferredLightingEffect.cpp
+++ b/libraries/render-utils/src/DeferredLightingEffect.cpp
@@ -187,10 +187,6 @@ gpu::PipelinePointer DeferredLightingEffect::bindSimpleProgram(gpu::Batch& batch
     return pipeline;
 }
 
-void DeferredLightingEffect::renderWireSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) {
-
-}
-
 uint32_t toCompactColor(const glm::vec4& color) {
     uint32_t compactColor = ((int(color.x * 255.0f) & 0xFF)) |
         ((int(color.y * 255.0f) & 0xFF) << 8) |
@@ -203,11 +199,11 @@ static const size_t INSTANCE_TRANSFORM_BUFFER = 0;
 static const size_t INSTANCE_COLOR_BUFFER = 1;
 
 template <typename F>
-void renderInstances(const std::string& name, gpu::Batch& batch, const Transform& xfm, const glm::vec4& color, F f) {
+void renderInstances(const std::string& name, gpu::Batch& batch, const Transform& transform, const glm::vec4& color, F f) {
     {
         gpu::BufferPointer instanceTransformBuffer = batch.getNamedBuffer(name, INSTANCE_TRANSFORM_BUFFER);
-        glm::mat4 xfmMat4;
-        instanceTransformBuffer->append(xfm.getMatrix(xfmMat4));
+        glm::mat4 glmTransform;
+        instanceTransformBuffer->append(transform.getMatrix(glmTransform));
 
         gpu::BufferPointer instanceColorBuffer = batch.getNamedBuffer(name, INSTANCE_COLOR_BUFFER);
         auto compactColor = toCompactColor(color);
@@ -225,21 +221,32 @@ void renderInstances(const std::string& name, gpu::Batch& batch, const Transform
     });
 }
 
-void DeferredLightingEffect::renderSolidSphereInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) {
+void DeferredLightingEffect::renderSolidSphereInstance(gpu::Batch& batch, const Transform& transform, const glm::vec4& color) {
     static const std::string INSTANCE_NAME = __FUNCTION__;
-    renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
-        DependencyManager::get<GeometryCache>()->renderSphereInstances(batch, data._count,
+    renderInstances(INSTANCE_NAME, batch, transform, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
+        DependencyManager::get<GeometryCache>()->renderShapeInstances(batch, GeometryCache::Sphere, data._count,
             data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]);
     });
 }
 
-static auto startTime = usecTimestampNow();
+void DeferredLightingEffect::renderWireSphereInstance(gpu::Batch& batch, const Transform& transform, const glm::vec4& color) {
+    static const std::string INSTANCE_NAME = __FUNCTION__;
+    renderInstances(INSTANCE_NAME, batch, transform, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
+        DependencyManager::get<GeometryCache>()->renderWireShapeInstances(batch, GeometryCache::Sphere, data._count,
+            data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]);
+    });
+}
 
-void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) {
+// Enable this in a debug build to cause 'box' entities to iterate through all the 
+// available shape types, both solid and wireframes
+//#define DEBUG_SHAPES
+
+void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Transform& transform, const glm::vec4& color) {
     static const std::string INSTANCE_NAME = __FUNCTION__;
 
 #ifdef DEBUG_SHAPES
-    renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
+    static auto startTime = usecTimestampNow();
+    renderInstances(INSTANCE_NAME, batch, transform, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
 
         auto usecs = usecTimestampNow();
         usecs -= startTime;
@@ -249,7 +256,9 @@ void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Tr
         float fractionalSeconds = seconds - floor(seconds);
         int shapeIndex = (int)seconds;
 
-        GeometryCache::Shape shapes[] = {
+        // Every second we flip to the next shape.
+        static const int SHAPE_COUNT = 5;
+        GeometryCache::Shape shapes[SHAPE_COUNT] = {
             GeometryCache::Cube,
             GeometryCache::Tetrahedron,
             GeometryCache::Sphere,
@@ -257,9 +266,10 @@ void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Tr
             GeometryCache::Line,
         };
 
-        shapeIndex %= 5;
+        shapeIndex %= SHAPE_COUNT;
         GeometryCache::Shape shape = shapes[shapeIndex];
 
+        // For the first half second for a given shape, show the wireframe, for the second half, show the solid.
         if (fractionalSeconds > 0.5f) {
             DependencyManager::get<GeometryCache>()->renderShapeInstances(batch, shape, data._count,
                 data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]);
@@ -269,16 +279,16 @@ void DeferredLightingEffect::renderSolidCubeInstance(gpu::Batch& batch, const Tr
         }
     });
 #else
-    renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
+    renderInstances(INSTANCE_NAME, batch, transform, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
         DependencyManager::get<GeometryCache>()->renderCubeInstances(batch, data._count,
             data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]);
     });
 #endif
 }
 
-void DeferredLightingEffect::renderWireCubeInstance(gpu::Batch& batch, const Transform& xfm, const glm::vec4& color) {
+void DeferredLightingEffect::renderWireCubeInstance(gpu::Batch& batch, const Transform& transform, const glm::vec4& color) {
     static const std::string INSTANCE_NAME = __FUNCTION__;
-    renderInstances(INSTANCE_NAME, batch, xfm, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
+    renderInstances(INSTANCE_NAME, batch, transform, color, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
         DependencyManager::get<GeometryCache>()->renderWireCubeInstances(batch, data._count,
             data._buffers[INSTANCE_TRANSFORM_BUFFER], data._buffers[INSTANCE_COLOR_BUFFER]);
     });
diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp
index 55575022a5..319fc710b1 100644
--- a/libraries/render-utils/src/GeometryCache.cpp
+++ b/libraries/render-utils/src/GeometryCache.cpp
@@ -57,7 +57,7 @@ static const uint SHAPE_VERTEX_STRIDE = sizeof(glm::vec3) * 2; // vertices and n
 static const uint SHAPE_NORMALS_OFFSET = sizeof(glm::vec3);
 
 
-void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, const VVertex& vertices) {
+void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, const VertexVector& vertices) {
     vertexBuffer->append(vertices);
 
     _positionView = gpu::BufferView(vertexBuffer, 0,
@@ -66,7 +66,7 @@ void GeometryCache::ShapeData::setupVertices(gpu::BufferPointer& vertexBuffer, c
         vertexBuffer->getSize(), SHAPE_VERTEX_STRIDE, NORMAL_ELEMENT);
 }
 
-void GeometryCache::ShapeData::setupIndices(gpu::BufferPointer& indexBuffer, const VIndex& indices, const VIndex& wireIndices) {
+void GeometryCache::ShapeData::setupIndices(gpu::BufferPointer& indexBuffer, const IndexVector& indices, const IndexVector& wireIndices) {
     _indices = indexBuffer;
     if (!indices.empty()) {
         _indexOffset = indexBuffer->getSize();
@@ -118,12 +118,12 @@ void GeometryCache::ShapeData::drawWireInstances(gpu::Batch& batch, size_t count
     }
 }
 
-const VVertex& icosahedronVertices() {
+const VertexVector& icosahedronVertices() {
     static const float phi = (1.0 + sqrt(5.0)) / 2.0;
     static const float a = 0.5;
     static const float b = 1.0 / (2.0 * phi);
 
-    static const VVertex vertices{ //
+    static const VertexVector vertices{ //
         vec3(0, b, -a), vec3(-b, a, 0), vec3(b, a, 0), // 
         vec3(0, b, a), vec3(b, a, 0), vec3(-b, a, 0), //
         vec3(0, b, a), vec3(-a, 0, b), vec3(0, -b, a), //
@@ -148,13 +148,13 @@ const VVertex& icosahedronVertices() {
     return vertices;
 }
 
-const VVertex& tetrahedronVertices() {
+const VertexVector& tetrahedronVertices() {
     static const float a = 1.0f / sqrt(2.0f);
     static const auto A = vec3(0, 1, a);
     static const auto B = vec3(0, -1, a);
     static const auto C = vec3(1, 0, -a);
     static const auto D = vec3(-1, 0, -a);
-    static const VVertex vertices{
+    static const VertexVector vertices{
         A, B, C,
         D, B, A,
         C, D, A,
@@ -163,8 +163,13 @@ const VVertex& tetrahedronVertices() {
     return vertices;
 }
 
-VVertex tesselate(const VVertex& startingTriangles, int count) {
-    VVertex triangles = startingTriangles;
+static const size_t TESSELTATION_MULTIPLIER = 4;
+static const size_t ICOSAHEDRON_TO_SPHERE_TESSELATION_COUNT = 3;
+static const size_t VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER = 2;
+
+
+VertexVector tesselate(const VertexVector& startingTriangles, int count) {
+    VertexVector triangles = startingTriangles;
     if (0 != (triangles.size() % 3)) {
         throw std::runtime_error("Bad number of vertices for tesselation");
     }
@@ -173,11 +178,13 @@ VVertex tesselate(const VVertex& startingTriangles, int count) {
         triangles[i] = glm::normalize(triangles[i]);
     }
 
-    VVertex newTriangles;
+    VertexVector newTriangles;
     while (count) {
         newTriangles.clear();
-        newTriangles.reserve(triangles.size() * 4);
-        for (size_t i = 0; i < triangles.size(); i += 3) {
+        // Tesselation takes one triangle and makes it into 4 triangles
+        // See https://en.wikipedia.org/wiki/Space-filling_tree#/media/File:Space_Filling_Tree_Tri_iter_1_2_3.png
+        newTriangles.reserve(triangles.size() * TESSELTATION_MULTIPLIER);
+        for (size_t i = 0; i < triangles.size(); i += VERTICES_PER_TRIANGLE) {
             const vec3& a = triangles[i];
             const vec3& b = triangles[i + 1];
             const vec3& c = triangles[i + 2];
@@ -221,7 +228,7 @@ void GeometryCache::buildShapes() {
     startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE;
     {
         ShapeData& shapeData = _shapes[Cube];
-        VVertex vertices;
+        VertexVector vertices;
         // front
         vertices.push_back(vec3(1, 1, 1));
         vertices.push_back(vec3(0, 0, 1));
@@ -282,14 +289,21 @@ void GeometryCache::buildShapes() {
         vertices.push_back(vec3(1, 1, -1));
         vertices.push_back(vec3(0, 0, -1));
 
+        static const size_t VERTEX_FORMAT_SIZE = 2;
+        static const size_t VERTEX_OFFSET = 0;
+        static const size_t NORMAL_OFFSET = 1;
+
         for (size_t i = 0; i < vertices.size(); ++i) {
-            if (0 == i % 2) {
-                vertices[i] *= 0.5f;
+            auto vertexIndex = i;
+            // Make a unit cube by having the vertices (at index N) 
+            // while leaving the normals (at index N + 1) alone
+            if (VERTEX_OFFSET == vertexIndex % VERTEX_FORMAT_SIZE) {
+                vertices[vertexIndex] *= 0.5f;
             }
         }
         shapeData.setupVertices(_shapeVertices, vertices);
 
-        VIndex indices{
+        IndexVector indices{
             0, 1, 2, 2, 3, 0, // front
             4, 5, 6, 6, 7, 4, // right
             8, 9, 10, 10, 11, 8, // top
@@ -297,11 +311,11 @@ void GeometryCache::buildShapes() {
             16, 17, 18, 18, 19, 16, // bottom
             20, 21, 22, 22, 23, 20  // back
         };
-        for (int i = 0; i < indices.size(); ++i) {
-            indices[i] += startingIndex;
+        for (auto& index : indices) {
+            index += startingIndex;
         }
 
-        VIndex wireIndices{
+        IndexVector wireIndices{
             0, 1, 1, 2, 2, 3, 3, 0, // front
             20, 21, 21, 22, 22, 23, 23, 20, // back
             0, 23, 1, 22, 2, 21, 3, 20 // sides
@@ -318,33 +332,41 @@ void GeometryCache::buildShapes() {
     {
         ShapeData& shapeData = _shapes[Tetrahedron];
         size_t vertexCount = 4;
-        VVertex vertices;
+        VertexVector vertices;
         {
-            VVertex originalVertices = tetrahedronVertices();
+            VertexVector originalVertices = tetrahedronVertices();
             vertexCount = originalVertices.size();
-            vertices.reserve(originalVertices.size() * 2);
-            for (size_t i = 0; i < originalVertices.size(); i += 3) {
+            vertices.reserve(originalVertices.size() * VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER);
+            for (size_t i = 0; i < originalVertices.size(); i += VERTICES_PER_TRIANGLE) {
+                auto triangleStartIndex = i;
                 vec3 faceNormal;
-                for (size_t j = 0; j < 3; ++j) {
-                    faceNormal += originalVertices[i + j];
+                for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) {
+                    auto triangleVertexIndex = j;
+                    auto vertexIndex = triangleStartIndex + triangleVertexIndex;
+                    faceNormal += originalVertices[vertexIndex];
                 }
                 faceNormal = glm::normalize(faceNormal);
-                for (size_t j = 0; j < 3; ++j) {
-                    vertices.push_back(glm::normalize(originalVertices[i + j]));
+                for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) {
+                    auto triangleVertexIndex = j;
+                    auto vertexIndex = triangleStartIndex + triangleVertexIndex;
+                    vertices.push_back(glm::normalize(originalVertices[vertexIndex]));
                     vertices.push_back(faceNormal);
                 }
             }
         }
         shapeData.setupVertices(_shapeVertices, vertices);
 
-        VIndex indices;
-        for (size_t i = 0; i < vertexCount; i += 3) {
-            for (size_t j = 0; j < 3; ++j) {
-                indices.push_back(i + j + startingIndex);
+        IndexVector indices;
+        for (size_t i = 0; i < vertexCount; i += VERTICES_PER_TRIANGLE) {
+            auto triangleStartIndex = i;
+            for (size_t j = 0; j < VERTICES_PER_TRIANGLE; ++j) {
+                auto triangleVertexIndex = j;
+                auto vertexIndex = triangleStartIndex + triangleVertexIndex;
+                indices.push_back(vertexIndex + startingIndex);
             }
         }
 
-        VIndex wireIndices{
+        IndexVector wireIndices{
             0, 1, 1, 2, 2, 0,
             0, 3, 1, 3, 2, 3,
         };
@@ -362,16 +384,21 @@ void GeometryCache::buildShapes() {
     startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE;
     {
         ShapeData& shapeData = _shapes[Sphere];
-        VVertex vertices;
-        VIndex indices;
+        VertexVector vertices;
+        IndexVector indices;
         {
-            VVertex originalVertices = tesselate(icosahedronVertices(), 3);
-            vertices.reserve(originalVertices.size() * 2);
-            for (size_t i = 0; i < originalVertices.size(); i += 3) {
-                for (int j = 0; j < 3; ++j) {
-                    vertices.push_back(originalVertices[i + j]);
-                    vertices.push_back(originalVertices[i + j]);
-                    indices.push_back(i + j + startingIndex);
+            VertexVector originalVertices = tesselate(icosahedronVertices(), ICOSAHEDRON_TO_SPHERE_TESSELATION_COUNT);
+            vertices.reserve(originalVertices.size() * VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER);
+            for (size_t i = 0; i < originalVertices.size(); i += VERTICES_PER_TRIANGLE) {
+                auto triangleStartIndex = i;
+                for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) {
+                    auto triangleVertexIndex = j;
+                    auto vertexIndex = triangleStartIndex + triangleVertexIndex;
+                    const auto& vertex = originalVertices[i + j];
+                    // Spheres use the same values for vertices and normals
+                    vertices.push_back(vertex);
+                    vertices.push_back(vertex);
+                    indices.push_back(vertexIndex + startingIndex);
                 }
             }
         }
@@ -386,21 +413,26 @@ void GeometryCache::buildShapes() {
     {
         ShapeData& shapeData = _shapes[Icosahedron];
 
-        VVertex vertices;
-        VIndex indices;
+        VertexVector vertices;
+        IndexVector indices;
         {
-            const VVertex& originalVertices = icosahedronVertices();
-            vertices.reserve(originalVertices.size() * 2);
+            const VertexVector& originalVertices = icosahedronVertices();
+            vertices.reserve(originalVertices.size() * VECTOR_TO_VECTOR_WITH_NORMAL_MULTIPLER);
             for (size_t i = 0; i < originalVertices.size(); i += 3) {
+                auto triangleStartIndex = i;
                 vec3 faceNormal;
-                for (size_t j = 0; j < 3; ++j) {
-                    faceNormal += originalVertices[i + j];
+                for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) {
+                    auto triangleVertexIndex = j;
+                    auto vertexIndex = triangleStartIndex + triangleVertexIndex;
+                    faceNormal += originalVertices[vertexIndex];
                 }
                 faceNormal = glm::normalize(faceNormal);
-                for (int j = 0; j < 3; ++j) {
-                    vertices.push_back(glm::normalize(originalVertices[i + j]));
+                for (int j = 0; j < VERTICES_PER_TRIANGLE; ++j) {
+                    auto triangleVertexIndex = j;
+                    auto vertexIndex = triangleStartIndex + triangleVertexIndex;
+                    vertices.push_back(glm::normalize(originalVertices[vertexIndex]));
                     vertices.push_back(faceNormal);
-                    indices.push_back(i + j + startingIndex);
+                    indices.push_back(vertexIndex + startingIndex);
                 }
             }
         }
@@ -410,6 +442,24 @@ void GeometryCache::buildShapes() {
         shapeData.setupIndices(_shapeIndices, indices, indices);
     }
 
+    // Line
+    startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE;
+    {
+        ShapeData& shapeData = _shapes[Line];
+        shapeData.setupVertices(_shapeVertices, VertexVector{
+            vec3(-0.5, 0, 0), vec3(-0.5f, 0, 0),
+            vec3(0.5f, 0, 0), vec3(0.5f, 0, 0)
+        });
+        IndexVector wireIndices;
+        // Only two indices
+        wireIndices.push_back(0 + startingIndex);
+        wireIndices.push_back(1 + startingIndex);
+
+        shapeData.setupIndices(_shapeIndices, IndexVector(), wireIndices);
+    }
+
+    // Not implememented yet:
+
     //Triangle,
     //Quad,
     //Circle,
@@ -418,20 +468,6 @@ void GeometryCache::buildShapes() {
     //Torus,
     //Cone,
     //Cylinder,
-    // Line
-    startingIndex = _shapeVertices->getSize() / SHAPE_VERTEX_STRIDE;
-    {
-        ShapeData& shapeData = _shapes[Line];
-        shapeData.setupVertices(_shapeVertices, VVertex{
-            vec3(-0.5, 0, 0), vec3(-0.5, 0, 0),
-            vec3(0.5f, 0, 0), vec3(0.5f, 0, 0)
-        });
-        VIndex wireIndices;
-        wireIndices.push_back(0 + startingIndex);
-        wireIndices.push_back(1 + startingIndex);
-
-        shapeData.setupIndices(_shapeIndices, VIndex(), wireIndices);
-    }
 }
 
 gpu::Stream::FormatPointer& getSolidStreamFormat() {
@@ -1092,11 +1128,6 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co
     batch.draw(gpu::TRIANGLE_STRIP, 4, 0);
 }
 
-//void GeometryCache::renderUnitCube(gpu::Batch& batch) {
-//    static const glm::vec4 color(1);
-//    renderSolidCube(batch, 1, color);
-//}
-
 void GeometryCache::renderUnitQuad(gpu::Batch& batch, const glm::vec4& color, int id) {
     static const glm::vec2 topLeft(-1, 1);
     static const glm::vec2 bottomRight(1, -1);
@@ -2015,29 +2046,54 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas
             // otherwise, at least the cluster indices/weights can be static
             networkMesh->_vertexStream = std::make_shared<gpu::BufferStream>();
             networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, 0, sizeof(glm::vec3));
-            if (mesh.normals.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, normalsOffset, sizeof(glm::vec3));
-            if (mesh.tangents.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, tangentsOffset, sizeof(glm::vec3));
-            if (mesh.colors.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, colorsOffset, sizeof(glm::vec3));
-            if (mesh.texCoords.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoordsOffset, sizeof(glm::vec2));
-            if (mesh.texCoords1.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoords1Offset, sizeof(glm::vec2));
-            if (mesh.clusterIndices.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterIndicesOffset, sizeof(glm::vec4));
-            if (mesh.clusterWeights.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterWeightsOffset, sizeof(glm::vec4));
-
+            if (mesh.normals.size()) {
+                networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, normalsOffset, sizeof(glm::vec3));
+            }
+            if (mesh.tangents.size()) {
+                networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, tangentsOffset, sizeof(glm::vec3));
+            }
+            if (mesh.colors.size())  {
+                networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, colorsOffset, sizeof(glm::vec3));
+            }
+            if (mesh.texCoords.size())  {
+                networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoordsOffset, sizeof(glm::vec2));
+            }
+            if (mesh.texCoords1.size()) {
+                networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoords1Offset, sizeof(glm::vec2));
+            }
+            if (mesh.clusterIndices.size()) {
+                networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterIndicesOffset, sizeof(glm::vec4));
+            }
+            if (mesh.clusterWeights.size()) {
+                networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterWeightsOffset, sizeof(glm::vec4));
+            }
             int channelNum = 0;
             networkMesh->_vertexFormat = std::make_shared<gpu::Stream::Format>();
             networkMesh->_vertexFormat->setAttribute(gpu::Stream::POSITION, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0);
-            if (mesh.normals.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::NORMAL, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
-            if (mesh.tangents.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
-            if (mesh.colors.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB));
-            if (mesh.texCoords.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV));
+            if (mesh.normals.size()) {
+                networkMesh->_vertexFormat->setAttribute(gpu::Stream::NORMAL, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
+            }
+            if (mesh.tangents.size()) {
+                networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
+            }
+            if (mesh.colors.size()) {
+                networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB));
+            }
+            if (mesh.texCoords.size()) {
+                networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV));
+            }
             if (mesh.texCoords1.size()) {
                 networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV));
             } else if (checkForTexcoordLightmap && mesh.texCoords.size()) {
                 // need lightmap texcoord UV but doesn't have uv#1 so just reuse the same channel
                 networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD1, channelNum - 1, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV));
             }
-            if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW));
-            if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW));
+            if (mesh.clusterIndices.size()) {
+                networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW));
+            }
+            if (mesh.clusterWeights.size()) {
+                networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW));
+            }
         }
         else {
             int colorsOffset = mesh.tangents.size() * sizeof(glm::vec3);
@@ -2056,21 +2112,43 @@ static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBas
                                                    mesh.clusterWeights.size() * sizeof(glm::vec4), (gpu::Byte*) mesh.clusterWeights.constData());
 
             networkMesh->_vertexStream = std::make_shared<gpu::BufferStream>();
-            if (mesh.tangents.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, 0, sizeof(glm::vec3));
-            if (mesh.colors.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, colorsOffset, sizeof(glm::vec3));
-            if (mesh.texCoords.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoordsOffset, sizeof(glm::vec2));
-            if (mesh.clusterIndices.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterIndicesOffset, sizeof(glm::vec4));
-            if (mesh.clusterWeights.size()) networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterWeightsOffset, sizeof(glm::vec4));
+            if (mesh.tangents.size()) {
+                networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, 0, sizeof(glm::vec3));
+            }
+            if (mesh.colors.size()) {
+                networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, colorsOffset, sizeof(glm::vec3));
+            }
+            if (mesh.texCoords.size()) {
+                networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, texCoordsOffset, sizeof(glm::vec2));
+            }
+            if (mesh.clusterIndices.size()) {
+                networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterIndicesOffset, sizeof(glm::vec4));
+            }
+            if (mesh.clusterWeights.size()) {
+                networkMesh->_vertexStream->addBuffer(networkMesh->_vertexBuffer, clusterWeightsOffset, sizeof(glm::vec4));
+            }
 
             int channelNum = 0;
             networkMesh->_vertexFormat = std::make_shared<gpu::Stream::Format>();
             networkMesh->_vertexFormat->setAttribute(gpu::Stream::POSITION, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
-            if (mesh.normals.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::NORMAL, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
-            if (mesh.tangents.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
-            if (mesh.colors.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB));
-            if (mesh.texCoords.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV));
-            if (mesh.clusterIndices.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW));
-            if (mesh.clusterWeights.size()) networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW));
+            if (mesh.normals.size()) {
+                networkMesh->_vertexFormat->setAttribute(gpu::Stream::NORMAL, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
+            }
+            if (mesh.tangents.size()) {
+                networkMesh->_vertexFormat->setAttribute(gpu::Stream::TANGENT, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
+            }
+            if (mesh.colors.size()) {
+                networkMesh->_vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum++, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::RGB));
+            }
+            if (mesh.texCoords.size()) {
+                networkMesh->_vertexFormat->setAttribute(gpu::Stream::TEXCOORD, channelNum++, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV));
+            }
+            if (mesh.clusterIndices.size()) {
+                networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_INDEX, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW));
+            }
+            if (mesh.clusterWeights.size()) {
+                networkMesh->_vertexFormat->setAttribute(gpu::Stream::SKIN_CLUSTER_WEIGHT, channelNum++, gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::XYZW));
+            }
         }
     }
 
diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h
index d911629001..df57b5cfb4 100644
--- a/libraries/render-utils/src/GeometryCache.h
+++ b/libraries/render-utils/src/GeometryCache.h
@@ -121,8 +121,8 @@ inline uint qHash(const Vec4PairVec4Pair& v, uint seed) {
                 seed);
 }
 
-using VVertex = std::vector<glm::vec3>;
-using VIndex = std::vector<uint16_t>;
+using VertexVector = std::vector<glm::vec3>;
+using IndexVector = std::vector<uint16_t>;
 
 /// Stores cached geometry.
 class GeometryCache : public ResourceCache, public Dependency {
@@ -262,8 +262,8 @@ private:
         gpu::BufferView _normalView;
         gpu::BufferPointer _indices;
 
-        void setupVertices(gpu::BufferPointer& vertexBuffer, const VVertex& vertices);
-        void setupIndices(gpu::BufferPointer& indexBuffer, const VIndex& indices, const VIndex& wireIndices);
+        void setupVertices(gpu::BufferPointer& vertexBuffer, const VertexVector& vertices);
+        void setupIndices(gpu::BufferPointer& indexBuffer, const IndexVector& indices, const IndexVector& wireIndices);
         void setupBatch(gpu::Batch& batch) const;
         void draw(gpu::Batch& batch) const;
         void drawWire(gpu::Batch& batch) const;

From 0401672c82e099a04112c5766d505c21d0bd9da0 Mon Sep 17 00:00:00 2001
From: Bradley Austin Davis <bdavis@saintandreas.org>
Date: Mon, 21 Sep 2015 16:24:55 -0700
Subject: [PATCH 21/21] Fixing lighting again

---
 libraries/render-utils/src/DeferredLightingEffect.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp
index 690cc8c6bd..b120bb8a57 100644
--- a/libraries/render-utils/src/DeferredLightingEffect.cpp
+++ b/libraries/render-utils/src/DeferredLightingEffect.cpp
@@ -599,7 +599,7 @@ void DeferredLightingEffect::render(RenderArgs* args) {
                 } else {
                     Transform model;
                     model.setTranslation(glm::vec3(light->getPosition().x, light->getPosition().y, light->getPosition().z));
-                    batch.setModelTransform(model);
+                    batch.setModelTransform(model.postScale(expandedRadius));
                     batch._glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
                     geometryCache->renderSphere(batch);
                 }