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

しゅみぷろ

プログラミングとか

Forward Renderingについてまとめてみた

Unity Shader

UnityのForwardRenderingについて自分なりにまとめてみます。

docs.unity3d.com

f:id:es_program:20160426003316p:plain

ForwardRenderingで実行されるパス

ForwardRenderingでは、1つかそれ以上のパスが実行されます。実行されるパスは

  • Baseパス (ForwardBase)
    • オブジェクトを1つのピクセルライティングと、頂点ライティング、SHライティングによってレンダリングする。
  • Additionalパス (ForwardAdd)
    • 追加されたピクセルライティングを用いるライトをレンダリングする。

です。それぞれのパスでライティングが計算されます。

ライティングの計算方法

ライティングの計算方法は以下の3種類です。

  • ピクセルライティング
    • フラグメントシェーダーでライティングを計算しピクセル色に適用する。ピクセルごとに計算処理を行うため高価なライティング。
  • 頂点ライティング
    • 頂点ごとにライティングを計算する。頂点ごとに計算処理を行うので比較的安価なライティング。
  • 球面調和(SH)によるライティング
    • 頂点シェーダーにより複数のライトの球面調和(SH)を求めることによるライティング。計算コストが低く実質コスト0。

どのライトにどのライティングが適用されるか

各オブジェクトに作用するいくつかの最も明るいライトはピクセルライティングモードでレンダリングされます。次に最大で4つのポイントライトが頂点ごとに計算されます。他のライトは球面調和(SH)として計算され、近似値になりますがより速くなります。ライトがピクセルライティングとなるかどうかは次のことに依存します。

  • RenderModeをNotImportantに設定してあるライトは頂点ライティングかSHライティングになります。
  • RenderModeをImportantに設定してあるライトは常にピクセルライティングになります。
  • 最も明るいライトは常にピクセルライティングになります。
  • もしQuaritySettingsのRendering->PixelLightCountの数よりピクセルライティングが適用されるライトが少ない場合、足りない分だけ頂点ライティングまたはSHライティングが行われるはずだったライトをピクセルライティングにより描画します(計算処理の昇華)。これはForwardAddパスで計算が行われます。

docs.unity3d.com

例えば

f:id:es_program:20160426003736p:plain

ライトAからHが同じカラーと強度であると仮定した場合、すべて自動レンダリングモードであるため、このオブジェクトにおいてこの順番のとおりにライトがソートされます。最も明るい光(AからD)はピクセルライティングでレンダリングされ、4 つのライト(DからG)が頂点ライティングとなり、最後に残りのライト(G から H)が球面調和(SH)となります。

f:id:es_program:20160426003743p:plain

Baseパスは一つのピクセルごとの指向性ライトおよびすべての頂点ライティング/SHライティングを適用します。 他のピクセルライトはAdditionalパス(追加のパス)でレンダリングされ、ひとつのライトごとにひとつのパスを使用します

ForwardRenderingで実行されるシェーダーを自作する準備

シェーダーの基本はこちらを参照してください。

 

esprog.hatenablog.com

 

 

ForwardBase、ForwardAddでの処理をカスタマイズすることができます。この場合、自身で頂点・フラグメントシェーダーにおけるライティング計算を行う必要があります。

ForwardBaseパスで実行されるシェーダーを書く場合、Pass内でTags{"LightMode"="ForwardBase"}を指定する必要があります。シェーダーを書く際に、ForwardBaseパスではLightColor0(一番明るいライトの色)やWirldSpaceLightPos0(一番明るいライトの位置。指向性ライトの場合はその方向)などの値がUnity側で自動的に値が設定されます(この値を使う場合は、Cg/HLSLで同名の変数を宣言するか、その変数の宣言を含むライブラリをincludeする必要があります)。

ForwardAddパスも同様です。ただし、Pass内で設定するTagsはTags{"LightMode"="ForwardAdd"}とします。

シェーダーを書いてみる

実際にForwardBaseパスで実行されるシェーダーを書いてみました。

github.com

シェーダーは以下の様な感じです。頂点のみで色を計算するのでライティングは高速です。 1つのメインとなるライトを頂点シェーダーでDiffuseカラーとして計算し、他のライトはSHライティングを行っています。

Shader "ForwardRendering/CustomLightingVertexForwardBase"
{
    SubShader
    {
        Pass
        {
            Tags{"LightMode"="ForwardBase"}

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float4 diffuse : COLOR0;
            };

            //ライトの色
            fixed4 _LightColor0;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                float3 normal = UnityObjectToWorldNormal(v.normal);
                //Diffuseカラー計算
                o.diffuse = max(0, dot(normal, _WorldSpaceLightPos0.xyz)) * _LightColor0;
                //球面調和
                o.diffuse.rgb += ShadeSH9(half4(normal,1));
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = float4(0.5, 0.5, 0.5, 1);
                col *= i.diffuse;
                return col;
            }
            ENDCG
        }
    }
}

f:id:es_program:20160426003824p:plain

球に注目するとわかりますが、頂点ごとに計算されているためか色の遷移がかなり荒いです。

メインのライトをピクセルライティングにしてみましょう。新しく以下のシェーダーを作成しました。

Shader "ForwardRendering/CustomLightingForwardBase"
{
    SubShader
    {

        Pass
        {
            Tags{"LightMode"="ForwardBase"}

            CGPROGRAM
           #pragma vertex vert
           #pragma fragment frag
           #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 normal : NORMAL;
                float4 diffuse : COLOR0;
            };

            //ライトの色
            fixed4 _LightColor0;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
                o.normal = UnityObjectToWorldNormal(v.normal);
                //球面調和
                o.diffuse.rgb = ShadeSH9(half4(v.normal,1));
                o.diffuse.a = 1;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = float4(0.5, 0.5, 0.5, 1);
                //Diffuseカラー計算
                i.diffuse += max(0, dot(i.normal, _WorldSpaceLightPos0.xyz)) * _LightColor0;
                col *= i.diffuse;
                return col;
            }
            ENDCG
        }
    }
}

f:id:es_program:20160426003837p:plain

左がピクセルライティングを行っているシェーダーを適用させたもので、右は頂点ライティングを適用させたものです。 結構見た目に差があります。

まとめ

ForwardRenderingのライティングでは

  • ピクセルライティング
  • 頂点ライティング
  • 球面調和

から、ライトの設定等で適したものが適用されます。 ピクセルライティングを適用するライトが多い場合、負荷が大きくなるので

  • QuaritySettingsのRendering->PixelLightCountの数
  • ライトの設定がImportantのもの

を増やしすぎるのは要注意です。 また、ForwardBaseパスでライティングを計算するシェーダーも書いてみました。

このプロジェクトはGitHubで公開していますので、興味があったら見てみてください。

github.com