以前書いたファーシェーダの記事を元に、公開していたファーシェーダをライティング不具合の修正に加え、プロシージャルテクスチャ生成法を用いたよりリアルに見えるファーの機能を実装しました。
ちなみにMaker Faire Tokyo 2014で地鶏で自撮り(w)というコンテンツで実際に使いました。テクスチャをうまく調整すればこんな感じのファーが実現できます
影の生成
影の生成にはいくつか手法がありますが、Unityではシャドウマップを用いた影生成を行っています。
影の情報はテクスチャに格納され、それを適宜取り出して計算してやることで影をレンダリングすることが出来ます。
Shadow Caster, Shadow Collector
影を落とす処理(いわゆる「cast shadow」)はUnity側で自動的に行なってくれます。
詳細については ShadowCaster , ShadowCollector あたりで検索すると出てくると思います。
(ちなみにこちらの記事が分かりやすかったです)
ざっくり言うと、影を計算するためには何回かのパスに分けてオフスクリーンレンダリングを行う必要があります。
そのためのシェーダはUnityがすでに準備しており、通常はなにもしなくても Fallbackをしっかりと指定しておけば 自動的に計算してくれます。
(逆にFallbackを書いておかないと適切に影が生成されません)
要は、影をレンダリングするための事前情報をUnityが集めてくれる、ってわけですね。(なのでShadowCollectorって名前なんだと思う)
しかし情報を集めてくれても、影を受ける側(receive shadow)については、カスタムシェーダを記述する側が計算を行わないとなりません。
(例えば影を受けないオブジェクトを作ろうと思っているのに勝手に影が反映されては困りますね)
当然、今回のファーシェーダはカスタムシェーダなので影の部分は自分で書かないとなりません。
が、シャドウを正しく実装するにはそれなりの知識が必要になります。
(かくいう自分もまだしっかりと理解できていませんorz)
なので、 ある程度の範囲と不確かな情報になるので、参考にする場合はご注意ください。
Unityで準備されている機能でシャドウを実装する
Unityの場合、基本的なシャドウについての便利なマクロや関数、変数が既存のインクルードファイルに定義されているので、それを組み合わせることである程度簡単に適用することができます。
ざっくりと手順を書くと以下のようになります。
-
Tagsの中に
"LightMode"="ForwardBase"
(※1)を入れる -
CGPROGRAMの下に
#pragma
を追加。(#pragma multi_compile_fwdbase
) -
CGPROGRAM内でファイルをincludeする。(
#include "UnityCG.cginc"
、#include "AutoLight.cginc"
) -
vertex shaderからfragment shaderに渡す構造体の定義の中に
LIGHTING_COORDS(idx1, idx2)
を入れる(※2) -
vertex shader内で
TRANSFER_VERTEX_TO_FRAGMENT(o);
(マクロ)を記述する。
(o)は出力される構造体名 -
fragment shader内で
LIGHT_ATTENUATION(i)
マクロを使って影の度合いを得る
(i)は入力される構造体名
※1...ForwardBase
では平行光源によるライティングしか考慮されないようです。
※2...idx1, idx2
は、未使用のTEXCOORD#idx
を指定します。 (文末にセミコロンは不要なので注意)
例えば構造体内でTEXCOORD3
まで使われていたらLIGHTING_COORDS(4, 5)
と指定する。引数の数値を利用して自動的にTEXCOORD
を割り当ててくれるマクロになっています。
※ ちなみに、冒頭で書いた通り、シェーダーコードの最後にFallBack
を指定しないとシャドウが出ないので注意です。
UnityのForumで詳細が書かれていました。
マクロを紐解いてみる
いくつかのマクロを使っていますが、それぞれどんなことをやっているのでしょうか。
正直な所、すべてを書くのは非常にむずかしいのでポイントだけ書こうと思います。
LIGHTING_COORDS
LIGHTING_COORDS
マクロは構造体内に適切にメンバを追加してくれます。
定義は「AutoLight.cginc」内でされており、いくつかのライトごとに内容を出し分けています。
(例えば平行光源の場合(#ifdef DIRECTIONAL
)、上記マクロはたんに無視されます)
追加されるメンバは_LightCoord
と_ShadowCoord
のふたつです。(セマンティクスはTEXCOORD#n
)
このふたつのメンバはフラグメントシェーダ内でライティングとシャドウの計算に利用されます。
TRANSFER_VERTEX_TO_FRAGMENT
TRANSFER_VERTEX_TO_FRAGMENT
の定義はライトの種類に応じていくつかのパターンがあります。
が、どのパターンでもやっていることは「ライトの情報を適切にフラグメントシェーダに渡す」という処理です。マクロ名がまさにそうなっていますね。
以下はスポットライトの場合に計算される式を抜粋したものです。
#define TRANSFER_VERTEX_TO_FRAGMENT(a) a._LightCoord = mul(_LightMatrix0, mul(_Object2World, v.vertex)); TRANSFER_SHADOW(a)
LIGHTING_COORDS
で定義した_LightCoord
に何かしらの値を入れています。
_LightCoord
ではライトの位置や強さなどを、ライトテクスチャからlookupするための情報を格納します。
そのための変換行列が_LightMatrix0
というわけですね。
ちなみに最後のTRANSFER_SHADOW
がさらにマクロを呼び出していて、これまたいくつかのパターンが存在します。
その中の定義を抜粋したものが以下。
// パターン1
#define TRANSFER_SHADOW(a) a._ShadowCoord = mul( unity_World2Shadow[0], mul( _Object2World, v.vertex ) );
// パターン2
#define TRANSFER_SHADOW(a) a._ShadowCoord = ComputeScreenPos(a.pos);
パターンは上記ふたつ以外にもありますが、どれも_ShadowCoord
に値を保持します。
_shadowCoord
のレンダリングをするのに利用します。
LIGHT_ATTENUATION
ライトの減衰率を計算するマクロ。
こちらもライトによってマクロの内容が若干異なります。
以下はポイントライトの場合。
#define LIGHT_ATTENUATION(a) (tex2D(_LightTexture0, dot(a._LightCoord,a._LightCoord).rr).UNITY_ATTEN_CHANNEL * SHADOW_ATTENUATION(a))
前述の_LightCoord
へ書き込まれた情報を元に、フェッチするテクセルの場所を計算します。
(ちなみにrr
は、float
型のスカラー値からfloat2
型のベクトルを作り出す方法です)
そして_LightTexture0
(ライトの諸々の情報を格納しているテクスチャ)から適切に値を取り出します。
UNITY_ATTEN_CHANNEL
は、ドキュメントを引用すると以下のように説明されています。
UNITY_ATTEN_CHANNEL - which channel of light attenuation texture contains the data; used in per-pixel lighting code. Defined to either ‘r’ or ‘a’.
つまり、r
かa
どちらに目的の情報があるかが変わるため、それを適切に切り替えてくれているわけですね。
SHADOW_ATTENUATION
こちらは影の減衰率。
定義を見ると以下のように関数を呼び出しています。
#define SHADOW_ATTENUATION(a) unitySampleShadow(a._ShadowCoord)
関数では以下のように計算を行っています。
inline fixed unitySampleShadow (float4 shadowCoord)
{
fixed shadow = tex2Dproj( _ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord) ).r;
return shadow;
}
ポイントはtex2Dproj
関数を使ってテクセルを参照している点。
これはデプスバッファシャドウのためでしょう。
(デプスバッファシャドウについては以前記事を書いたのでそちらを参照)
UNITY_PROJ_COORD
UNITY_PROJ_COORD
の定義は以下のようになっています。
#define UNITY_PROJ_COORD(a) (a).xyw
// or
#define UNITY_PROJ_COORD(a) a
環境によって値を出し分けしているようです。
基本的には受け取ったshadowCoord
を利用して影の減衰率をテクスチャからlookupしています。
Forward Renderingとシャドウ
こちらの記事に、FowardRenderingでの影について書かれています。
上記記事から引用させていただくと影の処理の流れは以下になるようです。
- シャドウマップを生成します。
QualitySettingのShadowCascadesによって、1,2,4枚のカスケードシャドウマップが生成されます。
(2,4枚の場合でも、1枚のテクスチャにまとめられる[=テクスチャアトラス]ようです)
ここではシェーダのShadowCasterパスが用いられます。
- 深度を比較して、画面空間上での影を求めます。
ここではシェーダのShadowCollectorパスが用いられます。
- 2で求めた影の画像に対してブラー処理が行われます。
- FowardRenderingのbaseパスで、3で生成した影テクスチャから影の量を取得し、ライトをその分だけ減衰させます。
※ ちなみにテクスチャやシャドウマップなど、影自体の仕組みは少し前にWebGLの記事([WebGL] デプスバッファシャドウ)として書いたのでそちらが参考になるかもしれません。
LightModeタグ
LightMode
タグは、「ライティングパイプラインの中での、 パスの役割 を定義」します。
指定できるパラメータは以下のとおり。(Unityのドキュメントから抜粋)
パラメータ | 意味 |
---|---|
Always | 常にレンダリングされ、ライティングは適用されません。 |
ForwardBase | Forward rendering で使用され、環境光、メイン指向性ライト、頂点/SHライトが適用されます。 |
ForwardAdd | Forward rendering で使用され、ライトごとに一つのパスで、Additive ピクセルライティングが適用されます。 |
PrepassBase | Deferred Lighting で使用され、法線や鏡面指数をレンダリングします。 |
PrepassFinal | Deferred Lighting で使用され、最終的なカラーについて、テクスチャ、ライト、および自己発光、を合成することでレンダリングします。 |
Vertex | Vertex Lit rendering でオブジェクトがライトマップされてない場合に使用され、全ての頂点ライトが適用されます。 |
VertexLMRGBM | Vertex Lit rendering でオブジェクトがライトマップされてる場合、ライトマップがRGBMエンコードされるプラットフォームにおいて使用されます。 |
VertexLM | Vertex Lit rendering でオブジェクトがライトマップされてる場合に、ライトマップがdoub-LDRエンコードされるプラットフォームにおいて(一般にモバイル向けプラットフォームや古いデスクトップ向けGPUにて)使用されます。 |
ShadowCaster | オブジェクトを、Shadow Casterとしてレンダリングします。 |
ShadowCollector | Forward Renderingのために、オブジェクトの影をScreen-Spaceバッファに集めます。 |
ライティングに関するメモ
ライティングは色々理解がむずかしく、Unity側でよしなにやってくれる部分もあるのでだいぶ把握が大変です。
いまだにしっかりと把握できているわけではありませんが、ハマった点、注意する点などをメモしておきます。
なお、ライティングについては以前の記事([Unity] デフォルトのDiffuseライティングをカスタムシェーダで実装する)で解説しているのでそちらをご覧ください。
Forward AddingにはBlendを指定する
ライティングを行う上で、複数ライトによるライティングはTags { "LightMode"="ForwardAdd" }
を指定して複数パスでレンダリングする必要があります。
複数パスが意味する通り、「ForwardBase」でレンダリングされたものに加算する形でライティングが施されます。
ただ、「ForwardAdd」は複数パスでのレンダリングを実行しているに過ぎないので、Blend
をしっかり指定しないと、最後に計算されたライトの色だけが反映されてしまいます。
Pass {
Tags { "LightMode"="ForwardAdd" }
Blend One One // Blendをしっかり指定する
// 省略
}
上記のようにBlend
をしっかり指定する必要があることに注意です。
(最初、これをやらずに色々書いていてハマりました)
CGIncludesに含まれるファイルのメモ
UnityのShader向けに、Unityが最初から準備してくれているファイルがあります。
- AutoLight.cginc
- HLSLSupport.cginc
- Lighting.cginc
- TerrainEngine.cginc
- Tessellation.cginc
- UnityCG.cginc
- UnityCG.glslinc
- UnityShaderVariables.cginc
HLSLSupport.cgincは自動的に読み込まれるようです。
それ以外のものについては任意で読み込みます。
(AutoLight.cgincは、surface shaderの場合は自動的に利用されるようです)
ライティングやシャドウを実装しようとする場合、これらのサポートファイルを使うと便利です。
が、中で定義されているものや、(おそらく)Unityが内部的に宣言しているマクロなど分からない部分もあるので、それらをメモしていきます。
シャドウ関連
サポートファイルを見ているといくつかの定義が見つかります。
- SHADOWS_SCREEN
- SHADOWS_NATIVE
などですね。
ただ、これがどこで定義され、どう使われるのかがイマイチ分かりません。
色々探してみたところ、こちらの記事のコメントが参考になるかもしれません。
それを引用させていただくと、
Just want to share what I found. After looking at the compiled code, I found that
SHADOWS_NATIVE seems to use shadow2DEXT to sample the shadow map, in which the calculation whether the current z is in front of the shadow map is done in the hardware directly.
Meanwhile, SHADOWS_SCREEN seems to be the usual shadow map technique, using texture2DProj to sample the shadow map, and then compare the current z to the result in the code.
コンパイル後のコードを覗いて確認されたようです。
SHADOWS_SCREEN
は、いわゆるデプスバッファシャドウによるシャドウイング? をしているようです。
SHADOWS_NATIVE
は、ハードウェアで直接計算を行っている? 具体的には分かりませんが、確かに「ネイティブ」と書かれていることからすると納得です。
ただどちらもシャドウの演算方法を決めているにすぎないようです。
雑記
以下は、自分がこうかなーと考えたメモなので、参考にする場合は自己責任でお願いします。
UNITY_DECLARE_SHADOWMAP
SHADOWS_NATIVE
が有効な場合に、適切にハードウェアごとにシャドウマップテクスチャを宣言するマクロ。
ComputeScreenPos
スクリーン座標を計算するインライン関数。いくつか分岐があるものの、最終的には引数のpos
がスクリーン上のどこか、を計算してくれる関数ぽい。
コメントを引用すると
Projected screen position helpers
ということのよう。