下の画像をクリックすると、徐々にライティングが発生するアニメーションが見られます。
最近、spinさんが、狂ったように更新、詳細な解説をされているのですが(「近日発売予定のコンピュータグラフィックス技術解説書」なんて、散財させてくれるような記事はうれしいやら悲しいやら…)、その中の1つに表面下散乱の解説記事がありました。表面下散乱は、BBSでもリクエストされていたので、とりあえず、もっとも簡単な範囲でやってみました。
まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。
hlsl.fx | シェーダの入ったエフェクトファイル |
main.h | アプリケーションのヘッダ |
main.cpp | アプリケーションのソース |
あと、実行ファイル、地面のモデル及び、プロジェクトファイルが入っています。
表面下散乱は、皮膚をよりよく表現するためのレンダリング方法で、一度皮膚の中にもぐりこんだ光が、皮膚の中で散乱されて拡散光として出てくる様子をシミュレートします。
例えば、普通のLambert拡散光と、表面下散乱を比較すると、次のように見た目が異なります。
そもそも、拡散光はどのようにして起きているのでしょうか?
いつもは疑問に思われていないかも知れませんが、拡散光は物体表面から入射した光が物体内部の電子によってランダムな方向に散乱されて、出てきたものです。
光の量が多くなれば、その分反射される量も増えるので、光は表面に入射する強さに応じて強く拡散されます。
ところが、現実はそう簡単ではありません。
物体の組成にもよるのですが、表面から入った光は、何度も散乱されたりして複雑な反射を示します。
特に重要な点は、散乱される光は一点だけで散乱されないということです。
物体に入射された光は、物体内部で長い旅をして表面の別の場所から出てくるケースも発生します。別の場所から光が出てくるということは、本来光が当たらない光源の影になった部分にも光が当たるということです。ほかにも、影の陰影が柔らかく見えたりします。
これらは、皮膚、ミルク、大理石などの自然物に関して特に顕著な現象で、やわらかく見える物質にこれら多重散乱の効果を含めると、よりリアルなレンダリングが可能になります。
表面下散乱の方法は、まだ方法論が確立されていないので、さまざまな方法が考えられます。
例えば、視点が注目する周辺の領域周辺の光を拾い集めてそれらの合計を散乱光とする方法
や、多重散乱を累積して合成した結果を1つの双極子として表現しなおした方法があります。
双極子は、物体内部に置く光を放出する点光源と、ちょうどその真上にある光を吸収する点光源でできています。それらの光源による効果は、物体が凸になっている部分では光を放出する成分が強くなり、光がすけた効果を演出しますが、凹んだ部分では、光が吸収される成分が強くなり、より暗くレンダリングされます。
今回の方法は、それらよく知られた方法ではありません。より流体的な手法をとります。
入射された光を適当に拡散させて、光を広がらせます。同時に一部が熱等に変化したりする効果を考えて広がった光を減衰させます。
最終的に入射した光と吸収される光が平衡状態に落ち着いた状態が照明結果になります。
具体的な方法は、メッシュに照射された光の量をデカールと同じテクスチャ座標にマッピングします。
次に、その画像にブラーをかけてぼやかし、そのテクスチャを散乱された結果として照明計算に使います。
ブラーをかけた結果は次のフレームでも使用して、指数関数的な光の減衰を表現します。
今回はテクスチャ空間でそのまま散逸させましたが、テクスチャにローカル座標系でのピクセル間距離をサンプリングして、その距離に応じて散逸させたり、曲率をマッピングして、それに応じて拡散させるとよりよい結果が得られると思います。
最初のステップは、テクスチャ座標系に入射光をレンダリングすることです。
今回は、テクスチャ座標をスクリーン座標に使います。ただし、射影空間の座標系は-1から1までなので、スケーリングなどを施します。
色は、普通のLambert拡散の計算をおこないます。
hlsl.fx 0209: // ------------------------------------------------------------- 0210: // 頂点シェーダプログラム 0211: // ------------------------------------------------------------- 0212: VS_OUTPUT_Irradiance IrradianceV ( VS_INPUT_Irradiance In ) 0213: { 0214: VS_OUTPUT_Irradiance Out = (VS_OUTPUT_Irradiance)0;// 出力データ 0215: float4 offsetS = { 2.0f, -2.0f, 0.0f, 0.0f}; 0216: float4 offsetB = {-1.0f, +1.0f, 0.5f, 1.0f}; 0217: 0218: // 座標変換 0219: Out.Pos = In.Tex * offsetS + offsetB; 0220: 0221: // 色 0222: Out.Color = vLightDir.w // 環境色 0223: + max( dot(vLightDir.xyz, In.Normal.xyz), 0);// 拡散色 0224: 0225: return Out; 0226: }
ピクセルシェーダは簡単に色を出力します。
hlsl.fx 0227: // ------------------------------------------------------------- 0228: // ピクセルシェーダプログラム 0229: // ------------------------------------------------------------- 0230: float4 IrradianceP ( VS_OUTPUT_Irradiance In ) : COLOR0 0231: { 0232: float4 Col = 0; 0233: 0234: Col = In.Color; 0235: 0236: return Col; 0237: }
次のステップは、ブラーをかけることです。
これは、テクスチャ1枚だけを張るフィルタリング処理になります。
頂点計算では、適当に設定されたポリゴンの位置とテクスチャ座標をそのまま出力します。
hlsl.fx 0301: // ------------------------------------------------------------- 0302: // 頂点シェーダプログラム 0303: // ------------------------------------------------------------- 0304: VS_OUTPUT_SubLight SubLightV ( VS_INPUT_SubLight In ) 0305: { 0306: VS_OUTPUT_SubLight Out = (VS_OUTPUT_SubLight)0;// 出力データ 0307: 0308: // 座標変換 0309: Out.Pos = In.Pos; 0310: 0311: // テクスチャ座標 0312: Out.Tex = In.Tex; 0313: 0314: return Out; 0315: }
ピクセルシェーダでは、先ほど作成した入射光をサンプリングするとともに、
1フレーム前のブラーをかけた結果を上下左右のピクセルからサンプリングしてぼかします。
このときの、それぞれのサンプリングを掛け合わせる強さが減衰率や反射率を決定するので、適当に調整します。
hlsl.fx 0316: // ------------------------------------------------------------- 0317: // ピクセルシェーダプログラム 0318: // ------------------------------------------------------------- 0319: float4 SubLightP ( VS_OUTPUT_SubLight In ) : COLOR0 0320: { 0321: float4 Col = 0; 0322: 0323: float4 irr = tex2D( IrradianceSamp, In.Tex ); 0324: 0325: float2 offsetP0 = { +1.0f/256.0f, 0.0f }; 0326: float2 offsetN0 = { -1.0f/256.0f, 0.0f }; 0327: float2 offset0P = { 0.0f, +1.0f/256.0f }; 0328: float2 offset0N = { 0.0f, -1.0f/256.0f }; 0329: 0330: float4 colP0 = tex2D( SubLightSamp, In.Tex + offsetP0 ); 0331: float4 colN0 = tex2D( SubLightSamp, In.Tex + offsetN0 ); 0332: float4 col0P = tex2D( SubLightSamp, In.Tex + offset0P ); 0333: float4 col0N = tex2D( SubLightSamp, In.Tex + offset0N ); 0334: 0335: Col = 0.20f * irr 0336: + 0.20f * (colP0 + colN0 + col0P + col0N); 0337: 0338: return Col; 0339: }
例えば、1フレーム前の結果「colP0」等を合成するときの強さを0.25にすれば、まったく減衰しない結果が得られます(すなわち真っ白になっていくのですけど)。
最後にブラーをかけたテクスチャをライトマップとして合成します。
それぞれのテクスチャはデカールマップと同じ座標系に存在するので、同じ座標をデカールとライトマップに出力します。
hlsl.fx 0398: // ------------------------------------------------------------- 0399: // 頂点シェーダプログラム 0400: // ------------------------------------------------------------- 0401: VS_OUTPUT_Final FinalV ( VS_INPUT_Irradiance In ) 0402: { 0403: VS_OUTPUT_Final Out = (VS_OUTPUT_Final)0;// 出力データ 0404: 0405: // 座標変換 0406: Out.Pos = Out.Pos = mul(In.Pos, mWVP); 0407: 0408: Out.Tex0 = In.Tex; 0409: Out.Tex1 = In.Tex; 0410: 0415: return Out; 0416: }
ピクセルシェーダでは、それらを掛け合わせて合成します。
ライトの強さを2倍していますが、雰囲気的なもので、
特にこれといって強さを決めた理由はありません。
hlsl.fx 0417: // ------------------------------------------------------------- 0418: // ピクセルシェーダプログラム 0419: // ------------------------------------------------------------- 0420: float4 FinalP ( VS_OUTPUT_Final In ) : COLOR0 0421: { 0422: float4 Col = 0; 0423: 0424: float4 decale = tex2D( DecaleSamp, In.Tex0 ); 0425: float4 light = saturate(2.0f*tex2D( SubLightSamp, In.Tex1 )); 0426: //light=In.Color; 0427:decale = float4(0.8f, 0.8f, 0.8f, 1.0f);// 白色にする 0428: Col = decale * light; 0429: 0430: return Col; 0431: }
裏ネタ:
0426行のコメントアウトをはずすと、テクスチャを使わないライティングが行えます。
0427行をコメントアウトすると、デカールテクスチャを張ります。
実に適当ですが、表面下散乱をしました。
反射率や吸収率の決定もいい加減ですし、遅れが必ず発生するのでまだまだ改良が必要です。
他の方法も吟味していきたいので、これから長くお付き合いしていくことになりそうです。