令和元年に登場したテクスチャフォーマットBasisを試す

## はじめに
昨年のAdvent Calendarは[Crunched Texture](https://qiita.com/nyamadandan/items/89f2d2a8b139d850edc2)について書きました。
しかし、令和元年、Crunchedテクスチャを過去のものにする新テクスチャフォーマットが登場しました。

## Crunchedとは何なのか
WebGLのテクスチャフォーマットとしてはPNGやJPEGなどブラウザではおなじみの画像フォーマットが使用されますが、これはGPU上では非圧縮で32bppまたは24bppに展開されるため、効率がよくありません。
一方でDXTCやETCなどGPU用の圧縮テクスチャを使用すれば8bppから4bppまで圧縮できます。
これらDXTCやETCをさらにCPUで圧縮したのがCrunchedです。
これによりダウンロードサイズやストレージサイズを節約できます。
Crunched Textureは[Unityにも組み込まれており](https://blogs.unity3d.com/jp/2017/11/15/updated-crunch-texture-compression-library/)、PCのみならず、スマートフォンデバイスでも動作します。

## Crunchedではだめなのか
Crunchedではなく、そもそも圧縮テクスチャに問題があります。
WebGLはPC/Android/iOSで動作しますが、圧縮テクスチャはそれぞれに対応したフォーマットを使用する必要があります。
PCではDXTC(BC1,BC3)、iOSではPVRTC、AndroidではETCが使われます。
そのためPC用にはPC用のDXTCに展開できるCrunchedテクスチャ、Android用にはETCに展開できるCrunchedテクスチャを作成し、それぞれダウンロードする必要があります。また、CrunchedはPVRTCには対応していません。
そもそも圧縮テクスチャはWebGLにおいては拡張機能としてのサポートであり、たとえAndroidであってもETCで対応している保証はありません。[^etc]
[^etc]: OpenGL ES 3.0は圧縮テクスチャを標準仕様としてサポートしていますが、OpenGL ES 3.0に相当するWebGL 2.0では拡張機能としてのサポートとなっています。

## ETC1S to BC1
この問題を解決するカギの一つが[ETC1S](http://richg42.blogspot.com/2018/06/etc1s-texture-format-encoding.html)というフォーマットです。
このフォーマットはETC1SはETC1のサブセットで、BC1に変換可能なフォーマットとなっています。
ETC1Sフォーマットを使用すればPCではBC1を使用し、AndroidではETC1を使用するといったことができるようになります。
しかし、ETC1SをBC1に変換できたとしてもiOSではPVRTCが使用されていますし、アルファチャンネルにも対応できていません。

## ある日突然登場したBasis Universal
ETC1Sを試そうかなーとずっと考えてたんですけどなかなかまとまった時間が取れない日々が続いていた私の耳に飛び込んできたのが[このBasis Universal Texture](https://www.khronos.org/blog/google-and-binomial-contribute-basis-universal-texture-format-to-khronos-gltf-3d-transmission-open-standard)フォーマットです。
Basis Universal Textureは中間フォーマットbasisからそれぞれのプラットフォームに対応した圧縮テクスチャに変換するフォーマットで、内部的には先ほど説明したETC1Sが使用されています。
Basis Universal TextureはETC1/ETC2/PVRTC1/BC1-5/BC7/ASTCと多くのテクスチャフォーマットへと変換できます。 
さらにbasisはCrunched同様、CPUで圧縮がかかっており、これらは0.3～1.25bpp程度の圧縮ができます。
ほかにはミップマップ、複数画像の格納、動画用フレームレートなどREADMEを読み込んでみるとかなり多機能です。

GoogleとKhronosが共同でglTFのテクスチャフォーマット仕様としてコントリビュートしていくと発表されており、ソースコードも公開されました。
dracoと組み合わせて使用すれば、glTFサイズを大きく削減できそうです。

## 早速試してみる
Basis Universal Textureのソースコードは[GitHubに公開](https://github.com/binomialLLC/basis_universal)されています。
このレポジトリには圧縮/展開ツールのbasisu及び、ランタイムで圧縮テクスチャに含まれているbasisTranscoderが含まれています。
多くの環境ではbasisuはCMakeを使用すればビルドできますが、WindowsではCMakeではうまくビルドできないようですので同梱されているbasisu.slnを使用してbasisu.exeをビルドします。

basisu.exeは変換元データとしてpngファイルを使用します。
`basisu image.png` コマンドで`image.basis`を作成できます。
`basisu`には様々なオプションを与えることができます。
たとえば`-comp_level`で圧縮レベルが設定可能で、`basisu image.png -comp_level 5`とすれば時間はかかりますが最高品質で圧縮ができます。
また、圧縮テクスチャでは `UNPACK_FLIP_Y_WEBGL` が機能しないため、上下反転オプションの`-y_flip`も合わせて指定しておくとよいでしょう。

basisTranscoderはEmscriptenを使用してWebAssemblyにビルドして使用します。CMakeを使用したビルド方法がwebgl/transcoder以下のREADMEで説明されているので、説明に従ってビルドします。
WebAssemblyにビルドされたbasisTranscoderはASTC/BC1/BC3/ETC1/ETC2/PVRTCに対応しています。
また、これら圧縮テクスチャが使用できなかった場合のフォールバック先としてRGBA32とRGB565にも変換できますのでWebAssemblyが動作すればテクスチャが使用できないといったことはありません。
PNGやJPEGといったテクスチャを使っているとRGBA32に変換することが多いので、RGB565のテクスチャを使うのはなかなか珍しいかなと思います。

basisTranscoder.jsを読み込むと`BASIS`がグローバル空間に定義されるので `then` メソッドでWASMがコンパイルされるのを待ち、 `initializeBasis` で初期化します。
その後basisFileのメソッドを呼び出すことでbasisから圧縮テクスチャへの変換ができます。

たとえばBC1に変換するコードはこんな感じになります。

```js
BASIS().then(basisTransCoder => {
  const gl = document.querySelector("#canvas").getContext("webgl2");

  // basisTranscoder初期化
  const {initializeBasis} = basisTransCoder;
  initializeBasis();

  // basisファイルを変換
  const BasisFile = basisTransCoder.BasisFile;
  const basisFile = new BasisFile(image);
  const width = basisFile.getImageWidth(0, 0);
  const height = basisFile.getImageHeight(0, 0);

  if (!basisFile.startTranscoding()) {
    console.error("failed to startTranscoding.");
    basisFile.close();
    basisFile.delete();
    return;
  }

  const BC1 = 1;

  const extractSize = basisFile.getImageTranscodedSizeInBytes(0, 0, BC1);
  const textureSource = new Uint8Array(extractSize);

  if (!basisFile.transcodeImage(textureSource, 0, 0, basisFormat, 0, 0)) {
    console.error("failed to transcodeImage.");
  }

  basisFile.close();
  basisFile.delete();

  // 圧縮テクスチャを作成
  const tex = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, tex);
  const COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0;
  gl.compressedTexImage2D(
    gl.TEXTURE_2D,
    0,
    COMPRESSED_RGB_S3TC_DXT1_EXT,
    width,
    height,
    0,
    textureSource
  );

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
});
```

そして、実際組み込んでみたものが以下となります。

 * [ソースコード](https://github.com/nyamadan/hello_basis)
 * [デモ](https://nyamadan.github.io/hello_basis/)

## 動かしてみた結果

PCとAndroidで圧縮テクスチャに変換できることが確認できました。確認はできていませんがおそらくiOSでもPVRTCに変換して使用できます。

多くの圧縮テクスチャに変換できますが、プラットフォームや品質面、アルファチャンネルの有無などにより、どのテクスチャに変換するかは状況に応じて考える必要がありそうです。