本日はShader勉強枠です。
実際のサンプルを読み解きながら構造を勉強しています。
Unityシェーダーのサンプルやデモを数多く紹介しているサイトShaders Laboratoryを基に勉強していきます。
〇フラグメントシェーダーでの基本照明のフォンシェーディング
フォンシェーディングとは3DCGの陰影計算の手法のことで滑らかな見た目を提供することができます。
前回記述した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と同様です。
●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の関数ではあくまで座標の返還や光源の方向の処理のみで見た目の処理の多くはフラグメント関数で処理されていることでシンプルな作りになっています。