Skip to content

Instantly share code, notes, and snippets.

@birdinforest
Last active December 15, 2023 01:16
Show Gist options
  • Save birdinforest/5b802cdc36a71f77c1fcae501bad0c06 to your computer and use it in GitHub Desktop.
Save birdinforest/5b802cdc36a71f77c1fcae501bad0c06 to your computer and use it in GitHub Desktop.
Decompress .basis compressed texture. #webgl #texture_compression #basisu #javascript #decompression
/* eslint-disable */
// const BASIS = require('./basis_transcoder');
import BASIS from './basis_transcoder/build/basis_transcoder.js';
/*global clay */
/**
* @typedef MipLevelInfo
* @property {number} level - Mip level
* @property {number} offset - Buffer view offset
* @property {number} size - Buffer view size
* @property {number} width - Mip map width
* @property {number} height - Mip map height
*/
// This utility currently only transcodes the first image in the file.
const IMAGE_INDEX = 0;
const TOP_LEVEL_MIP = 0;
/**
* Loader of Basis Universal compressed texture
* Modified from corresponding file in ThreeJS.
*
* **Priority of transcode format:**
*
* ASTC, PVRTC, DXT, RGB565 or RGBA32 (uncompressed)
*
* **Target format on normal cases**
*
* Desktop - COMPRESSED_RGBA_S3TC_DXT5_EXT
*
* iOS 13 - COMPRESSED_RGB_PVRTC_4BPPV1_IMG (Opaque) or COMPRESSED_RGBA_PVRTC_4BPPV1_IMG (Opaque and alpha)
*
* iOS 14 - RGBA_ASTC_4x4_Format
*
* **Case to apply uncompressed format:**
*
* iOS 13 - When texture is not square. RGB565(Opaque) or RGBA32(Opaque and alpha)
*
* **Mipmap:**
*
* To apply mipmaps for compressed format, have to generate mipmaps data in compression process by arguments `-mipmaps`
* If target format is uncompressed format, will only take data of level 0 to generate texture 2D. Mipmaps will be generated on flying.
*
* @Reference: https://github.com/BinomialLLC/basis_universal/blob/master/webgl/transcoder/build/basis_loader.js
*/
class BasisTextureLoader {
constructor() {
this.etcSupported = false;
this.dxtSupported = false;
this.pvrtcSupported = false;
this.astcSupported = false;
this.format = null;
this.type = UNSIGNED_BYTE;
this.getModulePending = null;
this.workCount = 0;
this.doCacheModule = true;
/**
* For opaque texture, do we want to export RGB format (`BASIS_FORMAT.cTFRGB565`) with pixel type `UNSIGNED_SHORT_5_6_5`?
* @Reference: https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texImage2D
*/
this.applyBpp16RGB = true;
this.debug =false;
}
getBasisModule() {
if (!this.getModulePending) {
this.getModulePending = new Promise(resolve => {
if (window.BasisFile) {
resolve();
} else {
const start = performance.now();
/*global BASIS */
BASIS().then(Module => {
const {BasisFile, initializeBasis} = Module;
initializeBasis();
window.BasisFile = BasisFile;
resolve();
});
}
});
}
this.workCount++;
return this.getModulePending;
}
releaseBasisModule() {
window.BasisFile = null;
this.getModulePending = null;
}
initBasisLoader(renderer, opt) {
const options = opt || {};
this.doCacheModule = options.doCacheModule || true;
this._detectSupport(renderer);
}
_detectSupport(renderer) {
const context = renderer.gl;
return this._detectSupportGL(context);
}
_detectSupportGL(gl) {
this.etcSupported = !!gl.getExtension('WEBGL_compressed_texture_etc1');
this.dxtSupported = !!gl.getExtension('WEBGL_compressed_texture_s3tc');
this.pvrtcSupported = !!gl.getExtension('WEBGL_compressed_texture_pvrtc') || !!gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc');
this.astcSupported = !!gl.getExtension('WEBGL_compressed_texture_astc');
console.log('supported -> ');
console.log('etc: ', this.etcSupported);
console.log('dxt: ', this.dxtSupported);
console.log('pvrtc: ', this.pvrtcSupported);
console.log('astc: ', this.astcSupported);
if (!this.astcSupported && !this.pvrtcSupported && !this.dxtSupported && !this.etcSupported) {
throw new Error('BasisTextureLoader: No suitable compressed texture format found.');
return;
}
// Be noted that WebGL on iOS safari supports less compressed texture formats than iOS.
// iOS 13 supports: pvrtc.
// iOS 14 supports: pvrtc, astc, etc(RGB).
// Desktop supports: dxt
// So that we set priority order as astc, pvrtc, dxt, etc, uncompressed.
if(this.astcSupported) {
this.format = BASIS_FORMAT.cTFASTC_4x4;
} else if(this.pvrtcSupported) {
// COMPRESSED_RGBA_PVRTC_4BPPV1_IMG
// Check alpha channel when decoding texture, change to BASIS_FORMAT.cTFPVRTC1_4_RGB if it has no alpha channel.
this.format = BASIS_FORMAT.cTFPVRTC1_4_RGBA;
} else if(this.dxtSupported) {
// COMPRESSED_RGBA_S3TC_DXT5_EXT
// COMPRESSED_RGB_S3TC_DXT1_EXT fails on macOS Chrome, so that don't change the format by alpha channel check.
this.format = BASIS_FORMAT.cTFBC3;
} else if(this.etcSupported) {
// RGB_ETC1_Format
// ETC2 RGBA is supported according to doc of BasisU. Test failed on iOS 14. Haven't tested on Android yet.
// https://github.com/BinomialLLC/basis_universal
this.format = BASIS_FORMAT.cTFETC1;
} else {
// Uncompressed
this.format = BASIS_FORMAT.cTFRGBA32;
}
return this;
}
/**
* Decompress texture by initialized basisU decoder.
* Before decompression, override texture format on basis of conditions of this specific texture.
* Return success status and message if failed.
* Return success status and texture data when success.
* @param arrayBuffer
* @returns {{success: boolean, data: Uint8Array, internalFormat: number, width: *, height: *} |
* {success: boolean, message: string}}
*/
createTextureData(arrayBuffer, url) {
if(!window.BasisFile) {
return {success: false, message:'BasisFile is disposed or it was not initialized'};
}
let format = this.format;
let type = this.type;
const basisFile = new window.BasisFile(new Uint8Array(arrayBuffer));
const width = basisFile.getImageWidth(0, 0);
const height = basisFile.getImageHeight(0, 0);
const images = basisFile.getNumImages();
const hasAlpha = basisFile.getHasAlpha();
let levels = basisFile.getNumLevels(IMAGE_INDEX); // How many mipmap levels.
if (!width || !height || !images || !levels) {
const message = 'BasisTextureLoader: Invalid .basis file';
this.basisFileFail(0, basisFile, message);
return {success: false, message};
}
if (!basisFile.startTranscoding()) {
const message = 'BasisTextureLoader: Invalid .basis file';
this.basisFileFail(0, basisFile, message);
return {success: false, message};
}
// Override format.
// Safari iOS 13 only supports PVRTC. PVRTC requests that the width and height are equal.
// Fall back to uncompressed format on iOS 13, when width and height are different.
if((format === BASIS_FORMAT.cTFPVRTC1_4_RGBA || format === BASIS_FORMAT.cTFPVRTC1_4_RGB) &&
width !== height) {
format = BASIS_FORMAT.cTFRGBA32;
}
// Override format on basis of alpha.
if(hasAlpha === 0) {
if(format === BASIS_FORMAT.cTFPVRTC1_4_RGBA) {
format = BASIS_FORMAT.cTFPVRTC1_4_RGB;
}
// TODO: So far there is no option for RGB32. If it is necessary we could choose `cTFRGB565`,
// In that case the `texture.type` have to be `UNSIGNED_SHORT_5_6_5` when calling `gl.texImage2D`.
// ref: https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/texImage2D
// ref: https://webglfundamentals.org/webgl/lessons/webgl-data-textures.html
if(format === BASIS_FORMAT.cTFRGBA32 && this.applyBpp16RGB) {
format = BASIS_FORMAT.cTFRGB565;
type = UNSIGNED_SHORT_5_6_5;
}
}
const isUnCompressedFormat =
format === BASIS_FORMAT.cTFRGBA32 ||
format === BASIS_FORMAT.cTFRGB565 ||
format === BASIS_FORMAT.cTFBGR565 ||
format === BASIS_FORMAT.cTFRGBA4444;
/**
* Gather information about each mip level to be transcoded.
* @type {MipLevelInfo[]}
*/
let mipLevelInfoArray = [];
let totalTranscodeSize = 0;
// For uncompressed texture format, generate mipmap on fly.
if(isUnCompressedFormat) {
levels = 1;
}
for (let mipLevel = 0; mipLevel < levels; ++mipLevel) {
let transcodeSize = basisFile.getImageTranscodedSizeInBytes(IMAGE_INDEX, mipLevel, format);
mipLevelInfoArray.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);
// Transcode each mip level into the appropriate section of the overall buffer.
for (let mipLevel of mipLevelInfoArray) {
let levelData = new Uint8Array(transcodeData.buffer, mipLevel.offset, mipLevel.size);
if (!basisFile.transcodeImage(levelData, IMAGE_INDEX, mipLevel.level, format, 1, hasAlpha)) {
const message = 'BasisTextureLoader: transcodeImage failed';
this.basisFileFail(0, basisFile, message);
return {success: false, message};
}
}
this.basisFileCleanUp(basisFile);
/*jshint camelcase: false */
let internalFormat = INTERNAL_FORMAT_MAP[format];
if(!internalFormat) {
throw new Error('BasisTextureLoader: No supported format available.');
}
this.workCount--;
if(this.workCount === 0 && !this.doCacheModule) {
this.releaseBasisModule();
}
// For those type the typed array view has to be `Uint16Array`
if(type === UNSIGNED_SHORT_5_6_5 || type === UNSIGNED_SHORT_4_4_4_4 || type === UNSIGNED_SHORT_5_5_5_1) {
transcodeData = new Uint16Array(transcodeData.buffer);
}
return {
success: true, data: transcodeData,
mipLevelInfoArray, width, height, internalFormat, type
};
}
basisFileCleanUp(basisFile) {
basisFile.close();
basisFile.delete();
}
// Throw error when a texture has failed to load for any reason.
basisFileFail(id, basisFile, errorMsg) {
throw new Error(`BasisTextureLoader: No suitable compressed texture format found. id: ${id}, error: ${errorMsg}`);
this.basisFileCleanUp(basisFile);
}
dispose() {
this.releaseBasisModule();
}
};
const BASIS_FORMAT = {
cTFETC1: 0,
cTFETC2: 1,
cTFBC1: 2,
cTFBC3: 3,
cTFBC4: 4,
cTFBC5: 5,
cTFBC7_M6_OPAQUE_ONLY: 6,
cTFBC7_M5: 7,
cTFPVRTC1_4_RGB: 8,
cTFPVRTC1_4_RGBA: 9,
cTFASTC_4x4: 10,
cTFATC_RGB: 11,
cTFATC_RGBA_INTERPOLATED_ALPHA: 12,
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, R: 5 bit, G: 6 bits, B: 5 bits.
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,
};
/**
* Note: Find constant of extension:
* `gl.getExtension('WEBGL_compressed_texture_s3tc').COMPRESSED_RGB_S3TC_DXT1_EXT`
* `gl.getExtension('WEBGL_compressed_texture_astc').COMPRESSED_RGBA_ASTC_4x4_KHR`
* Normally we can find all constants of one extension in MDN page. For example:
* https://developer.mozilla.org/en-US/docs/Web/API/WEBGL_compressed_texture_astc
*/
// DXT formats, from:
// http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
/*jshint unused:false*/
const COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0; // 33776.
const COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1;
const COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2;
const COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3; // 33779
const DXT_FORMAT_MAP = {};
DXT_FORMAT_MAP[BASIS_FORMAT.cTFBC1] = COMPRESSED_RGB_S3TC_DXT1_EXT;
DXT_FORMAT_MAP[BASIS_FORMAT.cTFBC3] = COMPRESSED_RGBA_S3TC_DXT5_EXT;
// TODO: Look for following constant variable in ClayGL
// Ref: threejs src/constants.js
const RGB_ETC1_Format = 36196;
// Ref: https://github.com/mrdoob/three.js/pull/18581/files
const RGB_ETC2_Format = 37492;
const RGBA_ETC2_EAC_Format = 37496; // ETC2 RGBA is supported according to doc of BasisU. Not sure if this one. Test failed on iOS 14. Haven't tested on Android yet.
// https://github.com/BinomialLLC/basis_universal
const ETC_FORMAT_MAP = {};
ETC_FORMAT_MAP[BASIS_FORMAT.cTFETC1] = RGB_ETC1_Format;
// PVRTC
// Ref: claygl src/Textures.js
const COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00;
const COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02;
const PVRTC_FORMAT_MAP = {};
PVRTC_FORMAT_MAP[BASIS_FORMAT.cTFPVRTC1_4_RGB] = COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
PVRTC_FORMAT_MAP[BASIS_FORMAT.cTFPVRTC1_4_RGBA] = COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
// ASTC
// Ref: threejs src/constants.js
const RGBA_ASTC_4x4_Format = 37808; // iOS 13 supports this format, iOS 14 doesn't.
const SRGB8_ALPHA8_ASTC_4x4_KHR = 37840; // Works on iOS 13.
const RGBA_ASTC_5x4_Format = 37809; // Don't support now
const RGBA_ASTC_5x5_Format = 37810; // Don't support now
const ASTC_FORMAT_MAP = {};
ASTC_FORMAT_MAP[BASIS_FORMAT.cTFASTC_4x4] = RGBA_ASTC_4x4_Format;
// Uncompressed
// ref: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants, Pixel formats.
const RGB = 0x1907; // 6407
const RGBA = 0x1908; // 6408
// Type
const UNSIGNED_SHORT_4_4_4_4 = 0x8033; // 32819
const UNSIGNED_SHORT_5_5_5_1 = 0x8034; // 32820
const UNSIGNED_SHORT_5_6_5 = 0x8363; // 33635
const UNSIGNED_BYTE = 0x1401; // 5121
const INTERNAL_FORMAT_MAP = {};
INTERNAL_FORMAT_MAP[BASIS_FORMAT.cTFETC1] = RGB_ETC1_Format;
INTERNAL_FORMAT_MAP[BASIS_FORMAT.cTFPVRTC1_4_RGB] = COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
INTERNAL_FORMAT_MAP[BASIS_FORMAT.cTFPVRTC1_4_RGBA] = COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
INTERNAL_FORMAT_MAP[BASIS_FORMAT.cTFBC1] = COMPRESSED_RGB_S3TC_DXT1_EXT;
INTERNAL_FORMAT_MAP[BASIS_FORMAT.cTFBC3] = COMPRESSED_RGBA_S3TC_DXT5_EXT;
INTERNAL_FORMAT_MAP[BASIS_FORMAT.cTFASTC_4x4] = RGBA_ASTC_4x4_Format;
INTERNAL_FORMAT_MAP[BASIS_FORMAT.cTFRGBA32] = RGBA;
INTERNAL_FORMAT_MAP[BASIS_FORMAT.cTFRGB565] = RGB;
INTERNAL_FORMAT_MAP[BASIS_FORMAT.cTFRGB565] = RGB;
BasisTextureLoader.BASIS_FORMAT = BASIS_FORMAT;
BasisTextureLoader.DXT_FORMAT_MAP = DXT_FORMAT_MAP;
export default BasisTextureLoader;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment