basis.ts 19 KB

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