Source: tiny/plugins/compressed-texture/loaders/BASISLoader.js

/* eslint-disable no-useless-computed-key,camelcase */

import extensionChooser from '../extensionChooser';

const BASIS_FORMAT = {
  cTFETC1: 0, // not support alpha
  // cTFETC2: 1, // not WebGL
  cTFBC1: 2, // not support alpha
  cTFBC3: 3,
  // cTFBC4: 4, // not WebGL
  // cTFBC5: 5, // not WebGL
  // cTFBC7_M6_OPAQUE_ONLY: 6 // not WebGL
  // cTFBC7_M5 : 7 // not WebGL
  cTFPVRTC1_4_RGB: 8, // not support alpha
  cTFPVRTC1_4_RGBA: 9,
  cTFASTC_4x4: 10, // mobile alpha! Ehooo
  // cTFATC_RGB : 11 //  not WebGL
  // cTFATC_RGBA_INTERPOLATED_ALPHA : 12  not WebGL
  cTFRGBA32: 11, // why not?
};

const BASIS_HAS_ALPHA = {
  [3]: true,
  [9]: true,
  [10]: true,
  [11]: true,
};

const NON_COMPRESSED = -1;
const COMPRESSED_RGB_ETC1_WEBGL = 0x8D64;
const COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0;
// const COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1;
// const COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2;
const COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3;
const COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00;
const COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02;
const COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0;

const BASIS_TO_FMT = {
  // fallback
  [BASIS_FORMAT.cTFRGBA32]: NON_COMPRESSED,
  [BASIS_FORMAT.cTFETC1]: COMPRESSED_RGB_ETC1_WEBGL,
  [BASIS_FORMAT.cTFBC1]: COMPRESSED_RGB_S3TC_DXT1_EXT,
  [BASIS_FORMAT.cTFBC3]: COMPRESSED_RGBA_S3TC_DXT5_EXT,
  [BASIS_FORMAT.cTFPVRTC1_4_RGB]: COMPRESSED_RGB_PVRTC_4BPPV1_IMG,
  [BASIS_FORMAT.cTFPVRTC1_4_RGBA]: COMPRESSED_RGBA_PVRTC_4BPPV1_IMG,
  [BASIS_FORMAT.cTFASTC_4x4]: COMPRESSED_RGBA_ASTC_4x4_KHR,
};

const FMT_TO_BASIS = Object.keys(BASIS_TO_FMT).reduce((acc, next) => {
  acc[BASIS_TO_FMT[+next]] = +next;

  return acc;
}, {});

/**
 *
 * @example
 * // 初始化压缩纹理插件
 * Tiny.plugins.compressedTexture.init(app.renderer);
 * // 此 BASIS 方法是 basis_transcoder.js 提供的 wasm 模块
 * BASIS().then((Module) => {
 *  const { BasisFile, initializeBasis } = Module;
 *
 *  // run module
 *  initializeBasis();
 *  // 执行绑定
 *  Tiny.plugins.compressedTexture.BASISLoader.bindTranscoder(BasisFile);
 *
 *  const loader = new Tiny.loaders.Loader();
 *
 *  loader
 *  .add({
 *    name: 'xxx',
 *    url: './res/xxx.png',
 *    // 设置 metadata.useFormat 为 basis,告诉加载器明确只使用 BASIS 纹理
 *    metadata: { useCompressedTexture: true, useFormat: 'basis' },
 *  })
 *  .load((loaderInstance, resources) => {
 *  });
 * });
 *
 * @memberof Tiny.plugins.compressedTexture
 */
class BASISLoader {
  constructor(image) {
    this._image = image;
  }

  load(buffer) {
    this._loadAsync(buffer);
    return this._image;
  }

  _loadAsync(buffer) {
    // const startTime = performance.now();
    const BasisFileCtr = BASISLoader.BASIS_BINDING;
    const basisFile = new BasisFileCtr(new Uint8Array(buffer));
    const width = basisFile.getImageWidth(0, 0);
    const height = basisFile.getImageHeight(0, 0);
    // const images = await basisFile.getNumImages(); // not support yet
    const levels = 1; //await basisFile.getNumLevels( 0 ); // not support yet
    const hasAlpha = basisFile.getHasAlpha();
    const dest = this._image;

    if (!basisFile.startTranscoding()) {
      throw new Error('[BASISLoader] Transcoding error!');
    }

    const target = hasAlpha ? BASISLoader.RGBA_FORMAT : BASISLoader.RGB_FORMAT;

    // console.log('[BASISLoader] Grats! BASIS will be transcoded to:', target);

    const dst = new Uint8Array(basisFile.getImageTranscodedSizeInBytes(0, 0, target.basis));

    if (!basisFile.transcodeImage(dst, 0, 0, target.basis, !!0, !!0)) {
      throw new Error('[BASISLoader] Transcoding error!');
    }

    // console.log('[BASISLoader] Totla transcoding time:', performance.now() - startTime);

    const name = target.name.replace('COMPRESSED_', '');

    dest._getLevelSize = (level) => {
      return basisFile.getImageTranscodedSizeInBytes(0, level, FMT_TO_BASIS[target.native]);
    };

    return Promise.resolve(dest.init(dest.src, dst, 'BASIS|' + name, width, height, levels, target.native));
  }
}

BASISLoader.BASIS_BINDING = undefined;

BASISLoader.test = function(array) {
  const header = new Uint32Array(array, 0, 1)[0];
  const decoder = !!BASISLoader.BASIS_BINDING;
  const isValid = header === 0x134273 && decoder;
  const isSupported = BASISLoader.RGB_FORMAT && BASISLoader.RGBA_FORMAT;

  if (!isValid && isSupported) {
    console.warn('[BASISLoader] Is Supported, but transcoder not binded or file is not BASIS file!');
  }

  return (isSupported && isValid);
};

/**
 * @static
 * @param {function} fileCtr - BASIS transcoder 返回的绑定句柄
 */
BASISLoader.bindTranscoder = function(fileCtr) {
  const ext = extensionChooser.compressedExtensions;

  if (!fileCtr || !ext) {
    throw new Error('Invalid state! undef fileCtr or ext invalid!');
  };

  // fetch list of ALL extensions
  const plain = Object.keys(ext)
    .reduce((acc, key) => {
      const val = ext[key];

      if (!val) {
        return acc;
      };

      return Object.assign(acc, val.__proto__); // eslint-disable-line
    }, {});

  if (Object.keys(plain).length === 0) {
    return;
  }

  let latestOp;
  let lastestAlpha;

  // select support
  for (let v in plain) {
    const native = plain[v];

    if (FMT_TO_BASIS[native] !== undefined) {
      const basis = FMT_TO_BASIS[native];

      if (BASIS_HAS_ALPHA[basis]) {
        lastestAlpha = {
          native,
          name: v,
          basis,
        };
      } else {
        latestOp = {
          native,
          name: v,
          basis,
        };
      }
    }
  }

  BASISLoader._useWorker = false;
  BASISLoader.RGB_FORMAT = latestOp || lastestAlpha;
  BASISLoader.RGBA_FORMAT = lastestAlpha || latestOp;
  BASISLoader.BASIS_BINDING = fileCtr;
};

export default BASISLoader;
Documentation generated by JSDoc 3.4.3 on Fri Jul 09 2021 19:32:25 GMT+0800 (CST)