123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743 |
- /**
- * References:
- * - KTX: http://github.khronos.org/KTX-Specification/
- * - DFD: https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.html#basicdescriptor
- *
- * To do:
- * - [ ] High-quality demo
- * - [ ] Documentation
- * - [ ] (Optional) Include BC5
- * - [ ] (Optional) Include EAC RG on mobile (WEBGL_compressed_texture_etc)
- * - [ ] (Optional) Include two-texture output mode (see: clearcoat + clearcoatRoughness)
- * - [ ] (Optional) Support Web Workers, after #18234
- */
- import {
- CompressedTexture,
- CompressedTextureLoader,
- FileLoader,
- LinearEncoding,
- LinearFilter,
- LinearMipmapLinearFilter,
- MathUtils,
- 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,
- UnsignedByteType,
- sRGBEncoding,
- } from '../../../build/three.module.js';
- import { ZSTDDecoder } from '../libs/zstddec.module.js';
- // Data Format Descriptor (DFD) constants.
- const DFDModel = {
- ETC1S: 163,
- UASTC: 166,
- };
- const DFDChannel = {
- ETC1S: {
- RGB: 0,
- RRR: 3,
- GGG: 4,
- AAA: 15,
- },
- UASTC: {
- RGB: 0,
- RGBA: 3,
- RRR: 4,
- RRRG: 5
- },
- };
- //
- class KTX2Loader extends CompressedTextureLoader {
- constructor( manager ) {
- super( manager );
- this.basisModule = null;
- this.basisModulePending = null;
- this.transcoderConfig = {};
- }
- detectSupport( renderer ) {
- this.transcoderConfig = {
- 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;
- }
- initModule() {
- if ( this.basisModulePending ) {
- return;
- }
- var scope = this;
- // The Emscripten wrapper returns a fake Promise, which can cause
- // infinite recursion when mixed with native Promises. Wrap the module
- // initialization to return a native Promise.
- scope.basisModulePending = new Promise( function ( resolve ) {
- MSC_TRANSCODER().then( function ( basisModule ) { // eslint-disable-line no-undef
- scope.basisModule = basisModule;
- basisModule.initTranscoders();
- resolve();
- } );
- } );
- }
- load( url, onLoad, onProgress, onError ) {
- var scope = this;
- var texture = new CompressedTexture();
- var bufferPending = new Promise( function ( resolve, reject ) {
- new FileLoader( scope.manager )
- .setPath( scope.path )
- .setResponseType( 'arraybuffer' )
- .load( url, resolve, onProgress, reject );
- } );
- // parse() will call initModule() again, but starting the process early
- // should allow the WASM to load in parallel with the texture.
- this.initModule();
- Promise.all( [ bufferPending, this.basisModulePending ] )
- .then( function ( [ buffer ] ) {
- scope.parse( buffer, function ( _texture ) {
- texture.copy( _texture );
- texture.needsUpdate = true;
- if ( onLoad ) onLoad( texture );
- }, onError );
- } )
- .catch( onError );
- return texture;
- }
- parse( buffer, onLoad, onError ) {
- var scope = this;
- // load() may have already called initModule(), but call it again here
- // in case the user called parse() directly. Method is idempotent.
- this.initModule();
- this.basisModulePending.then( function () {
- var BasisLzEtc1sImageTranscoder = scope.basisModule.BasisLzEtc1sImageTranscoder;
- var UastcImageTranscoder = scope.basisModule.UastcImageTranscoder;
- var TextureFormat = scope.basisModule.TextureFormat;
- var ktx = new KTX2Container( scope.basisModule, buffer );
- // TODO(donmccurdy): Should test if texture is transcodable before attempting
- // any transcoding. If supercompressionScheme is KTX_SS_BASIS_LZ and dfd
- // colorModel is ETC1S (163) or if dfd colorModel is UASTCF (166)
- // then texture must be transcoded.
- var transcoder = ktx.getTexFormat() === TextureFormat.UASTC4x4
- ? new UastcImageTranscoder()
- : new BasisLzEtc1sImageTranscoder();
- ktx.initMipmaps( transcoder, scope.transcoderConfig )
- .then( function () {
- var texture = new CompressedTexture(
- ktx.mipmaps,
- ktx.getWidth(),
- ktx.getHeight(),
- ktx.transcodedFormat,
- UnsignedByteType
- );
- texture.encoding = ktx.getEncoding();
- texture.premultiplyAlpha = ktx.getPremultiplyAlpha();
- texture.minFilter = ktx.mipmaps.length === 1 ? LinearFilter : LinearMipmapLinearFilter;
- texture.magFilter = LinearFilter;
- onLoad( texture );
- } )
- .catch( onError );
- } );
- return this;
- }
- }
- class KTX2Container {
- constructor( basisModule, arrayBuffer ) {
- this.basisModule = basisModule;
- this.arrayBuffer = arrayBuffer;
- this.zstd = new ZSTDDecoder();
- this.zstd.init();
- this.mipmaps = null;
- this.transcodedFormat = null;
- // Confirm this is a KTX 2.0 file, based on the identifier in the first 12 bytes.
- var idByteLength = 12;
- var id = new Uint8Array( this.arrayBuffer, 0, idByteLength );
- if ( id[ 0 ] !== 0xAB || // '´'
- id[ 1 ] !== 0x4B || // 'K'
- id[ 2 ] !== 0x54 || // 'T'
- id[ 3 ] !== 0x58 || // 'X'
- id[ 4 ] !== 0x20 || // ' '
- id[ 5 ] !== 0x32 || // '2'
- id[ 6 ] !== 0x30 || // '0'
- id[ 7 ] !== 0xBB || // 'ª'
- id[ 8 ] !== 0x0D || // '\r'
- id[ 9 ] !== 0x0A || // '\n'
- id[ 10 ] !== 0x1A || // '\x1A'
- id[ 11 ] !== 0x0A // '\n'
- ) {
- throw new Error( 'THREE.KTX2Loader: Missing KTX 2.0 identifier.' );
- }
- // TODO(donmccurdy): If we need to support BE, derive this from typeSize.
- var littleEndian = true;
- ///////////////////////////////////////////////////
- // Header.
- ///////////////////////////////////////////////////
- var headerByteLength = 17 * Uint32Array.BYTES_PER_ELEMENT;
- var headerReader = new KTX2BufferReader( this.arrayBuffer, idByteLength, headerByteLength, littleEndian );
- this.header = {
- vkFormat: headerReader.nextUint32(),
- typeSize: headerReader.nextUint32(),
- pixelWidth: headerReader.nextUint32(),
- pixelHeight: headerReader.nextUint32(),
- pixelDepth: headerReader.nextUint32(),
- arrayElementCount: headerReader.nextUint32(),
- faceCount: headerReader.nextUint32(),
- levelCount: headerReader.nextUint32(),
- supercompressionScheme: headerReader.nextUint32(),
- dfdByteOffset: headerReader.nextUint32(),
- dfdByteLength: headerReader.nextUint32(),
- kvdByteOffset: headerReader.nextUint32(),
- kvdByteLength: headerReader.nextUint32(),
- sgdByteOffset: headerReader.nextUint64(),
- sgdByteLength: headerReader.nextUint64(),
- };
- if ( this.header.pixelDepth > 0 ) {
- throw new Error( 'THREE.KTX2Loader: Only 2D textures are currently supported.' );
- }
- if ( this.header.arrayElementCount > 1 ) {
- throw new Error( 'THREE.KTX2Loader: Array textures are not currently supported.' );
- }
- if ( this.header.faceCount > 1 ) {
- throw new Error( 'THREE.KTX2Loader: Cube textures are not currently supported.' );
- }
- ///////////////////////////////////////////////////
- // Level index
- ///////////////////////////////////////////////////
- var levelByteLength = this.header.levelCount * 3 * 8;
- var levelReader = new KTX2BufferReader( this.arrayBuffer, idByteLength + headerByteLength, levelByteLength, littleEndian );
- this.levels = [];
- for ( var i = 0; i < this.header.levelCount; i ++ ) {
- this.levels.push( {
- byteOffset: levelReader.nextUint64(),
- byteLength: levelReader.nextUint64(),
- uncompressedByteLength: levelReader.nextUint64(),
- } );
- }
- ///////////////////////////////////////////////////
- // Data Format Descriptor (DFD)
- ///////////////////////////////////////////////////
- var dfdReader = new KTX2BufferReader(
- this.arrayBuffer,
- this.header.dfdByteOffset,
- this.header.dfdByteLength,
- littleEndian
- );
- const sampleStart = 6;
- const sampleWords = 4;
- this.dfd = {
- vendorId: dfdReader.skip( 4 /* totalSize */ ).nextUint16(),
- versionNumber: dfdReader.skip( 2 /* descriptorType */ ).nextUint16(),
- descriptorBlockSize: dfdReader.nextUint16(),
- colorModel: dfdReader.nextUint8(),
- colorPrimaries: dfdReader.nextUint8(),
- transferFunction: dfdReader.nextUint8(),
- flags: dfdReader.nextUint8(),
- texelBlockDimension: {
- x: dfdReader.nextUint8() + 1,
- y: dfdReader.nextUint8() + 1,
- z: dfdReader.nextUint8() + 1,
- w: dfdReader.nextUint8() + 1,
- },
- bytesPlane0: dfdReader.nextUint8(),
- numSamples: 0,
- samples: [],
- };
- this.dfd.numSamples = ( this.dfd.descriptorBlockSize / 4 - sampleStart ) / sampleWords;
- dfdReader.skip( 7 /* bytesPlane[1-7] */ );
- for ( var i = 0; i < this.dfd.numSamples; i ++ ) {
- this.dfd.samples[ i ] = {
- channelID: dfdReader.skip( 3 /* bitOffset + bitLength */ ).nextUint8(),
- // ... remainder not implemented.
- };
- dfdReader.skip( 12 /* samplePosition[0-3], lower, upper */ );
- }
- if ( this.header.vkFormat !== 0x00 /* VK_FORMAT_UNDEFINED */ &&
- ! ( this.header.supercompressionScheme === 1 /* BasisLZ */ ||
- this.dfd.colorModel === DFDModel.UASTC ) ) {
- throw new Error( 'THREE.KTX2Loader: Only Basis Universal supercompression is currently supported.' );
- }
- ///////////////////////////////////////////////////
- // Key/Value Data (KVD)
- ///////////////////////////////////////////////////
- // Not implemented.
- this.kvd = {};
- ///////////////////////////////////////////////////
- // Supercompression Global Data (SGD)
- ///////////////////////////////////////////////////
- this.sgd = {};
- if ( this.header.sgdByteLength <= 0 ) return;
- var sgdReader = new KTX2BufferReader(
- this.arrayBuffer,
- this.header.sgdByteOffset,
- this.header.sgdByteLength,
- littleEndian
- );
- this.sgd.endpointCount = sgdReader.nextUint16();
- this.sgd.selectorCount = sgdReader.nextUint16();
- this.sgd.endpointsByteLength = sgdReader.nextUint32();
- this.sgd.selectorsByteLength = sgdReader.nextUint32();
- this.sgd.tablesByteLength = sgdReader.nextUint32();
- this.sgd.extendedByteLength = sgdReader.nextUint32();
- this.sgd.imageDescs = [];
- this.sgd.endpointsData = null;
- this.sgd.selectorsData = null;
- this.sgd.tablesData = null;
- this.sgd.extendedData = null;
- for ( var i = 0; i < this.header.levelCount; i ++ ) {
- this.sgd.imageDescs.push( {
- imageFlags: sgdReader.nextUint32(),
- rgbSliceByteOffset: sgdReader.nextUint32(),
- rgbSliceByteLength: sgdReader.nextUint32(),
- alphaSliceByteOffset: sgdReader.nextUint32(),
- alphaSliceByteLength: sgdReader.nextUint32(),
- } );
- }
- var endpointsByteOffset = this.header.sgdByteOffset + sgdReader.offset;
- var selectorsByteOffset = endpointsByteOffset + this.sgd.endpointsByteLength;
- var tablesByteOffset = selectorsByteOffset + this.sgd.selectorsByteLength;
- var extendedByteOffset = tablesByteOffset + this.sgd.tablesByteLength;
- this.sgd.endpointsData = new Uint8Array( this.arrayBuffer, endpointsByteOffset, this.sgd.endpointsByteLength );
- this.sgd.selectorsData = new Uint8Array( this.arrayBuffer, selectorsByteOffset, this.sgd.selectorsByteLength );
- this.sgd.tablesData = new Uint8Array( this.arrayBuffer, tablesByteOffset, this.sgd.tablesByteLength );
- this.sgd.extendedData = new Uint8Array( this.arrayBuffer, extendedByteOffset, this.sgd.extendedByteLength );
- }
- async initMipmaps( transcoder, config ) {
- await this.zstd.init();
- var TranscodeTarget = this.basisModule.TranscodeTarget;
- var TextureFormat = this.basisModule.TextureFormat;
- var ImageInfo = this.basisModule.ImageInfo;
- var scope = this;
- var mipmaps = [];
- var width = this.getWidth();
- var height = this.getHeight();
- var texFormat = this.getTexFormat();
- var hasAlpha = this.getAlpha();
- var isVideo = false;
- // PVRTC1 transcoders (from both ETC1S and UASTC) only support power of 2 dimensions.
- var pvrtcTranscodable = MathUtils.isPowerOfTwo( width ) && MathUtils.isPowerOfTwo( height );
- if ( texFormat === TextureFormat.ETC1S ) {
- var numEndpoints = this.sgd.endpointCount;
- var numSelectors = this.sgd.selectorCount;
- var endpoints = this.sgd.endpointsData;
- var selectors = this.sgd.selectorsData;
- var tables = this.sgd.tablesData;
- transcoder.decodePalettes( numEndpoints, endpoints, numSelectors, selectors );
- transcoder.decodeTables( tables );
- }
- var targetFormat;
- if ( config.astcSupported ) {
- targetFormat = TranscodeTarget.ASTC_4x4_RGBA;
- this.transcodedFormat = RGBA_ASTC_4x4_Format;
- } else if ( config.bptcSupported && texFormat === TextureFormat.UASTC4x4 ) {
- targetFormat = TranscodeTarget.BC7_M5_RGBA;
- this.transcodedFormat = RGBA_BPTC_Format;
- } else if ( config.dxtSupported ) {
- targetFormat = hasAlpha ? TranscodeTarget.BC3_RGBA : TranscodeTarget.BC1_RGB;
- this.transcodedFormat = hasAlpha ? RGBA_S3TC_DXT5_Format : RGB_S3TC_DXT1_Format;
- } else if ( config.pvrtcSupported && pvrtcTranscodable ) {
- targetFormat = hasAlpha ? TranscodeTarget.PVRTC1_4_RGBA : TranscodeTarget.PVRTC1_4_RGB;
- this.transcodedFormat = hasAlpha ? RGBA_PVRTC_4BPPV1_Format : RGB_PVRTC_4BPPV1_Format;
- } else if ( config.etc2Supported ) {
- targetFormat = hasAlpha ? TranscodeTarget.ETC2_RGBA : TranscodeTarget.ETC1_RGB/* subset of ETC2 */;
- this.transcodedFormat = hasAlpha ? RGBA_ETC2_EAC_Format : RGB_ETC2_Format;
- } else if ( config.etc1Supported ) {
- targetFormat = TranscodeTarget.ETC1_RGB;
- this.transcodedFormat = RGB_ETC1_Format;
- } else {
- console.warn( 'THREE.KTX2Loader: No suitable compressed texture format found. Decoding to RGBA32.' );
- targetFormat = TranscodeTarget.RGBA32;
- this.transcodedFormat = RGBAFormat;
- }
- if ( ! this.basisModule.isFormatSupported( targetFormat, texFormat ) ) {
- throw new Error( 'THREE.KTX2Loader: Selected texture format not supported by current transcoder build.' );
- }
- var imageDescIndex = 0;
- for ( var level = 0; level < this.header.levelCount; level ++ ) {
- var levelWidth = Math.max( 1, Math.floor( width / Math.pow( 2, level ) ) );
- var levelHeight = Math.max( 1, Math.floor( height / Math.pow( 2, level ) ) );
- var numImagesInLevel = 1; // TODO(donmccurdy): Support cubemaps, arrays and 3D.
- var imageOffsetInLevel = 0;
- var imageInfo = new ImageInfo( texFormat, levelWidth, levelHeight, level );
- var levelByteLength = this.levels[ level ].byteLength;
- var levelUncompressedByteLength = this.levels[ level ].uncompressedByteLength;
- for ( var imageIndex = 0; imageIndex < numImagesInLevel; imageIndex ++ ) {
- var result;
- var encodedData;
- if ( texFormat === TextureFormat.UASTC4x4 ) {
- // UASTC
- imageInfo.flags = 0;
- imageInfo.rgbByteOffset = 0;
- imageInfo.rgbByteLength = levelUncompressedByteLength;
- imageInfo.alphaByteOffset = 0;
- imageInfo.alphaByteLength = 0;
- encodedData = new Uint8Array( this.arrayBuffer, this.levels[ level ].byteOffset + imageOffsetInLevel, levelByteLength );
- if ( this.header.supercompressionScheme === 2 /* ZSTD */ ) {
- encodedData = this.zstd.decode( encodedData, levelUncompressedByteLength );
- }
- result = transcoder.transcodeImage( targetFormat, encodedData, imageInfo, 0, hasAlpha, isVideo );
- } else {
- // ETC1S
- var imageDesc = this.sgd.imageDescs[ imageDescIndex ++ ];
- imageInfo.flags = imageDesc.imageFlags;
- imageInfo.rgbByteOffset = 0;
- imageInfo.rgbByteLength = imageDesc.rgbSliceByteLength;
- imageInfo.alphaByteOffset = imageDesc.alphaSliceByteOffset > 0 ? imageDesc.rgbSliceByteLength : 0;
- imageInfo.alphaByteLength = imageDesc.alphaSliceByteLength;
- encodedData = new Uint8Array( this.arrayBuffer, this.levels[ level ].byteOffset + imageDesc.rgbSliceByteOffset, imageDesc.rgbSliceByteLength + imageDesc.alphaSliceByteLength );
- result = transcoder.transcodeImage( targetFormat, encodedData, imageInfo, 0, isVideo );
- }
- if ( result.transcodedImage === undefined ) {
- throw new Error( 'THREE.KTX2Loader: Unable to transcode image.' );
- }
- // Transcoded image is written in memory allocated by WASM. We could avoid copying
- // the image by waiting until the image is uploaded to the GPU, then calling
- // delete(). However, (1) we don't know if the user will later need to re-upload it
- // e.g. after calling texture.clone(), and (2) this code will eventually be in a
- // Web Worker, and transferring WASM's memory seems like a very bad idea.
- var levelData = result.transcodedImage.get_typed_memory_view().slice();
- result.transcodedImage.delete();
- mipmaps.push( { data: levelData, width: levelWidth, height: levelHeight } );
- imageOffsetInLevel += levelByteLength;
- }
- }
- scope.mipmaps = mipmaps;
- }
- getWidth() {
- return this.header.pixelWidth;
- }
- getHeight() {
- return this.header.pixelHeight;
- }
- getEncoding() {
- return this.dfd.transferFunction === 2 /* KHR_DF_TRANSFER_SRGB */
- ? sRGBEncoding
- : LinearEncoding;
- }
- getTexFormat() {
- var TextureFormat = this.basisModule.TextureFormat;
- return this.dfd.colorModel === DFDModel.UASTC ? TextureFormat.UASTC4x4 : TextureFormat.ETC1S;
- }
- getAlpha() {
- var TextureFormat = this.basisModule.TextureFormat;
- // TODO(donmccurdy): Handle all channelIDs (i.e. the R & R+G cases),
- // choosing appropriate transcode target formats or providing queries
- // for applications so they know what to do with the content.
- if ( this.getTexFormat() === TextureFormat.UASTC4x4 ) {
- // UASTC
- if ( ( this.dfd.samples[ 0 ].channelID & 0xF ) === DFDChannel.UASTC.RGBA ) {
- return true;
- }
- return false;
- }
- // ETC1S
- if ( this.dfd.numSamples === 2 && ( this.dfd.samples[ 1 ].channelID & 0xF ) === DFDChannel.ETC1S.AAA ) {
- return true;
- }
- return false;
- }
- getPremultiplyAlpha() {
- return !! ( this.dfd.flags & 1 /* KHR_DF_FLAG_ALPHA_PREMULTIPLIED */ );
- }
- }
- class KTX2BufferReader {
- constructor( arrayBuffer, byteOffset, byteLength, littleEndian ) {
- this.dataView = new DataView( arrayBuffer, byteOffset, byteLength );
- this.littleEndian = littleEndian;
- this.offset = 0;
- }
- nextUint8() {
- var value = this.dataView.getUint8( this.offset, this.littleEndian );
- this.offset += 1;
- return value;
- }
- nextUint16() {
- var value = this.dataView.getUint16( this.offset, this.littleEndian );
- this.offset += 2;
- return value;
- }
- nextUint32() {
- var value = this.dataView.getUint32( this.offset, this.littleEndian );
- this.offset += 4;
- return value;
- }
- nextUint64() {
- // https://stackoverflow.com/questions/53103695/
- var left = this.dataView.getUint32( this.offset, this.littleEndian );
- var right = this.dataView.getUint32( this.offset + 4, this.littleEndian );
- var value = this.littleEndian ? left + ( 2 ** 32 * right ) : ( 2 ** 32 * left ) + right;
- if ( ! Number.isSafeInteger( value ) ) {
- console.warn( 'THREE.KTX2Loader: ' + value + ' exceeds MAX_SAFE_INTEGER. Precision may be lost.' );
- }
- this.offset += 8;
- return value;
- }
- skip( bytes ) {
- this.offset += bytes;
- return this;
- }
- }
- export { KTX2Loader };
|