楽天モバイル[UNLIMITが今なら1円] ECナビでポインと Yahoo 楽天 LINEがデータ消費ゼロで月額500円〜!


無料ホームページ 無料のクレジットカード 海外格安航空券 解約手数料0円【あしたでんき】 海外旅行保険が無料! 海外ホテル

log

2013.05.06 C++:ライティングに関する真面目な考察

ファイル 427-1.jpgファイル 427-2.jpgファイル 427-3.jpgファイル 427-4.jpg

今回は今までいい加減な実装だったライティング部分を修正してみました。
実装方法についてはまず自然現象を観察してそこから実装したのですが、さてさてその
実装方法についてある程度以下にまとめてみたいと思います。
3Dやプログラミングに興味が無くても絵描きの方でも参考になるようにまとめてみた
予定ですが、…どうだろうなあ;

まずはライティングというものを大雑把に考えてみますが、"ライトの光の光が当たる
部分は明るくライトの光が当たらない部分は真っ暗になる"、これが超大雑把なライティ
ングの説明です笑。この理屈だけだと3番目の画像の左のように影の部分は真っ黒に
なって何も見えません。が、実際は影の部分も光を受けており、例えば青空の下では
青っぽい影となり夕焼けの下では赤っぽい影となります。
これは太陽以外の物体や粒子が受けた光が反射・散乱して影部分も照らしているから
ですが。ここでは3DCGでの呼び方にのっとり"Global Illumination(GI)"と呼ぶこと
にします。上の説明だけを鵜呑みにすると、「何だ、空の色を影の色にすればいいのか」と
考えるでしょう。つまり、HLSLでは以下のような式でライティングを計算します。
float3 LighingColor = lerp( SkyColor, SunColor, LightingAmount );

上の式でも、まぁある程度それっぽいライティングになります(2番目の画像)。ただ現実世界を
みてみると、例えば陸橋の下の面のライティングは陸橋下の草むらの影響を強く受けて
いるし、分かりやすい例だと自分の左手を右手に近づけたり離したりすると、近づけた時に
お互いに色を影響し合っているのが観察できると思います。つまり実際はライティングを計算
する点をカメラに見立てて、そのカメラから見える景色の色の総和がGI色(正確にはGI+
ライト色)になっていることが想像できます。

ライティングポイントから見えるシーンは遅延ライティングを行うことで一応実装可能な
テクではありますが、画面の画素数分レンダリングするのは今現在のところは非現実的です;
そこで頭のいい方がHDRライティングなりPRTなりを思いついて今に至る訳ですが、自分は
初めてPRTとシャドウマップを実装したときはどう合成していいか分からず、今回になるまで
以下のような方法で実装させていました。
// 旧式その1
float3 LighingColor = lerp( SkyColor, SunColor, LightingAmount ) * PRTColor;
// 旧式その2
float3 LighingColor = min( lerp( SkyColor, SunColor, LightingAmount ), PRTColor );
ちなみにこの手法でライティングした画像を用意しようと思いましたが2番目の画像と対して
変わらなかったので画像は省いています。

旧式その1では暗い部分が暗くなりすぎるし、その2の方では影部分がPRTの影響をほとんど
受けない画になります。
理由としては、その1ではSkyColorはそもそもPRTColorと本質は同じ(同じGI色)なので
GI色同士で乗算して暗くなっているためです。その2の方では暗い部分を選択することで
乗算で暗くなる問題を回避しているが、結局SkyColorの色を選択した場合はGI色とは
呼べない感じになってしまいます。

ここで、仮に現実世界のライトから受ける色を除くGI色をGIColorとすると以下の式で
完全なライティングが再現できると仮定できます。
float3 LightingColor = saturate( lerp( (float3)0, SunColor, LightingAmount ) + GIColor );
何故この式が正しいか証明する方法が思いつかないため、とりあえず矛盾が起きず
現実のライティングと似た挙動になるという点を以下にまとめてみます。
・現実世界では影部分だけ青くなり、明るい部分は青くならない現象が起こるが、これは上の
 式でいうところの白色から飽和することによって明るい部分は青くならないということが説明
 できる。
・現実世界では曇った窓ガラス越しに透過する光を受けた色は白色ではなく青っぽくなって
 いるが、これも同様にSunColorがガラスを通して減衰することでGIColorの色が飽和せず
 に影響しているということで説明できる。
こんな感じかな。「いやいや間違ってるぜ」という意見も出そうですが苦笑。
さて、では今度はどういう風にGIColorを実装したかという点を説明したいと思います。

今現在、自身のプログラムでは背景のような静止物体のみのGIしかPRTで実装できていない
のですが、これだけだと以下のような問題が起こります。
・そもそもPRTやHDRではサンプリング先の色しか分からない。マルチサンプルで実際の
 GI色に近づけることも出来るが、非現実的。(問題1)
・キャラクターから受けるGI色が全く反映されない。(問題2)

この2点を解決するために、自身のプログラムでは以下の2点の仮想GI色をしてみました。
・PRTサンプル先以外の色にみたてた半球ライティングによる固定GI色(問題1解決用)
・自身の色をキャラクターの自己反射にみたてた色(問題2解決用)
HLSLの式にすると以下のようになります。
const half3 SkyColor, HorizonColor;// 真上の空の色,地平線の空の色
float3 HemisphereColor;
if( Normal.y > 0 ) HemisphereColor = lerp( HorizonColor, SkyColor, Normal.y );
else HemisphereColor = lerp( HorizonColor, PRTColor, -Normal.y );
float3 GIColor = SunColor * ( PRTColor + SelfColor.xyz + ShadowColor ) / 3;
PRTの色だけでなく仮想GI色2色を合わせた3色の平均をGI色に見立てるといった感じです。
PRTはHDRより低周波なので問題1は目立たなくなるのですが、空の色もボケて
弱くなってる感じだったのでHemisphereColorも利用しています。
なので場合によってはHemisphereColor部分はいらないかもしれません。
それとライトが暗くなったときにGI色だけ明るくなり過ぎないようにSunColorで掛けて
ます。この方法によるライティングの画像が1番目のものになりますが、2番目の画像と
比べて結構それっぽくなっているかなと思います。位置ごとに3色の合成度合いを修正
した方がより理想的なんだろうけど、そのために必要だと思われるAO用PRTを今はまだ実装
してないのでした。

説明が長くて文章にするのにとっても疲れました。最後の"それっぽい"ライティングを説明
するのにどんだけ書いてるんだか。。そういえば他のサイトだと複数記事に分けて
書いてたりしますが、まとめてあった方が個人的には読みやすいんですよね。
後、実は他にも実験的にR8G8B8の3成分を8-bit1成分にまとめたり取り出したりする
手法も実装してたりしますが、長くなりすぎるので今回はこの辺にしておこうかと思います。

追記:
より分かりやすいように1番目の画像のキャラ部分をモノクロ化した画像もアップしてみました。