【Unity】SceneSemanticAPIを使ってオクルージョン処理を実現する

## Scene Semantic APIとは？
ARCoreの拡張機能の1つです。カメラ画像からセグメンテーションを行い、pixelがどのセグメントに割り当てられているかを計算し、Textureとして取得可能です。

![SceneSemanticEx.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/684481a6-daa3-5200-0fcd-a2147a80928a.gif)

引用元：[Understand the user's environment with the Scene Semantics API](https://developers.google.com/ar/develop/scene-semantics)

## デモ
先にデモをお見せします。空の領域にのみオブジェクトが描画されるデモです。オブジェクトが建物に遮蔽されているように見えるので、疑似的なオクルージョン表現が可能になります。

![SceneSemanticDemo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/8d5d77de-fabb-0c07-12d9-954bf4226321.gif)

## バージョン情報

| ツール、ライブラリ | バージョン |
|:-:|:-:|
|  Unity |  2022.3.24f1 |
|  [ARCore Extensions](https://github.com/google-ar/arcore-unity-extensions) | 1.46.0  |

## オクルージョンのロジック
セグメントした任意の領域にのみ描画することで、オクルージョンされているような見え方を実現します。今回は以下フローで実装しました。

:::note

1. MainCameraのnear clipギリギリの位置にQuadを置く

2. QuadをMainCameraの描画範囲にぴったり重ねる

3. SceneSemanticAPIで取得したTexutureを加工する

4. Shader側にて、3で加工したTextureを受け取り、オクルージョン処理を行う

:::

---

### 1. MainCameraのnear clipギリギリの位置にQuadを置く
この工程では遮蔽物としてQuadを配置しています。以後の過程で、このQuadを用いて任意のセグメント以外の描画を遮断するイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/ef2891dd-e4bd-172a-c5df-f62443c6e497.png)

near clipが0.1なのに対して、Quadの位置を0.1005にしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/79207e5b-c970-c010-3cba-305d9f27c7c9.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/b8a26fbf-25b2-456b-35fc-d3e9ef6fd100.png)

---


### 2. QuadをMainCameraの描画範囲にぴったり重ねる
Quadが正しいピクセルを遮断できるように、カメラの描画範囲とQuadがぴったりと重なるように計算します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/8e8f44e2-2f5e-361a-3b54-cd14a4b06c76.png)

### 3. SceneSemanticAPIで取得したTexutureを加工する

セグメント結果を利用しやすいように加工します。Shader側でR成分の値を元にオクルージョンの適用範囲を設定するので、任意のセグメントを赤色に塗りつぶしたTextureを動的に生成します。以下は空(sky)のセグメントを赤色に塗りつぶした想定のシミュレーションです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/23a08249-f47c-59c1-4671-d022cf077b6e.png)

---

### 4. Shader側にて、3で加工したTextureを受け取り、オクルージョン処理を行う
任意のセグメントのみ赤色に塗りつぶされたTextureが渡ってくるので、R成分を調べて塗り分けます。以下画像のように、赤色だった部分にはオブジェクトが描画され、それ以外の部分にはオブジェクトが描画されない(skyboxのみ映る)、という状態を作り出せました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/e7fed262-1c19-fd1c-de63-493521cd8286.png)

上記はUnityEditor上でのシミュレーションですが、実機の場合、オブジェクトが描画されない部分にはカメラ映像が映し出されることになります。

## サンプルコード

以下サンプルコード全文です。下準備としてARCoreを利用可能なプロジェクトを作って色々と下準備が必要ですが、そこは省略します。

```CS
using System.Collections;
using Google.XR.ARCoreExtensions;
using UnityEngine;

public class CustomSceneSemanticManager : MonoBehaviour
{
    [SerializeField] private ARSemanticManager _semanticManager;
    [SerializeField] private GameObject _semanticQuad;
    [SerializeField] private Sprite _debugImage;
    [SerializeField] private Material _semanticMaterial;
    [SerializeField] private float _checkForSupportFrequency = 0.1f;
    [SerializeField] private int _maxAttemptsCheckForSupport = 5;
    [SerializeField] private SemanticLabel[] _labels;

    private Texture2D _semanticTexture;
    private Texture2D _outputTexture;

    private bool _isSemanticModeSupported;
    private bool _isQuadSizeInitialized;
    private static readonly int TextureId = Shader.PropertyToID("_SemanticTex");

    private void Start()
    {
        if (Application.isEditor)
        {
            ResizeQuadToScreen(_debugImage.texture.width, _debugImage.texture.height);
            _semanticMaterial.SetTexture(TextureId, _debugImage.texture);
            return;
        }

        // Semantic APIがサポートされているかチェック開始。
        StartCoroutine(CheckForSemanticFeatureSupport());
    }

    private void Update()
    {
        if (!_isSemanticModeSupported) return;
        _semanticMaterial.SetTexture(TextureId, RequestSemanticTexture(ref _outputTexture) ? _outputTexture : null);
        if (!_semanticMaterial.GetTexture(TextureId)) return;

        // 画面サイズにぴったり合うようにQuadのサイズを調整する。
        // 何度も行う必要はないので、SemanticTextureが取得できたら1回だけ行う。
        if (_isQuadSizeInitialized) return;
        _isQuadSizeInitialized = true;
        ResizeQuadToScreen(_semanticTexture.width, _semanticTexture.height);
    }

    /// <summary>
    /// Semantic APIがサポートされているか確認する。
    /// </summary>
    private IEnumerator CheckForSemanticFeatureSupport()
    {
        var checkForSupportAttempts = 0;

        while (true)
        {
            if (_semanticManager.IsSemanticModeSupported(SemanticMode.Enabled) == FeatureSupported.Supported)
            {
                _isSemanticModeSupported = true;
                break;
            }
           
            checkForSupportAttempts++;

            if (checkForSupportAttempts >= _maxAttemptsCheckForSupport) break;

            yield return new WaitForSeconds(_checkForSupportFrequency);
        }
    }


    /// <summary>
    /// 画面サイズにぴったり合うようにQuadのサイズを調整する。
    /// Note：ScreenSpaceのCanvasを使えばぴったり合せられるのでは？と思ってやってみたが、なぜかずれるのでこのやり方を採用している。
    /// </summary>
    private void ResizeQuadToScreen(int imgWidth, int imgHeight)
    {
        var imageAspectRatio = imgHeight / (float)imgWidth;
        var cameraFovRad = Camera.main.fieldOfView * Mathf.Deg2Rad;
        var quadHeightAtDistance = 2.0f * _semanticQuad.transform.localPosition.z * Mathf.Tan(cameraFovRad / 2.0f);
        var quadWidthAtDistance = quadHeightAtDistance * imageAspectRatio;
        _semanticQuad.transform.localScale = new Vector3(quadWidthAtDistance, quadHeightAtDistance, 1);
    }

    private bool RequestSemanticTexture(ref Texture2D result)
    {
        if (!_semanticManager.TryGetSemanticTexture(ref _semanticTexture))
        {
            return false;
        }

        ConvertR8ToRGBA32Flipped(_semanticTexture,ref result);
        return true;
    }

    private void ConvertR8ToRGBA32Flipped(Texture2D semanticInputTexture, ref Texture2D outputTexture)
    {
        var width = semanticInputTexture.width;
        var height = semanticInputTexture.height;

        outputTexture = new Texture2D(height, width, TextureFormat.RGBA32, false);

        var rawSemanticTextureData = semanticInputTexture.GetRawTextureData<byte>();
        var pixels = new Color[rawSemanticTextureData.Length];

        for (var i = 0; i < height; i++)
        {
            for (var j = 0; j < width; j++)
            {
                var label = (SemanticLabel)rawSemanticTextureData[i * width + j];
                var color = GetColor(label);
                var index = outputTexture.width * outputTexture.height - (j * outputTexture.width + i + 1);
                pixels[index] = color;
            }
        }

        outputTexture.SetPixels(pixels);
        outputTexture.Apply();
    }

    private Color GetColor(SemanticLabel semanticLabel)
    {
        var notLabeledColor = new Color(0, 0, 0, 0);
        foreach (var label in _labels)
        {
            return label == semanticLabel ? Color.red : notLabeledColor;
        }

        return notLabeledColor;
    }
}
```

以下は、_SemanticTexを受け取って利用するShaderです。

```glsl

Shader "Custom/SceneSemantic"
{
    Properties
    {
        _SemanticTex("Semantic Texture", 2D) = "white" {}
    }
    SubShader
    {

        Pass
        {
            Tags
            {
                "Queue"="Geometry"
            }
            Blend One OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _SemanticTex;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                float4 semanticCol = tex2D(_SemanticTex, i.uv);
                float interpolation = saturate(semanticCol.r);
                if (interpolation > 0.5)
                {
                    discard;
                }
                return 0;
            }
            ENDCG
        }
    }
}
```

## 課題
横持ちにするとおかしくなります。issueも立てましたが、特に対応の見込みは無いとのことです。
![SceneSemanticBug.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244071/c093b32f-5baa-05a4-ed18-9c19d5f6a5c9.gif)


https://github.com/google-ar/arcore-unity-extensions/issues/204

対策書きました。→ [【Unity】モバイル端末の横持検知をジャイロで行う](https://qiita.com/OKsaiyowa/items/dc7c73de11617bee950d)

## 参考リンク
- [GeospatialCreatorDemos](https://github.com/dilmerv/GeospatialCreatorDemos)
- [demo-megagolf](https://github.com/google-ar/demo-megagolf)
- [ARCore-Scene-Semantics-Sample](https://github.com/TakashiYoshinaga/ARCore-Scene-Semantics-Sample)