Reference Source

src/viewer/scene/utils/textureTranscoders/KTX2TextureTranscoder/KTX2TextureTranscoder.js

import {FileLoader} from "../../FileLoader.js";
import {WorkerPool} from "../../WorkerPool.js";
import {
    LinearEncoding,
    LinearFilter,
    LinearMipmapLinearFilter,
    RGB_ETC1_Format,
    RGB_ETC2_Format,
    RGB_PVRTC_4BPPV1_Format,
    RGB_S3TC_DXT1_Format,
    RGBA_ASTC_4x4_Format,
    RGBA_BPTC_Format,
    RGBA_ETC2_EAC_Format,
    RGBA_PVRTC_4BPPV1_Format,
    RGBA_S3TC_DXT5_Format,
    RGBAFormat,
    sRGBEncoding
} from "../../../constants/constants.js";

const KTX2TransferSRGB = 2;
const KTX2_ALPHA_PREMULTIPLIED = 1;

let activeTranscoders = 0;

/**
 * Transcodes texture data from KTX2.
 *
 * ## Overview
 *
 * * Uses the [Basis Universal GPU Texture Codec](https://github.com/BinomialLLC/basis_universal) to
 * transcode [KTX2](https://github.khronos.org/KTX-Specification/) textures.
 * * {@link XKTLoaderPlugin} uses a KTX2TextureTranscoder to load textures in XKT files.
 * * {@link VBOSceneModel} uses a KTX2TextureTranscoder to enable us to add KTX2-encoded textures.
 * * Loads the Basis Codec from [CDN](https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/) by default, but can
 * also be configured to load the Codec from local files.
 * * We also bundle the Basis Codec with the xeokit-sdk npm package, and in the [repository](https://github.com/xeokit/xeokit-sdk/tree/master/dist/basis).
 *
 * ## What is KTX2?
 *
 * A [KTX2](https://github.khronos.org/KTX-Specification/) file stores GPU texture data in the Khronos Texture 2.0 (KTX2) container format. It contains image data for
 * a texture asset compressed with Basis Universal (BasisU) supercompression that can be transcoded to different formats
 * depending on the support provided by the target devices. KTX2 provides a lightweight format for distributing texture
 * assets to GPUs. Due to BasisU compression, KTX2 files can store any image format supported by GPUs.
 *
 * ## Loading XKT files containing KTX2 textures
 *
 * {@link XKTLoaderPlugin} uses a KTX2TextureTranscoder to load textures in XKT files. An XKTLoaderPlugin has its own
 * default KTX2TextureTranscoder, configured to load the Basis Codec from the [CDN](https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/). If we wish, we can override that with our own
 * KTX2TextureTranscoder, configured to load the Codec locally.
 *
 * In the example below, we'll create a {@link Viewer} and add an {@link XKTLoaderPlugin}
 * configured with a KTX2TextureTranscoder. Then we'll use the XKTLoaderPlugin to load an
 * XKT file that contains KTX2 textures, which the plugin will transcode using
 * its KTX2TextureTranscoder.
 *
 * We'll configure our KTX2TextureTranscoder to load the Basis Codec from a local directory. If we were happy with loading the
 * Codec from our [CDN](https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/) (ie. our app will always have an Internet connection) then we could just leave out the
 * KTX2TextureTranscoder altogether, and let the XKTLoaderPlugin use its internal default KTX2TextureTranscoder, which is configured to
 * load the Codec from the CDN. We'll stick with loading our own Codec, in case we want to run our app without an Internet connection.
 *
 * <a href="https://xeokit.github.io/xeokit-sdk/examples/#loading_XKT_Textures_HousePlan"><img src="https://xeokit.github.io/xeokit-sdk/assets/images/xktWithTextures.png"></a>
 *
 * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/#loading_XKT_Textures_HousePlan)]
 *
 * ````javascript
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.camera.eye = [-2.56, 8.38, 8.27];
 * viewer.camera.look = [13.44, 3.31, -14.83];
 * viewer.camera.up = [0.10, 0.98, -0.14];
 *
 * const textureTranscoder = new KTX2TextureTranscoder({
 *     viewer,
 *     transcoderPath: "./../dist/basis/" // <------ Path to Basis Universal transcoder
 * });
 *
 * const xktLoader = new XKTLoaderPlugin(viewer, {
 *     textureTranscoder // <<------------- Transcodes KTX2 textures in XKT files
 * });
 *
 * const sceneModel = xktLoader.load({
 *     id: "myModel",
 *     src: "./HousePlan.xkt" // <<------ XKT file with KTX2 textures
 * });
 * ````
 *
 * ## Loading KTX2 files into a VBOSceneModel
 *
 * A {@link SceneModel} that is configured with a KTX2TextureTranscoder will
 * allow us to load textures into it from KTX2-transcoded buffers or files.
 *
 * In the example below, we'll create a {@link Viewer}, containing a {@link VBOSceneModel} configured with a
 * KTX2TextureTranscoder.
 *
 * We'll then programmatically create a simple object within the VBOSceneModel, consisting of
 * a single box mesh with a texture loaded from a KTX2 file, which our VBOSceneModel internally transcodes, using
 * its KTX2TextureTranscoder.
 *
 * As in the previous example, we'll configure our KTX2TextureTranscoder to load the Basis Codec from a local directory.
 *
 * * [Run a similar example](http://localhost:8080/examples/sceneRepresentation_VBOSceneModel_batching_textures_ktx2.html)
 *
 * ````javascript
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
 * viewer.scene.camera.look = [0, -5.75, 0];
 * viewer.scene.camera.up = [0.37, 0.91, -0.11];
 *
 * const textureTranscoder = new KTX2TextureTranscoder({
 *     viewer,
 *     transcoderPath: "./../dist/basis/" // <------ Path to BasisU transcoder module
 * });
 *
 * const vboSceneModel = new VBOSceneModel(viewer.scene, {
 *      id: "myModel",
 *      textureTranscoder // <<-------------------- Configure model with our transcoder
 *  });
 *
 * vboSceneModel.createTexture({
 *      id: "myColorTexture",
 *      src: "../assets/textures/compressed/sample_uastc_zstd.ktx2" // <<----- KTX2 texture asset
 * });
 *
 * vboSceneModel.createTexture({
 *      id: "myMetallicRoughnessTexture",
 *      src: "../assets/textures/alpha/crosshatchAlphaMap.jpg" // <<----- JPEG texture asset
 * });
 *
 * vboSceneModel.createTextureSet({
 *      id: "myTextureSet",
 *      colorTextureId: "myColorTexture",
 *      metallicRoughnessTextureId: "myMetallicRoughnessTexture"
 *  });
 *
 * vboSceneModel.createMesh({
 *      id: "myMesh",
 *      textureSetId: "myTextureSet",
 *      primitive: "triangles",
 *      positions: [1, 1, 1, ...],
 *      normals: [0, 0, 1, 0, ...],
 *      uv: [1, 0, 0, ...],
 *      indices: [0, 1, 2, ...],
 *  });
 *
 * vboSceneModel.createEntity({
 *      id: "myEntity",
 *      meshIds: ["myMesh"]
 *  });
 *
 * vboSceneModel.finalize();
 * ````
 *
 * ## Loading KTX2 ArrayBuffers into a VBOSceneModel
 *
 * A {@link SceneModel} that is configured with a KTX2TextureTranscoder will also allow us to load textures into
 * it from KTX2 ArrayBuffers.
 *
 * In the example below, we'll create a {@link Viewer}, containing a {@link VBOSceneModel} configured with a
 * KTX2TextureTranscoder.
 *
 * We'll then programmatically create a simple object within the VBOSceneModel, consisting of
 * a single mesh with a texture loaded from a KTX2 ArrayBuffer, which our VBOSceneModel internally transcodes, using
 * its KTX2TextureTranscoder.
 *
 * ````javascript
 * const viewer = new Viewer({
 *     canvasId: "myCanvas",
 *     transparent: true
 * });
 *
 * viewer.scene.camera.eye = [-21.80, 4.01, 6.56];
 * viewer.scene.camera.look = [0, -5.75, 0];
 * viewer.scene.camera.up = [0.37, 0.91, -0.11];
 *
 * const textureTranscoder = new KTX2TextureTranscoder({
 *     viewer,
 *     transcoderPath: "./../dist/basis/" // <------ Path to BasisU transcoder module
 * });
 *
 * const vboSceneModel = new VBOSceneModel(viewer.scene, {
 *      id: "myModel",
 *      textureTranscoder // <<-------------------- Configure model with our transcoder
 * });
 *
 * utils.loadArraybuffer("../assets/textures/compressed/sample_uastc_zstd.ktx2",(arrayBuffer) => {
 *
 *     vboSceneModel.createTexture({
 *         id: "myColorTexture",
 *         buffers: [arrayBuffer] // <<----- KTX2 texture asset
 *     });
 *
 *     vboSceneModel.createTexture({
 *         id: "myMetallicRoughnessTexture",
 *         src: "../assets/textures/alpha/crosshatchAlphaMap.jpg" // <<----- JPEG texture asset
 *     });
 *
 *     vboSceneModel.createTextureSet({
 *        id: "myTextureSet",
 *        colorTextureId: "myColorTexture",
 *        metallicRoughnessTextureId: "myMetallicRoughnessTexture"
 *     });
 *
 *     vboSceneModel.createMesh({
 *          id: "myMesh",
 *          textureSetId: "myTextureSet",
 *          primitive: "triangles",
 *          positions: [1, 1, 1, ...],
 *          normals: [0, 0, 1, 0, ...],
 *          uv: [1, 0, 0, ...],
 *          indices: [0, 1, 2, ...],
 *     });
 *
 *     vboSceneModel.createEntity({
 *         id: "myEntity",
 *         meshIds: ["myMesh"]
 *     });
 *
 *     vboSceneModel.finalize();
 * });
 * ````
 *
 * @implements {TextureTranscoder}
 */
class KTX2TextureTranscoder {

    /**
     * Creates a new KTX2TextureTranscoder.
     *
     * @param {Viewer} viewer The Viewer that our KTX2TextureTranscoder will be used with. This KTX2TextureTranscoder
     * must only be used to transcode textures for this Viewer. This is because the Viewer's capabilities will decide
     * what target GPU formats this KTX2TextureTranscoder will transcode to.
     * @param {String} [transcoderPath="https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/"] Path to the Basis
     * transcoder module that internally does the heavy lifting for our KTX2TextureTranscoder. If we omit this configuration,
     * then our KTX2TextureTranscoder will load it from ````https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/```` by
     * default. Therefore, make sure your application is connected to the internet if you wish to use the default transcoder path.
     * @param {Number} [workerLimit] The maximum number of Workers to use for transcoding.
     */
    constructor({viewer, transcoderPath, workerLimit}) {

        this._transcoderPath = transcoderPath || "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/";
        this._transcoderBinary = null;
        this._transcoderPending = null;
        this._workerPool = new WorkerPool();
        this._workerSourceURL = '';

        if (workerLimit) {
            this._workerPool.setWorkerLimit(workerLimit);
        }

        const viewerCapabilities = viewer.capabilities;

        this._workerConfig = {
            astcSupported: viewerCapabilities.astcSupported,
            etc1Supported: viewerCapabilities.etc1Supported,
            etc2Supported: viewerCapabilities.etc2Supported,
            dxtSupported: viewerCapabilities.dxtSupported,
            bptcSupported: viewerCapabilities.bptcSupported,
            pvrtcSupported: viewerCapabilities.pvrtcSupported
        };

        this._supportedFileTypes = ["xkt2"];
    }

    _init() {
        if (!this._transcoderPending) {
            const jsLoader = new FileLoader();
            jsLoader.setPath(this._transcoderPath);
            jsLoader.setWithCredentials(this.withCredentials);
            const jsContent = jsLoader.loadAsync('basis_transcoder.js');
            const binaryLoader = new FileLoader();
            binaryLoader.setPath(this._transcoderPath);
            binaryLoader.setResponseType('arraybuffer');
            binaryLoader.setWithCredentials(this.withCredentials);
            const binaryContent = binaryLoader.loadAsync('basis_transcoder.wasm');
            this._transcoderPending = Promise.all([jsContent, binaryContent])
                .then(([jsContent, binaryContent]) => {
                    const fn = KTX2TextureTranscoder.BasisWorker.toString();
                    const body = [
                        '/* constants */',
                        'let _EngineFormat = ' + JSON.stringify(KTX2TextureTranscoder.EngineFormat),
                        'let _TranscoderFormat = ' + JSON.stringify(KTX2TextureTranscoder.TranscoderFormat),
                        'let _BasisFormat = ' + JSON.stringify(KTX2TextureTranscoder.BasisFormat),
                        '/* basis_transcoder.js */',
                        jsContent,
                        '/* worker */',
                        fn.substring(fn.indexOf('{') + 1, fn.lastIndexOf('}'))
                    ].join('\n');
                    this._workerSourceURL = URL.createObjectURL(new Blob([body]));
                    this._transcoderBinary = binaryContent;
                    this._workerPool.setWorkerCreator(() => {
                        const worker = new Worker(this._workerSourceURL);
                        const transcoderBinary = this._transcoderBinary.slice(0);
                        worker.postMessage({
                            type: 'init',
                            config: this._workerConfig,
                            transcoderBinary
                        }, [transcoderBinary]);
                        return worker;
                    });
                });
            if (activeTranscoders > 0) {
                console.warn('KTX2TextureTranscoder: Multiple active KTX2TextureTranscoder may cause performance issues.' + ' Use a single KTX2TextureTranscoder instance, or call .dispose() on old instances.');
            }
            activeTranscoders++;
        }
        return this._transcoderPending;
    }

    /**
     * Transcodes texture data from transcoded buffers into a {@link Texture2D}.
     *
     * @param {ArrayBuffer[]} buffers Transcoded texture data. Given as an array of buffers so that we can support multi-image textures, such as cube maps.
     * @param {*} config Transcoding options.
     * @param {Texture2D} texture The texture to load.
     * @returns {Promise} Resolves when the texture has loaded.
     */
    transcode(buffers, texture, config = {}) {
        return new Promise((resolve, reject) => {
            const taskConfig = config;
            this._init().then(() => {
                return this._workerPool.postMessage({
                    type: 'transcode',
                    buffers,
                    taskConfig: taskConfig
                }, buffers);
            }).then((e) => {
                const transcodeResult = e.data;
                const {mipmaps, width, height, format, type, error, dfdTransferFn, dfdFlags} = transcodeResult;
                if (type === 'error') {
                    return reject(error);
                }
                texture.setCompressedData({
                    mipmaps,
                    props: {
                        format: format,
                        minFilter: mipmaps.length === 1 ? LinearFilter : LinearMipmapLinearFilter,
                        magFilter: mipmaps.length === 1 ? LinearFilter : LinearMipmapLinearFilter,
                        encoding: dfdTransferFn === KTX2TransferSRGB ? sRGBEncoding : LinearEncoding,
                        premultiplyAlpha: !!(dfdFlags & KTX2_ALPHA_PREMULTIPLIED)
                    }
                });
                resolve()
            });
        });
    }

    /**
     * Destroys this KTX2TextureTranscoder
     */
    destroy() {
        URL.revokeObjectURL(this._workerSourceURL);
        this._workerPool.destroy();
        activeTranscoders--;
    }
}

/**
 * @private
 */
KTX2TextureTranscoder.BasisFormat = {
    ETC1S: 0,
    UASTC_4x4: 1
};

/**
 * @private
 */
KTX2TextureTranscoder.TranscoderFormat = {
    ETC1: 0,
    ETC2: 1,
    BC1: 2,
    BC3: 3,
    BC4: 4,
    BC5: 5,
    BC7_M6_OPAQUE_ONLY: 6,
    BC7_M5: 7,
    PVRTC1_4_RGB: 8,
    PVRTC1_4_RGBA: 9,
    ASTC_4x4: 10,
    ATC_RGB: 11,
    ATC_RGBA_INTERPOLATED_ALPHA: 12,
    RGBA32: 13,
    RGB565: 14,
    BGR565: 15,
    RGBA4444: 16
};

/**
 * @private
 */
KTX2TextureTranscoder.EngineFormat = {
    RGBAFormat: RGBAFormat,
    RGBA_ASTC_4x4_Format: RGBA_ASTC_4x4_Format,
    RGBA_BPTC_Format: RGBA_BPTC_Format,
    RGBA_ETC2_EAC_Format: RGBA_ETC2_EAC_Format,
    RGBA_PVRTC_4BPPV1_Format: RGBA_PVRTC_4BPPV1_Format,
    RGBA_S3TC_DXT5_Format: RGBA_S3TC_DXT5_Format,
    RGB_ETC1_Format: RGB_ETC1_Format,
    RGB_ETC2_Format: RGB_ETC2_Format,
    RGB_PVRTC_4BPPV1_Format: RGB_PVRTC_4BPPV1_Format,
    RGB_S3TC_DXT1_Format: RGB_S3TC_DXT1_Format
};

/* WEB WORKER */

/**
 * @private
 * @constructor
 */
KTX2TextureTranscoder.BasisWorker = function () {

    let config;
    let transcoderPending;
    let BasisModule;

    const EngineFormat = _EngineFormat; // eslint-disable-line no-undef
    const TranscoderFormat = _TranscoderFormat; // eslint-disable-line no-undef
    const BasisFormat = _BasisFormat; // eslint-disable-line no-undef

    self.addEventListener('message', function (e) {
        const message = e.data;
        switch (message.type) {
            case 'init':
                config = message.config;
                init(message.transcoderBinary);
                break;
            case 'transcode':
                transcoderPending.then(() => {
                    try {
                        const {
                            width,
                            height,
                            hasAlpha,
                            mipmaps,
                            format,
                            dfdTransferFn,
                            dfdFlags
                        } = transcode(message.buffers[0]);
                        const buffers = [];
                        for (let i = 0; i < mipmaps.length; ++i) {
                            buffers.push(mipmaps[i].data.buffer);
                        }
                        self.postMessage({
                            type: 'transcode',
                            id: message.id,
                            width,
                            height,
                            hasAlpha,
                            mipmaps,
                            format,
                            dfdTransferFn,
                            dfdFlags
                        }, buffers);
                    } catch (error) {
                        console.error(`[KTX2TextureTranscoder.BasisWorker]: ${error}`);
                        self.postMessage({type: 'error', id: message.id, error: error.message});
                    }
                });
                break;
        }
    });

    function init(wasmBinary) {
        transcoderPending = new Promise(resolve => {
            BasisModule = {
                wasmBinary,
                onRuntimeInitialized: resolve
            };
            BASIS(BasisModule); // eslint-disable-line no-undef
        }).then(() => {
            BasisModule.initializeBasis();
            if (BasisModule.KTX2File === undefined) {
                console.warn('KTX2TextureTranscoder: Please update Basis Universal transcoder.');
            }
        });
    }

    function transcode(buffer) {
        const ktx2File = new BasisModule.KTX2File(new Uint8Array(buffer));

        function cleanup() {
            ktx2File.close();
            ktx2File.delete();
        }

        if (!ktx2File.isValid()) {
            cleanup();
            throw new Error('KTX2TextureTranscoder: Invalid or unsupported .ktx2 file');
        }
        const basisFormat = ktx2File.isUASTC() ? BasisFormat.UASTC_4x4 : BasisFormat.ETC1S;
        const width = ktx2File.getWidth();
        const height = ktx2File.getHeight();
        const levels = ktx2File.getLevels();
        const hasAlpha = ktx2File.getHasAlpha();
        const dfdTransferFn = ktx2File.getDFDTransferFunc();
        const dfdFlags = ktx2File.getDFDFlags();
        const {transcoderFormat, engineFormat} = getTranscoderFormat(basisFormat, width, height, hasAlpha);
        if (!width || !height || !levels) {
            cleanup();
            throw new Error('KTX2TextureTranscoder: Invalid texture');
        }
        if (!ktx2File.startTranscoding()) {
            cleanup();
            throw new Error('KTX2TextureTranscoder: .startTranscoding failed');
        }
        const mipmaps = [];
        for (let mip = 0; mip < levels; mip++) {
            const levelInfo = ktx2File.getImageLevelInfo(mip, 0, 0);
            const mipWidth = levelInfo.origWidth;
            const mipHeight = levelInfo.origHeight;
            const dst = new Uint8Array(ktx2File.getImageTranscodedSizeInBytes(mip, 0, 0, transcoderFormat));
            const status = ktx2File.transcodeImage(dst, mip, 0, 0, transcoderFormat, 0, -1, -1);
            if (!status) {
                cleanup();
                throw new Error('KTX2TextureTranscoder: .transcodeImage failed.');
            }
            mipmaps.push({data: dst, width: mipWidth, height: mipHeight});
        }
        cleanup();
        return {width, height, hasAlpha, mipmaps, format: engineFormat, dfdTransferFn, dfdFlags};
    }

    // Optimal choice of a transcoder target format depends on the Basis format (ETC1S or UASTC),
    // device capabilities, and texture dimensions. The list below ranks the formats separately
    // for ETC1S and UASTC.
    //
    // In some cases, transcoding UASTC to RGBA32 might be preferred for higher quality (at
    // significant memory cost) compared to ETC1/2, BC1/3, and PVRTC. The transcoder currently
    // chooses RGBA32 only as a last resort and does not expose that option to the caller.

    const FORMAT_OPTIONS = [{
        if: 'astcSupported',
        basisFormat: [BasisFormat.UASTC_4x4],
        transcoderFormat: [TranscoderFormat.ASTC_4x4, TranscoderFormat.ASTC_4x4],
        engineFormat: [EngineFormat.RGBA_ASTC_4x4_Format, EngineFormat.RGBA_ASTC_4x4_Format],
        priorityETC1S: Infinity,
        priorityUASTC: 1,
        needsPowerOfTwo: false
    }, {
        if: 'bptcSupported',
        basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4],
        transcoderFormat: [TranscoderFormat.BC7_M5, TranscoderFormat.BC7_M5],
        engineFormat: [EngineFormat.RGBA_BPTC_Format, EngineFormat.RGBA_BPTC_Format],
        priorityETC1S: 3,
        priorityUASTC: 2,
        needsPowerOfTwo: false
    }, {
        if: 'dxtSupported',
        basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4],
        transcoderFormat: [TranscoderFormat.BC1, TranscoderFormat.BC3],
        engineFormat: [EngineFormat.RGB_S3TC_DXT1_Format, EngineFormat.RGBA_S3TC_DXT5_Format],
        priorityETC1S: 4,
        priorityUASTC: 5,
        needsPowerOfTwo: false
    }, {
        if: 'etc2Supported',
        basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4],
        transcoderFormat: [TranscoderFormat.ETC1, TranscoderFormat.ETC2],
        engineFormat: [EngineFormat.RGB_ETC2_Format, EngineFormat.RGBA_ETC2_EAC_Format],
        priorityETC1S: 1,
        priorityUASTC: 3,
        needsPowerOfTwo: false
    }, {
        if: 'etc1Supported',
        basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4],
        transcoderFormat: [TranscoderFormat.ETC1],
        engineFormat: [EngineFormat.RGB_ETC1_Format],
        priorityETC1S: 2,
        priorityUASTC: 4,
        needsPowerOfTwo: false
    }, {
        if: 'pvrtcSupported',
        basisFormat: [BasisFormat.ETC1S, BasisFormat.UASTC_4x4],
        transcoderFormat: [TranscoderFormat.PVRTC1_4_RGB, TranscoderFormat.PVRTC1_4_RGBA],
        engineFormat: [EngineFormat.RGB_PVRTC_4BPPV1_Format, EngineFormat.RGBA_PVRTC_4BPPV1_Format],
        priorityETC1S: 5,
        priorityUASTC: 6,
        needsPowerOfTwo: true
    }];
    const ETC1S_OPTIONS = FORMAT_OPTIONS.sort(function (a, b) {
        return a.priorityETC1S - b.priorityETC1S;
    });
    const UASTC_OPTIONS = FORMAT_OPTIONS.sort(function (a, b) {
        return a.priorityUASTC - b.priorityUASTC;
    });

    function getTranscoderFormat(basisFormat, width, height, hasAlpha) {
        let transcoderFormat;
        let engineFormat;
        const options = basisFormat === BasisFormat.ETC1S ? ETC1S_OPTIONS : UASTC_OPTIONS;
        for (let i = 0; i < options.length; i++) {
            const opt = options[i];
            if (!config[opt.if]) continue;
            if (!opt.basisFormat.includes(basisFormat)) continue;
            if (hasAlpha && opt.transcoderFormat.length < 2) continue;
            if (opt.needsPowerOfTwo && !(isPowerOfTwo(width) && isPowerOfTwo(height))) continue;
            transcoderFormat = opt.transcoderFormat[hasAlpha ? 1 : 0];
            engineFormat = opt.engineFormat[hasAlpha ? 1 : 0];
            return {
                transcoderFormat,
                engineFormat
            };
        }
        console.warn('KTX2TextureTranscoder: No suitable compressed texture format found. Decoding to RGBA32.');
        transcoderFormat = TranscoderFormat.RGBA32;
        engineFormat = EngineFormat.RGBAFormat;
        return {
            transcoderFormat,
            engineFormat
        };
    }

    function isPowerOfTwo(value) {
        if (value <= 2) return true;
        return (value & value - 1) === 0 && value !== 0;
    }
};

const cachedTranscoders = {};

/**
 * Returns a new {@link KTX2TextureTranscoder}.
 *
 * The ````transcoderPath```` config will be set to: "https://cdn.jsdelivr.net/npm/@xeokit/xeokit-sdk/dist/basis/"
 *
 * @private
 */
function getKTX2TextureTranscoder(viewer) {
    const sceneId = viewer.scene.id;
    let transcoder = cachedTranscoders[sceneId];
    if (!transcoder) {
        transcoder = new KTX2TextureTranscoder({viewer});
        cachedTranscoders[sceneId] = transcoder;
        viewer.scene.on("destroyed", () => {
            delete cachedTranscoders[sceneId];
            transcoder.destroy();
        });
    }
    return transcoder;
}

export {getKTX2TextureTranscoder, KTX2TextureTranscoder};