ktx2Decoder.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. /**
  2. * Resources used for the implementation:
  3. * - 3js KTX2 loader: https://github.com/mrdoob/three.js/blob/dfb5c23ce126ec845e4aa240599915fef5375797/examples/jsm/loaders/KTX2Loader.js
  4. * - Universal Texture Transcoders: https://github.com/KhronosGroup/Universal-Texture-Transcoders
  5. * - KTX2 specification: http://github.khronos.org/KTX-Specification/
  6. * - KTX2 binaries to convert files: https://github.com/KhronosGroup/KTX-Software/releases
  7. * - KTX specification: https://www.khronos.org/registry/DataFormat/specs/1.3/dataformat.1.3.html
  8. * - KTX-Software: https://github.com/KhronosGroup/KTX-Software
  9. */
  10. import { KTX2FileReader, SupercompressionScheme, IKTX2_ImageDesc } from './ktx2FileReader';
  11. import { TranscoderManager } from './transcoderManager';
  12. import { LiteTranscoder_UASTC_ASTC } from './Transcoders/liteTranscoder_UASTC_ASTC';
  13. import { LiteTranscoder_UASTC_BC7 } from './Transcoders/liteTranscoder_UASTC_BC7';
  14. import { MSCTranscoder } from './Transcoders/mscTranscoder';
  15. import { transcodeTarget, sourceTextureFormat } from './transcoder';
  16. import { ZSTDDecoder } from './zstddec';
  17. const COMPRESSED_RGBA_BPTC_UNORM_EXT = 0x8E8C;
  18. const COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0;
  19. const COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0;
  20. const COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3;
  21. const COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02;
  22. const COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00;
  23. const COMPRESSED_RGBA8_ETC2_EAC = 0x9278;
  24. const COMPRESSED_RGB8_ETC2 = 0x9274;
  25. const COMPRESSED_RGB_ETC1_WEBGL = 0x8D64;
  26. const RGBA8Format = 0x8058;
  27. export interface IDecodedData {
  28. width: number;
  29. height: number;
  30. transcodedFormat: number;
  31. mipmaps: Array<IMipmap>;
  32. isInGammaSpace: boolean;
  33. errors?: string;
  34. }
  35. export interface IMipmap {
  36. data: Uint8Array | null;
  37. width: number;
  38. height: number;
  39. }
  40. export interface ICompressedFormatCapabilities {
  41. astc?: boolean;
  42. bptc?: boolean;
  43. s3tc?: boolean;
  44. pvrtc?: boolean;
  45. etc2?: boolean;
  46. etc1?: boolean;
  47. }
  48. const isPowerOfTwo = (value: number) => {
  49. return (value & (value - 1)) === 0 && value !== 0;
  50. };
  51. /**
  52. * Class for decoding KTX2 files
  53. */
  54. export class KTX2Decoder {
  55. private _transcoderMgr: TranscoderManager;
  56. private _zstdDecoder: ZSTDDecoder;
  57. constructor() {
  58. this._transcoderMgr = new TranscoderManager();
  59. }
  60. public decode(data: Uint8Array, caps: ICompressedFormatCapabilities): Promise<IDecodedData | null> {
  61. const kfr = new KTX2FileReader(data);
  62. if (!kfr.isValid()) {
  63. throw new Error("Invalid KT2 file: wrong signature");
  64. }
  65. kfr.parse();
  66. if (kfr.needZSTDDecoder && !this._zstdDecoder) {
  67. this._zstdDecoder = new ZSTDDecoder();
  68. return this._zstdDecoder.init().then(() => {
  69. return this._decodeData(kfr, caps);
  70. });
  71. }
  72. return this._decodeData(kfr, caps);
  73. }
  74. private _decodeData(kfr: KTX2FileReader, caps: ICompressedFormatCapabilities): Promise<IDecodedData> {
  75. const width = kfr.header.pixelWidth;
  76. const height = kfr.header.pixelHeight;
  77. const srcTexFormat = kfr.textureFormat;
  78. // PVRTC1 transcoders (from both ETC1S and UASTC) only support power of 2 dimensions.
  79. const pvrtcTranscodable = isPowerOfTwo(width) && isPowerOfTwo(height);
  80. let targetFormat = -1;
  81. let transcodedFormat = -1;
  82. if (caps.astc) {
  83. targetFormat = transcodeTarget.ASTC_4x4_RGBA;
  84. transcodedFormat = COMPRESSED_RGBA_ASTC_4x4_KHR;
  85. } else if (caps.bptc) {
  86. targetFormat = transcodeTarget.BC7_RGBA;
  87. transcodedFormat = COMPRESSED_RGBA_BPTC_UNORM_EXT;
  88. } else if (caps.s3tc) {
  89. targetFormat = kfr.hasAlpha ? transcodeTarget.BC3_RGBA : transcodeTarget.BC1_RGB;
  90. transcodedFormat = kfr.hasAlpha ? COMPRESSED_RGBA_S3TC_DXT5_EXT : COMPRESSED_RGB_S3TC_DXT1_EXT;
  91. } else if (caps.pvrtc && pvrtcTranscodable) {
  92. targetFormat = kfr.hasAlpha ? transcodeTarget.PVRTC1_4_RGBA : transcodeTarget.PVRTC1_4_RGB;
  93. transcodedFormat = kfr.hasAlpha ? COMPRESSED_RGBA_PVRTC_4BPPV1_IMG : COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
  94. } else if (caps.etc2) {
  95. targetFormat = kfr.hasAlpha ? transcodeTarget.ETC2_RGBA : transcodeTarget.ETC1_RGB /* subset of ETC2 */;
  96. transcodedFormat = kfr.hasAlpha ? COMPRESSED_RGBA8_ETC2_EAC : COMPRESSED_RGB8_ETC2;
  97. } else if (caps.etc1) {
  98. targetFormat = transcodeTarget.ETC1_RGB;
  99. transcodedFormat = COMPRESSED_RGB_ETC1_WEBGL;
  100. } else {
  101. targetFormat = transcodeTarget.RGBA32;
  102. transcodedFormat = RGBA8Format;
  103. }
  104. const transcoder = this._transcoderMgr.findTranscoder(srcTexFormat, targetFormat);
  105. if (transcoder === null) {
  106. throw new Error(`no transcoder found to transcode source texture format "${sourceTextureFormat[srcTexFormat]}" to format "${transcodeTarget[targetFormat]}"`);
  107. }
  108. const mipmaps: Array<IMipmap> = [];
  109. const dataPromises: Array<Promise<Uint8Array | null>> = [];
  110. const mipmapBuffers: Array<ArrayBuffer> = [];
  111. const decodedData: IDecodedData = { width: 0, height: 0, transcodedFormat, mipmaps, isInGammaSpace: kfr.isInGammaSpace };
  112. let firstImageDescIndex = 0;
  113. for (let level = 0; level < kfr.header.levelCount; level ++) {
  114. if (level > 0) {
  115. firstImageDescIndex += Math.max(kfr.header.layerCount, 1) * kfr.header.faceCount * Math.max(kfr.header.pixelDepth >> (level - 1), 1);
  116. }
  117. const levelWidth = Math.ceil(width / (1 << level));
  118. const levelHeight = Math.ceil(height / (1 << level));
  119. const numImagesInLevel = kfr.header.faceCount; // note that cubemap are not supported yet (see KTX2FileReader), so faceCount == 1
  120. const levelImageByteLength = ((levelWidth + 3) >> 2) * ((levelHeight + 3) >> 2) * kfr.dfdBlock.bytesPlane[0];
  121. const levelUncompressedByteLength = kfr.levels[level].uncompressedByteLength;
  122. let levelDataBuffer = kfr.data.buffer;
  123. let levelDataOffset = kfr.levels[level].byteOffset + kfr.data.byteOffset;
  124. let imageOffsetInLevel = 0;
  125. if (kfr.header.supercompressionScheme === SupercompressionScheme.ZStandard) {
  126. levelDataBuffer = this._zstdDecoder.decode(new Uint8Array(levelDataBuffer, levelDataOffset, kfr.levels[level].byteLength), levelUncompressedByteLength);
  127. levelDataOffset = 0;
  128. }
  129. if (level === 0) {
  130. decodedData.width = levelWidth;
  131. decodedData.height = levelHeight;
  132. }
  133. for (let imageIndex = 0; imageIndex < numImagesInLevel; imageIndex ++) {
  134. let encodedData: Uint8Array;
  135. let imageDesc: IKTX2_ImageDesc | null = null;
  136. if (kfr.header.supercompressionScheme === SupercompressionScheme.BasisLZ) {
  137. imageDesc = kfr.supercompressionGlobalData.imageDescs![firstImageDescIndex + imageIndex];
  138. encodedData = new Uint8Array(levelDataBuffer, levelDataOffset + imageDesc.rgbSliceByteOffset, imageDesc.rgbSliceByteLength + imageDesc.alphaSliceByteLength);
  139. } else {
  140. encodedData = new Uint8Array(levelDataBuffer, levelDataOffset + imageOffsetInLevel, levelImageByteLength);
  141. imageOffsetInLevel += levelImageByteLength;
  142. }
  143. const mipmap: IMipmap = {
  144. data: null,
  145. width: levelWidth,
  146. height: levelHeight,
  147. };
  148. const transcodedData = transcoder.transcode(srcTexFormat, targetFormat, level, levelWidth, levelHeight, levelUncompressedByteLength, kfr, imageDesc, encodedData)
  149. .then((data) => {
  150. mipmap.data = data;
  151. if (data) {
  152. mipmapBuffers.push(data.buffer);
  153. }
  154. return data;
  155. })
  156. .catch((reason) => {
  157. decodedData.errors = decodedData.errors ?? "";
  158. decodedData.errors += reason + "\n";
  159. return null;
  160. });
  161. dataPromises.push(transcodedData);
  162. mipmaps.push(mipmap);
  163. }
  164. }
  165. return Promise.all(dataPromises).then(() => {
  166. return decodedData;
  167. });
  168. }
  169. }
  170. // Put in the order you want the transcoders to be used in priority
  171. TranscoderManager.RegisterTranscoder(LiteTranscoder_UASTC_ASTC);
  172. TranscoderManager.RegisterTranscoder(LiteTranscoder_UASTC_BC7);
  173. TranscoderManager.RegisterTranscoder(MSCTranscoder); // catch all transcoder - will throw an error if the format can't be transcoded