pixi-basis-ktx2
Version:
Loader for the *.basis & *.ktx2 supercompressed texture file format. This package also ships with the transcoder!
334 lines • 15.6 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
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 {
/**
* Runs transcoding and populates {@link imageArray}. It will run the transcoding in a web worker
* if they are available.
* @private
*/
static transcode(arrayBuffer) {
return __awaiter(this, void 0, void 0, function* () {
let resources;
if (typeof Worker !== 'undefined' && BasisParser.TranscoderWorker.wasmSource) {
resources = yield BasisParser.transcodeAsync(arrayBuffer);
}
else {
resources = BasisParser.transcodeSync(arrayBuffer);
}
return resources;
});
}
/**
* Finds a suitable worker for transcoding and sends a transcoding request
* @private
* @async
*/
static transcodeAsync(arrayBuffer) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
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
yield worker.initAsync();
const response = yield worker.transcodeAsync(new Uint8Array(arrayBuffer), BasisParser.defaultRGBAFormat.basisFormat, BasisParser.defaultRGBFormat.basisFormat);
const basisFormat = (_a = response.basisFormat) !== null && _a !== void 0 ? _a : 13;
const imageArray = (_b = response.imageArray) !== null && _b !== void 0 ? _b : [];
// 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) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
// 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: (_a = gl.getExtension('EXT_texture_compression_bptc')) !== null && _a !== void 0 ? _a : undefined,
astc: (_b = gl.getExtension('WEBGL_compressed_texture_astc')) !== null && _b !== void 0 ? _b : undefined,
etc: (_c = gl.getExtension('WEBGL_compressed_texture_etc')) !== null && _c !== void 0 ? _c : undefined,
s3tc: (_d = gl.getExtension('WEBGL_compressed_texture_s3tc')) !== null && _d !== void 0 ? _d : undefined,
s3tc_sRGB: (_e = gl.getExtension('WEBGL_compressed_texture_s3tc_srgb')) !== null && _e !== void 0 ? _e : undefined /* eslint-disable-line camelcase */,
pvrtc: (_f = (gl.getExtension('WEBGL_compressed_texture_pvrtc') || gl.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc'))) !== null && _f !== void 0 ? _f : undefined,
etc1: (_g = gl.getExtension('WEBGL_compressed_texture_etc1')) !== null && _g !== void 0 ? _g : undefined,
atc: (_h = gl.getExtension('WEBGL_compressed_texture_atc')) !== null && _h !== void 0 ? _h : 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 = (_j = supportedFormats[id]) !== null && _j !== void 0 ? _j : 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 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;
BasisParser.fallbackMode = false;
BasisParser.workerPool = [];
BasisParser.TranscoderWorker = TranscoderWorkerBasis_1.TranscoderWorkerBasis;
//# sourceMappingURL=BasisParser.js.map