週アレ(16) GLSLでディザパターン(BayerMatrix)

0 0
上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

週に一回アレしてアレしてたハズの記事:十六回目

ご無沙汰してました、もう週一回とか1ミリも関係ないのだけど、少し整理した技術系の話はこのカテゴリで通すことにしたのでこのタイトルになります。

今回はディザリングについてです。
具体的には、ディザのパターン生成(パターン配列としてはBayerMatrixを使用しました)と、パターンの複雑さと実際のディザの見た目について触れます。

今回のは若干マニアックな話題なんですが、昨今の3Dゲームだと半透明表現としてたまに(普通に?)使われているところではあるディザ半透明について、そもそもディザってどういう手法があるのかなと思って調べた記事になります。
雑な前置き

昨今のゲームの3D表現は飛躍的に向上してはいますが、未だに半透明オブジェクトの描画は処理的にも表現的にも鬼門とされています。
特に(主に)ディファードシェーディング(遅延シェーディングとも : see Wiki)と呼ばれる手法では半透明オブジェクトはまず描画できないので、完全に別個で半透明パスとしてレンダリングパスに組み込まれていたりとかして、苦労の後が見えたりしますし…。

これらディファードシェーディング×半透明の問題点の本質は、半透明オブジェクトは後ろ側も見えてしまうため、ZプリパスやGバッファなど「各ピクセルに対応するマテリアルは一つ」という前提を置いているアルゴリズムがすべて破綻してしまうことです。

一応、通常のディファードシェーディングを行った後に、デプステストをしながら半透明オブジェクトをフォワードシェーディングで描画すればそれっぽく表現できますが、次のような問題点があります。

1. ライトプリパスなど先述の「各ピクセルに対応するマテリアルは一つ」と前提を置いているアルゴリズムが使えず、従って半透明オブジェクトだけうまくライティングできないなどの問題が起きる(または、整合性を合わせるためにある程度コストを払う)
2. デプステストを行っているとはいえ、半透明オブジェクトの後ろ側のマテリアルはライティング計算を行う必要があるため、「後ろ側のマテリアル」+「半透明オブジェクトのマテリアル」の二つを計算する必要があり、処理が重くなりがち。
特にフィルが多い半透明オブジェクトは絵を破綻させないようにするとなるとZフィル、カラーフィル共に相当に高いコストが必要になりがちです。

そこでディザを使った半透明の出番。
ディザ半透明を使うと見た目はかなりチープになり得ますが、各ドット単位で半透明オブジェクトを描く・描かないを制御しているだけであり、不透明オブジェクトと同様に扱えるためディファードシェーディングでも正しく描画計算できるというメリットがあります。


ディザ

そもそもディザってどういうものか、通常の白黒のグラデーションと、それをディザによって表現したものそれぞれを上下に描いた画像が次。

dither_pattern.png
上のは今回紹介するBayerMatrixをパターン配列に使用したディザリングで、かなりパターン模様が見えます。
一方、フォトショップで同じようなシチュエーションを作って描画させてみると次の通り。

dither_pattern_ps.png
全体的にランダムなところに点をうっており、パターンのようなものは見えません。

上記で少しだけ見てきた通り、一言でディザと言ってもそれを実現するためにはいくつか手法があります。(そもそも古い技術なので、Wikiに結構詳しく載ってたりします)

今回はその中の一つ、組織的ディザリング(OrderedDithering : Wiki)について説明します。

組織的ディザリング(Ordered Dithering)

先に解決すべき問題について整理しておきましょう。
「ピクセルの位置(x,y)と、その値v」を入力として受け取って、「もし描画すべき点なら1を、そうでないなら0を」出力することです。

通常の白黒ディザリングであれば、「ピクセルの位置(x,y)と輝度値v」を受け取って、「もし描画すべき点なら1を、そうでないなら0を」出力します。
ディザ半透明であれば、「ピクセルの位置(x,y)とアルファ値v」を受け取って、「もし描画すべき点ならカラーを出力し、そうでないならその点の描画をdiscard」となりますね。

組織的ディザリングでは、これを閾値行列(Threshold Map)を使って決定します。
処理内容としては非常に単純で、各ピクセルに対応した閾値行列の値をとってきて、二つの値を比較して入力値が大きいなら1、そうでないなら0を出力するだけとなります。

例えば、次のような感じですね。
dither_sampler_2x2.png

典型的には閾値行列は入力画像よりも小さいので、タイリングして対応する値を求めます。
sampler_4x4tile.png

BayerMatrix

さて、組織的ディザリングでは閾値行列を用意しておけば、どのような入力画像に対してもディザリングできる手法です。
となると、組織的ディザリングの品質を一手に決めているのはその閾値行列となりますが、これまた幾つか手法があります。
ランダムにばらまいてもいいですし、Void-And-Cluster法などありますが、本記事では特徴的ではありますが均一な品質になるBayerMatrixを使ったディザについて説明します。

2x2 BayerMatrix

これはもう天下り的(というか公理的)に決まっているものなので、なんとも言えないのですが、次に示す通りです。


ちょっと変形してこう書いたりもします。


この行列が転置されていたり列ごと入れ替わっていたりしても使えます、つまり次のようなのもOK。


4x4 BayerMatrix

先に見せましょう、こういう感じです。


この形で2x2のBayerMatrixとの共通点、見えるでしょうか?
私は全然見えませんでした、例えば次図で赤で示したところを見てみてください。
bayer_4x4_oneLead.png
2x2のBayerMatrixと一致していますね。

では、次図で青で示したところを見てください。
bayer_4x4_twoLead.png
なんとなく分かってきたかもしれません。

左上、右上、左下、右下のブロックに分けたとき、何らかの相関がありそうです。
bayer_4x4_lead.png

つまり、NxNのBayerMatrixはN/2xN/2のBayerMatrixを元にして計算できそう! …ですが、微妙ですねw

NxN BayerMatrixの求め方

NxNのBayerMatrixを求めるには、実はちょっとだけ元の行列の変形が必要です。

まず、公理として与えられる2x2のBayerMatrixに、それの更に元となる行列Dを考えます


実際のBayerMatrixであるBへは、この行列Dから計算で求めます。
具体的には、Dの各要素に1を足して、(行列の要素数+1)で割ります。


このBayerMatrixへ変換する前の行列Dの状態だと、NxNとN/2xN/2の行列の相関関係が非常に綺麗に記述されます。


DからBへの求め方は、先に述べたとおりですし、画像が大きくなるので省きます。
少し遠回りをしましたが、これで無事NxNのBayerMatrixが求まります。

GLSLで書く

BayerMatrixの説明を端折り気味に終え、残った先ではGLSLでこれを実現することですが、もう既にBayerMatrixの求め方を上述したので、コードだけ投げ捨てるように置いておきます。

GLSL SandBoxで実行可能なソースコード。
(GLSL SandBox上にもセーブしました、見たい方はこちらからどうぞ)
#ifdef GL_ES
precision mediump float;
#endif

#extension GL_OES_standard_derivatives : enable

uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;

// visualization of gradation with dither pattern created by bayer-matrix

float bayer( int iter, vec2 rc )
{
float sum = 0.0;
for( int i=0; i<4; ++i )
{
if ( i >= iter ) break;
vec2 bsize = vec2(pow(2.0, float(i+1)));
vec2 t = mod(rc, bsize) / bsize;
int idx = int(dot(floor(t*2.0), vec2(2.0,1.0)));
float b = 0.0;
if ( idx == 0 ) { b = 0.0; } else if ( idx==1 ) { b = 2.0; } else if ( idx==2 ) { b = 3.0; } else { b = 1.0; }
sum += b * pow(4.0, float(iter-i-1));
}
float phi = pow(4.0, float(iter))+1.0;
return (sum+1.0) / phi;
}

void main( void )
{
vec2 position = ( gl_FragCoord.xy / resolution.xy );
vec2 m = mouse.xy;

float alpha = position.x * clamp(m.x*2.0-0.5, 0.0, 1.0);
float threshold = bayer( int(mix(1.0, 5.0, 1.0-m.y)), gl_FragCoord.xy-vec2(0.5) );
float p = mix(alpha, step(threshold, alpha), step(position.y, 0.5));
gl_FragColor = vec4( p, p, p, 1.0 );

}

マウスの座標でディザの諧調数とか右端のアルファ値とか変わります。
「この辺のアルファ値がディザパターンだとこういう風に見えるのか」について試してみたい人はどうぞ。
ソースコードは…まぁ腐るほど汚いわけだが。
dither_pattern_glslsandbox.png

終わりに

フツーにディザについて調べて、フツーにディザについてお話しして終わりました。
描き忘れてましたがディザ半透明というと巷ではスクリーンドア半透明(Screen-Door Tranparency)とも呼ばれているらしいですね、どっちが正式名称なんだろうか、技術的なところは分からない。

本当は実際のゲーム中のディザ半透明についてもなんとなく言及したかったんですが、これ違いがでるのディファードシェーディング使った3Dとかなので、手元でそういうのに使える手軽なシェーダがなかったし画像の一つもだせないんではなぁ、書いてもしょうがないなぁというのが本音の一つ。
また、アンチエイリアシング時にアルファ値からディザへ解決するAlphaToCoverageとかもあったりするんですが、そういうのはよっぽどAAAタイトルとかグラフィックに拘っているところが使っている手法なので、本記事では言及しませんでした。
こういう時、UnityとかUnrealEngineとかに通じていると楽なのかなぁとか思った。

そういえばUnityとかだとディザ半透明はDitherTemporalAAという項目らしく、毎フレーム異なるディザパターンを使って描画し、TemporalAAでディザをなめることによって品質のいい半透明を実現しているらしいです。
TemporalにAAをかけると得てしてオブジェクトが動いた後にゴーストが残ったりするものですが、実際にどれくらい問題になるのかは目で見てみたかった感ある。

単純なディザ半透明もアルファ値が0.5であるなら言うほど悪くはないので、モノは使いどころ。
あとはモニタへ転送するときに出るモアレ問題の方をうまく解決できれば、ディザ半透明をもっと積極的に使うようになると思うんだけどなぁ…。

まぁ、今後に期待。
関連記事
ページトップ