basis.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. import { Nullable } from '../types';
  2. import { Tools } from './tools';
  3. import { Texture } from '../Materials/Textures/texture';
  4. import { InternalTexture } from '../Materials/Textures/internalTexture';
  5. import { Scalar } from '../Maths/math.scalar';
  6. import { Constants } from '../Engines/constants';
  7. /**
  8. * Info about the .basis files
  9. */
  10. class BasisFileInfo {
  11. /**
  12. * If the file has alpha
  13. */
  14. public hasAlpha: boolean;
  15. /**
  16. * Info about each image of the basis file
  17. */
  18. public images: Array<{levels: Array<{width: number, height: number, transcodedPixels: ArrayBufferView}>}>;
  19. }
  20. /**
  21. * Result of transcoding a basis file
  22. */
  23. class TranscodeResult {
  24. /**
  25. * Info about the .basis file
  26. */
  27. public fileInfo: BasisFileInfo;
  28. /**
  29. * Format to use when loading the file
  30. */
  31. public format: number;
  32. }
  33. /**
  34. * Configuration options for the Basis transcoder
  35. */
  36. export class BasisTranscodeConfiguration {
  37. /**
  38. * Supported compression formats used to determine the supported output format of the transcoder
  39. */
  40. supportedCompressionFormats?: {
  41. /**
  42. * etc1 compression format
  43. */
  44. etc1?: boolean;
  45. /**
  46. * s3tc compression format
  47. */
  48. s3tc?: boolean;
  49. /**
  50. * pvrtc compression format
  51. */
  52. pvrtc?: boolean;
  53. /**
  54. * etc2 compression format
  55. */
  56. etc2?: boolean;
  57. };
  58. /**
  59. * If mipmap levels should be loaded for transcoded images (Default: true)
  60. */
  61. loadMipmapLevels?: boolean;
  62. /**
  63. * Index of a single image to load (Default: all images)
  64. */
  65. loadSingleImage?: number;
  66. }
  67. /**
  68. * @hidden
  69. * Enum of basis transcoder formats
  70. */
  71. enum BASIS_FORMATS {
  72. cTFETC1 = 0,
  73. cTFBC1 = 1,
  74. cTFBC4 = 2,
  75. cTFPVRTC1_4_OPAQUE_ONLY = 3,
  76. cTFBC7_M6_OPAQUE_ONLY = 4,
  77. cTFETC2 = 5,
  78. cTFBC3 = 6,
  79. cTFBC5 = 7
  80. }
  81. /**
  82. * Used to load .Basis files
  83. * See https://github.com/BinomialLLC/basis_universal/tree/master/webgl
  84. */
  85. export class BasisTools {
  86. private static _IgnoreSupportedFormats = false;
  87. /**
  88. * URL to use when loading the basis transcoder
  89. */
  90. public static JSModuleURL = "https://preview.babylonjs.com/basisTranscoder/basis_transcoder.js";
  91. /**
  92. * URL to use when loading the wasm module for the transcoder
  93. */
  94. public static WasmModuleURL = "https://preview.babylonjs.com/basisTranscoder/basis_transcoder.wasm";
  95. /**
  96. * Get the internal format to be passed to texImage2D corresponding to the .basis format value
  97. * @param basisFormat format chosen from GetSupportedTranscodeFormat
  98. * @returns internal format corresponding to the Basis format
  99. */
  100. public static GetInternalFormatFromBasisFormat(basisFormat: number) {
  101. // Corrisponding internal formats
  102. var COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0;
  103. var COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3;
  104. var RGB_ETC1_Format = 36196;
  105. if (basisFormat === BASIS_FORMATS.cTFETC1) {
  106. return RGB_ETC1_Format;
  107. }else if (basisFormat === BASIS_FORMATS.cTFBC1) {
  108. return COMPRESSED_RGB_S3TC_DXT1_EXT;
  109. }else if (basisFormat === BASIS_FORMATS.cTFBC3) {
  110. return COMPRESSED_RGBA_S3TC_DXT5_EXT;
  111. }else {
  112. throw "The chosen Basis transcoder format is not currently supported";
  113. }
  114. }
  115. private static _WorkerPromise: Nullable<Promise<Worker>> = null;
  116. private static _Worker: Nullable<Worker> = null;
  117. private static _actionId = 0;
  118. private static _CreateWorkerAsync() {
  119. if (!this._WorkerPromise) {
  120. this._WorkerPromise = new Promise((res) => {
  121. if (this._Worker) {
  122. res(this._Worker);
  123. }else {
  124. Tools.LoadFileAsync(BasisTools.WasmModuleURL).then((wasmBinary) => {
  125. const workerBlobUrl = URL.createObjectURL(new Blob([`(${workerFunc})()`], { type: "application/javascript" }));
  126. this._Worker = new Worker(workerBlobUrl);
  127. var initHandler = (msg: any) => {
  128. if (msg.data.action === "init") {
  129. this._Worker!.removeEventListener("message", initHandler);
  130. res(this._Worker!);
  131. }
  132. };
  133. this._Worker.addEventListener("message", initHandler);
  134. this._Worker.postMessage({action: "init", url: BasisTools.JSModuleURL, wasmBinary: wasmBinary});
  135. });
  136. }
  137. });
  138. }
  139. return this._WorkerPromise;
  140. }
  141. /**
  142. * Transcodes a loaded image file to compressed pixel data
  143. * @param imageData image data to transcode
  144. * @param config configuration options for the transcoding
  145. * @returns a promise resulting in the transcoded image
  146. */
  147. public static TranscodeAsync(imageData: ArrayBuffer, config: BasisTranscodeConfiguration): Promise<TranscodeResult> {
  148. return new Promise((res, rej) => {
  149. this._CreateWorkerAsync().then(() => {
  150. var actionId = this._actionId++;
  151. var messageHandler = (msg: any) => {
  152. if (msg.data.action === "transcode" && msg.data.id === actionId) {
  153. this._Worker!.removeEventListener("message", messageHandler);
  154. if (!msg.data.success) {
  155. rej("Transcode is not supported on this device");
  156. }else {
  157. res(msg.data);
  158. }
  159. }
  160. };
  161. this._Worker!.addEventListener("message", messageHandler);
  162. this._Worker!.postMessage({action: "transcode", id: actionId, imageData: imageData, config: config, ignoreSupportedFormats: this._IgnoreSupportedFormats}, [imageData]);
  163. });
  164. });
  165. }
  166. /**
  167. * Loads a texture from the transcode result
  168. * @param texture texture load to
  169. * @param transcodeResult the result of transcoding the basis file to load from
  170. */
  171. public static LoadTextureFromTranscodeResult(texture: InternalTexture, transcodeResult: TranscodeResult) {
  172. for (var i = 0; i < transcodeResult.fileInfo.images.length; i++) {
  173. var rootImage = transcodeResult.fileInfo.images[i].levels[0];
  174. texture._invertVScale = texture.invertY;
  175. if (transcodeResult.format === -1) {
  176. // No compatable compressed format found, fallback to RGB
  177. texture.type = Constants.TEXTURETYPE_UNSIGNED_SHORT_5_6_5;
  178. texture.format = Constants.TEXTUREFORMAT_RGB;
  179. if (texture.getEngine().webGLVersion < 2 && (Scalar.Log2(rootImage.width) % 1 !== 0 || Scalar.Log2(rootImage.height) % 1 !== 0)) {
  180. // Create non power of two texture
  181. let source = new InternalTexture(texture.getEngine(), InternalTexture.DATASOURCE_TEMP);
  182. texture._invertVScale = texture.invertY;
  183. source.type = Constants.TEXTURETYPE_UNSIGNED_SHORT_5_6_5;
  184. source.format = Constants.TEXTUREFORMAT_RGB;
  185. // Fallback requires aligned width/height
  186. source.width = (rootImage.width + 3) & ~3;
  187. source.height = (rootImage.height + 3) & ~3;
  188. texture.getEngine()._bindTextureDirectly(source.getEngine()._gl.TEXTURE_2D, source, true);
  189. texture.getEngine()._uploadDataToTextureDirectly(source, rootImage.transcodedPixels, i, 0, Constants.TEXTUREFORMAT_RGB, true);
  190. // Resize to power of two
  191. source.getEngine()._rescaleTexture(source, texture, texture.getEngine().scenes[0], source.getEngine()._getInternalFormat(Constants.TEXTUREFORMAT_RGB), () => {
  192. source.getEngine()._releaseTexture(source);
  193. source.getEngine()._bindTextureDirectly(source.getEngine()._gl.TEXTURE_2D, texture, true);
  194. });
  195. }else {
  196. // Fallback is already inverted
  197. texture._invertVScale = !texture.invertY;
  198. // Upload directly
  199. texture.width = (rootImage.width + 3) & ~3;
  200. texture.height = (rootImage.height + 3) & ~3;
  201. texture.getEngine()._uploadDataToTextureDirectly(texture, rootImage.transcodedPixels, i, 0, Constants.TEXTUREFORMAT_RGB, true);
  202. }
  203. }else {
  204. texture.width = rootImage.width;
  205. texture.height = rootImage.height;
  206. // Upload all mip levels in the file
  207. transcodeResult.fileInfo.images[i].levels.forEach((level: any, index: number) => {
  208. texture.getEngine()._uploadCompressedDataToTextureDirectly(texture, BasisTools.GetInternalFormatFromBasisFormat(transcodeResult.format!), level.width, level.height, level.transcodedPixels, i, index);
  209. });
  210. if (texture.getEngine().webGLVersion < 2 && (Scalar.Log2(texture.width) % 1 !== 0 || Scalar.Log2(texture.height) % 1 !== 0)) {
  211. Tools.Warn("Loaded .basis texture width and height are not a power of two. Texture wrapping will be set to Texture.CLAMP_ADDRESSMODE as other modes are not supported with non power of two dimensions in webGL 1.");
  212. texture._cachedWrapU = Texture.CLAMP_ADDRESSMODE;
  213. texture._cachedWrapV = Texture.CLAMP_ADDRESSMODE;
  214. }
  215. }
  216. }
  217. }
  218. }
  219. // WorkerGlobalScope
  220. declare function importScripts(...urls: string[]): void;
  221. declare function postMessage(message: any, transfer?: any[]): void;
  222. declare var Module: any;
  223. function workerFunc(): void {
  224. var _BASIS_FORMAT = {
  225. cTFETC1: 0,
  226. cTFBC1: 1,
  227. cTFBC4: 2,
  228. cTFPVRTC1_4_OPAQUE_ONLY: 3,
  229. cTFBC7_M6_OPAQUE_ONLY: 4,
  230. cTFETC2: 5,
  231. cTFBC3: 6,
  232. cTFBC5: 7,
  233. };
  234. var transcoderModulePromise: Nullable<Promise<any>> = null;
  235. onmessage = (event) => {
  236. if (event.data.action === "init") {
  237. // Load the transcoder if it hasn't been yet
  238. if (!transcoderModulePromise) {
  239. // Override wasm binary
  240. Module = { wasmBinary: (event.data.wasmBinary) };
  241. importScripts(event.data.url);
  242. transcoderModulePromise = new Promise((res) => {
  243. Module.onRuntimeInitialized = () => {
  244. Module.initializeBasis();
  245. res();
  246. };
  247. });
  248. }
  249. transcoderModulePromise.then(() => {
  250. postMessage({action: "init"});
  251. });
  252. }else if (event.data.action === "transcode") {
  253. // Transcode the basis image and return the resulting pixels
  254. var config: BasisTranscodeConfiguration = event.data.config;
  255. var imgData = event.data.imageData;
  256. var loadedFile = new Module.BasisFile(new Uint8Array(imgData));
  257. var fileInfo = GetFileInfo(loadedFile);
  258. var format = event.data.ignoreSupportedFormats ? null : GetSupportedTranscodeFormat(event.data.config, fileInfo);
  259. var needsConversion = false;
  260. if (format === null) {
  261. needsConversion = true;
  262. format = fileInfo.hasAlpha ? _BASIS_FORMAT.cTFBC3 : _BASIS_FORMAT.cTFBC1;
  263. }
  264. // Begin transcode
  265. var success = true;
  266. if (!loadedFile.startTranscoding()) {
  267. success = false;
  268. }
  269. var buffers: Array<any> = [];
  270. for (var imageIndex = 0; imageIndex < fileInfo.images.length; imageIndex++) {
  271. if (!success) {
  272. break;
  273. }
  274. var image = fileInfo.images[imageIndex];
  275. if (config.loadSingleImage === undefined || config.loadSingleImage === imageIndex) {
  276. var mipCount = image.levels.length;
  277. if (config.loadMipmapLevels === false) {
  278. mipCount = 1;
  279. }
  280. for (var levelIndex = 0; levelIndex < mipCount; levelIndex++) {
  281. var levelInfo = image.levels[levelIndex];
  282. var pixels = TranscodeLevel(loadedFile, imageIndex, levelIndex, format!, needsConversion);
  283. if (!pixels) {
  284. success = false;
  285. break;
  286. }
  287. levelInfo.transcodedPixels = pixels;
  288. buffers.push(levelInfo.transcodedPixels.buffer);
  289. }
  290. }
  291. }
  292. // Close file
  293. loadedFile.close();
  294. loadedFile.delete();
  295. if (needsConversion) {
  296. format = -1;
  297. }
  298. if (!success) {
  299. postMessage({action: "transcode", success: success, id: event.data.id});
  300. }else {
  301. postMessage({action: "transcode", success: success, id: event.data.id, fileInfo: fileInfo, format: format}, buffers);
  302. }
  303. }
  304. };
  305. /**
  306. * Detects the supported transcode format for the file
  307. * @param config transcode config
  308. * @param fileInfo info about the file
  309. * @returns the chosed format or null if none are supported
  310. */
  311. function GetSupportedTranscodeFormat(config: BasisTranscodeConfiguration, fileInfo: BasisFileInfo): Nullable<number> {
  312. var format = null;
  313. if (config.supportedCompressionFormats) {
  314. if (config.supportedCompressionFormats.etc1) {
  315. format = _BASIS_FORMAT.cTFETC1;
  316. }else if (config.supportedCompressionFormats.s3tc) {
  317. format = fileInfo.hasAlpha ? _BASIS_FORMAT.cTFBC3 : _BASIS_FORMAT.cTFBC1;
  318. }else if (config.supportedCompressionFormats.pvrtc) {
  319. // TODO uncomment this after pvrtc bug is fixed is basis transcoder
  320. // See discussion here: https://github.com/mrdoob/three.js/issues/16524#issuecomment-498929924
  321. // format = _BASIS_FORMAT.cTFPVRTC1_4_OPAQUE_ONLY;
  322. }else if (config.supportedCompressionFormats.etc2) {
  323. format = _BASIS_FORMAT.cTFETC2;
  324. }
  325. }
  326. return format;
  327. }
  328. /**
  329. * Retreives information about the basis file eg. dimensions
  330. * @param basisFile the basis file to get the info from
  331. * @returns information about the basis file
  332. */
  333. function GetFileInfo(basisFile: any): BasisFileInfo {
  334. var hasAlpha = basisFile.getHasAlpha();
  335. var imageCount = basisFile.getNumImages();
  336. var images = [];
  337. for (var i = 0; i < imageCount; i++) {
  338. var imageInfo = {
  339. levels: ([] as Array<any>)
  340. };
  341. var levelCount = basisFile.getNumLevels(i);
  342. for (var level = 0; level < levelCount; level++) {
  343. var levelInfo = {
  344. width: basisFile.getImageWidth(i, level),
  345. height: basisFile.getImageHeight(i, level)
  346. };
  347. imageInfo.levels.push(levelInfo);
  348. }
  349. images.push(imageInfo);
  350. }
  351. var info = { hasAlpha, images };
  352. return info;
  353. }
  354. function TranscodeLevel(loadedFile: any, imageIndex: number, levelIndex: number, format: number, convertToRgb565: boolean): Nullable<Uint16Array> {
  355. var dstSize = loadedFile.getImageTranscodedSizeInBytes(imageIndex, levelIndex, format);
  356. var dst = new Uint8Array(dstSize);
  357. if (!loadedFile.transcodeImage(dst, imageIndex, levelIndex, format, 1, 0)) {
  358. return null;
  359. }
  360. // If no supported format is found, load as dxt and convert to rgb565
  361. if (convertToRgb565) {
  362. var alignedWidth = (loadedFile.getImageWidth(imageIndex, levelIndex) + 3) & ~3;
  363. var alignedHeight = (loadedFile.getImageHeight(imageIndex, levelIndex) + 3) & ~3;
  364. dst = ConvertDxtToRgb565(dst, 0, alignedWidth, alignedHeight);
  365. }
  366. return dst;
  367. }
  368. /**
  369. * From https://github.com/BinomialLLC/basis_universal/blob/master/webgl/texture/dxt-to-rgb565.js
  370. * An unoptimized version of dxtToRgb565. Also, the floating
  371. * point math used to compute the colors actually results in
  372. * slightly different colors compared to hardware DXT decoders.
  373. * @param src dxt src pixels
  374. * @param srcByteOffset offset for the start of src
  375. * @param width aligned width of the image
  376. * @param height aligned height of the image
  377. * @return the converted pixels
  378. */
  379. function ConvertDxtToRgb565(src: Uint8Array, srcByteOffset: number, width: number, height: number): Uint16Array {
  380. var c = new Uint16Array(4);
  381. var dst = new Uint16Array(width * height);
  382. var blockWidth = width / 4;
  383. var blockHeight = height / 4;
  384. for (var blockY = 0; blockY < blockHeight; blockY++) {
  385. for (var blockX = 0; blockX < blockWidth; blockX++) {
  386. var i = srcByteOffset + 8 * (blockY * blockWidth + blockX);
  387. c[0] = src[i] | (src[i + 1] << 8);
  388. c[1] = src[i + 2] | (src[i + 3] << 8);
  389. c[2] = (2 * (c[0] & 0x1f) + 1 * (c[1] & 0x1f)) / 3
  390. | (((2 * (c[0] & 0x7e0) + 1 * (c[1] & 0x7e0)) / 3) & 0x7e0)
  391. | (((2 * (c[0] & 0xf800) + 1 * (c[1] & 0xf800)) / 3) & 0xf800);
  392. c[3] = (2 * (c[1] & 0x1f) + 1 * (c[0] & 0x1f)) / 3
  393. | (((2 * (c[1] & 0x7e0) + 1 * (c[0] & 0x7e0)) / 3) & 0x7e0)
  394. | (((2 * (c[1] & 0xf800) + 1 * (c[0] & 0xf800)) / 3) & 0xf800);
  395. for (var row = 0; row < 4; row++) {
  396. var m = src[i + 4 + row];
  397. var dstI = (blockY * 4 + row) * width + blockX * 4;
  398. dst[dstI++] = c[m & 0x3];
  399. dst[dstI++] = c[(m >> 2) & 0x3];
  400. dst[dstI++] = c[(m >> 4) & 0x3];
  401. dst[dstI++] = c[(m >> 6) & 0x3];
  402. }
  403. }
  404. }
  405. return dst;
  406. }
  407. }