MTLLoader.js 11.7 KB
Newer Older
1 2 3 4 5 6
/**
 * Loads a Wavefront .mtl file specifying materials
 *
 * @author angelxuanchang
 */

7 8
THREE.MTLLoader = function( baseUrl, options ) {

9 10 11
    THREE.EventTarget.call( this );
    this.baseUrl = baseUrl;
    this.options = options;
12

13 14 15 16 17 18 19 20 21 22 23 24 25 26
};

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
     */
27 28
    load: function( url ) {

29 30 31
        var scope = this;
        var xhr = new XMLHttpRequest();

32 33 34 35 36 37
        function onloaded( event ) {

            if ( event.target.status === 200 || event.target.status === 0 ) {

                var materialCreator = scope.parse( event.target.responseText );

38
                // Notify caller, that I'm done
39

40
                scope.dispatchEvent( { type: 'load', content: materialCreator } );
41

42
            } else {
43

44 45
                scope.dispatchEvent( { type: 'error', message: 'Couldn\'t load URL [' + url + ']',
                    response: event.target.responseText } );
46

47
            }
48

49 50 51 52 53
        }

        xhr.addEventListener( 'load', onloaded, false );

        xhr.addEventListener( 'progress', function ( event ) {
54

55
            scope.dispatchEvent( { type: 'progress', loaded: event.loaded, total: event.total } );
56

57 58 59
        }, false );

        xhr.addEventListener( 'error', function () {
60

61
            scope.dispatchEvent( { type: 'error', message: 'Couldn\'t load URL [' + url + ']' } );
62

63 64 65 66 67 68 69 70 71 72 73
        }, false );

        xhr.open( 'GET', url, true );
        xhr.send( null );
    },

    /**
     * Parses loaded MTL file
     * @param text - Content of MTL file
     * @return {THREE.MTLLoader.MaterialCreator}
     */
74 75 76
    parse: function( text ) {

        var lines = text.split( "\n" );
77 78 79
        var info = {};
        var delimiter_pattern = /\s+/;
        var materialsInfo = {};
80 81 82 83 84 85 86 87

			for ( var i = 0; i < lines.length; i ++ ) {

			var line = lines[ i ];
            line = line.trim();

            if ( line.length === 0 || line.charAt( 0 ) === '#' ) {

88 89
                // Blank line or comment ignore
                continue;
90

91 92
            }

93 94 95
            var pos = line.indexOf( ' ' );

			var key = ( pos >= 0 ) ? line.substring( 0, pos) : line;
96
            key = key.toLowerCase();
97 98 99 100 101 102

            var value = ( pos >= 0 ) ? line.substring( pos + 1 ) : "";
            value = value.trim();

            if ( key === "newmtl" ) {

103
                // New material
104

105
                info = { name: value };
106 107 108 109 110 111 112 113 114
                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] ) ];

115
                } else {
116 117 118

                    info[ key ] = value;

119
                }
120

121
            }
122

123
        }
124 125 126

        var materialCreator = new THREE.MTLLoader.MaterialCreator( this.baseUrl, this.options );
        materialCreator.setMaterials( materialsInfo );
127
        return materialCreator;
128

129
    }
130

131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
};

/**
 * 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
 */
149 150
THREE.MTLLoader.MaterialCreator = function( baseUrl, options ) {

151 152 153 154 155 156 157 158
    THREE.EventTarget.call( this );
    this.baseUrl = baseUrl;
    this.options = options;
    this.materialsInfo = {};
    this.materials = {};
    this.materialsArray = [];
    this.nameLookup = {};

159 160 161
    this.side = ( this.options && this.options.side )? this.options.side: THREE.FrontSide;
    this.wrap = ( this.options && this.options.wrap )? this.options.wrap: THREE.RepeatWrapping;

162 163 164
};

THREE.MTLLoader.MaterialCreator.prototype = {
165 166 167 168

    setMaterials: function( materialsInfo ) {

        this.materialsInfo = this.convert( materialsInfo );
169 170 171
        this.materials = {};
        this.materialsArray = [];
        this.nameLookup = {};
172

173 174
    },

175 176 177 178
    convert: function( materialsInfo ) {

        if ( !this.options ) return materialsInfo;

179
        var converted = {};
180 181 182

        for ( var mn in materialsInfo ) {

183
            // Convert materials info into normalized form based on options
184 185 186

            var mat = materialsInfo[ mn ];

187
            var covmat = {};
188 189 190 191 192

            converted[ mn ] = covmat;

            for ( var prop in mat ) {

193
                var save = true;
194
                var value = mat[ prop ];
195
                var lprop = prop.toLowerCase();
196 197 198

                switch ( lprop ) {

199 200 201
                    case 'kd':
                    case 'ka':
                    case 'ks':
202

203
                        // Diffuse color (color under white light) using RGB values
204 205 206

                        if ( this.options && this.options.normalizeRGB ) {

207
                            value =  [ value[0]/255, value[1]/255, value[2]/255 ];
208

209
                        }
210 211 212 213 214

                        if ( this.options && this.options.ignoreZeroRGBs ) {

                            if ( value[0] === 0.0 && value[1] === 0.0 && value[1] === 0.0 ) {

215
                                // ignore
216

217
                                save = false;
218

219 220
                            }
                        }
221

222
                        break;
223

224
                    case 'd':
225

226 227 228
                        // 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)
229 230 231

                        if ( this.options && this.options.invertTransparency ) {

232
                            value = 1 - value;
233

234
                        }
235

236
                        break;
237

238
                    default:
239

240 241
                        break;
                }
242 243 244

                if ( save ) {

245
                    covmat[lprop] = value;
246

247
                }
248

249
            }
250

251
        }
252

253
        return converted;
254

255 256 257
    },

    preload: function () {
258 259 260 261 262

        for ( var mn in this.materialsInfo ) {

            this.create( mn );

263
        }
264

265 266
    },

267 268 269 270
    getIndex: function( materialName ) {

        return this.nameLookup[ materialName ];

271 272 273
    },

    getAsArray: function() {
274

275
        var index = 0;
276 277 278 279 280 281 282

        for ( var mn in this.materialsInfo ) {

            this.materialsArray[ index ] = this.create( mn );
            this.nameLookup[ mn ] = index;
            index ++;

283
        }
284

285
        return this.materialsArray;
286

287 288
    },

289 290 291 292 293 294
    create: function ( materialName ) {

        if ( this.materials[ materialName ] === undefined ) {

            this.createMaterial_( materialName );

295
        }
296 297 298

        return this.materials[ materialName ];

299 300
    },

301 302
    createMaterial_: function ( materialName ) {

303
        // Create material
304 305

        var mat = this.materialsInfo[ materialName ];
306
        var params = {
307

308 309
            name: materialName,
            side: this.side
310

311
        };
312 313 314 315 316 317 318

        for ( var prop in mat ) {

            var value = mat[ prop ];

            switch ( prop.toLowerCase() ) {

319
                // Ns is material specular exponent
320

321
                case 'kd':
322 323 324 325 326 327 328

					// Diffuse color (color under white light) using RGB values

					params[ 'diffuse' ] = new THREE.Color().setRGB( value[0], value[1], value[2] );

					break;

329
                case 'ka':
330

331
                    // Ambient color (color under shadow) using RGB values
332 333 334 335 336

                    params[ 'ambient' ] = new THREE.Color().setRGB( value[0], value[1], value[2] );

					break;

337
                case 'ks':
338 339 340 341

                    // 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] );

342
                    break;
343

344
                case 'map_kd':
345

346
                    // Diffuse texture map
347 348 349 350 351 352 353

                    params[ 'map' ] = THREE.MTLLoader.loadTexture( this.baseUrl + value );
                    params[ 'map' ].wrapS = this.wrap;
                    params[ 'map' ].wrapT = this.wrap;

					break;

354
                case 'ns':
355

356 357
                    // 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.
358 359 360 361 362

					params['shininess'] = value;

					break;

363
                case 'd':
364

365 366 367
                    // 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)
368 369 370

                    if ( value < 1 ) {

371 372
                        params['transparent'] = true;
                        params['opacity'] = value;
373

374
                    }
375

376
                    break;
377

378 379
                default:
                    break;
380

381
            }
382

383
        }
384 385 386 387 388 389

        if ( params[ 'diffuse' ] ) {

            if ( !params[ 'ambient' ]) params[ 'ambient' ] = params[ 'diffuse' ];
            params[ 'color' ] = params[ 'diffuse' ];

390
        }
391 392 393 394

        this.materials[ materialName ] = new THREE.MeshPhongMaterial( params );
        return this.materials[ materialName ];

395
    }
396

397
};
398

399
THREE.MTLLoader.loadTexture = function ( url, mapping, onLoad, onError ) {
400

401
	var isCompressed = url.toLowerCase().endsWith( ".dds" );
402

403
	if ( isCompressed ) {
404

405
		var texture = THREE.ImageUtils.loadCompressedTexture( url, mapping, onLoad, onError );
406

407
	} else {
408

409 410
		var image = new Image();
		var texture = new THREE.Texture( image, mapping );
411

412
		var loader = new THREE.ImageLoader();
413

414
		loader.addEventListener( 'load', function ( event ) {
415

416 417 418
			texture.image = THREE.MTLLoader.ensurePowerOfTwo_( event.content );
			texture.needsUpdate = true;
			if ( onLoad ) onLoad( texture );
419

420
		} );
421

422
		loader.addEventListener( 'error', function ( event ) {
423

424 425 426 427 428 429 430 431 432 433 434 435
			if ( onError ) onError( event.message );

		} );

		loader.crossOrigin = this.crossOrigin;
		loader.load( url, image );

	}

	return texture;

};
436

437 438 439 440 441 442 443 444
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 );

445
        var ctx = canvas.getContext("2d");
446
        ctx.drawImage( image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height );
447
        return canvas;
448

449
    }
450

451
    return image;
452

453
};
454

455 456 457 458
THREE.MTLLoader.isPowerOfTwo_ = function ( x ) {

	return ( x & ( x - 1 ) ) === 0;

459
};
460

461 462
THREE.MTLLoader.nextHighestPowerOfTwo_ = function( x ) {

463
    --x;
464 465 466

    for ( var i = 1; i < 32; i <<= 1 ) {

467
        x = x | x >> i;
468

469
    }
470

471
    return x + 1;
472

473 474
};