Last active
December 15, 2023 01:16
-
-
Save birdinforest/5b802cdc36a71f77c1fcae501bad0c06 to your computer and use it in GitHub Desktop.
Decompress .basis compressed texture. #webgl #texture_compression #basisu #javascript #decompression
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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