UnityのAR FundationでPeopleOcclusionしてみた

ARkit3.0で追加されたPeopleOcclusionを試してみました。

PeopleOcclusionのサンプルを動かそうとしたときにつまづいたので、まとめておきます。
[こちら](https://forum.unity.com/threads/how-to-setup-people-occlusion.691789/)を参考にしたのですが、自分の環境ではうまくいかず、やってみました。

Shaderは素人なので、もし不備やもっとよいいいやり方などあれば、コメントいただけるとうれしいです。

#イメージ
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ARkit3.0のPeopleOcclusion試してみました。<br><br>これを使ってゲーム作ろ。 <a href="https://t.co/Fv8GE5RBa5">pic.twitter.com/Fv8GE5RBa5</a></p>&mdash; 二月のタンクトップ@ARゲーム開発中 (@Raliemon) <a href="https://twitter.com/Raliemon/status/1177166064624422914?ref_src=twsrc%5Etfw">September 26, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

#環境
Unity 2019.2.3.f1
Version 11.0 beta
iPhone Xs (Xrでも動作しました）
iOS 13.0
他環境で検証していないため、動かない場合があるかもしれないです。

#ARFundationの導入方法

Package Managerより下記2つをインストールします。

・AR Foundation
・ARKit XR Plugin
<img width="688" alt="スクリーンショット 2019-09-26 17.53.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254171/126410e3-9218-ce0b-a309-9a7b064eda7a.png">


#PeopleOcclusion実装方法

###1.Unity上でシーンを作成
MainCameraを削除します。

###2.GameObject>XRより下記2つを追加
・AR Session
・AR SessionOrigin(こちらのカメラオブジェクトは配置されています）
<img width="396" alt="スクリーンショット 2019-09-26 18.01.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254171/11a8403a-06a1-60b6-e489-ec27f788ce39.png">

###3.AR Sessionはこんな感じ
特に追加するものはありません。
<img width="376" alt="スクリーンショット 2019-09-26 18.04.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254171/db53f15e-4792-3018-14fe-6b9e0d6550aa.png">

###4.AR SessionOriginオブジェクトにAR Human Body Managerを追加
画像のように各項目を設定する。
※現状Human Segmentation Stencil ModeとHuman Segmentation Depth Modeはこの組み合わせじゃないと失敗しました。
![スクリーンショット 2019-09-26 18.24.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254171/fbc58bc4-042d-f4c6-319c-cbe0c07f6934.png)

###5.AR CameraにPeopleOcclusionPostEffectを追加
※PeopleOcclusionPostEffectスクリプトとPeopleOcclusionシェーダーは後述。
※textTextureのテクスチャーはお好きなのを用意してください。

画像のように各項目を設定する。
![スクリーンショット 2019-09-26 18.38.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254171/a1eeed04-2622-7cc8-36f9-fe619b4cc88b.png)

###6.ビルド
ビルドして完成です。
これでLandScape/Portrait両方対応できているかと思います。
![iOS の画像 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254171/5a630956-0011-2458-d904-c32e2e419fb2.png)

###7.スクリプトとシェーダーのサンプル
```C#:PeopleOcclusionPostEffect.cs

using UnityEngine;
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

[RequireComponent(typeof(ARCameraManager))]
public class PeopleOcclusionPostEffect : MonoBehaviour
{
    [SerializeField] private ARSessionOrigin m_arOrigin = null;
    [SerializeField] private ARHumanBodyManager m_humanBodyManager = null;
    [SerializeField] private ARCameraManager m_cameraManager = null;
    [SerializeField] private Shader m_peopleOcclusionShader = null;
    [SerializeField] Texture2D testTexture;

    private Texture2D m_cameraFeedTexture = null;
    private Material m_material = null;

	void Awake()
	{
        m_material = new Material(m_peopleOcclusionShader);
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth;
    }

    private void OnEnable()
    {
        m_cameraManager.frameReceived += OnCameraFrameReceived;
    }

    private void OnDisable()
    {
        m_cameraManager.frameReceived -= OnCameraFrameReceived;
    }
    void OnRenderImage(RenderTexture source, RenderTexture destination)
	{
        if(PeopleOcclusionSupported())
        {
            if(m_cameraFeedTexture != null)
            {
                m_material.SetFloat("_UVMultiplierLandScape", CalculateUVMultiplierLandScape(m_cameraFeedTexture));
                m_material.SetFloat("_UVMultiplierPortrait", CalculateUVMultiplierPortrait(m_cameraFeedTexture));
            }
            
            if(Input.deviceOrientation == DeviceOrientation.LandscapeRight) 
            {
                m_material.SetFloat("_UVFlip", 0);
                m_material.SetInt("_ONWIDE", 1);
            }
            else if(Input.deviceOrientation == DeviceOrientation.LandscapeLeft) 
            {
                m_material.SetFloat("_UVFlip", 1);
                m_material.SetInt("_ONWIDE", 1);
            }
            else
            {
                m_material.SetInt("_ONWIDE", 0);
            }

            
            m_material.SetTexture("_OcclusionDepth", m_humanBodyManager.humanDepthTexture);
            m_material.SetTexture("_OcclusionStencil", m_humanBodyManager.humanStencilTexture);

            // m_material.SetFloat("_ARWorldScale", 1f/m_arOrigin.transform.localScale.x);

            Graphics.Blit(source, destination, m_material);
        }
        else
        {
            Graphics.Blit(source, destination);
        }
    }

    private void OnCameraFrameReceived(ARCameraFrameEventArgs eventArgs)
    {
        if(PeopleOcclusionSupported())
        {
            RefreshCameraFeedTexture();
        }
    }

    private bool PeopleOcclusionSupported()
    {
        return m_humanBodyManager.subsystem != null && m_humanBodyManager.humanDepthTexture != null && m_humanBodyManager.humanStencilTexture != null;
    }

    private void RefreshCameraFeedTexture()
    {
        XRCameraImage cameraImage;
        m_cameraManager.TryGetLatestImage(out cameraImage);
        if (m_cameraFeedTexture == null || m_cameraFeedTexture.width != cameraImage.width || m_cameraFeedTexture.height != cameraImage.height)
        {
            m_cameraFeedTexture = new Texture2D(cameraImage.width, cameraImage.height, TextureFormat.RGBA32, false);
            // m_cameraFeedTexture = new Texture2D(Screen.width, Screen.height, TextureFormat.RGBA32, false);
        }

        CameraImageTransformation imageTransformation = Input.deviceOrientation == DeviceOrientation.LandscapeRight ? CameraImageTransformation.MirrorY : CameraImageTransformation.MirrorX;
        XRCameraImageConversionParams conversionParams = new XRCameraImageConversionParams(cameraImage, TextureFormat.RGBA32, imageTransformation);

        NativeArray<byte> rawTextureData = m_cameraFeedTexture.GetRawTextureData<byte>();

        try
        {
            unsafe
            {
                cameraImage.Convert(conversionParams, new IntPtr(rawTextureData.GetUnsafePtr()), rawTextureData.Length);
            }
        }
        finally
        {
            cameraImage.Dispose();
        }

        m_cameraFeedTexture.Apply();
        m_material.SetTexture("_CameraFeed", testTexture);

    }

    private float CalculateUVMultiplierLandScape(Texture2D cameraTexture)
    {
        float screenAspect = (float)Screen.width / (float)Screen.height;
        float cameraTextureAspect = (float)cameraTexture.width / (float)cameraTexture.height;
        return screenAspect / cameraTextureAspect;

    }
    private float CalculateUVMultiplierPortrait(Texture2D cameraTexture)
    {
        float screenAspect = (float)Screen.height / (float)Screen.width;
        float cameraTextureAspect = (float)cameraTexture.width / (float)cameraTexture.height;
        return screenAspect / cameraTextureAspect;

    }
}
```
```glsl:PeopleOcclusion

Shader "Perchang/PeopleOcclusion"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _CameraFeed ("Texture", 2D) = "white" {}
        _OcclusionDepth ("Texture", 2D) = "white" {}
        _OcclusionStencil ("Texture", 2D) = "white" {}
        _UVMultiplierLandScape ("UV MultiplerLandScape", Float) = 0.0
        _UVMultiplierPortrait ("UV MultiplerPortrait", Float) = 0.0
        _UVFlip ("Flip UV", Float) = 0.0
        _ONWIDE("Onwide", Int) = 0
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float2 uv1 : TEXCOORD1;
                float2 uv2 : TEXCOORD2;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            sampler2D_float _OcclusionDepth;
            sampler2D _OcclusionStencil;
            sampler2D _CameraFeed;
            sampler2D_float _CameraDepthTexture;
            float _UVMultiplierLandScape;
            float _UVMultiplierPortrait;
            float _UVFlip;
            int _ONWIDE;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                if(_ONWIDE == 1)
                {
                    o.uv1 = float2(v.uv.x, (1.0 - (_UVMultiplierLandScape * 0.5f)) + (v.uv.y / _UVMultiplierLandScape));
                    o.uv2 = float2(lerp(1.0 - o.uv1.x, o.uv1.x, _UVFlip), lerp(o.uv1.y, 1.0 - o.uv1.y, _UVFlip));
                }
                else
                {
                    o.uv1 = float2((1.0 - (_UVMultiplierPortrait * 0.5f)) + (v.uv.x / _UVMultiplierPortrait), v.uv.y);
                    o.uv2 = float2(lerp(1.0 - o.uv1.y, o.uv1.y, 0), lerp(o.uv1.x, 1.0 - o.uv1.x, 1));
                }
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                fixed4 cameraFeedCol = tex2D(_CameraFeed, i.uv1 * _Time);
                float sceneDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
                float4 stencilCol = tex2D(_OcclusionStencil, i.uv2);
                float occlusionDepth = tex2D(_OcclusionDepth, i.uv2) * 0.625; //0.625 hack occlusion depth based on real world observation
                            
                float showOccluder = step(occlusionDepth, sceneDepth) * stencilCol.r; // 1 if (depth >= ocluderDepth && stencil)

                return lerp(col, cameraFeedCol, showOccluder);
            }

            ENDCG
        }
    }
}
```