basis.ts 19 KB

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