| /* |
| * Basis Loader |
| * Note: This is the same as transcoder/build/basis_loader.js, except modified to import basis_encoder.js (which also includes the transcoder). |
| * |
| * Usage: |
| * // basis_loader.js should be loaded from the same directory as |
| * // basis_encoder.js and basis_encoder.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_encoder.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`); |
| } |
| }; |
| } |