フラグメントシェーダからCPUへのデータ受け渡し

Last updated:

今回は、私が2010年代後半に使っていたWebGLに関する小技を紹介してみたい。

WebGLとOpenGL ES Shading Language(GLSL ES)を用いると、ウェブブラウザ上で比較的簡単にグラフィック処理を行うことが出来る。 せっかくウェブブラウザ上でGPUリソースを利用するインターフェースがあるので、これを用いて数値計算をやってみたくなるのだが、 WebGLはGPGPU(General-purpose computing on graphics processing units)を積極的にサポートしているわけではないので、いくつか工夫が必要になる。 今となっては、より便利なインターフェースが存在するので、実用的な技術ではないが、データ処理の手法として面白いので紹介してみたい。

フラグメントシェーダのデータを色に変換する

もともとフラグメントシェーダの役割は、各ピクセルで表示するべき色を決めることである。 この処理をプログラミングしてやることで、表示されるポリゴンに好きな色を指定したり、グラデーションをつけたりすることもできる。 そこで色を指定するための計算を、何かしらの数値計算アルゴリズムに置き換えてやれば、ピクセルの数だけ並列に数値計算を行うことが出来る。

フラグメントシェーダでの計算結果は、基本的には画面上に色として表示される。 一方、表示された色はWebGLのreadPixelsという関数を使うとJavaScript側で読みだすことも出来る。 なので、渡したいデータを色として表示して、それを読みだしてやれば、フラグメントシェーダからCPUへデータを受け渡すことが出来る。

色はRGBAの値で指定されるが、フラグメントシェーダではRGBAの値を0–1のfloatで指定するのに対して、JavaScript側では、RGBAの値を8ビットのunsigned intとして受け取るので注意しよう。

32ビットのintを色に変換するには、次のように8ビットごとにバイナリを取り出して、0–1のfloatに変換してやればよい。

vec4 intToVec4(int num) {
    int rIntValue = num & 0x000000FF;
    int gIntValue = (num & 0x0000FF00) >> 8;
    int bIntValue = (num & 0x00FF0000) >> 16;
    int aIntValue = (num & 0xFF000000) >> 24;
    vec4 numColor = vec4(float(rIntValue)/255.0, float(gIntValue)/255.0, float(bIntValue)/255.0, float(aIntValue)/255.0);
    return numColor;
}

同様に32ビットのuintも次のように色に変換できる。

vec4 uintToVec4(uint num) {
    uint rIntValue = num & 0x000000FFu;
    uint gIntValue = (num & 0x0000FF00u) >> 8;
    uint bIntValue = (num & 0x00FF0000u) >> 16;
    uint aIntValue = (num & 0xFF000000u) >> 24;
    vec4 numColor = vec4(float(rIntValue)/255.0, float(gIntValue)/255.0, float(bIntValue)/255.0, float(aIntValue)/255.0);
    return numColor;
}

32ビットfloatの場合は、ビット列を変えずにfloatをuintに変換した後、uintを色に変換してやればよい。 ただし、ここで使うfloatBitsToUintはWebGL2 (GLSL ES 3.00)以降でしか使えないので、WebGL1 (GLSL ES 1.00)の場合はこれをFloatの計算として実装する必要があり非常に難しい。

vec4 floatToVec4(float val) {
    uint conv = floatBitsToUint(val);
    return uintToVec4(conv);
}

色をデータとして読みだす

画面全体の色を、int、unsigned int、floatの配列としてJavaScript側で読み出す関数は次のような感じになる。

まず、読み出しに必要な分だけバイナリ配列の領域を用意して、そこに色データを読み出す。 必要なサイズは、ピクセル数×8ビット×RGBAである。 これを32ビットごとのまとまりとして認識し直せば、もともとのデータが得られる。

function readInt32Array() {
  let pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4);
  gl.readPixels(
    0,
    0,
    gl.drawingBufferWidth,
    gl.drawingBufferHeight,
    gl.RGBA,
    gl.UNSIGNED_BYTE,
    pixels
  );
  return new Int32Array(pixels.buffer);
}

function readUint32Array() {
  let pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4);
  gl.readPixels(
    0,
    0,
    gl.drawingBufferWidth,
    gl.drawingBufferHeight,
    gl.RGBA,
    gl.UNSIGNED_BYTE,
    pixels
  );
  return new Uint32Array(pixels.buffer);
}

function readFloat32Array() {
  let pixels = new Uint8Array(gl.drawingBufferWidth * gl.drawingBufferHeight * 4);
  gl.readPixels(
    0,
    0,
    gl.drawingBufferWidth,
    gl.drawingBufferHeight,
    gl.RGBA,
    gl.UNSIGNED_BYTE,
    pixels
  );
  return new Float32Array(pixels.buffer);
}

あとがき

この方法は、とくにWebGL2が登場する前のWebGL1の時代に、GPGPUをやるためのトリックとしてよく知られていた手法だ。 WebGL2が登場してからは、Transform Feedbackがサポートされるようになり、色読み出しを行わなくてもGPU-CPU間のデータの受け渡しができるようになっていたのだが、 Transform FeedbackはVertexシェーダからのデータの受け渡しにしか対応しておらず、私自身はあまり使いこなすことができなかった。 GPUはそもそもピクセル単位での計算を得意としていて、そこで並列計算した結果をガサッと読みだすことが出来たら便利、という感覚があり、大量に頂点を生成してVertexシェーダで計算する、というのがどうもしっくり来なかったのである。 今となっては、WebGPUのcompute shaderが使えるようになり、より抽象化されたインターフェースでウェブブラウザ上でもGPGPUができるようになって来ている。 そのため、実用上はほぼ将来性のない手法なのだが、GPUの特性とグラフィックスAPIの制約を考慮した、面白いハックとして今でも参考になると思う。

関連記事

浮動小数点数の丸め誤差と誤差伝搬

浮動小数点数を使った数値計算では、必ず丸め誤差が発生します。特に複数回演算操作を行った後、誤差がどう伝搬するかはそれほど明らかではありません。この記事では、IEEE 754-2019をもとに浮動小数点数の定義を確認し、誤差推定の基本的な方法について議論します。

APDLコマンドを用いたAnsys Workbench解析結果のエクスポート

Ansys Workbenchで得られた解析結果をエクスポートしたい場合、APDLコマンドを使用することで、より柔軟にフォーマットを指定して出力することが可能です。本記事では、熱解析のモデルデータおよび解析結果をCSV形式でエクスポートする方法を紹介します。