今回は作ったUnlitシェーダーにノーマルマップ(法線マップ、バンプマップ)を実装する記事です。
- はじめに
- プロパティを追加する
- 変数を定義する
- タンジェントを追加
- 正弦とワールド空間のタンジェントの計算
- ノーマルマップを追加する
- ワールド空間に変換する
- ノーマルを適応する
- 実行結果
- コード全文
- 参考
Unity 2021.3
UniversalRP 12.1
Forward
はじめに
ここまでの話は前回の記事をご覧ください。
前回まででテクスチャの表示とセルフシャドウを実装しました。
pinomatcha-gamedev.hatenablog.com
これにノーマルを付けていきます。
プロパティを追加する
まずはノーマルマップを追加するためにプロパティに追記します。
テクスチャ、強さの2つのプロパティです。
Properties {
// 省略
_NormalMap("Normal Map", 2D) = "bump" {}
_NormalStrength("Normal Strength", Range(0, 8)) = 1
}
インスペクタに「Normal Map」と「Normal Strength」の項目が追加されました。
変数を定義する
未だに「変数」という言い方であっているのかわかりませんが、定義したプロパティをシェーダー内で使うために必要な工程なのでとりあえず書きます。
「_MainTex」とかと同じなので詳細は省きます。
// ここは TEXTURE2D(_MainTex); とか
TEXTURE2D(_NormalMap);
SAMPLER(sampler_NormalMap);
float4 _NormalMap_ST;
float _NormalStrength;
タンジェントを追加
ノーマルマップは基本的にタンジェント空間で扱われるので、タンジェントを一通り使えるようにしてからワールド空間に変換します。
Attributesにオブジェクト空間のタンジェント
struct Attributes {
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float3 normalOS : NORMAL;
float4 tangentOS : TANGENT; // 追加
};
Varyingsにワールド空間のタンジェント
(前回からnormalの記述も少しだけ変えました。これによる支障は特にありません。ただの気分です)
頂点シェーダーでオブジェクト空間のタンジェント
struct Varyings {
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
float fogFactor : TEXCOORD1;
float3 normalOS : NORMAL;
float3 normalWS : NORMAL_WS;
float4 tangentWS : TANGENT_WS; // 追加
};
正弦とワールド空間のタンジェントの計算
頂点シェーダー
計算については、ここで長々と語ってもしょうがないので細かいことは省きます。
というか難しいので私もあまり分かっていません!
ということで
// サイン(正弦)とワールド空間のタンジェントの計算
float sign = IN.tangentOS.w * GetOddNegativeScale();
VertexNormalInputs vni = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);
OUT.tangentWS = float4(vni.tangentWS, sign);
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);
// サイン(正弦)とワールド空間のタンジェントの計算
float sign = IN.tangentOS.w * GetOddNegativeScale();
VertexNormalInputs vni = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);
OUT.tangentWS = float4(vni.tangentWS, sign);
return OUT;
}
ノーマルマップを追加する
ノーマルマップの展開には
タンジェント空間で取得できますがワールド空間で使いたいので後で変換します。
// ノーマルマップをサンプリング(タンジェント空間)
float3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, IN.uv), _NormalStrength);
参考
ワールド空間に変換する
計算した
// vert()で算出したサイン(正弦)
float sgn = IN.tangentWS.w;
// 従法線(bitangent / binormal)を計算
float3 bitangent = sgn * cross(IN.normalWS.xyz, IN.tangentWS.xyz);
// タンジェント空間からワールド空間へ変換
// normalize()(正規化)も同時にしておく
float3 normalWS = normalize(mul(normalTS, float3x3(IN.tangentWS.xyz, bitangent.xyz, IN.normalWS.xyz)));
テキトーなノーマルテクスチャを割り当てて
ノーマルを適応する
セルフシャドウの実装時にワールド空間に変換したオブジェクトのノーマルを使用していました。
なので上記で計算したワールド空間のノーマルに置き換えます。
セルフシャドウ処理の前にノーマル計算処理を挟み、セルフシャドウ処理内で
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();
// ↓ 追加
// ノーマルマップをサンプリング(タンジェント空間)
float3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, IN.uv), _NormalStrength);
// vert()で算出したサイン(正弦)
float sgn = IN.tangentWS.w;
// 従法線(bitangent / binormal)を計算
float3 bitangent = sgn * cross(IN.normalWS.xyz, IN.tangentWS.xyz);
// タンジェント空間からワールド空間へ変換
// normalize()(正規化)も同時にしておく
float3 normalWS = normalize(mul(normalTS, float3x3(IN.tangentWS.xyz, bitangent.xyz, IN.normalWS.xyz)));
// ↑ 追加
// ↓ dot()内のIN.normalWSを計算したnormalWSに置き換える
// ノーマル(法線)とライト方向の内積
// saturate()で 0 ~ 1 に固定
float NdotL = saturate(dot(normalWS, mainLight.direction));
float lightIntensity = saturate(smoothstep(0.005, 0.01, NdotL));
return float4(color.rgb * mainLight.color.rgb * lightIntensity, 1);
}
実行結果
セルフシャドウにノーマルが適応されていることが確認できました。
コード全文