MRが楽しい

MRやVRについて学習したことを書き残す

Quest3でオクルージョンの種類をアプリ内で切り替える

本日はQuest3の小ネタ枠です。
Quest3でオクルージョンの種類をアプリ内で切り替える実装を試したので記事にします。

Quest3でDepthAPIを使ったオクルージョンを実装する

Quest3でDepthAPIを使ったオクルージョンを実装する手順は過去の記事を参照ください。
今回は以下の2つの記事で実装したオクルージョンを利用します。
bluebirdofoz.hatenablog.com
bluebirdofoz.hatenablog.com

既存のオクルージョンと自作のオクルージョンを切り替える

既存のオクルージョンと自作のオクルージョンを切り替える処理を実装していきます。

シェーダの書き換え

既存のオクルージョンと境界を強調する自作のオクルージョンを切り替えるため、新たなキーワードを追加したシェーダを作成しました。
COLLISION_OCCLUSION のキーワードが有効になると強調表示の処理に分岐します。
・EnvironmentOcclusionCustom.cginc

#ifndef META_DEPTH_ENVIRONMENT_OCCLUSION_CUSTOM_INCLUDED
#define META_DEPTH_ENVIRONMENT_OCCLUSION_CUSTOM_INCLUDED

#include "Packages/com.meta.xr.depthapi/Runtime/Core/Shaders/EnvironmentOcclusion.cginc"

float CalculateDistanceFromEnvDepthSpace(float3 worldCoords, float bias)
{
    const float4 depthSpace =
      mul(_EnvironmentDepthReprojectionMatrices[unity_StereoEyeIndex], float4(worldCoords, 1.0));

    const float2 uvCoords = (depthSpace.xy / depthSpace.w + 1.0f) * 0.5f;

    float linearSceneDepth = (1.0f / ((depthSpace.z / depthSpace.w) + _EnvironmentDepthZBufferParams.y)) * _EnvironmentDepthZBufferParams.x;
    linearSceneDepth -= bias * linearSceneDepth * UNITY_NEAR_CLIP_VALUE;

    return SampleEnvironmentDepthLinear_Internal(uvCoords) - linearSceneDepth;
}

fixed4 CalculateOutputColor(float3 worldCoords, fixed4 inputColor, float occlusionValue, float collisionThreshold)
{
    if (occlusionValue < 0.01) {
        discard; // 完全な隠蔽部分は描画しない
    }
#if defined(COLLISION_OCCLUSION)
    const float distanceFromEnvDepthSpace = CalculateDistanceFromEnvDepthSpace(worldCoords, 0.0);
    if (distanceFromEnvDepthSpace > collisionThreshold) {
        // 接触範囲外なら通常のオクルージョン処理を行う
        return inputColor *= occlusionValue;
    }
    const float inversion = 1.0 - occlusionValue; // 境界部分の強調度合い(隠蔽部分に近いほど強調表示する)
    float emphasis = inputColor.r + ((1.0 - inputColor.r) * inversion); // 赤色の元値に強調度合いを足し合わせる
    return fixed4(emphasis, inputColor.g * occlusionValue, inputColor.b * occlusionValue, inputColor.a); // 計算後の色を返す
#else
    return inputColor *= occlusionValue;
#endif
}

#if defined(HARD_OCCLUSION) || defined(SOFT_OCCLUSION)

#define META_DEPTH_OCCLUDE_OUTPUT_PREMULTIPLY_WORLDPOS_INVERSION(posWorld, output, zBias, collisionThreshold) \
    float occlusionValue = META_DEPTH_GET_OCCLUSION_VALUE_WORLDPOS(posWorld, zBias); \
    output = CalculateOutputColor(posWorld.xyz, output, occlusionValue, collisionThreshold); \

#define META_DEPTH_OCCLUDE_OUTPUT_PREMULTIPLY_INVERSION(input, output, zBias, collisionThreshold) \
  META_DEPTH_OCCLUDE_OUTPUT_PREMULTIPLY_WORLDPOS_INVERSION(input.posWorld, output, zBias, collisionThreshold)

#else

#define META_DEPTH_OCCLUDE_OUTPUT_PREMULTIPLY_WORLDPOS_INVERSION(posWorld, output, zBias, collisionThreshold)
#define META_DEPTH_OCCLUDE_OUTPUT_PREMULTIPLY_INVERSION(input, output, zBias, collisionThreshold) output = output

#endif

#endif

MRTKのスタンダードシェーダについても合わせて修正を行います。
COLLISION_OCCLUSION の multi_compile 定義とシェーダへの参照を追加します。

// DepthAPI Environment Occlusion
#pragma multi_compile _ HARD_OCCLUSION SOFT_OCCLUSION
#pragma multi_compile _ COLLISION_OCCLUSION

// シェーダへの参照を追加する
#include "Assets/OVRSettings/Shader/EnvironmentOcclusionCustom.cginc"

またフラグメント関数の META_DEPTH_OCCLUDE_OUTPUT_PREMULTIPLY の呼び出し関数についても上記の新シェーダの関数に差し替えます。

// META_DEPTH_OCCLUDE_OUTPUT_PREMULTIPLYを使用する箇所を変更する
//META_DEPTH_OCCLUDE_OUTPUT_PREMULTIPLY(i, output, 0.0);
META_DEPTH_OCCLUDE_OUTPUT_PREMULTIPLY_INVERSION(i, output, 0.0, 0.005);

切り替えスクリプト用の作成

シェーダのキーワード定義を変更してオクルージョン切り替えを行うスクリプトも準備します。
パッケージに含まれる EnvironmentDepthOcclusionController.cs スクリプトは既存のオクルージョンのみ切り替えるので、これを基にした新しいスクリプトを作成しました。
・EnvironmentDepthOcclusionControllerCustom.cs

using Unity.XR.Oculus;
using Meta.XR.Depth;
using UnityEngine;

[RequireComponent(typeof(EnvironmentDepthTextureProvider))]
public class EnvironmentDepthOcclusionControllerCustom : MonoBehaviour
{
    /// <summary>
    /// このクラスをシングルトンと同じように扱う
    /// </summary>
    public static EnvironmentDepthOcclusionControllerCustom Instance;
    
    public CustomOcclusionType OcclusionType => _occlusionType;
    
    public enum CustomOcclusionType
    {
        NoOcclusion,
        HardOcclusion,
        SoftOcclusion,
        CollisionOcclusion
    }

    public static readonly string HardOcclusionKeyword = "HARD_OCCLUSION";
    public static readonly string SoftOcclusionKeyword = "SOFT_OCCLUSION";
    public static readonly string CollisionOcclusionKeyword = "COLLISION_OCCLUSION";

    [SerializeField] private CustomOcclusionType _occlusionType = CustomOcclusionType.NoOcclusion;

    private EnvironmentDepthTextureProvider _depthTextureProvider;

    private void Awake()
    {
        Instance = this;
        _depthTextureProvider = GetComponent<EnvironmentDepthTextureProvider>();
    }

    private void OnEnable()
    {
        _depthTextureProvider.OnDepthTextureAvailabilityChanged += HandleDepthTextureChanged;
    }

    private void OnDisable()
    {
        _depthTextureProvider.OnDepthTextureAvailabilityChanged -= HandleDepthTextureChanged;
    }

    private void Start()
    {
        if (_occlusionType != CustomOcclusionType.NoOcclusion)
        {
            EnableOcclusionType(_occlusionType);
        }
    }

#if UNITY_EDITOR
    private void OnApplicationQuit()
    {
        SetOcclusionShaderKeywords(CustomOcclusionType.NoOcclusion);
    }
#endif
    public void EnableOcclusionType(CustomOcclusionType newOcclusionType, bool updateDepthTextureProvider = true)
    {
        _occlusionType = Utils.GetEnvironmentDepthSupported() ? newOcclusionType : CustomOcclusionType.NoOcclusion;
        bool enableDepthTextureFlag =
            _occlusionType !=
            CustomOcclusionType.NoOcclusion; //true for no occlusion i.e. we want to enable it, false for occlusions
        if ((updateDepthTextureProvider) &&
            (_depthTextureProvider.GetEnvironmentDepthEnabled() !=
             enableDepthTextureFlag)) //we only SetEnvironmentEnabled if needed i.e. the state that it is in right now is different than the state that we want it to be in
        {
            _depthTextureProvider.SetEnvironmentDepthEnabled(isEnabled: enableDepthTextureFlag);
        }
        else
        {
            SetOcclusionShaderKeywords(_occlusionType);
        }
    }

    private void SetOcclusionShaderKeywords(CustomOcclusionType newOcclusionType)
    {
        switch (newOcclusionType)
        {
            case CustomOcclusionType.HardOcclusion:
                // ハード/ソフトオクルージョンの切り替え
                Shader.DisableKeyword(SoftOcclusionKeyword);
                Shader.EnableKeyword(HardOcclusionKeyword);
                // 衝突表示の切り替え
                Shader.DisableKeyword(CollisionOcclusionKeyword);
                break;
            case CustomOcclusionType.SoftOcclusion:
                // ハード/ソフトオクルージョンの切り替え
                Shader.DisableKeyword(HardOcclusionKeyword);
                Shader.EnableKeyword(SoftOcclusionKeyword);
                // 衝突表示の切り替え
                Shader.DisableKeyword(CollisionOcclusionKeyword);
                break;
            case CustomOcclusionType.CollisionOcclusion:
                // ハード/ソフトオクルージョンの切り替え
                Shader.DisableKeyword(HardOcclusionKeyword);
                Shader.EnableKeyword(SoftOcclusionKeyword);
                // 衝突表示の切り替え
                Shader.EnableKeyword(CollisionOcclusionKeyword);
                break;
            default:
                Shader.DisableKeyword(HardOcclusionKeyword);
                Shader.DisableKeyword(SoftOcclusionKeyword);
                Shader.DisableKeyword(CollisionOcclusionKeyword);
                break;
        }
    }

    private void HandleDepthTextureChanged(bool isDepthTextureAvailable)
    {
        if (isDepthTextureAvailable)
        {
            SetOcclusionShaderKeywords(_occlusionType);
        }
        else
        {
            SetOcclusionShaderKeywords(CustomOcclusionType.NoOcclusion);
        }
    }
}

上記の切り替え処理をMRTKのボタンから呼び出すスクリプトも合わせて作成しました。
・OcclusionTypeSwitcher.cs

using System.Collections;
using System.Collections.Generic;
using Microsoft.MixedReality.Toolkit.UI;
using UnityEngine;

public class OcclusionTypeSwitcher : MonoBehaviour
{
    [SerializeField]
    private Interactable switchButton;
    
    void Start()
    {
        switchButton.OnClick.AddListener(async () =>
        {
            // オクルージョンの種類を切り替える
            SwitchOcclusionType();
        });
    }
    
    public void SwitchOcclusionType()
    {
        if (EnvironmentDepthOcclusionControllerCustom.Instance == null) return;
        var currentOcclusionType = EnvironmentDepthOcclusionControllerCustom.Instance.OcclusionType;
        var nextOcclusionType = currentOcclusionType switch {
            EnvironmentDepthOcclusionControllerCustom.CustomOcclusionType.NoOcclusion =>
                EnvironmentDepthOcclusionControllerCustom.CustomOcclusionType.HardOcclusion,
            EnvironmentDepthOcclusionControllerCustom.CustomOcclusionType.HardOcclusion =>
                EnvironmentDepthOcclusionControllerCustom.CustomOcclusionType.SoftOcclusion,
            EnvironmentDepthOcclusionControllerCustom.CustomOcclusionType.SoftOcclusion =>
                EnvironmentDepthOcclusionControllerCustom.CustomOcclusionType.CollisionOcclusion,
            EnvironmentDepthOcclusionControllerCustom.CustomOcclusionType.CollisionOcclusion =>
                EnvironmentDepthOcclusionControllerCustom.CustomOcclusionType.NoOcclusion,
            _ => EnvironmentDepthOcclusionControllerCustom.CustomOcclusionType.NoOcclusion,
        };
        EnvironmentDepthOcclusionControllerCustom.Instance.EnableOcclusionType(nextOcclusionType);
    }
}

シーンの作成

MRTKのスタンダードシェーダを設定したオブジェクトとMRTKのボタンを配置したシーンを作成しました。
スクリプトにボタンの参照を設定して準備は完了です。

動作確認

本プロジェクトをQuest3にデプロイして動作を確認しました。
アプリ内でボタンを押下するごとに以下の通り、オクルージョンの種類を切り替えることができました。

オクルージョン無し


ハードオクルージョン


ソフトオクルージョン


境界を強調する