カラースケール概要
データの値に応じて色を変えたい場合、下図のようなカラースケールを用いることがある。温度によって色分けされたサーモグラフィーの画面や、気温に応じて色分けされた地図などを思い浮かべれば良い。

カラースケールは大きく分けて以下の3種類に区分することができる。
-
- Categorical
-
- Diverging
-
- Sequential
このうち、Categoricalなカラースケールは単にRGBの配列を用意することで容易に実装できるが、残りのDiverging, SequentialはRGBの連続的な補間が必要になり、言語ごとに実装が必要になる。このような連続的カラースケールをWebGL(GLSL)で実装するのが本記事の内容。
d3.jsの場合
JavaScriptでは、d3.jsの一部であるd3-scale-chromaticに全3パターンのカラースケールが実装されている。連続的なカラースケールの場合、0-1の範囲の任意の実数値を受けて色を返してくれるような関数になっている。
これらのソースコード(例えば冒頭の虹色スケールd3.interpolateSpectral()に関するSpectral.js)を読んでみると、いくつか(10程度)のRGBの配列を直書きしたうえで、それらのRGBをSpline補間することで任意の実数値を受けられるようにしていることがわかる。
補間前のRGBの配列はd3.schemeSpectralのような形で取得することができるので、これを借りてきて補間処理を実装すれば、他の言語でもカラースケール関数が利用できることになる。WebGLのGLSLの場合はJavaScriptから値を渡すことができるので、d3から補間前の配列を直接渡せばOKということになる。
GLSLでの実装
以下の手順によってGLSLで連続的なカラースケール関数を利用する
-
- (JavaScriptで)d3-scale-chromaticからカラースケールの元となるRGB配列を取得する
-
- 上記配列は
#ff0000のようなhex形式なので、0から1の間のRGBの実数値配列に変換する
- 上記配列は
-
- 上記RGBの配列を
uniformとしてJavaScriptからGLSLに渡す
- 上記RGBの配列を
-
- GLSLでBーspline補間を実装する
補間処理はd3-scale-chromaticのJavaScriptでの実装をGLSLに翻訳すれば良い。
補間処理とJavaScriptから渡されたカラースケールを用いて本記事冒頭のようなカラースケールを表示するフラグメントシェーダのコードは以下の通り。
このShaderを利用して実際に描画を行うJavaScriptコードはこちらを参照。
# 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);
}
以上


