Hololens2のSpatial Meshにデカールを貼る

# はじめに

Hololens2では現実の空間内に様々なオブジェクトを表示できます。
それに加えて、表示させたオブジェクトと実在する器物との関係を視覚的に表現したかったので、
Spatial Meshにデカールを貼ってみることにしました。

# デカールとは

ここではSpatial Mesh本体とは別のオブジェクトを利用して
Spatial Meshの任意の部位にテクスチャが張り付けられた状態を表現する手法を指します。

# 動作結果

以下の動画では、標準のQuadを摘まんで動かすことでデカールの適用範囲を設定しています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Hololens2のSpatial MeshにDecalを貼れるようになったぞ <a href="https://twitter.com/hashtag/Hololens?src=hash&amp;ref_src=twsrc%5Etfw">#Hololens</a> <a href="https://twitter.com/hashtag/Hololens2?src=hash&amp;ref_src=twsrc%5Etfw">#Hololens2</a> <a href="https://twitter.com/hashtag/AR?src=hash&amp;ref_src=twsrc%5Etfw">#AR</a> <a href="https://twitter.com/hashtag/MRTK?src=hash&amp;ref_src=twsrc%5Etfw">#MRTK</a> <a href="https://twitter.com/hashtag/MixedReality?src=hash&amp;ref_src=twsrc%5Etfw">#MixedReality</a> <a href="https://twitter.com/hashtag/XR?src=hash&amp;ref_src=twsrc%5Etfw">#XR</a><br>(テストパターンは以前個人的な用事で作ったPDFアイコン) <a href="https://t.co/guuhBrPL7l">pic.twitter.com/guuhBrPL7l</a></p>&mdash; 北村　圭 (@KeiKitamura_JP) <a href="https://twitter.com/KeiKitamura_JP/status/1449359481410932741?ref_src=twsrc%5Etfw">October 16, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


デカールで使うDepthの比較を応用すると、
壁とオブジェクトが相互に影響されるような表現を作れたりします。

<iframe width="560" height="315" src="https://www.youtube.com/embed/9s9t6BA5q5g" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# 開発環境

Unity 2019.4.3f1
Universal Render Pipeline　7.3.1
Shader Graph 7.3.1
MRTK 2.7.2.0

# 実装
Spatial Meshとテクスチャ制御用のオブジェクトでそれぞれシェーダーを用意します。

## SpatialMesh側のシェーダー

Spatial Mesh側では、Spatial MeshのDepthをDepth Bufferに書き込みます。
オブジェクト側でのテクスチャサンプリングでSpatial Meshの位置・形状を取得できるよう準備します。
DepthのDepth Bufferへの書き込みを行うため、Spatial Mesh側のシェーダーのRender Queueは2450以下にして下さい。

    
    Shader "DecalTest/DepthOnly"
    {
        Properties
        {
            
        }
        SubShader
        {
            Tags { "RenderType"="Opaque" "Queue"="Geometry-1"}
            LOD 100
    
            Pass
            {
                Tags{"LightMode" = "DepthOnly"}
    
                ZWrite On
                ColorMask 0
    
                HLSLPROGRAM
                // Required to compile gles 2.0 with standard srp library
                #pragma prefer_hlslcc gles
                #pragma exclude_renderers d3d11_9x
                #pragma target 2.0
    
                #pragma vertex DepthOnlyVertex
                #pragma fragment DepthOnlyFragment
    
                // -------------------------------------
                // Material Keywords
                #pragma shader_feature _ALPHATEST_ON
    
                //--------------------------------------
                // GPU Instancing
                #pragma multi_compile_instancing
    
                #include "Packages/com.unity.render-pipelines.universal/Shaders/UnlitInput.hlsl"
                #include "Packages/com.unity.render-pipelines.universal/Shaders/DepthOnlyPass.hlsl"
                ENDHLSL
            }
        }
    }
    

## オブジェクト側のシェーダー

オブジェクト側では、まずDepth BufferからSpatial Meshの形状を読み取ります。
読み取ったSpatial Meshの形状を元に、その形状にぴったり合うようにテクスチャをサンプリングします。
オブジェクト側ではDepthは読み込むだけで、このオブジェクト自体のDepthは書き込まない為、
オブジェクト側のシェーダーのRender Queueは3000以上にして下さい。

### Shader Graphの場合

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2365936/fb16edcc-49f9-196f-917f-d2d82113410a.png)

１．Scene DepthからSpatial Meshのワールドスペースの位置を算出します。
![Untitled 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2365936/eeeeec72-5e7c-2d1e-b288-03c502d52ace.png)

２．ワールドスペースの位置をオブジェクトスペースに変換します。
オブジェクトスペースのX, Y座標をUVとして使用しテクスチャをサンプリングします。
使用するオブジェクトのUVレイアウトによっては適宜値を調整して下さい。
今回はQuadにテクスチャを割り当てる際に各座標に0.5を足しています。

![Untitled 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2365936/9dae4514-04f1-41a8-0415-0e7204ea9875.png)


### 通常のシェーダーの場合

    Shader "DecalTest/DecalTest"
    {
        Properties
        {
            _MainTex("Texture", 2D) = "white" {}
        }
        SubShader
        {
            Tags { "RenderType" = "Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True" "renderPipeline" = "UniversalPipeline" }
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Back
            LOD 100
    
            Pass
            {
                Name "Unlit"
    
                HLSLPROGRAM
                #pragma prefer_hlslcc gles
                #pragma exclude_renderers d3d11_9x
    
                #pragma vertex vert
                #pragma fragment frag
                #pragma shader_feature _ALPHATEST_ON
                #pragma shader_feature _ALPHAPREMULTIPLY_ON
    
                #pragma multi_compile_instancing
    
                #pragma target 5.0
    
    						// _CameraDepthTextureの定義など
                #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl" 
    						// VertexPositionInputなど
                #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" 
    						// LinearEyeDepth()など
                #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" 
    
                struct Attributes
                {
                    float4 positionOS : POSITION;
                    float2 uv : TEXCOORD0;
                    UNITY_VERTEX_INPUT_INSTANCE_ID
                };
    
                struct Varyings
                {
                    float2 uv : TEXCOORD0;
                    float4 vertex : SV_POSITION;
                    float3 positionWS : TEXCOORD1;
                    float4 projectedPosition : TEXCOORD2; // スクリーンスペース座標系の位置として使用
                    float4 viewDirectionOS : TEXCOORD3;
                    float3 cameraPositionOS : TEXCOORD4;
                    UNITY_VERTEX_INPUT_INSTANCE_ID
                    UNITY_VERTEX_OUTPUT_STEREO
                };
    
                TEXTURE2D(_MainTex);
                SAMPLER(sampler_MainTex);
    
                // Inspectorに出すプロパティはCBUFFERに含める
                CBUFFER_START(UnityPerMaterial)
                float4 _MainTex_ST;
                CBUFFER_END
    
                Varyings vert(Attributes input)
                {
                    Varyings output = (Varyings)0;
    
                    UNITY_SETUP_INSTANCE_ID(input);
                    UNITY_TRANSFER_INSTANCE_ID(input, output);
                    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output);
    
                    VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
    
                    output.vertex = vertexInput.positionCS;
                    output.uv = TRANSFORM_TEX(input.uv, _MainTex);
                    output.positionWS = vertexInput.positionWS;
    
                    // スクリーンスペース座標系の位置を取得
                    output.projectedPosition = vertexInput.positionNDC;
    
                    // オブジェクトスペースで「頂点からカメラへ向かう」ベクトルを取得
                    float3 viewDir = vertexInput.positionVS;
                    viewDir *= -1;
    
                    float4x4 viewToObjectMatrix = mul(UNITY_MATRIX_I_M, UNITY_MATRIX_I_V);
                    output.viewDirectionOS.xyz = mul((float3x3)viewToObjectMatrix, viewDir);
                    output.viewDirectionOS.w = vertexInput.positionVS.z;
    
                    // カメラ位置をビュー座標系からオブジェクトスペースに変換
                    output.cameraPositionOS = mul(viewToObjectMatrix, float4(0, 0, 0, 1)).xyz;
    
                    return output;
                }
    
                half4 frag(Varyings input) : SV_Target
                {
                    UNITY_SETUP_INSTANCE_ID(input);
                    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    
                    half2 uv = input.uv;
    
                    // Depthをサンプリング
                    // オブジェクトの奥に他のメッシュが無ければ何も描画しない
                    float sceneDepth01 = Linear01Depth(SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, UnityStereoTransformScreenSpaceTex(input.projectedPosition.xy / input.projectedPosition.w)).r, _ZBufferParams);
                    clip(step(0.01, 1 - sceneDepth01) - 0.01);
    
                    // 奥のメッシュのオブジェクトスペースの位置を取得するのにLinearEyeDepth()を使用する
                    float sceneDepth = LinearEyeDepth(SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, UnityStereoTransformScreenSpaceTex(input.projectedPosition.xy / input.projectedPosition.w)).r, _ZBufferParams);
    
                    // input.viewDirectionOS.zをwで割ることでオブジェクトスペースの奥行きを求める
                    input.viewDirectionOS.xyz /= input.viewDirectionOS.w;
                    float3 objectSpacePosBehind = input.cameraPositionOS + input.viewDirectionOS.xyz * sceneDepth;
    
                    /**********************************************************************************************/
                
                
                
                    // Quad使用を前提にUVを調整
                    float2 decalUv = objectSpacePosBehind.xy + 0.5;
    
                    // タイリングとオフセットを適用
                    decalUv = decalUv * _MainTex_ST.xy + _MainTex_ST.zw;
    
                    // テクスチャをサンプリング
                    float4 texColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, decalUv);
    
                    return texColor;
                }
            ENDHLSL
            }
        }
        FallBack "Hidden/InternalErrorShader"
    }
    

シェーダーが準備出来たらQuad等のオブジェクトに適用し、テクスチャを割り当てます。

## MRTKの設定

MixedRealityToolkit > Spatial Awareness > Spatial Awareness System Settings に設定されているSpatial Mesh Observer内で
Display Settings > Display OptionをVisibleに設定します。
Display Settings > Visible Materialに先程のSpatial Mesh用シェーダーを割り当てたマテリアルを適用します。

![Untitled 3_edited.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2365936/00221496-3e91-8c13-fe19-77c675e863c8.png)


エディタ上で確認する際はSpatial Object Mesh Observerでも同様の設定をして下さい。

### その他

デカール用オブジェクトに割り当てるテクスチャのImport SettingsでWrap ModeをRepeatにしておくと
Spatial Meshにテクスチャがどのように貼られているか確認しやすくて良かったです。



これらの設定をした上でSpatial Meshの上にオブジェクト側用シェーダーを当てたオブジェクトをかざすと
冒頭の動画のような結果になります。

# 参考

[https://samdriver.xyz/article/decal-render-intro](https://samdriver.xyz/article/decal-render-intro)
[https://github.com/ColinLeung-NiloCat/UnityURPUnlitScreenSpaceDecalShader](https://github.com/ColinLeung-NiloCat/UnityURPUnlitScreenSpaceDecalShader)