げぇむぷろぐらみんぐ

日々の生活で得た知識、経験を書きます

Unity URPでの画面キャプチャ用パスを用いたCameraCaptureBridgeについて

久しぶりのブログです。
UnityのURPで、カメラに映っている画面をキャプチャして取り扱えるようにしてくれるパスであるCapturePassを用いた、CameraCaptureBridgeについて紹介します。

検証環境は下記になります。

  • Unity2021.3.14f1
  • Universal RP 12.1.8

できること

下記のスクショのように、描画結果をRenderTextureに書き出したりできます。 RenderTextureに書き出す以外にも、特定のRenderTargetに出力することもできます。

使い方

まずはソースコード全文になります。

using UnityEngine;
using UnityEngine.Rendering;

/// <summary>
/// 渡されたRenderTextureにキャプチャ結果を描画する
/// </summary>
public class ScreenCapture : MonoBehaviour
{
    [SerializeField]
    private RenderTexture _targetRenderTexture;

    private Camera _targetCamera;

    private void Start()
    {
        CaptureStart(Camera.main);
    }

    /// <summary>
    /// 指定したカメラが移しているもののキャプチャの開始
    /// </summary>
    /// <param name="camera">キャプチャしたい描画対象を写しているカメラ</param>
    public void CaptureStart(Camera camera)
    {
        _targetCamera = camera;
        CameraCaptureBridge.AddCaptureAction(_targetCamera, Capture);
    }

    /// <summary>
    /// キャプチャ終了
    /// </summary>
    public void CaptureEnd()
    {
        CameraCaptureBridge.RemoveCaptureAction(_targetCamera, Capture);
    }

    /// <summary>
    /// キャプチャ処理
    /// </summary>
    private void Capture(RenderTargetIdentifier renderTargetIdentifier, CommandBuffer commandBuffer)
    {
        commandBuffer.Blit(renderTargetIdentifier, _targetRenderTexture);
    }
}

上記のコードの中で重要なのは、キャプチャ開始部分の

CameraCaptureBridge.AddCaptureAction(_targetCamera, Capture);

キャプチャ終了部分の

CameraCaptureBridge.RemoveCaptureAction(_targetCamera, Capture);

になります。

キャプチャをしたい対象のカメラと、そのキャプチャによって得られたRenderTargetIdentifier をどう取り扱うかのActionをコールバックで渡します。
また、Removeするまではキャプチャをし続けられるので、不要になったタイミングでRemoveします。

基本的な使い方はこれだけです。
以降では、どのようにしてCaptureされているかをURPのソースコードを覗きつつ、注意したほうが良さそうなことについても紹介します。

コードを追ってみる

CameraCaptureBridgeのAddCaptureActionをまず見てみます。 すると、下記のような内容になっています。(コード)

public static void AddCaptureAction(Camera camera, Action<RenderTargetIdentifier, CommandBuffer> action)
{
   actionDict.TryGetValue(camera, out var actions);
   if (actions == null)
   {
         actions = new HashSet<Action<RenderTargetIdentifier, CommandBuffer>>();
         actionDict.Add(camera, actions);
   }

   actions.Add(action);
}

これを見ると、登録したActionはactionDictというDictionaryに追加されているようです。

なので、次はactionDictがどう使われているかを追ってみると、下記のようにDictionaryの内容をIEnumeratorとして返しているのがわかります。 (コード)

public static IEnumerator<Action<RenderTargetIdentifier, CommandBuffer>> GetCaptureActions(Camera camera)
{
   if (!actionDict.TryGetValue(camera, out var actions) || actions.Count == 0)
         return null;

   return actions.GetEnumerator();
}

さらに、上記のメソッドを呼んでいる箇所を探してみると、下記のようでした。 ここでは、CameraDataのcaptureActionsというものに取得した結果を渡しています。(コード)

static void InitializeStackedCameraData(Camera baseCamera, UniversalAdditionalCameraData baseAdditionalCameraData, ref CameraData cameraData)
{
...
cameraData.captureActions = CameraCaptureBridge.GetCaptureActions(baseCamera);
}

次に、cameraData.captureActionsを使っているところを探してみると、CapturePassに辿り着きました。 (コード)

internal class CapturePass : ScriptableRenderPass
{
   public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
   {
      CommandBuffer cmdBuf = CommandBufferPool.Get();
      using (new ProfilingScope(cmdBuf, m_ProfilingSampler))
      {
            var colorAttachmentIdentifier = m_CameraColorHandle.Identifier();
            var captureActions = renderingData.cameraData.captureActions;
            for (captureActions.Reset(); captureActions.MoveNext();)
               captureActions.Current(colorAttachmentIdentifier, cmdBuf);
      }

      context.ExecuteCommandBuffer(cmdBuf);
      CommandBufferPool.Release(cmdBuf);
   }
}

上のコードを見ると、cameraDataに渡したcaptureActionsをforで順次回して実行していることがわかります。
上記の挙動から、注意するべきこととして、渡すAction内ではRemoveCaptureAction を呼んではいけないことがわかります。
もし呼んでしまうと、ループ中にEnumeratorに変更が加わってしまい、例外が発生してしまいます。 なので、もし1Frameだけキャプチャしたい、という場合にも渡すAction内でRemoveするのではなく、上記の処理が終わったタイミングで別途Removeをしてあげる必要があります。

まとめ

今回は、URPで簡単にキャプチャ処理が実装できるCameraCaptureBridgeについて紹介しました。 こちらは、とても簡潔にキャプチャ処理ができる反面、キャプチャのタイミングをRenderPassEvent.AfterRendering以外にしたい場合などカスタマイズしたい場合には使用することができないので、使用機会は少ないかもしれませんが、求めている状況と合えばとても便利だと思います。