khronosTextureContainer2.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. import { AutoReleaseWorkerPool } from "./workerPool.js";
  2. import { Tools } from "./tools.js";
  3. import { EngineFormat, TranscodeTarget } from "../Materials/Textures/ktx2decoderTypes.js";
  4. import { applyConfig, initializeWebWorker, workerFunction } from "./khronosTextureContainer2Worker.js";
  5. /**
  6. * Class that defines the default KTX2 decoder options.
  7. *
  8. * This class is useful for providing options to the KTX2 decoder to control how the source data is transcoded.
  9. */
  10. export class DefaultKTX2DecoderOptions {
  11. constructor() {
  12. this._isDirty = true;
  13. this._useRGBAIfOnlyBC1BC3AvailableWhenUASTC = true;
  14. this._ktx2DecoderOptions = {};
  15. }
  16. /**
  17. * Gets the dirty flag
  18. */
  19. get isDirty() {
  20. return this._isDirty;
  21. }
  22. /**
  23. * force a (uncompressed) RGBA transcoded format if transcoding a UASTC source format and ASTC + BC7 are not available as a compressed transcoded format
  24. */
  25. get useRGBAIfASTCBC7NotAvailableWhenUASTC() {
  26. return this._useRGBAIfASTCBC7NotAvailableWhenUASTC;
  27. }
  28. set useRGBAIfASTCBC7NotAvailableWhenUASTC(value) {
  29. if (this._useRGBAIfASTCBC7NotAvailableWhenUASTC === value) {
  30. return;
  31. }
  32. this._useRGBAIfASTCBC7NotAvailableWhenUASTC = value;
  33. this._isDirty = true;
  34. }
  35. /**
  36. * force a (uncompressed) RGBA transcoded format if transcoding a UASTC source format and only BC1 or BC3 are available as a compressed transcoded format.
  37. * This property is true by default to favor speed over memory, because currently transcoding from UASTC to BC1/3 is slow because the transcoder transcodes
  38. * to uncompressed and then recompresses the texture
  39. */
  40. get useRGBAIfOnlyBC1BC3AvailableWhenUASTC() {
  41. return this._useRGBAIfOnlyBC1BC3AvailableWhenUASTC;
  42. }
  43. set useRGBAIfOnlyBC1BC3AvailableWhenUASTC(value) {
  44. if (this._useRGBAIfOnlyBC1BC3AvailableWhenUASTC === value) {
  45. return;
  46. }
  47. this._useRGBAIfOnlyBC1BC3AvailableWhenUASTC = value;
  48. this._isDirty = true;
  49. }
  50. /**
  51. * force to always use (uncompressed) RGBA for transcoded format
  52. */
  53. get forceRGBA() {
  54. return this._forceRGBA;
  55. }
  56. set forceRGBA(value) {
  57. if (this._forceRGBA === value) {
  58. return;
  59. }
  60. this._forceRGBA = value;
  61. this._isDirty = true;
  62. }
  63. /**
  64. * force to always use (uncompressed) R8 for transcoded format
  65. */
  66. get forceR8() {
  67. return this._forceR8;
  68. }
  69. set forceR8(value) {
  70. if (this._forceR8 === value) {
  71. return;
  72. }
  73. this._forceR8 = value;
  74. this._isDirty = true;
  75. }
  76. /**
  77. * force to always use (uncompressed) RG8 for transcoded format
  78. */
  79. get forceRG8() {
  80. return this._forceRG8;
  81. }
  82. set forceRG8(value) {
  83. if (this._forceRG8 === value) {
  84. return;
  85. }
  86. this._forceRG8 = value;
  87. this._isDirty = true;
  88. }
  89. /**
  90. * list of transcoders to bypass when looking for a suitable transcoder. The available transcoders are:
  91. * UniversalTranscoder_UASTC_ASTC
  92. * UniversalTranscoder_UASTC_BC7
  93. * UniversalTranscoder_UASTC_RGBA_UNORM
  94. * UniversalTranscoder_UASTC_RGBA_SRGB
  95. * UniversalTranscoder_UASTC_R8_UNORM
  96. * UniversalTranscoder_UASTC_RG8_UNORM
  97. * MSCTranscoder
  98. */
  99. get bypassTranscoders() {
  100. return this._bypassTranscoders;
  101. }
  102. set bypassTranscoders(value) {
  103. if (this._bypassTranscoders === value) {
  104. return;
  105. }
  106. this._bypassTranscoders = value;
  107. this._isDirty = true;
  108. }
  109. /** @internal */
  110. _getKTX2DecoderOptions() {
  111. if (!this._isDirty) {
  112. return this._ktx2DecoderOptions;
  113. }
  114. this._isDirty = false;
  115. const options = {
  116. useRGBAIfASTCBC7NotAvailableWhenUASTC: this._useRGBAIfASTCBC7NotAvailableWhenUASTC,
  117. forceRGBA: this._forceRGBA,
  118. forceR8: this._forceR8,
  119. forceRG8: this._forceRG8,
  120. bypassTranscoders: this._bypassTranscoders,
  121. };
  122. if (this.useRGBAIfOnlyBC1BC3AvailableWhenUASTC) {
  123. options.transcodeFormatDecisionTree = {
  124. UASTC: {
  125. transcodeFormat: [TranscodeTarget.BC1_RGB, TranscodeTarget.BC3_RGBA],
  126. yes: {
  127. transcodeFormat: TranscodeTarget.RGBA32,
  128. engineFormat: EngineFormat.RGBA8Format,
  129. roundToMultiple4: false,
  130. },
  131. },
  132. };
  133. }
  134. this._ktx2DecoderOptions = options;
  135. return options;
  136. }
  137. }
  138. /**
  139. * Class for loading KTX2 files
  140. */
  141. export class KhronosTextureContainer2 {
  142. static GetDefaultNumWorkers() {
  143. if (typeof navigator !== "object" || !navigator.hardwareConcurrency) {
  144. return 1;
  145. }
  146. // Use 50% of the available logical processors but capped at 4.
  147. return Math.min(Math.floor(navigator.hardwareConcurrency * 0.5), 4);
  148. }
  149. static _Initialize(numWorkers) {
  150. if (KhronosTextureContainer2._WorkerPoolPromise || KhronosTextureContainer2._DecoderModulePromise) {
  151. return;
  152. }
  153. const urls = {
  154. jsDecoderModule: Tools.GetBabylonScriptURL(this.URLConfig.jsDecoderModule, true),
  155. wasmUASTCToASTC: Tools.GetBabylonScriptURL(this.URLConfig.wasmUASTCToASTC, true),
  156. wasmUASTCToBC7: Tools.GetBabylonScriptURL(this.URLConfig.wasmUASTCToBC7, true),
  157. wasmUASTCToRGBA_UNORM: Tools.GetBabylonScriptURL(this.URLConfig.wasmUASTCToRGBA_UNORM, true),
  158. wasmUASTCToRGBA_SRGB: Tools.GetBabylonScriptURL(this.URLConfig.wasmUASTCToRGBA_SRGB, true),
  159. wasmUASTCToR8_UNORM: Tools.GetBabylonScriptURL(this.URLConfig.wasmUASTCToR8_UNORM, true),
  160. wasmUASTCToRG8_UNORM: Tools.GetBabylonScriptURL(this.URLConfig.wasmUASTCToRG8_UNORM, true),
  161. jsMSCTranscoder: Tools.GetBabylonScriptURL(this.URLConfig.jsMSCTranscoder, true),
  162. wasmMSCTranscoder: Tools.GetBabylonScriptURL(this.URLConfig.wasmMSCTranscoder, true),
  163. wasmZSTDDecoder: Tools.GetBabylonScriptURL(this.URLConfig.wasmZSTDDecoder, true),
  164. };
  165. if (numWorkers && typeof Worker === "function" && typeof URL !== "undefined") {
  166. KhronosTextureContainer2._WorkerPoolPromise = new Promise((resolve) => {
  167. const workerContent = `${applyConfig}(${workerFunction})()`;
  168. const workerBlobUrl = URL.createObjectURL(new Blob([workerContent], { type: "application/javascript" }));
  169. resolve(new AutoReleaseWorkerPool(numWorkers, () => initializeWebWorker(new Worker(workerBlobUrl), undefined, urls)));
  170. });
  171. }
  172. else {
  173. if (typeof KhronosTextureContainer2._KTX2DecoderModule === "undefined") {
  174. KhronosTextureContainer2._DecoderModulePromise = Tools.LoadBabylonScriptAsync(urls.jsDecoderModule).then(() => {
  175. KhronosTextureContainer2._KTX2DecoderModule = KTX2DECODER;
  176. KhronosTextureContainer2._KTX2DecoderModule.MSCTranscoder.UseFromWorkerThread = false;
  177. KhronosTextureContainer2._KTX2DecoderModule.WASMMemoryManager.LoadBinariesFromCurrentThread = true;
  178. applyConfig(urls, KhronosTextureContainer2._KTX2DecoderModule);
  179. return new KhronosTextureContainer2._KTX2DecoderModule.KTX2Decoder();
  180. });
  181. }
  182. else {
  183. KhronosTextureContainer2._KTX2DecoderModule.MSCTranscoder.UseFromWorkerThread = false;
  184. KhronosTextureContainer2._KTX2DecoderModule.WASMMemoryManager.LoadBinariesFromCurrentThread = true;
  185. KhronosTextureContainer2._DecoderModulePromise = Promise.resolve(new KhronosTextureContainer2._KTX2DecoderModule.KTX2Decoder());
  186. }
  187. }
  188. }
  189. /**
  190. * Constructor
  191. * @param engine The engine to use
  192. * @param numWorkersOrOptions The number of workers for async operations. Specify `0` to disable web workers and run synchronously in the current context.
  193. */
  194. constructor(engine, numWorkersOrOptions = KhronosTextureContainer2.DefaultNumWorkers) {
  195. this._engine = engine;
  196. const workerPoolOption = (typeof numWorkersOrOptions === "object" && numWorkersOrOptions.workerPool) || KhronosTextureContainer2.WorkerPool;
  197. if (workerPoolOption) {
  198. KhronosTextureContainer2._WorkerPoolPromise = Promise.resolve(workerPoolOption);
  199. }
  200. else {
  201. // set the KTX2 decoder module
  202. if (typeof numWorkersOrOptions === "object") {
  203. KhronosTextureContainer2._KTX2DecoderModule = numWorkersOrOptions?.binariesAndModulesContainer?.jsDecoderModule;
  204. }
  205. else if (typeof KTX2DECODER !== "undefined") {
  206. KhronosTextureContainer2._KTX2DecoderModule = KTX2DECODER;
  207. }
  208. const numberOfWorkers = typeof numWorkersOrOptions === "number" ? numWorkersOrOptions : numWorkersOrOptions.numWorkers ?? KhronosTextureContainer2.DefaultNumWorkers;
  209. KhronosTextureContainer2._Initialize(numberOfWorkers);
  210. }
  211. }
  212. /**
  213. * @internal
  214. */
  215. _uploadAsync(data, internalTexture, options) {
  216. const caps = this._engine.getCaps();
  217. const compressedTexturesCaps = {
  218. astc: !!caps.astc,
  219. bptc: !!caps.bptc,
  220. s3tc: !!caps.s3tc,
  221. pvrtc: !!caps.pvrtc,
  222. etc2: !!caps.etc2,
  223. etc1: !!caps.etc1,
  224. };
  225. if (KhronosTextureContainer2._WorkerPoolPromise) {
  226. return KhronosTextureContainer2._WorkerPoolPromise.then((workerPool) => {
  227. return new Promise((resolve, reject) => {
  228. workerPool.push((worker, onComplete) => {
  229. const onError = (error) => {
  230. worker.removeEventListener("error", onError);
  231. worker.removeEventListener("message", onMessage);
  232. reject(error);
  233. onComplete();
  234. };
  235. const onMessage = (message) => {
  236. if (message.data.action === "decoded") {
  237. worker.removeEventListener("error", onError);
  238. worker.removeEventListener("message", onMessage);
  239. if (!message.data.success) {
  240. reject({ message: message.data.msg });
  241. }
  242. else {
  243. try {
  244. this._createTexture(message.data.decodedData, internalTexture, options);
  245. resolve();
  246. }
  247. catch (err) {
  248. reject({ message: err });
  249. }
  250. }
  251. onComplete();
  252. }
  253. };
  254. worker.addEventListener("error", onError);
  255. worker.addEventListener("message", onMessage);
  256. worker.postMessage({ action: "setDefaultDecoderOptions", options: KhronosTextureContainer2.DefaultDecoderOptions._getKTX2DecoderOptions() });
  257. const dataCopy = new Uint8Array(data.byteLength);
  258. dataCopy.set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
  259. worker.postMessage({ action: "decode", data: dataCopy, caps: compressedTexturesCaps, options }, [dataCopy.buffer]);
  260. });
  261. });
  262. });
  263. }
  264. else if (KhronosTextureContainer2._DecoderModulePromise) {
  265. return KhronosTextureContainer2._DecoderModulePromise.then((decoder) => {
  266. if (KhronosTextureContainer2.DefaultDecoderOptions.isDirty) {
  267. KhronosTextureContainer2._KTX2DecoderModule.KTX2Decoder.DefaultDecoderOptions = KhronosTextureContainer2.DefaultDecoderOptions._getKTX2DecoderOptions();
  268. }
  269. return new Promise((resolve, reject) => {
  270. decoder
  271. .decode(data, caps)
  272. .then((data) => {
  273. this._createTexture(data, internalTexture);
  274. resolve();
  275. })
  276. .catch((reason) => {
  277. reject({ message: reason });
  278. });
  279. });
  280. });
  281. }
  282. throw new Error("KTX2 decoder module is not available");
  283. }
  284. _createTexture(data, internalTexture, options) {
  285. const oglTexture2D = 3553; // gl.TEXTURE_2D
  286. this._engine._bindTextureDirectly(oglTexture2D, internalTexture);
  287. if (options) {
  288. // return back some information about the decoded data
  289. options.transcodedFormat = data.transcodedFormat;
  290. options.isInGammaSpace = data.isInGammaSpace;
  291. options.hasAlpha = data.hasAlpha;
  292. options.transcoderName = data.transcoderName;
  293. }
  294. let isUncompressedFormat = true;
  295. switch (data.transcodedFormat) {
  296. case 0x8058 /* RGBA8 */:
  297. internalTexture.type = 0;
  298. internalTexture.format = 5;
  299. break;
  300. case 0x8229 /* R8 */:
  301. internalTexture.type = 0;
  302. internalTexture.format = 6;
  303. break;
  304. case 0x822b /* RG8 */:
  305. internalTexture.type = 0;
  306. internalTexture.format = 7;
  307. break;
  308. default:
  309. internalTexture.format = data.transcodedFormat;
  310. isUncompressedFormat = false;
  311. break;
  312. }
  313. internalTexture._gammaSpace = data.isInGammaSpace;
  314. internalTexture.generateMipMaps = data.mipmaps.length > 1;
  315. if (data.errors) {
  316. throw new Error("KTX2 container - could not transcode the data. " + data.errors);
  317. }
  318. for (let t = 0; t < data.mipmaps.length; ++t) {
  319. const mipmap = data.mipmaps[t];
  320. if (!mipmap || !mipmap.data) {
  321. throw new Error("KTX2 container - could not transcode one of the image");
  322. }
  323. if (isUncompressedFormat) {
  324. // uncompressed RGBA / R8 / RG8
  325. internalTexture.width = mipmap.width; // need to set width/height so that the call to _uploadDataToTextureDirectly uses the right dimensions
  326. internalTexture.height = mipmap.height;
  327. this._engine._uploadDataToTextureDirectly(internalTexture, mipmap.data, 0, t, undefined, true);
  328. }
  329. else {
  330. this._engine._uploadCompressedDataToTextureDirectly(internalTexture, data.transcodedFormat, mipmap.width, mipmap.height, mipmap.data, 0, t);
  331. }
  332. }
  333. internalTexture._extension = ".ktx2";
  334. internalTexture.width = data.mipmaps[0].width;
  335. internalTexture.height = data.mipmaps[0].height;
  336. internalTexture.isReady = true;
  337. this._engine._bindTextureDirectly(oglTexture2D, null);
  338. }
  339. /**
  340. * Checks if the given data starts with a KTX2 file identifier.
  341. * @param data the data to check
  342. * @returns true if the data is a KTX2 file or false otherwise
  343. */
  344. static IsValid(data) {
  345. if (data.byteLength >= 12) {
  346. // '«', 'K', 'T', 'X', ' ', '2', '0', '»', '\r', '\n', '\x1A', '\n'
  347. const identifier = new Uint8Array(data.buffer, data.byteOffset, 12);
  348. if (identifier[0] === 0xab &&
  349. identifier[1] === 0x4b &&
  350. identifier[2] === 0x54 &&
  351. identifier[3] === 0x58 &&
  352. identifier[4] === 0x20 &&
  353. identifier[5] === 0x32 &&
  354. identifier[6] === 0x30 &&
  355. identifier[7] === 0xbb &&
  356. identifier[8] === 0x0d &&
  357. identifier[9] === 0x0a &&
  358. identifier[10] === 0x1a &&
  359. identifier[11] === 0x0a) {
  360. return true;
  361. }
  362. }
  363. return false;
  364. }
  365. }
  366. /**
  367. * URLs to use when loading the KTX2 decoder module as well as its dependencies
  368. * If a url is null, the default url is used (pointing to https://preview.babylonjs.com)
  369. * Note that jsDecoderModule can't be null and that the other dependencies will only be loaded if necessary
  370. * Urls you can change:
  371. * URLConfig.jsDecoderModule
  372. * URLConfig.wasmUASTCToASTC
  373. * URLConfig.wasmUASTCToBC7
  374. * URLConfig.wasmUASTCToRGBA_UNORM
  375. * URLConfig.wasmUASTCToRGBA_SRGB
  376. * URLConfig.wasmUASTCToR8_UNORM
  377. * URLConfig.wasmUASTCToRG8_UNORM
  378. * URLConfig.jsMSCTranscoder
  379. * URLConfig.wasmMSCTranscoder
  380. * URLConfig.wasmZSTDDecoder
  381. * You can see their default values in this PG: https://playground.babylonjs.com/#EIJH8L#29
  382. */
  383. KhronosTextureContainer2.URLConfig = {
  384. jsDecoderModule: "https://cdn.babylonjs.com/babylon.ktx2Decoder.js",
  385. wasmUASTCToASTC: null,
  386. wasmUASTCToBC7: null,
  387. wasmUASTCToRGBA_UNORM: null,
  388. wasmUASTCToRGBA_SRGB: null,
  389. wasmUASTCToR8_UNORM: null,
  390. wasmUASTCToRG8_UNORM: null,
  391. jsMSCTranscoder: null,
  392. wasmMSCTranscoder: null,
  393. wasmZSTDDecoder: null,
  394. };
  395. /**
  396. * Default number of workers used to handle data decoding
  397. */
  398. KhronosTextureContainer2.DefaultNumWorkers = KhronosTextureContainer2.GetDefaultNumWorkers();
  399. /**
  400. * Default configuration for the KTX2 decoder.
  401. * The options defined in this way have priority over those passed when creating a KTX2 texture with new Texture(...).
  402. */
  403. KhronosTextureContainer2.DefaultDecoderOptions = new DefaultKTX2DecoderOptions();
  404. //# sourceMappingURL=khronosTextureContainer2.js.map