Unityシェーダー開発:UV座標・テクスチャ拡張とノーマルマッピングの実装方法

UV座標とテクスチャマッピングの基礎

3Dジオメトリに画像データを割り当てる際は、ピクセル単位の座標ではなくUV座標が使用されます。UVは範囲0.0〜1.0の正規化された座標系であり、画像の左下端が(0,0)、右上端が(1,1)に対応します。この設計により、モデルの解像度やスケール変更に依存せず、テクスチャが一貫して正しく貼付可能になります。

基本となる単一テクスチャシェーダー

まずは拡散反射(ディフューズ)とスペキュラー高光をベースに、テクスチャカラーで表面を覆うシェーダーを作成します。元の構造を整理し、可読性を高めるためにセマンティクスと変数命名規則を統一しています。

Shader "Custom/01_BasicTexture"
{
    Properties
    {
        _BaseMap("Base Texture", 2D) = "white" {}
        _Tint("Tint Color", Color) = (1, 1, 1, 1)
        _SpecularColor("Specular Color", Color) = (1, 1, 1, 1)
        _Glossiness("Gloss Range", Range(8, 128)) = 32
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Geometry" }
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            CGPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D _BaseMap;
            float4 _BaseMap_ST;
            fixed4 _Tint;
            fixed4 _SpecularColor;
            half _Glossiness;

            struct VertexInput
            {
                float4 pos    : POSITION;
                float3 norm   : NORMAL;
                float4 texcoord : TEXCOORD0;
            };

            struct FragmentInput
            {
                float4 clipPos : SV_POSITION;
                float3 worldNorm   : TEXCOORD0;
                float4 viewWorldPos : TEXCOORD1;
                float2 uv     : TEXCOORD2;
            };

            FragmentInput Vert(VertexInput i)
            {
                FragmentInput o;
                o.clipPos = UnityObjectToClipPos(i.pos);
                o.worldNorm = UnityObjectToWorldNormal(i.norm);
                o.viewWorldPos = mul(unity_ObjectToWorld, i.pos);
                o.uv = i.texcoord.xy;
                return o;
            }

            fixed4 Frag(FragmentInput i) : SV_Target
            {
                float3 N = normalize(i.worldNorm);
                float3 L = normalize(UnityWorldSpaceLightDir(i.viewWorldPos));
                
                // テクスチャサンプリングと染色の適用
                fixed3 baseColor = tex2D(_BaseMap, i.uv).rgb * _Tint.rgb;

                // ディフューズ反射
                fixed3 diff = _LightColor0.rgb * baseColor * max(dot(N, L), 0.0);

                // スペキュラー反射
                float3 V = normalize(UnityWorldSpaceViewDir(i.viewWorldPos));
                float3 H = normalize(L + V);
                fixed3 spec = _LightColor0.rgb * _SpecularColor.rgb * pow(max(dot(N, H), 0.0), _Glossiness);

                // 環境光の融合
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * baseColor;
                
                return fixed4(diff + spec + ambient, 1.0);
            }
            ENDCG
        }
    }
    FallBack "Specular"
}

このシェーダーをアタッチ後、インスペクターから画像をドラッグ&ドロップすれば即時反映されます。[図解: マテリアルプロパティへの画像アサイン例]

Tiling(展開)とOffset(シフト)の実装

テクスチャの繰り返し表示や位置調整を行うには、_STという特殊な形式の変数を定義する必要があります。このパターンはUnity内部的に自動で値を解決し、シェーダー側でUV座標に加算・乗算を行います。

// 変数宣言部分のみ抜粋
float4 _BaseMap_ST; // .xy = Tiling(.x, .y), .zw = Offset(.z, .w)

// ボーテックス関数内
o.uv = i.texcoord.xy * _BaseMap_ST.xy + _BaseMap_ST.zw;

動作原理:
コードの行はベクトル演算として解釈され、(元UV × 縮尺) + 移動量が計算されます。インスペクター上で値を変更すると、リアルタイムでUVの伸張と配置位置が更新されます。[図解: UVスクリプトによるタイル表示とシフト効果]

テクスチャプロパティの設定ガイド

エディタ上でテクスチャを選択した際に表示されるプロパティは、レンダリング品質とパフォーマンスに直結します。

  • Texture Type: 用途に応じた分類。標準画像はDefault、凸凹表現にはNormal map、UI要素にはSpriteなどを指定します。
  • Read/Write Enabled: CPU側からのランタイム読み書きを許可するかどうか。メモリ消費が増加するため、通常はOFFのままにします。
  • Mip Maps: 遠距離描画用の低解像版をプリコンパイルします。モアレ現象を防ぎ、キャッシュ効率を向上させます。
  • Wrap Mode: UVが0〜1を超えた際の挙動。Repeatはパターンを周期的に繰り返す一方、Clampは端のピクセルを引き延ばして境界線を固定します。
  • Filter Mode: スケーリング時の補間方式。Pointは階調を犠牲にしたドット絵風、Bilinearは滑らかさ、Trilinearは距離に応じたMipmap切り替えを行い最も高品質ですが負荷も大きいです。

ノーマルマッピングと接空間(Tangent Space)

ポリゴン数の増大を抑えながら表面の細かな凹凸を表現する手法がノーマルマップです。通常の法線マップは世界座標基準ですが、モデルの回転や位置変動に追従しにくい欠点があります。そのため、ローカルな形状のみを基準とする接空間(タンジェントスペース)が標準的に採用されます。

接空間は以下の3軸で構成される直交座標系です:

  • N (Normal): その頂点の表面に向かう標準的な法線
  • T (Tangent): U方向(経線)に沿った接線
  • B (Bitangent/Binormal): V方向(緯線)に沿った補助接線(N×Tから算出)

法線テクスチャのRGB値は実際の法線ベクトル(-1〜+1)ではなく、0〜1のカラー値に変換されて保存されています。復元時は逆変換が必要です:normal = pixel * 2.0 - 1.0。Unityでは組み込み関数UnpackNormal()がこの処理をカプセル化しているため、自前計算は不要です。

接空間ベースのノーマルマップシェーダー

照明計算をすべて接空間内で完結させることで、アセット回転時のズレを防ぐ実装例です。

Shader "Custom/02_NormalMapped"
{
    Properties
    {
        _BaseMap("Base Texture", 2D) = "white" {}
        _Tint("Tint Color", Color) = (1, 1, 1, 1)
        _NormalMap("Normal Texture", 2D) = "bump" {}
        _BumpFactor("Bump Intensity", Float) = 1.0
    }
    SubShader
    {
        Pass
        {
            Tags { "LightMode" = "ForwardBase" }
            CGPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D _BaseMap;
            float4 _BaseMap_ST;
            fixed4 _Tint;
            sampler2D _NormalMap;
            float4 _NormalMap_ST;
            float _BumpFactor;

            struct VertexInput
            {
                float4 pos    : POSITION;
                float3 norm   : NORMAL;
                float4 tangent : TANGENT;
                float4 texcoord : TEXCOORD0;
            };

            struct FragmentInput
            {
                float4 clipPos : SV_POSITION;
                float3 lightDirTS : TEXCOORD0; // 接空間内の光ベクトル
                float4 worldPos   : TEXCOORD1;
                float2 uvBase  : TEXCOORD2;
                float2 uvNorm  : TEXCOORD3;
            };

            FragmentInput Vert(VertexInput i)
            {
                FragmentInput o;
                o.clipPos = UnityObjectToClipPos(i.pos);
                o.worldPos = mul(unity_ObjectToWorld, i.pos);
                
                // UV補正の適用
                o.uvBase = i.texcoord.xy * _BaseMap_ST.xy + _BaseMap_ST.zw;
                o.uvNorm = i.texcoord.xy * _NormalMap_ST.xy + _NormalMap_ST.zw;

                // TBN行列の構築と光ベクトルの接空間変換
                TANGENT_SPACE_ROTATION;
                float3 lightWorldDir = ObjSpaceLightDir(i.pos);
                o.lightDirTS = mul(rotation, lightWorldDir);
                return o;
            }

            fixed4 Frag(FragmentInput i) : SV_Target
            {
                // 法線テクスチャから接空間法線を取得
                fixed3 tangentNormal = UnpackNormal(tex2D(_NormalMap, i.uvNorm));
                tangentNormal.xy *= _BumpFactor; // 凹凸の強さを微調整
                tangentNormal = normalize(tangentNormal);

                // 接空間内の照明計算
                float3 L = normalize(i.lightDirTS);
                float NdotL = max(dot(tangentNormal, L), 0.0);
                
                fixed3 albedo = tex2D(_BaseMap, i.uvBase).rgb * _Tint.rgb;
                fixed3 diffuse = _LightColor0.rgb * albedo * NdotL;
                
                return fixed4(diffuse + UNITY_LIGHTMODEL_AMBIENT.rgb, 1.0);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

[図解: 平坦な面で法線マップを適用した立体感の差異比較]

透明度処理とアルファブレンド

ガラス、 foliage、半透明素材などを描画するには、不透明キューの変更とピクセル合成ルーチンの指定が必要です。

// シェーダーフラグ設定
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }

// パス内設定
ZWrite Off                   // デpthバッファへの書き込み禁止(背面が透けて表示されるよう調整)
Blend SrcAlpha OneMinusSrcAlpha // 加法合成(アルファ混合)

フラグメント関数側では、最終的な戻り値のAチャンネルに透過率を指定します。以下はベースのテクスチャアルファを使用する実装パターンの抜粋です。

struct FragmentInput
{
    float4 clipPos : SV_POSITION;
    float3 lightDirTS : TEXCOORD0;
    float2 uvBase  : TEXCOORD2;
    // ...他
};

fixed4 Frag(FragmentInput i) : SV_Target
{
    fixed3 tangentNormal = UnpackNormal(tex2D(_NormalMap, i.uvNorm));
    tangentNormal.xy *= _BumpFactor;
    tangentNormal = normalize(tangentNormal);

    fixed4 baseSample = tex2D(_BaseMap, i.uvBase);
    float3 albedo = baseSample.rgb * _Tint.rgb;
    
    float3 L = normalize(i.lightDirTS);
    float NdotL = max(dot(tangentNormal, L), 0.0);
    fixed3 diffuse = _LightColor0.rgb * albedo * NdotL;

    // 環境光もテクスチャの発色に応じて弱めるのが一般的
    fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo;

    // アルファ値をそのまま出力
    return fixed4(diffuse + ambient, baseSample.a);
}

アルファブレンド時、描画順序が前後すると期待した結果になりにくい場合があります。特に複雑な形状ではシャドウ受けの有効無効を切り替えるか、カスタムレンダーキューを実装することが推奨されます。

タグ: Unity HLSL シェーダー UVEdition ノーマルマップ

6月21日 16:46 投稿