ゲーム制作技術録

主にUnity/Godot4関連の技術ブログです

【Unity】【URP12】自作Unlitシェーダーにノーマルマップを実装する

今回は作った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にオブジェクト空間のタンジェントtangentOSを追加します。

struct Attributes {
    float4 positionOS : POSITION;
    float2 uv : TEXCOORD0;
    float3 normalOS : NORMAL;
    float4 tangentOS : TANGENT;   // 追加
};

Varyingsにワールド空間のタンジェントtangentWSを追加します。
(前回からnormalの記述も少しだけ変えました。これによる支障は特にありません。ただの気分です)

頂点シェーダーでオブジェクト空間のタンジェントtangentOSからワールド空間に変換してtangentWSに格納し、フラグメントシェーダーで使用します。

struct Varyings {
    float4 positionHCS : SV_POSITION;
    float2 uv : TEXCOORD0;
    float fogFactor : TEXCOORD1;
    float3 normalOS : NORMAL;
    float3 normalWS : NORMAL_WS;
    float4 tangentWS : TANGENT_WS;   // 追加
};

正弦とワールド空間のタンジェントの計算

頂点シェーダーvert()でワールド空間のタンジェントを計算してVaryingstangentWSに格納します。

float4wsignを格納することでtangentWS.xyzからベクトル、tangentWS.wからsignが取り出せます。

計算については、ここで長々と語ってもしょうがないので細かいことは省きます。
というか難しいので私もあまり分かっていません!

ということでvert()の最後のほうに以下を追加します。

// サイン(正弦)とワールド空間のタンジェントの計算
float sign = IN.tangentOS.w * GetOddNegativeScale();
VertexNormalInputs vni = GetVertexNormalInputs(IN.normalOS, IN.tangentOS);
OUT.tangentWS = float4(vni.tangentWS, sign);

vert()全体

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;
}

ノーマルマップを追加する

ノーマルマップの展開にはUnpackNormalScale()を使用します。
タンジェント空間で取得できますがワールド空間で使いたいので後で変換します。

// ノーマルマップをサンプリング(タンジェント空間)
float3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, IN.uv), _NormalStrength);

参考

qiita.com

ワールド空間に変換する

計算したsignなどを使用してワールド空間に変換していきます。

// 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)));

テキトーなノーマルテクスチャを割り当ててnormalWS.xyzを出力するとこんな感じになります。

ノーマルを適応する

セルフシャドウの実装時にワールド空間に変換したオブジェクトのノーマルを使用していました。
なので上記で計算したワールド空間のノーマルに置き換えます。

セルフシャドウ処理の前にノーマル計算処理を挟み、セルフシャドウ処理内でIN.normalWSではなく計算したnormalWSに置き換えて計算したノーマルを基にセルフシャドウが計算されるようにします。

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);
}

実行結果

セルフシャドウにノーマルが適応されていることが確認できました。

コード全文

参考