夜風のMixedReality

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

MRTK StanderdShaderを読み解く その11 TriplanarMapping 前編

本日はMRTKのStandardShaderの調査枠です。

MRTKStandardShaderではUVに関してTriplanarMappingと呼ばれる仕組みがあります。

本日はTriplanarMappingに関して調査していきます。

〇TriplanarMappingとは?

Triplanar MappingはUnityの場合x,y,zの3軸からそれぞれ画像を投影する手法になります。

Tri-Planar(3つの平面)の名のごとく、2Dのテクスチャを3Dモデルの持つUV(モデルの持つメッシュに対して画像のどの部分が割り当てられるか?)の情報を無視して貼り付けることができます。

MRTKに同梱されている[MarsCuriosityRover]プレファブの3Dモデルを例にとってみていきます。(形状が複雑なモデルだったのでUVの説明で用います。)

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

個のプレファブはいくつかのメッシュのオブジェクトがパーツとして組み合わさり構成されています。このうちの[node_id39]というオブジェクトをピックアップしてみると以下のようにエンジン状のパーツをみれます。

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

これはした画像の赤丸で囲った部分のメッシュにテクスチャがアタッチしているMaterialで実現をしています。

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

f:id:Holomoto-Sumire:20200802112403p:plain

メッシュにUV情報が設定されており、その情報に合わせてテクスチャが用意されていることでメッシュの形状と見た目の画像がずれなく一体化します。

しかしUV情報はモデリング時にあらかじめ設定されており、通常Unityで3Dモデル自体のUV情報を変えることはできません。

そのため、モデリング時に作成したUV情報によっては思ったように画像がマッチしない場合が見られます。

[MarsCuriosityRover]のモデルはプロフェッショナルの作品なのでUVとテクスチャが最初から設定されたものを利用できますが、このように複雑なモデルや、逆にシンプルでも地面などの隆起があったり、非常に大きなメッシュのテクスチャなどはこのテクスチャとのマッチしない例が多く発生しやすいです。

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

 上画像はTriplanarMappingに関して詳しい情報を載せている下記参考記事からの引用ですが、このようにUVによってはある一点(画像の場合は上)から見てきれいにテクスチャが貼られていても、違う角度から見た場合テクスチャのゆがみが発生することは多いです。

gamedevelopment.tutsplus.com

 そこでテクスチャのずれを発生させないマッピング法として3つの軸からテクスチャを投影する手法が編み出されました。

f:id:Holomoto-Sumire:20200802114144j:plain
参考サイトから引用

〇MRTKStandardShaderの例

MRTKStandardShaderでは[TriplanarMapping]のチェックボックスにチェックを入れ有効化することでTriplanarMappingが使用することができます。

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

MRTKStandardShaderの場合2種類のTriplanarMappingを使用することができます。 一つはチェックボックスを入れた状態のUnityワールド座標でのTriplanarMapping

これはGIF画像のようにオブジェクトを動かすとそれに合わせてマッピングが行われます。

f:id:Holomoto-Sumire:20200802115008g:plain

 Dynamicなオブジェクトに対してどれだけオブジェクトが動いたりメッシュが変形してもテクスチャのゆがみが発生しない点で非常に有用に使用できます。

 本ブログでは以前UIを作る際に次のような変形するメッシュに対して画像がゆがみなく貼られることを期待して使用しています。

f:id:Holomoto-Sumire:20200202114352g:plain

redhologerbera.hatenablog.com

redhologerbera.hatenablog.com

 もう一つがオブジェクトの持つLocalSpaceでのマッピングです。

 こちらはオブジェクトを動かした場合通常のマッピング同様オブジェクトに合わせてテクスチャが固定されます。 これは[Local Space]のチェックボックスを有効化することで使用できます。

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

f:id:Holomoto-Sumire:20200802115510g:plain

単にUVが設定されていないオブジェクトにはこちらを使用することである程度テクスチャを貼ることができます。

〇今回作成したTriplanarMappingのShader

 TriplanarMappingの理解だけで長くなってしまったのでShaderで具体的にどのように実現しているかは後日後編で見ていきます。

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

Shader "Custom/MRTKStandardTriplanarMapping"
{
    Properties
    {
        // Main maps.
        _Color("Color", Color) = (1.0, 1.0, 1.0, 1.0)
        _MainTex("Albedo", 2D) = "white" {}
        [Enum(AlbedoAlphaMode)] _AlbedoAlphaMode("Albedo Alpha Mode", Float) = 0 // "Transparency"
        [Toggle] _AlbedoAssignedAtRuntime("Albedo Assigned at Runtime", Float) = 0.0
        _Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
        _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

        [Toggle(_TRIPLANAR_MAPPING)] _EnableTriplanarMapping("Triplanar Mapping", Float) = 0.0
        [Toggle(_LOCAL_SPACE_TRIPLANAR_MAPPING)] _EnableLocalSpaceTriplanarMapping("Local Space", Float) = 0.0
        _TriplanarMappingBlendSharpness("Blend Sharpness", Range(1.0, 16.0)) = 4.0


 
    }

    SubShader
    {
        Pass
        {
            Name "Main"


            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag



        
            #pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON
            #pragma shader_feature _DISABLE_ALBEDO_MAP
            #pragma shader_feature _ _METALLIC_TEXTURE_ALBEDO_CHANNEL_A _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
            #pragma shader_feature _NORMAL_MAP
            #pragma shader_feature _TRIPLANAR_MAPPING
            #pragma shader_feature _LOCAL_SPACE_TRIPLANAR_MAPPING
            #pragma shader_feature _DIRECTIONAL_LIGHT



            
            #include "UnityCG.cginc"
            #include "UnityStandardConfig.cginc"
            #include "UnityStandardUtils.cginc"
            #include "MixedRealityShaderUtils.cginc"

            // This define will get commented in by the UpgradeShaderForLightweightRenderPipeline method.
            //#define _LIGHTWEIGHT_RENDER_PIPELINE

#if defined(_TRIPLANAR_MAPPING) || 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(_TRIPLANAR_MAPPING) || defined(_NORMAL_MAP) 
            #define _UV
#else
            #undef _UV
#endif

            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(_TRIPLANAR_MAPPING)
                fixed3 worldNormal : COLOR3;
                fixed3 triplanarNormal : COLOR4;
                float3 triplanarPosition : TEXCOORD6;
#elif 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(_TRIPLANAR_MAPPING)
            float _TriplanarMappingBlendSharpness;
#endif

#if defined(_DIRECTIONAL_LIGHT)

            fixed4 _LightColor0;
#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)));
#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(_TRIPLANAR_MAPPING)
                o.worldNormal = worldNormal;
#if defined(_LOCAL_SPACE_TRIPLANAR_MAPPING)
                o.triplanarNormal = localNormal;
                o.triplanarPosition = vertexPosition;
#else
                o.triplanarNormal = worldNormal;
                o.triplanarPosition = o.worldPosition;
#endif
#elif 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
            {
#if defined(_TRIPLANAR_MAPPING)
                // Calculate triplanar uvs and apply texture scale and offset values like TRANSFORM_TEX.
                fixed3 triplanarBlend = pow(abs(i.triplanarNormal), _TriplanarMappingBlendSharpness);
                triplanarBlend /= dot(triplanarBlend, fixed3(1.0, 1.0, 1.0));
                float2 uvX = i.triplanarPosition.zy * _MainTex_ST.xy + _MainTex_ST.zw;
                float2 uvY = i.triplanarPosition.xz * _MainTex_ST.xy + _MainTex_ST.zw;
                float2 uvZ = i.triplanarPosition.xy * _MainTex_ST.xy + _MainTex_ST.zw;

                // Ternary operator is 2 instructions faster than sign() when we don't care about zero returning a zero sign.
                float3 axisSign = i.triplanarNormal < 0 ? -1 : 1;
                uvX.x *= axisSign.x;
                uvY.x *= axisSign.y;
                uvZ.x *= -axisSign.z;
#endif

            // Texturing.

#if defined(_TRIPLANAR_MAPPING)
                fixed4 albedo = tex2D(_MainTex, uvX) * triplanarBlend.x + 
                                tex2D(_MainTex, uvY) * triplanarBlend.y + 
                                tex2D(_MainTex, uvZ) * triplanarBlend.z;
#else
                fixed4 albedo = tex2D(_MainTex, i.uv);
#endif

                albedo *= _Color;

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

#if defined(_NORMAL_MAP)
#if defined(_TRIPLANAR_MAPPING)
                fixed3 tangentNormalX = UnpackScaleNormal(tex2D(_NormalMap, uvX), _NormalMapScale);
                fixed3 tangentNormalY = UnpackScaleNormal(tex2D(_NormalMap, uvY), _NormalMapScale);
                fixed3 tangentNormalZ = UnpackScaleNormal(tex2D(_NormalMap, uvZ), _NormalMapScale);
                tangentNormalX.x *= axisSign.x;
                tangentNormalY.x *= axisSign.y;
                tangentNormalZ.x *= -axisSign.z;

                // Swizzle world normals to match tangent space and apply Whiteout normal blend.
                tangentNormalX = fixed3(tangentNormalX.xy + i.worldNormal.zy, tangentNormalX.z * i.worldNormal.x);
                tangentNormalY = fixed3(tangentNormalY.xy + i.worldNormal.xz, tangentNormalY.z * i.worldNormal.y);
                tangentNormalZ = fixed3(tangentNormalZ.xy + i.worldNormal.xy, tangentNormalZ.z * i.worldNormal.z);

                // Swizzle tangent normals to match world normal and blend together.
                worldNormal = normalize(tangentNormalX.zyx * triplanarBlend.x +
                                        tangentNormalY.xzy * triplanarBlend.y +
                                        tangentNormalZ.xyz * triplanarBlend.z);
#else
                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
#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;

                // Fresnel lighting.
#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);

                fixed3 directionalLightColor = _LightColor0.rgb;
                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"
}