Posted at

[Unity] Vertex/Fragment shaderで通常のライティングとシャドウを適用するサンプル

More than 3 years have passed since last update.

目下、Unityのライティングとシャドウについて勉強中です。

まさにシェーディング、シェーダの醍醐味ですね。


ライトを複数適用するには複数パスが必要

Unityの仕組みで、複数ライトを適用する場合は複数パスが必要になります。

(詳細はUnityのドキュメントを参照

基本的なライティングや影を適用するサンプルプログラムは以下のようになるようです↓

(ただ、あくまでサンプルなのでデフォルトのdiffuseなどと同様には動かないようです)


サンプルプログラム


basic-shader

Shader "Custom/Sample" {

Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BumpMap ("Normal map", 2D) = "bump" {}
_Color ("Main Color", Color) = (1.0, 1.0, 1.0, 1.0)
_SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1.0)
_Shininess ("Shininess", Range (0.03, 1.0)) = 0.078125
}
SubShader {
ZWrite On
Blend SrcAlpha OneMinusSrcAlpha
Alphatest Greater [_Cutoff]

Tags { "Queue"="Geometry" "RenderType"="Opaque"}

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

CGPROGRAM

float4 _LightColor0;
float4 _Color;
float4 _SpecColor;
sampler2D _MainTex;
sampler2D _BumpMap;
half _Shininess;

#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#pragma fragmentoption ARB_fog_exp2
#pragma fragmentoption ARB_precision_hint_fastest
#pragma target 3.0

#include "UnityCG2.cginc"
#include "AutoLight.cginc"

struct vertInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD0;
};

struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 viewDir : TEXCOORD1;
float3 lightDir : TEXCOORD2;
LIGHTING_COORDS(3, 4)
};

// Vertex shader function.
v2f vert(appdata_tan v) {
v2f o;

o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord.xy;
TANGENT_SPACE_ROTATION;
o.viewDir = mul(rotation, ObjSpaceViewDir(v.vertex));
o.lightDir = mul(rotation, ObjSpaceLightDir(v.vertex));

TRANSFER_VERTEX_TO_FRAGMENT(o);

return o;
}

// Fragment shader function.
float4 frag(v2f i) : COLOR {
i.viewDir = normalize(i.viewDir);
i.lightDir = normalize(i.lightDir);

fixed atten = LIGHT_ATTENUATION(i);

fixed4 tex = tex2D(_MainTex, i.uv);
fixed gloss = tex.a;
tex *= _Color;
fixed3 normal = UnpackNormal(tex2D(_BumpMap, i.uv));

half3 h = normalize(i.lightDir + i.viewDir);

fixed4 diff = saturate(dot(normal, i.lightDir));

float nh = saturate(dot(normal, h));
float spec = pow(nh, _Shininess * 128.0) * gloss;

fixed4 color;
color.rgb = UNITY_LIGHTMODEL_AMBIENT.rgb * 2 * tex.rgb;
color.rgb += (tex.rgb * _LightColor0.rgb * diff + _LightColor0.rgb * _SpecColor.rgb * spec) * (atten * 2);
color.a = tex.a + (_LightColor0.a * _SpecColor.a * spec * atten);

return color;
}

ENDCG
}

// For forward add rendering pass.
Pass {
Tags { "LightMode"="ForwardAdd" }

Blend One One

CGPROGRAM

#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd
#pragma fragmentoption ARB_fog_exp2
#pragma fragmentoption ARB_precision_hint_fastest
#pragma target 3.0

#include "UnityCG.cginc"
#include "AutoLight.cginc"

struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 viewDir : TEXCOORD1;
float3 lightDir : TEXCOORD2;
LIGHTING_COORDS(3, 4)
};

// Vertes shader function.
v2f vert(appdata_tan v) {
v2f o;

o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv = v.texcoord.xy;

TANGENT_SPACE_ROTATION;

return o;
}

sampler2D _MainTex;
sampler2D _BumpMap;
fixed4 _Color;
half _Shininess;

fixed4 _SpecColor;
fixed4 _LightColor0;

float4 frag(v2f i) : COLOR {
i.viewDir = normalize(i.viewDir);
i.lightDir = normalize(i.lightDir);

fixed atten = LIGHT_ATTENUATION(i);

fixed4 tex = tex2D(_MainTex, i.uv);
fixed gloss = tex.a;
tex *= _Color;
fixed3 normal = UnpackNormal(tex2D(_BumpMap, i.uv));

half3 h = normalize(i.lightDir + i.viewDir);

fixed diff = saturate(dot(normal, i.lightDir));

float nh = saturate(dot(normal, h));
float spec = pow(nh, _Shininess * 128.0) * gloss;

fixed4 color;
color.rgb = (tex.rgb * _LightColor0.rgb * diff + _LightColor0.rgb * _SpecColor.rgb * spec) * (atten * 2);
color.a = tex.a + _LightColor0.a * _SpecColor.a * spec * atten;

return color;
}

ENDCG
}
}
FallBack "Diffuse"
}



内部で使われているマクロ、関数について

いくつか、Unityが準備してくれているマクロなどを使っているので、把握しているところをメモ。


TANGENT_SPACE_ROTATION

TANGENT_SPACE_ROTATIONは以下のマクロになっています。


TANGENT_SPACE_ROTATION

// Declares 3x3 matrix 'rotation', filled with tangent space basis

#define TANGENT_SPACE_ROTATION \
float3 binormal = cross( v.normal, v.tangent.xyz ) * v.tangent.w; \
float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )

接空間への、3x3の行列を生成するマクロのようです。

v.tangent.xyzは接ベクトル、そこから従法線を求め、最終的に3x3の行列にしています。

バンプマッピングについては以前記事を書いたのでそちらを参考に。


ObjSpaceViewDir

オブジェクト空間から見たカメラ方向を算出する関数です。

引数には頂点位置を取り、カメラの位置をオブジェクト空間に変換した後に、方向を求めます。


ObjSpaceViewDir

// Computes object space view direction

inline float3 ObjSpaceViewDir( in float4 v )
{
float3 objSpaceCameraPos = mul(_World2Object, float4(_WorldSpaceCameraPos.xyz, 1)).xyz * unity_Scale.w;
return objSpaceCameraPos - v.xyz;
}

unity_Scale.wを掛けているのは、元々1 / *として求めた値、ということでしょうか・・。


ObjSpaceLightDir

オブジェクト空間から見たライト方向を算出する関数です。

引数には頂点位置を取り、頂点位置からの相対方向を求めます。


ObjSpaceLightDir

// Computes object space light direction

inline float3 ObjSpaceLightDir( in float4 v )
{
float3 objSpaceLightPos = mul(_World2Object, _WorldSpaceLightPos0).xyz;
#ifndef USING_LIGHT_MULTI_COMPILE
return objSpaceLightPos.xyz - v.xyz * _WorldSpaceLightPos0.w;
#else
#ifndef USING_DIRECTIONAL_LIGHT
return objSpaceLightPos.xyz * unity_Scale.w - v.xyz;
#else
return objSpaceLightPos.xyz;
#endif
#endif
}


UnpackNormal

バンプマップのくだりで出てくる関数。

定義を見てみると、さらに内部的には以下のような関数になっています。(多少省略しているところあり)


UnpackNormal

inline fixed3 UnpackNormalDXT5nm (fixed4 packednormal)

{
fixed3 normal;
normal.xy = packednormal.wy * 2 - 1;
normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));
return normal;
}

packednormal.wy * 2 - 1は、0〜1の範囲のものを-1〜1の範囲に変換する常套句ですね。

一瞬不可解なのがこっち。normal.z = sqrt(1 - saturate(dot(normal.xy, normal.xy)));

これはxyの値からzを逆算して求めている計算です。

ただこれは、packednormal正規化済 のためにできることですね。

ちなみになんで同じベクトルの内積を計算しているのかと思ったんですが、これは単純にxyそれぞれの2乗を足す処理だからだと思います。

内積というより、計算方法が同じだからそれを利用している、ということでしょうか。