/* * Basis Loader * * Usage: * // basis_loader.js should be loaded from the same directory as * // basis_transcoder.js and basis_transcoder.wasm * * // Create the texture loader and set the WebGL context it should use. Spawns * // a worker which handles all of the transcoding. * let basisLoader = new BasisLoader(); * basisLoader.setWebGLContext(gl); * * // To allow separate color and alpha textures to be returned in cases where * // it would provide higher quality: * basisLoader.allowSeparateAlpha = true; * * // loadFromUrl() returns a promise which resolves to a completed WebGL * // texture or rejects if there's an error loading. * basisBasics.loadFromUrl(fullPathToTexture).then((result) => { * // WebGL color+alpha texture; * result.texture; * * // WebGL alpha texture, only if basisLoader.allowSeparateAlpha is true. * // null if alpha is encoded in result.texture or result.alpha is false. * result.alphaTexture; * * // True if the texture contained an alpha channel. * result.alpha; * * // Number of mip levels in texture/alphaTexture * result.mipLevels; * * // Dimensions of the base mip level. * result.width; * result.height; * }); */ // This file contains the code both for the main thread interface and the // worker that does the transcoding. const IN_WORKER = typeof importScripts === "function"; const SCRIPT_PATH = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; if (!IN_WORKER) { // // Main Thread // class PendingTextureRequest { constructor(gl, url) { this.gl = gl; this.url = url; this.texture = null; this.alphaTexture = null; this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } uploadImageData(webglFormat, buffer, mipLevels) { let gl = this.gl; let texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, mipLevels.length > 1 || webglFormat.uncompressed ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR); let levelData = null; for (let mipLevel of mipLevels) { if (!webglFormat.uncompressed) { levelData = new Uint8Array(buffer, mipLevel.offset, mipLevel.size); gl.compressedTexImage2D( gl.TEXTURE_2D, mipLevel.level, webglFormat.format, mipLevel.width, mipLevel.height, 0, levelData); } else { switch (webglFormat.type) { case WebGLRenderingContext.UNSIGNED_SHORT_4_4_4_4: case WebGLRenderingContext.UNSIGNED_SHORT_5_5_5_1: case WebGLRenderingContext.UNSIGNED_SHORT_5_6_5: levelData = new Uint16Array(buffer, mipLevel.offset, mipLevel.size / 2); break; default: levelData = new Uint8Array(buffer, mipLevel.offset, mipLevel.size); break; } gl.texImage2D( gl.TEXTURE_2D, mipLevel.level, webglFormat.format, mipLevel.width, mipLevel.height, 0, webglFormat.format, webglFormat.type, levelData); } } if (webglFormat.uncompressed && mipLevels.length == 1) { gl.generateMipmap(gl.TEXTURE_2D); } return texture; } }; class BasisLoader { constructor() { this.gl = null; this.supportedFormats = {}; this.pendingTextures = {}; this.nextPendingTextureId = 1; this.allowSeparateAlpha = false; // Reload the current script as a worker this.worker = new Worker(SCRIPT_PATH); this.worker.onmessage = (msg) => { // Find the pending texture associated with the data we just received // from the worker. let pendingTexture = this.pendingTextures[msg.data.id]; if (!pendingTexture) { if (msg.data.error) { console.error(`Basis transcode failed: ${msg.data.error}`); } console.error(`Invalid pending texture ID: ${msg.data.id}`); return; } // Remove the pending texture from the waiting list. delete this.pendingTextures[msg.data.id]; // If the worker indicated an error has occured handle it now. if (msg.data.error) { console.error(`Basis transcode failed: ${msg.data.error}`); pendingTexture.reject(`${msg.data.error}`); return; } // Upload the image data returned by the worker. pendingTexture.texture = pendingTexture.uploadImageData( msg.data.webglFormat, msg.data.buffer, msg.data.mipLevels); if (msg.data.alphaBuffer) { pendingTexture.alphaTexture = pendingTexture.uploadImageData( msg.data.webglFormat, msg.data.alphaBuffer, msg.data.mipLevels); } pendingTexture.resolve({ mipLevels: msg.data.mipLevels.length, width: msg.data.mipLevels[0].width, height: msg.data.mipLevels[0].height, alpha: msg.data.hasAlpha, texture: pendingTexture.texture, alphaTexture: pendingTexture.alphaTexture, }); }; } setWebGLContext(gl) { if (this.gl != gl) { this.gl = gl; if (gl) { this.supportedFormats = { s3tc: !!gl.getExtension('WEBGL_compressed_texture_s3tc'), etc1: !!gl.getExtension('WEBGL_compressed_texture_etc1'), etc2: !!gl.getExtension('WEBGL_compressed_texture_etc'), pvrtc: !!gl.getExtension('WEBGL_compressed_texture_pvrtc'), astc: !!gl.getExtension('WEBGL_compressed_texture_astc'), bptc: !!gl.getExtension('EXT_texture_compression_bptc') }; } else { this.supportedFormats = {}; } } } // This method changes the active texture unit's TEXTURE_2D binding // immediately prior to resolving the returned promise. loadFromUrl(url) { let pendingTexture = new PendingTextureRequest(this.gl, url); this.pendingTextures[this.nextPendingTextureId] = pendingTexture; this.worker.postMessage({ id: this.nextPendingTextureId, url: url, allowSeparateAlpha: this.allowSeparateAlpha, supportedFormats: this.supportedFormats }); this.nextPendingTextureId++; return pendingTexture.promise; } } window.BasisLoader = BasisLoader; } else { // // Worker // importScripts('basis_transcoder.js'); let BasisFile = null; const BASIS_INITIALIZED = BASIS().then((module) => { BasisFile = module.BasisFile; module.initializeBasis(); }); // Copied from enum class transcoder_texture_format in basisu_transcoder.h with minor javascript-ification const BASIS_FORMAT = { // Compressed formats // ETC1-2 cTFETC1_RGB: 0, // Opaque only, returns RGB or alpha data if cDecodeFlagsTranscodeAlphaDataToOpaqueFormats flag is specified cTFETC2_RGBA: 1, // Opaque+alpha, ETC2_EAC_A8 block followed by a ETC1 block, alpha channel will be opaque for opaque .basis files // BC1-5, BC7 (desktop, some mobile devices) cTFBC1_RGB: 2, // Opaque only, no punchthrough alpha support yet, transcodes alpha slice if cDecodeFlagsTranscodeAlphaDataToOpaqueFormats flag is specified cTFBC3_RGBA: 3, // Opaque+alpha, BC4 followed by a BC1 block, alpha channel will be opaque for opaque .basis files cTFBC4_R: 4, // Red only, alpha slice is transcoded to output if cDecodeFlagsTranscodeAlphaDataToOpaqueFormats flag is specified cTFBC5_RG: 5, // XY: Two BC4 blocks, X=R and Y=Alpha, .basis file should have alpha data (if not Y will be all 255's) cTFBC7_RGBA: 6, // RGB or RGBA, mode 5 for ETC1S, modes (1,2,3,5,6,7) for UASTC // PVRTC1 4bpp (mobile, PowerVR devices) cTFPVRTC1_4_RGB: 8, // Opaque only, RGB or alpha if cDecodeFlagsTranscodeAlphaDataToOpaqueFormats flag is specified, nearly lowest quality of any texture format. cTFPVRTC1_4_RGBA: 9, // Opaque+alpha, most useful for simple opacity maps. If .basis file doesn't have alpha cTFPVRTC1_4_RGB will be used instead. Lowest quality of any supported texture format. // ASTC (mobile, Intel devices, hopefully all desktop GPU's one day) cTFASTC_4x4_RGBA: 10, // Opaque+alpha, ASTC 4x4, alpha channel will be opaque for opaque .basis files. Transcoder uses RGB/RGBA/L/LA modes, void extent, and up to two ([0,47] and [0,255]) endpoint precisions. // Uncompressed (raw pixel) formats cTFRGBA32: 13, // 32bpp RGBA image stored in raster (not block) order in memory, R is first byte, A is last byte. cTFRGB565: 14, // 166pp RGB image stored in raster (not block) order in memory, R at bit position 11 cTFBGR565: 15, // 16bpp RGB image stored in raster (not block) order in memory, R at bit position 0 cTFRGBA4444: 16, // 16bpp RGBA image stored in raster (not block) order in memory, R at bit position 12, A at bit position 0 cTFTotalTextureFormats: 22, }; // WebGL compressed formats types, from: // http://www.khronos.org/registry/webgl/extensions/ // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/ const COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0; const COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1; const COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2; const COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3; // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_etc1/ const COMPRESSED_RGB_ETC1_WEBGL = 0x8D64; // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_etc/ const COMPRESSED_R11_EAC = 0x9270; const COMPRESSED_SIGNED_R11_EAC = 0x9271; const COMPRESSED_RG11_EAC = 0x9272; const COMPRESSED_SIGNED_RG11_EAC = 0x9273; const COMPRESSED_RGB8_ETC2 = 0x9274; const COMPRESSED_SRGB8_ETC2 = 0x9275; const COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276; const COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277; const COMPRESSED_RGBA8_ETC2_EAC = 0x9278; const COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279; // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_astc/ const COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0; // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_pvrtc/ const COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00; const COMPRESSED_RGB_PVRTC_2BPPV1_IMG = 0x8C01; const COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02; const COMPRESSED_RGBA_PVRTC_2BPPV1_IMG = 0x8C03; // https://www.khronos.org/registry/webgl/extensions/EXT_texture_compression_bptc/ const COMPRESSED_RGBA_BPTC_UNORM_EXT = 0x8E8C; const COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT = 0x8E8D; const COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT = 0x8E8E; const COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT = 0x8E8F; const BASIS_WEBGL_FORMAT_MAP = {}; // Compressed formats BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFBC1_RGB] = { format: COMPRESSED_RGB_S3TC_DXT1_EXT }; BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFBC3_RGBA] = { format: COMPRESSED_RGBA_S3TC_DXT5_EXT }; BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFBC7_RGBA] = { format: COMPRESSED_RGBA_BPTC_UNORM_EXT }; BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFETC1_RGB] = { format: COMPRESSED_RGB_ETC1_WEBGL }; BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFETC2_RGBA] = { format: COMPRESSED_RGBA8_ETC2_EAC }; BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFASTC_4x4_RGBA] = { format: COMPRESSED_RGBA_ASTC_4x4_KHR }; BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFPVRTC1_4_RGB] = { format: COMPRESSED_RGB_PVRTC_4BPPV1_IMG }; BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFPVRTC1_4_RGBA] = { format: COMPRESSED_RGBA_PVRTC_4BPPV1_IMG }; // Uncompressed formats BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFRGBA32] = { uncompressed: true, format: WebGLRenderingContext.RGBA, type: WebGLRenderingContext.UNSIGNED_BYTE }; BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFRGB565] = { uncompressed: true, format: WebGLRenderingContext.RGB, type: WebGLRenderingContext.UNSIGNED_SHORT_5_6_5 }; BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFRGBA4444] = { uncompressed: true, format: WebGLRenderingContext.RGBA, type: WebGLRenderingContext.UNSIGNED_SHORT_4_4_4_4 }; // Notifies the main thread when a texture has failed to load for any reason. function fail(id, errorMsg) { postMessage({ id: id, error: errorMsg }); } function basisFileFail(id, basisFile, errorMsg) { fail(id, errorMsg); basisFile.close(); basisFile.delete(); } // This utility currently only transcodes the first image in the file. const IMAGE_INDEX = 0; const TOP_LEVEL_MIP = 0; function transcode(id, arrayBuffer, supportedFormats, allowSeparateAlpha) { let basisData = new Uint8Array(arrayBuffer); let basisFile = new BasisFile(basisData); let images = basisFile.getNumImages(); let levels = basisFile.getNumLevels(IMAGE_INDEX); let hasAlpha = basisFile.getHasAlpha(); if (!images || !levels) { basisFileFail(id, basisFile, 'Invalid Basis data'); return; } if (!basisFile.startTranscoding()) { basisFileFail(id, basisFile, 'startTranscoding failed'); return; } let basisFormat = undefined; let needsSecondaryAlpha = false; if (hasAlpha) { if (supportedFormats.etc2) { basisFormat = BASIS_FORMAT.cTFETC2_RGBA; } else if (supportedFormats.bptc) { basisFormat = BASIS_FORMAT.cTFBC7_RGBA; } else if (supportedFormats.s3tc) { basisFormat = BASIS_FORMAT.cTFBC3_RGBA; } else if (supportedFormats.astc) { basisFormat = BASIS_FORMAT.cTFASTC_4x4_RGBA; } else if (supportedFormats.pvrtc) { if (allowSeparateAlpha) { basisFormat = BASIS_FORMAT.cTFPVRTC1_4_RGB; needsSecondaryAlpha = true; } else { basisFormat = BASIS_FORMAT.cTFPVRTC1_4_RGBA; } } else if (supportedFormats.etc1 && allowSeparateAlpha) { basisFormat = BASIS_FORMAT.cTFETC1_RGB; needsSecondaryAlpha = true; } else { // If we don't support any appropriate compressed formats transcode to // raw pixels. This is something of a last resort, because the GPU // upload will be significantly slower and take a lot more memory, but // at least it prevents you from needing to store a fallback JPG/PNG and // the download size will still likely be smaller. basisFormat = BASIS_FORMAT.RGBA32; } } else { if (supportedFormats.etc1) { // Should be the highest quality, so use when available. // http://richg42.blogspot.com/2018/05/basis-universal-gpu-texture-format.html basisFormat = BASIS_FORMAT.cTFETC1_RGB; } else if (supportedFormats.bptc) { basisFormat = BASIS_FORMAT.cTFBC7_RGBA; } else if (supportedFormats.s3tc) { basisFormat = BASIS_FORMAT.cTFBC1_RGB; } else if (supportedFormats.etc2) { basisFormat = BASIS_FORMAT.cTFETC2_RGBA; } else if (supportedFormats.astc) { basisFormat = BASIS_FORMAT.cTFASTC_4x4_RGBA; } else if (supportedFormats.pvrtc) { basisFormat = BASIS_FORMAT.cTFPVRTC1_4_RGB; } else { // See note on uncompressed transcode above. basisFormat = BASIS_FORMAT.cTFRGB565; } } if (basisFormat === undefined) { basisFileFail(id, basisFile, 'No supported transcode formats'); return; } let webglFormat = BASIS_WEBGL_FORMAT_MAP[basisFormat]; // If we're not using compressed textures it'll be cheaper to generate // mipmaps on the fly, so only transcode a single level. if (webglFormat.uncompressed) { levels = 1; } // Gather information about each mip level to be transcoded. let mipLevels = []; let totalTranscodeSize = 0; for (let mipLevel = 0; mipLevel < levels; ++mipLevel) { let transcodeSize = basisFile.getImageTranscodedSizeInBytes(IMAGE_INDEX, mipLevel, basisFormat); mipLevels.push({ level: mipLevel, offset: totalTranscodeSize, size: transcodeSize, width: basisFile.getImageWidth(IMAGE_INDEX, mipLevel), height: basisFile.getImageHeight(IMAGE_INDEX, mipLevel), }); totalTranscodeSize += transcodeSize; } // Allocate a buffer large enough to hold all of the transcoded mip levels at once. let transcodeData = new Uint8Array(totalTranscodeSize); let alphaTranscodeData = needsSecondaryAlpha ? new Uint8Array(totalTranscodeSize) : null; // Transcode each mip level into the appropriate section of the overall buffer. for (let mipLevel of mipLevels) { let levelData = new Uint8Array(transcodeData.buffer, mipLevel.offset, mipLevel.size); if (!basisFile.transcodeImage(levelData, IMAGE_INDEX, mipLevel.level, basisFormat, 1, 0)) { basisFileFail(id, basisFile, 'transcodeImage failed'); return; } if (needsSecondaryAlpha) { let alphaLevelData = new Uint8Array(alphaTranscodeData.buffer, mipLevel.offset, mipLevel.size); if (!basisFile.transcodeImage(alphaLevelData, IMAGE_INDEX, mipLevel.level, basisFormat, 1, 1)) { basisFileFail(id, basisFile, 'alpha transcodeImage failed'); return; } } } basisFile.close(); basisFile.delete(); // Post the transcoded results back to the main thread. let transferList = [transcodeData.buffer]; if (needsSecondaryAlpha) { transferList.push(alphaTranscodeData.buffer); } postMessage({ id: id, buffer: transcodeData.buffer, alphaBuffer: needsSecondaryAlpha ? alphaTranscodeData.buffer : null, webglFormat: webglFormat, mipLevels: mipLevels, hasAlpha: hasAlpha, }, transferList); } onmessage = (msg) => { // Each call to the worker must contain: let url = msg.data.url; // The URL of the basis image OR let buffer = msg.data.buffer; // An array buffer with the basis image data let allowSeparateAlpha = msg.data.allowSeparateAlpha; let supportedFormats = msg.data.supportedFormats; // The formats this device supports let id = msg.data.id; // A unique ID for the texture if (url) { // Make the call to fetch the basis texture data fetch(url).then(function(response) { if (response.ok) { response.arrayBuffer().then((arrayBuffer) => { if (BasisFile) { transcode(id, arrayBuffer, supportedFormats, allowSeparateAlpha); } else { BASIS_INITIALIZED.then(() => { transcode(id, arrayBuffer, supportedFormats, allowSeparateAlpha); }); } }); } else { fail(id, `Fetch failed: ${response.status}, ${response.statusText}`); } }); } else if (buffer) { if (BasisFile) { transcode(id, buffer, supportedFormats, allowSeparateAlpha); } else { BASIS_INITIALIZED.then(() => { transcode(id, buffer, supportedFormats, allowSeparateAlpha); }); } } else { fail(id, `No url or buffer specified`); } }; }