じゅころぐAR

ARのブログ

AR FoundationのAR Camera Backgroundを切り替える

技術書典向けにまとめてたんですが、AR Foundation の仕様が以前と変わっていて特殊な対応になるのでブログに書きます。

やりたいこと

画面をタップしたらARのカメラ背景にフィルターがかかるようにします。

切り替え処理のトリガーは画面タップではなくボタンのクリックやタイマーでも問題ありません。

※注:本記事で解説しているのはカメラ背景を動的に切り替える方法でARフィルター(シェーダー)には触れません

実装方法

AR Foundationでは、AR CamaeraのAR Camaera Backgroundでカメラ背景に使うマテリアルを変更できます。

アプリ実行中に切り替えができるよう以下のようなスクリプトを用意しました。

using UnityEngine;
using UnityEngine.XR.ARFoundation;

public class ChangeMaterial : MonoBehaviour
{

    [SerializeField]
    Material[] materials;

    [SerializeField]
    private ARCameraBackground m_ARCameraBackground;

    private int i = 0;

    void Update()
    {
        if (Input.touchCount == 1)
        {
            Touch touch = Input.GetTouch(0);

            if (touch.phase == TouchPhase.Began)
            {
                ChangeMaterial();
            }
        }
    }

    public void ChangeMaterial ()
    {
        i++;
        if (i == materials.Length)
        {
            i = 0;
        }

        m_ARCameraBackground.useCustomMaterial = true;
        m_ARCameraBackground.customMaterial = materials[i];
    }
}

AR Foundation 2.1.4の場合

上記のスクリプトにAR Camera Backgroundの参照を与えて実行するとカメラ背景が切り替わります。
簡単ですね。

AR Foundation 3.0.1の場合

上記のスクリプトを実行してもカメラ背景が切り替わりません。

なぜじゃ!

という訳でスクリプト読みます。

  • Packages > AR Foundation > Runtime > AR > ARBackground.cs

まず、getter,setterの部分です。
読みやすいようにコメントを削除して順序を並べ替えてます。

[SerializeField, FormerlySerializedAs("m_OverrideMaterial")]
bool m_UseCustomMaterial;

[SerializeField, FormerlySerializedAs("m_Material")]
Material m_CustomMaterial;

public bool useCustomMaterial { get => m_UseCustomMaterial; set => m_UseCustomMaterial = value; }
public Material customMaterial { get => m_CustomMaterial; set => m_CustomMaterial = value; }

public Material material
{
    get { return (useCustomMaterial && (customMaterial != null)) ? customMaterial : defaultMaterial; }
}

[SerializeField]で渡したプロパティはcustomMaterialを介して、最終的にはmaterialにセットされています。 materialに直接セットされていないのは、customMaterialがない場合にdefaultMaterialが使われるためです。

次にカメラフレームが更新された際に実行されるOnCameraFrameReceived()を見てみます。

/// <summary>
/// Callback for the camera frame event.
/// </summary>
/// <param name="eventArgs">The camera event arguments.</param>
void OnCameraFrameReceived(ARCameraFrameEventArgs eventArgs)
{
    // Enable background rendering when first frame is received.
    if (m_BackgroundRenderingEnabled)
    {
        if (m_CommandBuffer != null && m_CommandBufferCullingState != shouldInvertCulling)
        {
            ConfigureLegacyCommandBuffer(m_CommandBuffer);
        }
    }
    else
    {
        EnableBackgroundRendering();
    }

    Material material = this.material;
    if (material != null)
    {
        ...

この処理の中でマテリアルの変更がかかれば切り替わるはず。

で、

Material material = this.material;

それっぽい。
ぽいけど、切り替わってない・・・。

それより前の処理を見てみましょう。

// Enable background rendering when first frame is received.
if (m_BackgroundRenderingEnabled)
{
if (m_CommandBuffer != null && m_CommandBufferCullingState != shouldInvertCulling)
    {
        ConfigureLegacyCommandBuffer(m_CommandBuffer);
    }
}
else
{
    EnableBackgroundRendering();
}

m_BackgroundRenderingEnabledEnableBackgroundRendering()を通るとtureになるので、初回のフレームだけEnableBackgroundRendering()が実行され、次のフレームでConfigureLegacyCommandBuffer()が実行されます。

また、ConfigureLegacyCommandBuffer()を通るとm_CommandBufferCullingState == shouldInvertCullingになるのでConfigureLegacyCommandBuffer()は毎フレームは実行される処理ではないようです。

・・・怪しい。

ConfigureLegacyCommandBuffer()を見てみますね。

/// <summary>
/// Configures the <paramref name="commandBuffer"/> by first clearing it,
/// and then adding necessary render commands.
/// </summary>
/// <param name="commandBuffer">The command buffer to configure.</param>
protected virtual void ConfigureLegacyCommandBuffer(CommandBuffer commandBuffer)
{
    Texture texture = !material.HasProperty(k_MainTexName) ? null : material.GetTexture(k_MainTexName);

    commandBuffer.Clear();
    AddOpenGLES3ResetStateCommand(commandBuffer);
    m_CommandBufferCullingState = shouldInvertCulling;
    commandBuffer.SetInvertCulling(m_CommandBufferCullingState);
    commandBuffer.ClearRenderTarget(true, false, Color.clear);
    commandBuffer.Blit(texture, BuiltinRenderTextureType.CameraTarget, material);
}

はい、ここ!!
ちょっと止めてください。

commandBuffer.Blit(texture, BuiltinRenderTextureType.CameraTarget, material);

CommandBuffer.Blitmaterialを渡してますね?

CommandBufferでレンダリングパイプラインを拡張して、そこでcustomMaterialを使っているようです。
つまり、マテリアルを切り替えたらCommandBufferを変更しないといけないのでは?

少々乱暴ですが、if文の条件を書き換えて毎フレームConfigureLegacyCommandBuffer()を実行するようにしてみます。

/// <summary>
/// Callback for the camera frame event.
/// </summary>
/// <param name="eventArgs">The camera event arguments.</param>
void OnCameraFrameReceived(ARCameraFrameEventArgs eventArgs)
{
    // Enable background rendering when first frame is received.
    if (m_BackgroundRenderingEnabled)
    {
        if (m_CommandBuffer != null)
        {
            // m_CommandBufferCullingState != shouldInvertCullingの条件を削除
            // CommandBufferの設定が毎フレーム実行されるようになる
            ConfigureLegacyCommandBuffer(m_CommandBuffer);
        }
    }
    else
    {
        EnableBackgroundRendering();
    }

    Material material = this.material;
    if (material != null)
    {
        ...

無事タップ処理での切り替えができるようになりました。

上記はちょっと無理やり感がある実装でパフォーマンスに影響が出るかもしれないため、どうせ改変するならマテリアルが変更されたときだけConfigureLegacyCommandBufferを再実行するようスクリプトから値を渡すか関数を実行する形にした方がよいです。

まとめ

AR Foundationのスクリプトをハックしているため、利用は自己責任でお願いします。
AR FoundationはUnity Companion Licenseのため、改変して使うこと自体は問題ないですが、パッケージ管理に支障が出る恐れがあります。

ちなみに、AR Foundation 3.1.0 previewの仕様も3.0.1とほぼ同じです。