BasisTextureLoader.js 10.6 KB
Newer Older
D
Don McCurdy 已提交
1 2 3 4 5 6
/**
 * @author Don McCurdy / https://www.donmccurdy.com
 * @author Austin Eng / https://github.com/austinEng
 * @author Shrek Shao / https://github.com/shrekshao
 */

7 8
/* global Module, createBasisModule */

D
Don McCurdy 已提交
9 10 11 12 13 14 15 16 17 18 19 20
/**
 * Loader for Basis Universal GPU Texture Codec.
 *
 * Basis Universal is a "supercompressed" GPU texture and texture video
 * compression system that outputs a highly compressed intermediate file format
 * (.basis) that can be quickly transcoded to a wide variety of GPU texture
 * compression formats.
 *
 * This loader parallelizes the transcoding process across a configurable number
 * of web workers, before transferring the transcoded compressed texture back
 * to the main thread.
 */
21
THREE.BasisTextureLoader = function ( manager ) {
D
Don McCurdy 已提交
22

23
	this.manager = manager || THREE.DefaultLoadingManager;
D
Don McCurdy 已提交
24

25
	this.crossOrigin = 'anonymous';
D
Don McCurdy 已提交
26

27 28 29
	this.transcoderPath = '';
	this.transcoderBinary = null;
	this.transcoderPending = null;
30

31 32 33 34 35 36 37 38 39 40
	this.workerLimit = 4;
	this.workerPool = [];
	this.workerNextTaskID = 1;
	this.workerSourceURL = '';
	this.workerConfig = {
		format: null,
		etcSupported: false,
		dxtSupported: false,
		pvrtcSupported: false,
	};
D
Don McCurdy 已提交
41

42
};
D
Don McCurdy 已提交
43

44 45 46
THREE.BasisTextureLoader.prototype = {

	constructor: THREE.BasisTextureLoader,
D
Don McCurdy 已提交
47

48
	setCrossOrigin: function ( crossOrigin ) {
49 50 51 52 53

		this.crossOrigin = crossOrigin;

		return this;

54
	},
55

56
	setTranscoderPath: function ( path ) {
D
Don McCurdy 已提交
57 58 59

		this.transcoderPath = path;

60 61
		return this;

62
	},
63

64
	setWorkerLimit: function ( workerLimit ) {
65 66 67 68 69

		this.workerLimit = workerLimit;

		return this;

70
	},
D
Don McCurdy 已提交
71

72
	detectSupport: function ( renderer ) {
D
Don McCurdy 已提交
73 74 75 76

		var context = renderer.context;
		var config = this.workerConfig;

D
Don McCurdy 已提交
77 78 79 80
		config.etcSupported = !! context.getExtension( 'WEBGL_compressed_texture_etc1' );
		config.dxtSupported = !! context.getExtension( 'WEBGL_compressed_texture_s3tc' );
		config.pvrtcSupported = !! context.getExtension( 'WEBGL_compressed_texture_pvrtc' )
			|| !! context.getExtension( 'WEBKIT_WEBGL_compressed_texture_pvrtc' );
D
Don McCurdy 已提交
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101

		if ( config.etcSupported ) {

			config.format = THREE.BasisTextureLoader.BASIS_FORMAT.cTFETC1;

		} else if ( config.dxtSupported ) {

			config.format = THREE.BasisTextureLoader.BASIS_FORMAT.cTFBC1;

		} else if ( config.pvrtcSupported ) {

			config.format = THREE.BasisTextureLoader.BASIS_FORMAT.cTFPVRTC1_4_OPAQUE_ONLY;

		} else {

			throw new Error( 'THREE.BasisTextureLoader: No suitable compressed texture format found.' );

		}

		return this;

102
	},
D
Don McCurdy 已提交
103

104
	load: function ( url, onLoad, onProgress, onError ) {
D
Don McCurdy 已提交
105

106 107 108 109 110 111 112 113 114 115 116
		var loader = new THREE.FileLoader( this.manager );

		loader.setResponseType( 'arraybuffer' );

		loader.load( url, ( buffer ) => {

			this._createTexture( buffer )
				.then( onLoad )
				.catch( onError );

		}, onProgress, onError );
D
Don McCurdy 已提交
117

118
	},
D
Don McCurdy 已提交
119 120 121 122 123

	/**
	 * @param  {ArrayBuffer} buffer
	 * @return {Promise<THREE.CompressedTexture>}
	 */
124
	_createTexture: function ( buffer ) {
D
Don McCurdy 已提交
125

126 127 128
		var worker;
		var taskID;

129
		var texturePending = this._getWorker()
130
			.then( ( _worker ) => {
D
Don McCurdy 已提交
131

132
				worker = _worker;
D
Don McCurdy 已提交
133
				taskID = this.workerNextTaskID ++;
D
Don McCurdy 已提交
134

135
				return new Promise( ( resolve, reject ) => {
D
Don McCurdy 已提交
136

137
					worker._callbacks[ taskID ] = { resolve, reject };
D
Don McCurdy 已提交
138 139 140 141 142
					worker._taskCosts[ taskID ] = buffer.byteLength;
					worker._taskLoad += worker._taskCosts[ taskID ];

					worker.postMessage( { type: 'transcode', id: taskID, buffer }, [ buffer ] );

D
Don McCurdy 已提交
143
				} );
D
Don McCurdy 已提交
144 145 146 147 148 149

			} )
			.then( ( message ) => {

				var config = this.workerConfig;

150
				var { width, height, mipmaps } = message;
D
Don McCurdy 已提交
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171

				var texture;

				if ( config.etcSupported ) {

					texture = new THREE.CompressedTexture( mipmaps, width, height, THREE.RGB_ETC1_Format );

				} else if ( config.dxtSupported ) {

					texture = new THREE.CompressedTexture( mipmaps, width, height, THREE.BasisTextureLoader.DXT_FORMAT_MAP[ config.format ], THREE.UnsignedByteType );

				} else if ( config.pvrtcSupported ) {

					texture = new THREE.CompressedTexture( mipmaps, width, height, THREE.RGB_PVRTC_4BPPV1_Format );

				} else {

					throw new Error( 'THREE.BasisTextureLoader: No supported format available.' );

				}

172
				texture.minFilter = mipmaps.length === 1 ? THREE.LinearFilter : THREE.LinearMipMapLinearFilter;
D
Don McCurdy 已提交
173 174 175 176 177 178
				texture.magFilter = THREE.LinearFilter;
				texture.generateMipmaps = false;
				texture.needsUpdate = true;

				return texture;

D
Don McCurdy 已提交
179
			} );
D
Don McCurdy 已提交
180

181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
		texturePending
			.finally( () => {

				if ( worker && taskID ) {

					worker._taskLoad -= worker._taskCosts[ taskID ];
					delete worker._callbacks[ taskID ];
					delete worker._taskCosts[ taskID ];

				}

			} );

		return texturePending;

196
	},
D
Don McCurdy 已提交
197

198
	_initTranscoder: function () {
D
Don McCurdy 已提交
199 200 201

		if ( ! this.transcoderBinary ) {

202 203 204 205
			// Load transcoder wrapper.
			var jsLoader = new THREE.FileLoader( this.manager );
			jsLoader.setPath( this.transcoderPath );
			var jsContent = new Promise( ( resolve, reject ) => {
D
Don McCurdy 已提交
206

207 208 209 210 211 212 213 214 215 216 217 218 219
				jsLoader.load( 'basis_transcoder.js', resolve, undefined, reject );

			} );

			// Load transcoder WASM binary.
			var binaryLoader = new THREE.FileLoader( this.manager );
			binaryLoader.setPath( this.transcoderPath );
			binaryLoader.setResponseType( 'arraybuffer' );
			var binaryContent = new Promise( ( resolve, reject ) => {

				binaryLoader.load( 'basis_transcoder.wasm', resolve, undefined, reject );

			} );
D
Don McCurdy 已提交
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246

			this.transcoderPending = Promise.all( [ jsContent, binaryContent ] )
				.then( ( [ jsContent, binaryContent ] ) => {

					var fn = THREE.BasisTextureLoader.BasisWorker.toString();

					var body = [
						'/* basis_transcoder.js */',
						'var Module;',
						'function createBasisModule () {',
						'  ' + jsContent,
						'  return Module;',
						'}',
						'',
						'/* worker */',
						fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) )
					].join( '\n' );

					this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) );
					this.transcoderBinary = binaryContent;

				} );

		}

		return this.transcoderPending;

247
	},
D
Don McCurdy 已提交
248

249
	_getWorker: function () {
D
Don McCurdy 已提交
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273

		return this._initTranscoder().then( () => {

			if ( this.workerPool.length < this.workerLimit ) {

				var worker = new Worker( this.workerSourceURL );

				worker._callbacks = {};
				worker._taskCosts = {};
				worker._taskLoad = 0;

				worker.postMessage( {
					type: 'init',
					config: this.workerConfig,
					transcoderBinary: this.transcoderBinary,
				} );

				worker.onmessage = function ( e ) {

					var message = e.data;

					switch ( message.type ) {

						case 'transcode':
274 275 276 277 278
							worker._callbacks[ message.id ].resolve( message );
							break;

						case 'error':
							worker._callbacks[ message.id ].reject( message );
D
Don McCurdy 已提交
279 280 281
							break;

						default:
282
							console.error( 'THREE.BasisTextureLoader: Unexpected message, "' + message.type + '"' );
D
Don McCurdy 已提交
283 284 285

					}

D
Don McCurdy 已提交
286
				};
D
Don McCurdy 已提交
287 288 289 290 291

				this.workerPool.push( worker );

			} else {

D
Don McCurdy 已提交
292 293 294 295 296
				this.workerPool.sort( function ( a, b ) {

					return a._taskLoad > b._taskLoad ? - 1 : 1;

				} );
D
Don McCurdy 已提交
297 298 299 300 301 302 303

			}

			return this.workerPool[ this.workerPool.length - 1 ];

		} );

304
	},
D
Don McCurdy 已提交
305

306
	dispose: function () {
D
Don McCurdy 已提交
307

D
Don McCurdy 已提交
308
		for ( var i = 0; i < this.workerPool.length; i ++ ) {
D
Don McCurdy 已提交
309 310 311 312 313 314 315

			this.workerPool[ i ].terminate();

		}

		this.workerPool.length = 0;

316 317
		return this;

D
Don McCurdy 已提交
318
	}
D
Don McCurdy 已提交
319
};
D
Don McCurdy 已提交
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350

/* CONSTANTS */

THREE.BasisTextureLoader.BASIS_FORMAT = {
	cTFETC1: 0,
	cTFBC1: 1,
	cTFBC4: 2,
	cTFPVRTC1_4_OPAQUE_ONLY: 3,
	cTFBC7_M6_OPAQUE_ONLY: 4,
	cTFETC2: 5,
	cTFBC3: 6,
	cTFBC5: 7,
};

// DXT formats, from:
// http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
THREE.BasisTextureLoader.DXT_FORMAT = {
	COMPRESSED_RGB_S3TC_DXT1_EXT: 0x83F0,
	COMPRESSED_RGBA_S3TC_DXT1_EXT: 0x83F1,
	COMPRESSED_RGBA_S3TC_DXT3_EXT: 0x83F2,
	COMPRESSED_RGBA_S3TC_DXT5_EXT: 0x83F3,
};
THREE.BasisTextureLoader.DXT_FORMAT_MAP = {};
THREE.BasisTextureLoader.DXT_FORMAT_MAP[ THREE.BasisTextureLoader.BASIS_FORMAT.cTFBC1 ] =
	THREE.BasisTextureLoader.DXT_FORMAT.COMPRESSED_RGB_S3TC_DXT1_EXT;
THREE.BasisTextureLoader.DXT_FORMAT_MAP[ THREE.BasisTextureLoader.BASIS_FORMAT.cTFBC3 ] =
	THREE.BasisTextureLoader.DXT_FORMAT.COMPRESSED_RGBA_S3TC_DXT5_EXT;

/* WEB WORKER */

THREE.BasisTextureLoader.BasisWorker = function () {
D
Don McCurdy 已提交
351

D
Don McCurdy 已提交
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
	var config;
	var transcoderPending;
	var _BasisFile;

	onmessage = function ( e ) {

		var message = e.data;

		switch ( message.type ) {

			case 'init':
				config = message.config;
				init( message.transcoderBinary );
				break;

			case 'transcode':
				transcoderPending.then( () => {

370
					try {
371

372
						var { width, height, mipmaps } = transcode( message.buffer );
373

374
						var buffers = [];
375

D
Don McCurdy 已提交
376
						for ( var i = 0; i < mipmaps.length; ++ i ) {
377

D
Don McCurdy 已提交
378
							buffers.push( mipmaps[ i ].data.buffer );
379 380

						}
D
Don McCurdy 已提交
381

382 383 384 385 386 387 388 389 390
						self.postMessage( { type: 'transcode', id: message.id, width, height, mipmaps }, buffers );

					} catch ( error ) {

						console.error( error );

						self.postMessage( { type: 'error', id: message.id, error: error.message } );

					}
D
Don McCurdy 已提交
391 392 393 394 395 396 397 398

				} );
				break;

		}

	};

D
Don McCurdy 已提交
399
	function init( wasmBinary ) {
D
Don McCurdy 已提交
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424

		transcoderPending = new Promise( ( resolve ) => {

			// The 'Module' global is used by the Basis wrapper, which will check for
			// the 'wasmBinary' property before trying to load the file itself.

			// TODO(donmccurdy): This only works with a modified version of the
			// emscripten-generated wrapper. The default seems to have a bug making it
			// impossible to override the WASM binary.
			Module = { wasmBinary, onRuntimeInitialized: resolve };

		} ).then( () => {

			var { BasisFile, initializeBasis } = Module;

			_BasisFile = BasisFile;

			initializeBasis();

		} );

		createBasisModule();

	}

D
Don McCurdy 已提交
425
	function transcode( buffer ) {
D
Don McCurdy 已提交
426 427 428 429 430 431 432

		var basisFile = new _BasisFile( new Uint8Array( buffer ) );

		var width = basisFile.getImageWidth( 0, 0 );
		var height = basisFile.getImageHeight( 0, 0 );
		var levels = basisFile.getNumLevels( 0 );

D
Don McCurdy 已提交
433
		function cleanup() {
D
Don McCurdy 已提交
434 435 436 437 438 439

			basisFile.close();
			basisFile.delete();

		}

440
		if ( ! width || ! height || ! levels ) {
D
Don McCurdy 已提交
441 442 443 444 445 446 447 448 449 450 451 452 453

			cleanup();
			throw new Error( 'THREE.BasisTextureLoader:  Invalid .basis file' );

		}

		if ( ! basisFile.startTranscoding() ) {

			cleanup();
			throw new Error( 'THREE.BasisTextureLoader: .startTranscoding failed' );

		}

454 455 456 457 458 459
		if ( basisFile.getHasAlpha() ) {

			console.warn( 'THREE.BasisTextureLoader: Alpha not yet implemented.' );

		}

460
		var mipmaps = [];
D
Don McCurdy 已提交
461

D
Don McCurdy 已提交
462
		for ( var mip = 0; mip < levels; mip ++ ) {
D
Don McCurdy 已提交
463

464 465 466
			var mipWidth = basisFile.getImageWidth( 0, mip );
			var mipHeight = basisFile.getImageHeight( 0, mip );
			var dst = new Uint8Array( basisFile.getImageTranscodedSizeInBytes( 0, mip, config.format ) );
D
Don McCurdy 已提交
467

468 469 470 471 472 473 474 475 476 477 478 479 480
			var status = basisFile.transcodeImage(
				dst,
				0,
				mip,
				config.format,
				config.etcSupported ? 0 : ( config.dxtSupported ? 1 : 0 ),
				0
			);

			if ( ! status ) {

				cleanup();
				throw new Error( 'THREE.BasisTextureLoader: .transcodeImage failed.' );
D
Don McCurdy 已提交
481

482
			}
D
Don McCurdy 已提交
483

484
			mipmaps.push( { data: dst, width: mipWidth, height: mipHeight } );
D
Don McCurdy 已提交
485 486 487

		}

488 489 490
		cleanup();

		return { width, height, mipmaps };
D
Don McCurdy 已提交
491 492 493 494

	}

};