UnityでAR FoundationのPeople OcclusionとVFX Graphを組み合わせる

以前の記事でAR FoundationのPeople OcclusionとVFX Graphを組み合わせて、顔の周りにパーティクルを出してみました。

[UnityでAR FoundationのFace TrackingとVFX Graphを組み合わせて顔の周りにパーティクルを出す \- Qiita](https://qiita.com/aa_debdeb/items/59e641f122fcf5fa84f8)

今回はVFX GraphをAR FoundationのPeople Occlusionを組み合わせて、以下のように人を検出したところにパーティクルを出してみます。

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">AR Foundation People Occlusion + VFX Graph <a href="https://twitter.com/hashtag/madewithunity?src=hash&amp;ref_src=twsrc%5Etfw">#madewithunity</a> <a href="https://t.co/nyjeOPa0ES">pic.twitter.com/nyjeOPa0ES</a></p>&mdash; aadebdeb / Atsushi Asakura (@aa_debdeb) <a href="https://twitter.com/aa_debdeb/status/1258703190112874497?ref_src=twsrc%5Etfw">May 8, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

使用したUnityやパッケージのバージョン、実行端末は以下のようになっています。

- Unity: 2019.3.11f Personal
- AR Foundation: preview.8 - 3.1.0
- ARKit XR Plugion: preview.8 - 3.1.0
- ARKit Face Tracking: preview.8 - 3.1.0
- Visual Effect Graph: 7.3.1
- デバイス: iPad Pro (11インチ) (第2世代)
- OS: iPadOS 13.4

ビルトインレンダリングパイプラインを使用して、リニア色空間に設定しています。デバイスの向きは[DeviceOrientation\.LandscapeLeft](https://docs.unity3d.com/ScriptReference/DeviceOrientation.LandscapeLeft.html)のときしか動作確認していません。

---

AR Foundationでは`AROcclusionManager`コンポーネントからPeople Occlusionに関する深度テクスチャとステンシルテクスチャを取得することができます。`AROcclusionManager`コンポーネントは`ARCamera`コンポーネントを持つオブジェクトに追加します。また、`ARCameraBackground`コンポーネントからカメラが取得した画像をテクスチャとして取得できます。

深度テクスチャが持つ値自体はカメラから見た深度ですが、その値とUV座標およびカメラが持つ情報からワールド座標系に変換できます。Compute Shaderによりこの計算を行い、深度テクスチャをワールド座標を格納するテクスチャに変換します。このテクスチャをステンシルテクスチャとカメラ画像のテクスチャとともにVFX Graphに渡します。VFX Graph側ではこれらのテクスチャをAttribute Mapとして使用します。Attribute Mapからサンプリングしたときには人を検出していない場所も選ばれますが、ステンシルテクスチャを利用して人が検出できていない場合はlifetimeを0にすることで表示されないようにしています。

以下、使用したC#スクリプトとCompute ShaderのコードおよびVFX Graphです。

```cs:PeopleOcclusionVFXManager.cs
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.VFX;

[RequireComponent(typeof(Camera))]
[RequireComponent(typeof(ARCameraBackground))]
[RequireComponent(typeof(AROcclusionManager))]
public class PeopleOcclusionVFXManager : MonoBehaviour
{
    struct ThreadSize
    {
        public int x;
        public int y;
        public int z;

        public ThreadSize(uint x, uint y, uint z)
        {
            this.x = (int)x;
            this.y = (int)y;
            this.z = (int)z;
        }
    }

    [SerializeField]
    VisualEffect m_VfxPrefab;
    [SerializeField]
    ComputeShader m_ComputeShader;

    ARCameraBackground m_CameraBackground;
    AROcclusionManager m_OcclusionManager;
    Camera m_Camera;
    RenderTexture m_CaptureTexture, m_PositionTexture;
    VisualEffect m_VfxInstance;
    int m_Kernel;
    ThreadSize m_ThreadSize;

    void Awake()
    {
        m_Camera = GetComponent<Camera>();
        m_CameraBackground = GetComponent<ARCameraBackground>();
        m_OcclusionManager = GetComponent<AROcclusionManager>();
    }

    void OnEnable()
    {
        m_VfxInstance = Instantiate(m_VfxPrefab, transform.parent);
        m_CaptureTexture = new RenderTexture(Screen.width, Screen.height, 16);
        m_CaptureTexture.Create();
        SetupComputeShader();
    }

    void OnDisable()
    {
        Destroy(m_VfxInstance);
        m_CaptureTexture.Release();
        m_PositionTexture.Release();
    }

    void Update()
    {
        Texture2D stencilTexture = m_OcclusionManager.humanStencilTexture;
        Texture2D depthTexture = m_OcclusionManager.humanDepthTexture;

        if (stencilTexture == null || depthTexture == null)
        {
            return;
        }

        Matrix4x4 invVPMatrix = (m_Camera.projectionMatrix * m_Camera.transform.worldToLocalMatrix).inverse;

        m_ComputeShader.SetTexture(m_Kernel, "DepthTexture", depthTexture);
        m_ComputeShader.SetMatrix("InvVPMatrix", invVPMatrix);
        m_ComputeShader.SetMatrix("ProjectionMatrix", m_Camera.projectionMatrix);
        m_ComputeShader.Dispatch(m_Kernel, Mathf.CeilToInt(m_PositionTexture.width / m_ThreadSize.x), Mathf.CeilToInt(m_PositionTexture.height / m_ThreadSize.y), 1);

        m_VfxInstance.SetTexture("Color Map", m_CaptureTexture);
        m_VfxInstance.SetTexture("Stencil Map", stencilTexture);
    }

    void SetupComputeShader()
    {
        m_PositionTexture = new RenderTexture(Screen.width, Screen.height, 0, RenderTextureFormat.ARGBFloat);
        m_PositionTexture.enableRandomWrite = true;
        m_PositionTexture.Create();

        m_Kernel = m_ComputeShader.FindKernel("GeneratePositionTexture");
        uint threadSizeX, threadSizeY, threadSizeZ;
        m_ComputeShader.GetKernelThreadGroupSizes(m_Kernel, out threadSizeX, out threadSizeY, out threadSizeZ);
        m_ThreadSize = new ThreadSize(threadSizeX, threadSizeY, threadSizeZ);

        m_ComputeShader.SetTexture(m_Kernel, "PositionTexture", m_PositionTexture);
        m_VfxInstance.SetTexture("Position Map", m_PositionTexture);
    }

    void LateUpdate()
    {
        if (m_CameraBackground.material != null)
        {
            Graphics.Blit(null, m_CaptureTexture, m_CameraBackground.material);
        }
    }
}
```

```c:GeneratePositionTexture.compute
#pragma kernel GeneratePositionTexture

Texture2D<float> DepthTexture;
RWTexture2D<float3> PositionTexture;
float4x4 InvVPMatrix;
float4x4 ProjectionMatrix;

// ref: https://answers.unity.com/questions/1461036/worldtoviewportpoint-and-viewporttoworldpointworld.html
float3 ViewportToWorldPoint(float3 position)
{
    float4 projW = mul(ProjectionMatrix, float4(0, 0, position.z, 1));
    float4 pos4 = float4(1.0 - 2.0 * position.x, 1.0 - 2.0 * position.y, projW.z / projW.w, 1);
    float4 res4 = mul(InvVPMatrix, pos4);
    return res4.xyz / res4.w;
}

[numthreads(32,32,1)]
void GeneratePositionTexture(uint3 id : SV_DispatchThreadID)
{
    float width, height;
    PositionTexture.GetDimensions(width, height);
    float2 uv = float2(float(id.x) / width, float(id.y) / height);
    DepthTexture.GetDimensions(width, height);
    float depth = DepthTexture[uint2(uv.x * width, (1.0 - uv.y) * height)] * 0.625;
    float3 viewportPos = float3(uv.x, uv.y, depth);
    PositionTexture[id.xy] = ViewportToWorldPoint(viewportPos);
}
```

<img width="1268" alt="スクリーンショット 2020-05-08 23.56.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/2fb9ebb7-134f-dfb6-3f3a-a01e4eef9ccb.png">

実装としては、深度からワールド座標への変換をCompute Shaderでしているだけで後はいろいろなところから取得したテクスチャを`VisualEffect`コンポーネントに渡しているだけです。

C#スクリプトで`ARCameraBackground`コンポーネントからカメラ画像を取得している箇所は以下の記事を参考にしています。
[Unity AR FoundationでPeople Occlusionをやってみる \- e\.blog](https://edom18.hateblo.jp/entry/2019/08/11/223803)

Compute ShaderでUV座標と深度値からワールド座標を求めていますが、ここでの処理は[Camera\.ViewportToWorldPoint](https://docs.unity3d.com/ScriptReference/Camera.ViewportToWorldPoint.html)と同じです。Compute Shaderで行う必要があるので、以下のフォーラム投稿を参考に実装しました。
[WorldToViewportPoint and ViewportToWorldPoint math \- Unity Answers](https://answers.unity.com/questions/1461036/worldtoviewportpoint-and-viewporttoworldpointworld.html)

以下の記事を参考にして深度値に`0.625`をかけて補正しています。
[AR FoundationでPeople Occlusionを実装する \- じゅころぐAR](https://www.jyuko49.com/entry/2019/10/15/094408)