/**
 * @author Tony Parisi / http://www.tonyparisi.com/
 */


THREE.glTFLoader = function () {

	this.meshesRequested = 0;
	this.meshesLoaded = 0;
	this.pendingMeshes = [];
	this.animationsRequested = 0;
	this.animationsLoaded = 0;
	this.animations = [];
	this.shadersRequested = 0;
	this.shadersLoaded = 0;
	this.shaders = {};
	this.loadRequests = [];
	THREE.glTFShaders.removeAll();
	THREE.Loader.call( this );
}

THREE.glTFLoader.prototype = new THREE.Loader();
THREE.glTFLoader.prototype.constructor = THREE.glTFLoader;

THREE.glTFLoader.prototype.load = function( url, callback ) {

	var theLoader = this;
	// Utilities

	function RgbArraytoHex(colorArray) {
		if(!colorArray) return 0xFFFFFFFF;
		var r = Math.floor(colorArray[0] * 255),
			g = Math.floor(colorArray[1] * 255),
			b = Math.floor(colorArray[2] * 255),
			a = 255;

		var color = (a << 24) + (r << 16) + (g << 8) + b;

		return color;
	}

	function componentsPerElementForGLType(type) {
		switch(type) {
			case "SCALAR" :
				nElements = 1;
				break;
			case "VEC2" :
				nElements = 2;
				break;
			case "VEC3" :
				nElements = 3;
				break;
			case "VEC4" :
				nElements = 4;
				break;
			case "MAT2" :
				nElements = 4;
				break;
			case "MAT3" :
				nElements = 9;
				break;
			case "MAT4" :
				nElements = 16;
				break;
			default :
				debugger;
				break;
		}

		return nElements;
	}

	function replaceShaderDefinitions(shader, material) {

		// Three.js seems too dependent on attribute names so globally
		// replace those in the shader code
		var program = material.params.program;
		var shaderParams = material.params.technique.parameters;
		var shaderAttributes = material.params.technique.attributes;
		var params = {};

		for (var attribute in material.params.attributes) {
			var pname = shaderAttributes[attribute];
			var shaderParam = shaderParams[pname];
			var semantic = shaderParam.semantic;
			if (semantic) {
				params[attribute] = shaderParam;
			}
		}


		var s = shader;
		var r = "";
		for (var pname in params) {
			var param = params[pname];
			var semantic = param.semantic;

			r = eval("/" + pname + "/g");

			switch (semantic) {
				case "POSITION" :
					s = s.replace(r, 'position');
					break;
				case "NORMAL" :
					s = s.replace(r, 'normal');
					break;
				case "TEXCOORD_0" :
					s = s.replace(r, 'uv');
					break;
			   case "WEIGHT" :
					s = s.replace(r, 'skinWeight');
					break;
				case "JOINT" :
					s = s.replace(r, 'skinIndex');
					break;
				default :
					break;
			}

		}

		return s;
	}

	function replaceShaderSemantics(material) {

		var vertexShader = theLoader.shaders[material.params.vertexShader];
		if (vertexShader) {
			vertexShader = replaceShaderDefinitions(vertexShader, material);
			theLoader.shaders[material.params.vertexShader] = vertexShader;
		}

	}

	function createShaderMaterial(material) {

		// replace named attributes and uniforms with Three.js built-ins
		replaceShaderSemantics(material);

		var fragmentShader = theLoader.shaders[material.params.fragmentShader];
		if (!fragmentShader) {
			console.log("ERROR: Missing fragment shader definition:", material.params.fragmentShader);
			return new THREE.MeshPhongMaterial;
		}

		var vertexShader = theLoader.shaders[material.params.vertexShader];
		if (!vertexShader) {
			console.log("ERROR: Missing vertex shader definition:", material.params.vertexShader);
			return new THREE.MeshPhongMaterial;
		}

		// clone most uniforms but then clobber textures, we want them to
		// be reused
		var uniforms = THREE.UniformsUtils.clone(material.params.uniforms);
		for (uniform in material.params.uniforms) {
			var src = material.params.uniforms[uniform];
			var dst = uniforms[uniform];
			if (dst.type == "t") {
				dst.value = src.value;
			}
		}

		var shaderMaterial = new THREE.RawShaderMaterial( {

			fragmentShader: fragmentShader,
			vertexShader: vertexShader,
			uniforms: uniforms,
			transparent: material.params.transparent,

		} );

//        console.log("New shader material")
		return shaderMaterial;
	}


	function LoadTexture(src) {
		if(!src) { return null; }

		var isDataUriRegex = /^data:/;

		var loadImage = function(url, success, error) {
			var image = new Image();

			image.onload = function() {
				success(image);
			};

			if (typeof error !== 'undefined') {
				image.onerror = error;
			}

			image.src = url;
		};

		function loadImageFromTypedArray(uint8Array, format) {
			//>>includeStart('debug', pragmas.debug);
			if (!defined(uint8Array)) {
				throw new DeveloperError('uint8Array is required.');
			}

			if (!defined(format)) {
				throw new DeveloperError('format is required.');
			}
			//>>includeEnd('debug');

			var blob = new Blob([uint8Array], {
				type : format
			});

		};

		function decodeDataUriText(isBase64, data) {
			var result = decodeURIComponent(data);
			if (isBase64) {
				return atob(result);
			}
			return result;
		}

		function decodeDataUriArrayBuffer(isBase64, data) {
			var byteString = decodeDataUriText(isBase64, data);
			var buffer = new ArrayBuffer(byteString.length);
			var view = new Uint8Array(buffer);
			for (var i = 0; i < byteString.length; i++) {
				view[i] = byteString.charCodeAt(i);
			}
			return buffer;
		}

		function decodeDataUri(dataUriRegexResult, responseType) {
			responseType = typeof responseType !== 'undefined' ? responseType : '';
			var mimeType = dataUriRegexResult[1];
			var isBase64 = !!dataUriRegexResult[2];
			var data = dataUriRegexResult[3];

			switch (responseType) {
			case '':
			case 'text':
				return decodeDataUriText(isBase64, data);
			case 'ArrayBuffer':
				return decodeDataUriArrayBuffer(isBase64, data);
			case 'blob':
				var buffer = decodeDataUriArrayBuffer(isBase64, data);
				return new Blob([buffer], {
					type : mimeType
				});
			case 'document':
				var parser = new DOMParser();
				return parser.parseFromString(decodeDataUriText(isBase64, data), mimeType);
			case 'json':
				return JSON.parse(decodeDataUriText(isBase64, data));
			default:
				throw 'Unhandled responseType: ' + responseType;
			}
		}

		var dataUriRegex = /^data:(.*?)(;base64)?,(.*)$/;
		var dataUriRegexResult = dataUriRegex.exec(src);
		if (dataUriRegexResult !== null) {
			var texture = new THREE.Texture;
			var blob = decodeDataUri(dataUriRegexResult, 'blob');
			var blobUrl = window.URL.createObjectURL(blob);
			loadImage(blobUrl, function(img) {
				texture.image = img;
				texture.needsUpdate = true;
			});
			return texture;
		}

		return new THREE.TextureLoader().load(src);
	}

	function CreateTexture(resources, resource) {
		var texturePath = null;
		var textureParams = null;

		if (resource)
		{
			var texture = resource;
			if (texture) {
				var textureEntry = resources.getEntry(texture);
				if (textureEntry) {
					{
						var imageEntry = resources.getEntry(textureEntry.description.source);
						if (imageEntry) {
							texturePath = imageEntry.description.uri;
						}

						var samplerEntry = resources.getEntry(textureEntry.description.sampler);
						if (samplerEntry) {
							textureParams = samplerEntry.description;
						}
					}
				}
			}
		}

		var texture = LoadTexture(texturePath);
		if (texture && textureParams) {

			if (textureParams.wrapS == WebGLRenderingContext.REPEAT)
				texture.wrapS = THREE.RepeatWrapping;

			if (textureParams.wrapT == WebGLRenderingContext.REPEAT)
				texture.wrapT = THREE.RepeatWrapping;

			if (textureParams.magFilter == WebGLRenderingContext.LINEAR)
				texture.magFilter = THREE.LinearFilter;

//                  if (textureParams.minFilter == "LINEAR")
//                      texture.minFilter = THREE.LinearFilter;
		}

		return texture;
	}

	// Geometry processing

	var ClassicGeometry = function() {

		this.geometry = new THREE.BufferGeometry;
		this.totalAttributes = 0;
		this.loadedAttributes = 0;
		this.indicesLoaded = false;
		this.finished = false;

		this.onload = null;

		this.uvs = null;
		this.indexArray = null;
	};

	ClassicGeometry.prototype.constructor = ClassicGeometry;

	ClassicGeometry.prototype.buildBufferGeometry = function() {
		// Build indexed mesh
		var geometry = this.geometry;
		geometry.setIndex(new THREE.BufferAttribute( this.indexArray, 1 ) );

		var offset = {
				start: 0,
				index: 0,
				count: this.indexArray.length
			};

		geometry.groups.push( offset );

		geometry.computeBoundingSphere();
	}

	ClassicGeometry.prototype.checkFinished = function() {
		if(this.indexArray && this.loadedAttributes === this.totalAttributes) {

			this.buildBufferGeometry();

			this.finished = true;

			if(this.onload) {
				this.onload();
			}
		}
	};

	// Delegate for processing index buffers
	var IndicesDelegate = function() {};

	IndicesDelegate.prototype.handleError = function(errorCode, info) {
		// FIXME: report error
		console.log("ERROR(IndicesDelegate):"+errorCode+":"+info);
	};

	IndicesDelegate.prototype.convert = function(resource, ctx) {
		return new Uint16Array(resource, 0, ctx.indices.count);
	};

	IndicesDelegate.prototype.resourceAvailable = function(glResource, ctx) {
		var geometry = ctx.geometry;
		geometry.indexArray = glResource;
		geometry.checkFinished();
		return true;
	};

	var indicesDelegate = new IndicesDelegate();

	var IndicesContext = function(indices, geometry) {
		this.indices = indices;
		this.geometry = geometry;
	};

	// Delegate for processing vertex attribute buffers
	var VertexAttributeDelegate = function() {};

	VertexAttributeDelegate.prototype.handleError = function(errorCode, info) {
		// FIXME: report error
		console.log("ERROR(VertexAttributeDelegate):"+errorCode+":"+info);
	};

	VertexAttributeDelegate.prototype.convert = function(resource, ctx) {
		return resource;
	};


	VertexAttributeDelegate.prototype.bufferResourceAvailable = function(glResource, ctx) {
		var geom = ctx.geometry;
		var attribute = ctx.attribute;
		var semantic = ctx.semantic;
		var floatArray;
		var i, l;
		var nComponents;
		//FIXME: Float32 is assumed here, but should be checked.

		if (semantic == "POSITION") {
			// TODO: Should be easy to take strides into account here
			floatArray = new Float32Array(glResource, 0, attribute.count * componentsPerElementForGLType(attribute.type));
			geom.geometry.addAttribute( 'position', new THREE.BufferAttribute( floatArray, 3 ) );
		} else if (semantic == "NORMAL") {
			nComponents = componentsPerElementForGLType(attribute.type);
			floatArray = new Float32Array(glResource, 0, attribute.count * nComponents);
			geom.geometry.addAttribute( 'normal', new THREE.BufferAttribute( floatArray, 3 ) );
		} else if ((semantic == "TEXCOORD_0") || (semantic == "TEXCOORD" )) {

			nComponents = componentsPerElementForGLType(attribute.type);
			floatArray = new Float32Array(glResource, 0, attribute.count * nComponents);
			// N.B.: flip Y value... should we just set texture.flipY everywhere?
			for (i = 0; i < floatArray.length / 2; i++) {
				floatArray[i*2+1] = 1.0 - floatArray[i*2+1];
			}
			geom.geometry.addAttribute( 'uv', new THREE.BufferAttribute( floatArray, nComponents ) );
		}
		else if (semantic == "WEIGHT") {
			nComponents = componentsPerElementForGLType(attribute.type);
			floatArray = new Float32Array(glResource, 0, attribute.count * nComponents);
			geom.geometry.addAttribute( 'skinWeight', new THREE.BufferAttribute( floatArray, nComponents ) );
		}
		else if (semantic == "JOINT") {
			nComponents = componentsPerElementForGLType(attribute.type);
			floatArray = new Float32Array(glResource, 0, attribute.count * nComponents);
			geom.geometry.addAttribute( 'skinIndex', new THREE.BufferAttribute( floatArray, nComponents ) );
		}
	}

	VertexAttributeDelegate.prototype.resourceAvailable = function(glResource, ctx) {

		this.bufferResourceAvailable(glResource, ctx);

		var geom = ctx.geometry;
		geom.loadedAttributes++;
		geom.checkFinished();
		return true;
	};

	var vertexAttributeDelegate = new VertexAttributeDelegate();

	var VertexAttributeContext = function(attribute, semantic, geometry) {
		this.attribute = attribute;
		this.semantic = semantic;
		this.geometry = geometry;
	};

	var Mesh = function() {
		this.primitives = [];
		this.materialsPending = [];
		this.loadedGeometry = 0;
		this.onCompleteCallbacks = [];
	};

	Mesh.prototype.addPrimitive = function(geometry, material) {

		var self = this;
		geometry.onload = function() {
			self.loadedGeometry++;
			self.checkComplete();
		};

		this.primitives.push({
			geometry: geometry,
			material: material,
			mesh: null
		});
	};

	Mesh.prototype.onComplete = function(callback) {
		this.onCompleteCallbacks.push(callback);
		//this.checkComplete();
	};

	Mesh.prototype.checkComplete = function() {
		var self = this;
		if(this.onCompleteCallbacks.length && this.primitives.length == this.loadedGeometry) {
			this.onCompleteCallbacks.forEach(function(callback) {
				callback(self);
			});
			this.onCompleteCallbacks = [];
		}
	};

	Mesh.prototype.attachToNode = function(threeNode) {
		// Assumes that the geometry is complete
		var that = this;
		this.primitives.forEach(function(primitive) {
			/*if(!primitive.mesh) {
				primitive.mesh = new THREE.Mesh(primitive.geometry, primitive.material);
			}*/
			var material = primitive.material;
			var materialParams = material.params;
			if (!(material instanceof THREE.Material)) {
				material = createShaderMaterial(material);
			}

			if (!that.skin) {
				// console.log ("New mesh")
				var threeMesh = new THREE.Mesh(primitive.geometry.geometry, material);
				threeMesh.castShadow = true;
				threeNode.add(threeMesh);

				if (material instanceof THREE.ShaderMaterial) {
					var glTFShader = new THREE.glTFShader(material, materialParams, threeMesh, theLoader.rootObj);
					THREE.glTFShaders.add(glTFShader);

				}
			}
		});
	};

	// Delayed-loaded material
	var Material = function(params) {
		this.params = params;
	};

	// Delegate for processing animation parameter buffers
	var AnimationParameterDelegate = function() {};

	AnimationParameterDelegate.prototype.handleError = function(errorCode, info) {
		// FIXME: report error
		console.log("ERROR(AnimationParameterDelegate):"+errorCode+":"+info);
	};

	AnimationParameterDelegate.prototype.convert = function(resource, ctx) {
		var parameter = ctx.parameter;

		var glResource = null;
		switch (parameter.type) {
			case "SCALAR" :
			case "VEC2" :
			case "VEC3" :
			case "VEC4" :
				glResource = new Float32Array(resource, 0, parameter.count * componentsPerElementForGLType(parameter.type));
				break;
			default:
				break;
		}

		return glResource;
	};

	AnimationParameterDelegate.prototype.resourceAvailable = function(glResource, ctx) {
		var animation = ctx.animation;
		var parameter = ctx.parameter;
		parameter.data = glResource;
		animation.handleParameterLoaded(parameter);
		return true;
	};

	var animationParameterDelegate = new AnimationParameterDelegate();

	var AnimationParameterContext = function(parameter, animation) {
		this.parameter = parameter;
		this.animation = animation;
	};

	// Animations
	var Animation = function() {

		// create Three.js keyframe here
		this.totalParameters = 0;
		this.loadedParameters = 0;
		this.parameters = {};
		this.finishedLoading = false;
		this.onload = null;

	};

	Animation.prototype.constructor = Animation;

	Animation.prototype.handleParameterLoaded = function(parameter) {
		this.parameters[parameter.name] = parameter;
		this.loadedParameters++;
		this.checkFinished();
	};

	Animation.prototype.checkFinished = function() {
		if(this.loadedParameters === this.totalParameters) {
			// Build animation
			this.finishedLoading = true;

			if (this.onload) {
				this.onload();
			}
		}
	};

	// Delegate for processing inverse bind matrices buffer
	var InverseBindMatricesDelegate = function() {};

	InverseBindMatricesDelegate.prototype.handleError = function(errorCode, info) {
		// FIXME: report error
		console.log("ERROR(InverseBindMatricesDelegate):"+errorCode+":"+info);
	};

	InverseBindMatricesDelegate.prototype.convert = function(resource, ctx) {
		var parameter = ctx.parameter;

		var glResource = null;
		switch (parameter.type) {
			case "MAT4" :
				glResource = new Float32Array(resource, 0, parameter.count * componentsPerElementForGLType(parameter.type));
				break;
			default:
				break;
		}

		return glResource;
	};

	InverseBindMatricesDelegate.prototype.resourceAvailable = function(glResource, ctx) {
		var skin = ctx.skin;
		skin.inverseBindMatrices = glResource;
		return true;
	};

	var inverseBindMatricesDelegate = new InverseBindMatricesDelegate();

	var InverseBindMatricesContext = function(param, skin) {
		this.parameter = param;
		this.skin = skin;
	};

	// Delegate for processing shaders from external files
	var ShaderDelegate = function() {};

	ShaderDelegate.prototype.handleError = function(errorCode, info) {
		// FIXME: report error
		console.log("ERROR(ShaderDelegate):"+errorCode+":"+info);
	};

	ShaderDelegate.prototype.convert = function(resource, ctx) {
		return resource;
	}

	ShaderDelegate.prototype.resourceAvailable = function(data, ctx) {
		theLoader.shadersLoaded++;
		theLoader.shaders[ctx.id] = data;
		return true;
	};

	var shaderDelegate = new ShaderDelegate();

	var ShaderContext = function(id, path) {
		this.id = id;
		this.uri = path;
	};

	// Resource management

	var ResourceEntry = function(entryID, object, description) {
		this.entryID = entryID;
		this.object = object;
		this.description = description;
	};

	var Resources = function() {
		this._entries = {};
	};

	Resources.prototype.setEntry = function(entryID, object, description) {
		if (!entryID) {
			console.error("No EntryID provided, cannot store", description);
			return;
		}

		if (this._entries[entryID]) {
			console.warn("entry["+entryID+"] is being overwritten");
		}

		this._entries[entryID] = new ResourceEntry(entryID, object, description );
	};

	Resources.prototype.getEntry = function(entryID) {
		return this._entries[entryID];
	};

	Resources.prototype.clearEntries = function() {
		this._entries = {};
	};

	LoadDelegate = function() {
	}

	LoadDelegate.prototype.loadCompleted = function(callback, obj) {
		callback.call(Window, obj);
	}

	// Loader

	var ThreeGLTFLoader = Object.create(glTFParser, {

		load: {
			enumerable: true,
			value: function(userInfo, options) {
				this.resources = new Resources();
				this.cameras = [];
				this.lights = [];
				this.animations = [];
				this.joints = {};
				THREE.GLTFLoaderUtils.init();
				glTFParser.load.call(this, userInfo, options);
			}
		},

		cameras: {
			enumerable: true,
			writable: true,
			value : []
		},

		lights: {
			enumerable: true,
			writable: true,
			value : []
		},

		animations: {
			enumerable: true,
			writable: true,
			value : []
		},

		// Implement WebGLTFLoader handlers

		handleBuffer: {
			value: function(entryID, description, userInfo) {
				this.resources.setEntry(entryID, null, description);
				description.type = "ArrayBuffer";
				return true;
			}
		},

		handleBufferView: {
			value: function(entryID, description, userInfo) {
				this.resources.setEntry(entryID, null, description);

				var buffer =  this.resources.getEntry(description.buffer);
				description.type = "ArrayBufferView";

				var bufferViewEntry = this.resources.getEntry(entryID);
				bufferViewEntry.buffer = buffer;
				return true;
			}
		},

		handleShader: {
			value: function(entryID, description, userInfo) {
				this.resources.setEntry(entryID, null, description);
				var shaderRequest = {
						id : entryID,
						uri : description.uri,
				};

				var shaderContext = new ShaderContext(entryID, description.uri);

				theLoader.shadersRequested++;
				THREE.GLTFLoaderUtils.getFile(shaderRequest, shaderDelegate, shaderContext);

				return true;
			}
		},

		handleProgram: {
			value: function(entryID, description, userInfo) {
				this.resources.setEntry(entryID, null, description);
				return true;
			}
		},

		handleTechnique: {
			value: function(entryID, description, userInfo) {
				description.refCount = 0;
				this.resources.setEntry(entryID, null, description);
				return true;
			}
		},


		createShaderParams : {
			value: function(materialId, values, params, programID, technique) {
				var program = this.resources.getEntry(programID);

				params.uniforms = {};
				params.attributes = {};
				params.program = program;
				params.technique = technique;
				if (program) {
					params.fragmentShader = program.description.fragmentShader;
					params.vertexShader = program.description.vertexShader;
					for (var uniform in technique.uniforms) {
						var pname = technique.uniforms[uniform];
						var shaderParam = technique.parameters[pname];
						var ptype = shaderParam.type;
						var pcount = shaderParam.count;
						var value = values[pname];
						var utype = "";
						var uvalue;
						var ulength;

						// THIS: for (n in WebGLRenderingContext) { z = WebGLRenderingContext[n]; idx[z] = n; }
						//console.log("shader uniform param type: ", ptype, "-", theLoader.idx[ptype])


						switch (ptype) {
							case WebGLRenderingContext.FLOAT :
								utype = "f";
								uvalue = shaderParam.value;
								if (pname == "transparency") {
									var USE_A_ONE = true; // for now, hack because file format isn't telling us
									var opacity =  USE_A_ONE ? value : (1.0 - value);
									uvalue = opacity;
									params.transparent = true;
								}
								break;
							case WebGLRenderingContext.FLOAT_VEC2 :
								utype = "v2";
								uvalue = new THREE.Vector2;
								if (shaderParam && shaderParam.value) {
									var v2 = shaderParam.value;
									uvalue.fromArray(v2);
								}
								if (value) {
									uvalue.fromArray(value);
								}
								break;
							case WebGLRenderingContext.FLOAT_VEC3 :
								utype = "v3";
								uvalue = new THREE.Vector3;
								if (shaderParam && shaderParam.value) {
									var v3 = shaderParam.value;
									uvalue.fromArray(v3);
								}
								if (value) {
									uvalue.fromArray(value);
								}
								break;
							case WebGLRenderingContext.FLOAT_VEC4 :
								utype = "v4";
								uvalue = new THREE.Vector4;
								if (shaderParam && shaderParam.value) {
									var v4 = shaderParam.value;
									uvalue.fromArray(v4);
								}
								if (value) {
									uvalue.fromArray(value);
								}
								break;
							case WebGLRenderingContext.FLOAT_MAT2 :
								// what to do?
								console.log("Warning: FLOAT_MAT2");
								break;
							case WebGLRenderingContext.FLOAT_MAT3 :
								utype = "m3";
								uvalue = new THREE.Matrix3;
								if (shaderParam && shaderParam.value) {
									var m3 = shaderParam.value;
									uvalue.fromArray(m3);
								}
								if (value) {
									uvalue.fromArray(value);
								}
								break;
							case WebGLRenderingContext.FLOAT_MAT4 :
								if (pcount !== undefined) {
									utype = "m4v";
									uvalue = new Array(pcount);
									for (var mi = 0; mi < pcount; mi++) {
										uvalue[mi] = new THREE.Matrix4;
									}
									ulength = pcount;

									if (shaderParam && shaderParam.value) {
										var m4v = shaderParam.value;
										uvalue.fromArray(m4v);
									}
									if (value) {
										uvalue.fromArray(value);

									}
								}
								else {
									utype = "m4";
									uvalue = new THREE.Matrix4;

									if (shaderParam && shaderParam.value) {
										var m4 = shaderParam.value;
										uvalue.fromArray(m4);
									}
									if (value) {
										uvalue.fromArray(value);

									}
								}
								break;
							case WebGLRenderingContext.SAMPLER_2D :
								utype = "t";
								uvalue = value ? CreateTexture(this.resources, value) : null;
								break;
							default :
								throw new Error("Unknown shader uniform param type: " + ptype + " - " + theLoader.idx[ptype]);

								break;
						}


						var udecl = { type : utype, value : uvalue, length : ulength };

						params.uniforms[uniform] = udecl;
					}

					for (var attribute in technique.attributes) {
						var pname = technique.attributes[attribute];
						var param = technique.parameters[pname];
						var atype = param.type;
						var semantic = param.semantic;
						var adecl = { type : atype, semantic : semantic };

						params.attributes[attribute] = adecl;
					}

				}
			}
		},

		threeJSMaterialType : {
			value: function(materialId, material, params) {

				var extensions = material.extensions;
				var khr_material = extensions ? extensions.KHR_materials_common : null;

				var materialType = null;
				var values;

				if (khr_material) {

					switch (khr_material.technique)
					{
						case 'BLINN' :
						case 'PHONG' :
							materialType = THREE.MeshPhongMaterial;
							break;

						case 'LAMBERT' :
							materialType = THREE.MeshLambertMaterial;
							break;

						case 'CONSTANT' :
						default :
							materialType = THREE.MeshBasicMaterial;
							break;
					}

					if (khr_material.doubleSided)
					{
						params.side = THREE.DoubleSide;
					}

					if (khr_material.transparent)
					{
						params.transparent = true;
					}

					values = {};
					for (prop in khr_material.values) {
						values[prop] = khr_material.values[prop];
					}

				}
				else {
					var technique = material.technique ?
						this.resources.getEntry(material.technique) :
						null;

					values = material.values;
					var description = technique.description;

					if (++description.refCount > 1) {
						//console.log("refcount", description.refCount);
					}

					var programID = description.program;
					this.createShaderParams(materialId, values, params, programID, description);

					var loadshaders = true;

					if (loadshaders) {
						materialType = Material;
					}
				}

				if (values.diffuse && typeof(values.diffuse) == 'string') {
					params.map = CreateTexture(this.resources, values.diffuse);
				}
				if (values.reflective && typeof(values.reflective) == 'string') {
					params.envMap = CreateTexture(this.resources, values.reflective);
				}

				var shininess = values.shininesss || values.shininess; // N.B.: typo in converter!
				if (shininess)
				{
					shininess = shininess;
				}

				var diffuseColor = null;
				if (!params.map) {
					diffuseColor = values.diffuse;
				}
				var opacity = 1.0;
				if (values.hasOwnProperty("transparency"))
				{
					var USE_A_ONE = true; // for now, hack because file format isn't telling us
					opacity =  USE_A_ONE ? values.transparency : (1.0 - values.transparency);
				}

				// if (diffuseColor) diffuseColor = [0, 1, 0];

				params.color = RgbArraytoHex(diffuseColor);
				params.opacity = opacity;
				params.transparent = opacity < 1.0;
				// hack hack hack
				if (params.map && params.map.sourceFile.toLowerCase().indexOf(".png") != -1)
					params.transparent = true;

				if (!(shininess === undefined))
				{
					params.shininess = Math.max( shininess, 1e-4 );
				}

				delete params.ambient;
				if (!(values.ambient === undefined) && !(typeof(values.ambient) == 'string'))
				{
					//params.ambient = RgbArraytoHex(values.ambient);
				}

				if (!(values.emission === undefined))
				{
					params.emissive = RgbArraytoHex(values.emission);
				}

				if (!(values.specular === undefined))
				{
					params.specular = RgbArraytoHex(values.specular);
				}

				return materialType;

			}
		},

		handleMaterial: {
			value: function(entryID, description, userInfo) {
				var params = {};

				var materialType = this.threeJSMaterialType(entryID, description, params);

				var material = new materialType(params);

				this.resources.setEntry(entryID, material, description);

				return true;
			}
		},

		handleMesh: {
			value: function(entryID, description, userInfo) {
				var mesh = new Mesh();
				this.resources.setEntry(entryID, mesh, description);
				var primitivesDescription = description.primitives;
				if (!primitivesDescription) {
					//FIXME: not implemented in delegate
					console.log("MISSING_PRIMITIVES for mesh:"+ entryID);
					return false;
				}

				for (var i = 0 ; i < primitivesDescription.length ; i++) {
					var primitiveDescription = primitivesDescription[i];

					if (primitiveDescription.mode === WebGLRenderingContext.TRIANGLES) {

						var geometry = new ClassicGeometry();
						var materialEntry = this.resources.getEntry(primitiveDescription.material);

						mesh.addPrimitive(geometry, materialEntry.object);

						var allAttributes = Object.keys(primitiveDescription.attributes);

						// count them first, async issues otherwise
						allAttributes.forEach( function(semantic) {
							geometry.totalAttributes++;
						}, this);

						var indices = this.resources.getEntry(primitiveDescription.indices);
						var bufferEntry = this.resources.getEntry(indices.description.bufferView);
						var indicesObject = {
								bufferView : bufferEntry,
								byteOffset : indices.description.byteOffset,
								count : indices.description.count,
								id : indices.entryID,
								componentType : indices.description.componentType,
								type : indices.description.type
						};

						var indicesContext = new IndicesContext(indicesObject, geometry);
						var loaddata = {
							indicesObject : indicesObject,
							indicesDelegate : indicesDelegate,
							indicesContext : indicesContext
						};

						theLoader.scheduleLoad(function(data) {
							var alreadyProcessedIndices =
								THREE.GLTFLoaderUtils.getBuffer(data.indicesObject,
									data.indicesDelegate, data.indicesContext);

							if (alreadyProcessedIndices) {
								data.indicesDelegate.resourceAvailable(
									alreadyProcessedIndices, data.indicesContext);
							}

						}, loaddata);

						// Load Vertex Attributes
						allAttributes.forEach( function(semantic) {

							var attribute;
							var attributeID = primitiveDescription.attributes[semantic];
							var attributeEntry = this.resources.getEntry(attributeID);
							if (!attributeEntry) {
								//let's just use an anonymous object for the attribute
								attribute = description.attributes[attributeID];
								attribute.id = attributeID;
								this.resources.setEntry(attributeID, attribute, attribute);

								var bufferEntry = this.resources.getEntry(attribute.bufferView);
								attributeEntry = this.resources.getEntry(attributeID);

							} else {
								attribute = attributeEntry.object;
								attribute.id = attributeID;
								var bufferEntry = this.resources.getEntry(attribute.bufferView);
							}

							var attributeObject = {
									bufferView : bufferEntry,
									byteOffset : attribute.byteOffset,
									byteStride : attribute.byteStride,
									count : attribute.count,
									max : attribute.max,
									min : attribute.min,
									componentType : attribute.componentType,
									type : attribute.type,
									id : attributeID
							};

							var attribContext = new VertexAttributeContext(attributeObject, semantic, geometry);

							var loaddata = {
								attributeObject : attributeObject,
								vertexAttributeDelegate : vertexAttributeDelegate,
								attribContext : attribContext
							};

							theLoader.scheduleLoad(function(data) {
								var alreadyProcessedAttribute =
									THREE.GLTFLoaderUtils.getBuffer(data.attributeObject,
										data.vertexAttributeDelegate, data.attribContext);

								if (alreadyProcessedAttribute) {
									data.vertexAttributeDelegate.resourceAvailable(
										alreadyProcessedAttribute, data.attribContext);
								}

							}, loaddata);


						}, this);
					}
				}
				return true;
			}
		},

		handleCamera: {
			value: function(entryID, description, userInfo) {
				var camera;
				if (description.type == "perspective")
				{
					var znear = description.perspective.znear;
					var zfar = description.perspective.zfar;
					var yfov = description.perspective.yfov;
					var xfov = description.perspective.xfov;
					var aspect_ratio = description.perspective.aspect_ratio;

					if (!aspect_ratio)
						aspect_ratio = 1;

					if (xfov === undefined) {
						if (yfov)
						{
							// According to COLLADA spec...
							// aspect_ratio = xfov / yfov
							xfov = yfov * aspect_ratio;
						}
					}

					if (yfov === undefined)
					{
						if (xfov)
						{
							// According to COLLADA spec...
							// aspect_ratio = xfov / yfov
							yfov = xfov / aspect_ratio;
						}

					}

					if (xfov)
					{
						xfov = THREE.Math.radToDeg(xfov);

						camera = new THREE.PerspectiveCamera(xfov, aspect_ratio, znear, zfar);
					}
				}
				else
				{
					camera = new THREE.OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, znear, zfar );
				}

				if (camera)
				{
					this.resources.setEntry(entryID, camera, description);
				}

				return true;
			}
		},

		handleLight: {
			value: function(entryID, description, userInfo) {

				var light = null;
				var type = description.type;
				if (type && description[type])
				{
					var lparams = description[type];
					var color = RgbArraytoHex(lparams.color);

					switch (type) {
						case "directional" :
							light = new THREE.DirectionalLight(color);
							light.position.set(0, 0, 1);
						break;

						case "point" :
							light = new THREE.PointLight(color);
						break;

						case "spot " :
							light = new THREE.SpotLight(color);
							light.position.set(0, 0, 1);
						break;

						case "ambient" :
							light = new THREE.AmbientLight(color);
						break;
					}
				}

				if (light)
				{
					this.resources.setEntry(entryID, light, description);
				}

				return true;
			}
		},

		addPendingMesh: {
			value: function(mesh, threeNode) {
				theLoader.pendingMeshes.push({
					mesh: mesh,
					node: threeNode
				});
			}
		},

		handleNode: {
			value: function(entryID, description, userInfo) {

				var threeNode = null;
				if (description.jointName) {
					threeNode = new THREE.Bone();
					threeNode.jointName = description.jointName;
					this.joints[description.jointName] = entryID;
				}
				else {
					threeNode = new THREE.Object3D();
				}

				threeNode.name = description.name;
				threeNode.glTFID = entryID;
				threeNode.glTF = description;

				this.resources.setEntry(entryID, threeNode, description);

				var m = description.matrix;
				if(m) {
					threeNode.matrixAutoUpdate = false;
					threeNode.applyMatrix(new THREE.Matrix4().set(
						m[0],  m[4],  m[8],  m[12],
						m[1],  m[5],  m[9],  m[13],
						m[2],  m[6],  m[10], m[14],
						m[3],  m[7],  m[11], m[15]
					));
				}
				else {
					var t = description.translation;
					var r = description.rotation;
					var s = description.scale;

					var position = t ? new THREE.Vector3(t[0], t[1], t[2]) :
						new THREE.Vector3;

					var rotation = r ? new THREE.Quaternion(r[0], r[1], r[2], r[3]) :
						new THREE.Quaternion;
					var scale = s ? new THREE.Vector3(s[0], s[1], s[2]) :
						new THREE.Vector3(1, 1, 1);

					var matrix = new THREE.Matrix4;
					matrix.compose(position, rotation, scale);
					threeNode.matrixAutoUpdate = false;
					threeNode.applyMatrix(matrix);
				}

				var self = this;

				if (description.meshes) {
					description.meshInstances = {};
					var skinEntry;
					if (description.skin) {
						skinEntry =  this.resources.getEntry(description.skin);
					}

					description.meshes.forEach( function(meshID) {
						meshEntry = this.resources.getEntry(meshID);
						theLoader.meshesRequested++;
						meshEntry.object.onComplete(function(mesh) {
							self.addPendingMesh(mesh, threeNode);
							description.meshInstances[meshID] = meshEntry.object;
							if (skinEntry) {
								mesh.skin = skinEntry;
								description.instanceSkin = skinEntry.object;
							}

							theLoader.meshesLoaded++;
							theLoader.checkComplete();
						});
					}, this);
				}

				if (description.camera) {
					var cameraEntry = this.resources.getEntry(description.camera);
					if (cameraEntry) {
						threeNode.add(cameraEntry.object);
						this.cameras.push(cameraEntry.object);
					}
				}

				if (description.extensions && description.extensions.KHR_materials_common
						&& description.extensions.KHR_materials_common.light) {
					var lightID = description.extensions.KHR_materials_common.light;
					var lightEntry = this.resources.getEntry(lightID);
					if (lightEntry) {
						threeNode.add(lightEntry.object);
						this.lights.push(lightEntry.object);
					}
				}

				return true;
			}
		},

		handleExtension: {
			value: function(entryID, description, userInfo) {

				// console.log("Extension", entryID, description);

				switch (entryID) {
					case 'KHR_materials_common' :
						var lights = description.lights;
						for (lightID in lights) {
							var light = lights[lightID];
							this.handleLight(lightID, light);
						}
						break;
				}

				return true;
			}
		},

		buildNodeHirerachy: {
			value: function(nodeEntryId, parentThreeNode) {
				var nodeEntry = this.resources.getEntry(nodeEntryId);
				var threeNode = nodeEntry.object;
				parentThreeNode.add(threeNode);

				var children = nodeEntry.description.children;
				if (children) {
					children.forEach( function(childID) {
						this.buildNodeHirerachy(childID, threeNode);
					}, this);
				}

				return threeNode;
			}
		},

		buildSkin: {
			value: function(node) {

				var glTF = node.glTF;
				var skin = glTF.instanceSkin;
				var skeletons = glTF.skeletons;
				if (skin) {
					skeletons.forEach(function(skeleton) {
						var nodeEntry = this.resources.getEntry(skeleton);
						if (nodeEntry) {

							var rootSkeleton = nodeEntry.object;
							node.add(rootSkeleton);

							var dobones = true;

							for (meshID in glTF.meshInstances) {
								var mesh = glTF.meshInstances[meshID];
								var threeMesh = null;
								mesh.primitives.forEach(function(primitive) {

									var material = primitive.material;
									var materialParams = material.params;
									if (!(material instanceof THREE.Material)) {
										material = createShaderMaterial(material);
									}

									threeMesh = new THREE.SkinnedMesh(primitive.geometry.geometry, material, false);

									var geometry = primitive.geometry.geometry;
									var j;
/*                                    if (geometry.vertices) {
										for ( j = 0; j < geometry.vertices.length; j ++ ) {
											geometry.vertices[j].applyMatrix4( skin.bindShapeMatrix );
										}
									}
									else if (geometry.attributes.position) {
										var a = geometry.attributes.position.array;
										var v = new THREE.Vector3;
										for ( j = 0; j < a.length / 3; j++ ) {
											v.set(a[j * 3], a[j * 3 + 1], a[j * 3 + 2]);
											v.applyMatrix4( skin.bindShapeMatrix );
											a[j * 3] = v.x;
											a[j * 3 + 1] = v.y;
											a[j * 3 + 2] = v.z;
										}
									}*/

									if (threeMesh && dobones) {

										material.skinning = true;

										var jointNames = skin.jointNames;
										var joints = [];
										var bones = [];
										var boneInverses = [];
										var i, len = jointNames.length;
										for (i = 0; i < len; i++) {
											var jointName = jointNames[i];
											var nodeForJoint = this.joints[jointName];
											var joint = this.resources.getEntry(nodeForJoint).object;
											if (joint) {

												joint.skin = threeMesh;
												joints.push(joint);
												bones.push(joint);

												var m = skin.inverseBindMatrices;
												var mat = new THREE.Matrix4().set(
														m[i * 16 + 0],  m[i * 16 + 4],  m[i * 16 + 8],  m[i * 16 + 12],
														m[i * 16 + 1],  m[i * 16 + 5],  m[i * 16 + 9],  m[i * 16 + 13],
														m[i * 16 + 2],  m[i * 16 + 6],  m[i * 16 + 10], m[i * 16 + 14],
														m[i * 16 + 3],  m[i * 16 + 7],  m[i * 16 + 11], m[i * 16 + 15]
													);
												boneInverses.push(mat);

											} else {
												console.log("WARNING: jointName:"+jointName+" cannot be found in skeleton:"+skeleton);
											}
										}

										threeMesh.bind( new THREE.Skeleton( bones,
											boneInverses, false ), skin.bindShapeMatrix );

										//threeMesh.bindMode = "detached";
										//threeMesh.normalizeSkinWeights();
										//threeMesh.pose();
									}

									if (threeMesh) {
										threeMesh.castShadow = true;
										node.add(threeMesh);

										if (material instanceof THREE.ShaderMaterial) {
											materialParams.joints = joints;
											var glTFShader = new THREE.glTFShader(material, materialParams, threeMesh, theLoader.rootObj);
											THREE.glTFShaders.add(glTFShader);

										}
									}

								}, this);
							}

						}


					}, this);

				}
			}
		},

		buildSkins: {
			value: function(node) {

				if (node.glTF && node.glTF.instanceSkin)
					this.buildSkin(node);

				var children = node.children;
				if (children) {
					children.forEach( function(child) {
						this.buildSkins(child);
					}, this);
				}
			}
		},

		createMeshAnimations : {
			value : function(root) {
					this.buildSkins(root);
				}
		},

		handleScene: {
			value: function(entryID, description, userInfo) {

				if (!description.nodes) {
					console.log("ERROR: invalid file required nodes property is missing from scene");
					return false;
				}

				description.nodes.forEach( function(nodeUID) {
					this.buildNodeHirerachy(nodeUID, userInfo.rootObj);
				}, this);

				if (this.delegate) {
					this.delegate.loadCompleted(userInfo.callback, userInfo.rootObj);
				}

				theLoader.loadAllAssets();

				return true;
			}
		},

		handleImage: {
			value: function(entryID, description, userInfo) {
				this.resources.setEntry(entryID, null, description);
				return true;
			}
		},

		addNodeAnimationChannel : {
			value : function(name, channel, interp) {
				if (!this.nodeAnimationChannels)
					this.nodeAnimationChannels = {};

				if (!this.nodeAnimationChannels[name]) {
					this.nodeAnimationChannels[name] = [];
				}

				this.nodeAnimationChannels[name].push(interp);
			},
		},

		createAnimations : {
			value : function() {
				for (var name in this.nodeAnimationChannels) {
					var nodeAnimationChannels = this.nodeAnimationChannels[name];
					var i, len = nodeAnimationChannels.length;
					//console.log(" animation channels for node " + name);
					//for (i = 0; i < len; i++) {
					//  console.log(nodeAnimationChannels[i]);
					//}
					var anim = new THREE.glTFAnimation(nodeAnimationChannels);
					anim.name = "animation_" + name;
					this.animations.push(anim);
				}
			}
		},

		buildAnimation: {
			value : function(animation) {

				var interps = [];
				var i, len = animation.channels.length;
				for (i = 0; i < len; i++) {

					var channel = animation.channels[i];
					var sampler = animation.samplers[channel.sampler];
					if (sampler) {

						var input = animation.parameters[sampler.input];
						if (input && input.data) {

							var output = animation.parameters[sampler.output];
							if (output && output.data) {

								var target = channel.target;
								var node = this.resources.getEntry(target.id);
								if (node) {

									var path = target.path;

									var interp = {
											keys : input.data,
											values : output.data,
											count : input.count,
											target : node.object,
											path : path,
											type : sampler.interpolation
									};

									this.addNodeAnimationChannel(target.id, channel, interp);
									interps.push(interp);
								}
							}
						}
					}
				}
			}
		},

		handleAnimation: {
			value: function(entryID, description, userInfo) {

				var self = this;
				theLoader.animationsRequested++;
				var animation = new Animation();
				animation.name = entryID;
				animation.onload = function() {
					// self.buildAnimation(animation);
					theLoader.animationsLoaded++;
					theLoader.animations.push(animation);
					theLoader.checkComplete();
				};

				animation.channels = description.channels;
				animation.samplers = description.samplers;
				this.resources.setEntry(entryID, animation, description);
				var parameters = description.parameters;
				if (!parameters) {
					//FIXME: not implemented in delegate
					console.log("MISSING_PARAMETERS for animation:"+ entryID);
					return false;
				}

				// Load parameter buffers
				var params = Object.keys(parameters);
				params.forEach( function(param) {

					// async help
					animation.totalParameters++;

				}, this);

				var params = Object.keys(parameters);
				params.forEach( function(param) {

					var parameter = parameters[param];
					var accessor = this.resources.getEntry(parameter);
					if (!accessor)
						debugger;
					accessor = accessor.object;
					var bufferView = this.resources.getEntry(accessor.bufferView);
					var paramObject = {
							bufferView : bufferView,
							byteOffset : accessor.byteOffset,
							count : accessor.count,
							componentType : accessor.componentType,
							type : accessor.type,
							id : accessor.bufferView,
							name : param
					};

					var paramContext = new AnimationParameterContext(paramObject, animation);

					var loaddata = {
						paramObject : paramObject,
						animationParameterDelegate : animationParameterDelegate,
						paramContext : paramContext
					};

					theLoader.scheduleLoad(function(data) {

						var alreadyProcessedAttribute =
							THREE.GLTFLoaderUtils.getBuffer(data.paramObject,
								data.animationParameterDelegate, data.paramContext);

						if (alreadyProcessedAttribute) {
							data.animationParameterDelegate.resourceAvailable(
								alreadyProcessedAttribute, data.paramContext);
						}

					}, loaddata);


				}, this);

				return true;
			}
		},

		handleAccessor: {
			value: function(entryID, description, userInfo) {
				// Save attribute entry
				this.resources.setEntry(entryID, description, description);
				return true;
			}
		},

		handleSkin: {
			value: function(entryID, description, userInfo) {
				// Save skin entry

				var skin = {
				};

				var m = description.bindShapeMatrix;
				skin.bindShapeMatrix = new THREE.Matrix4().set(
						m[0],  m[4],  m[8],  m[12],
						m[1],  m[5],  m[9],  m[13],
						m[2],  m[6],  m[10], m[14],
						m[3],  m[7],  m[11], m[15]
					);

				skin.jointNames = description.jointNames;
				var inverseBindMatricesDescription = this.resources.getEntry(description.inverseBindMatrices);
				inverseBindMatricesDescription = inverseBindMatricesDescription.description;
				skin.inverseBindMatricesDescription = inverseBindMatricesDescription;
				skin.inverseBindMatricesDescription.id = description.inverseBindMatrices;

				var bufferEntry = this.resources.getEntry(inverseBindMatricesDescription.bufferView);

				var paramObject = {
						bufferView : bufferEntry,
						byteOffset : inverseBindMatricesDescription.byteOffset,
						count : inverseBindMatricesDescription.count,
						componentType : inverseBindMatricesDescription.componentType,
						type : inverseBindMatricesDescription.type,
						id : inverseBindMatricesDescription.bufferView,
						name : skin.inverseBindMatricesDescription.id
				};

				var context = new InverseBindMatricesContext(paramObject, skin);

				var loaddata = {
					paramObject : paramObject,
					inverseBindMatricesDelegate : inverseBindMatricesDelegate,
					context : context
				};

				theLoader.scheduleLoad(function(data) {

					var alreadyProcessedAttribute =
						THREE.GLTFLoaderUtils.getBuffer(data.paramObject,
							data.inverseBindMatricesDelegate, data.context);

					if (alreadyProcessedAttribute) {
						data.inverseBindMatricesDelegate.resourceAvailable(
							alreadyProcessedAttribute, data.context);
					}

				}, loaddata);



				var bufferView = this.resources.getEntry(skin.inverseBindMatricesDescription.bufferView);
				skin.inverseBindMatricesDescription.bufferView =
					bufferView.object;
				this.resources.setEntry(entryID, skin, description);
				return true;
			}
		},

		handleSampler: {
			value: function(entryID, description, userInfo) {
				// Save attribute entry
				this.resources.setEntry(entryID, description, description);
				return true;
			}
		},

		handleTexture: {
			value: function(entryID, description, userInfo) {
				// Save attribute entry
				this.resources.setEntry(entryID, null, description);
				return true;
			}
		},

		handleError: {
			value: function(msg) {

				throw new Error(msg);
				return true;
			}
		},

		_delegate: {
			value: new LoadDelegate,
			writable: true
		},

		delegate: {
			enumerable: true,
			get: function() {
				return this._delegate;
			},
			set: function(value) {
				this._delegate = value;
			}
		}
	});


	// Loader

	var Context = function(rootObj, callback) {
		this.rootObj = rootObj;
		this.callback = callback;
	};

	var rootObj = new THREE.Object3D();

	var self = this;

	var loader = Object.create(ThreeGLTFLoader);
	loader.initWithPath(url);
	loader.load(new Context(rootObj,
						function(obj) {
						}),
				null);

	this.loader = loader;
	this.callback = callback;
	this.rootObj = rootObj;
	return rootObj;
}


THREE.glTFLoader.prototype.scheduleLoad = function(loadFn, data) {

	this.loadRequests.push({fn: loadFn, data:data});
}

THREE.glTFLoader.prototype.loadAllAssets = function() {

	for (var i = 0, len = this.loadRequests.length; i < len; i++) {
		var request = this.loadRequests[i];
		request.fn(request.data);
	}
}

THREE.glTFLoader.prototype.callLoadedCallback = function() {
	var result = {
			scene : this.rootObj,
			cameras : this.loader.cameras,
			animations : this.loader.animations,
			shaders : this.loader.shaders,
	};

	this.callback(result);
}

THREE.glTFLoader.prototype.checkComplete = function() {
	if (this.meshesLoaded == this.meshesRequested
			&& this.shadersLoaded == this.shadersRequested
			&& this.animationsLoaded == this.animationsRequested)
	{
		for (var i = 0; i < this.pendingMeshes.length; i++) {
			var pending = this.pendingMeshes[i];
			pending.mesh.attachToNode(pending.node);
		}

		for (var i = 0; i < this.animationsLoaded; i++) {
			var animation = this.animations[i];
			this.loader.buildAnimation(animation);
		}

		this.loader.createAnimations();
		this.loader.createMeshAnimations(this.rootObj);
		THREE.glTFShaders.bindShaderParameters(this.rootObj);

		this.callLoadedCallback();
	}
}
