【URP14】残像が残るカメラを実装する

2023/05/20に公開

はじめに

URP14で、残像を作るようなカメラを作ってみました。

環境

Unity 2022.2.12
Universal RP 14.0.6

GitHub サンプル

https://github.com/rngtm/URP-Afterimage

標準カメラでは残像を作れない

URPの標準のカメラでは、 残像が表示されません。

残像が表示されない

残像を表示できない理由

これは、カメラのレンダリングが始まるタイミングでレンダーターゲットのクリアが行われてしまうためです。
直前フレームの描画結果が消されてしまうため、残像が表示されません。

FrameDebugger

残像を作る

今回の記事では、残像が残るようなカメラを実装します。

実装のアプローチ

以下のアプローチで実装します。

  1. 描画終了時、カメラのレンダーターゲットをRenderTextureへコピー
  2. 画面クリアが行われた直後のタイミングで、RenderTextureをカメラレンダーターゲットへコピーする

STEP1. RenderTextureを生成する

以下のコンポーネントを、シーン内の好きなオブジェクトにアタッチします。

CameraImage.cs
using UnityEngine;
using UnityEngine.Experimental.Rendering;

public static class RenderConfig
{
    public static RenderTexture CameraTexture { get; set; }
}

public class CameraImage : MonoBehaviour
{
    private RenderTexture _renderTexture; // カメラ内容を表示したいRenderTexture
    private MaterialPropertyBlock _materialPropertyBlock; // マテリアルのパラメータ変更用のMaterialPropertyBlock
    private MeshRenderer _meshRenderer; // RenderTextureを表示用のMeshRenderer

    private void Start()
    {
        Setup();
    }

    private void OnDestroy()
    {
        Release();
    }
    
    /// <summary>
    /// セットアップ処理
    /// </summary>
    private void Setup()
    {
        _renderTexture = new RenderTexture(Screen.width, Screen.height, 0, GraphicsFormat.R32G32B32A32_SFloat);
        
        // ScriptableRenderPassから見える場所にRenderTextureを登録する
        RenderConfig.CameraTexture = _renderTexture;
    }

    /// <summary>
    /// 解放処理
    /// </summary>
    private void Release()
    {
        if (_renderTexture != null)
        {
            if (Application.isPlaying)
            {
                Destroy(_renderTexture);
            }
            else
            {
                DestroyImmediate(_renderTexture);
            }

            _renderTexture = null;
        }
    }
}

STEP2. カメラ描画内容のコピー (ScriptableRendererFeature)

以下は、カメラ描画ターゲットとテクスチャの間のコピーを行うレンダーパスになります。
これを用いて、残像の描画を行います。

using UnityEngine;
using UnityEngine.Rendering.Universal;

/// <summary>
/// テクスチャコピーを行うRendererFeature
/// </summary>
public class CopyTextureFeature : ScriptableRendererFeature
{
    [SerializeField] private Settings settings = new Settings();
    private CopyTexturePass _copyTexturePass;

    public enum CopyMode
    {
        CameraToTexture,
        TextureToCamera,
    }

    [System.Serializable]
    private class Settings
    {
        public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRendering;
        public CopyMode copyMode = CopyMode.CameraToTexture;
    }

    /// <inheritdoc/>
    public override void Create()
    {
        _copyTexturePass = new CopyTexturePass
        {
            renderPassEvent = settings.renderPassEvent
        };
    }
    
    /// <inheritdoc/>
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        _copyTexturePass.Setup(settings.copyMode);
        renderer.EnqueuePass(_copyTexturePass);
    }
}
using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

/// <summary>
/// テクスチャコピーを行うRenderPass
/// </summary>
public class CopyTexturePass : ScriptableRenderPass
{
    private CopyTextureFeature.CopyMode _copyMode;

    public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    {
        base.Configure(cmd, cameraTextureDescriptor);
        ConfigureClear(ClearFlag.None, Color.clear);
    }

    // Here you can implement the rendering logic.
    // Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
    // https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
    // You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        // 再生中のみ実行
        if (!Application.isPlaying)
        {
            return;
        }

        if (RenderConfig.CameraPreviewTexture == null)
        {
            return;
        }
        
        // Gameカメラのみ実行
        var camera = renderingData.cameraData.camera;
        bool isGameCamera = (camera.cameraType == CameraType.Game || camera.cameraType == CameraType.VR);
        if (!isGameCamera)
        {
            return;
        }
        
        // テクスチャをコピー
        var cmd = CommandBufferPool.Get("Copy Texture");
        var cameraColorTargetHandle = renderingData.cameraData.renderer.cameraColorTargetHandle;
        switch (_copyMode)
        {
            case CopyTextureFeature.CopyMode.CameraToTexture:
                cmd.Blit(cameraColorTargetHandle, RenderConfig.CameraPreviewTexture);
                break;
            case CopyTextureFeature.CopyMode.TextureToCamera:
                cmd.Blit(RenderConfig.CameraPreviewTexture, cameraColorTargetHandle);
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    public void Setup(CopyTextureFeature.CopyMode copyMode)
    {
        _copyMode = copyMode;
    }
}

上記のRendererFeatureは、UniversalRenderPipelineAssetへ以下のように登録して使用します。

結果

以下のように残像が表示されます。

Discussion