夜風のMixedReality

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

UnityでShaderを勉強する その⑯基本照明のフォンシェーディング(フラグメントシェーダー)

本日はShader勉強枠です。

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

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

www.shaderslab.com

〇フラグメントシェーダーでの基本照明のフォンシェーディング

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

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

前回記述したShadereは頂点シェーダーで処理を行っていました。

今回はフラグメントシェーダーで同様の処理を行っていきます。

Shader "Custom/Lighting/BasicLightingPerFragment"
{
    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;
                float3 worldPos : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
            };
 
            v2f vert(appdata_base v)
            {
                v2f o;
                // World position
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
 
                // Clip position
                o.pos = mul(UNITY_MATRIX_VP, float4(o.worldPos, 1.));
 
                // Normal in WorldSpace
                o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
 
                o.uv = v.texcoord;
 
                return o;
            }
 
            sampler2D _MainTex;
 
            fixed4 _LightColor0;
            
            // Diffuse
            fixed _Diffuse;
            fixed4 _DifColor;
 
            //Specular
            fixed _Shininess;
            fixed4 _SpecColor;
            
            //Ambient
            fixed _Ambient;
            fixed4 _AmbColor;
 
            // Emission
            sampler2D _EmissionTex;
            fixed4 _EmiColor;
            fixed _EmiVal;
 
            fixed4 frag(v2f i) : SV_Target
            {
                fixed4 c = tex2D(_MainTex, i.uv);
 
                // Light direction
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
 
                // Camera direction
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
 
                float3 worldNormal = normalize(i.worldNormal);
 
                // 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;
 
                fixed4 light = dif + amb;
 
                // Compute the specular lighting
                #if _SPEC_ON
                float3 refl = normalize(reflect(-lightDir, worldNormal));
                float RdotV = max(0., dot(refl, viewDir));
                fixed4 spec = pow(RdotV, _Shininess) * _LightColor0 * ceil(NdotL) * _SpecColor;
 
                light += spec;
                #endif
 
                c.rgb *= light.rgb;
 
                // Compute emission
                fixed4 emi = tex2D(_EmissionTex, i.uv).r * _EmiColor * _EmiVal;
                c.rgb += emi.rgb;
 
                return c;
            }
 
            ENDCG
        }
    }
}

基本的なpropertyは前回のBasicLightingPerVertexShaderと同様です。

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

●v2f構造体
struct v2f {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldNormal : TEXCOORD2;
            };

TEXCOORD1,TEXCOORD2は2つ目、3つ目のテクスチャ座標を意味します。

●vert関数

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

v2f vert(appdata_base v)
            {
                v2f o;
                // World position
                o.worldPos = mul(unity_ObjectToWorld, v.vertex);
 
                // Clip position
                o.pos = mul(UNITY_MATRIX_VP, float4(o.worldPos, 1.));
 
                // Normal in WorldSpace
                o.worldNormal = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
 
                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
   o.worldPos = mul(unity_ObjectToWorld, v.vertex);

mul()は行列を意味します。 ここで頂点とunity_ObjectToWorldを計算することでワールド座標に変換します。

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

 o.posはv2fでSV_POSITIONと定義されています。これはUNITY_MATRIX_VPと頂点のワールド座標(float4として扱うために(x,y,z,1)としている)の行列を行うことでviewプロジェクション行列返還後の座標を意味します。

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

v2fのworldNormal=TEXCORD2=三つ目のテクスチャ座標は頂点の法線ベクトルとunity_WorldToObjectの行列を正規化したものです。

●frag()

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

   fixed4 frag(v2f i) : SV_Target
            {
                fixed4 c = tex2D(_MainTex, i.uv);
 
                // Light direction
                float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
 
                // Camera direction
                float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
 
                float3 worldNormal = normalize(i.worldNormal);
 
                // 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;
 
                fixed4 light = dif + amb;
 
                // Compute the specular lighting
                #if _SPEC_ON
                float3 refl = normalize(reflect(-lightDir, worldNormal));
                float RdotV = max(0., dot(refl, viewDir));
                fixed4 spec = pow(RdotV, _Shininess) * _LightColor0 * ceil(NdotL) * _SpecColor;
 
                light += spec;
                #endif
 
                c.rgb *= light.rgb;
 
                // Compute emission
                fixed4 emi = tex2D(_EmissionTex, i.uv).r * _EmiColor * _EmiVal;
                c.rgb += emi.rgb;
 
                return c;
            }
fixed4 c = tex2D(_MainTex, i.uv);

cは_MainTexを指します。

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

光源方向を指すlightDirはワールド座標での光源座標を正規化したものです。

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

カメラ位置はワールド座標でのカメラ座標からv2fのworldPos、つまりTEXCOORD1、2つ目のテクスチャ画像の座標を引いたものを正規化したものになります。

ワールド座標での法線ベクトルはTEXCORD2を正規化したものになります。

  // 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;

fixed4 light = dif + amb;

ディフューズライティングの処理です。

 NdotLは光源の影響による明るさを意味します。

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

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

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

このdifとambを合わせたものがlightになります。

前回のBasicLightingPerVertexShaderではo.light = dif + amb;という処理でしたが、今回のShaderではv2fにlightは定義されておらずフラグメントシェーダーの関数内で処理を行います。

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

light += spec;
#endif

スペキュラーのライティングの処理です。

if Space_ONはPropertiesの[Toggle] Spec("Enabled?", Float)がオンの場合#endifまでの間を実行される処理です。

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

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

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

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

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

 lightにspecが加算されています。

 ここでもBasicLightingPerVertexShaderではo.lightとv2fを経由していましたが、関数内で直接変数を扱っています。

 c.rgb *= light.rgb;

_MainTexのrgbにlight.rgbを乗算しています。

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

emissionの処理です。

emiはEmissionTexにEmiColorと_EmiValの値をかけたものです。

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

以上がBasicLightingPerFragmentShaerになります。

 前回のBasicLightingPerVertexShaderと比べVertexShaderの関数ではあくまで座標の返還や光源の方向の処理のみで見た目の処理の多くはフラグメント関数で処理されていることでシンプルな作りになっています。