夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

UnityでShaderを勉強する その⑮基本照明のフォンシェーディング

本日はShader勉強枠です。

実際のサンプルを読み解きながら構造を勉強しています。

Unityシェーダーのサンプルやデモを数多く紹介しているサイトShaders Laboratoryを基に勉強していきます。

www.shaderslab.com

〇基本照明のフォンシェーディング

フォンシェーディングとは3DCGの陰影計算の手法のことで滑らかな見た目を提供することができます。

f:id:Holomoto-Sumire:20200501080936j:plain
Wikipediaより

今回のShaderは以下になります。

Shader "Custom/Lighting/BasicLightingPerVertex"
{
    Properties
    {
        _MainTex ("Main Texture", 2D) = "white" {}
 
        [Header(Ambient)]
        _Ambient ("Intensity", Range(0., 1.)) = 0.1
        _AmbColor ("Color", color) = (1., 1., 1., 1.)
 
        [Header(Diffuse)]
        _Diffuse ("Val", Range(0., 1.)) = 1.
        _DifColor ("Color", color) = (1., 1., 1., 1.)
 
        [Header(Specular)]
        [Toggle] _Spec("Enabled?", Float) = 0.
        _Shininess ("Shininess", Range(0.1, 10)) = 1.
        _SpecColor ("Specular color", color) = (1., 1., 1., 1.)
 
        [Header(Emission)]
        _EmissionTex ("Emission texture", 2D) = "gray" {}
        _EmiVal ("Intensity", float) = 0.
        [HDR]_EmiColor ("Color", color) = (1., 1., 1., 1.)
    }
 
    SubShader
    {
        Pass
        {
            Tags { "RenderType"="Opaque" "Queue"="Geometry" "LightMode"="ForwardBase" }
 
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
 
            // Change "shader_feature" with "pragma_compile" if you want set this keyword from c# code
            #pragma shader_feature __ _SPEC_ON
 
            #include "UnityCG.cginc"
 
            struct v2f {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                fixed4 light : COLOR0;
            };
 
            fixed4 _LightColor0;
            
            // Diffuse
            fixed _Diffuse;
            fixed4 _DifColor;
 
            //Specular
            fixed _Shininess;
            fixed4 _SpecColor;
            
            //Ambient
            fixed _Ambient;
            fixed4 _AmbColor;
 
            v2f vert(appdata_base v)
            {
                v2f o;
                // World position
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
 
                // Clip position
                o.pos = mul(UNITY_MATRIX_VP, worldPos);
 
                // Light direction
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
 
                // Normal in WorldSpace
                float3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
 
                // Camera direction
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz);
 
                // Compute ambient lighting
                fixed4 amb = _Ambient * _AmbColor;
 
                // Compute the diffuse lighting
                fixed4 NdotL = max(0., dot(worldNormal, lightDir) * _LightColor0);
                fixed4 dif = NdotL * _Diffuse * _LightColor0 * _DifColor;
 
                o.light = dif + amb;
 
                // Compute the specular lighting
                #if _SPEC_ON
                float3 refl = reflect(-lightDir, worldNormal);
                float RdotV = max(0., dot(refl, viewDir));
                fixed4 spec = pow(RdotV, _Shininess) * _LightColor0 * ceil(NdotL) * _SpecColor;
 
                o.light += spec;
                #endif
                
                o.uv = v.texcoord;
 
                return o;
            }
 
            sampler2D _MainTex;
 
            // Emission
            sampler2D _EmissionTex;
            fixed4 _EmiColor;
            fixed _EmiVal;
 
            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 c = tex2D(_MainTex, i.uv);
                c.rgb *= i.light;
 
                // Compute emission
                fixed4 emi = tex2D(_EmissionTex, i.uv).r * _EmiColor * _EmiVal;
                c.rgb += emi.rgb;
 
                return c;
            }
 
            ENDCG
        }
    }
}

● Properties

 Properties
    {
        _MainTex ("Main Texture", 2D) = "white" {}
 
        [Header(Ambient)]
        _Ambient ("Intensity", Range(0., 1.)) = 0.1
        _AmbColor ("Color", color) = (1., 1., 1., 1.)
 
        [Header(Diffuse)]
        _Diffuse ("Val", Range(0., 1.)) = 1.
        _DifColor ("Color", color) = (1., 1., 1., 1.)
 
        [Header(Specular)]
        [Toggle] _Spec("Enabled?", Float) = 0.
        _Shininess ("Shininess", Range(0.1, 10)) = 1.
        _SpecColor ("Specular color", color) = (1., 1., 1., 1.)
 
        [Header(Emission)]
        _EmissionTex ("Emission texture", 2D) = "gray" {}
        _EmiVal ("Intensity", float) = 0.
        [HDR]_EmiColor ("Color", color) = (1., 1., 1., 1.)
    }

f:id:Holomoto-Sumire:20200501081508j:plain

[Header()]アトリビュートによって区切られています。

今回初めて扱うものとして

   [Toggle] _Spec("Enabled?", Float) = 0.

があります。

[Toggle]は扱うデータ形式IntもしくはFloatの時に使用できます。 値が1の際はオン、0の場合オフになります。

● Tags {}
 Tags { "RenderType" = "Opaque" "Queue" = "Geometry" "LightMode" = "ForwardBase" }

 ここではレンダリングタイプをOpaque=不透明QueueをGeometryライティングモードをForwardBaseに指定しています。

 Queueというものはレンダリングの順番です。 Geometryは不透明を指し値としては2000を取ります。 たいていの場合同じQueue同士であればカメラに近いオブジェクトが描画されますが、1999というQueueを持つオブジェクトと2000のQueueを持つオブジェクトが重なっている場合1999のオブジェクトが先に描画されます。

●v2f構造体
  struct v2f {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                fixed4 light : COLOR0;
            };

ここでは以下のものが定義されています。

・SV_POSITION=システム上の位置座標

・TEXCOORD0=_MainTexのuv座標

・COLOR0=線形補間された色

●v2f vert(appdata_base v){}

頂点シェーダーの関数です。

v2f vert(appdata_base v)
            {
                v2f o;
                // World position
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);

                // Clip position
                o.pos = mul(UNITY_MATRIX_VP, worldPos);

                // Light direction
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

                // Normal in WorldSpace
                float3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));

                // Camera direction
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz);

                // Compute ambient lighting
                fixed4 amb = _Ambient * _AmbColor;

                // Compute the diffuse lighting
                fixed4 NdotL = max(0., dot(worldNormal, lightDir) * _LightColor0);
                fixed4 dif = NdotL * _Diffuse * _LightColor0 * _DifColor;

                o.light = dif + amb;

                // Compute the specular lighting
                #if _SPEC_ON
                float3 refl = reflect(-lightDir, worldNormal);
                float RdotV = max(0., dot(refl, viewDir));
                fixed4 spec = pow(RdotV, _Shininess) * _LightColor0 * ceil(NdotL) * _SpecColor;

                o.light += spec;
                #endif

                o.uv = v.texcoord;

                return o;
            }

appdata_base はUnityCG.cgincというファイルでデフォルトで宣言されています。この中で宣言されているものをvertの因数として使用しているという意味になります。

これを使用するためには

#include "UnityCG.cginc"

という分を記述しインクルード(ほかのファイルで宣言されている内容を取り込む)します。

頂点シェーダーの関数は以下の部分が定型になります。

            v2f vert(appdata_base v) {
                v2f o;

                return o;
            }
  // World position
  float4 worldPos = mul(unity_ObjectToWorld, v.vertex);

unity_ObjectToWorldモデル行列を意味します。これに頂点の位置をかけることでワールド座標に変換しています。

 // Clip position
 o.pos = mul(UNITY_MATRIX_VP, worldPos);

o.posはSV_POSITIONを指します。 UNITY_MATRIX_VPは現在のビュー行列×射影行列を意味し、これにワールド座標をかけたものがSV_POSITIONになります。

// Light direction
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);

_WorldSpaceLightPos0はUNITY_CG.cgincで定義されている値でワールド座標でのライトの位置(x,y,z)を示します。lightDirはこれを正規化した値になります。

 float3 worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));

worldNormalはワールド座標での法線ベクトルを指し頂点の法線ベクトルとモデル行列をかけたものを正規化したものになります。

unity_WorldToObjectをかけることでUnityで扱える座標に変換するという意味でしょうか?

// Camera direction
 float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - worldPos.xyz);

カメラの方向viewDirはワールド空間でのカメラの位置からworldPosを引いたものを正規化したものになります。

// Compute ambient lighting
    fixed4 amb = _Ambient * _AmbColor;

ambはスライダーで指定される_Ambientの値×カラーで指定されます。これは環境照明の計算になります。

_Ambientの値は0~1を取るので0が与えられている場合カラーは(0,0,0,0)で真っ黒になります。

 // Compute the diffuse lighting
     fixed4 NdotL = max(0., dot(worldNormal, lightDir) * _LightColor0);
     fixed4 dif = NdotL * _Diffuse * _LightColor0 * _DifColor;

     o.light = dif + amb;

NdotLは前回でも扱いましたが法線ベクトルとライトの内積を示します。光源の影響による明るさを意味します。

difはNdotL×Diffuse×ライトの色を示すLightColor0×_DifColorで計算されています。

NdotL * LightColor0 * DifColorは_DifColorで指定した色と光源の色の影響による明るさを意味します。

ここに_Diffuseで指定した任意の値が入ることでその強さを変更しています。

このdifとambを合わせたものがv2fのlight=COLOR0=線形補間された色として入ります。

                // Compute the specular lighting
                #if _SPEC_ON
                float3 refl = reflect(-lightDir, worldNormal);
                float RdotV = max(0., dot(refl, viewDir));
                fixed4 spec = pow(RdotV, _Shininess) * _LightColor0 * ceil(NdotL) * _SpecColor;

                o.light += spec;
                #endif

ここでは鏡面照明を計算しています。

#if SPEC_ONはproperty{}で [Toggle] Spec("Enabled?", Float) がオンになっている場合#endifまでを実行するという条件文です。

 reflect();は入射光と法線ベクトルを使用して浮動小数点型反射ベクトルを返します。これがreflになります。

docs.microsoft.com

RdotBはreflとviewDirの内積です。max(A,B);はAとBで大きいほうを返します。viewDirはカメラの方向です。

specは RdotV^Shininessに光源の色とceil(NdotL)とSpaceColorが掛けられています。

 ここでceil(NdotL)はNdotLの小数部を切り上げる処理を行っています。(例ceil(1234.56)=1235)

 このspecを2fのlight=COLOR0=線形補間された色に加算しています。

この処理はマテリアル側でEnabled?のチェックボックスがオンの時に行われます。

●fixed4 frag(v2f i) : SV_Target{}

            fixed4 frag(v2f i) : SV_Target
            {
               fixed4 c = tex2D(_MainTex, i.uv);
                c.rgb *= i.light;
                
                // Compute emission
                fixed4 emi = tex2D(_EmissionTex, i.uv).r * _EmiColor * _EmiVal;
                c.rgb += emi.rgb;
                
                return c;
            }

フラグメントシェーダーの関数です。

fixed4 frag(v2f i) : SV_Targetで指定されているセマンティクスSV_Targetはピクセルの色を意味します。

 このフラグメントシェーダーの処理はピクセルの色として処理されます。

 fixed4 c = tex2D(_MainTex, i.uv);
 c.rgb*=i.lightl

 cは_MainTexのuvを示します。

 c.rgb*=i.lightで_MainTexのrgbにv2fのlightを加算しています。

    // Compute emission
                fixed4 emi = tex2D(_EmissionTex, i.uv).r * _EmiColor * _EmiVal;
                c.rgb += emi.rgb;

発光の加算処理です。

 emiはEmissionTexにEmiColorで指定した色と_EmiValの値を加算しています。

 最後に_MainTexのrgbにemiのrgbを加算しています。

 以上がこのShaderの処理を読み解いた内容になります。

●UNITYCG.cgin

 内部はこちらで見ることができます。 github.com