今回はURP対応の自作HLSLシェーダーでのセルフシャドウ実装に焦点を置いた話をします。
Unity 2021.3
UniversalRP 12.1
Forward
はじめに
ここまでの話は前回の記事をご覧ください。
pinomatcha-gamedev.hatenablog.com
ノーマル(法線)とライト方向の内積を使ってセルフシャドウを実装していきます。
仕組みは割と単純なので意外とすんなりできると思います。
必要なデータを取得する
セルフシャドウを実装するにはノーマルとライト方向のデータが必要なので順番にやっていきます。
ノーマル情報の取得
Attributes
Attributes構造体に一行追加して、ノーマルを扱えるようにします。
OSというのは
ここでは、オブジェクトから見たノーマルということですね。
struct Attributes {
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float3 normalOS : NORMAL; // 追加
};
Varyings
同時にVaryingsの方にも二行追加します。
オブジェクト空間のノーマルとワールド空間のノーマルです。
それぞれOS、WSを末尾に付けておきます。
これらは頂点シェーダーから値を設定してフラグメントシェーダーで使用するためのものです。
struct Varyings {
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
float fogFactor : TEXCOORD1;
float3 normalOS : NORMAL0; // 追加
float3 normalWS : NORMAL1; // 追加
};
頂点シェーダーにノーマル処理を追加する
頂点シェーダーの
オブジェクト空間のノーマル情報をそのまま格納する処理
// オブジェクト空間のノーマルはそのままVaryingsのnormalOSに格納
OUT.normalOS = IN.normalOS;
オブジェクト空間のノーマル情報をワールド空間に変換して格納する処理
// TransformObjectToWorldNormal()でノーマルをオブジェクト空間からワールド空間へ変換して格納
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
追加後
Varyings vert(Attributes IN) {
Varyings OUT;
// TransformObjectToHClip()で頂点位置をオブジェクト空間からクリップスペースへ変換
// UnityObjectToClipPos()がTransformObjectToHClip()になった
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
// TRANSFORM_TEX()マクロでタイリングなどを計算する
OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex);
// fog factor の計算
OUT.fogFactor = ComputeFogFactor(IN.positionOS.z);
// オブジェクト空間のノーマルはそのままVaryingsのnormalOSに格納
OUT.normalOS = IN.normalOS;
// TransformObjectToWorldNormal()でノーマルをオブジェクト空間からワールド空間へ変換して格納
OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);
return OUT;
}
確認
セルフシャドウの計算に必要なのは
// return をワールド空間のノーマルxyzにしてみる。
// だけど色情報はRGBAのfloat4なのでAには1を入れておく。
// XYZ(RGB) + A
return float4(IN.normalWS.xyz, 1);
xyzをRGBとして出力しているので、x+が赤色、y+が緑色、z+が青色となっており、正常にノーマルがワールド空間で取得できていることが分かりました。
ワールド空間なので、オブジェクトを回転してみてもノーマルの表示は変わりません。
ちなみに
return float4(IN.normalOS.xyz, 1);
確認出来たら元に戻しておきます。
reutn color;
ライト情報の取得
#include
Lighting.hlsl を include します。
Core.hlsl の下にでも書いておきましょう。
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// ↓ 追加
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
MainLightの取得
前述のLighting.hlslのインクルードをしておかないと
Light mainLight = GetMainLight();
float4 frag(Varyings IN) : SV_Target {
// テクスチャをサンプリングして、カラーを乗算する
float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv) * _MainColor;
// Fogを適用する
color.rgb = MixFog(color.rgb, IN.fogFactor);
// MainLightの取得
Light mainLight = GetMainLight();
return color;
}
陰影をつける
必要なデータは揃ったので、実際に陰影処理をしていきます。
ノーマルとライト方向の内積
内積について詳しくは説明しませんが、ここで内積を使って求めるのは2つのベクトルの角度差です。
1つのベクトルをノーマル、1つのベクトルをライトの方向としてその2つのベクトル間の角度を0~1で取得することで光が当たっているか、影になっているかを算出します。
詳しい説明は高校数学の解説でも見たほうが早いと思います……。
というわけで実際の処理はこんな感じです。
// メインライト取得の下に記述します。
// ノーマル(法線)とライト方向の内積
float NdotL = saturate(dot(IN.normalWS, mainLight.direction));
ノーマル(Normal)とライト方向(Light Direction)の内積(Dot Product)なので NdotL と表記することが多い気がします。
今回もそれにならって NdotL と命名しています。
計算は手動じゃないので記述は単純で、
ここでNdotLを出力してみます。
return NdotL;
ノーマルがライトの方向に向いているほど1(白色)、ライトの方向とは逆に向いているほど0(黒色)が取得できているのが確認できました。
(ベクトル的にはライト方向と逆が1、ライト方向が0という表現になるのかな?)
ここまで来たら、color とライトの色と NdotL を掛け合わせて出力してみると
return float4(color.rgb * mainLight.color.rgb * NdotL, 1);
正常に出力されているのが分かります。
ここまでのfrag()全体
float4 frag(Varyings IN) : SV_Target {
// テクスチャをサンプリングして、カラーを乗算する
float4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv) * _MainColor;
// Fogを適用する
color.rgb = MixFog(color.rgb, IN.fogFactor);
// MainLightの取得
Light mainLight = GetMainLight();
// ノーマル(法線)とライト方向の内積
float NdotL = saturate(dot(IN.normalWS, mainLight.direction));
return float4(color.rgb * mainLight.color.rgb * NdotL, 1);
}
影をトゥーンにする
このままだとLitシェーダーとあまり変わらないので、影をトゥーン調にします。
やり方は何通りかあると思いますが、今回は
NdotL の計算式の下に lightIntensity として処理を書きます。
float NdotL = saturate(dot(IN.normalWS, mainLight.direction));
float lightIntensity = saturate(smoothstep(0.005, 0.01, NdotL));
以下は lightIntensity のみ出力したものです。
きれいに二極化されています。
最後に、NdotL を掛けていた部分を lightIntensity に置き換えて出力するとトゥーン調になっているのが分かります。
return float4(color.rgb * mainLight.color.rgb * lightIntensity, 1);
コード全文