Help us understand the problem. What is going on with this article?

[Unity] AssetStoreのファーシェーダをupdateしたので分かったことを書いてみる

More than 5 years have passed since last update.

big_precise.jpg

以前書いたファーシェーダの記事を元に、公開していたファーシェーダをライティング不具合の修正に加え、プロシージャルテクスチャ生成法を用いたよりリアルに見えるファーの機能を実装しました。

現在バージョン2.0がAssetStoreにて公開中です。

ちなみにMaker Faire Tokyo 2014で地鶏で自撮り(w)というコンテンツで実際に使いました。テクスチャをうまく調整すればこんな感じのファーが実現できます
cap.jpg

影の生成

影の生成にはいくつか手法がありますが、Unityではシャドウマップを用いた影生成を行っています。
影の情報はテクスチャに格納され、それを適宜取り出して計算してやることで影をレンダリングすることが出来ます。

Shadow Caster, Shadow Collector

影を落とす処理(いわゆる「cast shadow」)はUnity側で自動的に行なってくれます。
詳細については ShadowCaster , ShadowCollector あたりで検索すると出てくると思います。
(ちなみにこちらの記事が分かりやすかったです)

ざっくり言うと、影を計算するためには何回かのパスに分けてオフスクリーンレンダリングを行う必要があります。
そのためのシェーダはUnityがすでに準備しており、通常はなにもしなくても Fallbackをしっかりと指定しておけば 自動的に計算してくれます。
(逆にFallbackを書いておかないと適切に影が生成されません)

要は、影をレンダリングするための事前情報をUnityが集めてくれる、ってわけですね。(なのでShadowCollectorって名前なんだと思う)

しかし情報を集めてくれても、影を受ける側(receive shadow)については、カスタムシェーダを記述する側が計算を行わないとなりません。
(例えば影を受けないオブジェクトを作ろうと思っているのに勝手に影が反映されては困りますね)

当然、今回のファーシェーダはカスタムシェーダなので影の部分は自分で書かないとなりません。
が、シャドウを正しく実装するにはそれなりの知識が必要になります。
(かくいう自分もまだしっかりと理解できていませんorz)
なので、 ある程度の範囲と不確かな情報になるので、参考にする場合はご注意ください。

Unityで準備されている機能でシャドウを実装する

Unityの場合、基本的なシャドウについての便利なマクロや関数、変数が既存のインクルードファイルに定義されているので、それを組み合わせることである程度簡単に適用することができます。

ざっくりと手順を書くと以下のようになります。

  1. Tagsの中に"LightMode"="ForwardBase"(※1)を入れる
  2. CGPROGRAMの下に#pragmaを追加。(#pragma multi_compile_fwdbase
  3. CGPROGRAM内でファイルをincludeする。(#include "UnityCG.cginc"#include "AutoLight.cginc"
  4. vertex shaderからfragment shaderに渡す構造体の定義の中にLIGHTING_COORDS(idx1, idx2)を入れる(※2)

  5. vertex shader内でTRANSFER_VERTEX_TO_FRAGMENT(o);(マクロ)を記述する。
    (o)は出力される構造体名

  6. 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’.

つまり、raどちらに目的の情報があるかが変わるため、それを適切に切り替えてくれているわけですね。

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での影について書かれています。

上記記事から引用させていただくと影の処理の流れは以下になるようです。

  1. シャドウマップを生成します。
    QualitySettingのShadowCascadesによって、1,2,4枚のカスケードシャドウマップが生成されます。
    (2,4枚の場合でも、1枚のテクスチャにまとめられる[=テクスチャアトラス]ようです)
    ここではシェーダのShadowCasterパスが用いられます。

  2. 深度を比較して、画面空間上での影を求めます。
    ここではシェーダのShadowCollectorパスが用いられます。

  3. 2で求めた影の画像に対してブラー処理が行われます。

  4. 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をしっかり指定しないと、最後に計算されたライトの色だけが反映されてしまいます。

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

ということのよう。

関連リンク

edo_m18
現在はUnity ARエンジニア。 主にARのコンテンツ制作をしています。 最近は機械学習にも興味が出て勉強中です。 Unityに関するブログは別で書いています↓ https://edom18.hateblo.jp/
http://edom18.hateblo.jp/
unity-game-dev-guild
趣味・仕事問わずUnityでゲームを作っている開発者のみで構成されるオンラインコミュニティです。Unityでゲームを開発・運用するにあたって必要なあらゆる知見を共有することを目的とします。
https://unity-game-dev-guild.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away