123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- /**
- * Resources used for the implementation:
- * - 3js KTX2 loader: https://github.com/mrdoob/three.js/blob/dfb5c23ce126ec845e4aa240599915fef5375797/examples/jsm/loaders/KTX2Loader.js
- * - Universal Texture Transcoders: https://github.com/KhronosGroup/Universal-Texture-Transcoders
- * - KTX2 specification: http://github.khronos.org/KTX-Specification/
- * - KTX2 binaries to convert files: https://github.com/KhronosGroup/KTX-Software/releases
- * - KTX specification: https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.html
- * - KTX-Software: https://github.com/KhronosGroup/KTX-Software
- */
- import { KTX2FileReader, SupercompressionScheme, IKTX2_ImageDesc } from './ktx2FileReader';
- import { TranscoderManager } from './transcoderManager';
- import { LiteTranscoder_UASTC_ASTC } from './Transcoders/liteTranscoder_UASTC_ASTC';
- import { LiteTranscoder_UASTC_BC7 } from './Transcoders/liteTranscoder_UASTC_BC7';
- import { MSCTranscoder } from './Transcoders/mscTranscoder';
- import { transcodeTarget, sourceTextureFormat } from './transcoder';
- import { ZSTDDecoder } from './zstddec';
- const COMPRESSED_RGBA_BPTC_UNORM_EXT = 0x8E8C;
- const COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0;
- const COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F1;
- const COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3;
- const COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02;
- const COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00;
- const COMPRESSED_RGBA8_ETC2_EAC = 0x9278;
- const COMPRESSED_RGB8_ETC2 = 0x9274;
- const COMPRESSED_RGB_ETC1_WEBGL = 0x8D64;
- const RGBA8Format = 0x8058;
- export interface IDecodedData {
- width: number;
- height: number;
- transcodedFormat: number;
- mipmaps: Array<IMipmap>;
- isInGammaSpace: boolean;
- errors?: string;
- transcoderName?: string;
- }
- export interface IMipmap {
- data: Uint8Array | null;
- width: number;
- height: number;
- }
- export interface ICompressedFormatCapabilities {
- astc?: boolean;
- bptc?: boolean;
- s3tc?: boolean;
- pvrtc?: boolean;
- etc2?: boolean;
- etc1?: boolean;
- }
- export interface IKTX2DecoderOptions {
- /** use RGBA format if ASTC and BC7 are not available as transcoded format */
- useRGBAIfASTCBC7NotAvailableWhenUASTC?: boolean;
- /** force to always use RGBA for transcoded format */
- forceRGBA?: boolean;
- /**
- * list of transcoders to bypass when looking for a suitable transcoder. The available transcoders are:
- * UniversalTranscoder_UASTC_ASTC
- * UniversalTranscoder_UASTC_BC7
- * MSCTranscoder
- */
- bypassTranscoders?: string[];
- }
- const isPowerOfTwo = (value: number) => {
- return (value & (value - 1)) === 0 && value !== 0;
- };
- /**
- * Class for decoding KTX2 files
- *
- */
- export class KTX2Decoder {
- private _transcoderMgr: TranscoderManager;
- private _zstdDecoder: ZSTDDecoder;
- constructor() {
- this._transcoderMgr = new TranscoderManager();
- }
- public decode(data: Uint8Array, caps: ICompressedFormatCapabilities, options?: IKTX2DecoderOptions): Promise<IDecodedData | null> {
- return Promise.resolve().then(() => {
- const kfr = new KTX2FileReader(data);
- if (!kfr.isValid()) {
- throw new Error("Invalid KT2 file: wrong signature");
- }
- kfr.parse();
- if (kfr.needZSTDDecoder) {
- if (!this._zstdDecoder) {
- this._zstdDecoder = new ZSTDDecoder();
- }
- return this._zstdDecoder.init().then(() => {
- return this._decodeData(kfr, caps, options);
- });
- }
- return this._decodeData(kfr, caps, options);
- });
- }
- private _decodeData(kfr: KTX2FileReader, caps: ICompressedFormatCapabilities, options?: IKTX2DecoderOptions): Promise<IDecodedData> {
- const width = kfr.header.pixelWidth;
- const height = kfr.header.pixelHeight;
- const srcTexFormat = kfr.textureFormat;
- // PVRTC1 transcoders (from both ETC1S and UASTC) only support power of 2 dimensions.
- const pvrtcTranscodable = isPowerOfTwo(width) && isPowerOfTwo(height);
- let targetFormat = -1;
- let transcodedFormat = -1;
- let roundToMultiple4 = true;
- if (options?.forceRGBA) {
- targetFormat = transcodeTarget.RGBA32;
- transcodedFormat = RGBA8Format;
- roundToMultiple4 = false;
- } else if (caps.astc) {
- targetFormat = transcodeTarget.ASTC_4x4_RGBA;
- transcodedFormat = COMPRESSED_RGBA_ASTC_4x4_KHR;
- } else if (caps.bptc) {
- targetFormat = transcodeTarget.BC7_RGBA;
- transcodedFormat = COMPRESSED_RGBA_BPTC_UNORM_EXT;
- } else if (options?.useRGBAIfASTCBC7NotAvailableWhenUASTC && srcTexFormat === sourceTextureFormat.UASTC4x4) {
- targetFormat = transcodeTarget.RGBA32;
- transcodedFormat = RGBA8Format;
- roundToMultiple4 = false;
- } else if (caps.s3tc) {
- targetFormat = kfr.hasAlpha ? transcodeTarget.BC3_RGBA : transcodeTarget.BC1_RGB;
- transcodedFormat = kfr.hasAlpha ? COMPRESSED_RGBA_S3TC_DXT5_EXT : COMPRESSED_RGB_S3TC_DXT1_EXT;
- } else if (caps.pvrtc && pvrtcTranscodable) {
- targetFormat = kfr.hasAlpha ? transcodeTarget.PVRTC1_4_RGBA : transcodeTarget.PVRTC1_4_RGB;
- transcodedFormat = kfr.hasAlpha ? COMPRESSED_RGBA_PVRTC_4BPPV1_IMG : COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
- } else if (caps.etc2) {
- targetFormat = kfr.hasAlpha ? transcodeTarget.ETC2_RGBA : transcodeTarget.ETC1_RGB /* subset of ETC2 */;
- transcodedFormat = kfr.hasAlpha ? COMPRESSED_RGBA8_ETC2_EAC : COMPRESSED_RGB8_ETC2;
- } else if (caps.etc1) {
- targetFormat = transcodeTarget.ETC1_RGB;
- transcodedFormat = COMPRESSED_RGB_ETC1_WEBGL;
- } else {
- targetFormat = transcodeTarget.RGBA32;
- transcodedFormat = RGBA8Format;
- roundToMultiple4 = false;
- }
- const transcoder = this._transcoderMgr.findTranscoder(srcTexFormat, targetFormat, options?.bypassTranscoders);
- if (transcoder === null) {
- throw new Error(`no transcoder found to transcode source texture format "${sourceTextureFormat[srcTexFormat]}" to format "${transcodeTarget[targetFormat]}"`);
- }
- const mipmaps: Array<IMipmap> = [];
- const dataPromises: Array<Promise<Uint8Array | null>> = [];
- const mipmapBuffers: Array<ArrayBuffer> = [];
- const decodedData: IDecodedData = { width: 0, height: 0, transcodedFormat, mipmaps, isInGammaSpace: kfr.isInGammaSpace, transcoderName: transcoder.getName() };
- let firstImageDescIndex = 0;
- for (let level = 0; level < kfr.header.levelCount; level ++) {
- if (level > 0) {
- firstImageDescIndex += Math.max(kfr.header.layerCount, 1) * kfr.header.faceCount * Math.max(kfr.header.pixelDepth >> (level - 1), 1);
- }
- const levelWidth = Math.floor(width / (1 << level));
- const levelHeight = Math.floor(height / (1 << level));
- const numImagesInLevel = kfr.header.faceCount; // note that cubemap are not supported yet (see KTX2FileReader), so faceCount == 1
- const levelImageByteLength = ((levelWidth + 3) >> 2) * ((levelHeight + 3) >> 2) * kfr.dfdBlock.bytesPlane[0];
- const levelUncompressedByteLength = kfr.levels[level].uncompressedByteLength;
- let levelDataBuffer = kfr.data.buffer;
- let levelDataOffset = kfr.levels[level].byteOffset + kfr.data.byteOffset;
- let imageOffsetInLevel = 0;
- if (kfr.header.supercompressionScheme === SupercompressionScheme.ZStandard) {
- levelDataBuffer = this._zstdDecoder.decode(new Uint8Array(levelDataBuffer, levelDataOffset, kfr.levels[level].byteLength), levelUncompressedByteLength);
- levelDataOffset = 0;
- }
- if (level === 0) {
- decodedData.width = roundToMultiple4 ? (levelWidth + 3) & ~3 : levelWidth;
- decodedData.height = roundToMultiple4 ? (levelHeight + 3) & ~3 : levelHeight;
- }
- for (let imageIndex = 0; imageIndex < numImagesInLevel; imageIndex ++) {
- let encodedData: Uint8Array;
- let imageDesc: IKTX2_ImageDesc | null = null;
- if (kfr.header.supercompressionScheme === SupercompressionScheme.BasisLZ) {
- imageDesc = kfr.supercompressionGlobalData.imageDescs![firstImageDescIndex + imageIndex];
- encodedData = new Uint8Array(levelDataBuffer, levelDataOffset + imageDesc.rgbSliceByteOffset, imageDesc.rgbSliceByteLength + imageDesc.alphaSliceByteLength);
- } else {
- encodedData = new Uint8Array(levelDataBuffer, levelDataOffset + imageOffsetInLevel, levelImageByteLength);
- imageOffsetInLevel += levelImageByteLength;
- }
- const mipmap: IMipmap = {
- data: null,
- width: levelWidth,
- height: levelHeight,
- };
- const transcodedData = transcoder.transcode(srcTexFormat, targetFormat, level, levelWidth, levelHeight, levelUncompressedByteLength, kfr, imageDesc, encodedData)
- .then((data) => {
- mipmap.data = data;
- if (data) {
- mipmapBuffers.push(data.buffer);
- }
- return data;
- })
- .catch((reason) => {
- decodedData.errors = decodedData.errors ?? "";
- decodedData.errors += reason + "\n";
- return null;
- });
- dataPromises.push(transcodedData);
- mipmaps.push(mipmap);
- }
- }
- return Promise.all(dataPromises).then(() => {
- return decodedData;
- });
- }
- }
- // Put in the order you want the transcoders to be used in priority
- TranscoderManager.RegisterTranscoder(LiteTranscoder_UASTC_ASTC);
- TranscoderManager.RegisterTranscoder(LiteTranscoder_UASTC_BC7);
- TranscoderManager.RegisterTranscoder(MSCTranscoder); // catch all transcoder - will throw an error if the format can't be transcoded
|