2014年03月23日

トゥーンシェーディングのテスト

MMDではトゥーンシェーディング用のテクスチャが設定されていますが、今まで適用してなかったのでメモしておきます。PMD/PMXファイルでは、独自テクスチャ/MMD付属テクスチャのどちらかを指定するフラグと、そのテクスチャの番号を設定する領域があります。それを読み込んだりする準備は省略します。

1. GLSLでテクスチャのメモ

「ボリュームレンダリングのテスト その2」でも書きましたが、sampler3Dだったのでもう一度メモです。無駄があったり間違っていたりするかもしれません。GLSLのフラグメントシェーダ等で、テクスチャを参照したい場合、sampler2Dを使う方法があります。例えば、モデルの普通のテクスチャ、スフィアマッピング用テクスチャ、トゥーンシェーディング用テクスチャの3枚を同時に使いたい場合は、3つのsampler2Dを宣言します。

test.frag
    uniform sampler2D texture0; // GL_TEXTURE0
uniform sampler2D texture1; // GL_TEXTURE1
uniform sampler2D texture2; // GL_TEXTURE2
...

ここでは、texture0, texture1, texture2と適当に名前を付けていますが、これがGL_TEXTURE0, GL_TEXTURE1, GL_TEXTURE2であると関連付けておく必要があります。
これはシェーダプログラムの準備後等に1度glUniform1iを使って関連付けます。

init.cpp
    // テクスチャユニット0を指定する //
glUniform1i(glGetUniformLocation(shdProg, "texture0"), 0);
// テクスチャユニット1を指定する //
glUniform1i(glGetUniformLocation(shdProg, "texture1"), 1);
// テクスチャユニット2を指定する //
glUniform1i(glGetUniformLocation(shdProg, "texture2"), 2);

ここまでがGLSL側の準備になります。

続いてテクスチャの準備です。テクスチャを読み込んで、色々パラメータを設定して、glTexImage2DでCPUからGPUにテクスチャのデータを転送します。PMDモデルのテクスチャのような、画像の内容が変わらないものは、モデルの読込時に1度転送しておけば、後はバインドするだけで良くなります。

    glEnable(GL_TEXTURE_2D); // 必要?
// これはテクスチャのIDであってGLSLとは関係なし
glGenTextures(1, &texId0);
glActiveTexture(GL_TEXTURE0); // 必要?
glBindTexture(GL_TEXTURE_2D, texId0);
// 適当なパラメータ設定
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
// CPU→GPU転送(imageに読込んだ画像データ)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0,
GL_RGBA, GL_FLOAT, image);
glActiveTexture(GL_TEXTURE0);
glDisable(GL_TEXTURE_2D);

最後に、描画行うときの設定方法です。glActiveTextureとglBindTextureを使用します。

draw.cpp
    // 0番目の画像はtexture0へ
glActiveTexture(GL_TEXTURE0);
glEnable(GL_TEXTURE_2D); //必要?
glBindTexture(GL_TEXTURE_2D, texId0);
// 1番目の画像はtexture1へ
glActiveTexture(GL_TEXTURE1);
glEnable(GL_TEXTURE_2D); //必要?
glBindTexture(GL_TEXTURE_2D, texId1);
// 2番目の画像はtexture2へ
glActiveTexture(GL_TEXTURE2);
glEnable(GL_TEXTURE_2D); //必要?
glBindTexture(GL_TEXTURE_2D, texId2);
glActiveTexture(GL_TEXTURE0);

あとはシェーダ側でvec4 texColor = texture2D(texture0, gl_TexCoord[0].st);とかやれば参照できます。

2.トゥーンシェーディング

法線ベクトルと光源へのベクトルの内積によって色を変える方法です。ランバートモデル等では変化が細かいので、テクスチャを使ってトゥーンっぽい陰影付けを行う方法です。
MMDでは、上半分が白で下半分が灰色など、画像の上がdotNL=1.0, 画像の下がdotNL=-1.0となるような使い方で、横方向は特に使っていない様子です。(エッジはトゥーンシェーディングではなく別計算しているようですので)

まぁ横軸は適当に定数でもいいのかもしれませんが、独自トゥーンテクスチャの中には横方向を法線ベクトルと視点へのベクトルの内積を使って変化させているようなものもあるかもしれません。texture2にトゥーンシェーディング用のテクスチャを割り当てている場合は、下記のような計算でいいと思います。

test.frag
    // toon
vec4 toon = texture2D(texture2, vec2(0.5*dotNV+0.5, 0.5*dotNL+0.5));
gl_FragColor = vec4(toon.rgb*tex.rgb, tex.a);


3. トゥーンシェーディング用テクスチャを用いたシャドウマッピング

通常のシャドウマッピングでは、光源から見た深度値を使って、影領域であれば色を0.5倍するといった処理を行うことで影を表現します。しかし、MMDなどでは、トゥーンシェーディング用テクスチャの縦軸(日向の場合には0, 陰の場合は1.0等)に使って影を表現しているのではないかなと思います。今回はもう一工夫?ということで、深度値の差を縦軸に使ってみるのはどうかなと考えました(下図)。

toonshading_001.png

テクスチャごとにoffset設定するの面倒ですが。

実行結果

あぴミクさんでトゥーンシェーダのテストです。あまり違いが判らないですね。

トゥーンシェーディングの色 トゥーンシェーディングなし トゥーンシェーディングあり
toonshading_002.png toonshading_003.png toonshading_004.png

     A:シャドウマッピングの色    B:トゥーンシェーディングの色 
toonshading_005.png toonshading_006.png

     min(A, B)            min(A, B)×レンダリング結果
toonshading_007.png toonshading_008.png


web拍手 by FC2
posted by シンドラー at 20:07 | Comment(0) | TrackBack(0) | OpenGL & Metasequoia | このブログの読者になる | 更新情報をチェックする

2014年03月14日

G-bufferと等高線の描画 その3

前回の続きです。3.3 等高線の描画に入ります。等高線は、ある点から追跡していく方法が考えられますが、この論文では画像処理のテクニックを使って各ピクセル毎に計算できるようにしています。

ある値pのところに等高線を引きたい場合、式(7)で計算することができます。ここで、sはピクセルの値、dは線の太さ、gは式(1)ではなく、式(9)で計算したgradient、Ccが等高線の密度、Cbが背景色?になります。輪郭線かどうかの判定(と輪郭線の濃さ)を行う関数がf1で、式(8)で定義されており、図4に示すように、線形や高次関数を使っても良く、当然高次の方がエイリアスは減らせますが計算量は増えます。で、式(7)にあるように、pがsとほぼ等しかったり、gがほぼ0の平坦な領域は計算できなくなります。そのための場合分けが式(10)と式(11)になります。

具体的には、gが小さい場合には、cbのような定数に近づけるという方法です。fgは、gが小さすぎる場合に、等高線の密度Ccを修正するための関数です。係数Csparseは、平坦な領域での等高線の密度に関連します。例えば、Csparse=0の場合、密度は背景色Cbとなります。この処理を入れると、一時的に線が消えたりして平坦な部分でノイジーに見えてしまいます。しかしながら、この処理を行うことで終わりのないループや重要な等高線の消失はなくなります。

実行結果(p=0.3)

contour_003.png

まだpに1個の値しか設定できませんので、1本(+輪郭線)の等高線になります。輪郭線は、これは|s-p|がある程度大きくても、それよりもgが大きければf1が大きい値をとるためのようです。そういう仕様なのか何か間違えているのかわかりませんが。

式(8)は図4からすると0と1が逆のような気がするんですがどうなのでしょうね。f1の計算ですが、ガウス関数を変形して、距離dで値が0.5となるような以下の関数を使いました。

double getF1(const double t, const double d)
{
return exp((t*t*log(0.5))/(d*d));
}

続いて、間隔を指定した等高線になります。例えば、0.1, 0.2, 0.3, ...のように0.1刻みで調べてきたい場合、sが0.05〜0.15の間はpk=0.1, sが0.15〜0.25の間はpk=0.2というふうに、sの値によりpkの値も変わってくる事になります。この刻み幅をq, 初期値をp0とすると、pk=p0+k*qと表すことができ、このkはsの値から計算できます(式(14))。

k=floor((s-p0)/q+0.5)

ここには絶対値がついていませんので、kは負の値をとることがあり、とったとしても問題ありません。p0は基準にしたい値を設定すれば良いかと思います。これに合わせて、fgの式を更新したのが式(15)になります。

実行結果

contour_004.png

q=0.02で描画した結果です。深度の変化が大きいところはたくさん入り、少ないところは間隔が広いので、まさに等高線ではありますが、なるべくなら等間隔になってくれた方がうれしい気がします。そのための方法が前回諦めたハッチングになります。

3.4 カーブハッチングでは、binary thinning out techniqueというものを使ってこれを解決していきます。勾配が大きいということは、変化が大きいので、等高線の密度が高くなるので間引きを行い、逆に、勾配が小さい平坦な領域では等高線を追加するという処理になります。そのアルゴリズムが式(17)〜(21)になります。ここで、pdはスカラ空間での標準的な間隔で、fdは2つの項を持っています;一つ目が通常の等高線の密度に関する項で、二つ目が等高線を間引くか加えるかに関する項です。これらの関数により、スクリーン上でdiの間隔を持つ等高線を近似することができます。で、以前は式(20)に説明のない謎パラメータpがでてきていて、これどうするんだろうということでさっさと諦めました。

そこで、アルゴリズム的には、勾配gによって3.3ででてきた間隔qを変更することになるだろうというところから考えてみました。gはsobel的な計算なので正確ではありませんが、8で割っているので1ピクセル移動すると、値が大体gぐらい変化すると考えることができます。つまり、間隔qで線を引いたとすると、1ピクセルの間にg/q本だけ線が引ける、ということになります。これを逆に考えて、diピクセルに1本線を引きたい場合には、g/q=1/diとなりますので、dig/q=1という関係が考えられます。というわけで、もしかしてpというのはgの誤植ではないかな、ということで試してみたところ、それっぽくはなりました。

実行結果

画像のx,yを-1≦x,y≦1の範囲で変化させて、f(x,y)=xyで作った画像に対して等高線を引いています。

左:3.3の方法(q=0.02, d=1.0) 右:3.4の方法(Pd=0.02, d=1.0, di=20.0)
hatching_001.png contour_002.png

おまけ

テクスチャのUV値を使ってのハッチングです。
contour_005.png+contour_006.png

contour_007.png
いつの間にかまた脇道にそれていたので、次回はボリュームレンダリングに戻りたいところです。

web拍手 by FC2
posted by シンドラー at 19:21 | Comment(0) | TrackBack(0) | OpenGL & Metasequoia | このブログの読者になる | 更新情報をチェックする

2014年03月13日

G-bufferと等高線の描画 その2

以前の記事では、論文[1]をちゃんと読まずに適当に式だけ実装したので、もう少し読みながら実装します。G-bufferとして、深度値や法線ベクトル、テクスチャ座標を出力します。それに対して、画像処理的な試みで等高線を描画したりする、というのが論文の説明です。

3.1 不連続部分の描画
まず、勾配(gradient)を計算する方法として、Sobelフィルタを用いています(式(1))。
g=(sobelX+sobelY)/8
もうひとつ、2次微分としてラプラシアンを計算しています(式(2))。
l=laplaceFilter/3

この方法だと、以下の問題点があるということです。
1) 不連続なのか、差が大きいエッジなのかを見分けることが困難
2) 差の大きさの程度によって、抽出した線が暗くなってしまう
3) 0次の不連続?が、負の線と正の線の両方として抽出されてしまう

1)と2)を解決するための正規化が式(3)になります。kgの値を変えると結果も変わります。

この式は、2次微分の方にもほぼそのまま適用できます。
3)を修正するために、式(4)を使用します。

3.2 エッジの描画
エッジには2種類あるそうです。
・profile - オブジェクトとスクリーンの境目
・internal edge - 2つの面が衝突している線
profileは、深度値画像に対して、3.1の式を適用すればよいとのことで、この深度値画像は、単純にZ値を出力するのではなく、式(6)が推奨されています。
zs=(d*d)/w*zv
普通はzv/wが深度値ですが、こういう計算をすると、図3のdepth imageのように、手前が明るく奥に行くほど暗いような深度値画像が得られます。
dはスクリーンまでの距離ですので、近クリップ面までの距離でいいのでしょうか。

実行結果

あぴミクさんを使用させていただいております。

depth image

gbuffer_depth.png

1st order differentialとprofile image

gbuffer_gradient.png gbuffer_profile.png

2nd order differentialとinternal edge image

gbuffer_laplace.png gbuffer_internal.png

profileとinternalとdepth image

gbuffer_edge.png

textureのみと、textureに↑を足したもの

gbuffer_texture.png gbuffer_result.png

以前はこの辺り読み飛ばしたので普通の深度値に対して適用しており、3.1の補正などもしていませんでした。困ったものです。次回は前回やってなかったハッチング辺りまでできるといいですね。

[1] T. Saito and T. Takahashi, "Comprehensible Rendering of 3-D Shapes", Computer Graphics, Vol. 24, 1990
web拍手 by FC2
posted by シンドラー at 01:09 | Comment(0) | TrackBack(0) | OpenGL & Metasequoia | このブログの読者になる | 更新情報をチェックする

2014年03月07日

シェーダのテスト

Blenderのシェーダノードの説明[1]を見て、いくつか試してみることにしました。

1. Oren-Nayarシェーダ[2][3]
φの計算をどうするのかと思いましたが、法線平面に射影してその差でいいのですね[3]。結局cosθiの影響で端が暗くなるので、cosθiの変わりにHalf-Lambertの値を使うことにしました。

実行結果1
sigma=0(Half-Lambert)   sigma=1.0(Oren-Nayarもどき)
shader_test_00.png shader_test_10.png
とりあえず球で動作確認です。あまり違いがわかりませんね。表面に標準偏差σの正規分布に従うノイズテクスチャでも使えばそれっぽく見えそうな気もするのですが。

2. Velvetシェーダ[4][5][6]
[5][6]は視点と法線の内積が大きいときに暗くなる感じですが、[4]をみたりちょっと試した感じでは、Blenderではそれに加えて光源と法線の内積が小さいときにも暗くなるような効果を足している気がします。Blenderでは説明のない謎パラメータsigmaがありましたので、sigmaといえば正規分布かな、ということで内積の値を正規分布(ただし分散にかかわらず最大値は1になるように)にいれて色を計算するようにしました。微妙に違う感じがしますがまぁ気にしないでおきます。

実行結果2
とりあえず球で動作確認です。視点と法線の内積でsigmaの値を変えています。

   sigma=0.0      sigma=0.2   
shader_test2_00.png shader_test2_03.png 
   sigma=0.5      sigma=1.0
shader_test2_05.png shader_test2_10.png

光源と法線ベクトルの内積の場合(左)と両者を加算したもの(右)
shader_test2l_02.png shader_test2lv_07.png

実行結果3
メタセコで生成した地形で同様に試してみました。

視点から、sigma=0.8   光源から、sigma=0.5   視点+光源、sigma=0.5
shader_test3v_08.png shader_test3l_05.png shader_test3lv_05.png

視点から、sigma=0.5   視点から、sigma=0.5、別角度
shader_test3v_05.png shader_test3v2_05.png
真上から見た場合暗いところが多く、横から見ると法線と視線の成す角が大きくなる部分が明るくなっています。

実行結果4
Ashikhmin+Velvetであぴミクさんです。顔のテカテカは抑えることにしました。肌の輪郭が明るくなっているのがVelvetの効果ですかね。
shader_test_002.png

[1] Blender 2.6 マニュアル - シェーダノード
[2] Wikipedia - オーレンネイヤー反射
[3] t-pot-粘土ライティング
[4] Blender Cycles memo - Velvet BSDF
[5] Maverick Project - ベルベットシェーダー
[6] 3Dゲームファンのための「ワンダと巨像」グラフィックス講座
web拍手 by FC2
posted by シンドラー at 22:42 | Comment(0) | TrackBack(0) | OpenGL & Metasequoia | このブログの読者になる | 更新情報をチェックする

2014年03月05日

マウスでカメラ操作のテスト

以前作っていたんですが、使い勝手が微妙だったので作り直すことにしました。MMDと同じ操作にしようかと思ったのですが、よくわからなかったのであきらめて適当に考えました。方法としては、極座標に変換して、球面上でカメラを操作する感じです。

mouse_test_001.png

この辺りの計算は、[1]を参考にさせていただきました。

mouse_test_002.png

実行結果
今回もあぴミクさんを使用させていただいております。

[1] 変数変換(ヤコビアン)
web拍手 by FC2
posted by シンドラー at 22:38 | Comment(0) | TrackBack(0) | OpenGL & Metasequoia | このブログの読者になる | 更新情報をチェックする