ゲーム開発
Unity
UnrealEngine
C++
Blender
ゲーム数学
ゲームAI
グラフィックス
サウンド
アニメーション
GBDK
制作日記
IT関連
ツール開発
フロントエンド関連
サーバサイド関連
WordPress関連
ソフトウェア設計
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
ラーメン日記
四コマ漫画
その他
おすすめアイテム
おもしろコラム
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • Blender
    • ゲーム数学
    • ゲームAI
    • グラフィックス
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • IT関連
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • WordPress関連
    • ソフトウェア設計
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • ラーメン日記
    • 四コマ漫画
      • その他
        • おすすめアイテム
        • おもしろコラム
      1. ホーム
      2. 20230312_01

      【Unity】第二回 シェーダーライティング入門 〜テクスチャマップを使用したライティング〜(法線マップ、スペキュラマップ、AOマップ)【シェーダー】

      Unityグラフィックスシェーダー
      2023-03-14

      マイケル
      マイケル
      みなさんこんにちは!
      マイケルです!
      エレキベア
      エレキベア
      こんにちクマ〜〜〜〜
      マイケル
      マイケル
      今日は前回と引き続き、シェーダーライティングを進めていきます!
      前回は基本のライティングを進めてきましたが、今回は「テクスチャマッピングによるライティング」編になります!
      エレキベア
      エレキベア
      テクスチャマップというと、法線マップとかのあれクマ??
      マイケル
      マイケル
      その通り!
      法線マップの他にもいろんな種類があるから、概要からシェーダーでのライティングについて解説していきます!
      エレキベア
      エレキベア
      楽しみクマ〜〜〜
      マイケル
      マイケル
      なお、今回実装するシェーダーはGitHubにも上げています!
      こちらもよければご参考ください!!

      GitHub – masarito617/unity-lighting-shader-sample

      エレキベア
      エレキベア
      毎度お馴染みクマ〜〜〜〜

      参考書籍

      マイケル
      マイケル
      シェーダー実装や内容をまとめるにあたり、
      下記の書籍を参考にさせていただきました!

      HLSL シェーダーの魔導書 シェーディングの基礎からレイトレーシングまで

      DirectX 9 シェーダプログラミングブック

      エレキベア
      エレキベア
      「DirectX 9 シェーダプログラミングブック」は今は手に入りにくいクマが、
      どっちも分かりやすくて定番書籍クマ〜〜〜

      テクスチャマッピングの概要と種類

      テクスチャマッピングとは

      マイケル
      マイケル
      まずテクスチャマッピングとはそもそも何なのか?という点についてですが、
      こちらは 「テクスチャを使用して3Dモデル表面に質感を与える手法」になります。
      このテクスチャをテクスチャマップといい、普段ただテクスチャを貼り付けて色を描画するだけのテクスチャもカラーマップ(アルベドマップ)と呼ばれるテクスチャマップの一種になります。
      エレキベア
      エレキベア
      逆に言えば色情報以外の情報もテクスチャに持たせることでいろんな表現ができるクマね

      テクスチャマップの種類

      マイケル
      マイケル
      代表的なテクスチャマップの種類としては下記のようなものがあります!
      名称 概要 格納情報
      カラーマップ オブジェクトの色を表現するためのテクスチャ 色情報(RGB)
      ハイトマップ 高低情報を示したテクスチャ 高低情報(グレースケール)
      法線マップ 法線情報を元にディテールを表現するためのテクスチャ 法線情報(RGB)
      スペキュラマップ 光沢の量を示したテクスチャ 光沢量(グレースケール)
      AOマップ 環境光の影響の強さを示したテクスチャ 環境光の影響(グレースケール) or 陰影を付与したテクスチャ(RGB)
      ディスプレイスメントマップ 実際の形状に変形してディテールを表現するためのテクスチャ 形状を動かす距離(グレースケール)
      マイケル
      マイケル
      この中でも得に物体表面に凸凹を付けてディテールを表現する手法バンプマッピングと呼びます。
      バンプマッピングは、初めはハイトマップが使用されていましたが、現在はより細かいディテールを表現できる法線マップの使用が主流のようです。
      また、ハイトマップはグレースケールで手軽に作成できるため、法線マップを生成するのにも使用されることがあるようです。
      エレキベア
      エレキベア
      バンプマッピングも名前はよく聞くクマね
      マイケル
      マイケル
      スペキュラマップAOマップはピクセルごとに影響量を持たせることで調整を行うテクスチャです。
      以前トゥーンシェーダーを作成した際に使用した、輪郭線の描画量を書き込んだテクスチャもこれらと似た要領になりますね。
      エレキベア
      エレキベア
      ピクセルごとに追加で必要な情報を載せるのクマね
      マイケル
      マイケル
      また、AOマップに関してはカラーマップに陰影を付与したものをAOマップと呼ぶことも多いようです。
      今回は陰影量のみを抜き出したグレースケールとして扱っていきます。
      マイケル
      マイケル
      ディスプレイスメントマップに関しては目的は法線マップと同じですが、実際に形状を変形させるすごいやつです!
      エレキベア
      エレキベア
      一番すごそうクマ・・・
      マイケル
      マイケル
      今回はこの中で
      ・法線マップ
      ・スペキュラマップ
      ・AOマップ
      の3種類を使用してライティングを行なっていきます!
      ディスプレイスメントマップに関しては、正確に使用するにはテッセレーション等の知識が必要になるため、今回は割愛とさせてください。。(いつかこちらもまとめるかもしれません)
      エレキベア
      エレキベア
      まあまずは基本どころを抑えるクマね

      テクスチャマップの生成方法

      マイケル
      マイケル
      テクスチャマップの生成方法としては、主に下記のような方法があります。


      1. WEBツールを活用する
      2. 3DCGソフトから出力する
      3. ペイントソフトから作成する

      マイケル
      マイケル
      2.3.はBlenderやPhotoshopといったソフトを使用して生成する方法です。
      これらのソフトでの生成方法についてはまた別途記事でまとめることにして、今回は1.のWEBツールを使用して出力したテクスチャを使用して進めます。
      エレキベア
      エレキベア
      各ソフトでの使い方まで書いてるとかなり多くなりそうクマからね
      マイケル
      マイケル
      今回、WEBツールは下記の NormalMap-Online というサービスを使用させていただきました。

      ScreenShot 2023 03 13 0 49 57

      NormalMap-Online

      マイケル
      マイケル
      こちらはアップロードしたハイトマップや画像から法線マップやディスプレイスメントマップといったテクスチャを作成するサービスになっています。
      今回はデフォルトで設定されているキューブの各面に渦模様が入ったテクスチャを使用させていただきました。
      エレキベア
      エレキベア
      なんてありがたいサービスクマ・・・

      法線マップを使用したシェーダー実装

      マイケル
      マイケル
      それでは早速法線マップを使用したライティングについて触れていきます!

      法線マップの考え方

      マイケル
      マイケル
      法線マップは前述した通り、法線情報を元に凸凹等のディテールを表現するために使用されます。
      イメージとしては、下記のように平面なポリゴン内で凸凹している箇所について法線マップで別途法線情報を書き込んでおくことで、ライティング計算する際に陰影を付けるようなものになります。
      ↑平面なポリゴン内で法線情報を詳細に定義する
      エレキベア
      エレキベア
      なるほどクマ
      これでポリゴン数を抑えながら凸凹を表現できるわけクマね

      オブジェクトスペースとタンジェントスペース

      マイケル
      マイケル
      法線マップに書き込む情報としては大きく
      ・オブジェクトスペース
      ・タンジェントスペース
      の2つの座標系で書き込む方法があります。
      マイケル
      マイケル
      オブジェクトスペースは法線のXYZ座標をそのまま書き込んだもので、
      タンジェントスペースは
      ・法線(normal)
      ・接線(tangent)
      ・従法線(binormal)
      の3つの軸に分けた法線座標系で書き込んだものになります。
      ↑オブジェクトスペースとタンジェントスペース
      エレキベア
      エレキベア
      タンジェントスペースは3つの成分に分けて書き込むクマね
      マイケル
      マイケル
      しかし、オブジェクトスペースで書き込んだ場合には頂点の変形に対応できないという問題点があるため、
      一般的に法線マップはタンジェントスペースで書き込まれているものがほとんどです。
      マイケル
      マイケル
      タンジェントスペースで法線マップを作成した例としては下記のようになります。
      RGB成分に対して(tangent, binormal, normal)で書き込むため、基本的には垂直なnormalが大きく、青成分が強く出ているのが特徴です。
      ↑タンジェントスペースの法線マップの例
      エレキベア
      エレキベア
      法線マップが青みがかってるのはそういう意味だったのクマね

      1. 法線情報をワールド座標空間に変換して計算する方法

      マイケル
      マイケル
      法線マップの情報を使用して法線を計算する方法としては大きく
      1. 法線情報をワールド座標空間に変換して計算する方法
      2. ライトやカメラを接空間に変換して計算する方法
      の2つがあります。
      まずは基本の考え方となる1.の方法を見ていきましょう!
      マイケル
      マイケル
      計算方法としては、法線マップに書き込まれたタンジェントスペースの法線情報と、
      頂点座標の基底軸を掛け合わせることで法線を算出します。
      簡単な数式で表すと下記のようになります。
      ↑基底軸と法線マップの情報から法線を算出する
      エレキベア
      エレキベア
      基底軸は頂点シェーダーに渡されてくる情報クマね
      これなら頂点が変形しても対応できそうクマ
      マイケル
      マイケル
      最終的にワールド座標の法線が必要となるため、
      1. 頂点シェーダー内で基底軸をワールド座標に変換する
      2. フラグメントシェーダー内で変換した基底軸と法線マップから取り出した法線座標を掛け合わせる

      といった流れになります。
      こちらをシェーダーで実装したものが下記になります。
      // 法線マップシェーダー
      // 法線情報をワールド座標空間に変換して計算する方法
      Shader "Unlit/NormalMapFromWorldSpace"
      {
          Properties
          {
              _MainTex ("Main Tex", 2D) = "white" {}
              _NormalMap ("Normal Map", 2D) = "bump" {}
              _SpecularLevel ("Specular Level", Range(0.1, 50)) = 30 // 鏡面反射指数 a
          }
          SubShader
          {
              Tags { "LightMode"="ForwardBase" }
              LOD 100
              Pass
              {
                  CGPROGRAM
                  #pragma vertex vert
                  #pragma fragment frag
                  #include "UnityCG.cginc"
                  #include "UnityLightingCommon.cginc"
                  struct appdata
                  {
                      float4 vertex : POSITION;
                      float2 uv : TEXCOORD0;
                      half3 normal : NORMAL;
                      half4 tangent : TANGENT;
                  };
                  struct v2f
                  {
                      float2 uv : TEXCOORD0;
                      float4 vertex : SV_POSITION;
                      float4 vertexWorld : TEXCOORD1;
                      // 基底軸のワールド座標情報
                      half3 normalWorld : TEXCOORD2;   // 法線
                      half3 tangentWorld : TEXCOORD3;  // 接線
                      half3 biNormalWorld : TEXCOORD4; // 従法線
                  };
                  sampler2D _MainTex;
                  float4 _MainTex_ST;
                  sampler2D _NormalMap;
                  float _SpecularLevel;
                  v2f vert (appdata v)
                  {
                      v2f o;
                      o.vertex = UnityObjectToClipPos(v.vertex);
                      o.vertexWorld = mul(unity_ObjectToWorld, v.vertex);
                      o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                      // 法線情報をワールド座標に変換
                      // 法線、接線
                      o.normalWorld = mul(unity_ObjectToWorld, v.normal);
                      o.tangentWorld = normalize(mul(unity_ObjectToWorld, v.tangent)).xyz;
                      // 従法線
                      // w成分は通常は-1だが、uv座標のいずれかが逆にマッピングされた場合には+1になるため乗算している
                      const float3 biNormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
                      o.biNormalWorld = normalize(mul(unity_ObjectToWorld, biNormal));
                      return o;
                  }
                  fixed4 frag (v2f i) : SV_Target
                  {
                      // 法線マップの情報からワールド座標の法線を計算
                      const float3 normalTangent = UnpackNormal(tex2D(_NormalMap, i.uv)); // -1〜1に変換して取得
                      const float3 normalWorld = normalize(i.tangentWorld * normalTangent.x + i.biNormalWorld * normalTangent.y + i.normalWorld * normalTangent.z);
                      // 拡散反射光(Lambert)
                      const float3 l = normalize(_WorldSpaceLightPos0.xyz);
                      const float3 diffuse = saturate(dot(normalWorld, l)) * _LightColor0;
                      // 鏡面反射光(Blinn-Phong)
                      const float3 v = normalize(_WorldSpaceCameraPos - i.vertexWorld);
                      const float3 h = normalize(l+v);
                      const float3 specular = pow(saturate(dot(normalWorld, h)), _SpecularLevel);
                      // 環境光
                      const half3 ambient = ShadeSH9(half4(normalWorld, 1));
                      // 最終的なカラーに乗算
                      const float3 finalColor = ambient + diffuse + specular;
                      fixed4 col = tex2D(_MainTex, i.uv);
                      col.xyz *= finalColor;
                      return col;
                  }
                  ENDCG
              }
          }
      }
      
      ↑法線情報をワールド座標空間に変換して計算する
      エレキベア
      エレキベア
      長いので主要部分の解説を頼むクマ
      マイケル
      マイケル
      よしきた!!
      マイケル
      マイケル
      まずはプロパティ部分を見てみましょう!
      こちらは先ほど解説したnormal、tangentの基底軸を受け取り、ワールド座標に変換したものを渡す流れとなっています。
                  struct appdata
                  {
                      float4 vertex : POSITION;
                      float2 uv : TEXCOORD0;
                      half3 normal : NORMAL;
                      half4 tangent : TANGENT;
                  };
                  struct v2f
                  {
                      float2 uv : TEXCOORD0;
                      float4 vertex : SV_POSITION;
                      float4 vertexWorld : TEXCOORD1;
                      // 基底軸のワールド座標情報
                      half3 normalWorld : TEXCOORD2;   // 法線
                      half3 tangentWorld : TEXCOORD3;  // 接線
                      half3 biNormalWorld : TEXCOORD4; // 従法線
                  };
      
      ↑プロパティの定義
      マイケル
      マイケル
      実際に基底軸をワールド座標に変換する処理は下記のようになります。
      法線、接線しか渡されてこないため、これらの外積を取ることで従法線を算出します。
                  v2f vert (appdata v)
                  {
                      v2f o;
                      o.vertex = UnityObjectToClipPos(v.vertex);
                      o.vertexWorld = mul(unity_ObjectToWorld, v.vertex);
                      o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                      // 法線情報をワールド座標に変換
                      // 法線、接線
                      o.normalWorld = mul(unity_ObjectToWorld, v.normal);
                      o.tangentWorld = normalize(mul(unity_ObjectToWorld, v.tangent)).xyz;
                      // 従法線
                      // w成分は通常は-1だが、uv座標のいずれかが逆にマッピングされた場合には+1になるため乗算している
                      const float3 biNormal = cross(normalize(v.normal), normalize(v.tangent.xyz)) * v.tangent.w;
                      o.biNormalWorld = normalize(mul(unity_ObjectToWorld, biNormal));
                      return o;
                  }
      
      ↑基底軸をワールド座標に変換
      マイケル
      マイケル
      最後に変換した基底軸と法線マップの情報から、上述した計算で法線を算出します。
      この時、用意されたUnpackNormal関数を使用することで書き込まれた0〜1の値を-1〜1に変換して取得することができるので便利です。
      
                  sampler2D _NormalMap;
      
      ・・・
      
                  fixed4 frag (v2f i) : SV_Target
                  {
                      // 法線マップの情報からワールド座標の法線を計算
                      const float3 normalTangent = UnpackNormal(tex2D(_NormalMap, i.uv)); // -1〜1に変換して取得
                      const float3 normalWorld = normalize(i.tangentWorld * normalTangent.x + i.biNormalWorld * normalTangent.y + i.normalWorld * normalTangent.z);
      
                      // 拡散反射光(Lambert)
                      const float3 l = normalize(_WorldSpaceLightPos0.xyz);
                      const float3 diffuse = saturate(dot(normalWorld, l)) * _LightColor0;
      
                      // 鏡面反射光(Blinn-Phong)
                      const float3 v = normalize(_WorldSpaceCameraPos - i.vertexWorld);
                      const float3 h = normalize(l+v);
                      const float3 specular = pow(saturate(dot(normalWorld, h)), _SpecularLevel);
      
                      // 環境光
                      const half3 ambient = ShadeSH9(half4(normalWorld, 1));
      
                      // 最終的なカラーに乗算
                      const float3 finalColor = ambient + diffuse + specular;
                      fixed4 col = tex2D(_MainTex, i.uv);
                      col.xyz *= finalColor;
                      return col;
                  }
      
      マイケル
      マイケル
      ワールド座標の法線座標が算出できたら、後はそれを使用してライティングを行うのみ!
      内容としては前回解説したBlinn-Phongモデルのライティングを使用しています。
      こちらの詳細について知りたい方は、第一回の記事もご参照ください!
      エレキベア
      エレキベア
      やはり基本のライティングの知識は必要になるクマね
      マイケル
      マイケル
      こちらのシェーダーに下記のようなカラーマップ、法線マップを適用すると下記のように描画されることが確認できました!
      ↑カラーマップ
      ↑法線マップ
      ↑法線マップ無し
      ↑法線マップ有り
      マイケル
      マイケル
      法線マップを適用すると、渦マークの部分の陰影が立体的に浮き上がって見えると思います!
      エレキベア
      エレキベア
      これが法線マップの力クマ〜〜〜〜

      2. ライトやカメラを接空間に変換して計算する方法

      マイケル
      マイケル
      次に2つ目の
      ライトやカメラを接空間に変換して計算する方法

      についてですが、こちらはそのままライトやカメラを接空間情報に変換して、法線マップも接空間情報のまま計算する手法です。
      マイケル
      マイケル
      1の方法では法線の算出をフラグメントシェーダー内で行う必要がありましたが、こちらの方法では頂点シェーダー内で計算を行うために負荷が少ないというのがメリットになります!
      エレキベア
      エレキベア
      処理負荷的にはこちらの方がよさそうクマね
      マイケル
      マイケル
      シェーダー全文は下記になります。
      // 法線マップシェーダー
      // ライト、カメラを接空間に変換して計算する方法
      Shader "Unlit/NormalMapFromTangentSpace"
      {
          Properties
          {
              _MainTex ("Main Tex", 2D) = "white" {}
              _NormalMap ("Normal Map", 2D) = "bump" {}
              _SpecularLevel ("Specular Level", Range(0.1, 2)) = 0.8 // 鏡面反射指数 a
          }
          SubShader
          {
              Tags { "LightMode"="ForwardBase" }
              LOD 100
      
              Pass
              {
                  CGPROGRAM
                  #pragma vertex vert
                  #pragma fragment frag
      
                  #include "UnityCG.cginc"
                  #include "UnityLightingCommon.cginc"
      
                  struct appdata
                  {
                      float4 vertex : POSITION;
                      float2 uv : TEXCOORD0;
                      half3 normal : NORMAL;
                      half4 tangent : TANGENT;
                  };
      
                  struct v2f
                  {
                      float2 uv : TEXCOORD0;
                      float4 vertex : SV_POSITION;
                      float3 normalWorld : NORMAL;
                      // ライト、カメラの接空間情報
                      half3 lightTangentDir : TEXCOORD1;
                      half3 viewTangentDir : TEXCOORD2;
                  };
      
                  sampler2D _MainTex;
                  float4 _MainTex_ST;
                  sampler2D _NormalMap;
                  float _SpecularLevel;
      
                  v2f vert (appdata v)
                  {
                      v2f o;
                      o.vertex = UnityObjectToClipPos(v.vertex);
                      o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                      o.normalWorld = mul(unity_ObjectToWorld, v.normal);
      
                      // ライト、カメラの方向ベクトルを接空間に変換
                      TANGENT_SPACE_ROTATION; // 接空間の行列を取得してrotationに格納
                      o.lightTangentDir = normalize(mul(rotation, ObjSpaceLightDir(v.vertex)));
                      o.viewTangentDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
                      return o;
                  }
      
                  fixed4 frag (v2f i) : SV_Target
                  {
                      // 拡散反射(Lambert)
                      const float3 normalTangent = UnpackNormal(tex2D(_NormalMap, i.uv)); // -1〜1に変換して取得
                      const float3 diffuse = saturate(dot(normalTangent, i.lightTangentDir)) * _LightColor0;
      
                      // 鏡面反射(Blinn-Phong)
                      const float h = normalize(i.lightTangentDir + i.viewTangentDir);
                      const float3 specular = pow(saturate(dot(normalTangent, h)), _SpecularLevel);
      
                      // 環境光
                      const half3 ambient = ShadeSH9(half4(i.normalWorld, 1));
      
                      // 最終的なカラーに乗算
                      const float3 finalColor = ambient + diffuse + specular;
                      fixed4 col = tex2D(_MainTex, i.uv);
                      col.xyz *= finalColor;
                      return col;
                  }
                  ENDCG
              }
          }
      }
      
      マイケル
      マイケル
      こちらの主要処理を抜粋していきます。
      まず、ライト、カメラを接空間に変換する処理は下記になります。
                  v2f vert (appdata v)
                  {
                      v2f o;
                      o.vertex = UnityObjectToClipPos(v.vertex);
                      o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                      o.normalWorld = mul(unity_ObjectToWorld, v.normal);
                      // ライト、カメラの方向ベクトルを接空間に変換
                      TANGENT_SPACE_ROTATION; // 接空間の行列を取得してrotationに格納
                      o.lightTangentDir = normalize(mul(rotation, ObjSpaceLightDir(v.vertex)));
                      o.viewTangentDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
                      return o;
                  }
      
      ↑ライト、カメラの方向ベクトルを接空間への変換
      マイケル
      マイケル
      ここではUnity側で用意されているTANGENT_SPACE_ROTATIONを使用しています。
      こちらは下記のような内容になっていて、頂点座標と法線情報から接空間変換用の行列を生成してrotation変数に格納しています。
      // Declares 3x3 matrix 'rotation', filled with tangent space basis
      #define TANGENT_SPACE_ROTATION \
          float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w; \
          float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )
      ↑変換処理は下記のようになっている
      エレキベア
      エレキベア
      用意されているのはありがたいクマね
      マイケル
      マイケル
      あとはこれらを使用して接空間情報上でライティング計算を行うのみ!
      下記のように法線マップから取り出した情報をそのまま計算に使用することができます。
      
                  fixed4 frag (v2f i) : SV_Target
                  {
                      // 拡散反射(Lambert)
                      const float3 normalTangent = UnpackNormal(tex2D(_NormalMap, i.uv)); // -1〜1に変換して取得
                      const float3 diffuse = saturate(dot(normalTangent, i.lightTangentDir)) * _LightColor0;
                      // 鏡面反射(Blinn-Phong)
                      const float h = normalize(i.lightTangentDir + i.viewTangentDir);
                      const float3 specular = pow(saturate(dot(normalTangent, h)), _SpecularLevel);
                      // 環境光
                      const half3 ambient = ShadeSH9(half4(i.normalWorld, 1));
                      // 最終的なカラーに乗算
                      const float3 finalColor = ambient + diffuse + specular;
                      fixed4 col = tex2D(_MainTex, i.uv);
                      col.xyz *= finalColor;
                      return col;
                  }
      
      ↑接空間でライティング計算を行う
      マイケル
      マイケル
      こちらも先ほどと同様テクスチャを設定すると、
      下記のように描画されることが確認できます。
      エレキベア
      エレキベア
      いい感じクマ〜〜〜〜

      スペキュラマップ、AOマップを使用したシェーダー実装

      マイケル
      マイケル
      次はスペキュラマップとAOマップを使用したライティングを見てみましょう!

      スペキュラマップ

      マイケル
      マイケル
      まずはスペキュラマップの使用から!
      こちらは光沢量をグレースケールで表現したテクスチャで、今回は読み取った値をPhong鏡面反射に乗算することで使用しました。
      シェーダー全文は下記になります。
      // スペキュラマップシェーダー
      Shader "Unlit/SpecularMap"
      {
          Properties
          {
              _MainTex ("Texture", 2D) = "white" {}
              _SpecularMap ("Specular Map", 2D) = "white" {}
              _SpecularLevel ("Specular Level", Range(0.1, 2)) = 0.8 // 鏡面反射指数 a
          }
          SubShader
          {
              Tags { "LightMode"="ForwardBase" }
              LOD 100
      
              Pass
              {
                  CGPROGRAM
                  #pragma vertex vert
                  #pragma fragment frag
      
                  #include "UnityCG.cginc"
                  #include "UnityLightingCommon.cginc"
      
                  struct appdata
                  {
                      float4 vertex : POSITION;
                      float2 uv : TEXCOORD0;
                      float3 normal : NORMAL;
                  };
      
                  struct v2f
                  {
                      float2 uv : TEXCOORD0;
                      float4 vertex : SV_POSITION;
                      float3 vertexWorld : TEXCOORD1;
                      float3 normal : NORMAL;
                  };
      
                  sampler2D _MainTex;
                  float4 _MainTex_ST;
                  sampler2D _SpecularMap;
                  float _SpecularLevel;
      
                  v2f vert (appdata v)
                  {
                      v2f o;
                      o.vertex = UnityObjectToClipPos(v.vertex);
                      o.vertexWorld = mul(unity_ObjectToWorld, v.vertex);
                      o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                      o.normal = mul(unity_ObjectToWorld, v.normal);
                      return o;
                  }
      
                  fixed4 frag (v2f i) : SV_Target
                  {
                      // 拡散反射光
                      const float3 l = normalize(_WorldSpaceLightPos0.xyz);
                      const float3 n = normalize(i.normal);
                      const float3 diffuse =  saturate(dot(n, l)) * _LightColor0;
      
                      // 鏡面反射光
                      const float3 v = normalize(_WorldSpaceCameraPos - i.vertexWorld);
                      const float3 h = normalize(l+v);
                      const float specularMask = tex2D(_SpecularMap, i.uv).r; // スペキュラマップからmask値を取得
                      const float3 specular = pow(saturate(dot(n, h)), _SpecularLevel) * specularMask; // mask値を乗算する
      
                      // 環境光
                      const half3 ambient = ShadeSH9(half4(i.normal, 1));
      
                      // 最終的なカラーに乗算
                      const float3 finalColor = ambient + diffuse + specular;
                      fixed4 col = tex2D(_MainTex, i.uv);
                      col.xyz *= finalColor;
                      return col;
                  }
                  ENDCG
              }
          }
      }
      
      エレキベア
      エレキベア
      ほぼBlind-Phongモデルのシェーダーsと同じクマね
      マイケル
      マイケル
      実際に調整を行なっている部分は下記になります。
      前述した通り、スペキュラマップから読みこんだ値を鏡面反射量に乗算しているだけですね。
                      // 鏡面反射光
                      const float3 v = normalize(_WorldSpaceCameraPos - i.vertexWorld);
                      const float3 h = normalize(l+v);
                      const float specularMask = tex2D(_SpecularMap, i.uv).r; // スペキュラマップからmask値を取得
                      const float3 specular = pow(saturate(dot(n, h)), _SpecularLevel) * specularMask; // mask値を乗算する
      
      ↑スペキュラマップから読みこんだ値を鏡面反射量に乗算
      マイケル
      マイケル
      こちらのシェーダーに下記のようなカラーマップ、スペキュラマップを設定した結果がこちらになります。
      ↑カラーマップ
      ↑スペキュラマップ
      ↑スペキュラマップ無し
      ↑スペキュラマップ有り
      マイケル
      マイケル
      スペキュラマップで白く指定した箇所が強く鏡面反射が出ていることが確認できます!
      エレキベア
      エレキベア
      やったクマ〜〜〜

      AOマップ

      マイケル
      マイケル
      最後にAOマップを使用した場合の例を見てみましょう!
      考え方はスペキュラマップの時と同じですが、こちらは環境光に対して値を乗算しています。
      マイケル
      マイケル
      シェーダー全文は下記になります。
      // AOマップシェーダー
      Shader "Unlit/AOMap"
      {
          Properties
          {
              _MainTex ("Texture", 2D) = "white" {}
              _AOMap ("AO Map", 2D) = "white" {}
              _SpecularLevel ("Specular Level", Range(0.1, 2)) = 0.8 // 鏡面反射指数 a
          }
          SubShader
          {
              Tags { "LightMode"="ForwardBase" }
              LOD 100
      
              Pass
              {
                  CGPROGRAM
                  #pragma vertex vert
                  #pragma fragment frag
      
                  #include "UnityCG.cginc"
                  #include "UnityLightingCommon.cginc"
      
                  struct appdata
                  {
                      float4 vertex : POSITION;
                      float2 uv : TEXCOORD0;
                      float3 normal : NORMAL;
                  };
      
                  struct v2f
                  {
                      float2 uv : TEXCOORD0;
                      float4 vertex : SV_POSITION;
                      float3 vertexWorld : TEXCOORD1;
                      float3 normal : NORMAL;
                  };
      
                  sampler2D _MainTex;
                  float4 _MainTex_ST;
                  sampler2D _AOMap;
                  float _SpecularLevel;
      
                  v2f vert (appdata v)
                  {
                      v2f o;
                      o.vertex = UnityObjectToClipPos(v.vertex);
                      o.vertexWorld = mul(unity_ObjectToWorld, v.vertex);
                      o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                      o.normal = mul(unity_ObjectToWorld, v.normal);
                      return o;
                  }
      
                  fixed4 frag (v2f i) : SV_Target
                  {
                      // 拡散反射光
                      const float3 l = normalize(_WorldSpaceLightPos0.xyz);
                      const float3 n = normalize(i.normal);
                      const float3 diffuse =  saturate(dot(n, l)) * _LightColor0;
      
                      // 鏡面反射光
                      const float3 v = normalize(_WorldSpaceCameraPos - i.vertexWorld);
                      const float3 h = normalize(l+v);
                      const float3 specular = pow(saturate(dot(n, h)), _SpecularLevel);
      
                      // 環境光
                      const float aoMask = tex2D(_AOMap, i.uv).r; // AOマップからmask値を取得
                      const half3 ambient = ShadeSH9(half4(i.normal, 1)) * aoMask; // mask値を乗算する
      
                      // 最終的なカラーに乗算
                      const float3 finalColor = ambient + diffuse + specular;
                      fixed4 col = tex2D(_MainTex, i.uv);
                      col.xyz *= finalColor;
                      return col;
                  }
                  ENDCG
              }
          }
      }
      
      マイケル
      マイケル
      実際に調整を行なっている箇所は下記になります。
      こちらもAOマップから値を読み込んで乗算しているだけですね
                      // 環境光
                      const float aoMask = tex2D(_AOMap, i.uv).r; // AOマップからmask値を取得
                      const half3 ambient = ShadeSH9(half4(i.normal, 1)) * aoMask; // mask値を乗算する
      
      ↑AOマップによる環境光の調整
      エレキベア
      エレキベア
      簡単クマ〜〜〜
      マイケル
      マイケル
      こちらのシェーダーに下記のようなカラーマップ、AOマップを適用した結果がこちらになります。
      ↑カラーマップ
      ↑AOマップ
      ↑AOマップ無し
      ↑AOマップ有り
      マイケル
      マイケル
      AOマップを使用して環境光を制限できていることが確認できました!
      エレキベア
      エレキベア
      やったクマ〜〜〜〜

      おわりに

      マイケル
      マイケル
      というわけで今回はテクスチャマップを使用したライティングについてでした!
      どうだったかな??
      エレキベア
      エレキベア
      こんなに種類があるとは知らなかったクマ〜〜〜
      テクスチャを使用して細かい調整ができるのは便利だと思ったクマ
      マイケル
      マイケル
      大事なのはテクスチャに追加情報を使用してライティングに使用できることだね!
      言ってしまえばRGBA情報に設定できれば何でもいいわけだから、今回紹介したものだけではなく状況に応じて柔軟に活用できるようになりたいね!
      エレキベア
      エレキベア
      心得たクマ
      マイケル
      マイケル
      それでは今日はこの辺で!
      アデューー!!!
      エレキベア
      エレキベア
      クマ〜〜〜〜

      【Unity】第二回 シェーダーライティング入門 〜テクスチャマップを使用したライティング〜(法線マップ、スペキュラマップ、AOマップ)【シェーダー】〜完〜


      Unityグラフィックスシェーダー
      2023-03-14

      関連記事
      【Unity】Timeline × Excelでスライドショーを効率よく制作する
      2024-10-31
      【書籍紹介】「コンピュータグラフィックス」に出てくる用語をまとめる【CGエンジニア検定】
      2024-07-13
      【UE5】Niagara SimulationStageによるシミュレーション環境構築
      2024-05-30
      【Unity】Boidsアルゴリズムを用いて魚の群集シミュレーションを実装する
      2024-05-28
      【Unity】GoでのランキングAPI実装とVPSへのデプロイ方法についてまとめる【Go言語】
      2024-04-14
      【Unity】第二回 Wwiseを使用したサウンド制御 〜インタラクティブミュージック編〜
      2024-03-30
      【Unity】第一回 Wwiseを使用したサウンド制御 〜基本動作編〜
      2024-03-30
      【Unity】第二回 CRI ADXを使用したサウンド制御 〜インタラクティブミュージック編〜
      2024-03-28