異方性フィルタリング
今回のサンプルの実行結果
テクスチャフィルタリング
前回は VAO(vertex array object)について解説しました。WebGL の拡張機能として実装されている VAO ですが、それを活用することで頂点情報をうまくパッケージ化することができ、簡潔なコードの記述が実現できるのでしたね。
あくまでも拡張機能なので、どんな環境でも完璧に動作するとは限りませんがそれは拡張機能に限った話でもないですし、ある程度、プログラムの動作環境を特定している場合などには積極的に使っていきたい機能だと言えるでしょう。
さて、今回も前回に引き続き、拡張機能を活用した事例のひとつとして異方性フィルタリング(anisotropic filtering)をやってみたいと思います。
残念ながら、本テキスト執筆時(2014年4月現在)では、本拡張機能はベンダープレフィックス付きでしか有効化できないようです。しかし、思いのほか利用してみた場合の結果には違いが出ましたし、せっかくなのでご紹介しようと思います。いずれはプレフィックス無しで普通に使えるようになるでしょうし、興味のある方は試してみてください。
そもそも異方性フィルタってなに?
実際の実装を見る前に、そもそも異方性フィルタリングってなんなの? というところから見ていきます。
異方性フィルタリングは、テクスチャマッピングを行う際に使われる補間方法のひとつです。同様のテクスチャフィルタリングの技術には、他にもバイリニアフィルタリングや、トライリニアフィルタリングなどがあります。
バイリニアはその名のとおり、二次元でテクセルに対して線形補間を行う手法です。トライリニアは名前から察するともうひとつ次元が増えるように思ってしまいますが、これは単純に三次元で――ということではなく、複数のミップマップを用いて補間を行う方法のことを言います。
WebGL ではミップマップも当然サポートされています。ミップマップを設定したり、あるいは自動的にミップマップを生成したりするためのメソッドが標準で実装されており、テクスチャパラメータを適切に設定することでミップマップを利用した補間を行うことが可能です。このあたりは、以前のテキストで解説したテクスチャパラメータの設定を見てもらえればイメージしやすいと思います。
参考:テクスチャパラメータ
いわゆる gl.NEAREST
と gl.LINEAR
の違いだけで考えてみると、テクスチャをマッピングする際に線形補間を行うのかどうか、という点が異なっていました。線形補間を行ったかどうかという単純な違いだけでも、それなりにレンダリング結果には差が出ます。これは上記のテキストのサンプルを実際に実行してみれば実際に見て確かめることができるでしょう。線形補間を行った方が当然高品位です。
そして、バイリニアやトライリニアといったテクスチャフィルタリングのさらに上を行くのが今回解説する異方性フィルタリングです。
ものすごくざっくり言えば、異方性フィルタリングでは視線が考慮されます。バイリニアやトライリニアは、あくまでも平面上でどのように補間を行うのかという点においてのみ考えますが、異方性フィルタリングの場合にはここに視線という要素が加わります。
異方性フィルタリングは、先述のバイリニアやトライリニアのフィルタリングよりも優れた結果を得られますが、ここで注意したいのは、レンダリングするシーンの状況次第ではそれほど顕著な違いが表れない場合がある点です。どうしてそのようなことが起こるのでしょうか。次項から、さらに詳しく見ていきましょう。
ミップマップとテクスチャフィルタリング
異方性フィルタリングに触れる前に、まずミップマップの仕組みをおさらいしてみます。
ミップマップはあらかじめ縮小されたテクスチャを用意しておき、テクスチャマッピングが行われる段階で、そのときの縮小具合に応じて適切なサイズのテクスチャを参照しながらマッピングする方法です。
ミップマップを用いると、あらかじめ縮小されたテクスチャが用意されているため補間処理が最適化され、高速化や負荷の軽減が期待できます。また、ミップマップ用の縮小されたテクスチャは自動で生成することもできますが、あらかじめ縮小時用に品質の高いミップマップ専用画像を用意しておき適用させることもできます。
ただし、ミップマップも良いことづくめというわけではなく、利用する場合には複数のテクスチャデータをメモリ上に展開することになってしまいますのでメモリを圧迫します。この点は確実にデメリットになります。当然と言えば当然ですね。
またミップマップはその仕組み上、解像度の異なる複数のテクスチャが WebGL 側で自動的に使いわけられることになります。ですから、要はサンプリングの際に同一面上でまったく別のテクスチャが使われる場合があり、異なるミップマップが適用されるちょうど境界部分では不自然にミップレベルの違いが露呈してしまう場合があります。
ミップレベルの違いが露呈してしまうイメージ図
さて、このミップマップですが、バイリニアフィルタリングやトライリニアフィルタリングではどのように使われているのでしょうか。
まずバイリニアの場合、二次元で単純な線形補間を行うのでしたね。カメラに非常に近いところにあるモデルをレンダリングする場合には、単純に隣接するテクセルで色を補間することができるでしょう。しかし、カメラから遠いところにあるピクセルの色を決定するためには、先ほどとは逆により広範囲なテクセルを参照して補間してやらなくてはいけません。遠くなればなるほどパースがきつくなるわけですから、補間のために参照するべきテクセルは比例して増えていくことになります。
このことから、あらかじめ縮小されているミップマップテクスチャを参照することで、結果的に補間作業が軽減されつつ品質の高い結果が得られるようになるというのがわかりますね。
ではトライリニアの場合はどうでしょうか。
トライリニアの場合は、本来参照するべきミップレベルと隣接するもうひとつの別のミップレベルも同時に参照します。これにより、カメラから近くても遠くても、ある程度一定の補間結果が得られるようになり、ミップレベル変化の境界部分にも不自然さはなくなります。負荷はもちろん高くなりますが、バイリニアと比較して高品質な補間が行われるのはこのためです。
トライリニアのイメージ図
それでは、今回の肝である異方性フィルタリングの場合はどうなのでしょうか。
今まで見てきたバイリニアにしてもトライリニアにしても、ミップマップを活用しつつ、あくまでも最終的には二次元の座標上で線形補間を行うフィルタリング技法でした。これは言い換えると、レンダリング対象となる面に対して、視線がまっすぐ垂直に向かっている場合に限って正しい補間結果を得られるということでもあります。そうでない場合、つまり面に対して視線が鋭角に向かっている場合にはどうしてもサンプリングするポイントにぶれが生じてしまう可能性が高くなります。
異方性フィルタリングでは、視線を考慮した上でサンプリングするべきテクセルを決定します。視線の向きが面に対して鋭角であればあるほど、サンプリングされる点の置かれる範囲が広くなります。
異方性フィルタリングのイメージ図
上記の画像でピンク色で書かれている矢印がサンプリングされる位置を表しています。このような視線に応じて適切な補間を行うことで、より自然な仕上がりになるわけですね。
さらに、異方性フィルタリングではピンク色の矢印の数、つまりサンプリングされるポイントの数を任意に調整できます。先ほどの画像ではピンク色の矢印はふたつでしたが、ハードウェアが許す範囲のなかでさらに増やすこともできます。当然、数が多ければ多いほど良質な結果が得られます。
ちなみに、異方性フィルタリングではこのサンプリングする点の数を 2X
というように X を使って表現することがあります。これは、ひとつの点の色を決めるために、異方性フィルタリングによってふたつの点を参照することを表します。※正確には「ふたつの地点」です。最終的にはこのふたつの地点からさらに周囲のテクセルが読み出されてバイリニアフィルタなどが適用されます。
今回のテキストのサンプルは、異方性フィルタリングを使用しているものと、していないもの。そして、異方性フィルタリングを使用した上で、サンプリング点の数を変更しているもの、これらを一度にスクリーン上にレンダリングしています。
サンプルの実行結果詳細
左半分には異方性フィルタリングを無効にした状態、つまり従来通りにテクスチャパラメータだけをいじってレンダリングしています。さすがに線形補間を行っているほうがだいぶキレイですね。
右半分には異方性フィルタリングを適用していますが、上段が 2X で下段が 16X です。これは静止画なのでわかりにくいと思いますが、実際に動作するものを見れば格段に 16X のレンダリング結果が優れていることがわかるでしょう。
実装方法を見ていく
かなり前置きが長くなりましたが、実装自体はそれほど難しくありません。
VAO や float texture のときと同様に、まずは拡張機能を有効化します。今回は getExtension
メソッドの引数には EXT_texture_filter_anisotropic
という文字列を渡します。
拡張機能を有効化する
// 拡張機能を有効にする
var ext, maxAnisotropy;
ext = gl.getExtension("EXT_texture_filter_anisotropic") ||
gl.getExtension("WEBKIT_EXT_texture_filter_anisotropic") ||
gl.getExtension("MOZ_EXT_texture_filter_anisotropic");
if(ext == null){
alert('texture filter anisotropic not supported');
return;
}else{
maxAnisotropy = gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
console.log(maxAnisotropy);
}
異方性フィルタリングは、まだプレフィックス付きでないと利用できない場合がありますので、初期化が少しだけ面倒な感じになってしまっていますがやっていることは今までと同じです。
また、サンプリングする地点の数を任意に指定できると先ほど書きましたが、この指定できる最大値も初期化と同時に調べておきます。ここで得られる数値は環境によって変わってきますが、恐らく 16 程度まではほぼいけるのかなという気がします。
サンプリングする地点の最大値を調べる場合は gl.getParameter
メソッドに、拡張機能のオブジェクトが持つ定数を渡します。非常に長くて辟易しますが ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT
を渡せばいいわけですね。
今回のサンプルでは、恒常ループの中で四つのシーンをレンダリングします。先ほど画像で登場した四分割された状態ですね。
これには、ビューポートを正しく設定したうえで、各種テクスチャ関連の設定を切り替えつつ処理しています。ループの中の該当する部分だけコード抜粋してみます。
恒常ループ内のコードを抜粋
// 左上半分:異方性フィルタリングなし + gl.NEAREST
gl.viewport(0, 256, 256, 256);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, 1);
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
// 左下半分:異方性フィルタリングなし + gl.LINEAR
gl.viewport(0, 0, 256, 256);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, 1);
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
// 右上半分に異方性フィルタリングあり + 2X
gl.viewport(256, 256, 256, 256);
gl.texParameteri(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, 2);
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
// 右上半分に異方性フィルタリングあり + max
gl.viewport(256, 0, 256, 256);
gl.texParameteri(gl.TEXTURE_2D, ext.TEXTURE_MAX_ANISOTROPY_EXT, maxAnisotropy);
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);
ビューポートを設定すると、スクリーン上の任意の矩形領域に対してのみレンダリングを行うことができます。今回の場合はスクリーンとなる canvas が 512 ピクセル正方形です。ですから、四分割するのであれば 256 ピクセルの矩形に対してそれぞれレンダリングを行えばいいですね。ただし注意すべきはその座標の指定方法です。
ビューポートを設定する gl.viewport
メソッドでは、第一引数と第二引数に矩形の開始位置を指定しますが、これは 左下隅 が原点です。ここは一般的な二次元座標のイメージと上下が反転しているので注意しましょう。
第三引数と第四引数には、矩形領域の幅と高さを指定します。これは簡単ですね。
一度目の描画では texParameteri
メソッドでフィルタリングに gl.NEAREST
を指定しています。二度目の描画では同様に gl.LINEAR
を指定し、線形補間を有効にしています。
異方性フィルタリングを有効化する場合には、同メソッドの第二引数に拡張機能オブジェクトが持つ定数である ext.TEXTURE_MAX_ANISOTROPY_EXT
を渡します。さらに、第三引数にはサンプリングする地点の数を指定します。
三度目のレンダリングではここで 2 を指定しているので、異方性フィルタリングの 2X 版ということになりますね。最後の四度目のレンダリングの際には、初期化時に取得してあったサンプリング地点の最大数を指定するようにしています。
テクスチャパラメータの指定について
上記で登場する texParameteri
メソッドに、非常に似た名前のものとして texParameterf
メソッドがあります。
MDN などにあるソースでは、異方性フィルタリングに関する設定に末尾が f になる texParameterf
のほうを使っています。つまり float とみなして値を設定するようになっているわけですね。これは、OpenGL の実装にならってのことだと思いますが、一応ちょっと調べてみた感じだと仕様の上ではどちらでも大丈夫なようです。
今後は、もしかしたら float でしか受け入れてくれない形に仕様が変わるかもしれません。ないとは思いますが一応。
まとめ
異方性フィルタリングを説明するために、ミップマップやそのほかのテクスチャフィルタリングについても解説する必要があり、すごく長いテキストになってしまいました。
ミップレベルが切り替わっている部分で境界が露呈してしまう現象や、実際にそれぞれのフィルタリング方法でどのくらいレンダリング結果に差が出るのかなど、実際に動くサンプルを見てみないとわかりにくい概念が今回は多かったですね。時間をかけてでもゆっくりあせらず理解していただければと思います。
また、冒頭でも書きましたが異方性フィルタリングは拡張機能という位置付けなので、必ずしも動作するとは言い切れません。こればかりは環境依存です。しかし、この異方性フィルタリングは奥行きが広く、またパースの掛かり方が強い部分ほど恩恵を受けられます。屋外のシーンや、狭い空間を奥に突き進んでいくシーンなどでは、あるのとないのとではかなり見た目が変わってくるでしょう。
逆に、あまり奥行きがないようなシーンでは、それほど結果に違いは出ません。利用すべきシーンをしっかりしぼって、賢く実装したいものですね。