ktx2Decoder.ts 11 KB

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