pixi-basis-ktx2
Version:
Loader for the *.basis & *.ktx2 supercompressed texture file format. This package also ships with the transcoder!
322 lines • 14.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BasisParser = void 0;
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
const compressed_textures_1 = require("@pixi/compressed-textures");
const core_1 = require("@pixi/core");
const Basis_1 = require("../Basis");
const TranscoderWorkerBasis_1 = require("../TranscoderWorkerBasis");
/**
* Loader plugin for handling BASIS supercompressed texture files.
*
* To use this loader, you must bind the basis_universal WebAssembly transcoder. There are two ways of
* doing this:
*
* 1. Adding a <script> tag to your HTML page to the transcoder bundle in this package, and serving
* the WASM binary from the same location.
*
* ```html
* <!-- Copy ./node_modules/pixi-basis-ktx2/assets/basis_.wasm into your assets directory
* as well, so it is served from the same folder as the JavaScript! -->
* <script src="./node_modules/pixi-basis-ktx2/assets/basis_transcoder.js"></script>
* ```
*
* NOTE: `basis_transcoder.js` expects the WebAssembly binary to be named `basis_transcoder.wasm`.
* NOTE-2: This method supports transcoding on the main-thread. Only use this if you have 1 or 2 *.basis
* files.
*
* 2. Loading the transcoder source from a URL.
*
* ```js
* // Use this if you to use the default CDN url for pixi-basis-ktx2
* BasisParser.loadTranscoder();
*
* // Use this if you want to serve the transcoder on your own
* BasisParser.loadTranscoder('./basis_transcoder.js', './basis_transcoder.wasm');
* ```
*
* NOTE: This can only be used with web-workers.
* @class
* @memberof PIXI
* @implements {PIXI.ILoaderPlugin}
*/
class BasisParser {
static basisBinding;
static defaultRGBFormat;
static defaultRGBAFormat;
static fallbackMode = false;
static workerPool = [];
/**
* Runs transcoding and populates {@link imageArray}. It will run the transcoding in a web worker
* if they are available.
* @private
*/
static async transcode(arrayBuffer) {
let resources;
if (typeof Worker !== 'undefined' && BasisParser.TranscoderWorker.wasmSource) {
resources = await BasisParser.transcodeAsync(arrayBuffer);
}
else {
resources = BasisParser.transcodeSync(arrayBuffer);
}
return resources;
}
/**
* Finds a suitable worker for transcoding and sends a transcoding request
* @private
* @async
*/
static async transcodeAsync(arrayBuffer) {
if (!BasisParser.defaultRGBAFormat && !BasisParser.defaultRGBFormat) {
BasisParser.autoDetectFormats();
}
const workerPool = BasisParser.workerPool;
let leastLoad = 0x10000000;
let worker = null;
for (let i = 0, j = workerPool.length; i < j; i++) {
if (workerPool[i].load < leastLoad) {
worker = workerPool[i];
leastLoad = worker.load;
}
}
if (!worker) {
/* eslint-disable-next-line no-use-before-define */
worker = new TranscoderWorkerBasis_1.TranscoderWorkerBasis();
workerPool.push(worker);
}
// Wait until worker is ready
await worker.initAsync();
const response = await worker.transcodeAsync(new Uint8Array(arrayBuffer), BasisParser.defaultRGBAFormat.basisFormat, BasisParser.defaultRGBFormat.basisFormat);
const basisFormat = response.basisFormat ?? 13;
const imageArray = response.imageArray ?? [];
// whether it is an uncompressed format
const fallbackMode = Number(basisFormat) > 12;
let imageResources;
if (!fallbackMode) {
const format = Basis_1.BASIS_FORMAT_TO_INTERNAL_FORMAT[basisFormat];
// HINT: this.imageArray is CompressedTextureResource[]
imageResources = new Array(imageArray.length);
for (let i = 0, j = imageArray.length; i < j; i++) {
imageResources[i] = new compressed_textures_1.CompressedTextureResource(null, {
format,
width: imageArray[i].width,
height: imageArray[i].height,
levelBuffers: imageArray[i].levelArray,
levels: imageArray[i].levelArray.length,
});
}
}
else {
// TODO: BufferResource does not support manual mipmapping.
imageResources = imageArray.map((image) => new core_1.BufferResource(new Uint16Array(image.levelArray[0].levelBuffer.buffer), {
width: image.width,
height: image.height,
}));
}
imageResources.basisFormat = basisFormat;
return imageResources;
}
/**
* Runs transcoding on the main thread.
* @private
*/
static transcodeSync(arrayBuffer) {
if (!BasisParser.defaultRGBAFormat && !BasisParser.defaultRGBFormat) {
BasisParser.autoDetectFormats();
}
const BASIS = BasisParser.basisBinding;
const data = new Uint8Array(arrayBuffer);
const basisFile = new BASIS.BasisFile(data);
const imageCount = basisFile.getNumImages();
const hasAlpha = basisFile.getHasAlpha();
const basisFormat = hasAlpha ? BasisParser.defaultRGBAFormat.basisFormat : BasisParser.defaultRGBFormat.basisFormat;
const basisFallbackFormat = Basis_1.BASIS_FORMATS.cTFRGB565;
const imageResources = new Array(imageCount);
let fallbackMode = BasisParser.fallbackMode;
if (!basisFile.startTranscoding()) {
// #if _DEBUG
console.error(`Basis failed to start transcoding!`);
// #endif
basisFile.close();
basisFile.delete();
return null;
}
for (let i = 0; i < imageCount; i++) {
// Don't transcode all mipmap levels in fallback mode!
const levels = !fallbackMode ? basisFile.getNumLevels(i) : 1;
const width = basisFile.getImageWidth(i, 0);
const height = basisFile.getImageHeight(i, 0);
const alignedWidth = (width + 3) & ~3;
const alignedHeight = (height + 3) & ~3;
const imageLevels = new Array(levels);
// Transcode mipmap levels into "imageLevels"
for (let j = 0; j < levels; j++) {
const levelWidth = basisFile.getImageWidth(i, j);
const levelHeight = basisFile.getImageHeight(i, j);
const byteSize = basisFile.getImageTranscodedSizeInBytes(i, 0, !fallbackMode ? basisFormat : basisFallbackFormat);
imageLevels[j] = {
levelID: j,
levelBuffer: new Uint8Array(byteSize),
levelWidth,
levelHeight,
};
if (!basisFile.transcodeImage(imageLevels[j].levelBuffer, i, 0, !fallbackMode ? basisFormat : basisFallbackFormat, false, false)) {
if (fallbackMode) {
// #if _DEBUG
console.error(`Basis failed to transcode image ${i}, level ${0}!`);
// #endif
break;
}
else {
// Try transcoding to an uncompressed format before giving up!
// NOTE: We must start all over again as all Resources must be in compressed OR uncompressed.
i = -1;
fallbackMode = true;
// #if _DEBUG
/* eslint-disable-next-line max-len */
console.warn(`Basis failed to transcode image ${i}, level ${0} to a compressed texture format. Retrying to an uncompressed fallback format!`);
// #endif
continue;
}
}
}
let imageResource;
if (!fallbackMode) {
imageResource = new compressed_textures_1.CompressedTextureResource(null, {
format: Basis_1.BASIS_FORMAT_TO_INTERNAL_FORMAT[basisFormat],
width: alignedWidth,
height: alignedHeight,
levelBuffers: imageLevels,
levels,
});
}
else {
// TODO: BufferResource doesn't support manual mipmap levels
imageResource = new core_1.BufferResource(new Uint16Array(imageLevels[0].levelBuffer.buffer), { width, height });
}
imageResources[i] = imageResource;
}
basisFile.close();
basisFile.delete();
const transcodedResources = imageResources;
transcodedResources.basisFormat = !fallbackMode ? basisFormat : basisFallbackFormat;
return transcodedResources;
}
/**
* Detects the available compressed texture formats on the device.
* @param extensions - extensions provided by a WebGL context
* @ignore
*/
static autoDetectFormats(extensions) {
// Auto-detect WebGL compressed-texture extensions
console.log('autoDetectFormats', extensions);
if (!extensions) {
const canvas = core_1.settings.ADAPTER.createCanvas();
const gl = canvas.getContext('webgl');
if (!gl) {
console.error('WebGL not available for BASIS transcoding. Silently failing.');
return;
}
extensions = {
bptc: gl.getExtension('EXT_texture_compression_bptc') ?? undefined,
astc: gl.getExtension('WEBGL_compressed_texture_astc') ?? undefined,
etc: gl.getExtension('WEBGL_compressed_texture_etc') ?? undefined,
s3tc: gl.getExtension('WEBGL_compressed_texture_s3tc') ?? undefined,
s3tc_sRGB: gl.getExtension('WEBGL_compressed_texture_s3tc_srgb') ?? undefined /* eslint-disable-line camelcase */,
pvrtc: (gl.getExtension('WEBGL_compressed_texture_pvrtc') || gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc')) ?? undefined,
etc1: gl.getExtension('WEBGL_compressed_texture_etc1') ?? undefined,
atc: gl.getExtension('WEBGL_compressed_texture_atc') ?? undefined,
};
}
// Discover the available texture formats
const supportedFormats = {};
for (const key in extensions) {
const extension = extensions[key];
if (!extension) {
continue;
}
Object.assign(supportedFormats, Object.getPrototypeOf(extension));
}
// Set the default alpha/non-alpha output formats for basisu transcoding
for (let i = 0; i < 2; i++) {
const detectWithAlpha = !!i;
let internalFormat = 0;
let basisFormat = Basis_1.BASIS_FORMATS.cTFRGB565;
for (const id in supportedFormats) {
internalFormat = supportedFormats[id] ?? 0;
basisFormat = Basis_1.INTERNAL_FORMAT_TO_BASIS_FORMAT[internalFormat];
if (basisFormat !== undefined) {
if ((detectWithAlpha && Basis_1.BASIS_FORMATS_ALPHA[basisFormat]) || (!detectWithAlpha && !Basis_1.BASIS_FORMATS_ALPHA[basisFormat])) {
break;
}
}
}
if (internalFormat) {
BasisParser[detectWithAlpha ? 'defaultRGBAFormat' : 'defaultRGBFormat'] = {
textureFormat: internalFormat,
basisFormat,
};
}
else {
BasisParser[detectWithAlpha ? 'defaultRGBAFormat' : 'defaultRGBFormat'] = {
textureFormat: core_1.TYPES.UNSIGNED_SHORT_5_6_5,
basisFormat: Basis_1.BASIS_FORMATS.cTFRGB565,
};
BasisParser.fallbackMode = true;
}
}
}
/**
* Binds the basis_universal transcoder to decompress *.basis files. You must initialize the transcoder library yourself.
* @example
* import { BasisParser } from 'pixi-basis-ktx2';
*
* // BASIS() returns a Promise-like object
* globalThis.BASIS().then((basisLibrary) =>
* {
* // Initialize basis-library; otherwise, transcoded results maybe corrupt!
* basisLibrary.initializeBasis();
*
* // Bind BasisParser to the transcoder
* BasisParser.bindTranscoder(basisLibrary);
* });
* @param basisLibrary - the initialized transcoder library
* @private
*/
static bindTranscoder(basisLibrary) {
BasisParser.basisBinding = basisLibrary;
}
/**
* Loads the transcoder source code for use in {@link PIXI.BasisParser.TranscoderWorker}.
* @private
* @param jsURL - URL to the javascript basis transcoder
* @param wasmURL - URL to the wasm basis transcoder
*/
static loadTranscoder(jsURL, wasmURL) {
return BasisParser.TranscoderWorker.loadTranscoder(jsURL, wasmURL);
}
/**
* Set the transcoder source code directly
* @private
* @param jsSource - source for the javascript basis transcoder
* @param wasmSource - source for the wasm basis transcoder
*/
static setTranscoder(jsSource, wasmSource) {
BasisParser.TranscoderWorker.setTranscoder(jsSource, wasmSource);
}
static TranscoderWorker = TranscoderWorkerBasis_1.TranscoderWorkerBasis;
static get TRANSCODER_WORKER_POOL_LIMIT() {
return this.workerPool.length || 1;
}
static set TRANSCODER_WORKER_POOL_LIMIT(limit) {
// TODO: Destroy workers?
for (let i = this.workerPool.length; i < limit; i++) {
this.workerPool[i] = new TranscoderWorkerBasis_1.TranscoderWorkerBasis();
void this.workerPool[i].initAsync();
}
}
}
exports.BasisParser = BasisParser;
//# sourceMappingURL=BasisParser.js.map