Răsfoiți Sursa

Add loader for .obj files with textures, and newer version of UTF8 loader.

Fix cloneObject to also copy source.useVertexTexture for SkinnedMesh.
Angel Xuan Chang 13 ani în urmă
părinte
comite
5b2d8fbe16

+ 311 - 0
examples/js/loaders/MTLLoader.js

@@ -0,0 +1,311 @@
+/**
+ * Loads a Wavefront .mtl file specifying materials
+ *
+ * @author angelxuanchang
+ */
+
+THREE.MTLLoader = function(baseUrl, options) {
+    THREE.EventTarget.call( this );
+    this.baseUrl = baseUrl;
+    this.options = options;
+};
+
+THREE.MTLLoader.prototype = {
+
+    /**
+     * Loads a MTL file
+     *
+     * Loading progress is indicated by the following events:
+     *   "load" event (successful loading): type = 'load', content = THREE.MTLLoader.MaterialCreator
+     *   "error" event (error loading): type = 'load', message
+     *   "progress" event (progress loading): type = 'progress', loaded, total
+     *
+     * @param url - location of MTL file
+     */
+    load: function(url) {
+        var scope = this;
+        var xhr = new XMLHttpRequest();
+
+        function onloaded(event) {
+            if (event.target.status === 200 || event.target.status === 0) {
+                var materialCreator = scope.parse(event.target.responseText);
+                // Notify caller, that I'm done
+                scope.dispatchEvent( { type: 'load', content: materialCreator } );
+            } else {
+                scope.dispatchEvent( { type: 'error', message: 'Couldn\'t load URL [' + url + ']',
+                    response: event.target.responseText } );
+            }
+        }
+
+        xhr.addEventListener( 'load', onloaded, false );
+
+        xhr.addEventListener( 'progress', function ( event ) {
+            scope.dispatchEvent( { type: 'progress', loaded: event.loaded, total: event.total } );
+        }, false );
+
+        xhr.addEventListener( 'error', function () {
+            scope.dispatchEvent( { type: 'error', message: 'Couldn\'t load URL [' + url + ']' } );
+        }, false );
+
+        xhr.open( 'GET', url, true );
+        xhr.send( null );
+    },
+
+    /**
+     * Parses loaded MTL file
+     * @param text - Content of MTL file
+     * @return {THREE.MTLLoader.MaterialCreator}
+     */
+    parse: function(text) {
+        var lines = text.split("\n");
+        var info = {};
+        var delimiter_pattern = /\s+/;
+        var materialsInfo = {};
+        for (var i = 0; i < lines.length; i++) {
+            var line = lines[i];
+            line = $.trim(line);
+            if (line.length == 0 || line.charAt(0) == '#') {
+                // Blank line or comment ignore
+                continue;
+            }
+
+            var pos = line.indexOf(' ');
+            var key = (pos >= 0)? line.substring(0,pos):line;
+            key = key.toLowerCase();
+            var value = (pos >= 0)? line.substring(pos+1):"";
+            value = $.trim(value);
+            if (key === "newmtl") {
+                // New material
+                info = { name: value };
+                materialsInfo[value] = info;
+            } else if (info) {
+                if (key === "ka" || key === "kd" || key === "ks") {
+                    var ss = value.split(delimiter_pattern, 3);
+                    info[key] = [ parseFloat(ss[0]), parseFloat(ss[1]), parseFloat(ss[2])];
+                } else {
+                    info[key] = value;
+                }
+            }
+        }
+        var materialCreator = new THREE.MTLLoader.MaterialCreator(this.baseUrl, this.options);
+        materialCreator.setMaterials(materialsInfo);
+        return materialCreator;
+    }
+};
+
+/**
+ * Create a new THREE-MTLLoader.MaterialCreator
+ * @param baseUrl - Url relative to which textures are loaded
+ * @param options - Set of options on how to construct the materials
+ *                  side: Which side to apply the material
+ *                        THREE.FrontSide (default), THREE.BackSide, THREE.DoubleSide
+ *                  wrap: What type of wrapping to apply for textures
+ *                        THREE.RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping
+ *                  normalizeRGB: RGBs need to be normalized to 0-1 from 0-255
+ *                                Default: false, assumed to be already normalized
+ *                  ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's
+ *                                  Default: false
+ *                  invertTransparency: If transparency need to be inverted (inversion is needed if d = 0 is fully opaque)
+ *                                      Default: false (d = 1 is fully opaque)
+ * @constructor
+ */
+THREE.MTLLoader.MaterialCreator = function(baseUrl, options) {
+    THREE.EventTarget.call( this );
+    this.baseUrl = baseUrl;
+    this.options = options;
+    this.materialsInfo = {};
+    this.materials = {};
+    this.materialsArray = [];
+    this.nameLookup = {};
+
+    this.side = (this.options && this.options.side)? this.options.side: THREE.FrontSide;
+    this.wrap = (this.options && this.options.wrap)? this.options.wrap: THREE.RepeatWrapping;
+};
+
+THREE.MTLLoader.MaterialCreator.prototype = {
+    setMaterials: function(materialsInfo) {
+        this.materialsInfo = this.convert(materialsInfo);
+        this.materials = {};
+        this.materialsArray = [];
+        this.nameLookup = {};
+    },
+
+    convert: function(materialsInfo) {
+        if (!this.options) return materialsInfo;
+        var converted = {};
+        for (var mn in materialsInfo) {
+            // Convert materials info into normalized form based on options
+            var mat = materialsInfo[mn];
+            var covmat = {};
+            converted[mn] = covmat;
+            for (var prop in mat) {
+                var save = true;
+                var value = mat[prop];
+                var lprop = prop.toLowerCase();
+                switch (lprop) {
+                    case 'kd':
+                    case 'ka':
+                    case 'ks':
+                        // Diffuse color (color under white light) using RGB values
+                        if (this.options && this.options.normalizeRGB) {
+                            value =  [ value[0]/255, value[1]/255, value[2]/255 ];
+                        }
+                        if (this.options && this.options.ignoreZeroRGBs) {
+                            if (value[0] === 0.0 && value[1] === 0.0 && value[1] === 0.0) {
+                                // ignore
+                                save = false;
+                            }
+                        }
+                        break;
+                    case 'd':
+                        // According to MTL format (http://paulbourke.net/dataformats/mtl/):
+                        //   d is dissolve for current material
+                        //   factor of 1.0 is fully opaque, a factor of 0 is fully dissolved (completely transparent)
+                        if (this.options && this.options.invertTransparency) {
+                            value = 1 - value;
+                        }
+                        break;
+                    default:
+                        break;
+                }
+                if (save) {
+                    covmat[lprop] = value;
+                }
+            }
+        }
+        return converted;
+    },
+
+    preload: function () {
+        for (var mn in this.materialsInfo) {
+            this.create(mn);
+        }
+    },
+
+    getIndex: function(materialName) {
+        return this.nameLookup[materialName];
+    },
+
+    getAsArray: function() {
+        var index = 0;
+        for (var mn in this.materialsInfo) {
+            this.materialsArray[index] = this.create(mn);
+            this.nameLookup[mn] = index;
+            index++;
+        }
+        return this.materialsArray;
+    },
+
+    create: function (materialName) {
+        if (this.materials[materialName] == undefined) {
+            this.createMaterial_(materialName);
+        }
+        return this.materials[materialName];
+    },
+
+    createMaterial_: function (materialName) {
+        // Create material
+        var mat = this.materialsInfo[materialName];
+        var params = {
+            name: materialName,
+            side: this.side
+        };
+        for (var prop in mat) {
+            var value = mat[prop];
+            switch (prop.toLowerCase()) {
+                // Ns is material specular exponent
+                case 'kd':
+                    // Diffuse color (color under white light) using RGB values
+                    params['diffuse'] = new THREE.Color().setRGB(value[0], value[1], value[2]);
+                    break;
+                case 'ka':
+                    // Ambient color (color under shadow) using RGB values
+                    params['ambient'] = new THREE.Color().setRGB(value[0], value[1], value[2]);
+                    break;
+                case 'ks':
+                    // specular color (color when light is reflected from shiny surface) using RGB values
+                    params['specular'] = new THREE.Color().setRGB(value[0], value[1], value[2]);
+                    break;
+                case 'map_kd':
+                    // Diffuse texture map
+                    params['map'] = THREE.MTLLoader.loadTexture( this.baseUrl + value );
+                    params['map'].wrapS = this.wrap;
+                    params['map'].wrapT = this.wrap;
+                    break;
+                case 'Ns':
+                    // The specular exponent (defines the focus of the specular highlight)
+                    // A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000.
+                    params['shininess'] = value;
+                    break;
+                case 'd':
+                    // According to MTL format (http://paulbourke.net/dataformats/mtl/):
+                    //   d is dissolve for current material
+                    //   factor of 1.0 is fully opaque, a factor of 0 is fully dissolved (completely transparent)
+                    if (value < 1) {
+                        params['transparent'] = true;
+                        params['opacity'] = value;
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+        if (params['diffuse']) {
+            if (!params['ambient']) params['ambient'] = params['diffuse'];
+            params['color'] = params['diffuse'];
+        }
+        this.materials[materialName] = new THREE.MeshPhongMaterial(params);
+        return this.materials[materialName];
+    }
+};
+
+THREE.MTLLoader.loadTexture = function ( url, mapping, onLoad, onError ) {
+        var image = new Image();
+        var texture = new THREE.Texture( image, mapping );
+
+        var loader = new THREE.ImageLoader();
+
+        loader.addEventListener( 'load', function ( event ) {
+            texture.image = THREE.MTLLoader.ensurePowerOfTwo_(event.content);
+            texture.needsUpdate = true;
+            if ( onLoad ) onLoad( texture );
+        } );
+
+        loader.addEventListener( 'error', function ( event ) {
+            if ( onError ) onError( event.message );
+        } );
+
+        loader.crossOrigin = this.crossOrigin;
+        loader.load( url, image );
+
+        return texture;
+    };
+
+THREE.MTLLoader.ensurePowerOfTwo_ = function (image)
+{
+    if (!THREE.MTLLoader.isPowerOfTwo_(image.width) || !THREE.MTLLoader.isPowerOfTwo_(image.height))
+    {
+        var canvas = document.createElement("canvas");
+        canvas.width = THREE.MTLLoader.nextHighestPowerOfTwo_(image.width);
+        canvas.height = THREE.MTLLoader.nextHighestPowerOfTwo_(image.height);
+        var ctx = canvas.getContext("2d");
+        ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);
+        return canvas;
+    }
+    return image;
+};
+
+THREE.MTLLoader.isPowerOfTwo_ = function (x)
+{
+    return (x & (x - 1)) == 0;
+};
+
+THREE.MTLLoader.nextHighestPowerOfTwo_ = function(x)
+{
+    --x;
+    for (var i = 1; i < 32; i <<= 1) {
+        x = x | x >> i;
+    }
+    return x + 1;
+};
+

+ 360 - 0
examples/js/loaders/OBJMTLLoader.js

@@ -0,0 +1,360 @@
+/**
+ * Loads a Wavefront .obj file with materials
+ *
+ * @author mrdoob / http://mrdoob.com/
+ * @author angelxuanchang
+ */
+
+THREE.OBJMTLLoader = function ( ) {
+	THREE.EventTarget.call( this );
+};
+
+THREE.OBJMTLLoader.prototype = {
+
+	constructor: THREE.OBJMTLLoader,
+
+    /**
+     * Load a Wavefront OBJ file with materials (MTL file)
+     *
+     * Loading progress is indicated by the following events:
+     *   "load" event (successful loading): type = 'load', content = THREE.Object3D
+     *   "error" event (error loading): type = 'load', message
+     *   "progress" event (progress loading): type = 'progress', loaded, total
+     *
+     * If the MTL file cannot be loaded, then a MeshLambertMaterial is used as a default
+     * @param url - Location of OBJ file to load
+     * @param mtlfileurl - MTL file to load (optional, if not specified, attempts to use MTL specified in OBJ file)
+     * @param options - Options on how to interpret the material (see THREE.MTLLoader.MaterialCreator )
+     *
+     */
+	load: function ( url, mtlfileurl, options ) {
+
+		var scope = this;
+		var xhr = new XMLHttpRequest();
+
+        var mtlDone;  // Is the MTL done (true if no MTL, error loading MTL, or MTL actually loaded)
+        var obj3d;    // Loaded model (from obj file)
+        var materialsCreator;  // Material creator is created when MTL file is loaded
+
+        // Loader for MTL
+        var mtlLoader = new THREE.MTLLoader(url.substr(0,url.lastIndexOf("/")+1), options);
+        mtlLoader.addEventListener( 'load', waitReady );
+        mtlLoader.addEventListener( 'error', waitReady );
+        // Try to load mtlfile
+        if (mtlfileurl) {
+            mtlLoader.load(mtlfileurl);
+            mtlDone = false;
+        } else {
+            mtlDone = true;
+        }
+
+        function waitReady(event) {
+            if (event.type === 'load') {
+                if (event.content instanceof THREE.MTLLoader.MaterialCreator) {
+                    // MTL file is loaded
+                    mtlDone = true;
+                    materialsCreator = event.content;
+                    materialsCreator.preload();
+                } else {
+                    // OBJ file is loaded
+                    if (event.target.status == 200 || event.target.status == 0) {
+                        var objContent = event.target.responseText;
+                        if (mtlfileurl) {
+                            // Parse with passed in MTL file
+                            obj3d = scope.parse(objContent);
+                        } else {
+                            // No passed in MTL file, look for mtlfile in obj file
+                            obj3d = scope.parse(objContent, function(mtlfile) {
+                                mtlDone = false;
+                                mtlLoader.load(mtlLoader.baseUrl + mtlfile);
+                            });
+                        }
+                    } else {
+                        // Error loading OBJ file....
+                        scope.dispatchEvent( {
+                            type: 'error',
+                            message: 'Couldn\'t load URL [' + url + ']',
+                            response: event.target.responseText } );
+                    }
+                }
+            } else if (event.type === 'error') {
+                // MTL failed to load -- oh well, we will just not have material...
+                mtlDone = true;
+            }
+            if (mtlDone && obj3d) {
+                // MTL file is loaded and OBJ file is loaded
+                // Apply materials to model
+                if (materialsCreator) {
+                    THREE.SceneUtils.traverseHierarchy(
+                        obj3d, function(node) {
+                            if (node instanceof THREE.Mesh) {
+                                if (node.material.name) {
+                                    var material = materialsCreator.create(node.material.name);
+                                    if (material) node.material = material;
+                                }
+                            }
+                        }
+                    )
+                }
+                // Notify listeners
+                scope.dispatchEvent( { type: 'load', content: obj3d } );
+            }
+        }
+
+        xhr.addEventListener( 'load', waitReady, false );
+
+		xhr.addEventListener( 'progress', function ( event ) {
+			scope.dispatchEvent( { type: 'progress', loaded: event.loaded, total: event.total } );
+		}, false );
+
+		xhr.addEventListener( 'error', function () {
+			scope.dispatchEvent( { type: 'error', message: 'Couldn\'t load URL [' + url + ']' } );
+		}, false );
+
+		xhr.open( 'GET', url, true );
+		xhr.send( null );
+
+    },
+
+    /**
+     * Parses loaded .obj file
+     * @param data - content of .obj file
+     * @param mtllibCallback - callback to handle mtllib declaration (optional)
+     * @return {THREE.Object3D} - Object3D (with default material)
+     */
+	parse: function ( data, mtllibCallback ) {
+
+        function vector( x, y, z ) {
+			return new THREE.Vector3( x, y, z );
+		}
+
+		function uv( u, v ) {
+			return new THREE.UV( u, v );
+		}
+
+		function face3( a, b, c, normals ) {
+			return new THREE.Face3( a, b, c, normals );
+		}
+
+		function face4( a, b, c, d, normals ) {
+			return new THREE.Face4( a, b, c, d, normals );
+		}
+
+        function finalize_mesh( group, mesh_info ) {
+            mesh_info.geometry.computeCentroids();
+            mesh_info.geometry.computeFaceNormals();
+            mesh_info.geometry.computeBoundingSphere();
+            group.add( new THREE.Mesh( mesh_info.geometry, mesh_info.material ) );
+        }
+
+		var vertices = [];
+		var normals = [];
+		var uvs = [];
+
+        // v float float float
+        var vertex_pattern = /v( +[\d|\.|\+|\-|e]+)( [\d|\.|\+|\-|e]+)( [\d|\.|\+|\-|e]+)/;
+        // vn float float float
+        var normal_pattern = /vn( +[\d|\.|\+|\-|e]+)( [\d|\.|\+|\-|e]+)( [\d|\.|\+|\-|e]+)/;
+        // vt float float
+        var uv_pattern = /vt( +[\d|\.|\+|\-|e]+)( [\d|\.|\+|\-|e]+)/;
+        // f vertex vertex vertex ...
+        var face_pattern1 = /f( +[\d]+)( [\d]+)( [\d]+)( [\d]+)?/;
+        // f vertex/uv vertex/uv vertex/uv ...
+        var face_pattern2 = /f( +([\d]+)\/([\d]+))( ([\d]+)\/([\d]+))( ([\d]+)\/([\d]+))( ([\d]+)\/([\d]+))?/;
+        // f vertex/uv/normal vertex/uv/normal vertex/uv/normal ...
+        var face_pattern3 = /f( +([\d]+)\/([\d]+)\/([\d]+))( ([\d]+)\/([\d]+)\/([\d]+))( ([\d]+)\/([\d]+)\/([\d]+))( ([\d]+)\/([\d]+)\/([\d]+))?/;
+        // f vertex//normal vertex//normal vertex//normal ...
+        var face_pattern4 = /f( +([\d]+)\/\/([\d]+))( ([\d]+)\/\/([\d]+))( ([\d]+)\/\/([\d]+))( ([\d]+)\/\/([\d]+))?/;
+
+        var final_model = new THREE.Object3D();
+        var geometry = new THREE.Geometry();
+        geometry.vertices = vertices;
+        var cur_mesh = {
+            material: new THREE.MeshLambertMaterial(),
+            geometry: geometry
+        };
+        var lines = data.split("\n");
+        for (var i = 0; i < lines.length; i++) {
+            var line = lines[i];
+            line = $.trim(line);
+            // temporary variable storing pattern matching result
+            var result;
+            if (line.length === 0 || line.charAt(0) === '#') {
+                continue;
+            } else if ((result = vertex_pattern.exec(line)) != null) {
+                // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"]
+                vertices.push( vector(
+                    parseFloat( result[ 1 ] ),
+                    parseFloat( result[ 2 ] ),
+                    parseFloat( result[ 3 ] )
+                ) );
+            } else if ((result = normal_pattern.exec(line)) != null) {
+                // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"]
+                normals.push( vector(
+                    parseFloat( result[ 1 ] ),
+                    parseFloat( result[ 2 ] ),
+                    parseFloat( result[ 3 ] )
+                ) );
+            } else if (( result = uv_pattern.exec(line)) != null) {
+                // ["vt 0.1 0.2", "0.1", "0.2"]
+                uvs.push( uv(
+                    parseFloat( result[ 1 ] ),
+                    parseFloat( result[ 2 ] )
+                ) );
+            } else if (( result = face_pattern1.exec( line ) ) != null ) {
+                // ["f 1 2 3", "1", "2", "3", undefined]
+                if ( result[ 4 ] === undefined ) {
+                    geometry.faces.push( face3(
+                        parseInt( result[ 1 ] ) - 1,
+                        parseInt( result[ 2 ] ) - 1,
+                        parseInt( result[ 3 ] ) - 1
+                    ) );
+                } else {
+                    geometry.faces.push( face4(
+                        parseInt( result[ 1 ] ) - 1,
+                        parseInt( result[ 2 ] ) - 1,
+                        parseInt( result[ 3 ] ) - 1,
+                        parseInt( result[ 4 ] ) - 1
+                    ) );
+                }
+            } else if ( ( result = face_pattern2.exec( line ) ) != null ) {
+                // ["f 1/1 2/2 3/3", " 1/1", "1", "1", " 2/2", "2", "2", " 3/3", "3", "3", undefined, undefined, undefined]
+                if ( result[ 10 ] === undefined ) {
+                    geometry.faces.push( face3(
+                        parseInt( result[ 2 ] ) - 1,
+                        parseInt( result[ 5 ] ) - 1,
+                        parseInt( result[ 8 ] ) - 1
+                    ) );
+                    geometry.faceVertexUvs[ 0 ].push( [
+                        uvs[ parseInt( result[ 3 ] ) - 1 ],
+                        uvs[ parseInt( result[ 6 ] ) - 1 ],
+                        uvs[ parseInt( result[ 9 ] ) - 1 ]
+                    ] );
+
+                } else {
+                    geometry.faces.push( face4(
+                        parseInt( result[ 2 ] ) - 1,
+                        parseInt( result[ 5 ] ) - 1,
+                        parseInt( result[ 8 ] ) - 1,
+                        parseInt( result[ 11 ] ) - 1
+                    ) );
+                    geometry.faceVertexUvs[ 0 ].push( [
+                        uvs[ parseInt( result[ 3 ] ) - 1 ],
+                        uvs[ parseInt( result[ 6 ] ) - 1 ],
+                        uvs[ parseInt( result[ 9 ] ) - 1 ],
+                        uvs[ parseInt( result[ 12 ] ) - 1 ]
+                    ] );
+                }
+            } else if ( ( result = face_pattern3.exec( line ) ) != null ) {
+                // ["f 1/1/1 2/2/2 3/3/3", " 1/1/1", "1", "1", "1", " 2/2/2", "2", "2", "2", " 3/3/3", "3", "3", "3", undefined, undefined, undefined, undefined]
+                if ( result[ 13 ] === undefined ) {
+                    geometry.faces.push( face3(
+                        parseInt( result[ 2 ] ) - 1,
+                        parseInt( result[ 6 ] ) - 1,
+                        parseInt( result[ 10 ] ) - 1,
+                        [
+                            normals[ parseInt( result[ 4 ] ) - 1 ],
+                            normals[ parseInt( result[ 8 ] ) - 1 ],
+                            normals[ parseInt( result[ 12 ] ) - 1 ]
+                        ]
+                    ) );
+                    geometry.faceVertexUvs[ 0 ].push( [
+                        uvs[ parseInt( result[ 3 ] ) - 1 ],
+                        uvs[ parseInt( result[ 7 ] ) - 1 ],
+                        uvs[ parseInt( result[ 11 ] ) - 1 ]
+                    ] );
+                } else {
+                    geometry.faces.push( face4(
+                        parseInt( result[ 2 ] ) - 1,
+                        parseInt( result[ 6 ] ) - 1,
+                        parseInt( result[ 10 ] ) - 1,
+                        parseInt( result[ 14 ] ) - 1,
+                        [
+                            normals[ parseInt( result[ 4 ] ) - 1 ],
+                            normals[ parseInt( result[ 8 ] ) - 1 ],
+                            normals[ parseInt( result[ 12 ] ) - 1 ],
+                            normals[ parseInt( result[ 16 ] ) - 1 ]
+                        ]
+                    ) );
+                    geometry.faceVertexUvs[ 0 ].push( [
+                        uvs[ parseInt( result[ 3 ] ) - 1 ],
+                        uvs[ parseInt( result[ 7 ] ) - 1 ],
+                        uvs[ parseInt( result[ 11 ] ) - 1 ],
+                        uvs[ parseInt( result[ 15 ] ) - 1 ]
+                    ] );
+
+                }
+            } else if ( ( result = face_pattern4.exec( line ) ) != null ) {
+                // ["f 1//1 2//2 3//3", " 1//1", "1", "1", " 2//2", "2", "2", " 3//3", "3", "3", undefined, undefined, undefined]
+                if ( result[ 10 ] === undefined ) {
+                    geometry.faces.push( face3(
+                        parseInt( result[ 2 ] ) - 1,
+                        parseInt( result[ 5 ] ) - 1,
+                        parseInt( result[ 8 ] ) - 1,
+                        [
+                            normals[ parseInt( result[ 3 ] ) - 1 ],
+                            normals[ parseInt( result[ 6 ] ) - 1 ],
+                            normals[ parseInt( result[ 9 ] ) - 1 ]
+                        ]
+                    ) );
+                } else {
+                    geometry.faces.push( face4(
+                        parseInt( result[ 2 ] ) - 1,
+                        parseInt( result[ 5 ] ) - 1,
+                        parseInt( result[ 8 ] ) - 1,
+                        parseInt( result[ 11 ] ) - 1,
+                        [
+                            normals[ parseInt( result[ 3 ] ) - 1 ],
+                            normals[ parseInt( result[ 6 ] ) - 1 ],
+                            normals[ parseInt( result[ 9 ] ) - 1 ],
+                            normals[ parseInt( result[ 12 ] ) - 1 ]
+                        ]
+                    ) );
+                }
+            } else if (line.startsWith("usemtl ")) {
+                var material_name = line.substring(7);
+                material_name = $.trim(material_name);
+                var material = new THREE.MeshLambertMaterial();
+                material.name = material_name;
+                if (geometry.faces.length > 0) {
+                    // Finalize previous geometry and add to model
+                    finalize_mesh(final_model, cur_mesh);
+                    geometry = new THREE.Geometry();
+                    geometry.vertices = vertices;
+                    cur_mesh = {
+                        geometry: geometry
+                    };
+                }
+                cur_mesh.material = material;
+                //material_index = materialsCreator.getIndex(material_name);
+            } else if (line.startsWith("g ")) {
+                // Polygon group for object
+                var group_name = line.substring(2);
+                group_name = $.trim(group_name);
+            } else if (line.startsWith("o ")) {
+                // Object
+                var object_name = line.substring(2);
+                object_name = $.trim(object_name);
+            } else if (line.startsWith("s ")) {
+                // Smooth shading
+            } else if (line.startsWith("mtllib ")) {
+                // mtl file
+                if (mtllibCallback) {
+                    var mtlfile = line.substring(7);
+                    mtlfile = $.trim(mtlfile);
+                    mtllibCallback(mtlfile);
+                }
+            } else {
+                console.error("Unhandled line " + line);
+            }
+        }
+        finalize_mesh(final_model, cur_mesh);
+		return final_model;
+	}
+};
+
+if (typeof String.prototype.startsWith != 'function') {
+    String.prototype.startsWith = function (str){
+        return this.slice(0, str.length) == str;
+    };
+}

+ 585 - 0
examples/js/loaders/UTF8v2Loader.js

@@ -0,0 +1,585 @@
+/**
+ * Loader for UTF8 version2 (after r51) encoded models generated by:
+ *	http://code.google.com/p/webgl-loader/
+ *
+ * Code to load/decompress mesh is taken from r100 of this webgl-loader
+ */
+
+THREE.UTF8v2Loader = function () {};
+
+/**
+ * Load UTF8 encoded model
+ * @param jsonUrl - URL from which to load json containing information about model
+ * @param callback - Callback(THREE.Object3D) on successful loading of model
+ * @param options - options on how to load model (see THREE.MTLLoader.MaterialCreator for basic options)
+ *                  Additional options include
+ *                   geometryBase: Base url from which to load referenced geometries
+ *                   materialBase: Base url from which to load referenced textures
+ */
+THREE.UTF8v2Loader.prototype.load = function ( jsonUrl, callback, options ) {
+    this.downloadModelJson(jsonUrl, options, callback);
+};
+
+THREE.UTF8v2Loader.GeometryCreator = function() {
+};
+
+THREE.UTF8v2Loader.GeometryCreator.prototype = {
+
+    create: function (attribArray, indexArray, bboxen) {
+        var geometry = new THREE.Geometry();
+        this.init_vertices( geometry, attribArray, 8, 0 );
+        var uvs = this.init_uvs( attribArray, 8, 3 );
+        var normals = this.init_normals( attribArray, 8, 5 );
+        this.init_faces( geometry, normals, uvs, indexArray );
+
+        geometry.computeCentroids();
+        geometry.computeFaceNormals();
+        return geometry;
+    },
+
+    init_vertices: function ( scope, data, stride, offset ) {
+        var i, x, y, z,
+            end = data.length;
+        for( i = offset; i < end; i += stride ) {
+            x = data[ i ];
+            y = data[ i + 1 ];
+            z = data[ i + 2 ];
+
+            this.addVertex( scope, x, y, z );
+        }
+    },
+
+    init_normals: function( data, stride, offset ) {
+        var normals = [];
+        var i, x, y, z,
+            end = data.length;
+
+        for( i = offset; i < end; i += stride ) {
+            x = data[ i ];
+            y = data[ i + 1 ];
+            z = data[ i + 2 ];
+
+            // assumes already normalized to <-1,1> (unlike previous version of UTF8Loader)
+            normals.push( x, y, z );
+        }
+        return normals;
+    },
+
+    init_uvs: function( data, stride, offset ) {
+        var uvs = [];
+        var i, u, v,
+            end = data.length;
+
+        for( i = offset; i < end; i += stride ) {
+            // Assumes uvs are already normalized (unlike previous version of UTF8Loader)
+            // uvs can be negative, need to set wrap for texture map later on...
+            u = data[ i ];
+            v = data[ i + 1 ];
+            uvs.push( u, v );
+        }
+        return uvs;
+    },
+
+    init_faces: function( scope, normals, uvs, indices ) {
+        var i,
+            a, b, c,
+            u1, v1, u2, v2, u3, v3,
+            m,
+            end = indices.length;
+
+        m = 0; // all faces defaulting to material 0
+        for( i = 0; i < end; i += 3 ) {
+            a = indices[ i ];
+            b = indices[ i + 1 ];
+            c = indices[ i + 2 ];
+
+            this.f3n( scope, normals, a, b, c, m, a, b, c );
+
+            u1 = uvs[ a * 2 ];
+            v1 = uvs[ a * 2 + 1 ];
+
+            u2 = uvs[ b * 2 ];
+            v2 = uvs[ b * 2 + 1 ];
+
+            u3 = uvs[ c * 2 ];
+            v3 = uvs[ c * 2 + 1 ];
+
+            this.uv3( scope.faceVertexUvs[ 0 ], u1, v1, u2, v2, u3, v3 );
+        }
+    },
+
+    addVertex: function ( scope, x, y, z ) {
+        scope.vertices.push( new THREE.Vector3( x, y, z ) );
+    },
+
+    f3n: function( scope, normals, a, b, c, mi, nai, nbi, nci ) {
+        var nax = normals[ nai * 3     ],
+            nay = normals[ nai * 3 + 1 ],
+            naz = normals[ nai * 3 + 2 ],
+
+            nbx = normals[ nbi * 3     ],
+            nby = normals[ nbi * 3 + 1 ],
+            nbz = normals[ nbi * 3 + 2 ],
+
+            ncx = normals[ nci * 3     ],
+            ncy = normals[ nci * 3 + 1 ],
+            ncz = normals[ nci * 3 + 2 ];
+
+        var na = new THREE.Vector3( nax, nay, naz ),
+            nb = new THREE.Vector3( nbx, nby, nbz ),
+            nc = new THREE.Vector3( ncx, ncy, ncz );
+
+        scope.faces.push( new THREE.Face3( a, b, c, [ na, nb, nc ], null, mi ) );
+    },
+
+    uv3: function ( where, u1, v1, u2, v2, u3, v3 ) {
+        var uv = [];
+        uv.push( new THREE.UV( u1, v1 ) );
+        uv.push( new THREE.UV( u2, v2 ) );
+        uv.push( new THREE.UV( u3, v3 ) );
+        where.push( uv );
+    }
+};
+
+
+// UTF-8 decoder from webgl-loader (r100)
+// http://code.google.com/p/webgl-loader/
+
+// Model manifest description. Contains objects like:
+// name: {
+//   materials: { 'material_name': { ... } ... },
+//   decodeParams: {
+//     decodeOffsets: [ ... ],
+//     decodeScales: [ ... ],
+//   },
+//   urls: {
+//     'url': [
+//       { material: 'material_name',
+//         attribRange: [#, #],
+//         indexRange: [#, #],
+//         names: [ 'object names' ... ],
+//         lengths: [#, #, # ... ]
+//       }
+//     ],
+//     ...
+//   }
+// }
+var DEFAULT_DECODE_PARAMS = {
+    decodeOffsets: [-4095, -4095, -4095, 0, 0, -511, -511, -511],
+    decodeScales: [1/8191, 1/8191, 1/8191, 1/1023, 1/1023, 1/1023, 1/1023, 1/1023]
+    // TODO: normal decoding? (see walt.js)
+    // needs to know: input, output (from vertex format!)
+    //
+    // Should split attrib/index.
+    // 1) Decode position and non-normal attributes.
+    // 2) Decode indices, computing normals
+    // 3) Maybe normalize normals? Only necessary for refinement, or fixed?
+    // 4) Maybe refine normals? Should this be part of regular refinement?
+    // 5) Morphing
+};
+
+// Triangle strips!
+
+// TODO: will it be an optimization to specialize this method at
+// runtime for different combinations of stride, decodeOffset and
+// decodeScale?
+THREE.UTF8v2Loader.prototype.decompressAttribsInner_ = function (str, inputStart, inputEnd,
+                                                                 output, outputStart, stride,
+                                                                 decodeOffset, decodeScale) {
+    var prev = 0;
+    for (var j = inputStart; j < inputEnd; j++) {
+        var code = str.charCodeAt(j);
+        prev += (code >> 1) ^ (-(code & 1));
+        output[outputStart] = decodeScale * (prev + decodeOffset);
+        outputStart += stride;
+    }
+};
+
+THREE.UTF8v2Loader.prototype.decompressIndices_ = function(str, inputStart, numIndices,
+                                                           output, outputStart) {
+    var highest = 0;
+    for (var i = 0; i < numIndices; i++) {
+        var code = str.charCodeAt(inputStart++);
+        output[outputStart++] = highest - code;
+        if (code == 0) {
+            highest++;
+        }
+    }
+};
+
+THREE.UTF8v2Loader.prototype.decompressAABBs_ = function (str, inputStart, numBBoxen,
+                                                          decodeOffsets, decodeScales) {
+    var numFloats = 6 * numBBoxen;
+    var inputEnd = inputStart + numFloats;
+    var bboxen = new Float32Array(numFloats);
+    var outputStart = 0;
+    for (var i = inputStart; i < inputEnd; i += 6) {
+        var minX = str.charCodeAt(i + 0) + decodeOffsets[0];
+        var minY = str.charCodeAt(i + 1) + decodeOffsets[1];
+        var minZ = str.charCodeAt(i + 2) + decodeOffsets[2];
+        var radiusX = (str.charCodeAt(i + 3) + 1) >> 1;
+        var radiusY = (str.charCodeAt(i + 4) + 1) >> 1;
+        var radiusZ = (str.charCodeAt(i + 5) + 1) >> 1;
+        bboxen[outputStart++] = decodeScales[0] * (minX + radiusX);
+        bboxen[outputStart++] = decodeScales[1] * (minY + radiusY);
+        bboxen[outputStart++] = decodeScales[2] * (minZ + radiusZ);
+        bboxen[outputStart++] = decodeScales[0] * radiusX;
+        bboxen[outputStart++] = decodeScales[1] * radiusY;
+        bboxen[outputStart++] = decodeScales[2] * radiusZ;
+    }
+    return bboxen;
+};
+
+THREE.UTF8v2Loader.prototype.decompressMesh =  function (str, meshParams, decodeParams, name, idx, callback) {
+    // Extract conversion parameters from attribArrays.
+    var stride = decodeParams.decodeScales.length;
+    var decodeOffsets = decodeParams.decodeOffsets;
+    var decodeScales = decodeParams.decodeScales;
+    var attribStart = meshParams.attribRange[0];
+    var numVerts = meshParams.attribRange[1];
+
+    // Decode attributes.
+    var inputOffset = attribStart;
+    var attribsOut = new Float32Array(stride * numVerts);
+    for (var j = 0; j < stride; j++) {
+        var end = inputOffset + numVerts;
+        var decodeScale = decodeScales[j];
+        if (decodeScale) {
+            // Assume if decodeScale is never set, simply ignore the
+            // attribute.
+            this.decompressAttribsInner_(str, inputOffset, end,
+                attribsOut, j, stride,
+                decodeOffsets[j], decodeScale);
+        }
+        inputOffset = end;
+    }
+
+    var indexStart = meshParams.indexRange[0];
+    var numIndices = 3*meshParams.indexRange[1];
+    var indicesOut = new Uint16Array(numIndices);
+    this.decompressIndices_(str, inputOffset, numIndices, indicesOut, 0);
+
+    // Decode bboxen.
+    var bboxen = undefined;
+    var bboxOffset = meshParams.bboxes;
+    if (bboxOffset) {
+        bboxen = this.decompressAABBs_(str, bboxOffset, meshParams.names.length,
+            decodeOffsets, decodeScales);
+    }
+    callback(name, idx, attribsOut, indicesOut, bboxen, meshParams);
+};
+
+THREE.UTF8v2Loader.prototype.copyAttrib = function (stride, attribsOutFixed, lastAttrib, index) {
+    for (var j = 0; j < stride; j++) {
+        lastAttrib[j] = attribsOutFixed[stride*index + j];
+    }
+};
+
+THREE.UTF8v2Loader.prototype.decodeAttrib2 = function (str, stride, decodeOffsets, decodeScales, deltaStart,
+                                                       numVerts, attribsOut, attribsOutFixed, lastAttrib,
+                                                       index) {
+    for (var j = 0; j < 5; j++) {
+        var code = str.charCodeAt(deltaStart + numVerts*j + index);
+        var delta = (code >> 1) ^ (-(code & 1));
+        lastAttrib[j] += delta;
+        attribsOutFixed[stride*index + j] = lastAttrib[j];
+        attribsOut[stride*index + j] =
+            decodeScales[j] * (lastAttrib[j] + decodeOffsets[j]);
+    }
+};
+
+THREE.UTF8v2Loader.prototype.accumulateNormal = function (i0, i1, i2, attribsOutFixed, crosses) {
+    var p0x = attribsOutFixed[8*i0 + 0];
+    var p0y = attribsOutFixed[8*i0 + 1];
+    var p0z = attribsOutFixed[8*i0 + 2];
+    var p1x = attribsOutFixed[8*i1 + 0];
+    var p1y = attribsOutFixed[8*i1 + 1];
+    var p1z = attribsOutFixed[8*i1 + 2];
+    var p2x = attribsOutFixed[8*i2 + 0];
+    var p2y = attribsOutFixed[8*i2 + 1];
+    var p2z = attribsOutFixed[8*i2 + 2];
+    p1x -= p0x;
+    p1y -= p0y;
+    p1z -= p0z;
+    p2x -= p0x;
+    p2y -= p0y;
+    p2z -= p0z;
+    p0x = p1y*p2z - p1z*p2y;
+    p0y = p1z*p2x - p1x*p2z;
+    p0z = p1x*p2y - p1y*p2x;
+
+    crosses[3*i0 + 0] += p0x;
+    crosses[3*i0 + 1] += p0y;
+    crosses[3*i0 + 2] += p0z;
+
+    crosses[3*i1 + 0] += p0x;
+    crosses[3*i1 + 1] += p0y;
+    crosses[3*i1 + 2] += p0z;
+
+    crosses[3*i2 + 0] += p0x;
+    crosses[3*i2 + 1] += p0y;
+    crosses[3*i2 + 2] += p0z;
+};
+
+THREE.UTF8v2Loader.prototype.decompressMesh2 = function(str, meshParams, decodeParams, name, idx, callback) {
+    var MAX_BACKREF = 96;
+    // Extract conversion parameters from attribArrays.
+    var stride = decodeParams.decodeScales.length;
+    var decodeOffsets = decodeParams.decodeOffsets;
+    var decodeScales = decodeParams.decodeScales;
+    var deltaStart = meshParams.attribRange[0];
+    var numVerts = meshParams.attribRange[1];
+    var codeStart = meshParams.codeRange[0];
+    var codeLength = meshParams.codeRange[1];
+    var numIndices = 3*meshParams.codeRange[2];
+    var indicesOut = new Uint16Array(numIndices);
+    var crosses = new Int32Array(3*numVerts);
+    var lastAttrib = new Uint16Array(stride);
+    var attribsOutFixed = new Uint16Array(stride * numVerts);
+    var attribsOut = new Float32Array(stride * numVerts);
+    var highest = 0;
+    var outputStart = 0;
+    for (var i = 0; i < numIndices; i += 3) {
+        var code = str.charCodeAt(codeStart++);
+        var max_backref = Math.min(i, MAX_BACKREF);
+        if (code < max_backref) {
+            // Parallelogram
+            var winding = code % 3;
+            var backref = i - (code - winding);
+            var i0, i1, i2;
+            switch (winding) {
+                case 0:
+                    i0 = indicesOut[backref + 2];
+                    i1 = indicesOut[backref + 1];
+                    i2 = indicesOut[backref + 0];
+                    break;
+                case 1:
+                    i0 = indicesOut[backref + 0];
+                    i1 = indicesOut[backref + 2];
+                    i2 = indicesOut[backref + 1];
+                    break;
+                case 2:
+                    i0 = indicesOut[backref + 1];
+                    i1 = indicesOut[backref + 0];
+                    i2 = indicesOut[backref + 2];
+                    break;
+            }
+            indicesOut[outputStart++] = i0;
+            indicesOut[outputStart++] = i1;
+            code = str.charCodeAt(codeStart++);
+            var index = highest - code;
+            indicesOut[outputStart++] = index;
+            if (code === 0) {
+                for (var j = 0; j < 5; j++) {
+                    var deltaCode = str.charCodeAt(deltaStart + numVerts*j + highest);
+                    var prediction = ((deltaCode >> 1) ^ (-(deltaCode & 1))) +
+                        attribsOutFixed[stride*i0 + j] +
+                        attribsOutFixed[stride*i1 + j] -
+                        attribsOutFixed[stride*i2 + j];
+                    lastAttrib[j] = prediction;
+                    attribsOutFixed[stride*highest + j] = prediction;
+                    attribsOut[stride*highest + j] =
+                        decodeScales[j] * (prediction + decodeOffsets[j]);
+                }
+                highest++;
+            } else {
+                this.copyAttrib(stride, attribsOutFixed, lastAttrib, index);
+            }
+            this.accumulateNormal(i0, i1, index, attribsOutFixed, crosses);
+        } else {
+            // Simple
+            var index0 = highest - (code - max_backref);
+            indicesOut[outputStart++] = index0;
+            if (code === max_backref) {
+                this.decodeAttrib2(str, stride, decodeOffsets, decodeScales, deltaStart,
+                    numVerts, attribsOut, attribsOutFixed, lastAttrib,
+                    highest++);
+            } else {
+                this.copyAttrib(stride, attribsOutFixed, lastAttrib, index0);
+            }
+            code = str.charCodeAt(codeStart++);
+            var index1 = highest - code;
+            indicesOut[outputStart++] = index1;
+            if (code === 0) {
+                this.decodeAttrib2(str, stride, decodeOffsets, decodeScales, deltaStart,
+                    numVerts, attribsOut, attribsOutFixed, lastAttrib,
+                    highest++);
+            } else {
+                this.copyAttrib(stride, attribsOutFixed, lastAttrib, index1);
+            }
+            code = str.charCodeAt(codeStart++);
+            var index2 = highest - code;
+            indicesOut[outputStart++] = index2;
+            if (code === 0) {
+                for (var j = 0; j < 5; j++) {
+                    lastAttrib[j] = (attribsOutFixed[stride*index0 + j] +
+                        attribsOutFixed[stride*index1 + j]) / 2;
+                }
+                this.decodeAttrib2(str, stride, decodeOffsets, decodeScales, deltaStart,
+                    numVerts, attribsOut, attribsOutFixed, lastAttrib,
+                    highest++);
+            } else {
+                this.copyAttrib(stride, attribsOutFixed, lastAttrib, index2);
+            }
+            this.accumulateNormal(index0, index1, index2, attribsOutFixed, crosses);
+        }
+    }
+    for (var i = 0; i < numVerts; i++) {
+        var nx = crosses[3*i + 0];
+        var ny = crosses[3*i + 1];
+        var nz = crosses[3*i + 2];
+        var norm = 511.0 / Math.sqrt(nx*nx + ny*ny + nz*nz);
+
+        var cx = str.charCodeAt(deltaStart + 5*numVerts + i);
+        var cy = str.charCodeAt(deltaStart + 6*numVerts + i);
+        var cz = str.charCodeAt(deltaStart + 7*numVerts + i);
+
+        attribsOut[stride*i + 5] = norm*nx + ((cx >> 1) ^ (-(cx & 1)));
+        attribsOut[stride*i + 6] = norm*ny + ((cy >> 1) ^ (-(cy & 1)));
+        attribsOut[stride*i + 7] = norm*nz + ((cz >> 1) ^ (-(cz & 1)));
+    }
+    callback(name, idx, attribsOut, indicesOut, undefined, meshParams);
+};
+
+THREE.UTF8v2Loader.prototype.downloadMesh = function (path, name, meshEntry, decodeParams, callback) {
+    var loader = this;
+    var idx = 0;
+    function onprogress(req, e) {
+        while (idx < meshEntry.length) {
+            var meshParams = meshEntry[idx];
+            var indexRange = meshParams.indexRange;
+            if (indexRange) {
+                var meshEnd = indexRange[0] + 3*indexRange[1];
+                if (req.responseText.length < meshEnd) break;
+
+                loader.decompressMesh(req.responseText, meshParams, decodeParams, name, idx, callback);
+            } else {
+                var codeRange = meshParams.codeRange;
+                var meshEnd = codeRange[0] + codeRange[1];
+
+                if (req.responseText.length < meshEnd) break;
+
+                loader.decompressMesh2(req.responseText, meshParams, decodeParams, name, idx, callback);
+            }
+            ++idx;
+        }
+    };
+    getHttpRequest(path, function(req, e) {
+        if (req.status === 200 || req.status === 0) {
+            onprogress(req, e);
+        }
+        // TODO: handle errors.
+    }, onprogress);
+};
+
+THREE.UTF8v2Loader.prototype.downloadMeshes = function (path, meshUrlMap, decodeParams, callback) {
+    for (var url in meshUrlMap) {
+        var meshEntry = meshUrlMap[url];
+        this.downloadMesh(path + url, url, meshEntry, decodeParams, callback);
+    }
+};
+
+THREE.UTF8v2Loader.prototype.createMeshCallback = function(materialBaseUrl, loadModelInfo, allDoneCallback) {
+    var expectedMeshesPerUrl = {};
+    var decodedMeshesPerUrl = {};
+    var nCompletedUrls = 0;
+    var nExpectedUrls = 0;
+    var modelParts = {};
+    var meshUrlMap = loadModelInfo.urls;
+    for (var url in meshUrlMap) {
+        expectedMeshesPerUrl[url] = meshUrlMap[url].length;
+        decodedMeshesPerUrl[url] = 0;
+        nExpectedUrls++;
+        modelParts[url] = new THREE.Object3D();
+    }
+    var model = new THREE.Object3D();
+    var geometryCreator = new THREE.UTF8v2Loader.GeometryCreator();
+    var materialCreator = new THREE.MTLLoader.MaterialCreator(materialBaseUrl, loadModelInfo.options);
+    materialCreator.setMaterials(loadModelInfo.materials);
+    // Prepare materials first...
+    materialCreator.preload();
+
+    return function(name, idx, attribArray, indexArray, bboxen, meshParams) {
+        // Got ourselves a new mesh
+
+        // name identifies this part of the model (url)
+        // idx is the mesh index of this mesh of the part
+        // attribArray defines the vertices
+        // indexArray defines the faces
+        // bboxen defines the bounding box
+        // meshParams contains the material info
+
+        var geometry = geometryCreator.create( attribArray, indexArray, bboxen);
+        var material = materialCreator.create( meshParams.material );
+        modelParts[name].add(new THREE.Mesh(geometry, material));
+        //model.add(new THREE.Mesh(geometry, material));
+        decodedMeshesPerUrl[name]++;
+        if (decodedMeshesPerUrl[name] === expectedMeshesPerUrl[name]) {
+            nCompletedUrls++;
+            model.add(modelParts[name]);
+            if (nCompletedUrls === nExpectedUrls) {
+                // ALL DONE!!!
+                allDoneCallback(model);
+            }
+        }
+    }
+};
+
+THREE.UTF8v2Loader.prototype.downloadModel = function (geometryBase, materialBase, model, callback) {
+    var meshCallback = this.createMeshCallback(materialBase, model, callback);
+    this.downloadMeshes(geometryBase, model.urls, model.decodeParams, meshCallback);
+};
+
+THREE.UTF8v2Loader.prototype.downloadModelJson = function (jsonUrl, options, callback ) {
+    getJsonRequest(jsonUrl, function(loaded) {
+        if (!loaded.decodeParams) {
+            if (options && options.decodeParams) {
+                loaded.decodeParams = options.decodeParams;
+            } else {
+                loaded.decodeParams = DEFAULT_DECODE_PARAMS;
+            }
+        }
+        loaded.options = options;
+        var geometryBase = jsonUrl.substr(0,jsonUrl.lastIndexOf("/")+1);
+        var materialBase = geometryBase;
+        if (options && options.geometryBase) {
+            geometryBase = options.geometryBase;
+            if (geometryBase.charAt(geometryBase.length-1) !== "/") {
+                geometryBase = geometryBase + "/";
+            }
+        }
+        if (options && options.materialBase) {
+            materialBase = options.materialBase;
+            if (materialBase.charAt(materialBase.length-1) !== "/") {
+                materialBase = materialBase  + "/";
+            }
+        }
+        this.downloadModel(geometryBase, materialBase, loaded, callback);
+    }.bind(this));
+};
+
+// XMLHttpRequest stuff.
+function getHttpRequest(url, onload, opt_onprogress) {
+    var LISTENERS = {
+        load: function(e) { onload(req, e); },
+        progress: function(e) { opt_onprogress(req, e); }
+    };
+
+    var req = new XMLHttpRequest();
+    addListeners(req, LISTENERS);
+    req.open('GET', url, true);
+    req.send(null);
+}
+
+function getJsonRequest(url, onjson) {
+    getHttpRequest(url,
+        function(e) { onjson(JSON.parse(e.responseText)); },
+        function() {});
+}
+
+function addListeners(dom, listeners) {
+    // TODO: handle event capture, object binding.
+    for (var key in listeners) {
+        dom.addEventListener(key, listeners[key]);
+    }
+}

+ 1 - 1
src/extras/SceneUtils.js

@@ -65,7 +65,7 @@ THREE.SceneUtils = {
 
 		} else if ( source instanceof THREE.SkinnedMesh ) {
 
-			object = new THREE.SkinnedMesh( source.geometry, source.material );
+			object = new THREE.SkinnedMesh( source.geometry, source.material, source.useVertexTexture );
 
 		} else if ( source instanceof THREE.Mesh ) {