using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering.RenderGraphModule; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; // Example of how Blit operatrions can be handled using frameData using multiple ScriptaleRenderPasses. public class BlitRendererFeature : ScriptableRendererFeature { // The class living in frameData. It will take care of managing the texture resources. public class BlitData : ContextItem, IDisposable { // Textures used for the blit operations. RTHandle m_TextureFront; RTHandle m_TextureBack; // Render graph texture handles. TextureHandle m_TextureHandleFront; TextureHandle m_TextureHandleBack; // Scale bias is used to control how the blit operation is done. The x and y parameter controls the scale // and z and w controls the offset. static Vector4 scaleBias = new Vector4(1f, 1f, 0f, 0f); // Bool to manage which texture is the most resent. bool m_IsFront = true; // The texture which contains the color buffer from the most resent blit operation. public TextureHandle texture; // Function used to initialize BlitDatat. Should be called before starting to use the class for each frame. public void Init(RenderGraph renderGraph, RenderTextureDescriptor targetDescriptor, string textureName = null) { // Checks if the texture name is valid and puts in default value if not. var texName = String.IsNullOrEmpty(textureName) ? "_BlitTextureData" : textureName; // Reallocate if the RTHandles are being initialized for the first time or if the targetDescriptor has changed since last frame. RenderingUtils.ReAllocateHandleIfNeeded(ref m_TextureFront, targetDescriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: texName + "Front"); RenderingUtils.ReAllocateHandleIfNeeded(ref m_TextureBack, targetDescriptor, FilterMode.Bilinear, TextureWrapMode.Clamp, name: texName + "Back"); // Create the texture handles inside render graph by importing the RTHandles in render graph. m_TextureHandleFront = renderGraph.ImportTexture(m_TextureFront); m_TextureHandleBack = renderGraph.ImportTexture(m_TextureBack); // Sets the active texture to the front buffer texture = m_TextureHandleFront; } // We will need to reset the texture handle after each frame to avoid leaking invalid texture handles // since the texture handles only lives for one frame. public override void Reset() { // Resets the color buffers to avoid carrying invalid references to the next frame. // This could be BlitData texture handles from last frame which will now be invalid. m_TextureHandleFront = TextureHandle.nullHandle; m_TextureHandleBack = TextureHandle.nullHandle; texture = TextureHandle.nullHandle; // Reset the acrive texture to be the front buffer. m_IsFront = true; } // The data we use to transfer data to the render function. class PassData { // When makeing a blit operation we will need a source, a destination and a material. // The source and destination is used to know where to copy from and to. public TextureHandle source; public TextureHandle destination; // The material is used to transform the color buffer while copying. public Material material; } // For this function we don't take a material as argument to show that we should remember to reset values // we don't use to avoid leaking values from last frame. public void RecordBlitColor(RenderGraph renderGraph, ContextContainer frameData) { // Check if BlitData's texture is valid if it isn't initialize BlitData. if (!texture.IsValid()) { // Setup the descriptor we use for BlitData. We should use the camera target's descriptor as a start. var cameraData = frameData.Get(); var descriptor = cameraData.cameraTargetDescriptor; // We disable MSAA for the blit operations. descriptor.msaaSamples = 1; // We disable the depth buffer, since we are only makeing transformations to the color buffer. descriptor.depthStencilFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.None; Init(renderGraph, descriptor); } // Starts the recording of the render graph pass given the name of the pass // and outputting the data used to pass data to the execution of the render function. using (var builder = renderGraph.AddRasterRenderPass("BlitColorPass", out var passData)) { // Fetch UniversalResourceData from frameData to retrive the camera's active color attachment. var resourceData = frameData.Get(); // Remember to reset material since it contains the value from last frame. // If we don't do this we would get the material last commited to the BlitPassData using RenderGraph // since we reuse the object allocation. passData.material = null; passData.source = resourceData.activeColorTexture; passData.destination = texture; // Sets input attachment to the cameras color buffer. builder.UseTexture(passData.source); // Sets output attachment 0 to BlitData's active texture. builder.SetRenderAttachment(passData.destination, 0); // Sets the render function. builder.SetRenderFunc((PassData passData, RasterGraphContext rgContext) => ExecutePass(passData, rgContext)); } } // Records a render graph render pass which blits the BlitData's active texture back to the camera's color attachment. public void RecordBlitBackToColor(RenderGraph renderGraph, ContextContainer frameData) { // Check if BlitData's texture is valid if it isn't it hasn't been initialized or an error has occured. if (!texture.IsValid()) return; // Starts the recording of the render graph pass given the name of the pass // and outputting the data used to pass data to the execution of the render function. using (var builder = renderGraph.AddRasterRenderPass($"BlitBackToColorPass", out var passData)) { // Fetch UniversalResourceData from frameData to retrive the camera's active color attachment. var resourceData = frameData.Get(); // Remember to reset material. Otherwise you would use the last material used in RecordFullScreenPass. passData.material = null; passData.source = texture; passData.destination = resourceData.activeColorTexture; // Sets input attachment to BitData's active texture. builder.UseTexture(passData.source); // Sets output attachment 0 to the cameras color buffer. builder.SetRenderAttachment(passData.destination, 0); // Sets the render function. builder.SetRenderFunc((PassData passData, RasterGraphContext rgContext) => ExecutePass(passData, rgContext)); } } // This function blits the whole screen for a given material. public void RecordFullScreenPass(RenderGraph renderGraph, string passName, Material material) { // Checks if the data is previously initialized and if the material is valid. if (!texture.IsValid() || material == null) { Debug.LogWarning("Invalid input texture handle, will skip fullscreen pass."); return; } // Starts the recording of the render graph pass given the name of the pass // and outputting the data used to pass data to the execution of the render function. using (var builder = renderGraph.AddRasterRenderPass(passName, out var passData)) { // Switching the active texture handles to avoid blit. If we want the most recent // texture we can simply look at the variable texture m_IsFront = !m_IsFront; // Setting data to be used when executing the render function. passData.material = material; passData.source = texture; // Swap the active texture. if (m_IsFront) passData.destination = m_TextureHandleFront; else passData.destination = m_TextureHandleBack; // Sets input attachment to BlitData's old active texture. builder.UseTexture(passData.source); // Sets output attachment 0 to BitData's new active texture. builder.SetRenderAttachment(passData.destination, 0); // Update the texture after switching. texture = passData.destination; // Sets the render function. builder.SetRenderFunc((PassData passData, RasterGraphContext rgContext) => ExecutePass(passData, rgContext)); } } // ExecutePass is the render function for each of the blit render graph recordings. // This is good practice to avoid using variables outside of the lambda it is called from. // It is static to avoid using member variables which could cause unintended behaviour. static void ExecutePass(PassData data, RasterGraphContext rgContext) { // We can use blit with or without a material both using the static scaleBias to avoid reallocations. if (data.material == null) Blitter.BlitTexture(rgContext.cmd, data.source, scaleBias, 0, false); else Blitter.BlitTexture(rgContext.cmd, data.source, scaleBias, data.material, 0); } // We need to release the textures once the renderer is released which will dispose every item inside // frameData (also data types previously created in earlier frames). public void Dispose() { m_TextureFront?.Release(); m_TextureBack?.Release(); } } // Initial render pass for the renderer feature which is run to initialize the data in frameData and copying // the camera's color attachment to a texture inside BlitData so we can do transformations using blit. class BlitStartRenderPass : ScriptableRenderPass { public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { // Creating the data BlitData inside frameData. var blitTextureData = frameData.Create(); // Copies the camera's color attachment to a texture inside BlitData. blitTextureData.RecordBlitColor(renderGraph, frameData); } } // Render pass which makes a blit for each material given to the renderer feature. class BlitRenderPass : ScriptableRenderPass { List m_Materials; // Setup function used to retrive the materials from the renderer feature. public void Setup(List materials) { m_Materials = materials; } public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { // Retrives the BlitData from the current frame. var blitTextureData = frameData.Get(); foreach(var material in m_Materials) { // Skip current cycle if the material is null since there is no need to blit if no // transformation happens. if (material == null) continue; // Records the material blit pass. blitTextureData.RecordFullScreenPass(renderGraph, $"Blit {material.name} Pass", material); } } } // Final render pass to copying the texture back to the camera's color attachment. class BlitEndRenderPass : ScriptableRenderPass { public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { // Retrives the BlitData from the current frame and blit it back again to the camera's color attachment. var blitTextureData = frameData.Get(); blitTextureData.RecordBlitBackToColor(renderGraph, frameData); } } [SerializeField] [Tooltip("Materials used for blitting. They will be blit in the same order they have in the list starting from index 0. ")] List m_Materials; BlitStartRenderPass m_StartPass; BlitRenderPass m_BlitPass; BlitEndRenderPass m_EndPass; // Here you can create passes and do the initialization of them. This is called everytime serialization happens. public override void Create() { m_StartPass = new BlitStartRenderPass(); m_BlitPass = new BlitRenderPass(); m_EndPass = new BlitEndRenderPass(); // Configures where the render pass should be injected. m_StartPass.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; m_BlitPass.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; m_EndPass.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; } // Here you can inject one or multiple render passes in the renderer. // This method is called when setting up the renderer once per-camera. public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { // Early return if there is no texture to blit. if (m_Materials == null || m_Materials.Count == 0) return; // Pass the material to the blit render pass. m_BlitPass.Setup(m_Materials); // Since they have the same RenderPassEvent the order matters when enqueueing them. renderer.EnqueuePass(m_StartPass); renderer.EnqueuePass(m_BlitPass); renderer.EnqueuePass(m_EndPass); } }