centroid 修飾子

実行結果

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

サンプリングの問題を解消する

前回は derivative 関数について扱いました。WebGL 1.0 時代は拡張機能として提供されていた derivative 関数も、WebGL 2.0 では標準機能となり、より使いやすくなったということで取り上げてみました。フラットシェーディングを題材にして紹介しましたが、それ以外にも使いみちはあると思いますので、工夫して活用してみてください。

さて、今回もやはり、GLSL のとある修飾子を扱ってみます。

その修飾子とは centroid 修飾子centroid qualifier)です。これは GLSL ES 3.0 から利用が可能になった、変数に対して付加する修飾子のうちのひとつです。

centroid とは直訳すると「重心」などの意味になるのかなと思いますが、果たしてこれがどのような効果を持つものなのかは、名前からはなかなか想像しにくいかなと思います。今回は、これをちょっと詳しく見ていきましょう。ただ、最初に断っておくと、私自身も若干説明する上で理解が正しくない場合も想定される内容なので、もし詳しい方がご覧になって間違いや表現が誤解を生みやすそうなところがあれば、教えていただけますと幸いです。

アンチエイリアスと深度テストの関係

さて、今回紹介する centroid 修飾子ですが、実はアンチエイリアスと深いかかわりがあります。

アンチエイリアスは、その名のとおり「エイリアス」を除去するための手法を広く指して使われる用語ですが、その手法には様々なものがあり、計算の原理のほか負荷の高さなども手法により様々です。

DirectX や OpenGL の場合は MSAA(Multi-Sample Anti-Aliasing)と呼ばれるアンチエイリアスの手法が標準でサポートされており、当サイトのサンプルなども、実行してみると特になんの指定もしていなくても、自然とアンチエイリアスが掛かったような状態でレンダリングされているはずです。(ブラウザやハードウェア構成によっては異なる場合があるかもしれません)

さて、このアンチエイリアスが、今回紹介する centroid 修飾子と実は結構深い関係があります。より正確には、アンチエイリアスの手法のひとつである MSAA の仕組みが抱える「ある問題」がまず先にあって、これを解消するために機能するのが他ならぬ centroid 修飾子です。これに対する正しい理解を得るために、まずは MSAA の仕組みから見ていきます。

MSAA は、ポリゴンなどのプリミティブと背景が切り替わるエッジ部分で発生する、ジャギーと呼ばれるギザギザを解消することができるアンチエイリアスの手法です。MSAA では、ピクセルの色を決定する際に、実際のスクリーンよりも 高解像度な深度バッファ を用いて深度テストを行います。よく、MSAA に「4x」とか「8x」みたいに数値が添えられているケースを目にしたことがある人も多いかと思いますが、この数値に相当する分だけ高解像度な深度バッファを内部的に生成して MSAA は実現されています。

高解像度な深度バッファが存在する、ということは、実際のスクリーン上の 1px をたとえば十字に四分割したような状態をイメージするといいでしょう。落ち着いて考えてみればわかるかと思いますが、1px が十字に四分割されているわけですから、深度バッファの大きさは縦横共に二倍になっているということですね。

この、実際のピクセルをさらに分割した、小さいマス目のことを サブピクセル と呼びます。サブピクセルが増えれば、それだけ深度テストを行う回数が増えていくことになるため当然ながらどんどん負荷が高くなっていきます。Chrome などの場合は、ブラウザ側でこのサブピクセルの数が指定される仕組みになっています。つまり、WebGL 側の設定ではなく、あくまでもそれを処理する Chrome の側で、サブピクセルの解像度を決めているわけです。ちなみに chrome://frags などで設定を変えることができますので、WebGL で行われる MSAA の品質は環境によって変化することになります。

さて、MSAA で高解像度な深度バッファが使われていることは理解できたとして、それがいったいどのような問題を引き起こし、かつ centroid 修飾子がどうやってそれを解消してくれるのでしょうか。

まずは、次の図を見てください。

ピクセルとカラーサンプル点

ブラウンのマス目の中に、黄色い点がぽつんと置いてあります。このブラウンのマス目がピクセルひとつ分を表しています。そして、真ん中に置かれている黄色い点は、これはシェーディングが行われるカラーサンプル点を表しています。

スクリーンには無数のピクセルがあるので、普段ディスプレイを眺めていてもそのひとつひとつに気を配ることはあまりないかもしれません。しかし、グラフィックスを描く過程では、当たり前ですがピクセルひとつひとつが厳密にしっかりと処理されていくことになります。WebGL などの 3DCG では、ポリゴンなどのプリミティブがラスタライズされるとき、このカラーサンプル点をベースにしてピクセルに色を塗るべきか否かを判断します。

例えばピクセルの大きさに対して考えると小さめな、次のようなプリミティブが描かれるとした場合、カラーサンプル点の上にエッジが掛かっていないピクセル、つまり黄色い点の上にポリゴンのエッジが乗っていないピクセルには、基本的には色が塗られないということになります。

小さなプリミティブが描かれようとするときの例

このように、カラーサンプル点をベースに処理を行うと、本来ならピクセルに若干エッジが入っているとしても色が付かない場合があるわけですから、当然ながらエイリアスが目立つ描画結果になるというわけですね。

これを解消するために MSAA を利用すると、どんなふうに処理内容が変化するのでしょうか。

先程も書いたように、MSAA ではサブピクセルと呼ばれる高解像度なバッファ上で深度テストが行われます。

そのようなサブピクセルのある状態を模したのが次の図です。

サブピクセルのイメージ図

先程までと違い、ピクセルのなかが更に細かく区切られているのがわかりますね。この小さな区画がサブピクセルです。

そしてサブピクセルの中にバツ印の模様がありますが、これが深度テストが実行される「深度テストのサンプル点」になります。

ちょっと紛らわしいので混乱しないようにしてください。丸い黄色い点は、シェーディングを行うためのカラーサンプル点です。バツ印のついている箇所がサブピクセルの深度テストが行われるポイントになります。

さて、ではそれらのことを踏まえて、もう一度プリミティブが描かれる場合のことを考えてみます。

状況を少しわかりやすくしたかったので先程よりも少し大きいですが、同じようなポリゴンをピクセルの上に配置してみます。

サブピクセルのイメージ図

さあここでは、赤い色の枠になっているピクセルに注目してみてください。

赤枠のピクセルでは、深度テストを通過しているサブピクセルが存在するにもかかわらず、残念なことにシェーディングが行われるカラーサンプル点にはエッジが届いていません。先程までの理屈なら、赤い色の枠の部分には色がつかないことになりますよね。しかし MSAA では、この深度テストを通過したサブピクセルの個数に応じて色が混ぜ合わせる処理が行われるようになります。

結果、深度テストを通過したサブピクセルの数に応じて微妙に色が付くピクセルが存在することとなり、エイリアスが軽減されるというわけです。

本来なら色がつかないはずのピクセルにも微妙に色が乗るようになる、とても良くできた仕組みですよね。

しかし、ここでよく考えてみてください。

先程の図をよく見るとわかると思うのですが、本来シェーディングが行われるカラーサンプル点は、深度テストを行ったサブピクセルより 外側 にありますよね。バツ印のところにエッジは乗っているものの、カラーサンプル点はエッジを越えたところにあるわけです。

すると、主にわかりやすい例で言えばテクスチャ座標がそれに相当するのですが、本来の頂点の持つ情報よりも「外側の情報を参照する割合が一定以上含まれる場合がある」ということになりますよね。もともと頂点が持っていたテクスチャ座標が 1.0 とかだったりすると、その外側の数値を参照することになるピクセルが出てきてしまう、ということです。1.0 よりもわずかにでも超過する値ということになると、もしテクスチャ側のサンプリングの設定が gl.REPEAT とかになっていたりすると、反対側の、テクスチャ座標 0.0 に相当する色が混入してくるなんてことが起こります。

実際にそれを再現したのが、今回のサンプルの実行結果の左側半分。

以下の画像は、それをわかりやすく拡大して抽出したものです。ちなみに右側は、今回のテーマである centroid 修飾子を使っている場合です。

MSAA により頂点情報が変化した結果の例

これを見ると、左側は、青い部分に反対側の黄色が混じって乗ってきていたり、赤い部分に反対側の緑の色が混じってしまっていたりするのがわかりますね。

テクスチャが全体的に似たような配色の場合はそれほど目立たない場合が多いかと思いますし、サンプリングの設定如何では、ほとんど気にならない場合もあります。しかし結構色の差分が大きかったりすると、どうにも違和感のある結果になってしまうわけですね。

実際に動いているサンプルで見たほうが、もしかしたらわかりやすいかもしれません。

では centroid とはなんなのか

上記で見てきたように、MSAA と呼ばれるアンチエイリアスの手法では、ポリゴンなどのプリミティブが本来持っている値の範囲を越えた補間が起こる可能性があり、主にテクスチャの参照などでこの効果が意図しない結果を生む場合があります。これを解消することができるのが、他ならぬ centroid 修飾子です。

修飾子、ということからもわかるかもしれませんが、これはあくまでも変数を宣言する際に、その変数の挙動を指定するために使われるものです。つまり、変数単位で効果を付与するかしないかを選ぶことができるわけですね。

今回のサンプルでは、テクスチャ座標の補間を制御したいので、シェーダ内でテクスチャ座標に相当する変数に対してのみ centroid 修飾子を付加しています。

そしてこの centroid 修飾子を付加された変数は、MSAA でサブピクセルによる深度テストが行われた結果に対して従来とは異なる挙動で処理が行われます。より具体的には、これまではサブピクセルでの深度テストの結果を均等に混ぜ合わせていた(ピクセルの中央部分でシェーディングしていた)のに対し、深度テストを通過したピクセルの中央部分を中心としたシェーディングが行われるようになります。

centroid 修飾子を利用した場合のサンプル点

本来なら黄色い点の箇所にあるはずのカラーサンプリングの位置が、赤い点のある位置、すなわち深度テストに合格したサブピクセルの中心に移動するわけです。

これにより、範囲の外側にあるサブピクセルの影響が打ち消され、純粋にプリミティブの内部でシェーディングが完結し、結果、範囲外を参照するような値が使われることがなくなるという仕組みです。

実際のシェーダのコードも、修飾子を付与するだけなので簡単です。

centroid 修飾子を使用した例

#version 300 es
layout (location = 0) in vec3 position;
layout (location = 1) in vec2 texCoord;

uniform mat4 mvpMatrix;

centroid out vec2 vTexCoord;

void main(){
    vTexCoord = texCoord;
    gl_Position = mvpMatrix * vec4(position, 1.0);
}

これを見るとわかるように、今回はテクスチャ座標に対してのみ修飾子を付与するようにしています。

ちょうどコードの中段あたりにある out 修飾子の手前に centroid 修飾子が使われているのがわかりますね。これは頂点シェーダのコードですので、頂点シェーダから出力されたテクスチャ座標が、フラグメントシェーダに渡る間の補間について centroid の効果が有効になり、フラグメントシェーダ側でこれを参照した際に範囲外の値が使われることがなくなる、というわけですね。

コード自体は非常に簡素な変更のみで済むのですが、どちらかというと概念のほうの理解が今回は大変ですね。

まとめ

さて、今回は特殊な修飾子について見てきましたがいかがでしたでしょうか。

アンチエイリアスの手法に関することなど、直接 WebGL のコーディングとは関係のない部分の知識を必要とする部分が大半なので、冒頭にも書いたように私の理解がまず間違っている可能性もあるので、もし間違いや誤った表現があるのを見つけた有識者の方は教えていただけるとうれしいです。※テキストを見た @c5h12 (Pentan) さんがいろいろ教えてくれましてテキストを一部、正しく修正することができました。ありがとうございます!

近年の 3DCG は様々な技術を下地に成り立っており、我々はそれらの技術の恩恵を知らず知らずに受けています。アンチエイリアスのような機構は、プログラマでなければその言葉さえも知らないという一般の方が多いと思いますが、それでもこうしてその原理やそれに基づく様々な技術について知ることは非常に刺激的です。

WebGL 1.0 の頃は、今回のような centroid 修飾子を利用した処理というのはそもそも実現することはできませんでした。ただしロジックさえ理解できていれば、これを自前でシェーダを書いて、同様のことを実現することも不可能ではないということがわかると思います。今は徐々に WebGL 2.0 も使える環境が整ってきていますので、無理せず修飾子で対応するというのが正解かなと思います。

実行結果のサンプルを見たほうが理解が進みやすいというのもあると思うのですが、ブラウザやハードウェア構成の違いによって、必ずしも同じ結果にならない場合があるかと思いますのでその点は注意して利用してみてください。

実際に動作するサンプルは以下のリンクから確認できます。

entry

PR

press Z key