【Unity ShaderLab】NPRなライティングを実装してみる
はじめに
UnityのShaderLabにて、NPR(Non Photorealtstic Rendering)を実装してみました。
今回実装したものは以下の3つです。
- 環境光 (Ambient)
- 拡散反射光 (Diffuse; Phong 反射)
- 鏡面反射光 (Specular; Blinn-Phong 鏡面反射)
複数Passによるライティング実装
Point Light や Directional Light を使ったライティングを行うには、ライトごとに対応するPassを実装する必要があります。
LightMode | 用途 |
---|---|
ForwardBase | Directional Light や Ambient Light |
ForwardAdd | Point Light や Spot Light |
これらのライティングは、加算合成します。
Passのブレンディング
シェーダー上に Blend を指定することで、Passの出力色と、フレームバッファをどのように合成されるかを指定できます。
Blend SrcFactor DstFactor
Passの最終出力色
シェーダー出力色
上書きブレンド
以下のように指定した場合、フレームバッファをシェーダー出力色で上書きします。
Blend One Zero
シェーダー例
以下のようなシェーダーを書いた場合、
1Pass目の出力 fixed4(1, 0, 0, 0)
がフレームバッファに書きこまれた後、
2Pass目の出力 fixed4(0, 1, 0, 0)
がフレームバッファを上書きするため、
最終的にfixed4(0, 1, 0, 0)
が画面に表示されます。
Pass
{
Blend One Zero // 上書きブレンド
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 frag(v2f i) : SV_Target
{
return fixed4(1, 0, 0, 0);
}
ENDCG
}
Pass
{
Blend One Zero // 上書きブレンド
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 frag(v2f i) : SV_Target
{
return fixed4(0, 1, 0, 0);
}
ENDCG
}
加算ブレンド
以下のように指定した場合、SrcValue(シェーダー出力色)とDstValue (フレームバッファ)の色を加算合成します。
Blend One One
シェーダー例
以下のようなシェーダーを書いた場合、
1Pass目の出力 fixed4(1, 0, 0, 0)
がフレームバッファに書きこまれた後、
2Pass目の出力 fixed4(0, 1, 0, 0)
がフレームバッファに加算されるので、
最終的にfixed4(1, 1, 0, 0)
が画面に表示されます。
Pass
{
Blend One Zero // 上書きブレンド
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 frag(v2f i) : SV_Target
{
return fixed4(1, 0, 0, 0);
}
ENDCG
}
Pass
{
Blend One One // 加算ブレンド
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
fixed4 frag(v2f i) : SV_Target
{
return fixed4(0, 1, 0, 0);
}
ENDCG
}
複数ライトのブレンディング
拡散反射光、鏡面反射光、環境光といったライティングを個別のPassで計算し、これらを加算でブレンドするようにします。
SubShader
{
Pass
{
Tags { "LightMode"="ForwardBase" }
Blend One Zero
// Directional Light や Ambient Light のライティング処理をここに書く
}
Pass
{
Tags { "LightMode"="ForwardAdd" }
Blend One One // 加算ブレンド
// Point Light のライティング処理をここに書く
}
}
実装1: Ambientライト + Directionalライト
疑似コード実装(GLSL)
ShaderLabを使わずに実装した場合、以下のようになります。
vec3 directionalLightNPR(vec3 viewDir, vec3 normal, vec3 light)
{
// 拡散反射光
float diffuse = dot(light, normal); // ライトベクトルと法線の内積
diffuse = saturate(diffuse);
// 鏡面反射光
vec3 refl = reflect(-LIGHT, normal); // 反射ベクトル
float specular = saturate(dot(viewDir, refl)); // 視線ベクトルと反射ベクトルの内積
specular = pow(specular, 80.0);
// 光の計算 (拡散反射光 + 鏡面反射光 + 環境光)
return diffuse * DIFFUSE_COLOR + specular * SPECULAR_COLOR + AMBIENT_COLOR;
}
ShaderLabによる実装 (Directional Light)
Directional Light のライティングは、ForwardBase
パスで実装します。
ForwardBase
パスでは、Directional Lightの色を _LightColor0
で取得でき、
Directional Lightのライトベクトルを _WorldSpaceLightPos0.xyz
で取得できます。
Pass
{
Tags { "LightMode" = "ForwardBase" }
Blend One Zero
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
// CPUからGPUに渡すデータ
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
// vertからfragに渡すデータ
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 normal : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _SpecularGloss; // 鏡面反射 光沢度
half4 _AmbientColor; // 環境光 カラー
v2f vert (appdata v)
{
v2f o;
// MVP座標変換
o.vertex = UnityObjectToClipPos(v.vertex);
// UV座標のタイリング・オフセット計算
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// ワールド空間のNormal
o.normal = UnityObjectToWorldNormal(v.normal);
// ワールド空間のViewベクトル(サーフェスからみたカメラ位置)
o.viewDir = normalize(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex));
return o;
}
float4 frag (v2f i) : SV_Target
{
// ライトベクトル
#define L _WorldSpaceLightPos0.xyz
// 反射ベクトル
#define R reflect(-L, i.normal)
fixed4 texColor = tex2D(_MainTex, i.uv);
// 拡散反射光
float diffuse = saturate(dot(i.normal, L));
// 鏡面反射光
float specular = pow(saturate(dot(i.viewDir, R)), _SpecularGloss);
// ディレクショナルライト + 環境光
return ((diffuse + specular) * _LightColor0 + _AmbientColor) * texColor;
}
ENDCG
}
実装2 : Point Light
疑似コード実装(GLSL)
ポイントライトによるライティングを、ShaderLabを使わずに実装した場合、以下のようになります。
vec3 pointLightNPR(vec3 position, vec3 viewDir, vec3 normal, vec3 worldLightPos)
{
// サーフェスを基準としたライトの位置
vec3 lightPos = worldLightPos - position;
// ライトベクトル
vec3 light = normalize(lightPos);
// 光の減衰
float atten = pow(length(lightPos) + 1, -2.0);
// 拡散反射光
float diffuse = dot(light, normal) * atten;
diffuse = saturate(diffuse);
// 鏡面反射光
vec3 refl = reflect(-light, normal); // 反射ベクトル
float specular = saturate(dot(viewDir, refl)); // 視線ベクトルと反射ベクトルの内積
specular = pow(specular, 80.0) * atten;
// 光の計算 (拡散反射光 + 鏡面反射光 + 環境光)
return diffuse * DIFFUSE_COLOR + specular * SPECULAR_COLOR + AMBIENT_COLOR;
}
Point Light と Directional Light の違い
Point Light のライティングでは、光の距離減衰atten
を計算に組み込んでいるところがDirectinal Lightとは異なる点です。
Point Lightは、光源からサーフェスまでの距離に応じて光が減衰します。
// 拡散反射光
- float diffuse = dot(light, normal);
+ float diffuse = dot(light, normal) * atten;
diffuse = saturate(diffuse);
// 鏡面反射光
vec3 refl = reflect(-light, normal); // 反射ベクトル
float specular = saturate(dot(viewDir, refl)); // 視線ベクトルと反射ベクトルの内積
- specular = pow(specular, 80.0);
+ specular = pow(specular, 80.0) * atten;
// ライティングの合成
return diffuse * DIFFUSE_COLOR + specular * SPECULAR_COLOR + AMBIENT_COLOR;
ShaderLabによる実装 (Point Light)
光の距離減衰
ポイントライトをシーンに配置すると、光源から遠い部分ほど光が弱くなります。
UNITY_LIGHT_ATTENUATION
を使う方法や、光源とサーフェス間の距離を利用して自前計算する方法があります。
光の減衰 | 結果 |
---|---|
UNITY_LIGHT_ATTENUATION |
|
距離の逆二乗 |
|
指数関数 |
ShaderLabによる実装
Point Light のライティングは、ForwardAdd
パスにて実装します。
ForwardAdd
パスでは、Point Lightの色を _LightColor0
で取得でき、
Point Lightのワールド座標を _WorldSpaceLightPos0.xyz
で取得できます。
光の減衰は UNITY_LIGHT_ATTENUATION
マクロで取得できます。
Pass
{
Tags { "LightMode" = "ForwardAdd" }
Blend One One // 加算ブレンド
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd_fullshadows
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
// CPUからGPUに渡すデータ
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
// vertからfragに渡すデータ
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 normal : TEXCOORD1;
float3 viewDir : TEXCOORD2;
float3 worldPos : TEXCOORD3;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _SpecularGloss; // 鏡面反射 光沢度
half4 _AmbientColor; // 環境光 カラー
v2f vert (appdata v)
{
v2f o;
// MVP座標変換
o.vertex = UnityObjectToClipPos(v.vertex);
// UV座標のタイリング・オフセット計算
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// ワールド空間のNormal
o.normal = UnityObjectToWorldNormal(v.normal);
// ワールド空間のViewベクトル(サーフェスからみたカメラ位置)
o.viewDir = normalize(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex));
// ワールド空間の座標
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
float4 frag (v2f i) : SV_Target
{
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
// ライトベクトル
#define L normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz)
// 反射ベクトル
#define R reflect(-L, i.normal)
fixed4 texColor = tex2D(_MainTex, i.uv);
// 拡散反射光
float diffuse = saturate(dot(i.normal, L));
// 鏡面反射光
float specular = pow(saturate(dot(i.viewDir, R)), _SpecularGloss);
return (diffuse + specular) * texColor * _LightColor0 * attenuation;
}
ENDCG
}
光の減衰の取得
UNITY_LIGHT_ATTENUATION
マクロを利用することで、光の減衰を取得できます。
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
UNITY_LIGHT_ATTENUATION
マクロを利用するためには、以下の一行を記述する必要があります。
#pragma multi_compile_fwdadd_fullshadows
AutoLight.cginc
をインクルードする必要もあります。
Point Light と Directional Light の実装の違い
Pass
{
- Tags { "LightMode" = "ForwardBase" }
+ Tags { "LightMode" = "ForwardAdd" }
Blend One One // 加算ブレンド
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
+ #pragma multi_compile_fwdadd_fullshadows
#include "UnityCG.cginc"
#include "Lighting.cginc"
+ #include "AutoLight.cginc"
// CPUからGPUに渡すデータ
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
// vertからfragに渡すデータ
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 normal : TEXCOORD1;
float3 viewDir : TEXCOORD2;
+ float3 worldPos : TEXCOORD3;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _SpecularGloss; // 鏡面反射 光沢度
half4 _AmbientColor; // 環境光 カラー
v2f vert (appdata v)
{
v2f o;
// MVP座標変換
o.vertex = UnityObjectToClipPos(v.vertex);
// UV座標のタイリング・オフセット計算
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// ワールド空間のNormal
o.normal = UnityObjectToWorldNormal(v.normal);
// ワールド空間のViewベクトル(サーフェスからみたカメラ位置)
o.viewDir = normalize(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex));
+ // ワールド空間の座標
+ o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
float4 frag (v2f i) : SV_Target
{
+ UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
// ライトベクトル
- #define L _WorldSpaceLightPos0.xyz
+ #define L normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz)
// 反射ベクトル
#define R reflect(-L, i.normal)
fixed4 texColor = tex2D(_MainTex, i.uv);
// 拡散反射光
float diffuse = saturate(dot(i.normal, L));
// 鏡面反射光
float specular = pow(saturate(dot(i.viewDir, R)), _SpecularGloss);
- return ((diffuse + specular) * _LightColor0 + _AmbientColor) * texColor;
+ return (diffuse + specular) * texColor * _LightColor0 * attenuation;
}
ENDCG
}
シェーダー全体
Shader "Day3/Pracitce01"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_SpecularGloss ("Specular Gloss", Float) = 50.0
_AmbientColor ("Ambient Color", Color) = (0.3, 0.3, 0.3, 1) // 環境 カラー
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
Tags { "LightMode" = "ForwardBase" }
Blend One Zero
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc"
// CPUからGPUに渡すデータ
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
// vertからfragに渡すデータ
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 normal : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _SpecularGloss; // 鏡面反射 光沢度
half4 _AmbientColor; // 環境光 カラー
v2f vert (appdata v)
{
v2f o;
// MVP座標変換
o.vertex = UnityObjectToClipPos(v.vertex);
// UV座標のタイリング・オフセット計算
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// ワールド空間のNormal
o.normal = UnityObjectToWorldNormal(v.normal);
// ワールド空間のViewベクトル(サーフェスからみたカメラ位置)
o.viewDir = normalize(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex));
return o;
}
float4 frag (v2f i) : SV_Target
{
// ライトベクトル
#define L _WorldSpaceLightPos0.xyz
// 反射ベクトル
#define R reflect(-L, i.normal)
fixed4 texColor = tex2D(_MainTex, i.uv);
// 拡散反射光
float diffuse = saturate(dot(i.normal, L));
// 鏡面反射光
float specular = pow(saturate(dot(i.viewDir, R)), _SpecularGloss);
return ((diffuse + specular) * _LightColor0 + _AmbientColor) * texColor;
}
ENDCG
}
Pass
{
Tags { "LightMode" = "ForwardAdd" }
Blend One One // 加算ブレンド
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdadd_fullshadows
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
// CPUからGPUに渡すデータ
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
// vertからfragに渡すデータ
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 normal : TEXCOORD1;
float3 viewDir : TEXCOORD2;
float3 worldPos : TEXCOORD3;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _SpecularGloss; // 鏡面反射 光沢度
half4 _AmbientColor; // 環境光 カラー
v2f vert (appdata v)
{
v2f o;
// MVP座標変換
o.vertex = UnityObjectToClipPos(v.vertex);
// UV座標のタイリング・オフセット計算
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// ワールド空間のNormal
o.normal = UnityObjectToWorldNormal(v.normal);
// ワールド空間のViewベクトル(サーフェスからみたカメラ位置)
o.viewDir = normalize(_WorldSpaceCameraPos - mul(unity_ObjectToWorld, v.vertex));
// ワールド空間の座標
o.worldPos = mul(unity_ObjectToWorld, v.vertex);
return o;
}
float4 frag (v2f i) : SV_Target
{
UNITY_LIGHT_ATTENUATION(attenuation, i, i.worldPos);
// ライトベクトル
#define L normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz)
// 反射ベクトル
#define R reflect(-L, i.normal)
fixed4 texColor = tex2D(_MainTex, i.uv);
// 拡散反射光
float diffuse = saturate(dot(i.normal, L));
// 鏡面反射光
float specular = pow(saturate(dot(i.viewDir, R)), _SpecularGloss);
return (diffuse + specular) * texColor * _LightColor0 * attenuation;
}
ENDCG
}
}
}
Discussion