|
@@ -6,9 +6,6 @@
|
|
* a wide variety of GPU texture compression formats. While KTX 2.0 also allows
|
|
* 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.
|
|
* 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:
|
|
* References:
|
|
* - KTX: http://github.khronos.org/KTX-Specification/
|
|
* - KTX: http://github.khronos.org/KTX-Specification/
|
|
* - DFD: https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.html#basicdescriptor
|
|
* - DFD: https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.html#basicdescriptor
|
|
@@ -16,33 +13,43 @@
|
|
|
|
|
|
import {
|
|
import {
|
|
CompressedTexture,
|
|
CompressedTexture,
|
|
- CompressedTextureLoader,
|
|
|
|
FileLoader,
|
|
FileLoader,
|
|
LinearEncoding,
|
|
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,
|
|
sRGBEncoding,
|
|
|
|
+ UnsignedByteType
|
|
} from '../../../build/three.module.js';
|
|
} 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 ) {
|
|
constructor( manager ) {
|
|
|
|
|
|
super( 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' ) {
|
|
if ( typeof MSC_TRANSCODER !== 'undefined' ) {
|
|
|
|
|
|
@@ -59,15 +66,15 @@ class KTX2Loader extends CompressedTextureLoader {
|
|
|
|
|
|
setTranscoderPath( path ) {
|
|
setTranscoderPath( path ) {
|
|
|
|
|
|
- this.basisLoader.setTranscoderPath( path );
|
|
|
|
|
|
+ this.transcoderPath = path;
|
|
|
|
|
|
return this;
|
|
return this;
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
- setWorkerLimit( path ) {
|
|
|
|
|
|
+ setWorkerLimit( num ) {
|
|
|
|
|
|
- this.basisLoader.setWorkerLimit( path );
|
|
|
|
|
|
+ this.workerPool.setWorkerLimit( num );
|
|
|
|
|
|
return this;
|
|
return this;
|
|
|
|
|
|
@@ -75,7 +82,15 @@ class KTX2Loader extends CompressedTextureLoader {
|
|
|
|
|
|
detectSupport( renderer ) {
|
|
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;
|
|
return this;
|
|
|
|
|
|
@@ -83,200 +98,462 @@ class KTX2Loader extends CompressedTextureLoader {
|
|
|
|
|
|
dispose() {
|
|
dispose() {
|
|
|
|
|
|
- this.basisLoader.dispose();
|
|
|
|
|
|
+ this.workerPool.dispose();
|
|
|
|
+ if ( this.workerSourceURL ) URL.revokeObjectURL( this.workerSourceURL );
|
|
|
|
|
|
return this;
|
|
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 ) {
|
|
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.copy( _texture );
|
|
texture.needsUpdate = true;
|
|
texture.needsUpdate = true;
|
|
|
|
|
|
if ( onLoad ) onLoad( texture );
|
|
if ( onLoad ) onLoad( texture );
|
|
|
|
|
|
- }, onError );
|
|
|
|
|
|
+ } )
|
|
|
|
+ .catch( onError );
|
|
|
|
|
|
- } )
|
|
|
|
- .catch( onError );
|
|
|
|
|
|
+ }, onProgress, onError );
|
|
|
|
|
|
return texture;
|
|
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 };
|
|
export { KTX2Loader };
|