ktx2Decoder.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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 = 0x83F1;
  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. transcoderName?: string;
  35. }
  36. export interface IMipmap {
  37. data: Uint8Array | null;
  38. width: number;
  39. height: number;
  40. }
  41. export interface ICompressedFormatCapabilities {
  42. astc?: boolean;
  43. bptc?: boolean;
  44. s3tc?: boolean;
  45. pvrtc?: boolean;
  46. etc2?: boolean;
  47. etc1?: boolean;
  48. }
  49. export interface IKTX2DecoderOptions {
  50. /** use RGBA format if ASTC and BC7 are not available as transcoded format */
  51. useRGBAIfASTCBC7NotAvailableWhenUASTC?: boolean;
  52. /** force to always use RGBA for transcoded format */
  53. forceRGBA?: boolean;
  54. /**
  55. * list of transcoders to bypass when looking for a suitable transcoder. The available transcoders are:
  56. * UniversalTranscoder_UASTC_ASTC
  57. * UniversalTranscoder_UASTC_BC7
  58. * MSCTranscoder
  59. */
  60. bypassTranscoders?: string[];
  61. }
  62. const isPowerOfTwo = (value: number) => {
  63. return (value & (value - 1)) === 0 && value !== 0;
  64. };
  65. /**
  66. * Class for decoding KTX2 files
  67. *
  68. */
  69. export class KTX2Decoder {
  70. private _transcoderMgr: TranscoderManager;
  71. private _zstdDecoder: ZSTDDecoder;
  72. constructor() {
  73. this._transcoderMgr = new TranscoderManager();
  74. }
  75. public decode(data: Uint8Array, caps: ICompressedFormatCapabilities, options?: IKTX2DecoderOptions): Promise<IDecodedData | null> {
  76. return Promise.resolve().then(() => {
  77. const kfr = new KTX2FileReader(data);
  78. if (!kfr.isValid()) {
  79. throw new Error("Invalid KT2 file: wrong signature");
  80. }
  81. kfr.parse();
  82. if (kfr.needZSTDDecoder) {
  83. if (!this._zstdDecoder) {
  84. this._zstdDecoder = new ZSTDDecoder();
  85. }
  86. return this._zstdDecoder.init().then(() => {
  87. return this._decodeData(kfr, caps, options);
  88. });
  89. }
  90. return this._decodeData(kfr, caps, options);
  91. });
  92. }
  93. private _decodeData(kfr: KTX2FileReader, caps: ICompressedFormatCapabilities, options?: IKTX2DecoderOptions): Promise<IDecodedData> {
  94. const width = kfr.header.pixelWidth;
  95. const height = kfr.header.pixelHeight;
  96. const srcTexFormat = kfr.textureFormat;
  97. // PVRTC1 transcoders (from both ETC1S and UASTC) only support power of 2 dimensions.
  98. const pvrtcTranscodable = isPowerOfTwo(width) && isPowerOfTwo(height);
  99. let targetFormat = -1;
  100. let transcodedFormat = -1;
  101. let roundToMultiple4 = true;
  102. if (options?.forceRGBA) {
  103. targetFormat = transcodeTarget.RGBA32;
  104. transcodedFormat = RGBA8Format;
  105. roundToMultiple4 = false;
  106. } else if (caps.astc) {
  107. targetFormat = transcodeTarget.ASTC_4x4_RGBA;
  108. transcodedFormat = COMPRESSED_RGBA_ASTC_4x4_KHR;
  109. } else if (caps.bptc) {
  110. targetFormat = transcodeTarget.BC7_RGBA;
  111. transcodedFormat = COMPRESSED_RGBA_BPTC_UNORM_EXT;
  112. } else if (options?.useRGBAIfASTCBC7NotAvailableWhenUASTC && srcTexFormat === sourceTextureFormat.UASTC4x4) {
  113. targetFormat = transcodeTarget.RGBA32;
  114. transcodedFormat = RGBA8Format;
  115. roundToMultiple4 = false;
  116. } else if (caps.s3tc) {
  117. targetFormat = kfr.hasAlpha ? transcodeTarget.BC3_RGBA : transcodeTarget.BC1_RGB;
  118. transcodedFormat = kfr.hasAlpha ? COMPRESSED_RGBA_S3TC_DXT5_EXT : COMPRESSED_RGB_S3TC_DXT1_EXT;
  119. } else if (caps.pvrtc && pvrtcTranscodable) {
  120. targetFormat = kfr.hasAlpha ? transcodeTarget.PVRTC1_4_RGBA : transcodeTarget.PVRTC1_4_RGB;
  121. transcodedFormat = kfr.hasAlpha ? COMPRESSED_RGBA_PVRTC_4BPPV1_IMG : COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
  122. } else if (caps.etc2) {
  123. targetFormat = kfr.hasAlpha ? transcodeTarget.ETC2_RGBA : transcodeTarget.ETC1_RGB /* subset of ETC2 */;
  124. transcodedFormat = kfr.hasAlpha ? COMPRESSED_RGBA8_ETC2_EAC : COMPRESSED_RGB8_ETC2;
  125. } else if (caps.etc1) {
  126. targetFormat = transcodeTarget.ETC1_RGB;
  127. transcodedFormat = COMPRESSED_RGB_ETC1_WEBGL;
  128. } else {
  129. targetFormat = transcodeTarget.RGBA32;
  130. transcodedFormat = RGBA8Format;
  131. roundToMultiple4 = false;
  132. }
  133. const transcoder = this._transcoderMgr.findTranscoder(srcTexFormat, targetFormat, options?.bypassTranscoders);
  134. if (transcoder === null) {
  135. throw new Error(`no transcoder found to transcode source texture format "${sourceTextureFormat[srcTexFormat]}" to format "${transcodeTarget[targetFormat]}"`);
  136. }
  137. const mipmaps: Array<IMipmap> = [];
  138. const dataPromises: Array<Promise<Uint8Array | null>> = [];
  139. const mipmapBuffers: Array<ArrayBuffer> = [];
  140. const decodedData: IDecodedData = { width: 0, height: 0, transcodedFormat, mipmaps, isInGammaSpace: kfr.isInGammaSpace, transcoderName: transcoder.getName() };
  141. let firstImageDescIndex = 0;
  142. for (let level = 0; level < kfr.header.levelCount; level ++) {
  143. if (level > 0) {
  144. firstImageDescIndex += Math.max(kfr.header.layerCount, 1) * kfr.header.faceCount * Math.max(kfr.header.pixelDepth >> (level - 1), 1);
  145. }
  146. const levelWidth = Math.floor(width / (1 << level));
  147. const levelHeight = Math.floor(height / (1 << level));
  148. const numImagesInLevel = kfr.header.faceCount; // note that cubemap are not supported yet (see KTX2FileReader), so faceCount == 1
  149. const levelImageByteLength = ((levelWidth + 3) >> 2) * ((levelHeight + 3) >> 2) * kfr.dfdBlock.bytesPlane[0];
  150. const levelUncompressedByteLength = kfr.levels[level].uncompressedByteLength;
  151. let levelDataBuffer = kfr.data.buffer;
  152. let levelDataOffset = kfr.levels[level].byteOffset + kfr.data.byteOffset;
  153. let imageOffsetInLevel = 0;
  154. if (kfr.header.supercompressionScheme === SupercompressionScheme.ZStandard) {
  155. levelDataBuffer = this._zstdDecoder.decode(new Uint8Array(levelDataBuffer, levelDataOffset, kfr.levels[level].byteLength), levelUncompressedByteLength);
  156. levelDataOffset = 0;
  157. }
  158. if (level === 0) {
  159. decodedData.width = roundToMultiple4 ? (levelWidth + 3) & ~3 : levelWidth;
  160. decodedData.height = roundToMultiple4 ? (levelHeight + 3) & ~3 : levelHeight;
  161. }
  162. for (let imageIndex = 0; imageIndex < numImagesInLevel; imageIndex ++) {
  163. let encodedData: Uint8Array;
  164. let imageDesc: IKTX2_ImageDesc | null = null;
  165. if (kfr.header.supercompressionScheme === SupercompressionScheme.BasisLZ) {
  166. imageDesc = kfr.supercompressionGlobalData.imageDescs![firstImageDescIndex + imageIndex];
  167. encodedData = new Uint8Array(levelDataBuffer, levelDataOffset + imageDesc.rgbSliceByteOffset, imageDesc.rgbSliceByteLength + imageDesc.alphaSliceByteLength);
  168. } else {
  169. encodedData = new Uint8Array(levelDataBuffer, levelDataOffset + imageOffsetInLevel, levelImageByteLength);
  170. imageOffsetInLevel += levelImageByteLength;
  171. }
  172. const mipmap: IMipmap = {
  173. data: null,
  174. width: levelWidth,
  175. height: levelHeight,
  176. };
  177. const transcodedData = transcoder.transcode(srcTexFormat, targetFormat, level, levelWidth, levelHeight, levelUncompressedByteLength, kfr, imageDesc, encodedData)
  178. .then((data) => {
  179. mipmap.data = data;
  180. if (data) {
  181. mipmapBuffers.push(data.buffer);
  182. }
  183. return data;
  184. })
  185. .catch((reason) => {
  186. decodedData.errors = decodedData.errors ?? "";
  187. decodedData.errors += reason + "\n";
  188. return null;
  189. });
  190. dataPromises.push(transcodedData);
  191. mipmaps.push(mipmap);
  192. }
  193. }
  194. return Promise.all(dataPromises).then(() => {
  195. return decodedData;
  196. });
  197. }
  198. }
  199. // Put in the order you want the transcoders to be used in priority
  200. TranscoderManager.RegisterTranscoder(LiteTranscoder_UASTC_ASTC);
  201. TranscoderManager.RegisterTranscoder(LiteTranscoder_UASTC_BC7);
  202. TranscoderManager.RegisterTranscoder(MSCTranscoder); // catch all transcoder - will throw an error if the format can't be transcoded