描画結果から色を取得する readPixels

実行結果

今回のサンプルの実行結果

描画結果を知るための手法

前回は MRT(Multiple Render Targets)を利用して異なるフレームバッファに複数の情報を書き出し、それらから異なるロジックでエッジを検出する手法について解説しました。

MRT は非常に応用範囲の広いテクニックで、工夫次第であっと驚くようなレンダリング結果を得られる可能性が多分に含まれています。ぜひがんばって挑戦してみてください。

さて今回ですが、意外と今まで扱ってなかったなと思ったので readPixels を用いた簡単なサンプルを作ってみます。

予備知識の無い方のために簡単に説明すると readPixels はその名前からも想像できるとおり、ピクセルから色を読み込むという処理が行えるメソッドです。ここで重要なのは、この「読み込むピクセル」というのが、いったいなんのピクセルを指しているのかを勘違いしないようにすることでしょう。

果たして読み込まれる対象となるピクセルとは、なんなのでしょうか。描画結果? それともテクスチャのテクセルでしょうか、はたまた画像の画素なのか?

そのあたりも踏まえつつ、詳しく見ていくことにしましょう。

Current という概念

WebGL では、様々なデータが利用される前の段階で事前にバインドなどを経てコンテキストに紐付けられます。

たとえばそれは頂点の情報を格納したバッファであったり、あるいはイメージデータを格納したテクスチャであったりします。これと同様に、WebGL ではフレームバッファと呼ばれるレンダリングが行われる描画領域についても、明示的にどれを利用するのか指定する仕組みになっています。

WebGL のプログラムでは、必ずしもフレームバッファの生成処理を行うとは限りません。なぜなら、既定のフレームバッファは WebGL コンテキストの初期化と同時に自動的に生成され、もともとコンテキストに紐付いた状態になっているからですね。デフォルトで生成されるこのフレームバッファの他に、別に描画領域を用意したい場合に、意図的にフレームバッファを生成することになります。

そして、このフレームバッファにもまた、テクスチャや VBO などのバッファ類と同様に「バインドする」という手順があります。利用するフレームバッファを切り替えたい場合、あらかじめ生成してあったフレームバッファオブジェクトを WebGL コンテキストに対してバインドしてやることで、描画命令の結果が、その時点でバインドされているフレームバッファに書き込まれていくようになるのですね。

話をわかりやすくするために、以下、今現在バインドされているフレームバッファのことを、Current のフレームバッファと呼ぶことにします。また、WebGL コンテキストが初期化された直後は、当然ながら既定で自動的に生成されるフレームバッファが Current のフレームバッファということになります。

さて、それではここで考えてみましょう。

この Current のフレームバッファに何かのレンダリングを行ったとします。そして、その描画結果がどんなふうになったのか、プログラムから参照したい場合にどのような処理を行えばいいでしょうか。

たとえば、既定のフレームバッファではなく事後的に生成したフレームバッファであれば、そのフレームバッファにアタッチしたテクスチャを参照すれば、当然色を値として取得することは可能です。しかしこの場合も、色情報を取得することができるのは GLSL 側、つまりシェーダ内だけであり javascript からその値を知ることはできません。

こんなときに役に立つのが、今回紹介する readPixels メソッドです。

このメソッドは、Current なフレームバッファから描画されている色情報を読み出すことができます。この Current な、というのがポイントでしょう。

つまり readPixels メソッドは「任意のバッファ」から値を読み出すことができるわけではないのです。あくまでも、Current なフレームバッファにカラーバッファとしてアタッチされているバッファから色を読み出します。フレームバッファにテクスチャがカラーバッファとしてアタッチされている場合は、そのフレームバッファが Current であればきちんとテクスチャから色が読み出されてきます。

また、前回までに紹介していた MRT では、フレームバッファに複数のカラーバッファをアタッチできますが、MRT を用いている場合には必ず gl.COLOR_ATTACHMENT0 としてアタッチされているフレームバッファの、カラーバッファから色の読み出しが行われます。当然ながら、色の参照はあくまでもカラーバッファに対してのものであり、深度バッファを狙い撃つなどといったことも不可能です。

なんとなく語感的に自在にバッファを指定して色を読み出せるような雰囲気がありますが、読み出しの対象となるバッファの定義はしっかり決まっているので、その点には注意しましょう。

矩形としてデータの塊を読み出す

さて、それでは実際に readPixels をどのように利用すればいいのか、見ていきます。

まず前提として理解しておいてほしいのは、このメソッドは色をデータのかたまりとして読み出すということです。普通、色を参照する処理を考える場合は 1px ずつ色を読み出す感じを想像すると思いますが、この readPixels というメソッドはそこがちょっと違います。

手順としては、事前に型付き配列で取り出した色を格納するための入れ物をまず先に用意します。そして readPixels を呼び出す際に、引数にこの型付き配列を渡して呼び出しを行います。

引数として渡した型付き配列は readPixels により更新されますので、呼び出し完了後に型付き配列の中身を見ると、読み出した色の情報を参照することができます。こういうポインタ渡すみたいな処理の仕方はあまり javascript では登場しないものなので、ちょっとわかりにくいかもしれませんね。簡単に言うと、型付き配列というカゴを作って店員さんに渡すと、勝手に中にいろいろ商品を詰め込んでおいてくれる、みたいな感じでしょうか。

注意すべき点として、型付き配列ならなんでもいいということではなく、利用すべき型付き配列の種類が決まっていることに気をつけましょう。

基本的にカラーバッファには 8 ビットでデータが格納される仕組みになっています。8 ビットとは、つまり 1Byte であり、0 から 255 までのデータが格納できます。ですから、これを読み出した結果を格納する型付き配列の型は、これに倣ったものでなければなりません。

具体的には以下のようにすればいいですね。

型付き配列を初期化しておく

// unsigned int で入れ物を作る
var u8 = new Uint8Array(4);

このように、取得されるデータに対して適切な型付き配列を準備してやる必要があるんですね。今回の場合は Uint8Array を使うことになります。

また、上記の例では初期化する際に要素の数を 4 つとしていますが、このようにした場合は 1px 分のデータが格納できる入れ物ということになります。よく考えて見れば当然のことですが、RGBA の各要素がそれぞれ 1Byte なので、ピクセル数の 4 倍の長さの配列が必要となるわけですね。

入れ物が準備できたら、この型付き配列にデータを格納するために readPixels を呼び出します。

引数の指定がここでも大事になりますので、気をつけましょう。

readPixels の呼び出し

// 矩形の指定では原点が左下である点に注意
gl.readPixels(矩形開始横位置, 矩形開始縦位置, 矩形の幅, 矩形の高さ, gl.RGBA, gl.UNSIGNED_BYTE, 型付き配列);

引数が若干多いですが、割とよくある矩形選択系の処理と同じような感じなので、落ち着いて見ていけば問題無いでしょう。

WebGL ではレンダリング領域の原点が左下となるので、矩形を定義する際の開始位置が左下を基準にしたものになります。

ですから、たとえば左下の隅にある 1px の色情報を取得する場合には、次のように引数を指定すればいいですね。

左下隅の 1px の色を取得する場合

gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, Uint8Array);

第五引数と第六引数の指定は、原則として変更しません。これはテクスチャを含むカラーバッファのフォーマットが一般には 8 ビットの RGBA で構成されているからです。ですから第五引数には gl.RGBA を、第六引数には gl.UNSIGNED_BYTE を指定するのですね。

もし拡張機能などを利用し浮動小数点数テクスチャなどに描画を行っている場合は、それに沿った指定を行う必要があるでしょう。私はまだ試していませんが、環境によっては引数の指定を適切に行うことで、浮動小数点数のデータとして取得が可能なようです。

矩形領域を格納する配列は何次元?

今回の readPixels は矩形領域を対象に色をブロックとして読み出してきます。1px ではなく、複数のピクセルを矩形選択した場合、それが格納される型付き配列の中身はどのような構造になるのでしょうか。

答えを書いてしまうと、型付き配列は常に一次元、つまり横にずらっとデータが並んだような構造で情報が格納されます。Canvas2D でイメージデータなどを扱ったことがあれば、想像しやすいかもしれませんね。

仮に色を読み出す領域が矩形で複数の行列を参照するような場合でも、配列が複数次元になったりすることはありませんので、それを踏まえた処理を記述するようにしましょう。

まとめ

今回のサンプルでは、矩形の開始位置となる座標をマウスカーソルから取得して、1 x 1 の領域を対象に色を取得するようになっています。実際にサンプルを実行してマウスカーソルを動かすと、カーソルがある地点の色が取得されてページが更新される様子を確認できると思います。

あまり WebGL や 3D の実装に慣れていない場合は、こんなふうに描画領域から色を取得することで何が嬉しいのか、想像がつかないという方もいるかもしれません。たとえば、深度値をテクスチャに焼いて、別のパスでその深度値を読み出し処理を行うであったり、単純に明るさを取得してから分岐する処理を組むであったり、あるいは取得した色情報をもとにして動的にサウンドデータを構築してみたりと、いろいろと使い道は考えられます。

仮に画面には表示されていなくても、フレームバッファが Current でさえあれば色は読み出せるので、オフスクリーンのフレームバッファから色を取り出し、なにかやるなんてこともできますね。応用次第で、かなりいろいろな用途に使える可能性があると思います。

そんなわけで、シェーダで高速にデータを書き出し、それを使って何か別の処理を行うという GPGPU 的なことをやる場合には、実は結構活躍する場面の多いメソッドでもあります。ただし注意点として readPixels は比較的重い処理であることを念頭に置きましょう。一般に、CPU と GPU との間でデータをやり取りすることはボトルネックになりやすい処理です。シェーダをアタッチしたプログラムオブジェクトを頻繁に切り替えたりするだけでも、簡単に重いプログラムができあがってしまうのが 3D プログラミングです。今回の readPixels はシェーダを利用して高速に描画した結果を CPU 側で参照できる便利なメソッドではありますが、調子に乗ってループ処理にこれを組み込んだりすると、容易にパフォーマンスの低下を招きますので注意しましょう。

今回も、サンプルは以下のリンクから参照できます。

entry

PR

press Z key