diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7106fe5..0000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -dist: bionic - -stages: - - build - -matrix: - include: - - name: Static checks (format.sh) - stage: build - os: linux - addons: - apt: - packages: - - dos2unix - - recode - -script: - - bash ./format.sh diff --git a/README.md b/README.md index 4ec10ce..eda5f82 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ ATC: See the OpenGL extension [GL_AMD_compressed_ATC_texture](https://www.khrono FXT1: See the OpenGL extension [GL_3DFX_texture_compression_FXT1](https://www.khronos.org/registry/OpenGL/extensions/3DFX/3DFX_texture_compression_FXT1.txt). -Also see [Intel® Open Source HD Graphics Programmers' Reference Manual (PRM)](https://01.org/sites/default/files/documentation/intel-gfx-bspec-osrc-chv-bsw-vol05-memory-views.pdf). This reference manual details how to encode FXT1, ETC1, ETC2, EAC, DXT/BC1-3, BC4/5/7, and ASTC. +Also see [Intel(R) Open Source HD Graphics Programmers' Reference Manual (PRM)](https://01.org/sites/default/files/documentation/intel-gfx-bspec-osrc-chv-bsw-vol05-memory-views.pdf). This reference manual details how to encode FXT1, ETC1, ETC2, EAC, DXT/BC1-3, BC4/5/7, and ASTC. ### Repository Licensing with REUSE diff --git a/appveyor.yml b/appveyor.yml index b9ba95c..d790c2e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,12 +1,21 @@ +--- + image: - - Visual Studio 2019 - - Ubuntu - macos + - Ubuntu2004 + - Visual Studio 2019 configuration: Release environment: APPVEYOR_YML_DISABLE_PS_LINUX: true + +install: +- sh: | + if [ "$(uname)" != "Darwin" ]; then + sudo apt-get update -y + sudo apt-get install -y dos2unix recode + fi build_script: - ps: | @@ -24,7 +33,16 @@ build_script: cmake --build . --config ${CONFIGURATION} cd ../ +test_script: + - sh: | + if [ "$(uname)" != "Darwin" ]; then + bash ./format.sh + fi + artifacts: - - path: bin\$(configuration)\basisu.exe + # Linux - path: bin/basisu + # MacOS - path: bin_osx/basisu + # Windows + - path: bin\$(configuration)\basisu.exe diff --git a/build_emscripten.sh b/build_emscripten.sh index 274f539..7b0cfb7 100755 --- a/build_emscripten.sh +++ b/build_emscripten.sh @@ -6,4 +6,4 @@ then fi emcmake cmake . -emcmake make \ No newline at end of file +emcmake make diff --git a/webgl/README.md b/webgl/README.md index 333c932..4ad633b 100644 --- a/webgl/README.md +++ b/webgl/README.md @@ -35,7 +35,7 @@ Renders a glTF 3D model with `.basis` texture files, transcoded into one of the * PVRTC * Tested in Chrome and Safari on iOS iPhone 6 Plus. -The glTF model in this demo uses a hypothetical `GOOGLE_texture_basis` extension. That extension is defined for the sake of example only – the glTF format will officially embed Basis files within a KTX2 wrapper, through a new +The glTF model in this demo uses a hypothetical `GOOGLE_texture_basis` extension. That extension is defined for the sake of example only - the glTF format will officially embed Basis files within a KTX2 wrapper, through a new extension that is [currently in development](https://github.com/KhronosGroup/glTF/pull/1612). ![Screenshot showing a basis texture rendered as the base color texture for a 3D model in a webpage.](gltf/preview.png) diff --git a/webgl/transcoder/build/basis_loader.js b/webgl/transcoder/build/basis_loader.js index 8cd4a0e..dcc0179 100644 --- a/webgl/transcoder/build/basis_loader.js +++ b/webgl/transcoder/build/basis_loader.js @@ -1,494 +1,494 @@ -/* - * 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`); - } - }; +/* + * 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`); + } + }; }