WebGL(GLSL)でカラースケール関数を実装する

# カラースケール概要
データの値に応じて色を変えたい場合、下図のようなカラースケールを用いることがある。温度によって色分けされたサーモグラフィーの画面や、気温に応じて色分けされた地図などを思い浮かべれば良い。
![ダウンロード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58850/40aed418-86ee-f69a-388e-5195f8b43ea3.png)

カラースケールは大きく分けて以下の3種類に区分することができる。

- 1. Categorical
    - 質的・離散的な色分け。例えば折れ線グラフの1本目、2本目、...を色分けする場合などに用いる。
![category10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58850/30e30f04-fce6-8178-c85b-fba37e16bbee.png)
- 2. Diverging
    - ある値を境として２色に分岐する。例えば赤字/黒字の程度などを示す場合に用いる。
    - <img width="881" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58850/49978371-f017-2e47-0e93-0687b09eebb0.png">
- 3. Sequential
    - 連続的な色分け。例えば温度の高さを青〜黄〜赤で表現する場合など。単色のグラデーションも含む。
    - <img width="877" alt="スクリーンショット 2019-10-28 20.54.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58850/b6537ae3-44b4-9160-f9c4-38d0000f8b3e.png">

このうち、`Categorical`なカラースケールは単にRGBの配列を用意することで容易に実装できるが、残りの`Diverging`, `Sequential`は**RGBの連続的な補間が必要**になり、言語ごとに実装が必要になる。このような連続的カラースケールをWebGL(GLSL)で実装するのが本記事の内容。


# d3.jsの場合
JavaScriptでは、d3.jsの一部である[d3-scale-chromatic](https://github.com/d3/d3-scale-chromatic)に全３パターンのカラースケールが実装されている。連続的なカラースケールの場合、0-1の範囲の任意の実数値を受けて色を返してくれるような関数になっている。

これらのソースコード（例えば冒頭の虹色スケール`d3.interpolateSpectral()`に関する[Spectral.js](https://github.com/d3/d3-scale-chromatic/blob/master/src/diverging/Spectral.js)）を読んでみると、いくつか（１０程度）のRGBの配列を直書きしたうえで、それらのRGBをSpline補間することで任意の実数値を受けられるようにしていることがわかる。
補間前のRGBの配列は`d3.schemeSpectral`のような形で取得することができるので、これを借りてきて補間処理を実装すれば、他の言語でもカラースケール関数が利用できることになる。WebGLのGLSLの場合はJavaScriptから値を渡すことができるので、d3から補間前の配列を直接渡せばOKということになる。

# GLSLでの実装
以下の手順によってGLSLで連続的なカラースケール関数を利用する

- 1. （JavaScriptで）d3-scale-chromaticからカラースケールの元となるRGB配列を取得する
- 2. 上記配列は`#ff0000`のようなhex形式なので、0から1の間のRGBの実数値配列に変換する
- 3. 上記RGBの配列を`uniform`としてJavaScriptからGLSLに渡す
- 4. GLSLでBーspline補間を実装する

補間処理は[d3-scale-chromaticのJavaScriptでの実装](https://github.com/d3/d3-interpolate/blob/master/src/basis.js)をGLSLに翻訳すれば良い。

補間処理とJavaScriptから渡されたカラースケールを用いて本記事冒頭のようなカラースケールを表示するフラグメントシェーダのコードは以下の通り。

このShaderを利用して実際に描画を行うJavaScriptコードは[こちらを参照](https://observablehq.com/@sw1227/d3-scale-chromatic-in-webgl)。


```GLSL:fragmentShader
#define MAX_SCHEME_LENGTH 16

precision highp float;

uniform float u_size;
uniform int u_n;
uniform vec3 u_colorScheme[MAX_SCHEME_LENGTH];


// Use this function because we cannot use dynamic value for array index in GLSL
vec3 getData(float x) {
  for (int j=0; j<MAX_SCHEME_LENGTH; j++) {
    if (j == int(floor(x))) return u_colorScheme[j];
  }
  return vec3(0.0, 0.0, 0.0);
}

// GLSL translation of basis() in d3
// https://github.com/d3/d3-interpolate/blob/master/src/basis.js
vec3 basis(float t1, vec3 v0, vec3 v1, vec3 v2, vec3 v3) {
  float t2 = t1 * t1;
  float t3 = t2 * t1;
  return ((1.0 - 3.0 * t1 + 3.0 * t2 - t3) * v0
    + (4.0 - 6.0 * t2 + 3.0 * t3) * v1
    + (1.0 + 3.0 * t1 + 3.0 * t2 - 3.0 * t3) * v2
    + t3 * v3) / 6.0;
}

// GLSL translation of exported function in the URL below
// https://github.com/d3/d3-interpolate/blob/master/src/basis.js
vec3 getColor(float t) {
  int n = u_n - 1;

  float i  = t <= 0.0 ? 0.0 : (t >= 1.0 ? float(n) - 1.0 : floor(t * float(n)));
  float t2 = t <= 0.0 ? 0.0 : (t >= 1.0 ? 1.0 : t);
  vec3 v1 = getData(i);
  vec3 v2 = getData(i+1.0);
  vec3 v0 = i > 0.0 ? getData(i-1.0) : 2.0 * v1 - v2;
  vec3 v3 = i < float(n) - 1.0 ? getData(i+2.0) : 2.0 * v2 - v1;

  return basis((t2 - i / float(n)) * float(n), v0, v1, v2, v3);
}


void main() {
  // Fill pixels based on x coordinate
  vec2 pos = gl_FragCoord.xy / u_size;
  gl_FragColor = vec4(getColor(pos.x), 1.0);
}
```

以上