UNPKG

pixi-basis-ktx2

Version:

Loader for the *.basis & *.ktx2 supercompressed texture file format. This package also ships with the transcoder!

343 lines 16 kB
"use strict"; 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.KTX2Parser = 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 TranscoderWorkerKTX2_1 = require("../TranscoderWorkerKTX2"); /** * Loader plugin for handling KTX2 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 &lt;script&gt; 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_transcoder.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 *.ktx2 * files. * * 2. Loading the transcoder source from a URL. * * ```js * // Use this if you to use the default CDN url for pixi-basis-ktx2 * KTX2Parser.loadTranscoder(); * * // Use this if you want to serve the transcoder on your own * KTX2Parser.loadTranscoder('./basis_transcoder.js', './basis_transcoder.wasm'); * ``` * * NOTE: This can only be used with web-workers. * @class * @memberof PIXI * @implements {PIXI.ILoaderPlugin} */ class KTX2Parser { /** * 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' && KTX2Parser.TranscoderWorker.wasmSource) { resources = yield KTX2Parser.transcodeAsync(arrayBuffer); } else { resources = KTX2Parser.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 (!KTX2Parser.defaultRGBAFormat && !KTX2Parser.defaultRGBFormat) { KTX2Parser.autoDetectFormats(); } const workerPool = KTX2Parser.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 TranscoderWorkerKTX2_1.TranscoderWorkerKTX2(); workerPool.push(worker); } // Wait until worker is ready yield worker.initAsync(); const response = yield worker.transcodeAsync(new Uint8Array(arrayBuffer), KTX2Parser.defaultRGBAFormat.basisFormat, KTX2Parser.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[response.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 (!KTX2Parser.defaultRGBAFormat && !KTX2Parser.defaultRGBFormat) { KTX2Parser.autoDetectFormats(); } const BASIS = KTX2Parser.ktx2Binding; let fallbackMode = KTX2Parser.fallbackMode; const data = new Uint8Array(arrayBuffer); const ktx2File = new BASIS.KTX2File(data); const dfdSize = ktx2File.getDFDSize(); const dvdData = new Uint8Array(dfdSize); ktx2File.getDFD(dvdData); // Don't transcode all mipmap levels in fallback mode! const levels = !fallbackMode ? ktx2File.getLevels() : 1; const layers = ktx2File.getLayers(); const faces = ktx2File.getFaces(); const hasAlpha = ktx2File.getHasAlpha(); const imageLevels = new Array(levels); const basisFormat = hasAlpha ? KTX2Parser.defaultRGBAFormat.basisFormat : KTX2Parser.defaultRGBFormat.basisFormat; const basisFallbackFormat = Basis_1.BASIS_FORMATS.cTFRGB565; const imageResources = new Array(levels); if (!ktx2File.startTranscoding()) { // #if _DEBUG console.error(`Basis failed to start transcoding!`); // #endif ktx2File.close(); ktx2File.delete(); return null; } // Transcode mipmap levels into "imageLevels" for (let i = 0; i < levels; i++) { const firstLevel = ktx2File.getImageLevelInfo(i, 0, 0); const width = firstLevel.origWidth; const height = firstLevel.origHeight; const alignedWidth = (width + 3) & ~3; const alignedHeight = (height + 3) & ~3; for (let j = 0; j < Math.max(1, layers); j++) { for (let k = 0; k < faces; k++) { const imageLevelInfo = ktx2File.getImageLevelInfo(i, j, k); const levelWidth = imageLevelInfo.width; const levelHeight = imageLevelInfo.height; const byteSize = ktx2File.getImageTranscodedSizeInBytes(i, j, k, !fallbackMode ? basisFormat : basisFallbackFormat); imageLevels[j] = { levelID: j, levelBuffer: new Uint8Array(byteSize), levelWidth, levelHeight, }; if (!ktx2File.transcodeImage( // eslint-disable-next-line max-len imageLevels[j].levelBuffer, i, j, k, !fallbackMode ? basisFormat : basisFallbackFormat, false, -1, -1)) { 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; } ktx2File.close(); ktx2File.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; // Auto-detect WebGL compressed-texture 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 = undefined; let basisFormat = Basis_1.BASIS_FORMATS.cTFRGB565; for (const id in supportedFormats) { internalFormat = supportedFormats[id]; 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 !== undefined) { KTX2Parser[detectWithAlpha ? 'defaultRGBAFormat' : 'defaultRGBFormat'] = { textureFormat: internalFormat, basisFormat, }; } else { KTX2Parser[detectWithAlpha ? 'defaultRGBAFormat' : 'defaultRGBFormat'] = { textureFormat: core_1.TYPES.UNSIGNED_SHORT_5_6_5, basisFormat: Basis_1.BASIS_FORMATS.cTFRGB565, }; KTX2Parser.fallbackMode = true; } } } /** * Binds the basis_universal transcoder to decompress *.ktx2 files. You must initialize the transcoder library yourself. * @example * import { KTX2Parser } 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 KTX2Parser to the transcoder * KTX2Parser.bindTranscoder(basisLibrary); * }); * @param basisLibrary - the initialized transcoder library * @private */ static bindTranscoder(basisLibrary) { KTX2Parser.ktx2Binding = basisLibrary; } /** * Loads the transcoder source code for use in {@link PIXI.KTX2Parser.TranscoderWorker}. * @private * @param jsURL - URL to the javascript basis transcoder * @param wasmURL - URL to the wasm basis transcoder */ static loadTranscoder(jsURL, wasmURL) { return KTX2Parser.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) { KTX2Parser.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 TranscoderWorkerKTX2_1.TranscoderWorkerKTX2(); void this.workerPool[i].initAsync(); } } } exports.KTX2Parser = KTX2Parser; KTX2Parser.fallbackMode = false; KTX2Parser.workerPool = []; KTX2Parser.TranscoderWorker = TranscoderWorkerKTX2_1.TranscoderWorkerKTX2; //# sourceMappingURL=KTX2Parser.js.map