夜風のMixedReality

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

MixedRealityGraphicsTools StandardShader全機能解説 その⑦NormalMapの実装を読み解く HoloLensアドベントカレンダー2022年3日目

本日はMRGT枠です。

〇HoloLens 2022年アドベントカレンダー

HoloLens 2022年アドベントカレンダーはQiita上で私の師であるがち本さんが開催している企画です。

 クリスマスまで毎日記事を埋めていくことが目的で本日は3日目の記事になります。

qiita.com

〇MixedRealityGraphicsToolsとは?

MixedRealityGraphicsTools(MRGT)はMicrosoftによってオープンソースな形で開発、提供されているMixedRealityデバイス向けのSDKである第三世代のMixedRealityToolkit(MRTK3)で提供されるグラフィック関連のパッケージを指します。

github.com

〇ノーマルマップとは?

ノーマルマップは一般的なシェーダーの機能で凸凹感を演出することでローポリゴンでありながら複雑なディティールを追加することができます。

〇頂点シェーダー

頂点シェーダーは次のようになります。

今回はURPに限って説明しています。

Varyings VertexStage(Attributes input)
{
    Varyings output = (Varyings)0;
  ・・・
    float4 vertexPosition = input.vertex;
    float3 worldVertexPosition = mul(UNITY_MATRIX_M, vertexPosition).xyz;
    half3 localNormal = input.normal;
    half3 worldNormal = TransformObjectToWorldNormal(localNormal);
    output.position = TransformObjectToHClip(vertexPosition.xyz);
    output.worldPosition.xyz = worldVertexPosition;
    output.uv = TRANSFORM_TEX(input.uv, _MainTex);

    //ワールド空間の Tangent へ変換
    half3 worldTangent = TransformObjectToWorldDir(input.tangent.xyz);
    half tangentSign = input.tangent.w * unity_WorldTransformParams.w;
   //ワールド空間のBitangent(従法線)へ変換
    half3 worldBitangent = cross(worldNormal, worldTangent) * tangentSign;
    output.tangentX = half3(worldTangent.x, worldBitangent.x, worldNormal.x);
    output.tangentY = half3(worldTangent.y, worldBitangent.y, worldNormal.y);
    output.tangentZ = half3(worldTangent.z, worldBitangent.z, worldNormal.z);

    return output;
}

頂点シェーダーでは軸変換とノーマルマップを使用するためにtangentの計算を行っています。

tangentは接ベクトルの意味で、筆者の大雑把な理解ではピクセル単位での向きに当たります。

 ノーマルマップは面ごとではなくピクセルごとにライトの処理を調整して凸凹感を演出しています。

ここでの処理がVaryings構造体へ格納されます。

/// <summary>
/// Vertex attributes passed into the vertex shader from the app.
/// </summary>
struct Attributes
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    float4 uv2 : TEXCOORD2;
    float2 uv3 : TEXCOORD3;
    half3 normal : NORMAL;
    half4 tangent : TANGENT;
  ・・・
};

struct Varyings
{
    float4 position : SV_POSITION;
    float2 uv : TEXCOORD0;
    float3 worldPosition : TEXCOORD2;
    half3 tangentX : COLOR3;//ノーマルマップのタンジェント
    half3 tangentY : COLOR4;
    half3 tangentZ : COLOR5;
・・・
};

ここで注目すべきがtangentがXYZでそれぞれベクトルに分けられているという点です。

 分けられている理由が三次元ベクトルであるそれぞれのtangentにワールド空間でのTangent、ワールド空間での従法線、そして法線の3つの値が格納されているためです。

 output.tangentX = half3(worldTangent.x, worldBitangent.x, worldNormal.x);
    output.tangentY = half3(worldTangent.y, worldBitangent.y, worldNormal.y);
    output.tangentZ = half3(worldTangent.z, worldBitangent.z, worldNormal.z);

MRGTシェーダーの特徴的な点としてこのようにtangentを分離格納している点です。

〇フラグメントシェーダーの処理

half4 PixelStage(Varyings input, bool facing : SV_IsFrontFace) : SV_Target
{
  ・・・・
    //メインテクスチャの取得とメインカラーをalbedoに代入
    half4 albedo = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
    albedo *= _Color;


    half3 worldNormal;

    //ノーマルマップのテクスチャをサンプリング
    half3 tangentNormal = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv));

    //ノーマルマップを反映したピクセルごとの法線を取得(従法線と法線の内積)
    worldNormal.x = dot(input.tangentX, tangentNormal);
    worldNormal.y = dot(input.tangentY, tangentNormal);
    worldNormal.z = dot(input.tangentZ, tangentNormal);
    //facing=面の向き 表が1裏が-1、worldNormalを正規化
    worldNormal = normalize(worldNormal) * (facing ? 1.0 : -1.0);


   ///ライトの処理
    half pointToLight = 1.0;
    half3 fluentLightColor = half3(0.0, 0.0, 0.0);

    Light directionalLight = GetMainLight();
    half3 directionalLightDirection = directionalLight.direction;
    half3 directionalLightColor = directionalLight.color;
    //ノーマルマップを反映してライティング
    half diffuse = max(0.0, dot(worldNormal, directionalLightDirection.xyz));
    half specular = 0.0;
    half3 ibl = unity_IndirectSpecColor.rgb;
    
    half4 output = albedo;
    half3 ambient = glstate_lightmodel_ambient.rgb + half3(0.25, 0.25, 0.25);
    half minProperty = min(_Smoothness, _Metallic);

    half oneMinusMetallic = (1.0 - _Metallic);
    output.rgb = lerp(output.rgb, ibl, minProperty);
    output.rgb *= lerp((ambient + directionalLightColor * diffuse + directionalLightColor * specular) * max(oneMinusMetallic, _MinMetallicLightContribution), albedo.rgb, minProperty);
    output.rgb += (directionalLightColor * albedo.rgb * specular) + (directionalLightColor * specular * _Smoothness);
    output.rgb += ibl * oneMinusMetallic * _IblContribution;


    return output;
}

ここで重要な処理は次の部分です。

    //ノーマルマップのテクスチャをサンプリング
    half3 tangentNormal = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv));

    //ノーマルマップを反映したピクセルごとの法線を取得(従法線と法線の内積)
    worldNormal.x = dot(input.tangentX, tangentNormal);
    worldNormal.y = dot(input.tangentY, tangentNormal);
    worldNormal.z = dot(input.tangentZ, tangentNormal);
    //facing=面の向き 表が1裏が-1、worldNormalを正規化
    worldNormal = normalize(worldNormal) * (facing ? 1.0 : -1.0);

ここではtangentNormalがノーマルマップのサンプリングデータですが、成分ごとの従法線との内積を正規化して初めてピクセルごとの法線(WorldNormal(実際はWorldTangent))を計算しています。

 最後にライティングの際のNdL(法線とライトの向きの内積)に反映させることでノーマルマップを反映したピクセルごとの影色を取得しています。

  //ノーマルマップを反映してライティング
    half diffuse = max(0.0, dot(worldNormal, directionalLightDirection.xyz));

処理としては外の機能との兼ね合いが多いため冗長になっていますが、意外と理屈がわかっていれば簡単な内容でした。

本日は以上です。

〇コード全文

〇GraphicsToolsStandardProgram

#ifndef GRAPHICS_TOOLS_STANDARD_PROGRAM
#define GRAPHICS_TOOLS_STANDARD_PROGRAM

#pragma vertex VertexStage
#pragma fragment PixelStage

// Comment in to help with RenderDoc debugging.
//#pragma enable_d3d11_debug_symbols

/// <summary>
/// Features.
/// </summary>

#pragma shader_feature_local _DISABLE_ALBEDO_MAP
#pragma shader_feature_local _NORMAL_MAP
#pragma shader_feature_local _DIRECTIONAL_LIGHT
#pragma shader_feature_local_fragment _SPECULAR_HIGHLIGHTS

/// <summary>
///  Defines and includes.
/// </summary>

#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


#if !defined(_DISABLE_ALBEDO_MAP) || defined(_NORMAL_MAP) 
#define _UV
#else
#undef _UV
#endif

#if defined(_URP)
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#else
#include "UnityCG.cginc"
#include "UnityStandardConfig.cginc"
#include "UnityStandardUtils.cginc"
#endif 

#include "GraphicsToolsCommon.hlsl"
#include "GraphicsToolsStandardInput.hlsl"

/// <summary>
/// Vertex shader entry point.
/// </summary>
Varyings VertexStage(Attributes input)
{
    Varyings output = (Varyings)0;
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_TRANSFER_INSTANCE_ID(input, output);
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);

    float4 vertexPosition = input.vertex;

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

    half3 localNormal = input.normal;

#if defined(_NORMAL) 
#if defined(_URP)
    half3 worldNormal = TransformObjectToWorldNormal(localNormal);
#else
    half3 worldNormal = UnityObjectToWorldNormal(localNormal);
#endif
#endif

#if defined(_URP)
    output.position = TransformObjectToHClip(vertexPosition.xyz);
#else
    output.position = UnityObjectToClipPos(vertexPosition);
#endif

#if defined(_WORLD_POSITION)
    output.worldPosition.xyz = worldVertexPosition;
#endif

#if defined(_UV)
    output.uv = TRANSFORM_TEX(input.uv, _MainTex);
#endif

#if defined(_NORMAL)
#if defined(_NORMAL_MAP)
#if defined(_URP)
    half3 worldTangent = TransformObjectToWorldDir(input.tangent.xyz);
#else
    half3 worldTangent = UnityObjectToWorldDir(input.tangent.xyz);
#endif
    half tangentSign = input.tangent.w * unity_WorldTransformParams.w;
    half3 worldBitangent = cross(worldNormal, worldTangent) * tangentSign;
    output.tangentX = half3(worldTangent.x, worldBitangent.x, worldNormal.x);
    output.tangentY = half3(worldTangent.y, worldBitangent.y, worldNormal.y);
    output.tangentZ = half3(worldTangent.z, worldBitangent.z, worldNormal.z);
#else
    output.worldNormal = worldNormal;
#endif
#endif

    return output;
}

/// <summary>
/// Fragment (pixel) shader entry point.
/// </summary>
half4 PixelStage(Varyings input, bool facing : SV_IsFrontFace) : SV_Target
{
    UNITY_SETUP_INSTANCE_ID(input);
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    
#if defined(_URP)
    half4 albedo = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
#else
    half4 albedo = tex2D(_MainTex, input.uv);
#endif



    albedo *= _Color;


    // Normal calculation.
#if defined(_NORMAL)

    half3 worldNormal;

#if defined(_NORMAL_MAP)
#if defined(_URP)
    half3 tangentNormal = UnpackNormal(SAMPLE_TEXTURE2D(_NormalMap, sampler_NormalMap, input.uv));
#else
    half3 tangentNormal = UnpackNormal(tex2D(_NormalMap, input.uv));
#endif
    worldNormal.x = dot(input.tangentX, tangentNormal);
    worldNormal.y = dot(input.tangentY, tangentNormal);
    worldNormal.z = dot(input.tangentZ, tangentNormal);
    worldNormal = normalize(worldNormal) * (facing ? 1.0 : -1.0);

#else
    worldNormal = normalize(input.worldNormal) * (facing ? 1.0 : -1.0);
#endif
#endif


    half pointToLight = 1.0;
    half3 fluentLightColor = half3(0.0, 0.0, 0.0);


    // Blinn phong lighting.
#if defined(_DIRECTIONAL_LIGHT)
#if defined(_URP)
    Light directionalLight = GetMainLight();
    half3 directionalLightDirection = directionalLight.direction;
    half3 directionalLightColor = directionalLight.color;
#else
    half3 directionalLightDirection = _WorldSpaceLightPos0.xyz;
    half3 directionalLightColor = _LightColor0.rgb;
#endif
    half diffuse = max(0.0, dot(worldNormal, directionalLightDirection.xyz));

    half specular = 0.0;
#endif
    half3 ibl = unity_IndirectSpecColor.rgb;
    
    half4 output = albedo;
    half3 ambient = glstate_lightmodel_ambient.rgb + half3(0.25, 0.25, 0.25);
    half minProperty = min(_Smoothness, _Metallic);
#if defined(_DIRECTIONAL_LIGHT)
    half oneMinusMetallic = (1.0 - _Metallic);
    output.rgb = lerp(output.rgb, ibl, minProperty);
    output.rgb *= lerp((ambient + directionalLightColor * diffuse + directionalLightColor * specular) * max(oneMinusMetallic, _MinMetallicLightContribution), albedo.rgb, minProperty);
    output.rgb += (directionalLightColor * albedo.rgb * specular) + (directionalLightColor * specular * _Smoothness);
    output.rgb += ibl * oneMinusMetallic * _IblContribution;
#endif
    
    return output;
}

#endif // GRAPHICS_TOOLS_STANDARD_PROGRAM

〇GraphicsToolsStandardInput

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#ifndef GRAPHICS_TOOLS_STANDARD_INPUT
#define GRAPHICS_TOOLS_STANDARD_INPUT

/// <summary>
/// Vertex attributes passed into the vertex shader from the app.
/// </summary>
struct Attributes
{
    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;
    half3 normal : NORMAL;
#if defined(_NORMAL_MAP)
    half4 tangent : TANGENT;
#endif
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

/// <summary>
/// Vertex attributes interpolated across a triangle and sent from the vertex shader to the fragment shader.
/// </summary>
struct Varyings
{
    float4 position : SV_POSITION;
#if defined(_UV)
    float2 uv : TEXCOORD0;
#endif
#if defined(_WORLD_POSITION)
    float3 worldPosition : TEXCOORD2;
#endif
#if defined(_NORMAL)
#if defined(_NORMAL_MAP)
    half3 tangentX : COLOR3;
    half3 tangentY : COLOR4;
    half3 tangentZ : COLOR5;
#else
    half3 worldNormal : COLOR3;
#endif
#endif
    UNITY_VERTEX_INPUT_INSTANCE_ID
    UNITY_VERTEX_OUTPUT_STEREO
};

/// <summary>
/// Textures and samplers.
/// </summary>

#if defined(_URP)
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
#if defined(_NORMAL_MAP)
TEXTURE2D(_NormalMap);
SAMPLER(sampler_NormalMap);
#endif
#else
sampler2D _MainTex;
#if defined(_NORMAL_MAP)
sampler2D _NormalMap;
#endif
#endif

/// <summary>
/// Global properties.
/// </summary>

#if defined(_URP)
// Empty.
#else
half4 _LightColor0;
#endif

/// <summary>
/// Per material properties.
/// </summary>

CBUFFER_START(UnityPerMaterial)

    half4 _Color;
    half4 _MainTex_ST;

CBUFFER_END


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

#endif