using System; using UnityEditor; using UnityEngine; using UnityEngine.Experimental.Rendering; using UnityEngine.Rendering.RenderGraphModule; using UnityEngine.Rendering; using UnityEngine.Rendering.RenderGraphModule.Util; using UnityEngine.Rendering.Universal; // Create a Scriptable Renderer Feature that replicates Don't Clear behavior by injecting two render passes into the pipeline. // The first pass copies the camera color target at the end of a frame. The second pass draws the contents of the copied texture at the beginning of a new frame. // For more information about creating Scriptable Renderer Features, refer to https://docs.unity3d.com/Manual/urp/customizing-urp.html. public class KeepFrameFeature : ScriptableRendererFeature { // Create the custom render pass that copies the camera color to a destination texture. class CopyFramePass : ScriptableRenderPass { // Declare the destination texture. RTHandle m_Destination; // Declare the resources the render pass uses. public void Setup(RTHandle destination) { m_Destination = destination; } #pragma warning disable 618, 672 // Type or member is obsolete, Member overrides obsolete member // Override the Execute method to implement the rendering logic. // This method is used only in the Compatibility Mode path. public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { // Skip rendering if the camera isn't a game camera. if (renderingData.cameraData.camera.cameraType != CameraType.Game) return; // Set the source texture as the camera color target. var source = renderingData.cameraData.renderer.cameraColorTargetHandle; // Get a command buffer. CommandBuffer cmd = CommandBufferPool.Get("CopyFramePass"); // Blit the camera color target to the destination texture. Blit(cmd, source, m_Destination); // Execute the command buffer. context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } #pragma warning restore 618, 672 // Override the RecordRenderGraph method to implement the rendering logic. // This method is used only in the render graph system path. public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { // Get the resources the pass uses. UniversalResourceData resourceData = frameData.Get(); UniversalCameraData cameraData = frameData.Get(); // Skip rendering if the camera isn't a game camera. if (cameraData.camera.cameraType != CameraType.Game) return; // Set the source texture as the camera color target. TextureHandle source = resourceData.activeColorTexture; // Import the texture that persists across frames, so the render graph system can use it. TextureHandle destination = renderGraph.ImportTexture(m_Destination); if (!source.IsValid() || !destination.IsValid()) return; // Blit the content of the copied texture to the camera color target with a material. RenderGraphUtils.BlitMaterialParameters para = new(source, destination, Blitter.GetBlitMaterial(TextureDimension.Tex2D), 0); renderGraph.AddBlitPass(para, "Copy Frame Pass"); } } // Create the custom render pass that draws the contents of the copied texture at the beginning of a new frame. class DrawOldFramePass : ScriptableRenderPass { // Declare the resources the render pass uses. class PassData { public TextureHandle source; public Material material; public string name; } Material m_DrawOldFrameMaterial; RTHandle m_Handle; string m_TextureName; // Set up the resources the render pass uses. public void Setup(Material drawOldFrameMaterial, RTHandle handle, string textureName) { m_DrawOldFrameMaterial = drawOldFrameMaterial; m_TextureName = textureName; m_Handle = handle; } // Blit the copied texture to the camera color target. // This method uses common draw commands that both the render graph system and Compatibility Mode paths can use. static void ExecutePass(RasterCommandBuffer cmd, RTHandle source, Material material) { if (material == null) return; // Get the viewport scale. Vector2 viewportScale = source.useScaling ? new Vector2(source.rtHandleProperties.rtHandleScale.x, source.rtHandleProperties.rtHandleScale.y) : Vector2.one; // Blit the copied texture to the camera color target. Blitter.BlitTexture(cmd, source, viewportScale, material, 0); } #pragma warning disable 618, 672 // Type or member is obsolete, Member overrides obsolete member // Override the Execute method to implement the rendering logic. // This method is used only in the Compatibility Mode path. public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { // Get a command buffer. CommandBuffer cmd = CommandBufferPool.Get(nameof(DrawOldFramePass)); cmd.SetGlobalTexture(m_TextureName, m_Handle); // Set the source texture as the camera color target. var source = renderingData.cameraData.renderer.cameraColorTargetHandle; // Blit the camera color target to the destination texture. ExecutePass(CommandBufferHelpers.GetRasterCommandBuffer(cmd), source, m_DrawOldFrameMaterial); // Execute the command buffer. context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } #pragma warning restore 618, 672 // Override the RecordRenderGraph method to implement the rendering logic. // This method is used only in the render graph system path. public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { // Get the resources the pass uses. UniversalResourceData resourceData = frameData.Get(); UniversalCameraData cameraData = frameData.Get(); // Import the texture that persists across frames, so the render graph system can use it. TextureHandle oldFrameTextureHandle = renderGraph.ImportTexture(m_Handle); using (var builder = renderGraph.AddRasterRenderPass("Draw Old Frame Pass", out var passData)) { // Set the destination texture as the camera color target. TextureHandle destination = resourceData.activeColorTexture; if (!oldFrameTextureHandle.IsValid() || !destination.IsValid()) return; // Set the resources the pass uses. passData.material = m_DrawOldFrameMaterial; passData.source = oldFrameTextureHandle; passData.name = m_TextureName; // Set the render graph system to read the copied texture, and write to the camera color target. builder.UseTexture(oldFrameTextureHandle, AccessFlags.Read); builder.SetRenderAttachment(destination, 0, AccessFlags.Write); // Allow global state modifications. Use this only where necessary as it introduces a synchronization point in the frame, which might have an impact on performance. builder.AllowGlobalStateModification(true); // Set the render method. builder.SetRenderFunc((PassData data, RasterGraphContext context) => { context.cmd.SetGlobalTexture(data.name, data.source); ExecutePass(context.cmd, data.source, data.material); }); } } } [Serializable] public class Settings { [Tooltip("Sets the material to use to draw the previous frame.")] public Material displayMaterial; [Tooltip("Sets the texture to copy each frame into. The default it _FrameCopyTex.")] public string textureName; } CopyFramePass m_CopyFrame; DrawOldFramePass m_DrawOldFrame; RTHandle m_OldFrameHandle; public Settings settings = new Settings(); // In this function the passes are created and their point of injection is set public override void Create() { m_CopyFrame = new CopyFramePass(); m_CopyFrame.renderPassEvent = RenderPassEvent.AfterRenderingTransparents; // Frame color is copied late in the frame m_DrawOldFrame = new DrawOldFramePass(); m_DrawOldFrame.renderPassEvent = RenderPassEvent.BeforeRenderingOpaques; // Old frame is drawn early in the frame } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { var descriptor = renderingData.cameraData.cameraTargetDescriptor; descriptor.msaaSamples = 1; descriptor.depthBufferBits = 0; descriptor.graphicsFormat = GraphicsFormat.R8G8B8A8_SRGB; var textureName = String.IsNullOrEmpty(settings.textureName) ? "_FrameCopyTex" : settings.textureName; RenderingUtils.ReAllocateHandleIfNeeded(ref m_OldFrameHandle, descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: textureName); m_CopyFrame.Setup(m_OldFrameHandle); m_DrawOldFrame.Setup(settings.displayMaterial, m_OldFrameHandle, textureName); renderer.EnqueuePass(m_CopyFrame); renderer.EnqueuePass(m_DrawOldFrame); } public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData) { // This path is not taken when using render graph. // The code to reallocate m_OldFrameHandle has been moved to AddRenderPasses in order to avoid duplication. } protected override void Dispose(bool disposing) { m_OldFrameHandle?.Release(); } }