khronosTextureContainer2.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import { InternalTexture } from "../Materials/Textures/internalTexture";
  2. import { ThinEngine } from "../Engines/thinEngine";
  3. import { Nullable } from '../types';
  4. import { Tools } from './tools';
  5. import { Constants } from '../Engines/constants';
  6. declare var KTX2DECODER: any;
  7. /**
  8. * Class for loading KTX2 files
  9. * @hidden
  10. */
  11. export class KhronosTextureContainer2 {
  12. /**
  13. * URL to use when loading the KTX2 decoder module
  14. */
  15. public static JSModuleURL = "https://preview.babylonjs.com/ktx2Decoder/babylon.ktx2Decoder.js";
  16. private _engine: ThinEngine;
  17. private static _WorkerPromise: Nullable<Promise<Worker>> = null;
  18. private static _Worker: Nullable<Worker> = null;
  19. private static _actionId = 0;
  20. private static _CreateWorkerAsync() {
  21. if (!this._WorkerPromise) {
  22. this._WorkerPromise = new Promise((res) => {
  23. if (this._Worker) {
  24. res(this._Worker);
  25. } else {
  26. const workerBlobUrl = URL.createObjectURL(new Blob([`(${workerFunc})()`], { type: "application/javascript" }));
  27. this._Worker = new Worker(workerBlobUrl);
  28. URL.revokeObjectURL(workerBlobUrl);
  29. const initHandler = (msg: any) => {
  30. if (msg.data.action === "init") {
  31. this._Worker!.removeEventListener("message", initHandler);
  32. res(this._Worker!);
  33. }
  34. };
  35. const loadWASMHandler = (msg: any) => {
  36. const cache: { [path: string]: Promise<ArrayBuffer | string> } = {};
  37. if (msg.data.action === "loadWASM") {
  38. let promise = cache[msg.data.path];
  39. if (!promise) {
  40. promise = Tools.LoadFileAsync(msg.data.path);
  41. cache[msg.data.path] = promise;
  42. }
  43. promise.then((wasmBinary) => {
  44. this._Worker!.postMessage({ action: "wasmLoaded", wasmBinary: wasmBinary, id: msg.data.id });
  45. return wasmBinary;
  46. });
  47. }
  48. };
  49. this._Worker.addEventListener("message", initHandler);
  50. this._Worker.addEventListener("message", loadWASMHandler);
  51. this._Worker.postMessage({ action: "init", jsPath: KhronosTextureContainer2.JSModuleURL });
  52. }
  53. });
  54. }
  55. return this._WorkerPromise;
  56. }
  57. public constructor(engine: ThinEngine) {
  58. this._engine = engine;
  59. }
  60. public uploadAsync(data: ArrayBufferView, internalTexture: InternalTexture): Promise<void> {
  61. return new Promise((res, rej) => {
  62. KhronosTextureContainer2._CreateWorkerAsync().then(() => {
  63. const actionId = KhronosTextureContainer2._actionId++;
  64. const messageHandler = (msg: any) => {
  65. if (msg.data.action === "decoded" && msg.data.id === actionId) {
  66. KhronosTextureContainer2._Worker!.removeEventListener("message", messageHandler);
  67. if (!msg.data.success) {
  68. rej({ message: msg.data.msg });
  69. } else {
  70. this._createTexture(msg.data.decodedData, internalTexture);
  71. res();
  72. }
  73. }
  74. };
  75. KhronosTextureContainer2._Worker!.addEventListener("message", messageHandler);
  76. const caps = this._engine.getCaps();
  77. const compressedTexturesCaps = {
  78. astc: !!caps.astc,
  79. bptc: !!caps.bptc,
  80. s3tc: !!caps.s3tc,
  81. pvrtc: !!caps.pvrtc,
  82. etc2: !!caps.etc2,
  83. etc1: !!caps.etc1,
  84. };
  85. KhronosTextureContainer2._Worker!.postMessage({
  86. action: "decode",
  87. id: actionId,
  88. data: data,
  89. caps: compressedTexturesCaps,
  90. }, [data.buffer]);
  91. });
  92. });
  93. }
  94. protected _createTexture(data: any /* IEncodedData */, internalTexture: InternalTexture) {
  95. this._engine._bindTextureDirectly(this._engine._gl.TEXTURE_2D, internalTexture);
  96. if (data.transcodedFormat === 0x8058 /* RGBA8 */) {
  97. internalTexture.type = Constants.TEXTURETYPE_UNSIGNED_BYTE;
  98. internalTexture.format = Constants.TEXTUREFORMAT_RGBA;
  99. } else {
  100. internalTexture.format = data.transcodedFormat;
  101. }
  102. for (let t = 0; t < data.mipmaps.length; ++t) {
  103. let mipmap = data.mipmaps[t];
  104. if (!mipmap || !mipmap.data) {
  105. throw new Error("KTX2 container - could not transcode one of the image");
  106. }
  107. if (data.transcodedFormat === 0x8058 /* RGBA8 */) {
  108. // uncompressed RGBA
  109. internalTexture.width = mipmap.width; // need to set width/height so that the call to _uploadDataToTextureDirectly uses the right dimensions
  110. internalTexture.height = mipmap.height;
  111. this._engine._uploadDataToTextureDirectly(internalTexture, mipmap.data, 0, t, undefined, true);
  112. } else {
  113. this._engine._uploadCompressedDataToTextureDirectly(internalTexture, data.transcodedFormat, mipmap.width, mipmap.height, mipmap.data, 0, t);
  114. }
  115. }
  116. internalTexture.width = data.mipmaps[0].width;
  117. internalTexture.height = data.mipmaps[0].height;
  118. internalTexture.generateMipMaps = data.mipmaps.length > 1;
  119. internalTexture.isReady = true;
  120. this._engine._bindTextureDirectly(this._engine._gl.TEXTURE_2D, null);
  121. }
  122. /**
  123. * Checks if the given data starts with a KTX2 file identifier.
  124. * @param data the data to check
  125. * @returns true if the data is a KTX2 file or false otherwise
  126. */
  127. public static IsValid(data: ArrayBufferView): boolean {
  128. if (data.byteLength >= 12) {
  129. // '«', 'K', 'T', 'X', ' ', '2', '0', '»', '\r', '\n', '\x1A', '\n'
  130. const identifier = new Uint8Array(data.buffer, data.byteOffset, 12);
  131. if (identifier[0] === 0xAB && identifier[1] === 0x4B && identifier[2] === 0x54 && identifier[3] === 0x58 && identifier[4] === 0x20 && identifier[5] === 0x32 &&
  132. identifier[6] === 0x30 && identifier[7] === 0xBB && identifier[8] === 0x0D && identifier[9] === 0x0A && identifier[10] === 0x1A && identifier[11] === 0x0A) {
  133. return true;
  134. }
  135. }
  136. return false;
  137. }
  138. }
  139. declare function importScripts(...urls: string[]): void;
  140. declare function postMessage(message: any, transfer?: any[]): void;
  141. declare var KTX2DECODER: any;
  142. export function workerFunc(): void {
  143. let ktx2Decoder: any;
  144. onmessage = (event) => {
  145. switch (event.data.action) {
  146. case "init":
  147. importScripts(event.data.jsPath);
  148. ktx2Decoder = new KTX2DECODER.KTX2Decoder();
  149. postMessage({ action: "init" });
  150. break;
  151. case "decode":
  152. try {
  153. ktx2Decoder.decode(event.data.data, event.data.caps).then((data: any) => {
  154. const buffers = [];
  155. for (let mip = 0; mip < data.mipmaps.length; ++mip) {
  156. const mipmap = data.mipmaps[mip];
  157. if (mipmap) {
  158. buffers.push(mipmap.data.buffer);
  159. }
  160. }
  161. postMessage({ action: "decoded", success: true, id: event.data.id, decodedData: data }, buffers);
  162. }).catch((reason: any) => {
  163. postMessage({ action: "decoded", success: false, id: event.data.id, msg: reason });
  164. });
  165. } catch (err) {
  166. postMessage({ action: "decoded", success: false, id: event.data.id, msg: err });
  167. }
  168. break;
  169. }
  170. };
  171. }