読者です 読者をやめる 読者になる 読者になる

Unityシェーダ入門

Unityシェーダ、レンダリング手法の解説

頂点空間表面下散乱(VSSS:Vertex Space Subsurface Scattering)

最近色々忙しくて全然更新できていませんでしたが、
ちょうどいいネタを思いついたのと、アドカレが期限超過で1枠空いてたので代わりに投下します。
実装に丸1日、記事書くのに1日くらいかかりました。

目次

  • 表面下散乱(Subsurface Scattering)とは?
  • SSSで出来ること
  • 実装に際しての考え方
  • 実装方法:ランバート反射
  • 実装方法:ハーフランバート
  • 実装方法:曲率を考慮したシェーダ
  • 実装方法:テクスチャ空間での拡散(Texture Space Diffusion:TSD)
  • 実装方法:スクリーン空間での拡散(Screen Space Subsurface Scattering:SSSS)
  • 実装方法:Translucent Shadow Maps:TSM
  • 頂点空間表面下散乱(VSSS:Vertex Space Subsurface Scattering)
  • 実装手順
  • 結果
  • まとめ
  • 参考

表面下散乱(Subsurface Scattering)とは?

 人肌や牛乳、ろうそくなど、不透明ですが強い光を当てると透けて見えるような材質があります。
そのような材質に共通して起こっている現象が表面下散乱(Subsurface Scattering 以下SSS)です。

 それは入射した光が材質の表面で反射(拡散反射)するだけではなく、

  • 表面より深くに潜り込み
  • 内部でランダムな反射(散乱)を何度も何度も繰り返し
  • 最終的に光が入った場所とは違うとこから出てくる

現象のことです。
f:id:IARIKE:20161225164458p:plain

大事な点は拡散反射とは違い、光が"入ったところとは違うところから出てくる"です。
(拡散反射も実際には光が材質の内部に入って散乱されますが、上記のような材質ではない場合その量が少ないため、
単に入射位置から全ての光が反射されるという近似がなされています)

SSSで出来ること

 SSSによってどういう表現が可能になるかというと、

  • 人肌がより人肌らしく見える
  • 逆光のとき、耳たぶから光が透けて見える
  • ヒスイや大理石などの彫刻がリアルに
  • 雪や氷がリアルに

特に人肌に有用で、最近のAAAゲームに出てくるリアルな人肌表現は全てSSSによるものです。
よく言われる、不気味の谷を越えるためには絶対に必要な技術になります。

実装に際しての考え方

 地点Aに入射した光が、少し離れた地点Bからも出射するということは、
BはAから光を"お裾分けしてもらった"と考えることが出来ます。
 またBはAからだけではなく、周囲のCからもDからもEからFからも受け取ることになります。
つまりBは、自分が光に照らされているかを周りの地点に聞きに回って、照らされていたらその一部を
奪っていくことになります。(逆にBの受け取った光は周辺からいくらか奪われます)

また、この図ではIとHは遠すぎるので、散乱光は届いていません。
f:id:IARIKE:20161225164515p:plain

まとめると、

  1. それぞれの地点で拡散反射光を求める
  2. 周囲と光の受け渡しをする

という二つの手順が必要になります。

これまでの実装方法

 ここから、これまでに提案されてきた代表的なアルゴリズム(リアルタイム系のみ)を紹介します。

実装方法:ランバート反射

 これは入射した光すべてが同じ場所から出射されるので、SSSを考慮していません。
解説上必要なので名前だけ載せます。

実装方法:ハーフランバート反射

 人肌などで、ランバート反射は陰のグラデーションがきつ過ぎる(SSSを考慮していないから)ことから、
陰のグラデーションをなだらかになるように式を調整したものが、ハーフランバートになります。

 この手法は周囲のライティング情報を一切考慮しないため、ランバートと同じく局所照明モデルと言われます。
当然周囲の情報を用いないために不正確(非リアル)になります。
3Dグラフィックス・マニアックス (57) 表面下散乱によるスキンシェーダ(1)~ハーフライフ2で採用の疑似ラジオシティライティング(1) | マイナビニュース

実装方法:曲率を考慮したシェーダ

 コーエーテクモで使われているらしいスキンシェーダの実装方法です。
こちらは曲率という形で周辺の形状を事前計算で求めておき、それを考慮したライティングを行います。
 この手法も局所照明モデルなので、高速ではありますが正確さに欠けます。
曲率に依存する反射関数を用いたリアルタイムスキンシェーダの提案

実装方法:テクスチャ空間での拡散(Texture Space Diffusion:TSD)

 一旦テクスチャに拡散反射光を計算してから、そのテクスチャにブラーを掛けることで、お隣さんと光を受け渡しする手法です。
ちゃんとそれぞれ光を受け取る量を確定したうえでお隣さんと分けあっているので、ハーフランバート等より正しい結果となります。

 問題はテクスチャはオブジェクト(例えば人)単位で貼られているので、その人数分だけ拡散反射光の計算をしなくてはならないことです。例えば2048x2048のテクスチャが貼られた人が10人登場したら、2048x2048x10=約4200万ピクセルだけ計算することになります。同じく、ブラー処理も人数分だけ行う必要があるので重い処理となり、キャラクタが多く出てくるゲームのような用途には使えません。
3Dグラフィックス・マニアックス (59) 表面下散乱によるスキンシェーダ(3)~表面下散乱とスキンシェーダ(1) | マイナビニュース

実装方法:スクリーン空間での拡散(Screen Space Subsurface Scattering:SSSS)

 テクスチャ空間でブラーを掛ける代わりに、スクリーン空間でブラーを掛けよう、という手法です。
TSDに対して利点と欠点があり

利点

  • 拡散反射光はもともと画面に表示するために計算するので、TSDのように別途求める必要がない
  • 画面に映っているSSSマテリアルのピクセルのみにブラーを掛けるので、いくら人数が増えても(見えている領域が小さい限り)負荷が上がらない
  • 一般的に画面のレンダリング解像度はテクスチャ解像度よりも低いことが多いので、TSDに比べて明らかに高速
  • ポストプロセスなので実装が容易

欠点

  • 画面から見えない領域は拡散反射光を求めず、結果を保持しておく場所もないので、本来貰えていた裏面から回り込む光を受け取れない
  • 4K環境やVRレンダリングでは通常より解像度が増えるため、人肌を近くから見つめるようなシチュエーションの場合、ブラーの負荷が上がる

近年のAAAゲームではSSSSが多く使われているようです。

4Gamer.net ― [SQEXOC 2012]リアルタイムレンダリングデモ「Agni's Philosophy」に用いられた最新グラフィックス技術の全容を見る(前編)
Unity Screen Space Sub-Surface Scattering for real time skin rendering – Unity上でスクリーンスペースSSSを実装!リアルタイムデモもあるよ | 3D人-3dnchu-
Separable Subsurface Scattering

名前 周辺のライティング情報 速度 リアリティ
ランバート 無し 高速 -
ハーフランバート 無し 高速 イマイチ
曲率を利用したスキンシェーダ 無し 高速 イマイチ
Texture Space Diffusion テクスチャ空間の周辺ピクセル 遅い すごくリアル
Screen Space Subsurface Scattering スクリーン空間の周辺ピクセル そこそこ かなりリアル

実装方法:Translucent Shadow Maps:TSM

 これまで紹介した方法では、隣接情報が無いか有ってもピクセルの隣接情報だったので、
例えば耳の裏から入った光が耳の表側から出てくるような表現は出来ません。
(TSDでいえば、耳の表のテクスチャと裏のテクスチャは近くに無いからです。
SSSSでは、耳の表と裏が同時に見えることがそもそも無いので、情報がありません)

 これを表現するためには、物体の厚みをリアルタイムで計算して、薄ければ光を透けさせるという感じの処理をします。
深度しか考慮してないので、あまり正確な手法とは言えない気がします。他のSSS手法と組み合わせるのだと思います。
3Dグラフィックス・マニアックス (63) 表面下散乱によるスキンシェーダ(7)~表面下散乱とスキンシェーダ(5) | マイナビニュース

頂点空間表面下散乱(VSSS:Vertex Space Subsurface Scattering)

 ようやく本題のVSSSについて解説します。鋭い読者なら名前を見ただけでもうお気づきでしょう。
そうです、テクスチャ空間でもスクリーン空間でもなく、頂点空間(つまり頂点同士)で光の受け渡しを行うのです。

ここからQ&A

Q1.頂点同士で光を受け渡すってどういうこと?

 テクスチャ空間もスクリーン空間も、どちらもピクセルに拡散反射光の情報を保存し、近隣のピクセルと光をやり取りしていました。
VSSSではピクセルではなく頂点単位で拡散反射光を求め、それから近くの頂点に光を受け渡しします。
ピクセルに比べ頂点数は比較的少ないので、より高速になるはずです。

Q2.近さの定義

 テクスチャ空間及びスクリーン空間は二次元なので、二次元上の距離が近さを判断する材料になります。

 SSSSにおいては奥行き方向の距離を考慮していないため、"画面上では近くても奥行きはすごく離れている"ような場合も近いと判断されてしまいます。なので画面から深度を拾ってきて、離れていたらブラーに寄与しないようにするなどの考慮が必要になります。

 TSDにおいてはUV展開する人次第となり、隣接するピクセルは3次元的にも近い位置にあるということを保証する必要があります。しかしテクスチャにはどうしても切れ目が存在するため、そこは別途考慮が必要になります。またテクスチャに歪みがあれば、ブラーの方向が縦横一方に伸びてしまったりすることもあるでしょう。これも別途考慮する必要があります。

 VSSSでは頂点座標を用いるので、直接3次元距離を求めて近いかどうかを決定します。

Q3.頂点の座標系は?

 近接頂点を求めるのはローカル空間で行い、ライティングはワールド空間で行います。

 ワールド空間では近接頂点を毎フレーム求める必要があるのに対し、ローカル空間では事前計算で近接頂点を決めておくことが出来ます。

 また人体のような変形するモデルで、手を顔に近づけたとします。ローカル空間(初期の姿勢)では手と顔は普通離れているので、手の頂点と顔の頂点で光のやり取りは行われません。
 一方ワールド空間では、手を顔に近づけた状態で近接頂点を求めることになり、本来手と顔で光のやり取り(散乱)は行われないにも関わらず、
近接頂点として扱われてしまいます。

Q4.物理的に正確?

 頂点数が増えるにしたがって探索半径内にサンプリング数が増えるので正確になり、頂点数が減ると高速化する代わりに正確性が下がります。
RDPなどちゃんと考慮すれば、それなりに正しくなるとは思います。どっちにしてもかなり大雑把な近似ですが。

Q5.利点は?
  • 全ての頂点の拡散反射光を求めるので、粒度は粗いがライティングに関する全ての情報使える
  • 近接頂点を3次元空間で決定するので、耳たぶなどの薄い物体は自動的に表と裏が近接頂点として扱われる。別途厚みなどを計算する必要が無く、透け表現ができる。
  • SSSオブジェクトの頂点数にもよるが、ピクセル数よりは少ないと思われるので、拡散反射光を毎フレーム求めても高速
  • ピクセル数によらないので、SSSオブジェクトにどれだけカメラを近づけても負荷が変わらない。4KやVRなどの高解像度環境で有用。
  • 前フレームにおける頂点単位の拡散反射光をキャッシュしておき、ライティングに変化があった頂点の近接頂点だけ拡散反射光の散乱(受け渡し)を計算しなおせばよいので、毎フレーム全頂点で受け渡し処理をする必要が無い。
Q6.欠点は?
  • ポリゴン分割数をある程度増やすなど、アセット作成段階から考慮に入れる必要がある
  • 頂点数によって見た目が変わる
  • 近接頂点を求める事前計算が必要(結構時間がかかる)
  • 近接頂点情報を保持するメモリ量
  • TSDのようにオブジェクトが増えると負荷が上がる
  • TSDほどではないが実装がそこそこ面倒

実装手順

  1. 事前計算(CPU)で各頂点の近接頂点(半径R以内)と、Weight(RDPに従ったり、任意の関数にしたり)を求める
  2. 各ライトごとに、毎フレーム拡散反射光(Diffuse)を計算してまとめる(ComputeShader)
  3. 拡散反射光に変化があった頂点に対する近接頂点全てにダーティフラグを立てる(ComputeShader)
  4. ダーティフラグがオンになっている頂点のみ、近接頂点から散乱光を集める(Diffuse*Weight)計算を行う(ComputeShader)
  5. 結果の頂点単位の散乱光を発光項に入れてレンダリングを行う(VertexShader+SurfaceShader)

結果

f:id:IARIKE:20161225161748p:plain
f:id:IARIKE:20161225161754p:plain
f:id:IARIKE:20161225161757p:plain
f:id:IARIKE:20161225161800p:plain
f:id:IARIKE:20161225161802p:plain

f:id:IARIKE:20161225172253p:plain
f:id:IARIKE:20161225172301p:plain

参考に計算速度はR9 390、2ライト(影付き平行光源+ポイントライト)、3オブジェクト、約3000頂点で

処理項目 計算時間
頂点ライティング 0.017ms
散乱光の収集 0.36ms

約25000頂点では

処理項目 計算時間
頂点ライティング 0.058ms
散乱光の収集 2ms

になりました。
ちなみに25000頂点に対し、近接頂点の合計は520万(5M * 8byte(index,weight) = 40MB)にもなるので、
1頂点あたり平均208点も近接頂点を持っていることになります。設定次第ではありますが、ちょっと多すぎる気がします。
また同じメッシュのオブジェクトなら、この値は使いまわせるでしょう。

 散乱光の収集は頂点のライティング環境が変化した場合(光源の移動や、影に入ったり出たり)のみ、
更に影響のある頂点だけ行えばよく、完全に静的な環境では頂点ライティングも一度だけ行えばいいことになります。
(つまりLightmapがベイク出来るような環境では、SSSも頂点にベイク出来るということになります)

まとめ

 今回はベイクも可能な頂点ベースのSSS手法について考えてみました。
テクスチャ空間、スクリーン空間があって頂点空間(3次元空間)が無いのは不思議ってくらい、誰かが既に考えていてもよさそうなものですが、ざっと検索した限り見つけられなかったので勝手に名前を付けました。
(既にやってる人を見つけたら教えてもらえると助かります)

 この手法、テクスチャ空間やスクリーン空間のように、サンプリング位置を任意に決められないのが一番の難点かなと思います。
探索半径球に触れる|含まれる面から、任意のサンプル頂点を生成するのもアリかもしれません。難しいのでやってませんが。

 近い頂点から光をもらってくるだけ、という言葉でいえば非常にシンプルなアルゴリズムなわりに、結構それっぽく見えるので満足しています。

参考

GitHub - SlightlyMad/VolumetricLights: Volumetric Lights for Unity UnityのDirectionalシャドウを直接サンプリングする方法が含まれている