Added basis_loader.js for WebGL
diff --git a/webgl/transcoder/.gitignore b/webgl/transcoder/.gitignore
index 707a2b1..324ab6c 100644
--- a/webgl/transcoder/.gitignore
+++ b/webgl/transcoder/.gitignore
@@ -1,3 +1,4 @@
 build/*
+!build/basis_loader.js
 !build/basis_transcoder.js
 !build/basis_transcoder.wasm
diff --git a/webgl/transcoder/build/basis_loader.js b/webgl/transcoder/build/basis_loader.js
new file mode 100644
index 0000000..3fef5ec
--- /dev/null
+++ b/webgl/transcoder/build/basis_loader.js
@@ -0,0 +1,493 @@
+/*

+ * Basis Loader

+ *

+ * Usage:

+ * // basis_loader.js should be loaded from the same directory as

+ * // basis_transcoder.js and basis_transcoder.wasm

+ *

+ * // Create the texture loader and set the WebGL context it should use. Spawns

+ * // a worker which handles all of the transcoding.

+ * let basisLoader = new BasisLoader();

+ * basisLoader.setWebGLContext(gl);

+ *

+ * // To allow separate color and alpha textures to be returned in cases where

+ * // it would provide higher quality:

+ * basisLoader.allowSeparateAlpha = true;

+ *

+ * // loadFromUrl() returns a promise which resolves to a completed WebGL

+ * // texture or rejects if there's an error loading.

+ * basisBasics.loadFromUrl(fullPathToTexture).then((result) => {

+ *   // WebGL color+alpha texture;

+ *   result.texture;

+ *

+ *   // WebGL alpha texture, only if basisLoader.allowSeparateAlpha is true.

+ *   // null if alpha is encoded in result.texture or result.alpha is false.

+ *   result.alphaTexture;

+ *

+ *   // True if the texture contained an alpha channel.

+ *   result.alpha;

+ *

+ *   // Number of mip levels in texture/alphaTexture

+ *   result.mipLevels;

+ *

+ *   // Dimensions of the base mip level.

+ *   result.width;

+ *   result.height;

+ * });

+ */

+

+// This file contains the code both for the main thread interface and the

+// worker that does the transcoding.

+const IN_WORKER = typeof importScripts === "function";

+const SCRIPT_PATH = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;

+

+if (!IN_WORKER) {

+  //

+  // Main Thread

+  //

+  class PendingTextureRequest {

+    constructor(gl, url) {

+      this.gl = gl;

+      this.url = url;

+      this.texture = null;

+      this.alphaTexture = null;

+      this.promise = new Promise((resolve, reject) => {

+        this.resolve = resolve;

+        this.reject = reject;

+      });

+    }

+

+    uploadImageData(webglFormat, buffer, mipLevels) {

+      let gl = this.gl;

+      let texture = gl.createTexture();

+      gl.bindTexture(gl.TEXTURE_2D, texture);

+      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

+      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, mipLevels.length > 1 || webglFormat.uncompressed ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR);

+

+      let levelData = null;

+

+      for (let mipLevel of mipLevels) {

+        if (!webglFormat.uncompressed) {

+          levelData = new Uint8Array(buffer, mipLevel.offset, mipLevel.size);

+          gl.compressedTexImage2D(

+            gl.TEXTURE_2D,

+            mipLevel.level,

+            webglFormat.format,

+            mipLevel.width,

+            mipLevel.height,

+            0,

+            levelData);

+        } else {

+          switch (webglFormat.type) {

+            case WebGLRenderingContext.UNSIGNED_SHORT_4_4_4_4:

+            case WebGLRenderingContext.UNSIGNED_SHORT_5_5_5_1:

+            case WebGLRenderingContext.UNSIGNED_SHORT_5_6_5:

+              levelData = new Uint16Array(buffer, mipLevel.offset, mipLevel.size / 2);

+              break;

+            default:

+              levelData = new Uint8Array(buffer, mipLevel.offset, mipLevel.size);

+              break;

+          }

+          gl.texImage2D(

+            gl.TEXTURE_2D,

+            mipLevel.level,

+            webglFormat.format,

+            mipLevel.width,

+            mipLevel.height,

+            0,

+            webglFormat.format,

+            webglFormat.type,

+            levelData);

+        }

+      }

+

+      if (webglFormat.uncompressed && mipLevels.length == 1) {

+        gl.generateMipmap(gl.TEXTURE_2D);

+      }

+

+      return texture;

+    }

+  };

+

+  class BasisLoader {

+    constructor() {

+      this.gl = null;

+      this.supportedFormats = {};

+      this.pendingTextures = {};

+      this.nextPendingTextureId = 1;

+      this.allowSeparateAlpha = false;

+

+      // Reload the current script as a worker

+      this.worker = new Worker(SCRIPT_PATH);

+      this.worker.onmessage = (msg) => {

+        // Find the pending texture associated with the data we just received

+        // from the worker.

+        let pendingTexture = this.pendingTextures[msg.data.id];

+        if (!pendingTexture) {

+          if (msg.data.error) {

+            console.error(`Basis transcode failed: ${msg.data.error}`);

+          }

+          console.error(`Invalid pending texture ID: ${msg.data.id}`);

+          return;

+        }

+

+        // Remove the pending texture from the waiting list.

+        delete this.pendingTextures[msg.data.id];

+

+        // If the worker indicated an error has occured handle it now.

+        if (msg.data.error) {

+          console.error(`Basis transcode failed: ${msg.data.error}`);

+          pendingTexture.reject(`${msg.data.error}`);

+          return;

+        }

+

+        // Upload the image data returned by the worker.

+        pendingTexture.texture = pendingTexture.uploadImageData(

+            msg.data.webglFormat,

+            msg.data.buffer,

+            msg.data.mipLevels);

+

+        if (msg.data.alphaBuffer) {

+          pendingTexture.alphaTexture = pendingTexture.uploadImageData(

+              msg.data.webglFormat,

+              msg.data.alphaBuffer,

+              msg.data.mipLevels);

+        }

+

+        pendingTexture.resolve({

+          mipLevels: msg.data.mipLevels.length,

+          width: msg.data.mipLevels[0].width,

+          height: msg.data.mipLevels[0].height,

+          alpha: msg.data.hasAlpha,

+          texture: pendingTexture.texture,

+          alphaTexture: pendingTexture.alphaTexture,

+        });

+      };

+    }

+

+    setWebGLContext(gl) {

+      if (this.gl != gl) {

+        this.gl = gl;

+        if (gl) {

+          this.supportedFormats = {

+            s3tc: !!gl.getExtension('WEBGL_compressed_texture_s3tc'),

+            etc1: !!gl.getExtension('WEBGL_compressed_texture_etc1'),

+            etc2: !!gl.getExtension('WEBGL_compressed_texture_etc'),

+            pvrtc: !!gl.getExtension('WEBGL_compressed_texture_pvrtc'),

+            astc: !!gl.getExtension('WEBGL_compressed_texture_astc'),

+            bptc: !!gl.getExtension('EXT_texture_compression_bptc')

+          };

+        } else {

+          this.supportedFormats = {};

+        }

+      }

+    }

+

+    // This method changes the active texture unit's TEXTURE_2D binding

+    // immediately prior to resolving the returned promise.

+    loadFromUrl(url) {

+      let pendingTexture = new PendingTextureRequest(this.gl, url);

+      this.pendingTextures[this.nextPendingTextureId] = pendingTexture;

+

+      this.worker.postMessage({

+        id: this.nextPendingTextureId,

+        url: url,

+        allowSeparateAlpha: this.allowSeparateAlpha,

+        supportedFormats: this.supportedFormats

+      });

+

+      this.nextPendingTextureId++;

+      return pendingTexture.promise;

+    }

+  }

+

+  window.BasisLoader = BasisLoader;

+

+} else {

+  //

+  // Worker

+  //

+  importScripts('basis_transcoder.js');

+

+  let BasisFile = null;

+

+  const BASIS_INITIALIZED = BASIS().then((module) => {

+    BasisFile = module.BasisFile;

+    module.initializeBasis();

+  });

+

+  // Copied from enum class transcoder_texture_format in basisu_transcoder.h with minor javascript-ification

+  const BASIS_FORMAT = {

+    // Compressed formats

+

+    // ETC1-2

+    cTFETC1_RGB: 0,							// Opaque only, returns RGB or alpha data if cDecodeFlagsTranscodeAlphaDataToOpaqueFormats flag is specified

+    cTFETC2_RGBA: 1,							// Opaque+alpha, ETC2_EAC_A8 block followed by a ETC1 block, alpha channel will be opaque for opaque .basis files

+

+    // BC1-5, BC7 (desktop, some mobile devices)

+    cTFBC1_RGB: 2,							// Opaque only, no punchthrough alpha support yet, transcodes alpha slice if cDecodeFlagsTranscodeAlphaDataToOpaqueFormats flag is specified

+    cTFBC3_RGBA: 3, 							// Opaque+alpha, BC4 followed by a BC1 block, alpha channel will be opaque for opaque .basis files

+    cTFBC4_R: 4,								// Red only, alpha slice is transcoded to output if cDecodeFlagsTranscodeAlphaDataToOpaqueFormats flag is specified

+    cTFBC5_RG: 5,								// XY: Two BC4 blocks, X=R and Y=Alpha, .basis file should have alpha data (if not Y will be all 255's)

+    cTFBC7_RGBA: 6,							// RGB or RGBA, mode 5 for ETC1S, modes (1,2,3,5,6,7) for UASTC

+

+    // PVRTC1 4bpp (mobile, PowerVR devices)

+    cTFPVRTC1_4_RGB: 8,						// Opaque only, RGB or alpha if cDecodeFlagsTranscodeAlphaDataToOpaqueFormats flag is specified, nearly lowest quality of any texture format.

+    cTFPVRTC1_4_RGBA: 9,					// Opaque+alpha, most useful for simple opacity maps. If .basis file doesn't have alpha cTFPVRTC1_4_RGB will be used instead. Lowest quality of any supported texture format.

+

+    // ASTC (mobile, Intel devices, hopefully all desktop GPU's one day)

+    cTFASTC_4x4_RGBA: 10,					// Opaque+alpha, ASTC 4x4, alpha channel will be opaque for opaque .basis files. Transcoder uses RGB/RGBA/L/LA modes, void extent, and up to two ([0,47] and [0,255]) endpoint precisions.

+

+    // Uncompressed (raw pixel) formats

+    cTFRGBA32: 13,							// 32bpp RGBA image stored in raster (not block) order in memory, R is first byte, A is last byte.

+    cTFRGB565: 14,							// 166pp RGB image stored in raster (not block) order in memory, R at bit position 11

+    cTFBGR565: 15,							// 16bpp RGB image stored in raster (not block) order in memory, R at bit position 0

+    cTFRGBA4444: 16,							// 16bpp RGBA image stored in raster (not block) order in memory, R at bit position 12, A at bit position 0

+

+    cTFTotalTextureFormats: 22,

+  };

+

+  // WebGL compressed formats types, from:

+  // http://www.khronos.org/registry/webgl/extensions/

+

+  // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/

+  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;

+

+  // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_etc1/

+  const COMPRESSED_RGB_ETC1_WEBGL = 0x8D64

+

+  // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_etc/

+  const COMPRESSED_R11_EAC                        = 0x9270;

+  const COMPRESSED_SIGNED_R11_EAC                 = 0x9271;

+  const COMPRESSED_RG11_EAC                       = 0x9272;

+  const COMPRESSED_SIGNED_RG11_EAC                = 0x9273;

+  const COMPRESSED_RGB8_ETC2                      = 0x9274;

+  const COMPRESSED_SRGB8_ETC2                     = 0x9275;

+  const COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2  = 0x9276;

+  const COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277;

+  const COMPRESSED_RGBA8_ETC2_EAC                 = 0x9278;

+  const COMPRESSED_SRGB8_ALPHA8_ETC2_EAC          = 0x9279;

+

+  // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_astc/

+  const COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0;

+

+  // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_pvrtc/

+  const COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00;

+  const COMPRESSED_RGB_PVRTC_2BPPV1_IMG  = 0x8C01;

+  const COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02;

+  const COMPRESSED_RGBA_PVRTC_2BPPV1_IMG = 0x8C03;

+

+  // https://www.khronos.org/registry/webgl/extensions/EXT_texture_compression_bptc/

+  const COMPRESSED_RGBA_BPTC_UNORM_EXT = 0x8E8C;

+  const COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT = 0x8E8D;

+  const COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT = 0x8E8E;

+  const COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT = 0x8E8F;

+

+  const BASIS_WEBGL_FORMAT_MAP = {};

+  // Compressed formats

+  BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFBC1_RGB] = { format: COMPRESSED_RGB_S3TC_DXT1_EXT };

+  BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFBC3_RGBA] = { format: COMPRESSED_RGBA_S3TC_DXT5_EXT };

+  BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFBC7_RGBA] = { format: COMPRESSED_RGBA_BPTC_UNORM_EXT };

+  BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFETC1_RGB] = { format: COMPRESSED_RGB_ETC1_WEBGL };

+  BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFETC2_RGBA] = { format: COMPRESSED_RGBA8_ETC2_EAC };

+  BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFASTC_4x4_RGBA] = { format: COMPRESSED_RGBA_ASTC_4x4_KHR };

+  BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFPVRTC1_4_RGB] = { format: COMPRESSED_RGB_PVRTC_4BPPV1_IMG };

+  BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFPVRTC1_4_RGBA] = { format: COMPRESSED_RGBA_PVRTC_4BPPV1_IMG };

+

+  // Uncompressed formats

+  BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFRGBA32] = { uncompressed: true, format: WebGLRenderingContext.RGBA, type: WebGLRenderingContext.UNSIGNED_BYTE };

+  BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFRGB565] = { uncompressed: true, format: WebGLRenderingContext.RGB, type: WebGLRenderingContext.UNSIGNED_SHORT_5_6_5 };

+  BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFRGBA4444] = { uncompressed: true, format: WebGLRenderingContext.RGBA, type: WebGLRenderingContext.UNSIGNED_SHORT_4_4_4_4 };

+

+  // Notifies the main thread when a texture has failed to load for any reason.

+  function fail(id, errorMsg) {

+    postMessage({

+      id: id,

+      error: errorMsg

+    });

+  }

+

+  function basisFileFail(id, basisFile, errorMsg) {

+    fail(id, errorMsg);

+    basisFile.close();

+    basisFile.delete();

+  }

+

+  // This utility currently only transcodes the first image in the file.

+  const IMAGE_INDEX = 0;

+  const TOP_LEVEL_MIP = 0;

+

+  function transcode(id, arrayBuffer, supportedFormats, allowSeparateAlpha) {

+    let basisData = new Uint8Array(arrayBuffer);

+

+    let basisFile = new BasisFile(basisData);

+    let images = basisFile.getNumImages();

+    let levels = basisFile.getNumLevels(IMAGE_INDEX);

+    let hasAlpha = basisFile.getHasAlpha();

+    if (!images || !levels) {

+      basisFileFail(id, basisFile, 'Invalid Basis data');

+      return;

+    }

+

+    if (!basisFile.startTranscoding()) {

+      basisFileFail(id, basisFile, 'startTranscoding failed');

+      return;

+    }

+

+    let basisFormat = undefined;

+    let needsSecondaryAlpha = false;

+    if (hasAlpha) {

+      if (supportedFormats.etc2) {

+        basisFormat = BASIS_FORMAT.cTFETC2_RGBA;

+      } else if (supportedFormats.bptc) {

+        basisFormat = BASIS_FORMAT.cTFBC7_RGBA;

+      } else if (supportedFormats.s3tc) {

+        basisFormat = BASIS_FORMAT.cTFBC3_RGBA;

+      } else if (supportedFormats.astc) {

+        basisFormat = BASIS_FORMAT.cTFASTC_4x4_RGBA;

+      } else if (supportedFormats.pvrtc) {

+        if (allowSeparateAlpha) {

+          basisFormat = BASIS_FORMAT.cTFPVRTC1_4_RGB;

+        } else {

+          basisFormat = BASIS_FORMAT.cTFPVRTC1_4_RGBA;

+        }

+      } else if (supportedFormats.etc1 && allowSeparateAlpha) {

+        basisFormat = BASIS_FORMAT.cTFETC1_RGB;

+        needsSecondaryAlpha = true;

+      } else {

+        // If we don't support any appropriate compressed formats transcode to

+        // raw pixels. This is something of a last resort, because the GPU

+        // upload will be significantly slower and take a lot more memory, but

+        // at least it prevents you from needing to store a fallback JPG/PNG and

+        // the download size will still likely be smaller.

+        basisFormat = BASIS_FORMAT.RGBA32;

+      }

+    } else {

+      if (supportedFormats.etc1) {

+        // Should be the highest quality, so use when available.

+        // http://richg42.blogspot.com/2018/05/basis-universal-gpu-texture-format.html

+        basisFormat = BASIS_FORMAT.cTFETC1_RGB;

+      } else if (supportedFormats.bptc) {

+        basisFormat = BASIS_FORMAT.cTFBC7_RGBA;

+      } else if (supportedFormats.s3tc) {

+        basisFormat = BASIS_FORMAT.cTFBC1_RGB;

+      } else if (supportedFormats.etc2) {

+        basisFormat = BASIS_FORMAT.cTFETC2_RGBA;

+      } else if (supportedFormats.astc) {

+        basisFormat = BASIS_FORMAT.cTFASTC_4x4_RGBA;

+      } else if (supportedFormats.pvrtc) {

+        basisFormat = BASIS_FORMAT.cTFPVRTC1_4_RGB;

+      } else {

+        // See note on uncompressed transcode above.

+        basisFormat = BASIS_FORMAT.cTFRGB565;

+      }

+    }

+

+    if (basisFormat === undefined) {

+      basisFileFail(id, basisFile, 'No supported transcode formats');

+      return;

+    }

+

+    let webglFormat = BASIS_WEBGL_FORMAT_MAP[basisFormat];

+

+    // If we're not using compressed textures it'll be cheaper to generate

+    // mipmaps on the fly, so only transcode a single level.

+    if (webglFormat.uncompressed) {

+      levels = 1;

+    }

+

+    // Gather information about each mip level to be transcoded.

+    let mipLevels = [];

+    let totalTranscodeSize = 0;

+

+    for (let mipLevel = 0; mipLevel < levels; ++mipLevel) {

+      let transcodeSize = basisFile.getImageTranscodedSizeInBytes(IMAGE_INDEX, mipLevel, basisFormat);

+      mipLevels.push({

+        level: mipLevel,

+        offset: totalTranscodeSize,

+        size: transcodeSize,

+        width: basisFile.getImageWidth(IMAGE_INDEX, mipLevel),

+        height: basisFile.getImageHeight(IMAGE_INDEX, mipLevel),

+      });

+      totalTranscodeSize += transcodeSize;

+    }

+

+    // Allocate a buffer large enough to hold all of the transcoded mip levels at once.

+    let transcodeData = new Uint8Array(totalTranscodeSize);

+    let alphaTranscodeData = needsSecondaryAlpha ? new Uint8Array(totalTranscodeSize) : null;

+

+    // Transcode each mip level into the appropriate section of the overall buffer.

+    for (let mipLevel of mipLevels) {

+      let levelData = new Uint8Array(transcodeData.buffer, mipLevel.offset, mipLevel.size);

+      if (!basisFile.transcodeImage(levelData, IMAGE_INDEX, mipLevel.level, basisFormat, 1, 0)) {

+        basisFileFail(id, basisFile, 'transcodeImage failed');

+        return;

+      }

+      if (needsSecondaryAlpha) {

+        let alphaLevelData = new Uint8Array(alphaTranscodeData.buffer, mipLevel.offset, mipLevel.size);

+        if (!basisFile.transcodeImage(alphaLevelData, IMAGE_INDEX, mipLevel.level, basisFormat, 1, 1)) {

+          basisFileFail(id, basisFile, 'alpha transcodeImage failed');

+          return;

+        }

+      }

+    }

+

+    basisFile.close();

+    basisFile.delete();

+

+    // Post the transcoded results back to the main thread.

+    let transferList = [transcodeData.buffer];

+    if (needsSecondaryAlpha) {

+      transferList.push(alphaTranscodeData.buffer);

+    }

+    postMessage({

+      id: id,

+      buffer: transcodeData.buffer,

+      alphaBuffer: needsSecondaryAlpha ? alphaTranscodeData.buffer : null,

+      webglFormat: webglFormat,

+      mipLevels: mipLevels,

+      hasAlpha: hasAlpha,

+    }, transferList);

+  }

+

+  onmessage = (msg) => {

+    // Each call to the worker must contain:

+    let url = msg.data.url; // The URL of the basis image OR

+    let buffer = msg.data.buffer; // An array buffer with the basis image data

+    let allowSeparateAlpha = msg.data.allowSeparateAlpha;

+    let supportedFormats = msg.data.supportedFormats; // The formats this device supports

+    let id = msg.data.id; // A unique ID for the texture

+

+    if (url) {

+      // Make the call to fetch the basis texture data

+      fetch(`../../${url}`).then(function(response) {

+        if (response.ok) {

+          response.arrayBuffer().then((arrayBuffer) => {

+            if (BasisFile) {

+              transcode(id, arrayBuffer, supportedFormats, allowSeparateAlpha);

+            } else {

+              BASIS_INITIALIZED.then(() => {

+                transcode(id, arrayBuffer, supportedFormats, allowSeparateAlpha);

+              });

+            }

+          });

+        } else {

+          fail(id, `Fetch failed: ${response.status}, ${response.statusText}`);

+        }

+      });

+    } else if (buffer) {

+      if (BasisFile) {

+        transcode(id, buffer, supportedFormats, allowSeparateAlpha);

+      } else {

+        BASIS_INITIALIZED.then(() => {

+          transcode(id, buffer, supportedFormats, allowSeparateAlpha);

+        });

+      }

+    } else {

+      fail(id, `No url or buffer specified`);

+    }

+  };

+}
\ No newline at end of file