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

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity で Standard Surface Shader の変換後のコードを追ってみた (Deferred)

Unity Shader

はじめに

過去幾つかの記事でディファードシェーディングのレンダリングパスで G-Buffer を色々いじることをやってきました。その際、G-Buffer に与える出力は適当な値を与えてきましたが、実際はもう少し真面目にやらなければなりません。というのも本来はエミッションのバッファにはエミッシブ値以外にもアンビエントや、ライトプローブ、GI といった事前計算された情報も書き込まれているからです。

例えば Anbient Intensity を変えてみると良くわかります。球が前回と同じくレイマーチで作成した球、床や立方体はデフォルトのマテリアルを使用しています。球のエミッションバッファの値は 0.0 になっています。

f:id:hecomi:20160927212012g:plain

一方、スタンダードサーフェスシェーダを利用するとこのようにはならず、適切にライティングされます。しかしながらサーフェスシェーダも最終的には頂点シェーダとフラグメントシェーダへと変換されているわけです。なので、変換後のコードを参考に G-Buffer の値を埋めてあげれば、適切にライティングの施された結果が得られると考えられます。そこで本エントリでは、ディファードレンダリングにおける、サーフェスシェーダに相当する最小限のフラグメントシェーダを示し、それぞれの処理がどういった意味を持っているのかを深掘りしてみたいと思います。

デモ

壁と床が前回の記事で紹介したレイマーチで描いた Static なオブジェクトで、ライトがベイクされています。動いているものはライトプローブから影響を受けているのがわかると思います。

環境 

サーフェスシェーダ変換後のコード

Project の Create > Shader > Standard Surface Shader からサーフェスシェーダを作成し、Show generated code ボタンを押下するとサーフェスシェーダを変換したコードを見ることが出来ます。このコードを頂点・フラグメントシェーダとして新しく作成したシェーダに貼り付ければ、サーフェスシェーダと同じ結果が得られます。生成されたシェーダのコード中には Forward BaseForward AddMeta といった様々なパスが含まれています。ディファードではこれらのパスは必要ないので消してしまいます。少しコードが長くなりますが、余分な箇所を省いて整形したコードを以下に示します。

Shader "Custom/DeferredSurface" 
{

Properties 
{
    _Color ("Color", Color) = (1,1,1,1)
    _MainTex ("Albedo (RGB)", 2D) = "white" {}
    _Glossiness ("Smoothness", Range(0,1)) = 0.5
    _Metallic ("Metallic", Range(0,1)) = 0.0
}

SubShader 
{

Tags { "RenderType"="Opaque" }

CGINCLUDE

#include "HLSLSupport.cginc"
#include "UnityShaderVariables.cginc"

#define UNITY_PASS_DEFERRED
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "UnityPBSLighting.cginc"

sampler2D _MainTex;
half _Glossiness;
half _Metallic;
fixed4 _Color;

struct Input 
{
    float2 uv_MainTex;
};

void surf (Input IN, inout SurfaceOutputStandard o) 
{
    fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Alpha = c.a;
}
        
struct v2f_surf 
{
    float4 pos         : SV_POSITION;
    float2 pack0       : TEXCOORD0;
    half3  worldNormal : TEXCOORD1;
    float3 worldPos    : TEXCOORD2;
#ifndef DIRLIGHTMAP_OFF
    half3  viewDir     : TEXCOORD3;
#endif
    float4 lmap        : TEXCOORD4;
#ifdef LIGHTMAP_OFF
    #if UNITY_SHOULD_SAMPLE_SH
    half3 sh           : TEXCOORD5;
    #endif
#else
    #ifdef DIRLIGHTMAP_OFF
    float4 lmapFadePos : TEXCOORD5;
    #endif
#endif
};

float4 _MainTex_ST;

v2f_surf vert_surf (appdata_full v) 
{
    v2f_surf o;
    UNITY_INITIALIZE_OUTPUT(v2f_surf, o);
    o.pos = UnityObjectToClipPos(v.vertex);
    o.pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.worldNormal = UnityObjectToWorldNormal(v.normal);

#ifndef DIRLIGHTMAP_OFF
    o.viewDir = UnityWorldSpaceViewDir(o.worldPos);
#endif

#ifndef DYNAMICLIGHTMAP_OFF
    o.lmap.zw = v.texcoord2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
#else
    o.lmap.zw = 0;
#endif

#ifndef LIGHTMAP_OFF
    o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
    #ifdef DIRLIGHTMAP_OFF
    o.lmapFadePos.xyz = (mul(unity_ObjectToWorld, v.vertex).xyz - unity_ShadowFadeCenterAndType.xyz) * unity_ShadowFadeCenterAndType.w;
    o.lmapFadePos.w = (-UnityObjectToViewPos(v.vertex).z) * (1.0 - unity_ShadowFadeCenterAndType.w);
    #endif
#else
    o.lmap.xy = 0;
    #if UNITY_SHOULD_SAMPLE_SH
    o.sh = 0;
    o.sh = ShadeSHPerVertex(o.worldNormal, o.sh);
    #endif
#endif

    return o;
}

void frag_surf (v2f_surf IN,
    out half4 outDiffuse        : SV_Target0,
    out half4 outSpecSmoothness : SV_Target1,
    out half4 outNormal         : SV_Target2,
    out half4 outEmission       : SV_Target3) 
{
    Input surfIN;
    UNITY_INITIALIZE_OUTPUT(Input, surfIN);
    surfIN.uv_MainTex = IN.pack0.xy;

    float3 worldPos = IN.worldPos;
    fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));

    SurfaceOutputStandard o;
    UNITY_INITIALIZE_OUTPUT(SurfaceOutputStandard, o);
    o.Albedo = 0.0;
    o.Emission = 0.0;
    o.Alpha = 0.0;
    o.Occlusion = 1.0;
    o.Normal = IN.worldNormal;

    surf(surfIN, o);

    UnityGI gi;
    UNITY_INITIALIZE_OUTPUT(UnityGI, gi);
    gi.indirect.diffuse = 0;
    gi.indirect.specular = 0;
    gi.light.color = 0;
    gi.light.dir = half3(0, 1, 0);
    gi.light.ndotl = LambertTerm(o.Normal, gi.light.dir);

    UnityGIInput giInput;
    UNITY_INITIALIZE_OUTPUT(UnityGIInput, giInput);
    giInput.light = gi.light;
    giInput.worldPos = worldPos;
    giInput.worldViewDir = worldViewDir;
    giInput.atten = 1;

#if defined(LIGHTMAP_ON) || defined(DYNAMICLIGHTMAP_ON)
    giInput.lightmapUV = IN.lmap;
#else
    giInput.lightmapUV = 0.0;
#endif

#if UNITY_SHOULD_SAMPLE_SH
    giInput.ambient = IN.sh;
#else
    giInput.ambient.rgb = 0.0;
#endif

    giInput.probeHDR[0] = unity_SpecCube0_HDR;
    giInput.probeHDR[1] = unity_SpecCube1_HDR;

#if UNITY_SPECCUBE_BLENDING || UNITY_SPECCUBE_BOX_PROJECTION
    giInput.boxMin[0] = unity_SpecCube0_BoxMin; // .w holds lerp value for blending
#endif

#if UNITY_SPECCUBE_BOX_PROJECTION
    giInput.boxMax[0] = unity_SpecCube0_BoxMax;
    giInput.probePosition[0] = unity_SpecCube0_ProbePosition;
    giInput.boxMax[1] = unity_SpecCube1_BoxMax;
    giInput.boxMin[1] = unity_SpecCube1_BoxMin;
    giInput.probePosition[1] = unity_SpecCube1_ProbePosition;
#endif

    LightingStandard_GI(o, giInput, gi);

    outEmission = LightingStandard_Deferred(o, worldViewDir, gi, outDiffuse, outSpecSmoothness, outNormal);
#ifndef UNITY_HDR_ON
    outEmission.rgb = exp2(-outEmission.rgb);
#endif

    UNITY_OPAQUE_ALPHA(outDiffuse.a);
}

ENDCG

Pass 
{

Tags { "LightMode" = "Deferred" }

CGPROGRAM
#pragma vertex vert_surf
#pragma fragment frag_surf
#pragma target 3.0
#pragma exclude_renderers nomrt
#pragma multi_compile_prepassfinal
#pragma skip_variants FOG_LINEAR FOG_EXP FOG_EXP2
ENDCG

}

}

FallBack "Diffuse"

}

ここから自分の使わない機能を削っていって最適化していく方針で行くのが良いと思います。ただ、最適化するにしてもどこがどういった処理を行っているか把握することが必要だと思いますので、以下、いくつか概要を紹介したいと思います。

様々な分岐

シェーダの中では #if#ifdef を使った分岐がたくさんあります。これは、スタンダードシェーダは汎用シェーダになっていて様々なケースに対応できるように書かれているのですが、それをオブジェクトやシーンの状態に従って分岐し、必要なメンバや処理を選択できるようにしているからです。

例えば、Baked GI を ON / OFF すると、Static なオブジェクトに設定されるキーワード(LIGHTMAP_ONLIGHTMAP_OFF 等)が切り替えられます。フレームデバッガの「Shader: ***」のところで設定されているバリアントが確認できます。

f:id:hecomi:20160928191002g:plain

Static でないオブジェクトは常に OFF が設定されています。

pragma 文

いくつか見慣れない pragma 文があると思いますので、先に説明しておきます。

multi_compile_prepassfinal

#pragma multi_compile_prepassfinalCGPROGRAM ブロックに追加されています。これは組み込みで用意されているバリアントをまとめて設定してくれるショートカットにあたるのですが、本機能の明確な説明はドキュメント含めどこにも見当たりませんでした。multi_compile_fwdbase がフォワードレンダリングでモデルを描画する際に必要なバリアントの設定を行うのを考えると、同様に PrePassFinal で必要なバリアントの設定がここでまとめて行われていると考えられます。

PrePassFinal は旧 Deferred Lighting の最終段のパスになります。

multi_compile_deferred のような指定があったら分かりやすいのですが使いまわしているのでしょうか。このあたりは公開されている情報からだけでは分かりませんでした。ただし、どういったバリアントが変化しているかは Frame Debugger 等で確認できます。分岐の項と同じシーンをこの pragma 文を書かない状態で試してみました。

f:id:hecomi:20160928191428p:plain

ライト関連のバリアントが消えています。このことから、#pragma multi_compile_prepassfinal をつけるとオブジェクトやシーンの状態に応じて適切なキーワードを設定してくれるものであると考えられます。

exclude_renderers nomrt

#pragma exclude_renderers nomrt は、マルチレンダーターゲットがない環境(nomrt = no MRTs)を除外(exclude_renderes)する、という意味です。MRT をサポートしない環境でシェーダがコンパイルされないようになります。

マニュアルには載っていませんが、Unity 5.0 のタイミングで入ったようです。

skip_variants

#pragma multi_compile_*** のショートカットを使うといくつかのバリアントが自動で設定されてしまいます。これらを無効化するためには #pragma skip_variants *** という形でスキップしたいバリアントを指定します。

ディファードシェーディングでフォグを利用したいときは Image Effect の Global Fog を使います。Lighting の設定のフォグを使用したとしても不透明なオブジェクトには効かず、フォワードのパスで描画される半透明なオブジェクトにのみ影響します。フォグの処理は無駄となるため、ここではスキップしている、というわけです。なお、UNITY_PASS_DEFERRED が定義されていると、UnityCG.cginc の中でフォグのキーワードは #undef されているのでスキップは必要ない気もします。

シェーダの概要

では具体的にシェーダの中で具体的に何が行われているのか見ていきます。

先程のシェーダを見てもわかるようにかなりコードは長いです。しかしながら、ここでの目的はベイクされたライティング成分を計算することで、大部分は状態に応じて諸々の変数を指定された構造体に詰め込み、双方向反射率分布関数(BRDF)をライトの直接光から計算する UNITY_BRDF_PBS() と GI による間接光から計算する UNITY_BRDF_GI() の結果を足し合わせ、それをエミッションバッファに格納する、ということを行っているに過ぎません。

ベイク対象でない動的なライトは、このモデルの描画よりも後のレンダリングパスで計算を行います。中では同様に UNITY_BRDF_PBS() を計算しています。このあたりは Unity 5 の CommandBuffer を利用したレンダリングパイプラインの拡張について調べてみた - 凹みTips の記事でも少し触れました。

少しまとめると、ディファードでは動的なライトは G-Buffer 生成後にライト単位で計算されますが、アンビエントや事前計算されたライトプローブ、GI といった計算はフォワードと同じようにモデル単位で計算されているわけです。

入力構造体

IA から渡ってくる構造体を見てみます。

        
struct v2f_surf 
{
    float4 pos         : SV_POSITION;
    float2 pack0       : TEXCOORD0;
    half3  worldNormal : TEXCOORD1;
    float3 worldPos    : TEXCOORD2;
#ifndef DIRLIGHTMAP_OFF
    half3  viewDir     : TEXCOORD3;
#endif
    float4 lmap        : TEXCOORD4;
#ifdef LIGHTMAP_OFF
    #if UNITY_SHOULD_SAMPLE_SH
    half3 sh           : TEXCOORD5;
    #endif
#else
    #ifdef DIRLIGHTMAP_OFF
    float4 lmapFadePos : TEXCOORD5;
    #endif
#endif
};

LIGHTMAP_OFF は GI が使用されていないとき(= Precomputed Realtime GI および Baked GI のチェックを外したとき)にセットされます。

DIRLIGHTMAP_*** は Sitatic なオブジェクトに対して、General GI の Directional Mode に応じて設定されます。

Directional Mode バリアント
Non-Directional DIRLGIHTMAP_OFF
Directional DIRLGIHTMAP_COMBINED
Directional Specular DIRLGIHTMAP_SEPARATE

余談ですが、DIRLGIHTMAP_COMBINED および DIRLGIHTMAP_SEPARATE はキーワードになっているのですが、DIRLGIHTMAP_OFF は昔はバリアントになっていたようですが今は(おそらくバリアント節約のために)セットされていません。代わりに HLSLSupport.cginc の中で両方のキーワードがセットされていないときに、#define されるようになっています。そのため、グラフィックデバッガからは確認できません。

これらのバリアントは、後でも触れますが、UnityGlobalIllumination.cginc の中で行われる GI の計算の分岐でも使われています。

頂点シェーダ

頂点シェーダを見ていきます。

v2f_surf vert_surf (appdata_full v) 
{
    v2f_surf o;
    UNITY_INITIALIZE_OUTPUT(v2f_surf, o);
    o.pos = UnityObjectToClipPos(v.vertex);
    o.pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
    o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    o.worldNormal = UnityObjectToWorldNormal(v.normal);

#ifndef DIRLIGHTMAP_OFF
    o.viewDir = UnityWorldSpaceViewDir(o.worldPos);
#endif

#ifndef DYNAMICLIGHTMAP_OFF
    o.lmap.zw = v.texcoord2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
#else
    o.lmap.zw = 0;
#endif

#ifndef LIGHTMAP_OFF
    o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
    #ifdef DIRLIGHTMAP_OFF
    o.lmapFadePos.xyz = (mul(unity_ObjectToWorld, v.vertex).xyz - unity_ShadowFadeCenterAndType.xyz) * unity_ShadowFadeCenterAndType.w;
    o.lmapFadePos.w = (-UnityObjectToViewPos(v.vertex).z) * (1.0 - unity_ShadowFadeCenterAndType.w);
    #endif
#else
    o.lmap.xy = 0;
    #if UNITY_SHOULD_SAMPLE_SH
    o.sh = 0;
    o.sh = ShadeSHPerVertex(o.worldNormal, o.sh);
    #endif
#endif

    return o;
}

最初は構造体を初期化し、位置や法線を通常通り詰めています。

ST という文字が色々見えますが、これは Scale Translate のことで、テクスチャ名が _MainTex なら float4 _MainTex_ST でテクスチャの TilingOffset を取ってこれるものです。

TRANSFORM_TEX() はこの X_ST 成分を考慮して UV を計算してくれるものです。unity_***ST は Untiy 側が管理しているテクスチャの情報です。unity_LightmapST の詳しい説明はこちらに載っていました。

lmap には xy 成分に Baked GI の結果が、zw 成分に Precomputed Realtime GI の結果が格納されているようです。lmapFadePos は Non-Directional モードのときに計算されるようですが、コードを読むとフラグメントシェーダ側で利用されることはないように思えます(いまのところ不明、消しても問題ない?)。

UNITY_SHOULD_SAMPLE_SH のブロックには対象が Static でない動的なオブジェクトの場合に入ります。ShadeSHPerVertex() は頂点ごとに球面調和(SH: Spherical Harmonics)関数を使ってベイクされたライティングを復元するもので UnityStandardUtils.cginc に定義されており、中では ShadeSH9()UnityCG.cginc で定義されている)を呼んでアンビエントおよびライトプローブの強さを計算しています。

球面調和関数によるライティングは以下の記事が詳しいです。要はフーリエ変換のようなことを 3 次元的な情報を持つキューブマップへと適用しているイメージです。

ここでは、それを頂点単位で行っている、というわけですね。

フラグメントシェーダ

次にフラグメントシェーダを見ていきます。

サーフェスシェーダの呼び出し

まずは前半です。

struct Input 
{
    float2 uv_MainTex;
};

void surf(Input IN, inout SurfaceOutputStandard o) 
{
    fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Alpha = c.a;
}
        
void frag_surf (v2f_surf IN,
    out half4 outDiffuse        : SV_Target0,
    out half4 outSpecSmoothness : SV_Target1,
    out half4 outNormal         : SV_Target2,
    out half4 outEmission       : SV_Target3) 
{
    Input surfIN;
    UNITY_INITIALIZE_OUTPUT(Input, surfIN);
    surfIN.uv_MainTex = IN.pack0.xy;

    float3 worldPos = IN.worldPos;
    fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));

    SurfaceOutputStandard o;
    UNITY_INITIALIZE_OUTPUT(SurfaceOutputStandard, o);
    o.Albedo = 0.0;
    o.Emission = 0.0;
    o.Alpha = 0.0;
    o.Occlusion = 1.0;
    o.Normal = IN.worldNormal;

    surf(surfIN, o);

frag_surf() は G-Buffer に値を出力する形で引数をとっています。Input および surf()サーフェスシェーダで記述した形そのままです。変換後はこの関数をフラグメントシェーダから呼び出して情報を SurfaceOutputStandard 構造体に詰めている形になっています。特に複雑なことはしておらずシンプルです。

UnityGI

次に GI に必要な構造体に値を詰めるところです。ここではライトの情報が保存されます。

UnityGI gi;
UNITY_INITIALIZE_OUTPUT(UnityGI, gi);
gi.indirect.diffuse = 0;
gi.indirect.specular = 0;
gi.light.color = 0;
gi.light.dir = half3(0, 1, 0);
gi.light.ndotl = LambertTerm(o.Normal, gi.light.dir);

UnityGI および次の節で見る UnityGIInputUnityLightingCommon.cginc にて宣言されています。UnityGI は次のような構造体です。

struct UnityLight
{
    half3 color;
    half3 dir;
    half  ndotl;
};

struct UnityIndirect
{
    half3 diffuse;
    half3 specular;
};

struct UnityGI
{
    UnityLight light;
    #ifdef DIRLIGHTMAP_SEPARATE
        #ifdef LIGHTMAP_ON
            UnityLight light2;
        #endif
        #ifdef DYNAMICLIGHTMAP_ON
            UnityLight light3;
        #endif
    #endif
    UnityIndirect indirect;
};

LIGHTMAP_ON のとき(Baked GI が ON のとき)は、light2 が追加され、DYNAMICLIGHTMAP_ONのとき(Precomputed Realtime GI が ON のとき)は、light3 が追加されます。これらのメンバは後の関数の中でセットされるので、特にここでは値がセットされたりはしません。light に関しても、後でまた初期化されるので単純に以下の形で良い気がします。

UnityGI gi;
UNITY_INITIALIZE_OUTPUT(UnityGI, gi);

これは、GI に応じていくつライトの情報が必要かを保存しておく構造体になっています。Directional Specular のとき、つまり DIRLIGHTMAP_SEPARATE が定義されているときが一番情報が必要なわけです。

UnityGIInput

UnityGIInput にデータをつめます。構造体は以下のようになっています。

struct UnityGIInput 
{
    UnityLight light;
    float3 worldPos;
    half3 worldViewDir;
    half atten;
    half3 ambient;
    float4 lightmapUV;
    float4 boxMax[2];
    float4 boxMin[2];
    float4 probePosition[2];
    float4 probeHDR[2];
};

頂点シェーダから渡ってきた値や組み込みの変数をセットします。

    UnityGIInput giInput;
    UNITY_INITIALIZE_OUTPUT(UnityGIInput, giInput);
    giInput.light = gi.light;
    giInput.worldPos = worldPos;
    giInput.worldViewDir = worldViewDir;
    giInput.atten = 1;

#if defined(LIGHTMAP_ON) || defined(DYNAMICLIGHTMAP_ON)
    giInput.lightmapUV = IN.lmap;
#else
    giInput.lightmapUV = 0.0;
#endif

#if UNITY_SHOULD_SAMPLE_SH
    giInput.ambient = IN.sh;
#else
    giInput.ambient.rgb = 0.0;
#endif

    giInput.probeHDR[0] = unity_SpecCube0_HDR;
    giInput.probeHDR[1] = unity_SpecCube1_HDR;

#if UNITY_SPECCUBE_BLENDING || UNITY_SPECCUBE_BOX_PROJECTION
    giInput.boxMin[0] = unity_SpecCube0_BoxMin;
#endif

#if UNITY_SPECCUBE_BOX_PROJECTION
    giInput.boxMax[0] = unity_SpecCube0_BoxMax;
    giInput.probePosition[0] = unity_SpecCube0_ProbePosition;
    giInput.boxMax[1] = unity_SpecCube1_BoxMax;
    giInput.boxMin[1] = unity_SpecCube1_BoxMin;
    giInput.probePosition[1] = unity_SpecCube1_ProbePosition;
#endif

unity_SpecCubeX_HDR および unity_SpecCubeX_BoxMinunity_SpecCubeX_BoxMaxUnityShaderVariables.cginc で宣言されているリフレクションプローブの組み込み変数をセットしています。ただ、ディファードではリフレクションはモデル描画の後段の RenderDeferred.Reflection および RenderDeferred.ReflectionToEmissive で G-Buffer に対して処理されるので、ここでは必要ない気もしています(このブロックを外しても影響がなかったので...)。

ライティング関数の実行

最後に構造体および出力用の変数ををライティングの関数に渡します。

    LightingStandard_GI(o, giInput, gi);

    outEmission = LightingStandard_Deferred(o, worldViewDir, gi, outDiffuse, outSpecSmoothness, outNormal);
#ifndef UNITY_HDR_ON
    outEmission.rgb = exp2(-outEmission.rgb);
#endif

    UNITY_OPAQUE_ALPHA(outDiffuse.a);
}

ライティング関数の詳細は省きます(というか私も深く理解できていないのです)が、LightingStandard_GI()UnityGIInput に詰め込まれた情報をもとに UnityGI へと GI の光の情報を書き出しています。LightingStandard_Deferred() は、こうして得られた情報を使って BRDF の直接光の寄与成分を計算する UNITY_BRDF_PBS() および間接光の寄与成分を計算する UNITY_BRDF_GI() を計算します。この結果が、HDR の場合は exp で圧縮される部分を挟んだ後、エミッションバッファへと書き出さます。

UNITY_OPAWUE_ALPHA は、単に与えられた引数(拡散色のバッファのアルファ成分)に 1.0 を代入しているだけです。

これでようやく、全体の流れが追えました。

既存のシェーダを改造する

さて、長く険しい道程を経てシェーダの大まかな流れを理解したので、前回のレイマーチしたオブジェクトがこの計算を通るように改造してみます。レイマーチ部はすこし改造してしまっていますが、意味合いとしては、出力された位置・法線をポリゴンのそれらと置き換える、という感じです。長いですがほとんどコピペです。

#ifndef PBS_H
#define PBS_H

#include "HLSLSupport.cginc"
#include "UnityShaderVariables.cginc"

#define UNITY_PASS_DEFERRED
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "UnityPBSLighting.cginc"

#include "Raymarching.cginc"

struct VertPbsObjectOutput
{
    float4 pos         : SV_POSITION;
    float4 screenPos   : TEXCOORD0;
    float4 worldPos    : TEXCOORD1;
    float3 worldNormal : TEXCOORD2;
    float4 lmap        : TEXCOORD3;
#ifdef LIGHTMAP_OFF
    #if UNITY_SHOULD_SAMPLE_SH
    half3 sh           : TEXCOORD4;
    #endif
#endif
};

VertPbsObjectOutput VertPbsObject(appdata_full v)
{
    VertPbsObjectOutput o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.screenPos = o.pos;
    o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    o.worldNormal = UnityObjectToWorldNormal(v.normal);

#ifndef DYNAMICLIGHTMAP_OFF
    o.lmap.zw = v.texcoord2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
#else
    o.lmap.zw = 0;
#endif

#ifndef LIGHTMAP_OFF
    o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
#else
    o.lmap.xy = 0;
    #if UNITY_SHOULD_SAMPLE_SH
    o.sh = 0;
    o.sh = ShadeSHPerVertex(o.worldNormal, o.sh);
    #endif
#endif

    return o;
}

void FragPbsObject(
    VertPbsObjectOutput i,
    out half4 outDiffuse        : SV_Target0,
    out half4 outSpecSmoothness : SV_Target1,
    out half4 outNormal         : SV_Target2,
    out half4 outEmission       : SV_Target3,
    out float outDepth          : SV_Depth) 
{
    RaymarchInput rayIn;
    rayIn.rayDir = GetCameraDirection(i.screenPos);
    rayIn.startPos = i.worldPos;
    rayIn.polyNormal = i.worldNormal;
    rayIn.minDist = _MinDistance;
    rayIn.maxDist = GetCameraMaxDistance();
    rayIn.loop = _Loop;

    RaymarchOutput rayOut = Raymarch(rayIn);
    outDepth = rayOut.depth;

    float3 worldPos = rayOut.pos;
    float3 worldNormal = 2.0 * rayOut.normal - 1.0;
    fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(worldPos));

    SurfaceOutputStandard o;
    UNITY_INITIALIZE_OUTPUT(SurfaceOutputStandard, o);
    o.Albedo = _Color.rgb;
    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Emission = 0.0;
    o.Alpha = _Color.a;
    o.Occlusion = 1.0;
    o.Normal = worldNormal;

    UnityGI gi;
    UNITY_INITIALIZE_OUTPUT(UnityGI, gi);
    gi.indirect.diffuse = 0;
    gi.indirect.specular = 0;
    gi.light.color = 0;
    gi.light.dir = half3(0, 1, 0);
    gi.light.ndotl = LambertTerm(worldNormal, gi.light.dir);

    UnityGIInput giInput;
    UNITY_INITIALIZE_OUTPUT(UnityGIInput, giInput);
    giInput.light = gi.light;
    giInput.worldPos = worldPos;
    giInput.worldViewDir = worldViewDir;
    giInput.atten = 1;

#if defined(LIGHTMAP_ON) || defined(DYNAMICLIGHTMAP_ON)
    giInput.lightmapUV = i.lmap;
#else
    giInput.lightmapUV = 0.0;
#endif

#if UNITY_SHOULD_SAMPLE_SH
    giInput.ambient = i.sh;
#else
    giInput.ambient.rgb = 0.0;
#endif

    giInput.probeHDR[0] = unity_SpecCube0_HDR;
    giInput.probeHDR[1] = unity_SpecCube1_HDR;

#if UNITY_SPECCUBE_BLENDING || UNITY_SPECCUBE_BOX_PROJECTION
    giInput.boxMin[0] = unity_SpecCube0_BoxMin; // .w holds lerp value for blending
#endif

#if UNITY_SPECCUBE_BOX_PROJECTION
    giInput.boxMax[0] = unity_SpecCube0_BoxMax;
    giInput.probePosition[0] = unity_SpecCube0_ProbePosition;
    giInput.boxMax[1] = unity_SpecCube1_BoxMax;
    giInput.boxMin[1] = unity_SpecCube1_BoxMin;
    giInput.probePosition[1] = unity_SpecCube1_ProbePosition;
#endif

    LightingStandard_GI(o, giInput, gi);

    outEmission = LightingStandard_Deferred(o, worldViewDir, gi, outDiffuse, outSpecSmoothness, outNormal);
#ifndef UNITY_HDR_ON
    outEmission.rgb = exp2(-outEmission.rgb);
#endif

    UNITY_OPAQUE_ALPHA(outDiffuse.a);
}

#endif

それでは結果を見てみましょう。

f:id:hecomi:20160930152616p:plain

左半分が通常のスタンダードサーフェスシェーダ、右がレイマーチしたものです。下側が両者ともに Static なオブジェクトとなっています。エミッションバッファも見てみます。

f:id:hecomi:20160930153219p:plain

アンビエント、ライトプローブ、GI の影響を受けているのがわかります。...が、このレイマーチしたオブジェクトはキューブをいじって球にしているのですが、何やらもとのキューブがうっすらと見えます。

Static にしてしまった下側の球は、もとのポリゴンの形状をもとにライトマップを焼かれてしまっているのでどうしようもありません...。頂点シェーダから渡ってくる lmap をいじったところで、UV が変わるだけでライティングには影響がないからです。

一方、Static でない上側の球にうっすらとキューブのライティングが現れているのは、球面調和関数によるライティングが頂点単位で行われていたからだと考えられます。ShadeSHPerVertex() ではキューブの法線の情報を使ってライティングの情報をデコードしていたので、それがここに現れているわけです。そこで、代わりに処理は重くなりますが、ピクセル単位でライティングを行う ShadeSHPerPixel() を使ってレイマーチ後の位置・法線情報を使ってライティングをデコードしてみたいと思います。下記公式エントリを参考にします。

struct VertPbsObjectOutput
{
    float4 pos         : SV_POSITION;
    float4 screenPos   : TEXCOORD0;
    float4 worldPos    : TEXCOORD1;
    float3 worldNormal : TEXCOORD2;
    float4 lmap        : TEXCOORD3;
// #ifdef LIGHTMAP_OFF
//     #if UNITY_SHOULD_SAMPLE_SH
//     half3 sh           : TEXCOORD4;
//     #endif
// #endif
};

VertPbsObjectOutput VertPbsObject(appdata_full v)
{
    ...
#ifndef LIGHTMAP_OFF
    ...
#else
    ...
    // #if UNITY_SHOULD_SAMPLE_SH
    // o.sh = 0;
    // o.sh = ShadeSHPerVertex(o.worldNormal, o.sh);
    // #endif
#endif
    ...
}

void FragPbsObject(...)
{
    ...
#if UNITY_SHOULD_SAMPLE_SH
    // giInput.ambient = i.sh;
    giInput.ambient = ShadeSHPerPixel(worldNormal, 0.0, worldPos);
#else
    ...
#endif
    ...
}

f:id:hecomi:20160930161507p:plain

頂点でやるよりも少し明るくなっていまが、きれいになりました。同じ考えで、Static なものに関しても、ベイク時に周囲に影響は及ぼすものの自身はライトマップを使わずに球面調和関数によるライティングを使う、という考えでシェーダを書き直すことは出来ます。ただ、呼び出している関数の内部で結構分岐が多く、同様のライティングを再現するコードにまとめるのは少し面倒そうです。Static なときはなるべくもとのポリゴンの形状からかけ離れない形状にするのが吉だと思います(影も変形した形状ではベイクされないので)。

おわりに

スタンダードシェーダの内容を追いかけるのは、資料も少なく、推測しながら進めなければならない所も多々あり、非常に骨の折れる仕事でした...。

参考