diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6877e05162e89f79d3501ff60fa312dbb33b88db..7d2eca82670aeb015a7ba01cc388643135a1db8e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -18,7 +18,7 @@ variables:
   # precompiled / assets
   # the URLs are the same as inside the game, change both if updating
   WEBGL_NAME: "WebGL art assets"
-  WEBGL_URL: "https://mega.nz/folder/ulIX2CAR#_g6wAcOLSCwIeGqrH7oXkA"
+  WEBGL_URL: "https://mega.nz/folder/P45nRALC#JkdALlE_w_cHDitz4Xhjeg"
   RENDER_NAME: "Rendered imagepack (outdated)"
   RENDER_URL: "https://mega.nz/file/upoAlBaZ#EbZ5wCixxZxBhMN_ireJTXt0SIPOywO2JW9XzTIPhe0"
 
diff --git a/js/003-data/gameVariableData.js b/js/003-data/gameVariableData.js
index 0e20f0d364566ec3db6130a6b7a6467d19c2b0ce..ae39a285bce96aafb93f09e39c65ab5b16bdc379 100644
--- a/js/003-data/gameVariableData.js
+++ b/js/003-data/gameVariableData.js
@@ -137,6 +137,7 @@ App.Data.defaultGameStateVariables = {
 	seeSummaryImages: 1,
 	seeVectorArtHighlights: 1,
 	setSuperSampling: 2,
+	setZoomSpeed: 1,
 	showAgeDetail: 1,
 	showAppraisal: 1,
 	showAssignToScenes: 1,
diff --git a/src/art/artJS.js b/src/art/artJS.js
index 8536de1254de7a353625b932b61f5c049fef066c..7c543c6db663b453bfb41cd25b1485d87570e2ab 100644
--- a/src/art/artJS.js
+++ b/src/art/artJS.js
@@ -168,9 +168,40 @@ App.Art.refreshSlaveArt = function(artSlave, artSize, elementID, UIDisplay = 0)
 	}
 };
 
+/**
+ * @param {string} event
+ * @param {number} message
+ * @returns {void}
+ */
+App.Art.errorHandler = function(event, message) {
+	switch(message) {
+		case 0:
+			App.Art.webglErrorMessage = "Success";
+			break;
+		case 1:
+			App.Art.webglErrorMessage = "Failed to start WebGL engine.";
+			break;
+		case 2:
+			App.Art.webglErrorMessage = "Could not find art assets.";
+			break;
+		case 3:
+			App.Art.webglErrorMessage = "Version mismatch.\nUpdate the assets using the link in Game Options.";
+			break;
+		case 4: // temporary option
+			App.Art.webglErrorMessage = "Could not find art assets.\nOlder versions need to be updated using\nthe download link in Game Options.";
+			break;
+	}
+
+	// fire event to art elements
+	let containers = document.getElementsByClassName("artContainer");
+	for (let i = 0; i < containers.length; i++) {
+		containers[i].dispatchEvent(new Event(event));
+	}
+};
+
 App.Art.webglInitialize = function() {
-	// asynchronously load webgl assets if present (this can briefly hang the browser)
 	let loadLockID = LoadScreen.lock();
+
 	let script = document.createElement("script");
 	script.onload = function() {
 		try {
@@ -181,10 +212,7 @@ App.Art.webglInitialize = function() {
 			scene.lockView = false;
 			scene.resetView = false;
 			scene.faceView = true;
-			scene.camera.xr = -6;
-			scene.camera.z = -275;
-			scene.camera.y = 127;
-			scene.camera.fov = 40;
+			scene.inspectView = false;
 
 			App.Art.scenes = {};
 			App.Art.defaultScene = JSON.parse(JSON.stringify(scene));
@@ -193,34 +221,38 @@ App.Art.webglInitialize = function() {
 			App.Art.engine = new App.Art.Engine();
 			App.Art.engine.bind(sceneData, scene);
 			App.Art.engineReady = true;
-			LoadScreen.unlock(loadLockID);
 
-			// when ready fires event to art elements to start rendering
-			let containers = document.getElementsByClassName("artContainer");
-			for (let i = 0; i < containers.length; i++) {
-				containers[i].dispatchEvent(new Event("engineLoaded"));
-			}
+			// when ready, fire event to art elements to start rendering
+			App.Art.errorHandler("engineLoaded", 0);
+			LoadScreen.unlock(loadLockID);
 		} catch(e) {
-			App.Art.engineReady = false;
+			App.Art.errorHandler("engineFailed", 1);
 			LoadScreen.unlock(loadLockID);
-
-			let containers = document.getElementsByClassName("artContainer");
-			for (let i = 0; i < containers.length; i++) {
-				containers[i].dispatchEvent(new Event("engineFailed2"));
-			}
 		}
 	};
 	script.onerror = function() {
-		App.Art.engineReady = false;
+		App.Art.errorHandler("engineFailed", 2);
 		LoadScreen.unlock(loadLockID);
+	};
+	script.src = "resources/webgl/scene1/scene1.js";
 
-		let containers = document.getElementsByClassName("artContainer");
-		for (let i = 0; i < containers.length; i++) {
-			containers[i].dispatchEvent(new Event("engineFailed"));
+	// asynchronously load webgl assets if present
+	let load = document.createElement("script");
+	load.onload = function() {
+		// but only if version is correct
+		if (App.Art.version === "1.2") {
+			document.head.appendChild(script);
+		} else {
+			App.Art.errorHandler("engineFailed", 3);
+			LoadScreen.unlock(loadLockID);
 		}
 	};
-	script.src = "resources/webgl/scene1.js";
-	document.head.appendChild(script);
+	load.onerror = function() {
+		App.Art.errorHandler("engineFailed", 4); // temporary display different message until next update
+		LoadScreen.unlock(loadLockID);
+	};
+	load.src = "resources/webgl/load.js";
+	document.head.appendChild(load);
 }();
 
 /**
@@ -231,9 +263,10 @@ App.Art.webglInitialize = function() {
 App.Art.webglArtElement = function(slave, artSize) {
 	let container = document.createElement("div");
 	container.setAttribute("class", "artContainer");
+	container.style.fontSize = "large";
 	container.innerText = "Loading...";
 
-	container.addEventListener("engineLoaded", function(e) {
+	container.addEventListener("engineLoaded", function() {
 		// when engine is ready, attach default scene to new slaves
 		if (!(slave.ID in App.Art.scenes)) {
 			App.Art.scenes[slave.ID] = JSON.parse(JSON.stringify(App.Art.defaultScene));
@@ -241,31 +274,28 @@ App.Art.webglArtElement = function(slave, artSize) {
 		let scene = App.Art.scenes[slave.ID];
 
 		// apply the model transforms
+		App.Art.applyFigures(slave, scene);
 		App.Art.applySurfaces(slave, scene);
 		App.Art.applyMaterials(slave, scene);
 		App.Art.applyMorphs(slave, scene);
+		App.Art.Frame(slave, scene);
 
 		// draw on canvas and create UI
 		container.innerText = "";
 		let canvas = App.Art.createWebglUI(container, slave, artSize, scene);
 		App.Art.engine.render(scene, canvas);
-	}, true);
-
-	container.addEventListener("engineFailed", function(e) {
-		container.style.color = "#BB2027";
-		container.innerText = "Failed to start WebGL engine.";
-	}, true);
+	});
 
-	container.addEventListener("engineFailed2", function(e) {
+	container.addEventListener("engineFailed", function() {
+		container.style.fontSize = "small";
 		container.style.color = "#BB2027";
-		container.innerText = "Art asset out of date.";
-	}, true);
+		container.innerText = App.Art.webglErrorMessage;
+	});
 
 	// incase engine is loaded, trigger listeners manually
 	if (App.Art.engineReady === true) {
 		container.dispatchEvent(new Event("engineLoaded"));
-	}
-	if (App.Art.engineReady === false) {
+	} else {
 		container.dispatchEvent(new Event("engineFailed"));
 	}
 
diff --git a/src/art/webgl/art.js b/src/art/webgl/art.js
index c696278575ee2d5f927ad1a2e66e4479cf9c723a..bc071085384ac5a3f50fb39ba773093d1ec6e08b 100644
--- a/src/art/webgl/art.js
+++ b/src/art/webgl/art.js
@@ -1,35 +1,88 @@
 App.Art.getMaterialById = function(scene, id) {
-	for (let i =0; i < scene.materials.length; i++) {
-		if(scene.materials[i].matId === id) {
-			return scene.materials[i];
+	for (const material of scene.materials) {
+		if(material.matId === id) {
+			return material;
 		}
 	}
 	return null;
 };
 
 App.Art.getMorphById = function(scene, id) {
-	for (let i =0; i < scene.model.morphs.length; i++) {
-		if(scene.model.morphs[i].morphId === id) {
-			return scene.model.morphs[i];
+	for (const morph of scene.models[0].morphs) {
+		if(morph.morphId === id) {
+			return morph;
 		}
 	}
 	return null;
 };
 
 App.Art.getSurfaceById = function(scene, id) {
-	for (let i=0, count=0; i < scene.model.figures.length; i++) {
-		for (let j=0; j < scene.model.figures[i].surfaces.length; j++, count++) {
-			if(scene.model.figures[i].surfaces[j].surfaceId === id) {
-				return scene.model.figures[i].surfaces[j];
+	for (const figure of scene.models[0].figures) {
+		for (const surface of figure.surfaces) {
+			if (surface.surfaceId === id) {
+				return surface;
 			}
 		}
 	}
 	return null;
 };
 
-App.Art.resetMorphs = function(slave, scene) {
-	for (let i =0; i < scene.model.morphs.length; i++) {
-		scene.model.morphs[i].value = App.Art.defaultScene.model.morphs[i].value;
+App.Art.getMatIdsBySurface = function(scene, id) {
+	for (const figure of scene.models[0].figures) {
+		for (const surface of figure.surfaces) {
+			if (surface .surfaceId === id) {
+				return surface.matIds;
+			}
+		}
+	}
+};
+
+App.Art.resetMorphs = function(scene) {
+	for (let i =0; i < scene.models[0].morphs.length; i++) {
+		scene.models[0].morphs[i].value = App.Art.defaultScene.models[0].morphs[i].value;
+	}
+};
+
+App.Art.applyFigures = function(slave, scene) {
+	let figures = [];
+
+	figures.push("Genesis8Female");
+	figures.push("Geometry");
+	figures.push("Futalicious");
+	figures.push("Genesis8FemaleEyelashes");
+
+	switch(slave.hStyle) {
+		case "afro": figures.push("prae-yarahair_174011"); break;
+		case "cornrows": figures.push("TIGER"); break;
+		case "bun": figures.push("Adia"); break;
+		case "neat": figures.push("SamiraHair_103927"); break;
+		case "strip": figures.push("ValRebelH_12512"); break;
+		case "tails": figures.push("LLF-DazStudioFemaleHair_758"); break;
+		case "up": figures.push("Pina"); break;
+		case "ponytail": figures.push("ElitePonytail"); break;
+		case "braided": figures.push("LLF-MishkaGeBase1_31774"); break;
+		case "dreadlocks": figures.push("Dreads_197696"); break;
+		case "permed": figures.push("IchigoHair_77918"); break;
+		case "curled": figures.push("aprilyshHavanaHair_32519"); break;
+		case "luxurious": figures.push("SW_BaronessHR_33121"); break;
+		case "messy bun": figures.push("KrayonHair_47547"); break;
+		case "messy": figures.push("MessyHair_35245"); break;
+		case "eary": figures.push("GeorginaHair_72382"); break;
+		case "undercut": figures.push("EditHairGN2Female_112247"); break;
+		case "bald": break;
+		case "shaved": break;
+		case "buzzcut": break;
+		case "trimmed": break;
+		default: break;
+	}
+
+	for (let i=0; i < scene.models[0].figures.length; i++) {
+		scene.models[0].figures[i].visible = false;
+		for (let j =0; j < figures.length; j++) {
+			if (scene.models[0].figures[i].figId === figures[j]) {
+				scene.models[0].figures[i].visible = true;
+			}
+		}
 	}
 };
 
@@ -167,8 +220,20 @@ App.Art.applySurfaces = function(slave, scene) {
 	surfaces.push(["Torso_Middle", "matIds", [skin + "Torso"]]);
 	surfaces.push(["Torso_Back", "matIds", [skin + "Torso"]]);
 
-	surfaces.push(["Torso", "matIds", [skin + "Torso"]]);
-	surfaces.push(["Face", "matIds", [skin + "Face"]]);
+	switch(slave.hStyle) {
+		case "buzzcut":
+		case "trimmed":
+			surfaces.push(["Torso", "matIds", [skin + "Torso", "shaved_torso"]]);
+			surfaces.push(["Face", "matIds", [skin + "Face", "shaved_face"]]);
+			break;
+		case "bald":
+		case "shaved":
+		default:
+			surfaces.push(["Torso", "matIds", [skin + "Torso"]]);
+			surfaces.push(["Face", "matIds", [skin + "Face"]]);
+			break;
+	}
+
 	surfaces.push(["Lips", "matIds", [skin + "Lips"]]);
 	surfaces.push(["Ears", "matIds", [skin + "Ears"]]);
 	surfaces.push(["Legs", "matIds", [skin + "Legs"]]);
@@ -220,11 +285,11 @@ App.Art.applySurfaces = function(slave, scene) {
 			break;
 	}
 
-	for (let i=0, count=0; i < scene.model.figures.length; i++) {
-		for (let j=0; j < scene.model.figures[i].surfaces.length; j++, count++) {
+	for (let i=0, count=0; i < scene.models[0].figures.length; i++) {
+		for (let j=0; j < scene.models[0].figures[i].surfaces.length; j++, count++) {
 			for (let h =0; h < surfaces.length; h++) {
-				if (scene.model.figures[i].surfaces[j].surfaceId === surfaces[h][0]) {
-					scene.model.figures[i].surfaces[j][surfaces[h][1]] = surfaces[h][2];
+				if (scene.models[0].figures[i].surfaces[j].surfaceId === surfaces[h][0]) {
+					scene.models[0].figures[i].surfaces[j][surfaces[h][1]] = surfaces[h][2];
 				}
 			}
 		}
@@ -344,14 +409,98 @@ App.Art.applyMaterials = function(slave, scene) {
 
 	nailColor = hexToRgb(nailColor);
 
-
-	materials.push(["HeadThin", "Kd", hairColor]);
-	materials.push(["Head", "Kd", hairColor]);
-	materials.push(["TuckedThin", "Kd", hairColor]);
-	materials.push(["TuckedR", "Kd", hairColor]);
-	materials.push(["BangsThin", "Kd", hairColor]);
-	materials.push(["Bangs", "Kd", hairColor]);
-	materials.push(["Scalp", "Kd", hairColor]);
+	switch(slave.hStyle) {
+		case "afro":
+			materials.push(["yara_scalp", "Kd", hairColor]);
+			materials.push(["yara_hair", "Kd", hairColor]);
+			break;
+		case "cornrows":
+			materials.push(["tiger_scalp", "Kd", hairColor]);
+			materials.push(["tiger_hair", "Kd", hairColor]);
+			break;
+		case "bun":
+			materials.push(["adia_scalp", "Kd", hairColor]);
+			materials.push(["adia_hair", "Kd", hairColor]);
+			break;
+		case "neat":
+			materials.push(["samira_scalp", "Kd", hairColor]);
+			materials.push(["samira_hair", "Kd", hairColor]);
+			break;
+		case "strip":
+			materials.push(["rebel_scalp", "Kd", hairColor]);
+			materials.push(["rebel_hair", "Kd", hairColor]);
+			break;
+		case "tails":
+			materials.push(["kinley_scalp", "Kd", hairColor]);
+			materials.push(["kinley_hair_thin_strands", "Kd", hairColor]);
+			materials.push(["kinley_hair_long", "Kd", hairColor]);
+			materials.push(["kinley_hair_strands", "Kd", hairColor]);
+			materials.push(["kinley_hair_base", "Kd", hairColor]);
+			materials.push(["kinley_hair_tie", "Kd", hairColor]);
+			break;
+		case "up":
+			materials.push(["pina_scalp", "Kd", hairColor]);
+			materials.push(["pina_hair1", "Kd", hairColor]);
+			materials.push(["pina_hair2", "Kd", hairColor]);
+			break;
+		case "ponytail":
+			materials.push(["ponytail_scalp", "Kd", hairColor]);
+			materials.push(["ponytail_hair1", "Kd", hairColor]);
+			materials.push(["ponytail_hair2", "Kd", hairColor]);
+			materials.push(["ponytail_hair3", "Kd", hairColor]);
+			materials.push(["ponytail_holder", "Kd", hairColor]);
+			break;
+		case "braided":
+			materials.push(["mishka_scalp", "Kd", hairColor]);
+			materials.push(["mishka_hair1", "Kd", hairColor]);
+			materials.push(["mishka_hair2", "Kd", hairColor]);
+			materials.push(["mishka_hair3", "Kd", hairColor]);
+			break;
+		case "dreadlocks":
+			materials.push(["dreads_scalp", "Kd", hairColor]);
+			materials.push(["dreads_hair", "Kd", hairColor]);
+			break;
+		case "permed":
+			materials.push(["ichigo_scalp", "Kd", hairColor]);
+			materials.push(["ichigo_hair1", "Kd", hairColor]);
+			materials.push(["ichigo_hair2", "Kd", hairColor]);
+			break;
+		case "curled":
+			materials.push(["havana_hair", "Kd", hairColor]);
+			break;
+		case "luxurious":
+			materials.push(["baroness_scalp", "Kd", hairColor]);
+			materials.push(["baroness_hair", "Kd", hairColor]);
+			break;
+		case "messy bun":
+			materials.push(["krayon_scalp", "Kd", hairColor]);
+			materials.push(["krayon_hair1", "Kd", hairColor]);
+			materials.push(["krayon_hair2", "Kd", hairColor]);
+			materials.push(["krayon_hair3", "Kd", hairColor]);
+			materials.push(["krayon_hair4", "Kd", hairColor]);
+			break;
+		case "messy":
+			materials.push(["messy_scalp", "Kd", hairColor]);
+			materials.push(["messy_hair", "Kd", hairColor]);
+			break;
+		case "eary":
+			materials.push(["georgina_scalp", "Kd", hairColor]);
+			materials.push(["georgina_hair1", "Kd", hairColor]);
+			materials.push(["georgina_hair2", "Kd", hairColor]);
+			break;
+		case "undercut":
+			materials.push(["edit_scalp", "Kd", hairColor]);
+			materials.push(["edit_hair", "Kd", hairColor]);
+			break;
+		case "buzzcut":
+		case "trimmed":
+			materials.push(["shaved_face", "Kd", hairColor]);
+			materials.push(["shaved_torso", "Kd", hairColor]);
+			break;
+		case "bald":
+		case "shaved":
+		default: break;
+	}
 
 	let irisColor;
 	let scleraColor;
@@ -504,6 +653,14 @@ App.Art.applyMaterials = function(slave, scene) {
 			break;
 	}
 
+	let torso = App.Art.getMatIdsBySurface(scene, "Torso")[0];
+
+	if (slave.scar.hasOwnProperty("belly") && slave.scar.belly["c-section"] > 0) {
+		materials.push([torso, "map_Kn", scene.textureMap["Victoria8_Torso_CNM_1002.jpg"]]);
+	} else {
+		materials.push([torso, "map_Kn", scene.textureMap["Victoria8_Torso_NM_1002.jpg"]]);
+	}
+
 	for (let i =0; i < scene.materials.length; i++) {
 		for (let j =0; j < materials.length; j++) {
 			if (scene.materials[i].matId === materials[j][0]) {
@@ -521,27 +678,32 @@ App.Art.applyMorphs = function(slave, scene) {
 	}
 
 	function random(seed) {
-		let x = Math.sin(seed++) * 10000;
+		let x = Math.sin(seed+1) * 10000;
 		return x - Math.floor(x);
 	}
 
+
 	if(hasBothArms(slave) && hasBothLegs(slave)) {
-		if (slave.devotion > 50) {
+		if (scene.inspectView) {
+			morphs.push(["posesInspect", 1]);
+			morphs.push(["posesInspectGen", 1]);
+		} else if (slave.devotion > 50) {
 			morphs.push(["posesHigh", 1]);
-		} else if (slave.trust >= -20) {
-			if (slave.devotion <= 20) {
-				morphs.push(["posesLow", 1]);
-			} else {
-				morphs.push(["posesMid", 1]);
-			}
-		} else {
+		} else if (slave.devotion > -20) {
 			morphs.push(["posesMid", 1]);
+		} else {
+			morphs.push(["posesLow", 1]);
 		}
 	}
 
+	if (slave.trust < 0) {
+		morphs.push(["expressionsFear", Math.abs(slave.trust)/100]);
+	} else if (slave.devotion > 0) {
+		morphs.push(["expressionsHappy", slave.trust/100]);
+	}
+
 	// used for interpolating mixed race based on slave ID
-	let races = ["raceWhite" , "raceAsian", "raceLatina", "raceBlack", "racePacific", "raceEuropean" ,"raceAmerindian", "raceSemitic", "raceEastern", "raceAryan", "raceLatina", "raceMalay"];
-	let rand = random(slave.ID);
+	let races = ["raceWhite", "raceAsian", "raceLatina", "raceBlack", "racePacific", "raceEuropean", "raceAmerindian", "raceSemitic", "raceEastern", "raceAryan", "raceLatina", "raceMalay"];
 	let index1 = Math.floor(random(slave.ID+1) * races.length);
 	let index2 = Math.floor(random(slave.ID-1) * (races.length-1));
 
@@ -569,12 +731,44 @@ App.Art.applyMorphs = function(slave, scene) {
 		case "malay":
 			morphs.push(["raceMalay", 1]); break;
 		case "mixed race":
-			morphs.push([races[index1], rand]);
+			morphs.push([races[index1], 0.5]);
 			races.splice(index1, index1);
-			morphs.push([races[index2], 1-rand]);
+			morphs.push([races[index2], 0.5]);
 			break;
 	}
 
+	if (slave.lips < 10) {
+		morphs.push(["lipsShapeThin", 1]);
+	} else if (slave.lips < 20) {
+		morphs.push(["lipsShapeNormal", 1]);
+	} else if (slave.lips < 40) {
+		morphs.push(["lipsShapePretty", 1]);
+	} else if (slave.lips < 70) {
+		morphs.push(["lipsShapePlush", 1]);
+	} else if (slave.lips < 95) {
+		morphs.push(["lipsShapeHuge", 1]);
+	} else {
+		morphs.push(["lipsShapeFacepussy", slave.lips/100]);
+	}
+
+	let eyeShape = ["eyeShapeNormal", "eyeShapeWide", "eyeShapeRound", "eyeShapeSmall", "eyeShapeSlit", "eyeShapeCute", "eyeShapeOpen"];
+	let eye = Math.floor(random(slave.ID+3) * eyeShape.length);
+	if (eye > 0) {
+		morphs.push(eyeShape[eye], 1);
+	}
+
+	let noseShape = ["noseShapeNormal", "noseShapeWide", "noseShapeForward", "noseShapeFlat", "noseShapeTriangular", "noseShapeSmall"];
+	let nose = Math.floor(random(slave.ID+4) * noseShape.length);
+	if (nose > 0) {
+		morphs.push(noseShape[nose], 1);
+	}
+
+	let foreheadShape = ["foreheadShapeNormal", "foreheadShapeRound", "foreheadShapeSmall"];
+	let forehead = Math.floor(random(slave.ID+5) * foreheadShape.length);
+	if (forehead > 0) {
+		morphs.push(foreheadShape[forehead], 1);
+	}
+
 	switch (slave.faceShape) {
 		case "normal":
 			break;
@@ -591,7 +785,7 @@ App.Art.applyMorphs = function(slave, scene) {
 	}
 
 	if (slave.boobs < 600) {
-		morphs.push(["boobsSmall", -(slave.boobs-600)/600]);
+		morphs.push(["boobShapeSmall", -(slave.boobs-600)/600]);
 	} else {
 		switch (slave.boobShape) {
 			case "normal":
@@ -599,15 +793,25 @@ App.Art.applyMorphs = function(slave, scene) {
 			case "perky":
 				morphs.push(["boobShapePerky", (Math.sqrt(slave.boobs-600)/125)]); break;
 			case "saggy":
-				morphs.push(["boobShapeSaggy", (Math.sqrt(slave.boobs-600)/50)]); break;
+				morphs.push(["boobShapeSaggy", (Math.sqrt(slave.boobs-600)/55)]); break;
 			case "torpedo-shaped":
 				morphs.push(["boobShapeTorpedo", (Math.sqrt(slave.boobs-600)/35)]); break;
 			case "downward-facing":
-				morphs.push(["boobShapeDownward", (Math.sqrt(slave.boobs-600)/160)]); break;
+				// special case to make nipple work
+				if (slave.nipples === "flat") {
+					morphs.push(["boobShapeDownward_nipplesFlat", (Math.sqrt(slave.boobs-600)/80)]); break;
+				} else {
+					morphs.push(["boobShapeDownward", (Math.sqrt(slave.boobs-600)/80)]); break;
+				}
 			case "wide-set":
 				morphs.push(["boobShapeWide", (Math.sqrt(slave.boobs-600)/40)]); break;
 			case "spherical":
-				morphs.push(["boobShapeSpherical", (Math.sqrt(slave.boobs-600)/60)]); break;
+				// special case to make nipple work
+				if (slave.nipples === "flat") {
+					morphs.push(["boobShapeSpherical_nipplesFlat", (Math.sqrt(slave.boobs-600)/80)]); break;
+				} else {
+					morphs.push(["boobShapeSpherical", (Math.sqrt(slave.boobs-600)/80)]); break;
+				}
 		}
 	}
 
@@ -615,13 +819,13 @@ App.Art.applyMorphs = function(slave, scene) {
 		case "flat":
 			break;
 		case "huge":
-			morphs.push(["nipplesHuge", Math.sqrt(slave.boobs)/20 + 0.5]); break;
+			morphs.push(["nipplesHuge", Math.sqrt(slave.boobs)/40 + 0.5]); break;
 		case "tiny":
-			morphs.push(["nipplesHuge", Math.sqrt(slave.boobs)/60 + 0.10]); break;
+			morphs.push(["nipplesHuge", Math.sqrt(slave.boobs)/90 + 0.1]); break;
 		case "cute":
 			morphs.push(["nipplesCute", Math.sqrt(slave.boobs)/20 + 0.5]); break;
 		case "puffy":
-			morphs.push(["nipplesPuffy", Math.sqrt(slave.boobs)/20 + 0.5]); break;
+			morphs.push(["nipplesPuffy", Math.sqrt(slave.boobs)/35 + 0.5]); break;
 		case "inverted":
 			morphs.push(["nipplesInverted", Math.sqrt(slave.boobs)/20 + 0.5]); break;
 		case "partially inverted":
@@ -656,13 +860,12 @@ App.Art.applyMorphs = function(slave, scene) {
 
 	morphs.push(["areolae", convertRange(0, 4, 0, 5, slave.areolae)]);
 	morphs.push(["shoulders", slave.shoulders/1.2]);
-	morphs.push(["lips", convertRange(0, 100, -1, 3, slave.lips)]);
-	scene.transform.scale = slave.height/175; // height by object transform
+	scene.models[0].transform.scale = slave.height/175; // height by object transform
 	if (slave.muscles > 0) {
-		morphs.push(["muscles", slave.muscles/50]);
+		morphs.push(["muscles", slave.muscles/33]);
 	}
 
-	morphs.push(["belly", Math.sqrt(slave.belly)/175]);
+	morphs.push(["belly", Math.sqrt(slave.belly)/150]);
 	morphs.push(["hips", slave.hips/2]);
 
 	if (slave.butt<=1) {
@@ -678,15 +881,15 @@ App.Art.applyMorphs = function(slave, scene) {
 	}
 
 	if (slave.weight >= 0) {
-		morphs.push(["weight", slave.weight/50]);
+		morphs.push(["weight", slave.weight/75]);
 	} else {
 		morphs.push(["weightThin", -slave.weight/80]);
 	}
 
 	if (slave.visualAge < 20) {
-		morphs.push(["physicalAgeYoung", -(slave.visualAge-20)/15]);
+		morphs.push(["physicalAgeYoung", -(slave.visualAge-20)/20]);
 	} else {
-		morphs.push(["physicalAgeOld", (slave.visualAge-20)/100]);
+		morphs.push(["physicalAgeOld", (slave.visualAge-20)/66]);
 	}
 
 	if (!hasLeftArm(slave)) {
@@ -702,12 +905,12 @@ App.Art.applyMorphs = function(slave, scene) {
 		morphs.push(["amputeeRightLeg", 1]);
 	}
 
-	App.Art.resetMorphs(slave, scene);
+	App.Art.resetMorphs(scene);
 
-	for (let i =0; i < scene.model.morphs.length; i++) {
+	for (let i =0; i < scene.models[0].morphs.length; i++) {
 		for (let j =0; j < morphs.length; j++) {
-			if (scene.model.morphs[i].morphId === morphs[j][0]) {
-				scene.model.morphs[i].value = morphs[j][1];
+			if (scene.models[0].morphs[i].morphId === morphs[j][0]) {
+				scene.models[0].morphs[i].value = morphs[j][1];
 			}
 		}
 	}
diff --git a/src/art/webgl/contents.txt b/src/art/webgl/contents.txt
index 9bbdc2f4bdfa898e6d6de99b4de692343656a102..bdd046e9ea25adfb6b10b34cf56e1545f4a6ea98 100644
--- a/src/art/webgl/contents.txt
+++ b/src/art/webgl/contents.txt
@@ -55,11 +55,45 @@ faceShape
 	sensual
 	exotic
 
+lips	
+	thin
+	normal
+	pretty
+	plush
+	huge
+	facepussy
+	
+eyes	
+	normal
+	wide
+	round
+	cute
+	slit
+	small
+	open
+	
+nose	
+	normal
+	wide
+	forward
+	flat
+	triangular
+	small
+	
+forehead	
+	normal
+	round
+	small
+
 areolaeShape	
 	heart
 	star
 	circle
 
+expressions
+	happy
+	fear
+
 race	
 	white
 	asian
@@ -149,4 +183,24 @@ General
     dick
 
 Hairstyle
-    bobHair
+	Neat
+	Afro
+	Braided
+	Cornrows
+	Curled
+	Dreadlocks
+	Eary
+	In a bun
+	In a messy bun
+	In a ponytail
+	In tails
+	Luxurious
+	Messy
+	Permed
+	Shaved sides
+	Up
+	Undercut
+	Shaved
+	Buzzcut
+	Trimmed short
+
diff --git a/src/art/webgl/engine.js b/src/art/webgl/engine.js
index 697b77ced0c6fc344d3d50fa5309be85b0df12c0..d0056f30c15898aa4485cb27c0c79300559a1825 100644
--- a/src/art/webgl/engine.js
+++ b/src/art/webgl/engine.js
@@ -190,61 +190,154 @@ App.Art.Engine = class {
                 }`;
 	}
 
-	initBuffers() {
-		this.backgroundPositionBuffer = this.gl.createBuffer();
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.backgroundPositionBuffer);
+	initBuffers(sceneData) {
+		// init buffer containers
+		this.buffers = new class {};
+		this.buffers.models = [];
+		for (let m=0; m < sceneData.models.length; m++) {
+			this.buffers.models[m] = new class {};
+		}
+
+		// init background buffers
+		this.buffers.backgroundPositionBuffer = this.gl.createBuffer();
+		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffers.backgroundPositionBuffer);
 		this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, 1, 1, -1, 1]), this.gl.STATIC_DRAW);
 
-		this.backgroundIndexBuffer = this.gl.createBuffer();
-		this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.backgroundIndexBuffer);
+		this.buffers.backgroundIndexBuffer = this.gl.createBuffer();
+		this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.buffers.backgroundIndexBuffer);
 		this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), this.gl.STATIC_DRAW);
 
-		this.verticesPositionBuffer = this.gl.createBuffer();
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesPositionBuffer);
-		this.gl.bufferData(this.gl.ARRAY_BUFFER, this.base64ToFloat(this.sceneData.model.verts), this.gl.STATIC_DRAW);
-		this.vertexCount = this.gl.getBufferParameter(this.gl.ARRAY_BUFFER, this.gl.BUFFER_SIZE)/4;
-
-		this.verticesNormalBuffer = this.gl.createBuffer();
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesNormalBuffer);
-		this.gl.bufferData(this.gl.ARRAY_BUFFER, this.base64ToFloat(this.sceneData.model.vertsn), this.gl.STATIC_DRAW);
-
-		this.verticesTextureCoordBuffer = this.gl.createBuffer();
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesTextureCoordBuffer);
-		this.gl.bufferData(this.gl.ARRAY_BUFFER, this.base64ToFloat(this.sceneData.model.texts), this.gl.STATIC_DRAW);
-
-		this.verticesTangentBuffer = this.gl.createBuffer();
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesTangentBuffer);
-		this.gl.bufferData(this.gl.ARRAY_BUFFER, this.base64ToFloat(this.sceneData.model.tans), this.gl.STATIC_DRAW);
-
-		this.vertexPositionMorphs = [];
-		this.vertexNormalMorphs = [];
-		this.vertexIndexMorphs = [];
-		for(let i=0; i < this.sceneData.model.mverts.length; i++) {
-			this.vertexPositionMorphs[i] = this.base64ToFloat(this.sceneData.model.mverts[i]);
-			this.vertexNormalMorphs[i] = this.base64ToFloat(this.sceneData.model.mvertsn[i]);
-			let vertexIndexMorph = this.base64ToInt(this.sceneData.model.mvertsi[i]);
-			this.vertexIndexMorphs[i] = vertexIndexMorph.map((sum => value => sum += value)(0));
+		// init model buffers
+		for (let m=0; m < this.buffers.models.length; m++) {
+			let modelBuffers = this.buffers.models[m];
+			let modelData = sceneData.models[m];
+
+			modelBuffers.verticesPositionBuffer = [];
+			modelBuffers.verticesNormalBuffer = [];
+			modelBuffers.verticesTextureCoordBuffer = [];
+			modelBuffers.verticesTangentBuffer = [];
+			modelBuffers.vertexCount = [];
+			modelBuffers.verticesMorphBuffer = [];
+			modelBuffers.verticesNormalMorphBuffer = [];
+
+			modelBuffers.verticesIndexBuffer = [];
+			modelBuffers.indexSizes = [];
+			for (let i=0, count=0; i < modelData.figures.length; i++) {
+				modelBuffers.verticesPositionBuffer[i] = this.gl.createBuffer();
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesPositionBuffer[i]);
+				this.gl.bufferData(this.gl.ARRAY_BUFFER, this.base64ToFloat(modelData.figures[i].verts), this.gl.STATIC_DRAW);
+				modelBuffers.vertexCount[i] = this.gl.getBufferParameter(this.gl.ARRAY_BUFFER, this.gl.BUFFER_SIZE)/4;
+
+				modelBuffers.verticesNormalBuffer[i] = this.gl.createBuffer();
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesNormalBuffer[i]);
+				this.gl.bufferData(this.gl.ARRAY_BUFFER, this.base64ToFloat(modelData.figures[i].vertsn), this.gl.STATIC_DRAW);
+
+				modelBuffers.verticesTextureCoordBuffer[i] = this.gl.createBuffer();
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesTextureCoordBuffer[i]);
+				this.gl.bufferData(this.gl.ARRAY_BUFFER, this.base64ToFloat(modelData.figures[i].texts), this.gl.STATIC_DRAW);
+
+				modelBuffers.verticesTangentBuffer[i] = this.gl.createBuffer();
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesTangentBuffer[i]);
+				this.gl.bufferData(this.gl.ARRAY_BUFFER, this.base64ToFloat(modelData.figures[i].tans), this.gl.STATIC_DRAW);
+
+				// return dummy morph
+				modelBuffers.verticesMorphBuffer[i] = this.gl.createBuffer();
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesMorphBuffer[i]);
+				this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(0), this.gl.STATIC_DRAW);
+
+				modelBuffers.verticesNormalMorphBuffer[i] = this.gl.createBuffer();
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesNormalMorphBuffer[i]);
+				this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(0), this.gl.STATIC_DRAW);
+
+				for (let j=0; j < modelData.figures[i].surfaces.length; j++, count++) {
+					modelBuffers.verticesIndexBuffer[count] = this.gl.createBuffer();
+					this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, modelBuffers.verticesIndexBuffer[count]);
+					let intArray = this.base64ToInt(modelData.figures[i].surfaces[j].vertsi);
+					this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, intArray, this.gl.STATIC_DRAW);
+					modelBuffers.indexSizes[count] = intArray.length;
+				}
+			}
+
+			this.initMorphs(modelBuffers, modelData);
 		}
+	}
 
-		this.verticesMorphBuffer = this.gl.createBuffer();
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesMorphBuffer);
-		this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(0), this.gl.STATIC_DRAW);
-
-		this.verticesNormalMorphBuffer = this.gl.createBuffer();
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesNormalMorphBuffer);
-		this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(0), this.gl.STATIC_DRAW);
-
-		this.verticesIndexBuffer = [];
-		this.indexSizes = [];
-		for (let i=0, count=0; i < this.sceneData.model.figures.length; i++) {
-			for (let j=0; j < this.sceneData.model.figures[i].surfaces.length; j++, count++) {
-				this.verticesIndexBuffer[count] = this.gl.createBuffer();
-				this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.verticesIndexBuffer[count]);
-				let intArray = this.base64ToInt(this.sceneData.model.figures[i].surfaces[j].vertsi);
-				this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, intArray, this.gl.STATIC_DRAW);
-				this.indexSizes[count] = intArray.length;
+	initMorphs(modelBuffers, modelData) {
+		window.sceneBlocks = {}; // automatically populated during loading of morphs
+
+		let promisedMorphs = [];
+		modelBuffers.vertexPositionMorphs = [];
+		modelBuffers.vertexNormalMorphs = [];
+		modelBuffers.vertexIndexMorphs = [];
+		for (let m=0; m < modelData.morphs.length; m++) {
+			modelBuffers.vertexPositionMorphs[m] = [];
+			modelBuffers.vertexNormalMorphs[m] = [];
+			modelBuffers.vertexIndexMorphs[m] = [];
+
+			for (let f=0; f < modelData.figures.length; f++) {
+				modelBuffers.vertexPositionMorphs[m].push(new Float32Array(0));
+				modelBuffers.vertexNormalMorphs[m].push(new Float32Array(0));
+				modelBuffers.vertexIndexMorphs[m].push(new Int32Array(0));
 			}
+
+			// stream real morphs
+			promisedMorphs.push(this.loadMorph(modelBuffers, m, modelData.morphs[m]));
 		}
+
+		Promise.all(promisedMorphs).then((values) => {
+			if (values.length > 0) { // promise triggers twice (?)
+				window.sceneBlocks = null; // let garbage collector clean
+				if (App.Art.engineReady) { // re-send loaded event after morphs finish streaming
+					modelBuffers.oldMorphValues = null;
+					let containers = document.getElementsByClassName("artContainer");
+					for (let i = 0; i < containers.length; i++) {
+						containers[i].dispatchEvent(new Event("engineLoaded"));
+					}
+				}
+			}
+		});
+	}
+
+	loadMorph(modelBuffers, m, path) {
+		let engine = this;
+		return new Promise(function(resolve, reject) {
+			let script = document.createElement("script");
+			script.onload = function() {
+				let morph = window.sceneBlocks[path];
+
+				for (let i=0; i < morph.length; i+=3) {
+					modelBuffers.vertexPositionMorphs[m][i/3] = engine.base64ToFloat(morph[i+0]);
+					modelBuffers.vertexNormalMorphs[m][i/3] = engine.base64ToFloat(morph[i+1]);
+					// reconstruct compressed indices
+					modelBuffers.vertexIndexMorphs[m][i/3] = engine.base64ToInt(morph[i+2]).map((sum => value => sum += value)(0));
+				}
+				resolve();
+			};
+			script.onerror = function(e) {
+				reject(e, script);
+			};
+			script.src = path;
+			document.head.appendChild(script);
+		});
+	}
+
+	initTextures(sceneData) {
+		// load model textures
+		this.textures = [];
+		let promisedTextures = [];
+		for (let i=0; i < sceneData.textures.length; i++) {
+			const {texture, promise} = this.loadTexture(this.gl, sceneData.textures[i]);
+			this.textures[i] = texture;
+			promisedTextures[i] = promise;
+		}
+		Promise.all(promisedTextures).then(() => {
+			if (App.Art.engineReady) { // re-send loaded event after textures finish streaming
+				let containers = document.getElementsByClassName("artContainer");
+				for (let i = 0; i < containers.length; i++) {
+					containers[i].dispatchEvent(new Event("engineLoaded"));
+				}
+			}
+		});
 	}
 
 	loadTexture(gl, url) {
@@ -271,25 +364,6 @@ App.Art.Engine = class {
 		return {texture, promise};
 	}
 
-	initTextures() {
-		// load model textures
-		this.modelTextures = [];
-		let promisedTextures = [];
-		for (let i=0; i < this.sceneData.textures.length; i++) {
-			const {texture, promise} = this.loadTexture(this.gl, this.sceneData.textures[i]);
-			this.modelTextures[i] = texture;
-			promisedTextures[i] = promise;
-		}
-		Promise.all(promisedTextures).then(() => {
-			if (App.Art.engineReady) { // re-send loaded event after textures finish streaming
-				let containers = document.getElementsByClassName("artContainer");
-				for (let i = 0; i < containers.length; i++) {
-					containers[i].dispatchEvent(new Event("engineLoaded"));
-				}
-			}
-		});
-	}
-
 	initShaders(sceneParams) {
 		// compile shaders
 		let vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
@@ -344,8 +418,6 @@ App.Art.Engine = class {
 	}
 
 	bind(sceneData, sceneParams) {
-		this.sceneData = sceneData;
-
 		this.offscreenCanvas = document.createElement("canvas");
 		this.gl = this.offscreenCanvas.getContext("webgl2", {alpha:true, premultipliedAlpha: true});
 
@@ -357,8 +429,8 @@ App.Art.Engine = class {
 		this.gl.blendEquation( this.gl.FUNC_ADD );
 		this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
 
-		this.initBuffers();
-		this.initTextures();
+		this.initBuffers(sceneData);
+		this.initTextures(sceneData);
 		this.initShaders(sceneParams);
 	}
 
@@ -376,10 +448,10 @@ App.Art.Engine = class {
 			this.drawBackground(sceneParams);
 		}
 
-		// draw model
+		// draw scene
 		this.gl.clear(this.gl.DEPTH_BUFFER_BIT);
 		this.gl.useProgram(this.shaderProgram);
-		this.drawModel(sceneParams);
+		this.drawScene(sceneParams);
 
 		// clone from offscreen to real canvas
 		let ctx = canvas.getContext('2d', {alpha:true});
@@ -392,16 +464,16 @@ App.Art.Engine = class {
 		this.gl.uniform4fv(this.gl.getUniformLocation(this.shaderProgramBg, "backgroundColor"), sceneParams.background.color);
 
 		this.gl.activeTexture(this.gl.TEXTURE0);
-		this.gl.bindTexture(this.gl.TEXTURE_2D, this.modelTextures[sceneParams.background.filename]);
+		this.gl.bindTexture(this.gl.TEXTURE_2D, this.textures[sceneParams.background.filename]);
 
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.backgroundPositionBuffer);
+		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffers.backgroundPositionBuffer);
 		this.gl.vertexAttribPointer(this.backgroundPositionAttribute, 2, this.gl.FLOAT, false, 0, 0);
 
-		this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.backgroundIndexBuffer);
+		this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.buffers.backgroundIndexBuffer);
 		this.gl.drawElements(this.gl.TRIANGLES, 6, this.gl.UNSIGNED_SHORT, 0);
 	}
 
-	drawModel(sceneParams) {
+	drawScene(sceneParams) {
 		// create camera
 		let camRotX = this.degreeToRad(-sceneParams.camera.xr);
 		let camRotY = this.degreeToRad(-sceneParams.camera.yr);
@@ -415,15 +487,11 @@ App.Art.Engine = class {
 		let target = this.vectorAdd(lookDir, camera);
 		let matCamera = this.matrixPointAt(camera, target, up);
 
-		// create transforms
-		this.applyMorphs(sceneParams);
+		// create scene transforms
 		let matProj = this.matrixMakeProjection(sceneParams.camera.fov, sceneParams.settings.rheight/sceneParams.settings.rwidth, sceneParams.camera.fnear, sceneParams.camera.ffar);
 		let matView = this.matrixInverse(matCamera);
-		let matRot = this.matrixMakeRotation(this.degreeToRad(sceneParams.transform.xr), this.degreeToRad(sceneParams.transform.yr), this.degreeToRad(sceneParams.transform.zr));
-		let matTrans = this.matrixMakeTranslation(sceneParams.transform.x, sceneParams.transform.y, sceneParams.transform.z);
-		let matScale = this.matrixMakeScaling( sceneParams.transform.scale);
 
-		// set uniforms
+		// set scene uniforms
 		this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "sNormals"), sceneParams.settings.normals);
 		this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "sAmbient"), sceneParams.settings.ambient);
 		this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "sDiffuse"), sceneParams.settings.diffuse);
@@ -452,135 +520,157 @@ App.Art.Engine = class {
 
 		this.gl.uniform3fv(this.gl.getUniformLocation(this.shaderProgram, "lookDir"), lookDir);
 
-		for (let i = 0; i < sceneParams.model.morphs.length; i++) {
-			this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, sceneParams.model.morphs[i].morphId), sceneParams.model.morphs[i].value);
-		}
-
-		this.gl.uniformMatrix4fv(this.gl.getUniformLocation(this.shaderProgram, "matTrans"), false, new Float32Array(this.matrixFlatten(matTrans)));
-		this.gl.uniformMatrix4fv(this.gl.getUniformLocation(this.shaderProgram, "matScale"), false, new Float32Array(this.matrixFlatten(matScale)));
-		this.gl.uniformMatrix4fv(this.gl.getUniformLocation(this.shaderProgram, "matRot"), false, new Float32Array(this.matrixFlatten(matRot)));
 		this.gl.uniformMatrix4fv(this.gl.getUniformLocation(this.shaderProgram, "matProj"), false, new Float32Array(this.matrixFlatten(matProj)));
 		this.gl.uniformMatrix4fv(this.gl.getUniformLocation(this.shaderProgram, "matView"), false, new Float32Array(this.matrixFlatten(matView)));
 
-		// bind vertex buffers
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesPositionBuffer);
-		this.gl.vertexAttribPointer(this.vertexPositionAttribute, 3, this.gl.FLOAT, false, 0, 0);
+		// process each model in the scene
+		for (let m=0; m < this.buffers.models.length; m++) {
+			let modelBuffers = this.buffers.models[m];
+			let modelParams = sceneParams.models[m];
 
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesTextureCoordBuffer);
-		this.gl.vertexAttribPointer(this.textureCoordAttribute, 2, this.gl.FLOAT, false, 0, 0);
-
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesNormalBuffer);
-		this.gl.vertexAttribPointer(this.vertexNormalAttribute, 3, this.gl.FLOAT, false, 0, 0);
+			if(!modelParams.visible) {
+				continue;
+			}
 
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesTangentBuffer);
-		this.gl.vertexAttribPointer(this.vertexTangentAttribute, 3, this.gl.FLOAT, false, 0, 0);
+			// create model transforms
+			this.applyMorphs(modelParams, modelBuffers);
 
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesMorphBuffer);
-		this.gl.vertexAttribPointer(this.vertexPositionMorphAttribute, 3, this.gl.FLOAT, false, 0, 0);
+			let matRot = this.matrixMakeRotation(this.degreeToRad(modelParams.transform.xr), this.degreeToRad(modelParams.transform.yr), this.degreeToRad(modelParams.transform.zr));
+			let matTrans = this.matrixMakeTranslation(modelParams.transform.x, modelParams.transform.y, modelParams.transform.z);
+			let matScale = this.matrixMakeScaling( modelParams.transform.scale);
 
-		this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesNormalMorphBuffer);
-		this.gl.vertexAttribPointer(this.vertexNormalMorphAttribute, 3, this.gl.FLOAT, false, 0, 0);
+			// set model uniforms
+			this.gl.uniformMatrix4fv(this.gl.getUniformLocation(this.shaderProgram, "matTrans"), false, new Float32Array(this.matrixFlatten(matTrans)));
+			this.gl.uniformMatrix4fv(this.gl.getUniformLocation(this.shaderProgram, "matScale"), false, new Float32Array(this.matrixFlatten(matScale)));
+			this.gl.uniformMatrix4fv(this.gl.getUniformLocation(this.shaderProgram, "matRot"), false, new Float32Array(this.matrixFlatten(matRot)));
 
-		// bind materials per surface
-		for (let i=0, count=0; i < this.sceneData.model.figures.length; i++) {
-			for (let j=0; j < this.sceneData.model.figures[i].surfaces.length; j++, count++) {
-				if(!sceneParams.model.figures[i].visible) {
+			for (let i=0, count=0; i < modelParams.figures.length; i++) {
+				if(!modelParams.figures[i].visible) {
+					count += modelParams.figures[i].surfaces.length;
 					continue;
 				}
 
-				let visible = sceneParams.model.figures[i].surfaces[j].visible;
+				// bind vertex buffers per figure
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesPositionBuffer[i]);
+				this.gl.vertexAttribPointer(this.vertexPositionAttribute, 3, this.gl.FLOAT, false, 0, 0);
 
-				for (let h=0; h < sceneParams.model.figures[i].surfaces[j].matIds.length; h++) {
-					let matId = sceneParams.model.figures[i].surfaces[j].matIds[h];
-					let matIdx = sceneParams.materials.map(e => e.matId).indexOf(matId);
-					if (matIdx === -1) {
-						continue;
-					}
-					let mat = sceneParams.materials[matIdx];
-
-					if (mat.d > 0 && visible) {
-						this.gl.activeTexture(this.gl.TEXTURE0);
-						this.gl.bindTexture(this.gl.TEXTURE_2D, this.modelTextures[parseInt(mat.map_Ka)]);
-						this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgram, "textSampler[0]"), 0);
-
-						this.gl.activeTexture(this.gl.TEXTURE1);
-						this.gl.bindTexture(this.gl.TEXTURE_2D, this.modelTextures[parseInt(mat.map_Kd)]);
-						this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgram, "textSampler[1]"), 1);
-
-						this.gl.activeTexture(this.gl.TEXTURE2);
-						this.gl.bindTexture(this.gl.TEXTURE_2D, this.modelTextures[parseInt(mat.map_Ks)]);
-						this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgram, "textSampler[2]"), 2);
-
-						this.gl.activeTexture(this.gl.TEXTURE3);
-						this.gl.bindTexture(this.gl.TEXTURE_2D, this.modelTextures[parseInt(mat.map_Ns)]);
-						this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgram, "textSampler[3]"), 3);
-
-						this.gl.activeTexture(this.gl.TEXTURE4);
-						this.gl.bindTexture(this.gl.TEXTURE_2D, this.modelTextures[parseInt(mat.map_D)]);
-						this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgram, "textSampler[4]"), 4);
-
-						this.gl.activeTexture(this.gl.TEXTURE5);
-						this.gl.bindTexture(this.gl.TEXTURE_2D, this.modelTextures[parseInt(mat.map_Kn)]);
-						this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgram, "textSampler[5]"), 5);
-
-						this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "d"), mat.d);
-						this.gl.uniform3fv(this.gl.getUniformLocation(this.shaderProgram, "Ka"), mat.Ka);
-						this.gl.uniform3fv(this.gl.getUniformLocation(this.shaderProgram, "Kd"), mat.Kd);
-						this.gl.uniform3fv(this.gl.getUniformLocation(this.shaderProgram, "Ks"), mat.Ks);
-						this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "Ns"), mat.Ns);
-
-						// draw materials
-						this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.verticesIndexBuffer[count]);
-						this.gl.drawElements(this.gl.TRIANGLES, this.indexSizes[count], this.gl.UNSIGNED_INT, 0);
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesTextureCoordBuffer[i]);
+				this.gl.vertexAttribPointer(this.textureCoordAttribute, 2, this.gl.FLOAT, false, 0, 0);
+
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesNormalBuffer[i]);
+				this.gl.vertexAttribPointer(this.vertexNormalAttribute, 3, this.gl.FLOAT, false, 0, 0);
+
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesTangentBuffer[i]);
+				this.gl.vertexAttribPointer(this.vertexTangentAttribute, 3, this.gl.FLOAT, false, 0, 0);
+
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesMorphBuffer[i]);
+				this.gl.vertexAttribPointer(this.vertexPositionMorphAttribute, 3, this.gl.FLOAT, false, 0, 0);
+
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesNormalMorphBuffer[i]);
+				this.gl.vertexAttribPointer(this.vertexNormalMorphAttribute, 3, this.gl.FLOAT, false, 0, 0);
+
+				// bind materials per surface and set uniforms
+				for (let j=0; j < modelParams.figures[i].surfaces.length; j++, count++) {
+					let visible = modelParams.figures[i].surfaces[j].visible;
+
+					for (let h=0; h < modelParams.figures[i].surfaces[j].matIds.length; h++) {
+						let matId = modelParams.figures[i].surfaces[j].matIds[h];
+						let matIdx = sceneParams.materials.map(e => e.matId).indexOf(matId);
+						if (matIdx === -1) {
+							continue;
+						}
+						let mat = sceneParams.materials[matIdx];
+
+						if (mat.d > 0 && visible) {
+							this.gl.activeTexture(this.gl.TEXTURE0);
+							this.gl.bindTexture(this.gl.TEXTURE_2D, this.textures[parseInt(mat.map_Ka)]);
+							this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgram, "textSampler[0]"), 0);
+
+							this.gl.activeTexture(this.gl.TEXTURE1);
+							this.gl.bindTexture(this.gl.TEXTURE_2D, this.textures[parseInt(mat.map_Kd)]);
+							this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgram, "textSampler[1]"), 1);
+
+							this.gl.activeTexture(this.gl.TEXTURE2);
+							this.gl.bindTexture(this.gl.TEXTURE_2D, this.textures[parseInt(mat.map_Ks)]);
+							this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgram, "textSampler[2]"), 2);
+
+							this.gl.activeTexture(this.gl.TEXTURE3);
+							this.gl.bindTexture(this.gl.TEXTURE_2D, this.textures[parseInt(mat.map_Ns)]);
+							this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgram, "textSampler[3]"), 3);
+
+							this.gl.activeTexture(this.gl.TEXTURE4);
+							this.gl.bindTexture(this.gl.TEXTURE_2D, this.textures[parseInt(mat.map_D)]);
+							this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgram, "textSampler[4]"), 4);
+
+							this.gl.activeTexture(this.gl.TEXTURE5);
+							this.gl.bindTexture(this.gl.TEXTURE_2D, this.textures[parseInt(mat.map_Kn)]);
+							this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgram, "textSampler[5]"), 5);
+
+							this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "d"), mat.d);
+							this.gl.uniform3fv(this.gl.getUniformLocation(this.shaderProgram, "Ka"), mat.Ka);
+							this.gl.uniform3fv(this.gl.getUniformLocation(this.shaderProgram, "Kd"), mat.Kd);
+							this.gl.uniform3fv(this.gl.getUniformLocation(this.shaderProgram, "Ks"), mat.Ks);
+							this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "Ns"), mat.Ns);
+
+							// draw materials
+							this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, modelBuffers.verticesIndexBuffer[count]);
+							this.gl.drawElements(this.gl.TRIANGLES, modelBuffers.indexSizes[count], this.gl.UNSIGNED_INT, 0);
+						}
 					}
 				}
 			}
 		}
 	}
 
-	applyMorphs(sceneParams) {
-		if(this.oldMorphValues !== JSON.stringify(sceneParams.model.morphs)) {
-			let vertexPositionMorph = new Float32Array(this.vertexCount);
-			let vertexNormalMorph = new Float32Array(this.vertexCount);
-
-			for(let i=0; i < this.vertexPositionMorphs.length; i++) {
-				let morphValue = sceneParams.model.morphs[i].value;
-
-				if (morphValue !== 0) {
-					let vp = this.vertexPositionMorphs[i];
-					let vn = this.vertexNormalMorphs[i];
-					let vi = this.vertexIndexMorphs[i];
+	applyMorphs(modelParams, modelBuffers) {
+		if(modelBuffers.oldMorphValues !== JSON.stringify(modelParams.morphs) + JSON.stringify(modelParams.figures)) {
+			for (let f=0; f < modelParams.figures.length; f++) {
+				if(!modelParams.visible || !modelParams.figures[f].visible) {
+					continue;
+				}
 
-					if (morphValue === 1) {
-						for(let j = 0; j < vi.length; j++) {
-							vertexPositionMorph[vi[j]] += vp[j];
-							vertexNormalMorph[vi[j]] += vn[j];
-						}
-					} else {
-						for(let j=0; j < vi.length; j++) {
-							vertexPositionMorph[vi[j]] += vp[j] * morphValue;
-							vertexNormalMorph[vi[j]] += vn[j] * morphValue;
+				let vertexPositionMorph = new Float32Array(modelBuffers.vertexCount[f]);
+				let vertexNormalMorph = new Float32Array(modelBuffers.vertexCount[f]);
+
+				for(let m=0; m < modelParams.morphs.length; m++) {
+					let morphValue = modelParams.morphs[m].value;
+
+					if (morphValue !== 0) {
+						let vp = modelBuffers.vertexPositionMorphs[m][f];
+						let vn = modelBuffers.vertexNormalMorphs[m][f];
+						let vi = modelBuffers.vertexIndexMorphs[m][f];
+
+						if (morphValue === 1) {
+							for(let j = 0; j < vi.length; j++) {
+								vertexPositionMorph[vi[j]] += vp[j];
+								vertexNormalMorph[vi[j]] += vn[j];
+							}
+						} else {
+							for(let j=0; j < vi.length; j++) {
+								vertexPositionMorph[vi[j]] += vp[j] * morphValue;
+								vertexNormalMorph[vi[j]] += vn[j] * morphValue;
+							}
 						}
 					}
 				}
-			}
 
-			this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesMorphBuffer);
-			this.gl.bufferData(this.gl.ARRAY_BUFFER, vertexPositionMorph, this.gl.STATIC_DRAW);
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesMorphBuffer[f]);
+				this.gl.bufferData(this.gl.ARRAY_BUFFER, vertexPositionMorph, this.gl.STATIC_DRAW);
 
-			this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesNormalMorphBuffer);
-			this.gl.bufferData(this.gl.ARRAY_BUFFER, vertexNormalMorph, this.gl.STATIC_DRAW);
+				this.gl.bindBuffer(this.gl.ARRAY_BUFFER, modelBuffers.verticesNormalMorphBuffer[f]);
+				this.gl.bufferData(this.gl.ARRAY_BUFFER, vertexNormalMorph, this.gl.STATIC_DRAW);
+			}
 
-			this.oldMorphValues = JSON.stringify(sceneParams.model.morphs);
+			modelBuffers.oldMorphValues = JSON.stringify(modelParams.morphs) + JSON.stringify(modelParams.figures);
 		}
 	}
 
 	base64ToFloat(array) {
-		let b	= window.atob(array),
-			fLen	= b.length / (Float32Array.BYTES_PER_ELEMENT-1),
-			dView	= new DataView(new ArrayBuffer(Float32Array.BYTES_PER_ELEMENT)),
-			fAry	= new Float32Array(fLen),
-			p		= 0;
+		let b = window.atob(array);
+		let fLen = b.length / (Float32Array.BYTES_PER_ELEMENT-1);
+		let dView = new DataView(new ArrayBuffer(Float32Array.BYTES_PER_ELEMENT));
+		let fAry = new Float32Array(fLen);
+		let p = 0;
 
 		for(let j=0; j < fLen; j++){
 			p = j * 3;
@@ -594,11 +684,11 @@ App.Art.Engine = class {
 	}
 
 	base64ToInt(array) {
-		let b	= window.atob(array),
-			fLen	= b.length / Int32Array.BYTES_PER_ELEMENT,
-			dView	= new DataView(new ArrayBuffer(Int32Array.BYTES_PER_ELEMENT)),
-			fAry	= new Int32Array(fLen),
-			p		= 0;
+		let b = window.atob(array);
+		let fLen = b.length / Int32Array.BYTES_PER_ELEMENT;
+		let dView = new DataView(new ArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
+		let fAry = new Int32Array(fLen);
+		let p = 0;
 
 		for(let j=0; j < fLen; j++){
 			p = j * 4;
@@ -612,10 +702,10 @@ App.Art.Engine = class {
 	}
 
 	base64ToByte(array) {
-		let b	= window.atob(array),
-			fLen	= b.length / Uint8Array.BYTES_PER_ELEMENT,
-			dView	= new DataView(new ArrayBuffer(Uint8Array.BYTES_PER_ELEMENT)),
-			fAry	= new Uint8Array(fLen);
+		let b = window.atob(array);
+		let fLen = b.length / Uint8Array.BYTES_PER_ELEMENT;
+		let dView = new DataView(new ArrayBuffer(Uint8Array.BYTES_PER_ELEMENT));
+		let fAry = new Uint8Array(fLen);
 
 		for(let j=0; j < fLen; j++){
 			dView.setUint8(0, b.charCodeAt(j));
diff --git a/src/art/webgl/ui.js b/src/art/webgl/ui.js
index 15335fd01e571ed3411b2b455d3eecd4f17ad0be..0234056472c6a0429e78437b87f5321d8c4c4b25 100644
--- a/src/art/webgl/ui.js
+++ b/src/art/webgl/ui.js
@@ -7,6 +7,8 @@ App.Art.createWebglUI = function(container, slave, artSize, scene) {
 	let faceViewEnabled = "resources/webgl/ui/faceViewEnabled.png";
 	let resetViewDisabled = "resources/webgl/ui/resetViewDisabled.png";
 	let resetViewEnabled = "resources/webgl/ui/resetViewEnabled.png";
+	let inspectViewDisabled = "resources/webgl/ui/inspectViewDisabled.png";
+	let inspectViewEnabled = "resources/webgl/ui/inspectViewEnabled.png";
 
 	let uicontainer = document.createElement("div");
 	uicontainer.setAttribute("style", "left: 82.5%; top: 5%; position: absolute; width: 15%; border: 0px; padding: 0px;");
@@ -33,34 +35,59 @@ App.Art.createWebglUI = function(container, slave, artSize, scene) {
 	btnResetView.setAttribute("type", "image");
 	btnResetView.setAttribute("src", scene.resetView ? resetViewEnabled : resetViewDisabled);
 
+	// btnInspectView
+	let btnInspectView = document.createElement("input");
+	btnInspectView.setAttribute("style", "display: flex; width: 100%; position: relative; border: 0px; padding: 0px; background-color: transparent;");
+	btnInspectView.setAttribute("type", "image");
+	btnInspectView.setAttribute("src", scene.inspectView ? inspectViewDisabled : inspectViewEnabled);
+
 	// events
+	btnInspectView.onclick = function(e){
+		scene.inspectView = true;
+		scene.resetView = true;
+		scene.faceView = false;
+		btnFaceView.src = faceViewEnabled;
+		btnResetView.src = resetViewEnabled;
+		btnInspectView.src = inspectViewDisabled;
+
+		scene.models[0].transform.yr = 180;
+		App.Art.applyMorphs(slave, scene);
+		App.Art.Frame(slave, scene);
+		App.Art.engine.render(scene, cvs);
+	};
+
 	btnLockView.onclick = function(e){
 		scene.lockView = !scene.lockView;
-
 		btnLockView.src = scene.lockView ? lockViewDisabled : lockViewEnabled;
 	};
 
 	btnFaceView.onclick = function(e){
 		scene.resetView = true;
+		scene.inspectView = false;
 		scene.faceView = false;
 		btnFaceView.src = faceViewDisabled;
 		btnResetView.src = resetViewEnabled;
+		btnInspectView.src = inspectViewEnabled;
 
 		scene.camera.y = slave.height-5;
-		scene.transform.yr = 0;
-		scene.camera.z = -40 - slave.height/20;
+		scene.models[0].transform.yr = 0;
+		scene.camera.xr = -6;
+		scene.camera.z = -slave.height/3.85;
+		App.Art.applyMorphs(slave, scene);
 		App.Art.engine.render(scene, cvs);
 	};
 
 	btnResetView.onclick = function(e){
 		scene.resetView = false;
 		scene.faceView = true;
+		scene.inspectView = false;
 		btnResetView.src = resetViewDisabled;
 		btnFaceView.src = faceViewEnabled;
+		btnInspectView.src = inspectViewEnabled;
 
-		scene.camera.y = App.Art.defaultScene.camera.y;
-		scene.transform.yr = App.Art.defaultScene.transform.yr;
-		scene.camera.z = App.Art.defaultScene.camera.z;
+		scene.models[0].transform.yr = App.Art.defaultScene.models[0].transform.yr;
+		App.Art.applyMorphs(slave, scene);
+		App.Art.Frame(slave, scene);
 		App.Art.engine.render(scene, cvs);
 	};
 
@@ -75,8 +102,8 @@ App.Art.createWebglUI = function(container, slave, artSize, scene) {
 		btnResetView.src = resetViewEnabled;
 		btnFaceView.src = faceViewEnabled;
 
-		scene.camera.y = scene.camera.y + e.movementY/10;
-		scene.transform.yr = scene.transform.yr + e.movementX*5;
+		scene.camera.y = scene.camera.y + e.movementY/7;
+		scene.models[0].transform.yr = scene.models[0].transform.yr + e.movementX*5;
 		App.Art.engine.render(scene, cvs);
 	};
 
@@ -111,14 +138,13 @@ App.Art.createWebglUI = function(container, slave, artSize, scene) {
 		btnResetView.src = resetViewEnabled;
 		btnFaceView.src = faceViewEnabled;
 
-		scene.camera.z = scene.camera.z - e.deltaY/7;
-
-		if (scene.camera.z < -900) {
-			scene.camera.z = -900;
-		}
-		if (scene.camera.z > -10) {
-			scene.camera.z = -10;
-		}
+		// zoom speed based on distance from origin, and along direction of camera
+		let zOld = scene.camera.z;
+		let magnitude = e.deltaY/(10/V.setZoomSpeed) * (-scene.camera.z/50 + 0.2);
+		let zDistance = Math.cos(-scene.camera.xr * (Math.PI/180)) * magnitude;
+		scene.camera.z -= zDistance;
+		scene.camera.z = Math.clamp(scene.camera.z, -900, -10);
+		scene.camera.y += Math.sin(-scene.camera.xr * (Math.PI/180)) * magnitude * -(scene.camera.z - zOld)/zDistance;
 
 		App.Art.engine.render(scene, cvs);
 		return false;
@@ -127,9 +153,11 @@ App.Art.createWebglUI = function(container, slave, artSize, scene) {
 	container.appendChild(cvs);
 	uicontainer.appendChild(btnLockView);
 	uicontainer.appendChild(btnFaceView);
+	uicontainer.appendChild(btnInspectView);
 	uicontainer.appendChild(btnResetView);
 	container.appendChild(uicontainer);
 
+	// calculate canvas resolution
 	if (artSize) {
 		let sz;
 		switch (artSize) {
@@ -161,3 +189,46 @@ App.Art.createWebglUI = function(container, slave, artSize, scene) {
 
 	return cvs;
 };
+
+App.Art.Frame = function(slave, scene) {
+	if (slave.height > 185) {
+		App.Art.AutoFrame(scene, slave.height, 127);
+	} else {
+		App.Art.FixedFrame(scene);
+	}
+};
+
+App.Art.AutoFrame = function(scene, slaveHeight, cameraHeight) {
+	// auto-frame based on camera height and FoV
+	let n = Math.max(slaveHeight * 1.05 - cameraHeight, 1);
+	let m = cameraHeight * 1.065;
+	let fov = scene.camera.fov;
+
+	let a = fov * (Math.PI/180);
+	let r = m/n;
+	let h = 0;
+
+	// solve for distance
+	if (a !== Math.PI/2) {
+		if (a > Math.PI/2) {
+			h = n/((-(r + 1) - ((r+1)**2 + 4*r*Math.tan(a)**2)**(1/2))/(2*Math.tan(a)*r)); // take negative discriminant
+		} else {
+			h = n/((-(r + 1) +  ((r+1)**2 + 4*r*Math.tan(a)**2)**(1/2))/(2*Math.tan(a)*r)); // take positive discriminant
+		}
+	} else {
+		h = (m+n)/2 * Math.sin(Math.acos(((m+n)/2-n)/((m+n)/2))); // edge case
+	}
+
+	// solve for rotation
+	let rot = fov/2 - Math.atan(n/h) * (180/Math.PI);
+
+	scene.camera.z = -h;
+	scene.camera.y = cameraHeight;
+	scene.camera.xr = -rot;
+};
+
+App.Art.FixedFrame = function(scene) {
+	scene.camera.z = -275;
+	scene.camera.y = 127;
+	scene.camera.xr = -6;
+};
diff --git a/src/gui/options/options.js b/src/gui/options/options.js
index 2f5502192990cd58e5caea0ea4f0e91ea129cc43..29d254fe68cde67490a4808b86fcd257a097148c 100644
--- a/src/gui/options/options.js
+++ b/src/gui/options/options.js
@@ -1111,13 +1111,15 @@ App.UI.artOptions = function() {
 			options.addOption("Clothing erection bulges are", "showClothingErection")
 				.addValue("Enabled", true).on().addValue("Disabled", false).off();
 		} else if (V.imageChoice === 4) {
-			options.addComment("You need to" +
-				" <a href='https://mega.nz/folder/ulIX2CAR#_g6wAcOLSCwIeGqrH7oXkA' target='_blank'>download the WebGL art assets</a>" +
-				" and put the 'webgl' folder into the resources/ folder where this html file is. Then refresh the page.");
+			options.addComment(`<a href='https://mega.nz/folder/P45nRALC#JkdALlE_w_cHDitz4Xhjeg' target='_blank'> Download the WebGL art assets</a> and place the 'webgl' folder into the resources/ folder where this HTML file is. 
+			Then <b>refresh</b> the page.
+			Create the resources folder if it does not exist. <span class="warning">(Android/MacOS not supported)</span>`);
 
 			options.addOption("Supersampling (SSAA)", "setSuperSampling")
 				.addValue("0.25", 0.25).off().addValue("0.5", 0.5).off().addValue("1", 1).off().addValue("2", 2).on().addValue("4", 4).off()
-				.addComment("This effectively multiplies the resolution of the render before downsampling again. Use a smaller factor for low-end GPUs.");
+				.addComment("This effectively multiplies the resolution of the render before downsampling again. Use a smaller factor for low-end GPU's.");
+			options.addOption("Zoom speed", "setZoomSpeed")
+				.addValue("0.25", 0.25).off().addValue("0.5", 0.5).off().addValue("1", 1).on().addValue("2", 2).off().addValue("4", 4).off();
 		}
 
 		options.addOption("PA avatar art is", "seeAvatar")