diff --git a/src/art/artJS.js b/src/art/artJS.js
index 5e4b1fc326a96f5649212c1d4df808d490122976..da5a1fe82d2422344da962f3c80f55f5fe40462c 100644
--- a/src/art/artJS.js
+++ b/src/art/artJS.js
@@ -21,7 +21,9 @@ globalThis.SlaveArt = function(artSlave, artSize, UIDisplay) {
 		return LegacyVectorArt(artSlave, UIDisplay);
 	} else if (imageChoice === 3) { /* VECTOR ART REVAMP*/
 		return RevampedVectorArt(artSlave);
-	} else { /* RENDERED IMAGES BY SHOKUSHU */
+	} else if (imageChoice === 0) { /* RENDERED IMAGES BY SHOKUSHU */
+		return ArtControlRendered(artSlave, artSize);
+	} else if (imageChoice === 4) { /* Elohiem's Webgl */
 		return ArtControlRendered(artSlave, artSize);
 	}
 };
@@ -141,8 +143,10 @@ App.Art.SlaveArtElement = function(artSlave, artSize, UIDisplay) {
 		return App.Art.legacyVectorArtElement(artSlave, UIDisplay);
 	} else if (imageChoice === 3) { /* VECTOR ART REVAMP*/
 		return App.Art.revampedVectorArtElement(artSlave);
-	} else { /* RENDERED IMAGES BY SHOKUSHU */
+	} else if (imageChoice === 0) { /* RENDERED IMAGES BY SHOKUSHU */
 		return App.Art.renderedArtElement(artSlave, artSize);
+	} else if (imageChoice === 4) { /* Elohiem's Webgl */
+		return App.Art.renderedArtElement2(artSlave, artSize);
 	}
 };
 
@@ -194,6 +198,113 @@ globalThis.CustomArt = function(slave, imageSize) {
 	return App.Art.elementToMarkup(App.Art.customArtElement(slave, imageSize));
 };
 
+App.Art.doneOnce = false;
+App.Art.engine = "";
+App.Art.defaultSceneParams = "";
+App.Art.isDraggingCanvas = false;
+
+/**
+ * @param {App.Entity.SlaveState} slave
+ * @param {number} artSize
+ * @returns {HTMLElement}
+ */
+App.Art.renderedArtElement2 = function(slave, artSize) {
+
+	if (App.Art.doneOnce == false) {
+		let sceneData = App.Art.sceneGetData();
+		let sceneParams = App.Art.sceneGetParams();
+		App.Art.engine = new App.Art.Engine();
+		App.Art.engine.bind(sceneData);
+		App.Art.defaultSceneParams = JSON.parse(JSON.stringify(sceneParams));
+		App.Art.doneOnce = true;
+	}
+
+	if (slave.sceneParams == "") {
+		slave.sceneParams = JSON.parse(JSON.stringify(App.Art.defaultSceneParams));
+	}
+
+	let cvs = document.createElement("canvas");
+	cvs.setAttribute("style", "z-index: 2;");
+
+	cvs.onmousemove= function(e){
+		if(!App.Art.isDraggingCanvas){return;}
+		e.preventDefault();
+		e.stopPropagation();
+
+		slave.sceneParams.transform.y = slave.sceneParams.transform.y - e.movementY/10;
+		slave.sceneParams.transform.yr = slave.sceneParams.transform.yr + e.movementX*5;
+		App.Art.engine.render(slave.sceneParams, cvs);
+	}
+
+	cvs.onmousedown= function(e){
+		e.preventDefault();
+		e.stopPropagation();
+		App.Art.isDraggingCanvas=true;
+	}
+	
+	cvs.onmouseup= function(e){
+		if(!App.Art.isDraggingCanvas){return;}
+		e.preventDefault();
+		e.stopPropagation();
+		App.Art.isDraggingCanvas=false;
+	}
+
+	cvs.onmouseout = function(e){
+		if(!App.Art.isDraggingCanvas){return;}
+		e.preventDefault();
+		e.stopPropagation();
+		App.Art.isDraggingCanvas=false;
+	}
+
+	cvs.onmousewheel = function(e){
+		slave.sceneParams.camera.fov = slave.sceneParams.camera.fov + e.deltaY/10;
+
+		if (slave.sceneParams.camera.fov < 1)
+			slave.sceneParams.camera.fov = 1;
+		if (slave.sceneParams.camera.fov > 179)
+			slave.sceneParams.camera.fov = 179;
+
+
+		App.Art.engine.render(slave.sceneParams, cvs);
+		return false;
+	}
+
+	if (artSize) {
+		let sz;
+		switch (artSize) {
+			case 3: 
+				sz = [300, 530]; 
+				break;
+			case 2:	
+				sz = [300, 530]; 
+				break;
+			case 1:	
+				sz = [150, 150]; 
+				break;
+			default: 
+				sz = [120, 120]; 
+				break;
+		}
+		cvs.width = sz[0];
+		cvs.height = sz[1];
+	}
+
+	slave.sceneParams.transform.y = App.Art.defaultSceneParams.transform.y;
+	slave.sceneParams.transform.yr = App.Art.defaultSceneParams.transform.yr;
+	slave.sceneParams.camera.fov = App.Art.defaultSceneParams.camera.fov;
+	slave.sceneParams.settings.rwidth = cvs.width*2;
+	slave.sceneParams.settings.rheight = cvs.height*2;
+
+	App.Art.applySurfaces(slave);
+	App.Art.applyMaterials(slave);
+	App.Art.applyMorphs(slave);
+
+	App.Art.engine.render(slave.sceneParams, cvs);
+
+	console.log(slave.sceneParams)
+	return cvs;
+};
+
 /**
  * @param {App.Entity.SlaveState} slave
  * @param {number} artSize
@@ -357,7 +468,32 @@ globalThis.extractColor = (function() {
 		["strawberry-blonde", "#e5a88c"],
 		/* these are not actually FreeCities canon, but like to appear in custom descriptions */
 		["brunette", "#6d4936"],
-		["dark", "#463325"]
+		["dark", "#463325"],
+
+		["aliceblue","#f0f8ff"],["antiquewhite","#faebd7"],["aqua","#00ffff"],["aquamarine","#7fffd4"],["azure","#f0ffff"],
+		["beige","#f5f5dc"],["bisque","#ffe4c4"],["black","#000000"],["blanchedalmond","#ffebcd"],["blue","#0000ff"],["blueviolet","#8a2be2"],["brown","#a52a2a"],["burlywood","#deb887"],
+		["cadetblue","#5f9ea0"],["chartreuse","#7fff00"],["chocolate","#d2691e"],["coral","#ff7f50"],["cornflowerblue","#6495ed"],["cornsilk","#fff8dc"],["crimson","#dc143c"],["cyan","#00ffff"],
+		["darkblue","#00008b"],["darkcyan","#008b8b"],["darkgoldenrod","#b8860b"],["darkgray","#a9a9a9"],["darkgreen","#006400"],["darkkhaki","#bdb76b"],["darkmagenta","#8b008b"],["darkolivegreen","#556b2f"],
+		["darkorange","#ff8c00"],["darkorchid","#9932cc"],["darkred","#8b0000"],["darksalmon","#e9967a"],["darkseagreen","#8fbc8f"],["darkslateblue","#483d8b"],["darkslategray","#2f4f4f"],["darkturquoise","#00ced1"],
+		["darkviolet","#9400d3"],["deeppink","#ff1493"],["deepskyblue","#00bfff"],["dimgray","#696969"],["dodgerblue","#1e90ff"],
+		["firebrick","#b22222"],["floralwhite","#fffaf0"],["forestgreen","#228b22"],["fuchsia","#ff00ff"],
+		["gainsboro","#dcdcdc"],["ghostwhite","#f8f8ff"],["gold","#ffd700"],["goldenrod","#daa520"],["gray","#808080"],["green","#008000"],["greenyellow","#adff2f"],
+		["honeydew","#f0fff0"],["hotpink","#ff69b4"],
+		["indianred ","#cd5c5c"],["indigo","#4b0082"],["ivory","#fffff0"],["khaki","#f0e68c"],
+		["lavender","#e6e6fa"],["lavenderblush","#fff0f5"],["lawngreen","#7cfc00"],["lemonchiffon","#fffacd"],["lightblue","#add8e6"],["lightcoral","#f08080"],["lightcyan","#e0ffff"],["lightgoldenrodyellow","#fafad2"],
+		["lightgrey","#d3d3d3"],["lightgreen","#90ee90"],["lightpink","#ffb6c1"],["lightsalmon","#ffa07a"],["lightseagreen","#20b2aa"],["lightskyblue","#87cefa"],["lightslategray","#778899"],["lightsteelblue","#b0c4de"],
+		["lightyellow","#ffffe0"],["lime","#00ff00"],["limegreen","#32cd32"],["linen","#faf0e6"],
+		["magenta","#ff00ff"],["maroon","#800000"],["mediumaquamarine","#66cdaa"],["mediumblue","#0000cd"],["mediumorchid","#ba55d3"],["mediumpurple","#9370d8"],["mediumseagreen","#3cb371"],["mediumslateblue","#7b68ee"],
+		["mediumspringgreen","#00fa9a"],["mediumturquoise","#48d1cc"],["mediumvioletred","#c71585"],["midnightblue","#191970"],["mintcream","#f5fffa"],["mistyrose","#ffe4e1"],["moccasin","#ffe4b5"],
+		["navajowhite","#ffdead"],["navy","#000080"],
+		["oldlace","#fdf5e6"],["olive","#808000"],["olivedrab","#6b8e23"],["orange","#ffa500"],["orangered","#ff4500"],["orchid","#da70d6"],
+		["palegoldenrod","#eee8aa"],["palegreen","#98fb98"],["paleturquoise","#afeeee"],["palevioletred","#d87093"],["papayawhip","#ffefd5"],["peachpuff","#ffdab9"],["peru","#cd853f"],["pink","#ffc0cb"],["plum","#dda0dd"],["powderblue","#b0e0e6"],["purple","#800080"],
+		["rebeccapurple","#663399"],["red","#ff0000"],["rosybrown","#bc8f8f"],["royalblue","#4169e1"],
+		["saddlebrown","#8b4513"],["salmon","#fa8072"],["sandybrown","#f4a460"],["seagreen","#2e8b57"],["seashell","#fff5ee"],["sienna","#a0522d"],["silver","#c0c0c0"],["skyblue","#87ceeb"],["slateblue","#6a5acd"],["slategray","#708090"],["snow","#fffafa"],["springgreen","#00ff7f"],["steelblue","#4682b4"],
+		["tan","#d2b48c"],["teal","#008080"],["thistle","#d8bfd8"],["tomato","#ff6347"],["turquoise","#40e0d0"],
+		["violet","#ee82ee"],
+		["wheat","#f5deb3"],["white","#ffffff"],["whitesmoke","#f5f5f5"],
+		["yellow","#ffff00"],["yellowgreen","#9acd32"]
 	]);
 
 	/* these are HTML color names supported by most browsers */
@@ -529,6 +665,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#C39696";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#E1B585";
 						colorSlave.areolaColor = "#C39696";
@@ -646,6 +784,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#7C594B";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#AC7C4A";
 						colorSlave.areolaColor = "#7C594B";
@@ -767,6 +907,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#92684C";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#B27554";
 						colorSlave.areolaColor = "#92684C";
@@ -888,6 +1030,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#AC8074";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#CFB48D";
 						colorSlave.areolaColor = "#AC8074";
@@ -1005,6 +1149,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#A7624F";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#CC8D53";
 						colorSlave.areolaColor = "#A7624F";
@@ -1122,6 +1268,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#AC8074";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#CFB48D";
 						colorSlave.areolaColor = "#AC8074";
@@ -1239,6 +1387,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#BF7577";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#DCA972";
 						colorSlave.areolaColor = "#BF7577";
@@ -1356,6 +1506,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#A7624F";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#CC8D53";
 						colorSlave.areolaColor = "#A7624F";
@@ -1473,6 +1625,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#976051";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#BA855E";
 						colorSlave.areolaColor = "#976051";
@@ -1590,6 +1744,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#C36E45";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#C17848";
 						colorSlave.areolaColor = "#C36E45";
@@ -1707,6 +1863,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#976051";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#BA855E";
 						colorSlave.areolaColor = "#976051";
@@ -1824,6 +1982,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#92684C";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#B27554";
 						colorSlave.areolaColor = "#92684C";
@@ -1945,6 +2105,8 @@ globalThis.skinColorCatcher = function(artSlave) {
 						colorSlave.areolaColor = "#92684C";
 						colorSlave.labiaColor = "#F977A3";
 						break;
+					case "sun tanned":
+					case "spray tanned":
 					case "tan":
 						colorSlave.skinColor = "#B27554";
 						colorSlave.areolaColor = "#92684C";
diff --git a/src/art/rendered/engine.js b/src/art/rendered/engine.js
new file mode 100644
index 0000000000000000000000000000000000000000..92e53e14a3959b33cb0139f8021f12ae89db6be4
--- /dev/null
+++ b/src/art/rendered/engine.js
@@ -0,0 +1,662 @@
+'use strict';
+
+App.Art.Engine = class {
+    constructor() {
+        this.vsSourceBg = `#version 300 es
+                    precision highp float;
+
+                    in vec2 vertexPosition;
+                    out vec2 vertexPos;
+            
+                    void main() {
+                        vertexPos = vertexPosition;
+                        gl_Position = vec4(vertexPosition, 0.0, 1.0);
+                    }`;
+
+        this.fsSourceBg = `#version 300 es
+                    precision highp float;
+                    precision highp sampler2D;
+
+                    uniform sampler2D textSampler;
+                    uniform vec4 backgroundColor;
+
+                    in vec2 vertexPos;
+                    out vec4 outputColor;
+
+                    void main() {
+                        vec2 textureCoord = vec2(vertexPos.s, -vertexPos.t) * 0.5 + 0.5;
+                        vec3 c = backgroundColor.rgb * texture(textSampler, textureCoord.st).rgb;
+                        outputColor  = vec4(c.rgb * backgroundColor.a, backgroundColor.a);
+                    }`;
+
+        this.vsSource = `#version 300 es
+                    precision highp float;
+
+                    uniform mat4 matView;
+                    uniform mat4 matProj;
+                    uniform mat4 matRot;
+                    uniform mat4 matTrans;
+                    uniform mat4 matScale;
+
+                    in vec3 vertexNormal;
+                    in vec3 vertexPosition;
+                    in vec2 textureCoordinate;
+                    in vec3 vertexTangent;
+
+                    in vec3 vertexNormalMorph;
+                    in vec3 vertexPositionMorph;
+
+                    out vec2 textureCoord;
+                    out vec3 normal;
+                    out mat3 TBN;
+
+                    void main() {
+                        gl_Position = matProj * matView * matTrans * matScale * matRot * vec4(vertexPosition + vertexPositionMorph, 1.0) + 0.01;
+                        normal = normalize((matRot * vec4(vertexNormal + vertexNormalMorph, 1.0)).xyz);
+
+                        vec3 T = normalize(vec3(matTrans * matRot * vec4(vertexTangent, 0.0)));
+                        vec3 N = normalize(vec3(matTrans * matRot * vec4(vertexNormal + vertexNormalMorph, 0.0)));
+                        T = normalize(T - dot(T, N) * N);
+                        vec3 B = cross(N, T);
+                        TBN = mat3(T, B, N);
+
+                        textureCoord = textureCoordinate;
+                    }`;
+
+        this.fsSource = `#version 300 es
+                    precision highp float;
+                    precision highp sampler2D;
+                    
+                    uniform float lightInt;
+                    uniform float lightAmb;
+                    uniform vec3 lightColor;
+                    uniform float whiteM;
+                    uniform float gammaY;
+
+                    uniform vec3 Ka;
+                    uniform vec3 Kd;
+                    uniform vec3 Ks;
+                    uniform float d;
+                    uniform float Ns;
+
+                    uniform float sNormals;
+                    uniform float sAmbient;
+                    uniform float sDiffuse;
+                    uniform float sSpecular;
+                    uniform float sAlpha;
+                    uniform float sGamma;
+                    uniform float sReinhard;
+                    uniform float sNormal;
+                    
+                    uniform vec3 lightVect;
+                    uniform vec3 lookDir;
+
+                    uniform sampler2D textSampler[6];
+
+                    in vec2 textureCoord;
+                    in vec3 normal;
+                    in mat3 TBN;
+
+                    out vec4 outputColor;
+
+                    void main() {
+                        vec3 new_normal = normal;
+                        vec3 map_Ka = vec3(0.0,0.0,0.0);
+                        vec3 map_Kd = vec3(0.0,0.0,0.0);
+                        vec3 map_Ks = vec3(0.0,0.0,0.0);
+                        float map_Ns = 0.0;
+                        float map_d = 1.0;
+                        float specular = 1.0;
+
+                        if (sNormal == 1.0) {
+                            vec3 map_Kn = texture(textSampler[5], textureCoord.st).rgb *2.0-1.0;
+                            if (map_Kn != vec3(-1.0,-1.0,-1.0))
+                                new_normal = normalize(TBN * map_Kn);
+                        }
+
+                        float angle = max(dot(-lightVect, new_normal),0.0);
+                        vec3 reflection = reflect(-lightVect, new_normal);
+
+                        if (sAmbient == 1.0)
+                            map_Ka = Ka * texture(textSampler[0], textureCoord.st).rgb;
+
+                        if (sDiffuse == 1.0)
+                            map_Kd = Kd * texture(textSampler[1], textureCoord.st).rgb;
+
+                        if (sSpecular == 1.0) {
+                            map_Ks = Ks * texture(textSampler[2], textureCoord.st).rgb;
+                            map_Ns = Ns * texture(textSampler[3], textureCoord.st).r;
+                            specular = pow(max(dot(reflection, lookDir),0.0), (0.0001+map_Ns));
+                        }
+
+                        if (sAlpha == 1.0)
+                            map_d = d * texture(textSampler[4], textureCoord.st).r;
+
+                        vec3 Ld = map_Kd * lightInt * angle * lightColor;
+                        vec3 Ls = map_Ks * specular * lightColor;
+                        vec3 La = map_Ka * lightAmb * lightColor;
+
+                        vec3 vLighting = Ld + Ls + La;
+                        vec3 c = map_Kd * vLighting;
+
+                        if (sReinhard == 1.0) {
+                            float l_old = 0.2126*c.r+0.7152*c.g+0.0722*c.b;
+                            float numerator = l_old * (1.0 + (l_old / (whiteM*whiteM)));
+                            float l_new = numerator / (1.0 + l_old);
+                            c = c * (l_new / l_old);
+                        }
+
+                        if (sGamma == 1.0) {
+                            c.r = pow(c.r, (1.0/gammaY));
+                            c.g = pow(c.g, (1.0/gammaY));
+                            c.b = pow(c.b, (1.0/gammaY));
+                        }
+
+                        if (sNormals == 1.0) {
+                            c = new_normal;
+                        }
+
+                        outputColor = vec4(c*map_d, map_d);
+                    }`;
+    }
+
+    initBuffers = function() {
+        this.backgroundPositionBuffer = this.gl.createBuffer();
+        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.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.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, new Float32Array(this.base64ToFloat(this.sceneData.model.verts)), this.gl.STATIC_DRAW);
+
+        this.verticesNormalBuffer = this.gl.createBuffer();
+        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesNormalBuffer);
+        this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(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, new Float32Array(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, new Float32Array(this.base64ToFloat(this.sceneData.model.tans)), this.gl.STATIC_DRAW);
+
+        this.vertexPositionMorphs = [];
+        this.vertexNormalMorphs = [];
+        for(let i=0; i < this.sceneData.model.mverts.length; i++) {
+            this.vertexPositionMorphs[i] = new Float32Array(this.base64ToFloat(this.sceneData.model.mverts[i]));
+            this.vertexNormalMorphs[i] = new Float32Array(this.base64ToFloat(this.sceneData.model.mvertsn[i]));
+        }
+
+        this.verticesMorphBuffer = this.gl.createBuffer();
+        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesMorphBuffer);
+        this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(this.sceneData.model.verts.length), 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(this.sceneData.model.vertsn.length), 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, new Uint32Array(intArray), this.gl.STATIC_DRAW);
+                this.indexSizes[count] = intArray.length;
+            }
+        }
+    }
+
+    loadTexture = function(gl, url) {
+        // return dummy texture
+        let texture = gl.createTexture();
+        gl.bindTexture(gl.TEXTURE_2D, texture);
+        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255, 255]));
+      
+        // stream real textures
+        let image = new Image();
+        image.onload = function() {
+          gl.bindTexture(gl.TEXTURE_2D, texture);
+          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
+          gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
+          gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
+        };
+        image.src = url;
+
+        return texture;
+    }
+
+    initTextures = function() {
+        // load model textures
+        this.modelTextures = [];
+        for (let i=0; i < this.sceneData.textures.length; i++) {
+            this.modelTextures[i] = this.loadTexture(this.gl, this.sceneData.textures[i]);
+        }
+    }
+
+    initShaders = function() {
+        // compile shaders
+        let vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
+        this.gl.shaderSource(vertexShader, this.vsSource);
+        this.gl.compileShader(vertexShader);
+
+        let fragmentShader = this.gl.createShader(this.gl.FRAGMENT_SHADER);
+        this.gl.shaderSource(fragmentShader, this.fsSource);
+        this.gl.compileShader(fragmentShader);
+
+        this.shaderProgram = this.gl.createProgram();
+        this.gl.attachShader(this.shaderProgram, vertexShader);
+        this.gl.attachShader(this.shaderProgram, fragmentShader);
+        this.gl.linkProgram(this.shaderProgram);
+        //console.log(this.gl.getProgramInfoLog(this.shaderProgram));
+
+        let vertexShaderBg = this.gl.createShader(this.gl.VERTEX_SHADER);
+        this.gl.shaderSource(vertexShaderBg, this.vsSourceBg);
+        this.gl.compileShader(vertexShaderBg);
+
+        let fragmentShaderBg = this.gl.createShader(this.gl.FRAGMENT_SHADER);
+        this.gl.shaderSource(fragmentShaderBg, this.fsSourceBg);
+        this.gl.compileShader(fragmentShaderBg);
+
+        this.shaderProgramBg = this.gl.createProgram();
+        this.gl.attachShader(this.shaderProgramBg, vertexShaderBg);
+        this.gl.attachShader(this.shaderProgramBg, fragmentShaderBg);
+        this.gl.linkProgram(this.shaderProgramBg);
+
+        this.gl.useProgram(this.shaderProgram);
+
+        // enable vertex attributes
+        this.backgroundPositionAttribute = this.gl.getAttribLocation(this.shaderProgramBg, "vertexPosition");
+        this.gl.enableVertexAttribArray(this.backgroundPositionAttribute);
+
+        this.vertexPositionAttribute = this.gl.getAttribLocation(this.shaderProgram, "vertexPosition");
+        this.gl.enableVertexAttribArray(this.vertexPositionAttribute);
+
+        this.textureCoordAttribute = this.gl.getAttribLocation(this.shaderProgram, "textureCoordinate");
+        this.gl.enableVertexAttribArray(this.textureCoordAttribute);
+
+        this.vertexNormalAttribute = this.gl.getAttribLocation(this.shaderProgram, "vertexNormal");
+        this.gl.enableVertexAttribArray(this.vertexNormalAttribute);
+
+        this.vertexTangentAttribute = this.gl.getAttribLocation(this.shaderProgram, "vertexTangent");
+        this.gl.enableVertexAttribArray(this.vertexTangentAttribute);
+
+        this.vertexNormalMorphAttribute = this.gl.getAttribLocation(this.shaderProgram, "vertexNormalMorph");
+        this.gl.enableVertexAttribArray(this.vertexNormalMorphAttribute);
+
+        this.vertexPositionMorphAttribute = this.gl.getAttribLocation(this.shaderProgram, "vertexPositionMorph");
+        this.gl.enableVertexAttribArray(this.vertexPositionMorphAttribute);
+    }
+
+    bind = function(sceneData) {
+        this.sceneData = sceneData;
+       
+        this.offscreenCanvas = document.createElement("canvas");
+        this.gl = this.offscreenCanvas.getContext("webgl2", {alpha:true, premultipliedAlpha: true});
+
+        this.gl.enable(this.gl.CULL_FACE);
+        this.gl.cullFace(this.gl.BACK);
+        this.gl.enable(this.gl.DEPTH_TEST);
+        this.gl.depthFunc(this.gl.LEQUAL);
+        this.gl.enable(this.gl.BLEND);
+        this.gl.blendEquation( this.gl.FUNC_ADD );
+        this.gl.blendFunc(this.gl.ONE, this.gl.ONE_MINUS_SRC_ALPHA);
+
+        this.initBuffers();
+        this.initTextures();
+        this.initShaders();
+    }
+
+    render = function(sceneParams, canvas) {
+        // set render resolution
+        this.offscreenCanvas.width = sceneParams.settings.rwidth;
+        this.offscreenCanvas.height = sceneParams.settings.rheight;
+        this.gl.viewport(0, 0, this.gl.drawingBufferWidth, this.gl.drawingBufferHeight);
+
+        // draw background
+        this.gl.clearColor(0, 0, 0, 0);
+        this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
+        this.gl.useProgram(this.shaderProgramBg);
+        if (sceneParams.background.visible)
+            this.drawBackground(sceneParams);
+
+        // draw model
+        this.gl.clear(this.gl.DEPTH_BUFFER_BIT);
+        this.gl.useProgram(this.shaderProgram);
+        this.drawModel(sceneParams);
+
+        // clone from offscreen to real canvas
+        let ctx = canvas.getContext('2d', {alpha:true});
+        ctx.clearRect(0, 0, canvas.width, canvas.height);
+        ctx.drawImage(this.gl.canvas, 0, 0, canvas.width, canvas.height);
+    }
+
+    drawBackground = function(sceneParams) {
+        this.gl.uniform1i(this.gl.getUniformLocation(this.shaderProgramBg, "textSampler"), 0);
+        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.bindBuffer(this.gl.ARRAY_BUFFER, this.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.drawElements(this.gl.TRIANGLES, 6, this.gl.UNSIGNED_SHORT, 0);
+    }
+
+    drawModel = function(sceneParams) {
+        // create camera
+        let camRotX = this.degreeToRad(-sceneParams.camera.xr);
+        let camRotY = this.degreeToRad(-sceneParams.camera.yr);
+        let camRotZ = this.degreeToRad(sceneParams.camera.zr);
+
+        let up = [Math.sin(camRotZ), Math.cos(camRotZ), Math.sin(camRotZ)];
+        let camera = [sceneParams.camera.x, sceneParams.camera.y, sceneParams.camera.z];
+
+        let matCameraRot = this.matrixMulMatrix(this.matrixMakeRotationX(camRotX), this.matrixMakeRotationY(camRotY));
+        let lookDir = this.matrixMulVector(matCameraRot, [0, 0, 1]);
+        let target = this.vectorAdd(lookDir, camera);
+        let matCamera = this.matrixPointAt(camera, target, up);
+
+        // create transforms
+        this.applyMorphs(sceneParams);
+        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);
+
+        let lightVect = this.polarToCart(this.degreeToRad(sceneParams.light.yr), this.degreeToRad(sceneParams.light.xr));
+        let lightAmb = sceneParams.light.ambient;
+        let lightInt = sceneParams.light.intensity;
+        let lightColor = sceneParams.light.color;
+        let whiteM = sceneParams.settings.whiteM;
+        let gammaY = sceneParams.settings.gammaY;
+
+        // set 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);
+        this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "sSpecular"), sceneParams.settings.specular);
+        this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "sNormal"), sceneParams.settings.normal);
+        this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "sAlpha"), sceneParams.settings.alpha);
+        this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "sReinhard"), sceneParams.settings.reinhard);
+        this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "sGamma"), sceneParams.settings.gamma);
+        this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "whiteM"), whiteM);
+        this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "gammaY"), gammaY);
+        this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "lightAmb"), lightAmb);
+        this.gl.uniform1f(this.gl.getUniformLocation(this.shaderProgram, "lightInt"), lightInt);
+        this.gl.uniform3fv(this.gl.getUniformLocation(this.shaderProgram, "lightColor"), lightColor);
+        this.gl.uniform3fv(this.gl.getUniformLocation(this.shaderProgram, "lightVect"), lightVect);
+        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);
+
+        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);
+
+        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesTangentBuffer);
+        this.gl.vertexAttribPointer(this.vertexTangentAttribute, 3, this.gl.FLOAT, false, 0, 0);
+
+        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesMorphBuffer);
+        this.gl.vertexAttribPointer(this.vertexPositionMorphAttribute, 3, this.gl.FLOAT, false, 0, 0);
+
+        this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.verticesNormalMorphBuffer);
+        this.gl.vertexAttribPointer(this.vertexNormalMorphAttribute, 3, this.gl.FLOAT, false, 0, 0);
+
+        // 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)
+                    continue;
+
+                let visible = sceneParams.model.figures[i].surfaces[j].visible;
+                let matId = sceneParams.model.figures[i].surfaces[j].matId;
+                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);
+                }
+            }
+        }
+    }
+
+    applyMorphs = function(sceneParams) {
+        if(this.oldMorphValues != JSON.stringify(sceneParams.model.morphs)) {
+            let vertexPositionMorph = new Float32Array(this.sceneData.model.verts.length);
+            let vertexNormalMorph = new Float32Array(this.sceneData.model.vertsn.length);
+
+            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];
+
+                    if (morphValue == 1)
+                        for(let j=0; j < this.vertexPositionMorphs[i].length; j++) {
+                            vertexPositionMorph[j] += vp[j];
+                            vertexNormalMorph[j] += vn[j];
+                        }
+                    else
+                        for(let j=0; j < this.vertexPositionMorphs[i].length; j++) {
+                            vertexPositionMorph[j] += vp[j] * morphValue;
+                            vertexNormalMorph[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, this.verticesNormalMorphBuffer);
+            this.gl.bufferData(this.gl.ARRAY_BUFFER, vertexNormalMorph, this.gl.STATIC_DRAW);
+
+            this.oldMorphValues = JSON.stringify(sceneParams.model.morphs);
+        }
+    }
+
+    base64ToFloat = function(array) {
+        let b	= window.atob(array),
+        fLen	= b.length / Float32Array.BYTES_PER_ELEMENT,
+        dView	= new DataView(new ArrayBuffer(Float32Array.BYTES_PER_ELEMENT)),
+        fAry	= new Float32Array(fLen),
+        p		= 0;
+    
+        for(let j=0; j < fLen; j++){
+            p = j * 4;
+            dView.setUint8(0, b.charCodeAt(p));
+            dView.setUint8(1, b.charCodeAt(p+1));
+            dView.setUint8(2, b.charCodeAt(p+2));
+            dView.setUint8(3, b.charCodeAt(p+3));
+            fAry[j] = dView.getFloat32(0, true);
+        }
+        return fAry;
+    }
+
+    base64ToInt = function(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;
+    
+        for(let j=0; j < fLen; j++){
+            p = j * 4;
+            dView.setUint8(0, b.charCodeAt(p));
+            dView.setUint8(1, b.charCodeAt(p+1));
+            dView.setUint8(2, b.charCodeAt(p+2));
+            dView.setUint8(3, b.charCodeAt(p+3));
+            fAry[j] = dView.getInt32(0, true);
+        }
+        return fAry;
+    }
+
+    base64ToByte = function(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);
+
+        for(let j=0; j < fLen; j++){
+            dView.setUint8(0, b.charCodeAt(j));
+            fAry[j] = dView.getUint8(0);
+        }
+        return fAry;
+    }
+
+    degreeToRad = (d) => d * (Math.PI / 180)
+
+    polarToCart = (y, p) => ([Math.sin(p) * Math.cos(y), Math.sin(p) * Math.sin(y), Math.cos(p)])
+                        
+    vectorAdd = (v1, v2) => ([v1[0] + v2[0], v1[1] + v2[1], v1[2] + v2[2]])
+
+    vectorSub = (v1, v2) => ([v1[0] - v2[0], v1[1] - v2[1], v1[2] - v2[2]])
+
+    vectorMul = (v, k) => ([v[0] * k, v[1] * k, v[2] * k])
+
+    vectorDotProduct = (v1, v2) => ([v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]])
+
+    vectorCrossProduct = (v1, v2) => ([v1[1] * v2[2] - v1[2] * v2[1], v1[2] * v2[0] - v1[0] * v2[2], v1[0] * v2[1] - v1[1] * v2[0]])
+
+    vectorNormalize = function(v) {
+        let l = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
+        return [v[0]/l, v[1]/l, v[2]/l]
+    }
+
+    matrixMakeProjection = (fov, aspect, near, far) => ([[aspect * (1/Math.tan(fov*0.5/180*Math.PI)), 0, 0, 0],
+                                                        [0, (1/Math.tan(fov*0.5/180*Math.PI)), 0, 0],
+                                                        [0, 0, far/(far-near), 1],
+                                                        [0, 0, (-far*near)/(far-near), 0]])
+
+    matrixPointAt = function(pos, target, up){
+        let newForward = this.vectorNormalize(this.vectorSub(target, pos));
+        let a = this.vectorMul(newForward, this.vectorDotProduct(up, newForward));
+        let newUp = this.vectorNormalize(this.vectorSub(up, a));
+        let newRight = this.vectorCrossProduct(newUp, newForward);
+
+        return [[newRight[0], newRight[1], newRight[2], 0],
+                [newUp[0], newUp[1], newUp[2], 0],
+                [newForward[0], newForward[1], newForward[2], 0],
+                [pos[0], pos[1], pos[2], 1]]
+    }
+    
+    matrixMakeRotation = function(xr, yr, zr){
+        let cosA = Math.cos(xr);
+        let cosB = Math.cos(yr);
+        let cosC = Math.cos(zr);
+        let sinA = Math.sin(xr);
+        let sinB = Math.sin(yr);
+        let sinC = Math.sin(zr);
+
+        return([[cosB*cosC, -cosB*sinC, sinB, 0],
+                [sinA*sinB*cosC+cosA*sinC, -sinA*sinB*sinC+cosA*cosC, -sinA*cosB, 0],
+                [-cosA*sinB*cosC+sinA*sinC, cosA*sinB*sinC+sinA*cosC, cosA*cosB, 0],
+                [0, 0, 0, 1]])
+    }
+
+    matrixMakeRotationX = (r) => ([[1, 0, 0],
+                                  [0, Math.cos(r), Math.sin(r)],
+                                  [0, -Math.sin(r), Math.cos(r)]])
+
+    matrixMakeRotationY = (r) => ([[Math.cos(r), 0, Math.sin(r)],
+                                  [0, 1, 0],
+                                  [-Math.sin(r), 0, Math.cos(r)]])
+          
+    matrixMakeRotationZ = (r) => ([[Math.cos(r), Math.sin(r), 0],
+                                  [-Math.sin(r), Math.cos(r), 0],
+                                  [0, 0, 1]])
+
+    matrixMakeTranslation = (x, y, z) => ([[1, 0, 0, 0],
+                                          [0, 1, 0, 0],
+                                          [0, 0, 1, 0],
+                                          [x, y, z, 1]])
+
+    matrixMakeScaling = (s) => ([[s, 0, 0, 0],
+                                 [0, s, 0, 0],
+                                 [0, 0, s, 0],
+                                 [0, 0, 0, 1]])
+
+    matrixMulMatrix = (m1, m2) => ([[m1[0][0] * m2[0][0] + m1[0][1] * m2[1][0] + m1[0][2] * m2[2][0],
+                                    m1[0][0] * m2[0][1] + m1[0][1] * m2[1][1] + m1[0][2] * m2[2][1],
+                                    m1[0][0] * m2[0][2] + m1[0][1] * m2[1][2] + m1[0][2] * m2[2][2]],
+                                   [m1[1][0] * m2[0][0] + m1[1][1] * m2[1][0] + m1[1][2] * m2[2][0],
+                                    m1[1][0] * m2[0][1] + m1[1][1] * m2[1][1] + m1[1][2] * m2[2][1],
+                                    m1[1][0] * m2[0][2] + m1[1][1] * m2[1][2] + m1[1][2] * m2[2][2]],
+                                   [m1[2][0] * m2[0][0] + m1[2][1] * m2[1][0] + m1[2][2] * m2[2][0],
+                                    m1[2][0] * m2[0][1] + m1[2][1] * m2[1][1] + m1[2][2] * m2[2][1],
+                                    m1[2][0] * m2[0][2] + m1[2][1] * m2[1][2] + m1[2][2] * m2[2][2]]])
+
+    matrixMulVector = (m, v) => ([v[0] * m[0][0] + v[1] * m[1][0] + v[2] * m[2][0],
+                                 v[0] * m[0][1] + v[1] * m[1][1] + v[2] * m[2][1],
+                                 v[0] * m[0][2] + v[1] * m[1][2] + v[2] * m[2][2]])
+
+    matrixInverse = (m) => ([[m[0][0], m[1][0], m[2][0], 0],
+                            [m[0][1], m[1][1], m[2][1], 0],
+                            [m[0][2], m[1][2], m[2][2], 0],
+                            [-(m[3][0]*m[0][0]+m[3][1]*m[0][1]+m[3][2]*m[0][2]), -(m[3][0]*m[1][0]+m[3][1]*m[1][1]+m[3][2]*m[1][2]), -(m[3][0]*m[2][0]+m[3][1]*m[2][1]+m[3][2]*m[2][2]), 1]])
+
+    matrixFlatten = (m) => ([m[0][0],m[0][1],m[0][2],m[0][3],
+                            m[1][0],m[1][1],m[1][2],m[1][3],
+                            m[2][0],m[2][1],m[2][2],m[2][3],
+                            m[3][0],m[3][1],m[3][2],m[3][3]])
+}
diff --git a/src/art/rendered/renderedArt.js b/src/art/rendered/renderedArt.js
new file mode 100644
index 0000000000000000000000000000000000000000..830dd8db12d4dd355f15dd09ef1ac8352e3a04ac
--- /dev/null
+++ b/src/art/rendered/renderedArt.js
@@ -0,0 +1,510 @@
+App.Art.getMaterialById = function(sceneParams, id) {
+	for (let i =0; i < sceneParams.materials.length; i++)
+		if(sceneParams.materials[i].matId == id)
+			return sceneParams.materials[i]
+
+	return null;
+};
+
+App.Art.getMorphById = function(sceneParams, id) {
+	for (let i =0; i < sceneParams.model.morphs.length; i++)
+		if(sceneParams.model.morphs[i].morphId == id)
+			return sceneParams.model.morphs[i]
+
+	return null;
+};
+
+App.Art.getSurfaceById = function(sceneParams, id) {
+	for (let i=0, count=0; i < sceneParams.model.figures.length; i++)
+		for (let j=0; j < sceneParams.model.figures[i].surfaces.length; j++, count++)
+			if(sceneParams.model.figures[i].surfaces[j].surfaceId == id)
+				return sceneParams.model.figures[i].surfaces[j]
+	return null;
+};
+
+App.Art.resetMorphs = function(slave) {
+	for (let i =0; i < slave.sceneParams.model.morphs.length; i++)
+		slave.sceneParams.model.morphs[i].value = App.Art.defaultSceneParams.model.morphs[i].value;
+};
+
+App.Art.applySurfaces = function(slave) {
+	let surfaces = [];
+
+	if (slave.dick != 0 || (!(slave.scrotum <= 0 || slave.balls <= 0))) {
+		surfaces.push(["Futalicious_Genitalia_G8F_Shaft_Futalicious_Shell", "visible", true]);
+		surfaces.push(["Futalicious_Genitalia_G8F_Glans_Futalicious_Shell", "visible", true]);
+		surfaces.push(["Futalicious_Genitalia_G8F_Testicles_Futalicious_Shell", "visible", true]);
+		surfaces.push(["Futalicious_Genitalia_G8F_Torso_Front_Futalicious_Shell", "visible", true]);
+		surfaces.push(["Futalicious_Genitalia_G8F_Torso_Middle_Futalicious_Shell", "visible", true]);
+		surfaces.push(["Futalicious_Genitalia_G8F_Torso_Back_Futalicious_Shell", "visible", true]);
+		surfaces.push(["Futalicious_Genitalia_G8F_Rectum_Futalicious_Shell", "visible", false]);
+		surfaces.push(["Torso_Front", "visible", true]);
+		surfaces.push(["Torso_Middle", "visible", true]);
+		surfaces.push(["Torso_Back", "visible", true]);
+
+		surfaces.push(["Genitalia", "visible", true]);
+		surfaces.push(["Anus", "visible", true]);
+		surfaces.push(["new_gens_V8_1840_Genitalia", "visible", true]);
+		surfaces.push(["new_gens_V8_1840_Anus", "visible", true]);
+	}
+	else {
+		surfaces.push(["Futalicious_Genitalia_G8F_Shaft_Futalicious_Shell", "visible", false]);
+		surfaces.push(["Futalicious_Genitalia_G8F_Glans_Futalicious_Shell", "visible", false]);
+		surfaces.push(["Futalicious_Genitalia_G8F_Testicles_Futalicious_Shell", "visible", false]);
+		surfaces.push(["Futalicious_Genitalia_G8F_Torso_Front_Futalicious_Shell", "visible", false]);
+		surfaces.push(["Futalicious_Genitalia_G8F_Torso_Middle_Futalicious_Shell", "visible", false]);
+		surfaces.push(["Futalicious_Genitalia_G8F_Torso_Back_Futalicious_Shell", "visible", false]);
+		surfaces.push(["Futalicious_Genitalia_G8F_Rectum_Futalicious_Shell", "visible", false]);
+		surfaces.push(["Torso_Front", "visible", false]);
+		surfaces.push(["Torso_Middle", "visible", false]);
+		surfaces.push(["Torso_Back", "visible", false]);
+
+		surfaces.push(["Genitalia", "visible", true]);
+		surfaces.push(["Anus", "visible", true]);
+		surfaces.push(["new_gens_V8_1840_Genitalia", "visible", true]);
+		surfaces.push(["new_gens_V8_1840_Anus", "visible", true]);
+	}
+
+	surfaces.push(["Arms", "visible", hasBothArms(slave)]);
+	surfaces.push(["Fingernails", "visible", hasBothArms(slave)]);
+	surfaces.push(["Legs", "visible", hasBothLegs(slave)]);
+	surfaces.push(["Toenails", "visible", hasBothLegs(slave)]);
+
+    let cockSkin;
+    let skin;
+
+	switch (slave.skin) {
+		case "pure white":
+		case "ivory":
+		case "white":
+            cockSkin = "White";
+            skin = "Ceridwen";
+			break;
+		case "extremely pale":
+		case "very pale":
+            cockSkin = "White";
+            skin = "Celinette";
+			break;
+		case "pale":
+		case "extremely fair":
+            cockSkin = "White";
+            skin = "Kimmy";
+			break;
+		case "very fair":
+		case "fair":
+            cockSkin = "Light";
+            skin = "Saffron";
+			break;
+		case "light":
+		case "light olive":
+            cockSkin = "Light";
+            skin = "FemaleBase";
+			break;
+		case "sun tanned":
+		case "spray tanned":
+		case "tan":
+            cockSkin = "Light";
+            skin = "Reagan";
+			break;
+		case "olive":
+            cockSkin = "Mid";
+            skin = "Kathy";
+			break;
+		case "bronze":
+            cockSkin = "Mid";
+            skin = "Mylou";
+			break;
+		case "dark olive":
+            cockSkin = "Mid";
+            skin = "Adaline";
+			break;
+		case "dark":
+            cockSkin = "Mid";
+            skin = "Daphne";
+			break;
+		case "light beige":
+            cockSkin = "Mid";
+            skin = "Minami";
+			break;
+		case "beige":
+            cockSkin = "Mid";
+            skin = "Tara";
+			break;
+		case "dark beige":
+		case "light brown":
+            cockSkin = "Dark";
+            skin = "Topmodel";
+			break;
+		case "brown":
+		case "dark brown":
+            cockSkin = "Dark";
+            skin = "Angelica";
+			break;
+		case "black":
+		case "ebony":
+		case "pure black":
+            cockSkin = "Dark";
+            skin = "DarkSkin";
+			break;
+    }
+    
+    surfaces.push(["Futalicious_Genitalia_G8F_Glans_Futalicious_Shell", "matId", cockSkin + "Futalicious_Genitalia_G8F_Glans_Futalicious_Shell"]);
+    surfaces.push(["Futalicious_Genitalia_G8F_Shaft_Futalicious_Shell", "matId", cockSkin + "Futalicious_Genitalia_G8F_Glans_Futalicious_Shell"]);
+    surfaces.push(["Futalicious_Genitalia_G8F_Testicles_Futalicious_Shell", "matId", cockSkin + "Futalicious_Genitalia_G8F_Glans_Futalicious_Shell"]);
+    surfaces.push(["Futalicious_Genitalia_G8F_Torso_Front_Futalicious_Shell", "matId", cockSkin + "Futalicious_Genitalia_G8F_Glans_Futalicious_Shell"]);
+    surfaces.push(["Futalicious_Genitalia_G8F_Torso_Middle_Futalicious_Shell", "matId", cockSkin + "Futalicious_Genitalia_G8F_Glans_Futalicious_Shell"]);
+    surfaces.push(["Futalicious_Genitalia_G8F_Torso_Back_Futalicious_Shell", "matId", cockSkin + "Futalicious_Genitalia_G8F_Glans_Futalicious_Shell"]);
+    surfaces.push(["Futalicious_Genitalia_G8F_Rectum_Futalicious_Shell", "matId", cockSkin + "Futalicious_Genitalia_G8F_Glans_Futalicious_Shell"]);
+    surfaces.push(["Torso_Front", "matId", skin + "Torso"]);
+    surfaces.push(["Torso_Middle", "matId",	skin + "Torso"]);
+    surfaces.push(["Torso_Back", "matId", skin + "Torso"]);
+
+    surfaces.push(["Torso", "matId", skin + "Torso"]);
+    surfaces.push(["Face", "matId", skin + "Face"]);
+    surfaces.push(["Lips", "matId", skin + "Lips"]);
+    surfaces.push(["Ears", "matId", skin + "Ears"]);
+    surfaces.push(["Legs", "matId", skin + "Legs"]);
+    surfaces.push(["Arms", "matId", skin + "Arms"]);
+    surfaces.push(["EyeSocket", "matId", skin + "Face"]);
+    surfaces.push(["Toenails", "matId", skin + "Toenails"]);
+    surfaces.push(["Fingernails", "matId", skin + "Fingernails"]);
+    surfaces.push(["Genitalia",	"matId", skin + "Genitalia"]);
+    surfaces.push(["Anus", "matId", skin + "Anus"]);
+
+	let sceneParams = slave.sceneParams;
+	for (let i=0, count=0; i < sceneParams.model.figures.length; i++)
+		for (let j=0; j < sceneParams.model.figures[i].surfaces.length; j++, count++)
+			for (let h =0; h < surfaces.length; h++)
+				if (sceneParams.model.figures[i].surfaces[j].surfaceId == surfaces[h][0])
+					sceneParams.model.figures[i].surfaces[j][surfaces[h][1]] = surfaces[h][2];
+};
+
+App.Art.applyMaterials = function(slave) {
+	let materials = [];
+
+	function hexToRgb(hex) {
+		hex = hex.replace('#','');
+		let r = parseInt(hex.substring(0,2), 16);
+		let g = parseInt(hex.substring(2,4), 16);
+		let b = parseInt(hex.substring(4,6), 16);
+		return [r/255,g/255,b/255];
+	}
+
+	let hairColor = hexToRgb(extractColor(slave.hColor));
+	let lipsColor = hexToRgb(skinColorCatcher(slave).lipsColor);
+
+	let makeupColor;
+	let makeupOpacity;
+
+	switch (slave.makeup) {
+		case 1:
+			// Nice
+			makeupColor = "#ff69b4";
+			makeupOpacity = 0.5;
+			break;
+		case 2:
+			// Gorgeous
+			makeupColor = "#8b008b";
+			makeupOpacity = 0.7;
+			break;
+		case 3:
+			// Hair coordinated
+			makeupColor = extractColor(slave.hColor);
+			makeupOpacity = 0.3;
+			break;
+		case 4:
+			// Slutty
+			makeupColor = "#ff0000";
+			makeupOpacity = 1;
+			break;
+		case 5:
+			// Neon
+			makeupColor = "#DC143C";
+			makeupOpacity = 1;
+			break;
+		case 6:
+			// Neon hair coordinated
+			makeupColor = extractColor(slave.hColor);
+			makeupOpacity = 1;
+			break;
+		case 7:
+			// Metallic
+			makeupColor = "#b22222";
+			makeupOpacity = 0.7;
+			break;
+		case 8:
+			// Metallic hair coordinated
+			makeupColor = extractColor(slave.hColor);
+			makeupOpacity = 0.7;
+			break;
+		default:
+			makeupColor = "#ffffff"
+			makeupOpacity = 0;
+			break;
+	}
+	makeupColor = hexToRgb(makeupColor);
+	lipsColor[0] = makeupColor[0] * makeupOpacity + lipsColor[0] * (1 - makeupOpacity);
+	lipsColor[1] = makeupColor[1] * makeupOpacity + lipsColor[1] * (1 - makeupOpacity);
+    lipsColor[2] = makeupColor[2] * makeupOpacity + lipsColor[2] * (1 - makeupOpacity);
+    
+    let nailColor;
+    switch (slave.nails) {
+		case 2:
+			// color-coordinated with hair
+			nailColor = extractColor(slave.hColor);
+			break;
+		case 4:
+			// bright and glittery
+			nailColor = "#ff0000";
+			break;
+		case 6:
+			// neon
+			nailColor = "#DC143C";
+			break;
+		case 7:
+			// color-coordinated neon
+			nailColor = extractColor(slave.hColor);
+			break;
+		case 8:
+			// metallic
+			nailColor = "#b22222";
+			break;
+		case 9:
+			// color-coordinated metallic
+			nailColor = extractColor(slave.hColor);
+			break;
+		default:
+            nailColor = "#ffffff"
+			break;
+    }
+
+    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]);
+
+	let irisColor;
+	let scleraColor;
+
+	if (hasAnyEyes(slave)) {
+		irisColor = hexToRgb(extractColor(hasLeftEye(slave) ? extractColor(slave.eye.left.iris) : extractColor(slave.eye.right.iris)));
+		scleraColor = hexToRgb(extractColor(hasLeftEye(slave) ? extractColor(slave.eye.left.sclera) : extractColor(slave.eye.left.sclera)));
+	} else {
+		irisColor = hexToRgb(extractColor("black"));
+		scleraColor = hexToRgb(extractColor("black"));
+	}
+
+	materials.push(["Irises", "Kd", [irisColor[0] * 1.5, irisColor[1] * 1.5, irisColor[2] * 1.5]]);
+	materials.push(["Sclera", "Kd", [scleraColor[0] * 1.2, scleraColor[1] * 1.2, scleraColor[2] * 1.2]]);
+
+	switch (slave.skin) {
+		case "pure white":
+		case "ivory":
+        case "white":
+            materials.push(["CeridwenFingernails", "Kd", nailColor]);
+			materials.push(["CeridwenLips", "Kd", lipsColor]);
+			materials.push(["WhiteFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [1.05, 1, 1]]);
+			break;
+		case "extremely pale":
+        case "very pale":
+            materials.push(["CelinetteFingernails", "Kd", nailColor]);
+			materials.push(["CelinetteLips", "Kd", lipsColor]);
+			materials.push(["WhiteFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [1.05, 1, 1]]);
+			break;
+		case "pale":
+        case "extremely fair":
+            materials.push(["KimmyFingernails", "Kd", nailColor]);
+			materials.push(["KimmyLips", "Kd", lipsColor]);
+			materials.push(["WhiteFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [1, 0.95, 0.91]]);
+			break;
+		case "very fair":
+        case "fair":
+            materials.push(["SaffronFingernails", "Kd", nailColor]);
+			materials.push(["SaffronLips", "Kd", lipsColor]);
+			materials.push(["LightFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [1.1, 1.1, 1.1]]);
+			break;
+		case "light":
+        case "light olive":
+            materials.push(["FemaleBaseFingernails", "Kd", nailColor]);
+			materials.push(["FemaleBaseLips", "Kd", lipsColor]);
+			materials.push(["LightFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [1.0, 1.0, 1.0]]);
+			break;
+		case "sun tanned":
+		case "spray tanned":
+        case "tan":
+            materials.push(["ReaganFingernails", "Kd", nailColor]);
+			materials.push(["ReaganLips", "Kd", lipsColor]);
+			materials.push(["MidFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [0.97, 0.95, 0.95]]);
+			break;
+        case "olive":
+            materials.push(["KathyFingernails", "Kd", nailColor]);
+			materials.push(["KathyLips", "Kd", lipsColor]);
+			materials.push(["MidFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [0.95, 0.92, 0.92]]);
+			break;
+        case "bronze":
+            materials.push(["MylouFingernails", "Kd", nailColor]);
+			materials.push(["MylouLips", "Kd", lipsColor]);
+			materials.push(["MidFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [0.91, 0.95, 0.98]]);
+			break;
+        case "dark olive":
+            materials.push(["AdalineFingernails", "Kd", nailColor]);
+			materials.push(["AdalineLips", "Kd", lipsColor]);
+			materials.push(["MidFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [0.90, 0.90, 0.90]]);
+			break;
+        case "dark":
+            materials.push(["DaphneFingernails", "Kd", nailColor]);
+			materials.push(["DaphneLips", "Kd", lipsColor]);
+			materials.push(["MidFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [0.88, 0.93, 0.96]]);
+			break;
+        case "light beige":
+            materials.push(["MinamiFingernails", "Kd", nailColor]);
+			materials.push(["MinamiLips", "Kd", lipsColor]);
+			materials.push(["MidFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [0.68, 0.74, 0.8]]);
+			break;
+        case "beige":
+            materials.push(["TaraFingernails", "Kd", nailColor]);
+			materials.push(["TaraLips", "Kd", lipsColor]);
+			materials.push(["MidFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [0.77, 0.77, 0.77]]);
+			break;
+		case "dark beige":
+        case "light brown":
+            materials.push(["TopmodelFingernails", "Kd", nailColor]);
+			materials.push(["TopmodelLips", "Kd", lipsColor]);
+			materials.push(["DarkFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [1.7, 1.75, 1.75]]);
+			break;
+		case "brown":
+        case "dark brown":
+            materials.push(["AngelicaFingernails", "Kd", nailColor]);
+			materials.push(["AngelicaLips", "Kd", lipsColor]);
+			materials.push(["DarkFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [0.85, 0.85, 0.85]]);
+			break;
+		case "black":
+		case "ebony":
+        case "pure black":
+            materials.push(["DarkSkinFingernails", "Kd", nailColor]);
+			materials.push(["DarkSkinLips", "Kd", lipsColor]);
+			materials.push(["DarkFutalicious_Genitalia_G8F_Glans_Futalicious_Shell", "Kd", [0.7, 0.7, 0.77]]);
+			break;
+	}
+
+	let sceneParams = slave.sceneParams;
+	for (let i =0; i < sceneParams.materials.length; i++)
+		for (let j =0; j < materials.length; j++)
+			if (sceneParams.materials[i].matId == materials[j][0])
+				sceneParams.materials[i][materials[j][1]] = materials[j][2];
+};
+
+App.Art.applyMorphs = function(slave) {
+	let morphs = [];
+
+	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 {
+		morphs.push(["posesMid", 1]);
+	}
+
+	switch (slave.faceShape) {
+		case "normal":
+			morphs.push(["faceShapeNormal", 1]); break;
+		case "masculine":
+			morphs.push(["faceShapeMasculine", 1]); break;
+		case "androgynous":
+			break;
+		case "cute":
+			morphs.push(["faceShapeCute", 1]); break;
+		case "sensual":
+			morphs.push(["faceShapeSensual", 1]); break;
+		case "exotic":
+			morphs.push(["faceShapeExotic", 1]); break;
+	}
+	
+	switch (slave.boobShape) {
+		case "normal":
+			break;
+		case "perky":
+			morphs.push(["boobShapePerky", 1]); break;
+		case "saggy":
+			morphs.push(["boobShapeSaggy", 1]); break;
+		case "torpedo-shaped":
+			morphs.push(["boobShapeTorpedo", 1]); break;
+		case "downward-facing":
+			morphs.push(["boobShapeDownward", 1]); break;
+		case "wide-set":
+			morphs.push(["boobShapeWide", 1]); break;
+	}
+
+	switch (slave.nipples) {
+		case "huge":
+			morphs.push(["nipplesHuge", 1]); break;
+		case "tiny":
+			morphs.push(["nipplesTiny", 1]); break;
+		case "cute":
+			morphs.push(["nipplesCute", 1]); break;
+		case "puffy":
+			morphs.push(["nipplesPuffy", 1]); break;
+		case "inverted":
+			morphs.push(["nipplesInverted", 1]); break;
+		case "partially inverted":
+			morphs.push(["nipplesPartiallyInverted", 1]); break;
+		case "fuckable":
+			morphs.push(["nipplesFuckable", 1]); break;
+	}
+
+	if (slave.foreskin !== 0)
+		morphs.push(["foreskin", 1]);
+
+	if (slave.dick == 0 && !(slave.scrotum <= 0 || slave.balls <= 0)) {
+		morphs.push(["dickRemove", 1]);
+	}
+	else if (slave.dick != 0) {
+		morphs.push(["dick", (slave.dick / 8) -1]);
+	}
+
+	if (slave.vagina == -1)
+		morphs.push(["vaginaRemove", 1]);
+
+	if (slave.scrotum <= 0 || slave.balls <= 0) {
+		morphs.push(["ballsRemove", 1]);
+	}
+	else {
+		morphs.push(["balls", slave.scrotum / 6 -1]);
+	}
+
+	morphs.push(["areolae", slave.areolae / 2 -1]);
+
+	morphs.push(["shoulders", slave.shoulders / -2]);
+	morphs.push(["lips", slave.lips / 100 - 0.5]);
+	slave.sceneParams.transform.scale = slave.height/175 // height by object transform
+	morphs.push(["muscles", (slave.muscles+30) /50]);
+	morphs.push(["belly", slave.belly/10000]);
+	morphs.push(["hips", slave.hips/2]);
+	morphs.push(["butt", (slave.butt-1)/31]);
+	morphs.push(["boobs", (slave.boobs-1)/1000]);
+	morphs.push(["waist", slave.waist/50]);
+	morphs.push(["weight", slave.weight/100]);
+
+	if (slave.visualAge < 20)
+		morphs.push(["physicalAgeYoung", -(slave.visualAge-20)/15]);
+	//else
+	//	morphs.push(["physicalAgeOld", (slave.visualAge-20)/50]);
+
+    App.Art.resetMorphs(slave);
+
+	let sceneParams = slave.sceneParams;
+	for (let i =0; i < sceneParams.model.morphs.length; i++)
+		for (let j =0; j < morphs.length; j++)
+			if (sceneParams.model.morphs[i].morphId == morphs[j][0])
+				sceneParams.model.morphs[i].value = morphs[j][1];
+};
\ No newline at end of file
diff --git a/src/art/rendered/scene1.js b/src/art/rendered/scene1.js
new file mode 100644
index 0000000000000000000000000000000000000000..e375ddbb16a3fcb667256092a9dcf6e3cf14430b
Binary files /dev/null and b/src/art/rendered/scene1.js differ
diff --git a/tsconfig.json b/tsconfig.json
index 15bd704fb06e74387a05b159108e8e79ceef1c71..a3374d53e3b53f906afeb2cefa81fdf4fc8f5c6c 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -16,6 +16,7 @@
 	"exclude": [
 		"src/001-lib/Jquery/**",
 		"src/001-lib/mousetrap/**",
-		"src/js/displayVariables.js"
+		"src/js/displayVariables.js",
+		"src/art/rendered/scene1.js",
 	]
 }