夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

MRTK StanderdShaderを読み解く その7 ノーマルマップ

本日はMRTKStandardShaderの勉強枠です。

〇これまでのMRTKStandardShaderの記事

redhologerbera.hatenablog.com

redhologerbera.hatenablog.com

redhologerbera.hatenablog.com

redhologerbera.hatenablog.com

redhologerbera.hatenablog.com

redhologerbera.hatenablog.com

〇ノーマルマップ

ノーマルマップの画像を使用するためにPropertiesにノーマルマップのパラメータを追加します。

    Properties
    {
            // Main maps.
            _Color("Color", Color) = (1.0, 1.0, 1.0, 1.0)            
            _MainTex("Albedo", 2D) = "white" {}

            _Metallic("Metallic", Range(0.0, 1.0)) = 0.0
            _Smoothness("Smoothness", Range(0.0, 1.0)) = 0.0

            [Toggle(_NORMAL_MAP)] _EnableNormalMap("Enable Normal Map", Float) = 0.0//追加
            [NoScaleOffset] _NormalMap("Normal Map", 2D) = "bump" {}//追加
    }

[NoScaleOffset]という属性はこれを付けたテクスチャのtiling/offset欄をマテリアルのパラメータ上で非表示にします。

f:id:Holomoto-Sumire:20200621141503j:plain

tiling/offsetを用いないテクスチャの場合などに使用します。

〇appdata_t構造体

appdata_t構造体は頂点シェーダーで用いるデータを格納します。

                struct appdata_t
                {
                     ...
                    // Used for smooth normal data (or UGUI scaling data).
                    float4 uv2 : TEXCOORD2;
                    // Used for UGUI scaling data.
                    float2 uv3 : TEXCOORD3;

                   ...
    #if defined(_NORMAL_MAP)
                    fixed4 tangent : TANGENT;
    #endif
                    ...
                };

appdataでは前回のライトの影響を受ける状態から上記が追加されています。

uv2はスムーズな法線のデータ(またはUGIスケーリングデータ)に使用されます。

uv3はUGUIのスケーリングのデータに使用されます。

 #if defined(_NORMAL_MAP)
                    fixed4 tangent : TANGENT;
    #endif

ノーマルマップがオンの場合コンパイルされる部分でtangent(接線)が追加されています。

〇v2f構造体

頂点シェーダーで計算されたデータをフラグメントシェーダーに渡すデータが記述されます。

前回の状態から追加した部分は次になります。

struct v2f{
...
#if defined(_WORLD_POSITION)
                    float3 worldPosition : TEXCOORD2;
    #endif
    #if defined(_SCALE)
                    float3 scale : TEXCOORD3;
    #endif
 #if defined(_NORMAL)
 
    #if defined(_NORMAL_MAP)
                    fixed3 tangentX : COLOR3;
                    fixed3 tangentY : COLOR4;
                    fixed3 tangentZ : COLOR5;
    #else
                    fixed3 worldNormal : COLOR3;
    #endif
    #endif
                   ...
}

worldPositionScaleはそれぞれ3番目、4番目のUV座標として扱われます。

tangentにはX、Y、Zそれぞれ頂点色情報として扱われます。

worldNormalはtangentXと同じ頂点色情報としてあ疲れますが、ノーマルマップを使用する場合はコンパイルされません。

〇頂点シェーダー

v2f vert(appdata_t v)
{
  v2f o;
    ...
    #if defined(_WORLD_POSITION) 
                    float3 worldVertexPosition = mul(unity_ObjectToWorld, vertexPosition).xyz;
    #endif

    #if defined(_SCALE)
                    o.scale.x = length(mul(unity_ObjectToWorld, float4(1.0, 0.0, 0.0, 0.0)));
                    o.scale.y = length(mul(unity_ObjectToWorld, float4(0.0, 1.0, 0.0, 0.0)));
  
                    o.scale.z = length(mul(unity_ObjectToWorld, float4(0.0, 0.0, 1.0, 0.0)));

                    if (v.uv3.y < 0.0)
                    {
                        o.scale.x *= v.uv2.x;
                        o.scale.y *= v.uv2.y;
                        o.scale.z *= v.uv3.x;
                    }
    #endif
  ...
  #if defined(_WORLD_POSITION)
                    o.worldPosition.xyz = worldVertexPosition;
    #endif
  #if defined(_NORMAL)
   
    #if defined(_NORMAL_MAP)
                    fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                    fixed tangentSign = v.tangent.w * unity_WorldTransformParams.w;
                    fixed3 worldBitangent = cross(worldNormal, worldTangent) * tangentSign;
                    o.tangentX = fixed3(worldTangent.x, worldBitangent.x, worldNormal.x);
                    o.tangentY = fixed3(worldTangent.y, worldBitangent.y, worldNormal.y);
                    o.tangentZ = fixed3(worldTangent.z, worldBitangent.z, worldNormal.z);
    #else
                    o.worldNormal = worldNormal;
    #endif
    #endif
   return o;
}
float3 worldVertexPosition = mul(unity_ObjectToWorld, vertexPosition).xyz;

UnityのWorld座標に頂点を変換したものがworldVertexPositionになります。

                 o.scale.x = length(mul(unity_ObjectToWorld, float4(1.0, 0.0, 0.0, 0.0)));
                    o.scale.y = length(mul(unity_ObjectToWorld, float4(0.0, 1.0, 0.0, 0.0)));
  
                    o.scale.z = length(mul(unity_ObjectToWorld, float4(0.0, 0.0, 1.0, 0.0)));

                    if (v.uv3.y < 0.0)
                    {
                        o.scale.x *= v.uv2.x;
                        o.scale.y *= v.uv2.y;
                        o.scale.z *= v.uv3.x;
                    }

v2fのscale(UV座標として扱う)のx、y、zにはそれぞれのUnityWorld座標での各成分ごとのベクトルの長さが入ります。length関数は()内の長さをfloatで返します。

if文でappdata_tのuv3のy成分が負の場合v2fのscale(直前で代入した単位ベクトル)にはx,yにはappdata_tのuv2のx,y成分が、zにはappdata_tのuv3のx成分が積算差されます。

o.worldPosition.xyz = worldVertexPosition;

v2fのworldPosition.xyzにはそのままUnityWorld座標に変換したオブジェクトの頂点座標が代入されます。

                    fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                    fixed tangentSign = v.tangent.w * unity_WorldTransformParams.w;
                    fixed3 worldBitangent = cross(worldNormal, worldTangent) * tangentSign;
                    o.tangentX = fixed3(worldTangent.x, worldBitangent.x, worldNormal.x);
                    o.tangentY = fixed3(worldTangent.y, worldBitangent.y, worldNormal.y);
                    o.tangentZ = fixed3(worldTangent.z, worldBitangent.z, worldNormal.z);

wordlTangentはappdata_tのtangentをUnityWorld座標の法線に変換され代入されます。 UnityObjectToWorldDir()はUnity座標での法線を求める関数です。

tangentSignはappdata_tのtangentのw成分とunity_WorldTransformParams.wが積算されて代入されています。

ここでtangentのw成分は従法線を反転させるために使用されます。unity_WorldTransformParams は奇数負のスケール変換を行います。

tangentSignにはどのような意味を持つのか?同じような疑問を持った方の質問が見つかりました。

https://forum.unity.com/threads/what-is-tangent-w-how-to-know-whether-its-1-or-1-tangent-w-vs-unity_worldtransformparams-w.468395/

The model from an external application usually comes with normals, tangents, and binormals / bitangents (depending on which terminology you subscribe to). Unity throws away the actual binormal and encode the cross direction in the tanget's w component. You are also correct that for OpenGL and DirectX this value is inverted, but not exactly for the reason you're thinking. This comes down to what the tangent and binormal are representing; they are the direction of flow of the UVs. The tangent is the U of the UV, which for both OpenGL and DirectX is left to right (0.0 on the left, 1.0 on the right). The binormal is the V of the UV, which is different in OpenGL and DirectX. OpenGL is bottom to top, and DirectX is top to bottom. This is also where the difference in many engine's and 3d tools' preference for "+Y / -Y" normal maps comes from. Unity is +Y, which is the OpenGL standard, and Unreal is -Y, which is the DirectX standard. Obviously Unity is using DirectX on Windows, so most of the time the w component is going to be negative. However what if the texture UVs are inverted, not because the mesh scale is inverted, but because the mesh's UVs themselves are going the opposite direction life if the textures are mirrored, then it also needs the w inverted for the half that's mirrored.

(簡単に訳) 他のアプリケーションからのモデルは通常、法線、接線、双法線/二角線などの情報があるけどUnityは、それらの情報を捨てて、TANGENTのwコンポーネントで交差方向をエンコードします。 Unity は 鉛直軸が+Y のため多くの場合w 成分は負の値になることがあるが、テクスチャのUVに関してはメッシュのスケールが反転しているのではなく、メッシュのUV自体が逆方向になっているため、テクスチャがミラーリングされている場合は、ミラーリングされている半分についてもwを反転させる必要があります。

とあります。 つまりUnityの軸に合わせて計算するとテクスチャとかのUVが壊れることがあるから補正するために計算したものがtangentSignになるようです。

                    fixed3 worldBitangent = cross(worldNormal, worldTangent) * tangentSign;

worldBitangentにはworldNormaとworldTangentとの外積に先ほどのtangentSignが積算されます。

Bitanentとはジオメトリを動的に維持するためのサブルーチンになるようです。

en.wikipedia.org

worldNormalは頂点持つ法線です。 worldTangentは接線の法線です。

o.tangentX、o.tangentY、o.tangentZにはそれぞれ対応するworldTangent成分, worldBitangent成分, worldNormal成分を持つfloat3型として代入されます。

〇頂点シェーダーでやっていること(要約)

①頂点の座標、法線をUnityの座標に変換

②Scaleには頂点の座標成分ごとの長さが代入される。

③法線を計算する

長くなってしまったのでフラグメントシェーダーは明日見ていきます。

〇MRTK StandardShader(Normal)

Shader "Custom/MRTKNormalMap"
{
    Properties
    {
        // Main maps.
        _Color("Color", Color) = (1.0, 1.0, 1.0, 1.0)
        _MainTex("Albedo", 2D) = "white" {}
        _Metallic("Metallic", Range(0.0, 1.0)) = 0.0
        _Smoothness("Smoothness", Range(0.0, 1.0)) = 0.5
     
        [Toggle(_NORMAL_MAP)] _EnableNormalMap("Enable Normal Map", Float) = 0.0
        [NoScaleOffset] _NormalMap("Normal Map", 2D) = "bump" {}
        _NormalMapScale("Scale", Float) = 1.0
      

            // Rendering options.
            [Toggle(_DIRECTIONAL_LIGHT)] _DirectionalLight("Directional Light", Float) = 1.0
           
    }

        SubShader
            {
                Pass
                {
                    Name "Main"
                    Tags{ "RenderType" = "Opaque" "LightMode" = "ForwardBase" }
                    LOD 100
                  
                    CGPROGRAM

                    #pragma vertex vert
                    #pragma fragment frag
                  
                    #pragma shader_feature _NORMAL_MAP
          
                    #pragma shader_feature _DIRECTIONAL_LIGHT
              
                    #include "UnityCG.cginc"
                    #include "UnityStandardConfig.cginc"
                    #include "UnityStandardUtils.cginc"
                    #include "MixedRealityShaderUtils.cginc"

    #if defined(_DIRECTIONAL_LIGHT)
                #define _NORMAL
    #else
                #undef _NORMAL
    #endif

    #if defined(_NORMAL)
                #define _WORLD_POSITION
    #else
                #undef _WORLD_POSITION
    #endif
 
    #if defined(_DIRECTIONAL_LIGHT)
                #define _FRESNEL
    #else
                #undef _FRESNEL
    #endif
                #define _UV

                struct appdata_t
                {
                    float4 vertex : POSITION;
                    // The default UV channel used for texturing.
                    float2 uv : TEXCOORD0;
                    // Used for smooth normal data (or UGUI scaling data).
                    float4 uv2 : TEXCOORD2;
                    // Used for UGUI scaling data.
                    float2 uv3 : TEXCOORD3;

                    fixed3 normal : NORMAL;
    #if defined(_NORMAL_MAP)
                    fixed4 tangent : TANGENT;
    #endif
                    UNITY_VERTEX_INPUT_INSTANCE_ID
                };

                struct v2f
                {
                    float4 position : SV_POSITION;

    #if defined(_UV)
                    float2 uv : TEXCOORD0;
    #endif
 
    #if defined(_WORLD_POSITION)
                    float3 worldPosition : TEXCOORD2;
    #endif
    #if defined(_SCALE)
                    float3 scale : TEXCOORD3;
    #endif
    #if defined(_NORMAL)
 
    #if defined(_NORMAL_MAP)
                    fixed3 tangentX : COLOR3;
                    fixed3 tangentY : COLOR4;
                    fixed3 tangentZ : COLOR5;
    #else
                    fixed3 worldNormal : COLOR3;
    #endif
    #endif
                    UNITY_VERTEX_OUTPUT_STEREO
                };

 
                fixed4 _Color;
                sampler2D _MainTex;
                fixed4 _MainTex_ST;


                fixed _Metallic;
                fixed _Smoothness;



    #if defined(_NORMAL_MAP)
                sampler2D _NormalMap;
                float _NormalMapScale;
    #endif



#if defined(_DIRECTIONAL_LIGHT)
#if defined(_LIGHTWEIGHT_RENDER_PIPELINE)
                CBUFFER_START(_LightBuffer)
                    float4 _MainLightPosition;
                half4 _MainLightColor;
                CBUFFER_END
#else
                fixed4 _LightColor0;
#endif
#endif


    #if defined(_DIRECTIONAL_LIGHT)
                static const fixed _MinMetallicLightContribution = 0.7;
                static const fixed _IblContribution = 0.1;
    #endif

    #if defined(_FRESNEL)
                static const float _FresnelPower = 8.0;
    #endif


                v2f vert(appdata_t v)
                {
                    v2f o;
                    UNITY_SETUP_INSTANCE_ID(v);
                    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
 
                    float4 vertexPosition = v.vertex;

    #if defined(_WORLD_POSITION) 
                    float3 worldVertexPosition = mul(unity_ObjectToWorld, vertexPosition).xyz;
    #endif

    #if defined(_SCALE)
                    o.scale.x = length(mul(unity_ObjectToWorld, float4(1.0, 0.0, 0.0, 0.0)));
                    o.scale.y = length(mul(unity_ObjectToWorld, float4(0.0, 1.0, 0.0, 0.0)));
  
                    o.scale.z = length(mul(unity_ObjectToWorld, float4(0.0, 0.0, 1.0, 0.0)));

                    if (v.uv3.y < 0.0)
                    {
                        o.scale.x *= v.uv2.x;
                        o.scale.y *= v.uv2.y;
                        o.scale.z *= v.uv3.x;
                    }
    #endif

                    fixed3 localNormal = v.normal;

    #if defined(_NORMAL) 
                    fixed3 worldNormal = UnityObjectToWorldNormal(localNormal);
    #endif

                    o.position = UnityObjectToClipPos(vertexPosition);

    #if defined(_WORLD_POSITION)
                    o.worldPosition.xyz = worldVertexPosition;
    #endif
  
    #if defined(_UV)
                    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    #endif

 
    #if defined(_NORMAL)
   
    #if defined(_NORMAL_MAP)
                    fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
                    fixed tangentSign = v.tangent.w * unity_WorldTransformParams.w;
                    fixed3 worldBitangent = cross(worldNormal, worldTangent) * tangentSign;
                    o.tangentX = fixed3(worldTangent.x, worldBitangent.x, worldNormal.x);
                    o.tangentY = fixed3(worldTangent.y, worldBitangent.y, worldNormal.y);
                    o.tangentZ = fixed3(worldTangent.z, worldBitangent.z, worldNormal.z);
    #else
                    o.worldNormal = worldNormal;
    #endif
    #endif
                    return o;
                }
                fixed4 frag(v2f i, fixed facing : VFACE) : SV_Target
                {

                // Texturing.
                  fixed4 albedo = tex2D(_MainTex, i.uv);

                    // Normal calculation.
    #if defined(_NORMAL)
                    fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPosition.xyz));
                    fixed3 worldNormal;

    #if defined(_NORMAL_MAP)
 
                    fixed3 tangentNormal = UnpackScaleNormal(tex2D(_NormalMap, i.uv), _NormalMapScale);
                    worldNormal.x = dot(i.tangentX, tangentNormal);
                    worldNormal.y = dot(i.tangentY, tangentNormal);
                    worldNormal.z = dot(i.tangentZ, tangentNormal);
                    worldNormal = normalize(worldNormal) * facing;
    #endif
    #else
                    worldNormal = normalize(i.worldNormal) * facing;
    #endif

                    fixed pointToLight = 1.0;
                    fixed3 fluentLightColor = fixed3(0.0, 0.0, 0.0);

                    // Blinn phong lighting.
    #if defined(_DIRECTIONAL_LIGHT)
    #if defined(_LIGHTWEIGHT_RENDER_PIPELINE)
                    float4 directionalLightDirection = _MainLightPosition;
    #else
                    float4 directionalLightDirection = _WorldSpaceLightPos0;
    #endif
                    fixed diffuse = max(0.0, dot(worldNormal, directionalLightDirection)); 
                    fixed specular = 0.0;
    #endif

                    fixed3 ibl = unity_IndirectSpecColor.rgb;
    #if defined(_FRESNEL)
                    fixed fresnel = 1.0 - saturate(abs(dot(worldViewDir, worldNormal)));
                    fixed3 fresnelColor = unity_IndirectSpecColor.rgb * (pow(fresnel, _FresnelPower) * max(_Smoothness, 0.5));
    #endif
                    // Final lighting mix.
                    fixed4 output = albedo;  
                    fixed3 ambient = glstate_lightmodel_ambient + fixed3(0.25, 0.25, 0.25);
                    fixed minProperty = min(_Smoothness, _Metallic);
    #if defined(_DIRECTIONAL_LIGHT)
                    fixed oneMinusMetallic = (1.0 - _Metallic);
                    output.rgb = lerp(output.rgb, ibl, minProperty);
    #if defined(_LIGHTWEIGHT_RENDER_PIPELINE)
                    fixed3 directionalLightColor = _MainLightColor.rgb;
    #else
                    fixed3 directionalLightColor = _LightColor0.rgb;
    #endif
                    output.rgb *= lerp((ambient + directionalLightColor * diffuse + directionalLightColor * specular) * max(oneMinusMetallic, _MinMetallicLightContribution), albedo, minProperty);
                    output.rgb += (directionalLightColor * albedo * specular) + (directionalLightColor * specular * _Smoothness);
                    output.rgb += ibl * oneMinusMetallic * _IblContribution;
    #endif

    #if defined(_FRESNEL)
                    output.rgb += fresnelColor * (1.0 - minProperty);
    #endif
                    return output;
                }
                ENDCG
            }
            }
                Fallback "Hidden/InternalErrorShader"
 }