土屋つかさの技術ブログは今か無しか

土屋つかさが主にプログラミングについて語るブログです。

#unity コンタクトシャドウの前にAO(アンビエントオクルージョン)を知ろう

Unity HDRP アドベントカレンダー13日目です。
qiita.com
 予定ではHDRPにはあってURPには無い機能の一つである「コンタクトシャドウ」について書くつもりだったんですが、本業が忙しくて調査が半分しか終わっていません。なので今回は前半部分についてまとめ、後半は日を分けてアップしたいと思います。なのでコンタクトシャドウ自体については今回触れません(ごめんなさい)。

コンタクトシャドウの前にAO(アンビエントオクルージョン)を知ろう

 コンタクトシャドウはSSAO(スクリーンスペースアンビエントオクルージョン)の発展手法の一つです。SSAOは、リアルタイムレンダリングでAO(アンビエントオクルージョン)を実現するポストプロセス手法です。そしてAOは、静的オブジェクトのライトベイク時に、より自然な遮蔽を反映する手法になります。

 というわけで、コンタクトシャドウを理解するには、「AO→SSAO→コンタクトシャドウ」という順に知る必要があります。一個ずつやっていきましょう。

注意

 言い忘れていましたが、この記事の大部分はビルトインレンダリングパイプラインで動かしています(だって普段HDRP使ってないんだもん! ごめん!)

AO概要とサンプルシーンの説明

 AO(Ambient Oclusion)は日本語訳されることがあまりなく、敢えて訳すと「環境光遮蔽」でしょうか。静的オブジェクトのライトベイク時に、環境光の遮蔽(ここでは「暗くすること」くらいの意味)をより自然にする処理を指します。まず、リアルタイムレンダリングのみの状態を見て、徐々にAO処理に向かう事にしましょう。

 サンプルシーンには広い床と、キューブを引き延ばした板が立っています*1。床も板も新規マテリアルを設定し、デフォルトのStandardシェーダーをそのまま使います
 ライトはディレクショナルライトのみ。板の面に向かって45度の角度から照らしています。スカイボックスは削除しました*2

リアルタイム描画

f:id:t_tutiya:20201213130915p:plain
 上は、環境光を黒、ベイクをオフにした状態です。板の影が床に伸びているのと、板の裏が真っ暗なことがわかります。影が板の付け根から少し離れていますが、これはピーターパン現象と呼ばれる物で、シャドウバイアス値を適切な値にすれば改善されます。今回は面倒なのでやっていません(以下同じです)。
 リアルタイムレンダリングでは、ライトから放たれた光(直接光と言います)が、オブジェクトの表面(サーフェイス)に1度だけ反射してカメラに到達できる時、それを色として描画します。これはつまり、光が飛んでくる方向に向いていないサーフェイスは、描画の対象にならないということです。板の裏面が描画されないのはこういう理由になります。

現実世界での光の挙動を確認する

 さて、現実世界ではライトに直接面を向けていない面が一様に真っ黒になったりはしません。光は物体と物体の間で反射しながら、物体を回り込んで空間全体を満たすからです。これは光に意思があるとかではなく、光が物体表面で反射する際、確率的に全方向に反射していると仮定できるためです。
f:id:t_tutiya:20201213110430j:plainf:id:t_tutiya:20201213110420j:plain
 上図は、自宅の洗面所を真っ暗にして撮影した物です。1枚目が光源無し、2枚目が光源ありです。真ん中に置いた板(温度計です)の裏側が明るくなっているのが分かります。これは、光源から放出された光が、床の上で反射して、板の裏側に届いたと考えられます*3

環境光

 物体からの反射によって描画対象のポリゴンに到達する光を間接光(indirect light)と言います。リアルタイムレンダリングでは、この間接光を近似的に再現するために、環境光(Environment Light)を設定します。
f:id:t_tutiya:20201213131110p:plain
 上図は、環境光を黒から50%グレーに変更した状態です。板の裏面に色がついていることがわかります。床全体の色が先程より明るくなっているのは、Standardシェーダーでは直接光によって描画される個所にも環境光が加算されるためです。環境光は空間全体を満たしている(と想定している)ので、こういう処理になっています。
 色がついたとはいえ、単に色を加算しているだけなので明らかに不自然です。より自然な描画をするには、直接光だけでなく、物体に反射した後の光、すなわち間接光についても計算が必要です。

ライトマップをベイク(AO無し)

 3DCGで間接光を表現するには、間接光がどのようにシーン内を満たすかについて事前に計算しておく必要があります。この作業を「ライトマップをベイクする」と言って、それを行うフレームワークを一般に「GI(グローバルイルミネーション/大域照明)」と呼びます。Unityでは「The Progressive Lightmapper」というGIフレームワークが標準で実装されています*4
f:id:t_tutiya:20201213131221p:plain
 上図はライトマップをベイクした物です。板の裏側が間接光に照らされてグラデーションになり、先ほどよりずっと自然に見えると思います。
 何故下の方が暗く、上の方が明るいのかというと、GIでは物体のある地点に到達するレイ(光が反射した時の線だと思ってください)の本数でその場所の明るさが決まるのですが、影の部分からはレイが飛んでこないので、影の近くにある物体は相対的に間接光が弱くなるためです。

AOが必要になる理由

 前述した「影の近くの物体表面の間接光は弱くなる」という現象は、リアルな描写を目指す際に重要です。
f:id:t_tutiya:20201213131439p:plain
 さきほどの実写を拡大してみました。温度計の側面は角が丸くなっているので、床との間に細い隙間があります。写真を見ると、この部分が真っ暗になっていて、床と温度計の隙間に黒く細い筋として描かれているのが分かります。こういう細い隙間には間接光が入り込みにくいため、このように黒く潰れて見えるのです。電池を入れる蓋の境界も黒い筋になっているのがわかると思います。
 このような「細い隙間に間接光が十分に届かず黒く潰れて見える」という現象を、現実空間では広く観察できます。プラモ工作で溝に黒を塗る「墨入れ」という技法がありますが、これを意識した物じゃないかと思います(違ってたらすみません)。一般に部屋の角や家具と壁の境界などでもこのような暗がりが確認できます。

ライトマップをベイク(AOアリ)

 さて、実は、GIのベイクでは、この現象を上手く再現できません。ライトマップベイクではあるレイが何回反射するまで計算するかが予め決まっています*5。「狭い空間」というのは詰まるところ「レイの反射が非常に沢山行われる空間」なわけで、限られた反射回数では正確に計算できないので、結局近似値で色を付けることになるのです。

 そこで、このような場所についてのみ追加でレイの反射を計算し、該当箇所をより暗くベイクする処理をAO、アンビエントオクルージョン(環境光遮蔽)と言います。
f:id:t_tutiya:20201213131235p:plain
 Lightingウィンドウの以下の項目をオンにするとAOが有効になります。
f:id:t_tutiya:20201213131244p:plain
 上図がAOを有効にした場合です。さっきより境界部が暗くなっているのが分かる……かなあ?w
f:id:t_tutiya:20201213131353p:plainf:id:t_tutiya:20201213131402p:plain
 床との境界部分をアップにしました。1枚目がAOなし、2枚目がAOありです。右の方が、床との境界部分がより暗くなっているのが分かると思います。

SSAOとコンタクトシャドウ(後編へ続く)

 さて、ここまではGIによる事前ベイクにおけるAOの話でした。ベイクするということは、オブジェクトはstaticである必要があります*6。しかし、フォトリアルな描画を行う際は、リアルタイムに動くオブジェクトにもAOが有効化されて欲しくなります。

 例えば、プレイヤーが動かすことが出来る木材が床にばらまかれた時や、プレイヤーキャラクター自身が床に座った時に、AOが反映されればよりリアルな描画ができます。しかし、AOはレイの反射を計算しなければならないので、リアルタイムには演算できません。

 そこで、少しトリッキーなポストプロセス処理によって、リアルタイムAOを実現するようになりました。それがSSAO(スクリーンスペースアンビエントオクルージョン)であり、その発展手法としてのコンタクトシャドウです。

 次回はSSAOとコンタクトシャドウについて解説します(年が明けたらごめんなさい)。

*1:実際には、床もキューブで作っています。これは、ポリゴンの裏面が外に出ていると、ベイク時に意図しない出力になるのを避けるためです

*2:Unityではスカイボックスを前提として行われる処理が色々あるため、これはあまり良くない方法です。純色が指定できるスカイボックスがあればいいんだけど。いつか作ろう。

*3:洗面所の壁に到達した光が跳ね返っているのもあると思われます

*4:なお、Unityには現在提供されていませんが、間接光をリアルタイムに計算するGIもあります

*5:デフォルトでは2回。回数を増やすことは出来るけど計算時間が爆発的に増加します

*6:サンプルの床と壁もstatic指定しています