khronosTextureContainer2.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import { InternalTexture } from "../Materials/Textures/internalTexture";
  2. import { ThinEngine } from "../Engines/thinEngine";
  3. import { Constants } from '../Engines/constants';
  4. import { WorkerPool } from './workerPool';
  5. declare var KTX2DECODER: any;
  6. /**
  7. * Class for loading KTX2 files
  8. * @hidden
  9. */
  10. export class KhronosTextureContainer2 {
  11. private static _WorkerPoolPromise?: Promise<WorkerPool>;
  12. private static _Initialized: boolean;
  13. private static _Ktx2Decoder: any; // used when no worker pool is used
  14. /**
  15. * URL to use when loading the KTX2 decoder module
  16. */
  17. public static JSModuleURL = "https://preview.babylonjs.com/ktx2Decoder/babylon.ktx2Decoder.js";
  18. /**
  19. * Default number of workers to create when creating the draco compression object.
  20. */
  21. public static DefaultNumWorkers = KhronosTextureContainer2.GetDefaultNumWorkers();
  22. private static GetDefaultNumWorkers(): number {
  23. if (typeof navigator !== "object" || !navigator.hardwareConcurrency) {
  24. return 1;
  25. }
  26. // Use 50% of the available logical processors but capped at 4.
  27. return Math.min(Math.floor(navigator.hardwareConcurrency * 0.5), 4);
  28. }
  29. private _engine: ThinEngine;
  30. private static _CreateWorkerPool(numWorkers: number) {
  31. this._Initialized = true;
  32. if (numWorkers && typeof Worker === "function") {
  33. KhronosTextureContainer2._WorkerPoolPromise = new Promise((resolve) => {
  34. const workerContent = `(${workerFunc})()`;
  35. const workerBlobUrl = URL.createObjectURL(new Blob([workerContent], { type: "application/javascript" }));
  36. const workerPromises = new Array<Promise<Worker>>(numWorkers);
  37. for (let i = 0; i < workerPromises.length; i++) {
  38. workerPromises[i] = new Promise((resolve, reject) => {
  39. const worker = new Worker(workerBlobUrl);
  40. const onError = (error: ErrorEvent) => {
  41. worker.removeEventListener("error", onError);
  42. worker.removeEventListener("message", onMessage);
  43. reject(error);
  44. };
  45. const onMessage = (message: MessageEvent) => {
  46. if (message.data.action === "init") {
  47. worker.removeEventListener("error", onError);
  48. worker.removeEventListener("message", onMessage);
  49. resolve(worker);
  50. }
  51. };
  52. worker.addEventListener("error", onError);
  53. worker.addEventListener("message", onMessage);
  54. worker.postMessage({
  55. action: "init",
  56. jsPath: KhronosTextureContainer2.JSModuleURL
  57. });
  58. });
  59. }
  60. Promise.all(workerPromises).then((workers) => {
  61. resolve(new WorkerPool(workers));
  62. });
  63. });
  64. } else {
  65. KTX2DECODER.MSCTranscoder.UseFromWorkerThread = false;
  66. KTX2DECODER.WASMMemoryManager.LoadBinariesFromCurrentThread = true;
  67. }
  68. }
  69. /**
  70. * Constructor
  71. * @param numWorkers The number of workers for async operations. Specify `0` to disable web workers and run synchronously in the current context.
  72. */
  73. public constructor(engine: ThinEngine, numWorkers = KhronosTextureContainer2.DefaultNumWorkers) {
  74. this._engine = engine;
  75. if (!KhronosTextureContainer2._Initialized) {
  76. KhronosTextureContainer2._CreateWorkerPool(numWorkers);
  77. }
  78. }
  79. public uploadAsync(data: ArrayBufferView, internalTexture: InternalTexture): Promise<void> {
  80. const caps = this._engine.getCaps();
  81. const compressedTexturesCaps = {
  82. astc: !!caps.astc,
  83. bptc: !!caps.bptc,
  84. s3tc: !!caps.s3tc,
  85. pvrtc: !!caps.pvrtc,
  86. etc2: !!caps.etc2,
  87. etc1: !!caps.etc1,
  88. };
  89. if (KhronosTextureContainer2._WorkerPoolPromise) {
  90. return KhronosTextureContainer2._WorkerPoolPromise.then((workerPool) => {
  91. return new Promise((resolve, reject) => {
  92. workerPool.push((worker, onComplete) => {
  93. const onError = (error: ErrorEvent) => {
  94. worker.removeEventListener("error", onError);
  95. worker.removeEventListener("message", onMessage);
  96. reject(error);
  97. onComplete();
  98. };
  99. const onMessage = (message: MessageEvent) => {
  100. if (message.data.action === "decoded") {
  101. worker.removeEventListener("error", onError);
  102. worker.removeEventListener("message", onMessage);
  103. if (!message.data.success) {
  104. reject({ message: message.data.msg });
  105. } else {
  106. this._createTexture(message.data.decodedData, internalTexture);
  107. resolve();
  108. }
  109. onComplete();
  110. }
  111. };
  112. worker.addEventListener("error", onError);
  113. worker.addEventListener("message", onMessage);
  114. worker.postMessage({ action: "decode", data, caps: compressedTexturesCaps }, [data.buffer]);
  115. });
  116. });
  117. });
  118. }
  119. return new Promise((resolve, reject) => {
  120. if (!KhronosTextureContainer2._Ktx2Decoder) {
  121. KhronosTextureContainer2._Ktx2Decoder = new KTX2DECODER.KTX2Decoder();
  122. }
  123. try {
  124. KhronosTextureContainer2._Ktx2Decoder.decode(data, caps).then((data: any) => {
  125. const buffers = [];
  126. for (let mip = 0; mip < data.mipmaps.length; ++mip) {
  127. const mipmap = data.mipmaps[mip];
  128. if (mipmap) {
  129. buffers.push(mipmap.data.buffer);
  130. }
  131. }
  132. resolve();
  133. this._createTexture(data, internalTexture);
  134. }).catch((reason: any) => {
  135. reject({ message: reason });
  136. });
  137. } catch (err) {
  138. reject({ message: err });
  139. }
  140. });
  141. }
  142. /**
  143. * Stop all async operations and release resources.
  144. */
  145. public dispose(): void {
  146. if (KhronosTextureContainer2._WorkerPoolPromise) {
  147. KhronosTextureContainer2._WorkerPoolPromise.then((workerPool) => {
  148. workerPool.dispose();
  149. });
  150. }
  151. delete KhronosTextureContainer2._WorkerPoolPromise;
  152. }
  153. protected _createTexture(data: any /* IEncodedData */, internalTexture: InternalTexture) {
  154. this._engine._bindTextureDirectly(this._engine._gl.TEXTURE_2D, internalTexture);
  155. if (data.transcodedFormat === 0x8058 /* RGBA8 */) {
  156. internalTexture.type = Constants.TEXTURETYPE_UNSIGNED_BYTE;
  157. internalTexture.format = Constants.TEXTUREFORMAT_RGBA;
  158. } else {
  159. internalTexture.format = data.transcodedFormat;
  160. }
  161. for (let t = 0; t < data.mipmaps.length; ++t) {
  162. let mipmap = data.mipmaps[t];
  163. if (!mipmap || !mipmap.data) {
  164. throw new Error("KTX2 container - could not transcode one of the image");
  165. }
  166. if (data.transcodedFormat === 0x8058 /* RGBA8 */) {
  167. // uncompressed RGBA
  168. internalTexture.width = mipmap.width; // need to set width/height so that the call to _uploadDataToTextureDirectly uses the right dimensions
  169. internalTexture.height = mipmap.height;
  170. this._engine._uploadDataToTextureDirectly(internalTexture, mipmap.data, 0, t, undefined, true);
  171. } else {
  172. this._engine._uploadCompressedDataToTextureDirectly(internalTexture, data.transcodedFormat, mipmap.width, mipmap.height, mipmap.data, 0, t);
  173. }
  174. }
  175. internalTexture.width = data.mipmaps[0].width;
  176. internalTexture.height = data.mipmaps[0].height;
  177. internalTexture.generateMipMaps = data.mipmaps.length > 1;
  178. internalTexture.isReady = true;
  179. this._engine._bindTextureDirectly(this._engine._gl.TEXTURE_2D, null);
  180. }
  181. /**
  182. * Checks if the given data starts with a KTX2 file identifier.
  183. * @param data the data to check
  184. * @returns true if the data is a KTX2 file or false otherwise
  185. */
  186. public static IsValid(data: ArrayBufferView): boolean {
  187. if (data.byteLength >= 12) {
  188. // '«', 'K', 'T', 'X', ' ', '2', '0', '»', '\r', '\n', '\x1A', '\n'
  189. const identifier = new Uint8Array(data.buffer, data.byteOffset, 12);
  190. if (identifier[0] === 0xAB && identifier[1] === 0x4B && identifier[2] === 0x54 && identifier[3] === 0x58 && identifier[4] === 0x20 && identifier[5] === 0x32 &&
  191. identifier[6] === 0x30 && identifier[7] === 0xBB && identifier[8] === 0x0D && identifier[9] === 0x0A && identifier[10] === 0x1A && identifier[11] === 0x0A) {
  192. return true;
  193. }
  194. }
  195. return false;
  196. }
  197. }
  198. declare function importScripts(...urls: string[]): void;
  199. declare function postMessage(message: any, transfer?: any[]): void;
  200. declare var KTX2DECODER: any;
  201. export function workerFunc(): void {
  202. let ktx2Decoder: any;
  203. onmessage = (event) => {
  204. switch (event.data.action) {
  205. case "init":
  206. importScripts(event.data.jsPath);
  207. ktx2Decoder = new KTX2DECODER.KTX2Decoder();
  208. postMessage({ action: "init" });
  209. break;
  210. case "decode":
  211. try {
  212. ktx2Decoder.decode(event.data.data, event.data.caps).then((data: any) => {
  213. const buffers = [];
  214. for (let mip = 0; mip < data.mipmaps.length; ++mip) {
  215. const mipmap = data.mipmaps[mip];
  216. if (mipmap) {
  217. buffers.push(mipmap.data.buffer);
  218. }
  219. }
  220. postMessage({ action: "decoded", success: true, decodedData: data }, buffers);
  221. }).catch((reason: any) => {
  222. postMessage({ action: "decoded", success: false, msg: reason });
  223. });
  224. } catch (err) {
  225. postMessage({ action: "decoded", success: false, msg: err });
  226. }
  227. break;
  228. }
  229. };
  230. }