浏览代码

enable zstddec decode in web worker in KTX2Loader (#21984)

* feat: add zstddec worker

* fix buffer.slice

* fix: indentation

* fix: indentation

* fix: indentation

* fix: indentation

* feat: update basis_transcoder & KTX2Loader & WorkPool locate worker on demand & add sample_uastc_zstd.ktx2

* remove zstddec & zstddec.worker

* misc: code format

* fix: typo queue
DeepKolos 4 年之前
父节点
当前提交
9a31750a24

文件差异内容过多而无法显示
+ 0 - 0
examples/js/libs/basis/basis_transcoder.js


二进制
examples/js/libs/basis/basis_transcoder.wasm


文件差异内容过多而无法显示
+ 0 - 114
examples/jsm/libs/zstddec.module.js


+ 398 - 121
examples/jsm/loaders/KTX2Loader.js

@@ -6,9 +6,6 @@
  * a wide variety of GPU texture compression formats. While KTX 2.0 also allows
  * other hardware-specific formats, this loader does not yet parse them.
  *
- * This loader parses the KTX 2.0 container and then relies on
- * THREE.BasisTextureLoader to complete the transcoding process.
- *
  * References:
  * - KTX: http://github.khronos.org/KTX-Specification/
  * - DFD: https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.html#basicdescriptor
@@ -16,33 +13,43 @@
 
 import {
 	CompressedTexture,
-	CompressedTextureLoader,
 	FileLoader,
 	LinearEncoding,
+	LinearFilter,
+	LinearMipmapLinearFilter,
+	Loader,
+	RGBAFormat,
+	RGBA_ASTC_4x4_Format,
+	RGBA_BPTC_Format,
+	RGBA_ETC2_EAC_Format,
+	RGBA_PVRTC_4BPPV1_Format,
+	RGBA_S3TC_DXT5_Format,
+	RGB_ETC1_Format,
+	RGB_ETC2_Format,
+	RGB_PVRTC_4BPPV1_Format,
+	RGB_S3TC_DXT1_Format,
 	sRGBEncoding,
+	UnsignedByteType
 } from '../../../build/three.module.js';
-import {
-	read as readKTX,
-	KTX2ChannelETC1S,
-	KTX2ChannelUASTC,
-	KTX2Flags,
-	KTX2Model,
-	KTX2SupercompressionScheme,
-	KTX2Transfer
-} from '../libs/ktx-parse.module.js';
-import { BasisTextureLoader } from './BasisTextureLoader.js';
-import { ZSTDDecoder } from '../libs/zstddec.module.js';
-
-class KTX2Loader extends CompressedTextureLoader {
+import { WorkerPool } from '../utils/WorkerPool.js'
+
+const KTX2TransferSRGB = 2;
+const KTX2_ALPHA_PREMULTIPLIED = 1;
+const _taskCache = new WeakMap();
+
+class KTX2Loader extends Loader {
 
 	constructor( manager ) {
 
 		super( manager );
 
-		this.basisLoader = new BasisTextureLoader( manager );
-		this.zstd = new ZSTDDecoder();
+		this.transcoderPath = '';
+		this.transcoderBinary = null;
+		this.transcoderPending = null;
 
-		this.zstd.init();
+		this.workerPool = new WorkerPool();
+		this.workerSourceURL = '';
+		this.workerConfig = null;
 
 		if ( typeof MSC_TRANSCODER !== 'undefined' ) {
 
@@ -59,15 +66,15 @@ class KTX2Loader extends CompressedTextureLoader {
 
 	setTranscoderPath( path ) {
 
-		this.basisLoader.setTranscoderPath( path );
+		this.transcoderPath = path;
 
 		return this;
 
 	}
 
-	setWorkerLimit( path ) {
+	setWorkerLimit( num ) {
 
-		this.basisLoader.setWorkerLimit( path );
+		this.workerPool.setWorkerLimit( num );
 
 		return this;
 
@@ -75,7 +82,15 @@ class KTX2Loader extends CompressedTextureLoader {
 
 	detectSupport( renderer ) {
 
-		this.basisLoader.detectSupport( renderer );
+		this.workerConfig = {
+			astcSupported: renderer.extensions.has( 'WEBGL_compressed_texture_astc' ),
+			etc1Supported: renderer.extensions.has( 'WEBGL_compressed_texture_etc1' ),
+			etc2Supported: renderer.extensions.has( 'WEBGL_compressed_texture_etc' ),
+			dxtSupported: renderer.extensions.has( 'WEBGL_compressed_texture_s3tc' ),
+			bptcSupported: renderer.extensions.has( 'EXT_texture_compression_bptc' ),
+			pvrtcSupported: renderer.extensions.has( 'WEBGL_compressed_texture_pvrtc' )
+				|| renderer.extensions.has( 'WEBKIT_WEBGL_compressed_texture_pvrtc' )
+		};
 
 		return this;
 
@@ -83,200 +98,462 @@ class KTX2Loader extends CompressedTextureLoader {
 
 	dispose() {
 
-		this.basisLoader.dispose();
+		this.workerPool.dispose();
+		if ( this.workerSourceURL ) URL.revokeObjectURL( this.workerSourceURL );
 
 		return this;
 
 	}
 
+	init() {
+
+		if ( ! this.transcoderPending ) {
+
+			// Load transcoder wrapper.
+			const jsLoader = new FileLoader( this.manager );
+			jsLoader.setPath( this.transcoderPath );
+			jsLoader.setWithCredentials( this.withCredentials );
+			const jsContent = jsLoader.loadAsync( 'basis_transcoder.js' );
+
+			// Load transcoder WASM binary.
+			const binaryLoader = new FileLoader( this.manager );
+			binaryLoader.setPath( this.transcoderPath );
+			binaryLoader.setResponseType( 'arraybuffer' );
+			binaryLoader.setWithCredentials( this.withCredentials );
+			const binaryContent = binaryLoader.loadAsync( 'basis_transcoder.wasm' )
+
+			this.transcoderPending = Promise.all( [ jsContent, binaryContent ] )
+				.then( ( [ jsContent, binaryContent ] ) => {
+
+					const fn = KTX2Loader.BasisWorker.toString();
+
+					const body = [
+						'/* constants */',
+						'let _EngineFormat = ' + JSON.stringify( KTX2Loader.EngineFormat ),
+						'let _TranscoderFormat = ' + JSON.stringify( KTX2Loader.TranscoderFormat ),
+						'let _BasisFormat = ' + JSON.stringify( KTX2Loader.BasisFormat ),
+						'/* basis_transcoder.js */',
+						jsContent,
+						'/* worker */',
+						fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) )
+					].join( '\n' );
+
+					this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) );
+					this.transcoderBinary = binaryContent;
+
+					this.workerPool.setWorkerCreator( () => {
+
+						const worker = new Worker( this.workerSourceURL );
+						const transcoderBinary = this.transcoderBinary.slice( 0 );
+			
+						worker.postMessage( { type: 'init', config: this.workerConfig, transcoderBinary }, [ transcoderBinary ] );
+			
+						return worker;
+			
+					} );
+
+				} );
+
+		}
+
+		return this.transcoderPending;
+
+	}
+
 	load( url, onLoad, onProgress, onError ) {
 
-		var scope = this;
+		const loader = new FileLoader( this.manager );
 
-		var texture = new CompressedTexture();
+		loader.setResponseType( 'arraybuffer' );
+		loader.setWithCredentials( this.withCredentials );
 
-		var bufferPending = new Promise( function ( resolve, reject ) {
+		const texture = new CompressedTexture();
 
-			new FileLoader( scope.manager )
-				.setPath( scope.path )
-				.setResponseType( 'arraybuffer' )
-				.load( url, resolve, onProgress, reject );
+		loader.load( url, ( buffer ) => {
 
-		} );
+			// Check for an existing task using this buffer. A transferred buffer cannot be transferred
+			// again from this thread.
+			if ( _taskCache.has( buffer ) ) {
+
+				const cachedTask = _taskCache.get( buffer );
 
-		bufferPending
-			.then( function ( buffer ) {
+				return cachedTask.promise.then( onLoad ).catch( onError );
+
+			}
 
-				scope.parse( buffer, function ( _texture ) {
+			this._createTexture( [ buffer ] )
+				.then( function ( _texture ) {
 
 					texture.copy( _texture );
 					texture.needsUpdate = true;
 
 					if ( onLoad ) onLoad( texture );
 
-				}, onError );
+				} )
+				.catch( onError );
 
-			} )
-			.catch( onError );
+		}, onProgress, onError );
 
 		return texture;
 
 	}
 
-	parse( buffer, onLoad, onError ) {
+	createTextureFrom( transcodeResult ) {
+		const { mipmaps, width, height, format, type, error, dfdTransferFn, dfdFlags } = transcodeResult;
 
-		var scope = this;
+		if ( type === 'error' ) return Promise.reject( error );
 
-		var ktx = readKTX( new Uint8Array( buffer ) );
+		const texture = new CompressedTexture( mipmaps, width, height, format, UnsignedByteType );
+		texture.minFilter = mipmaps.length === 1 ? LinearFilter : LinearMipmapLinearFilter;
+		texture.magFilter = LinearFilter;
+		texture.generateMipmaps = false;
+		texture.needsUpdate = true;
+		texture.encoding = dfdTransferFn === KTX2TransferSRGB ? sRGBEncoding: LinearEncoding;
+		texture.premultiplyAlpha = !! ( dfdFlags & KTX2_ALPHA_PREMULTIPLIED );
 
-		if ( ktx.pixelDepth > 0 ) {
+		return texture;
 
-			throw new Error( 'THREE.KTX2Loader: Only 2D textures are currently supported.' );
+	}
 
-		}
+	/**
+	 * @param {ArrayBuffer[]} buffers
+	 * @param {object?} config
+	 * @return {Promise<CompressedTexture>}
+	 */
+	_createTexture( buffers, config = {} ) {
 
-		if ( ktx.layerCount > 1 ) {
+		const taskConfig = config;
+		const texturePending = this.init().then( () => {
 
-			throw new Error( 'THREE.KTX2Loader: Array textures are not currently supported.' );
+			return this.workerPool.postMessage( { type: 'transcode', buffers, taskConfig: taskConfig }, buffers );
 
-		}
+		} ).then( ( e ) => this.createTextureFrom( e.data ) );
 
-		if ( ktx.faceCount > 1 ) {
+		// Cache the task result.
+		_taskCache.set( buffers[ 0 ], { promise: texturePending } );
 
-			throw new Error( 'THREE.KTX2Loader: Cube textures are not currently supported.' );
+		return texturePending;
 
-		}
+	}
 
-		var dfd = KTX2Utils.getBasicDFD( ktx );
+	dispose() {
 
-		KTX2Utils.createLevels( ktx, this.zstd ).then( function ( levels ) {
+		URL.revokeObjectURL( this.workerSourceURL );
+		this.workerPool.dispose();
 
-			var basisFormat = dfd.colorModel === KTX2Model.UASTC
-				? BasisTextureLoader.BasisFormat.UASTC_4x4
-				: BasisTextureLoader.BasisFormat.ETC1S;
+		return this;
 
-			var parseConfig = {
+	}
 
-				levels: levels,
-				width: ktx.pixelWidth,
-				height: ktx.pixelHeight,
-				basisFormat: basisFormat,
-				hasAlpha: KTX2Utils.getAlpha( ktx ),
+}
 
-			};
 
-			if ( basisFormat === BasisTextureLoader.BasisFormat.ETC1S ) {
+/* CONSTANTS */
 
-				parseConfig.globalData = ktx.globalData;
+KTX2Loader.BasisFormat = {
+	ETC1S: 0,
+	UASTC_4x4: 1,
+};
 
-			}
+KTX2Loader.TranscoderFormat = {
+	ETC1: 0,
+	ETC2: 1,
+	BC1: 2,
+	BC3: 3,
+	BC4: 4,
+	BC5: 5,
+	BC7_M6_OPAQUE_ONLY: 6,
+	BC7_M5: 7,
+	PVRTC1_4_RGB: 8,
+	PVRTC1_4_RGBA: 9,
+	ASTC_4x4: 10,
+	ATC_RGB: 11,
+	ATC_RGBA_INTERPOLATED_ALPHA: 12,
+	RGBA32: 13,
+	RGB565: 14,
+	BGR565: 15,
+	RGBA4444: 16,
+};
+
+KTX2Loader.EngineFormat = {
+	RGBAFormat: RGBAFormat,
+	RGBA_ASTC_4x4_Format: RGBA_ASTC_4x4_Format,
+	RGBA_BPTC_Format: RGBA_BPTC_Format,
+	RGBA_ETC2_EAC_Format: RGBA_ETC2_EAC_Format,
+	RGBA_PVRTC_4BPPV1_Format: RGBA_PVRTC_4BPPV1_Format,
+	RGBA_S3TC_DXT5_Format: RGBA_S3TC_DXT5_Format,
+	RGB_ETC1_Format: RGB_ETC1_Format,
+	RGB_ETC2_Format: RGB_ETC2_Format,
+	RGB_PVRTC_4BPPV1_Format: RGB_PVRTC_4BPPV1_Format,
+	RGB_S3TC_DXT1_Format: RGB_S3TC_DXT1_Format,
+};
 
-			return scope.basisLoader.parseInternalAsync( parseConfig );
 
-		} ).then( function ( texture ) {
+/* WEB WORKER */
 
-			texture.encoding = dfd.transferFunction === KTX2Transfer.SRGB
-				? sRGBEncoding
-				: LinearEncoding;
-			texture.premultiplyAlpha = KTX2Utils.getPremultiplyAlpha( ktx );
+KTX2Loader.BasisWorker = function () {
 
-			onLoad( texture );
+	let config;
+	let transcoderPending;
+	let BasisModule;
 
-		} ).catch( onError );
+	const EngineFormat = _EngineFormat; // eslint-disable-line no-undef
+	const TranscoderFormat = _TranscoderFormat; // eslint-disable-line no-undef
+	const BasisFormat = _BasisFormat; // eslint-disable-line no-undef
 
-		return this;
+	self.addEventListener( 'message', function ( e ) {
 
-	}
+		const message = e.data;
 
-}
+		switch ( message.type ) {
+
+			case 'init':
+				config = message.config;
+				init( message.transcoderBinary );
+				break;
+
+			case 'transcode':
+				transcoderPending.then( () => {
+
+					try {
+
+						const { width, height, hasAlpha, mipmaps, format, dfdTransferFn, dfdFlags } = transcode( message.buffers[ 0 ] );
+
+						const buffers = [];
+
+						for ( let i = 0; i < mipmaps.length; ++ i ) {
 
-var KTX2Utils = {
+							buffers.push( mipmaps[ i ].data.buffer );
 
-	createLevels: async function ( ktx, zstd ) {
+						}
 
-		if ( ktx.supercompressionScheme === KTX2SupercompressionScheme.ZSTD ) {
+						self.postMessage( { type: 'transcode', id: message.id, width, height, hasAlpha, mipmaps, format, dfdTransferFn, dfdFlags }, buffers );
 
-			await zstd.init();
+					} catch ( error ) {
+
+						console.error( error );
+
+						self.postMessage( { type: 'error', id: message.id, error: error.message } );
+
+					}
+
+				} );
+				break;
 
 		}
 
-		var levels = [];
-		var width = ktx.pixelWidth;
-		var height = ktx.pixelHeight;
+	} );
 
-		for ( var levelIndex = 0; levelIndex < ktx.levels.length; levelIndex ++ ) {
+	function init( wasmBinary ) {
 
-			var levelWidth = Math.max( 1, Math.floor( width / Math.pow( 2, levelIndex ) ) );
-			var levelHeight = Math.max( 1, Math.floor( height / Math.pow( 2, levelIndex ) ) );
-			var levelData = ktx.levels[ levelIndex ].levelData;
+		transcoderPending = new Promise( ( resolve ) => {
 
-			if ( ktx.supercompressionScheme === KTX2SupercompressionScheme.ZSTD ) {
+			BasisModule = { wasmBinary, onRuntimeInitialized: resolve };
+			BASIS( BasisModule ); // eslint-disable-line no-undef
 
-				levelData = zstd.decode( levelData, ktx.levels[ levelIndex ].uncompressedByteLength );
+		} ).then( () => {
 
-			}
+			BasisModule.initializeBasis();
+
+		} );
+
+	}
 
-			levels.push( {
+	function transcode( buffer ) {
 
-				index: levelIndex,
-				width: levelWidth,
-				height: levelHeight,
-				data: levelData,
+		const ktx2File = new BasisModule.KTX2File( new Uint8Array( buffer ) );
 
-			} );
+		function cleanup() {
+
+			ktx2File.close();
+			ktx2File.delete();
+
+		}
+
+		if ( !ktx2File.isValid() ) {
+
+			cleanup();
+			throw new Error( 'THREE.KTX2Loader:	Invalid or unsupported .ktx2 file' );
 
 		}
 
-		return levels;
+		const basisFormat = ktx2File.isUASTC() ? BasisFormat.UASTC_4x4 : BasisFormat.ETC1S;
+		const width = ktx2File.getWidth();
+		const height = ktx2File.getHeight();
+		const levels = ktx2File.getLevels();
+		const hasAlpha = ktx2File.getHasAlpha();
+		const dfdTransferFn = ktx2File.getDFDTransferFunc();
+		const dfdFlags = ktx2File.getDFDFlags();
 
-	},
+		const { transcoderFormat, engineFormat } = getTranscoderFormat( basisFormat, width, height, hasAlpha );
 
-	getBasicDFD: function ( ktx ) {
+		if ( ! width || ! height || ! levels ) {
 
-		// Basic Data Format Descriptor Block is always the first DFD.
-		return ktx.dataFormatDescriptor[ 0 ];
+			cleanup();
+			throw new Error( 'THREE.KTX2Loader:	Invalid texture' );
 
-	},
+		}
+
+		if ( ! ktx2File.startTranscoding() ) {
 
-	getAlpha: function ( ktx ) {
+			cleanup();
+			throw new Error( 'THREE.KTX2Loader: .startTranscoding failed' );
 
-		var dfd = this.getBasicDFD( ktx );
+		}
 
-		// UASTC
+		const mipmaps = [];
 
-		if ( dfd.colorModel === KTX2Model.UASTC ) {
+		for ( let mip = 0; mip < levels; mip ++ ) {
 
-			if ( ( dfd.samples[ 0 ].channelID & 0xF ) === KTX2ChannelUASTC.RGBA ) {
+			const levelInfo = ktx2File.getImageLevelInfo( mip, 0, 0 )
+			const mipWidth = levelInfo.origWidth;
+			const mipHeight = levelInfo.origHeight;
+			const dst = new Uint8Array( ktx2File.getImageTranscodedSizeInBytes( mip, 0, 0, transcoderFormat ) );
+
+			const status = ktx2File.transcodeImage(
+				dst,
+				mip,
+				0,
+				0,
+				transcoderFormat,
+				0,
+				-1,
+				-1,
+			);
 
-				return true;
+			if ( ! status ) {
+
+				cleanup();
+				throw new Error( 'THREE.KTX2Loader: .transcodeImage failed.' );
 
 			}
 
-			return false;
+			mipmaps.push( { data: dst, width: mipWidth, height: mipHeight } );
 
 		}
 
-		// ETC1S
+		cleanup();
+
+		return { width, height, hasAlpha, mipmaps, format: engineFormat, dfdTransferFn, dfdFlags };
 
-		if ( dfd.samples.length === 2
-			&& ( dfd.samples[ 1 ].channelID & 0xF ) === KTX2ChannelETC1S.AAA ) {
+	}
 
-			return true;
+	//
+
+	// Optimal choice of a transcoder target format depends on the Basis format (ETC1S or UASTC),
+	// device capabilities, and texture dimensions. The list below ranks the formats separately
+	// for ETC1S and UASTC.
+	//
+	// In some cases, transcoding UASTC to RGBA32 might be preferred for higher quality (at
+	// significant memory cost) compared to ETC1/2, BC1/3, and PVRTC. The transcoder currently
+	// chooses RGBA32 only as a last resort and does not expose that option to the caller.
+	const FORMAT_OPTIONS = [
+		{
+			if: 'astcSupported',
+			basisFormat: [ BasisFormat.UASTC_4x4 ],
+			transcoderFormat: [ TranscoderFormat.ASTC_4x4, TranscoderFormat.ASTC_4x4 ],
+			engineFormat: [ EngineFormat.RGBA_ASTC_4x4_Format, EngineFormat.RGBA_ASTC_4x4_Format ],
+			priorityETC1S: Infinity,
+			priorityUASTC: 1,
+			needsPowerOfTwo: false,
+		},
+		{
+			if: 'bptcSupported',
+			basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC_4x4 ],
+			transcoderFormat: [ TranscoderFormat.BC7_M5, TranscoderFormat.BC7_M5 ],
+			engineFormat: [ EngineFormat.RGBA_BPTC_Format, EngineFormat.RGBA_BPTC_Format ],
+			priorityETC1S: 3,
+			priorityUASTC: 2,
+			needsPowerOfTwo: false,
+		},
+		{
+			if: 'dxtSupported',
+			basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC_4x4 ],
+			transcoderFormat: [ TranscoderFormat.BC1, TranscoderFormat.BC3 ],
+			engineFormat: [ EngineFormat.RGB_S3TC_DXT1_Format, EngineFormat.RGBA_S3TC_DXT5_Format ],
+			priorityETC1S: 4,
+			priorityUASTC: 5,
+			needsPowerOfTwo: false,
+		},
+		{
+			if: 'etc2Supported',
+			basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC_4x4 ],
+			transcoderFormat: [ TranscoderFormat.ETC1, TranscoderFormat.ETC2 ],
+			engineFormat: [ EngineFormat.RGB_ETC2_Format, EngineFormat.RGBA_ETC2_EAC_Format ],
+			priorityETC1S: 1,
+			priorityUASTC: 3,
+			needsPowerOfTwo: false,
+		},
+		{
+			if: 'etc1Supported',
+			basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC_4x4 ],
+			transcoderFormat: [ TranscoderFormat.ETC1, TranscoderFormat.ETC1 ],
+			engineFormat: [ EngineFormat.RGB_ETC1_Format, EngineFormat.RGB_ETC1_Format ],
+			priorityETC1S: 2,
+			priorityUASTC: 4,
+			needsPowerOfTwo: false,
+		},
+		{
+			if: 'pvrtcSupported',
+			basisFormat: [ BasisFormat.ETC1S, BasisFormat.UASTC_4x4 ],
+			transcoderFormat: [ TranscoderFormat.PVRTC1_4_RGB, TranscoderFormat.PVRTC1_4_RGBA ],
+			engineFormat: [ EngineFormat.RGB_PVRTC_4BPPV1_Format, EngineFormat.RGBA_PVRTC_4BPPV1_Format ],
+			priorityETC1S: 5,
+			priorityUASTC: 6,
+			needsPowerOfTwo: true,
+		},
+	];
+
+	const ETC1S_OPTIONS = FORMAT_OPTIONS.sort( function ( a, b ) {
+
+		return a.priorityETC1S - b.priorityETC1S;
+
+	} );
+	const UASTC_OPTIONS = FORMAT_OPTIONS.sort( function ( a, b ) {
+
+		return a.priorityUASTC - b.priorityUASTC;
+
+	} );
+
+	function getTranscoderFormat( basisFormat, width, height, hasAlpha ) {
+
+		let transcoderFormat;
+		let engineFormat;
+
+		const options = basisFormat === BasisFormat.ETC1S ? ETC1S_OPTIONS : UASTC_OPTIONS;
+
+		for ( let i = 0; i < options.length; i ++ ) {
+
+			const opt = options[ i ];
+
+			if ( ! config[ opt.if ] ) continue;
+			if ( ! opt.basisFormat.includes( basisFormat ) ) continue;
+			if ( opt.needsPowerOfTwo && ! ( isPowerOfTwo( width ) && isPowerOfTwo( height ) ) ) continue;
+
+			transcoderFormat = opt.transcoderFormat[ hasAlpha ? 1 : 0 ];
+			engineFormat = opt.engineFormat[ hasAlpha ? 1 : 0 ];
+
+			return { transcoderFormat, engineFormat };
 
 		}
 
-		return false;
+		console.warn( 'THREE.KTX2Loader: No suitable compressed texture format found. Decoding to RGBA32.' );
 
-	},
+		transcoderFormat = TranscoderFormat.RGBA32;
+		engineFormat = EngineFormat.RGBAFormat;
 
-	getPremultiplyAlpha: function ( ktx ) {
+		return { transcoderFormat, engineFormat };
 
-		var dfd = this.getBasicDFD( ktx );
+	}
 
-		return !! ( dfd.flags & KTX2Flags.ALPHA_PREMULTIPLIED );
+	function isPowerOfTwo( value ) {
 
-	},
+		if ( value <= 2 ) return true;
 
-};
+		return ( value & ( value - 1 ) ) === 0 && value !== 0;
+
+	}
+
+}
 
 export { KTX2Loader };

+ 102 - 0
examples/jsm/utils/WorkerPool.js

@@ -0,0 +1,102 @@
+/**
+ * @author Deepkolos / https://github.com/deepkolos
+ */
+
+export class WorkerPool {
+
+	constructor ( pool = 4 ) {
+
+		this.pool = pool;
+		this.queue = [];
+		this.workers = [];
+		this.workersResolve = [];
+		this.workerStatus = 0;
+
+	}
+
+	_initWorker ( workerId ) {
+
+		if ( !this.workers[ workerId ] ) {
+
+			const worker = this.workerCreator();
+			worker.addEventListener( 'message', this._onMessage.bind( this, workerId ) );
+			this.workers[ workerId ] = worker;
+
+		}
+
+	}
+
+	_getIdleWorker () {
+
+		for ( let i = 0 ; i < this.pool ; i ++ ) 
+			if ( ! ( this.workerStatus & ( 1 << i ) ) ) return i;
+
+		return -1;
+
+	}
+
+	_onMessage( workerId, msg ) {
+
+		const resolve = this.workersResolve[ workerId ];
+		resolve && resolve( msg );
+
+		if ( this.queue.length ) {
+
+			const { resolve, msg, transfer } = this.queue.shift();
+			this.workersResolve[ workerId ] = resolve;
+			this.workers[ workerId ].postMessage( msg, transfer );
+
+		} else {
+
+			this.workerStatus ^= 1 << workerId;
+
+		}
+
+	}
+
+	setWorkerCreator ( workerCreator ) {
+
+		this.workerCreator = workerCreator;
+
+	}
+
+	setWorkerLimit ( pool ) {
+
+		this.pool = pool;
+
+	}
+
+	postMessage ( msg, transfer ) {
+
+		return new Promise( ( resolve ) => {
+
+			const workerId = this._getIdleWorker();
+
+			if ( workerId !== -1 ) {
+
+				this._initWorker( workerId );
+				this.workerStatus |= 1 << workerId;
+				this.workersResolve[ workerId ] = resolve;
+				this.workers[ workerId ].postMessage( msg, transfer );
+
+			} else {
+
+				this.queue.push( { resolve, msg, transfer } );
+
+			}
+
+		} );
+
+	}
+
+	dispose () {
+
+		this.workers.forEach( ( worker ) => worker.terminate() );
+		this.workersResolve.length = 0;
+		this.workers.length = 0;
+		this.queue.length = 0;
+		this.workerStatus = 0;
+
+	}
+
+}

二进制
examples/textures/compressed/sample_uastc_zstd.ktx2


+ 3 - 2
examples/webgl_loader_texture_ktx2.html

@@ -51,6 +51,7 @@
 
 			const formatStrings = {
 				[ THREE.RGBAFormat ]: "RGBA32",
+				[ THREE.RGBA_BPTC_Format ]: "RGBA_BPTC",
 				[ THREE.RGBA_ASTC_4x4_Format ]: "RGBA_ASTC_4x4",
 				[ THREE.RGB_S3TC_DXT1_Format ]: "RGB_S3TC_DXT1",
 				[ THREE.RGBA_S3TC_DXT5_Format ]: "RGBA_S3TC_DXT5",
@@ -61,11 +62,11 @@
 				[ THREE.RGBA_ETC2_EAC_Format ]: "RGB_ETC2_EAC",
 			};
 
-			// Samples: sample_etc1s.ktx2, sample_uastc.ktx2
+			// Samples: sample_etc1s.ktx2, sample_uastc.ktx2, sample_uastc_zstd.ktx2
 			new KTX2Loader()
 				.setTranscoderPath( 'js/libs/basis/' )
 				.detectSupport( renderer )
-				.load( './textures/compressed/sample_uastc.ktx2', ( texture ) => {
+				.load( './textures/compressed/sample_uastc_zstd.ktx2', ( texture ) => {
 
 					console.info( `transcoded to ${formatStrings[ texture.format ]}` );
 

部分文件因为文件数量过多而无法显示