Browse Source

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

Angel Xuan Chang 13 years ago
parent
commit
9fe28db70a
3 changed files with 1256 additions and 0 deletions
  1. 311 0
      examples/js/loaders/MTLLoader.js
  2. 360 0
      examples/js/loaders/OBJMTLLoader.js
  3. 585 0
      examples/js/loaders/UTF8v2Loader.js

+ 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'] = this.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];
+    },
+
+    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 = this.ensurePowerOfTwo(event.content);
+            texture.needsUpdate = true;
+            if ( onLoad ) onLoad( texture );
+        }.bind(this) );
+
+        loader.addEventListener( 'error', function ( event ) {
+            if ( onError ) onError( event.message );
+        } );
+
+        loader.crossOrigin = this.crossOrigin;
+        loader.load( url, image );
+
+        return texture;
+    },
+
+    ensurePowerOfTwo: function(image)
+    {
+        if (!this.isPowerOfTwo(image.width) || !this.isPowerOfTwo(image.height))
+        {
+            var canvas = document.createElement("canvas");
+            canvas.width = this.nextHighestPowerOfTwo(image.width);
+            canvas.height = this.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;
+    },
+
+    isPowerOfTwo: function (x)
+    {
+        return (x & (x - 1)) == 0;
+    },
+
+    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]);
+    }
+}