Render Passes for a Texture

I have a texture that I want to apply different render pass “filters” on to change the image. Many of the URP render pass examples assume render textures based on cameras, but I don’t necessarily need to render any cameras, I just need to change the texture. What would be the best way to go about that? Thanks in advance!

This is what I’ve got so far, but I don’t see any change in the target texture. The _targetMaterial shader has a _TargetTex texture property, and the _renderPassMaterial has a _BlitTexture texture property.

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Visuals
{
    public class EnqueueRenderPasses : MonoBehaviour
    {
        private CustomRenderPass customRenderPass;

        [SerializeField]
        private Renderer _rendererWithTargetTexture;
        private Material _targetMaterial;

        private Material _renderPassMaterial;
        
        [SerializeField]
        private Shader _renderPassShader;

        private void OnEnable()
        {
            _targetMaterial = _rendererWithTargetTexture.material;
            _renderPassMaterial = CoreUtils.CreateEngineMaterial(_renderPassShader);
            customRenderPass = new CustomRenderPass(_targetMaterial, _renderPassMaterial);
            RenderPipelineManager.beginCameraRendering += OnBeginCamera;
        }

        private void OnDisable()
        {
            RenderPipelineManager.beginCameraRendering -= OnBeginCamera;
            customRenderPass.Dispose();
        }

        private void OnBeginCamera(ScriptableRenderContext context, Camera cam)
        {
            cam.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(customRenderPass);
        }
    }
}
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Visuals
{
    public class CustomRenderPass : ScriptableRenderPass
    {
        private Material _targetMaterial;
        private Material _renderPassMaterial;
        private const string _targetTexturePropertyName = "_TargetTex";
        private int _targetTextureID = Shader.PropertyToID(_targetTexturePropertyName);

        private RenderTextureDescriptor _textureDescriptor;

        private RTHandle _targetRTHandle;
        private RTHandle _renderPassRTHandle; 

        public CustomRenderPass(Material targetMaterial, Material renderPassMaterial)
        {
            _targetMaterial = targetMaterial;
            _renderPassMaterial = renderPassMaterial;
            _textureDescriptor = new RenderTextureDescriptor(_targetMaterial.GetTexture(_targetTextureID).width, 
                                                             _targetMaterial.GetTexture(_targetTextureID).height, 
                                                             RenderTextureFormat.Default, 0);
        }

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            _targetRTHandle = RTHandles.Alloc(_targetMaterial.GetTexture(_targetTextureID));
            RenderingUtils.ReAllocateIfNeeded(ref _renderPassRTHandle, _textureDescriptor);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get();
            Blitter.BlitTexture(cmd, _targetRTHandle, _renderPassRTHandle, _renderPassMaterial, 0);
            cmd.SetGlobalTexture(_targetTextureID, _renderPassRTHandle.nameID);
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
            CommandBufferPool.Release(cmd);
        }

        public void Dispose()
        {
            
#if UNITY_EDITOR
            if (EditorApplication.isPlaying)
            {
                Object.Destroy(_renderPassMaterial);
            }
            else
            {
                Object.DestroyImmediate(_renderPassMaterial);
            }
#else
            Object.Destroy(_renderPassMaterial);
#endif
            _renderPassRTHandle?.Release();
            _targetRTHandle?.Release();
        }
    }
}

I had to uncheck Exposed on the _BlitTexture property in my shader for cmd.SetGlobalTexture to work. My working code is a bit messy at the moment, but I’ll try and clean it up and post a sample.

I’m struggling to figure out how to set the source texture to equal the destination texture. When I call _sourceMaterial.SetTexture(_sourceTextureID, _destinationRTHandle); the resulting texture is black.

I’m starting to wonder if I’m even on the right track? If I want to apply multiple different shader passes to the same texture, is this approach correct, or should I do something different? I think what I’m trying to do is multipass rendering, which I know can be tricky in URP. Should I be using something like CommandBuffer.DrawRenderer instead? Could really use some help.

Here’s an image of a cat to illustrate what I’m going for:

EnqueueRenderPasses

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Visuals
{
    public class EnqueueRenderPasses : MonoBehaviour
    {
        private CustomRenderPass customRenderPass;

        [SerializeField]
        private Renderer _sourceRenderer;
        private Material _sourceMaterial;

        private Material _destinationMaterial;
        
        [SerializeField]
        private Shader _destinationShader;

        private void OnEnable()
        {
            _sourceMaterial = _sourceRenderer.material;
            _destinationMaterial = CoreUtils.CreateEngineMaterial(_destinationShader);
            customRenderPass = new CustomRenderPass(_sourceMaterial, _destinationMaterial);
            RenderPipelineManager.beginCameraRendering += OnBeginCamera;        
        }

        private void OnDisable()
        {
            RenderPipelineManager.beginCameraRendering -= OnBeginCamera;
            customRenderPass?.Dispose();
        }

        private void OnBeginCamera(ScriptableRenderContext context, Camera cam)
        {
            cam.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(customRenderPass);
        }
    }
}

CustomRenderPass

using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Visuals
{
    public class CustomRenderPass : ScriptableRenderPass
    {
        private RenderTextureDescriptor _textureDescriptor;

        //Source
        private Material _sourceMaterial;
        private const string _sourceTexturePropertyName = "_MainTex";
        private int _sourceTextureID = Shader.PropertyToID(_sourceTexturePropertyName);
        private Texture _sourceTexture;
        private RTHandle _sourceRTHandle;
        private RenderTexture _sourceRT;    

        //Destination
        private Material _destinationMaterial;
        private RTHandle _destinationRTHandle; 
        
        public CustomRenderPass(Material sourceMaterial, Material destinationMaterial)
        {
            //Source
            _sourceMaterial = sourceMaterial;
            _sourceTexture = _sourceMaterial.GetTexture(_sourceTextureID);
            _sourceRT = new RenderTexture(_sourceTexture.width, _sourceTexture.height, 0);
            
            //Destination
            _destinationMaterial = destinationMaterial;

            _textureDescriptor = new RenderTextureDescriptor(_sourceMaterial.GetTexture(_sourceTextureID).width,
                         _sourceMaterial.GetTexture(_sourceTextureID).height,
                         RenderTextureFormat.Default, 0);
            RTHandles.Initialize(_sourceTexture.width, _sourceTexture.height);
        }

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            //Source            
            RenderTexture.active = _sourceRT;
            Graphics.Blit(_sourceTexture, _sourceRT);
            _sourceRTHandle = RTHandles.Alloc(_sourceRT);

            //Destination
            RenderingUtils.ReAllocateIfNeeded(ref _destinationRTHandle, _textureDescriptor);
            ConfigureTarget(_destinationRTHandle);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get();
            Blitter.BlitTexture(cmd, _sourceRTHandle, _destinationRTHandle, _destinationMaterial, 0);
            //I get a black texture here :(
            _sourceMaterial.SetTexture(_sourceTextureID, _destinationRTHandle);
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
            CommandBufferPool.Release(cmd);
        }

        public void Dispose()
        {

#if UNITY_EDITOR
            if (EditorApplication.isPlaying)
            {
                Object.Destroy(_destinationMaterial);
            }
            else
            {
                Object.DestroyImmediate(_destinationMaterial);
            }
#else
            Object.Destroy(_renderPassMaterial);
#endif
            _destinationRTHandle?.Release();
            _sourceRTHandle?.Release();
        }
    }
}

Have you tried cmd.SetGlobalTexture? And can you see your Blit destination texture’s result in Frame Debugger? And maybe you should set texture for your source material after execute commandbuffer.
By the way, if you need to copy the source texture to the destination texture, just use Blitter.BlitCameraTexture(cmd, src, dst) which will blit texture using Unity’s default copy pass.But if using Blitter.BlitTexture(cmd, src, dst, mat, passindex), you have to write your own blit shader. So do you have your own blit shader? and can you share with us?
Further more, I’m not sure it’s right or not, Blitter.BlitTexture will not set render target for you, and you may need to use cmd.SetRenderTarget to set it by yourself.

Thanks for the reply.

Here is some updated code with the following edits:

  • Changed _sourceTexturePropertyName = "_MainTex" to be _sourceTexturePropertyName = "_SourceTex" so I could try out cmd.SetGlobalTexture(_sourceTextureID, _destinationRTHandle) with a more unique name instead of _MainTex.
  • Replaced _sourceMaterial.SetTexture with cmd.SetGlobalTexture

Unfortunately, running cmd.SetGlobalTexture or _sourceMaterial.SetTexture before or after context.ExecuteCommandBuffer(cmd) resulted in the same black texture.

I’ve also included some screenshots of the shaders and frame debugger. Note: the destination shader has a _BlitTexture unexposed property.

CustomRenderPass

using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Visuals
{
    public class CustomRenderPass : ScriptableRenderPass
    {
        private RenderTextureDescriptor _textureDescriptor;

        //Source
        private Material _sourceMaterial;
        private const string _sourceTexturePropertyName = "_SourceTex";
        private int _sourceTextureID = Shader.PropertyToID(_sourceTexturePropertyName);
        private Texture _sourceTexture;
        private RTHandle _sourceRTHandle;
        private RenderTexture _sourceRT;

        //Destination
        private Material _destinationMaterial;
        private RTHandle _destinationRTHandle;

        public CustomRenderPass(Material sourceMaterial, Material destinationMaterial)
        {
            //Source
            _sourceMaterial = sourceMaterial;
            _sourceTexture = _sourceMaterial.GetTexture(_sourceTextureID);
            _sourceRT = new RenderTexture(_sourceTexture.width, _sourceTexture.height, 0);

            //Destination
            _destinationMaterial = destinationMaterial;

            _textureDescriptor = new RenderTextureDescriptor(_sourceMaterial.GetTexture(_sourceTextureID).width,
                         _sourceMaterial.GetTexture(_sourceTextureID).height,
                         RenderTextureFormat.Default, 0);
            RTHandles.Initialize(_sourceTexture.width, _sourceTexture.height);
        }

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            //Source            
            RenderTexture.active = _sourceRT;
            Graphics.Blit(_sourceTexture, _sourceRT);
            _sourceRTHandle = RTHandles.Alloc(_sourceRT);

            //Destination
            RenderingUtils.ReAllocateIfNeeded(ref _destinationRTHandle, _textureDescriptor);
            ConfigureTarget(_destinationRTHandle);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get();
            Blitter.BlitTexture(cmd, _sourceRTHandle, _destinationRTHandle, _destinationMaterial, 0);
            //I still get a black texture here :(
            cmd.SetGlobalTexture(_sourceTextureID, _destinationRTHandle);
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
            CommandBufferPool.Release(cmd);
        }

        public void Dispose()
        {

#if UNITY_EDITOR
            if (EditorApplication.isPlaying)
            {
                Object.Destroy(_destinationMaterial);
            }
            else
            {
                Object.DestroyImmediate(_destinationMaterial);
            }
#else
            Object.Destroy(_renderPassMaterial);
#endif
            _destinationRTHandle?.Release();
            _sourceRTHandle?.Release();
        }
    }
}

Runtime Result

Source Shader

Destination Shader

Frame Debugger: Blitter.BlitTexture SRP Batch

Frame Debugger: cmd.SetGlobalTexture SRP Batch

From looking at the source, it appears that it does set the render target:

/// <summary>
/// Blit a Texture with a specified material. The reference name "_BlitTexture" will be used to bind the input texture.
/// </summary>
/// <param name="cmd">Command Buffer used for rendering.</param>
/// <param name="source">Source render target.</param>
/// <param name="destination">Destination render target.</param>
/// <param name="material">Material to invoke when blitting.</param>
/// <param name="pass">Pass idx within the material to invoke.</param>
public static void BlitTexture(CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination, Material material, int pass)
{
    // Unfortunately there is no function bind a RenderTargetIdentifier with a property block so we have to bind it globally.
    cmd.SetGlobalTexture(BlitShaderIDs._BlitTexture, source);
    cmd.SetRenderTarget(destination);
    DrawTriangle(cmd, material, pass);
}

Can you show me your Destination ShaderGraph’s Graph Settings? Is it in FullScreen Mode now? It should looks like this pic:


And for Blit Texture ,you can use URP Sample Buffer node, and set Source Buffer to Blit Source, for example:

By the way, if you don’t want to perform a full screen blit action, just send your source texture to a shader graph and apply some filters in it, there’s no need to create an additional render pass.

The destination shader graph is set to Unlit. When I set it to Fullscreen with a URP Sample Buffer set to BlitSource the _SourceTex remains black. After modifying the code so I could see the _destinationRTHandle Render Texture in the Inspector, it also appears black, so that seems to be the fundamental problem.

Regarding simply sending the texture through a shader instead of render passes, I can’t do that because I need runtime control to apply any number of render passes to the same texture, in various orders.

Maybe the issue is that I’m injecting my pass in a scene with RenderPipelineManager.beginCameraRendering instead of a Renderer Feature?

Here is my updated code:

EnqueueRenderPasses

  • Added RenderTexture _sourceRT and RenderTexture _destinationRT to see the textures in the Inspector
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Visuals
{
    public class EnqueueRenderPasses : MonoBehaviour
    {
        private CustomRenderPass customRenderPass;

        //Source

        [SerializeField]
        private Renderer _sourceRenderer;
        private Material _sourceMaterial;
        [SerializeField]
        private RenderTexture _sourceRT;

        //Destination
        private Material _destinationMaterial;
        [SerializeField]
        private Shader _destinationShader;
        [SerializeField]
        private RenderTexture _destinationRT;

        private void OnEnable()
        {
            _sourceMaterial = _sourceRenderer.material;
            _destinationMaterial = CoreUtils.CreateEngineMaterial(_destinationShader);
            customRenderPass = new CustomRenderPass(_sourceMaterial, _destinationMaterial);

            //Event listeners
            RenderPipelineManager.beginCameraRendering += OnBeginCamera;
            customRenderPass.OnSourceRTCreated += OnSourceRTCreated;
            customRenderPass.OnDestinationRTCreated += OnDestinationRTCreated;
        }

        private void OnDisable()
        {
            RenderPipelineManager.beginCameraRendering -= OnBeginCamera;
            customRenderPass?.Dispose();
        }

        private void OnBeginCamera(ScriptableRenderContext context, Camera cam)
        {
            cam.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(customRenderPass);
        }

        private void OnSourceRTCreated(RenderTexture rt)
        {
            _sourceRT = rt;
        }

        private void OnDestinationRTCreated(RenderTexture rt)
        {
            _destinationRT = rt;
        }
    }
}

CustomRenderPass

  • Added events for the render texture creation
  • Added an _OutputTex unexposed property to a new Output shader to use with the cmd.SetGlobalTexture call. Did this so I could expose the _SourceTex property to set the source cat texture.
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Visuals
{
    public class CustomRenderPass : ScriptableRenderPass
    {
        private RenderTextureDescriptor _textureDescriptor;

        //Source
        private Material _sourceMaterial;
        private const string _sourceTexturePropertyName = "_SourceTex";
        private int _sourceTextureID = Shader.PropertyToID(_sourceTexturePropertyName);
        private Texture _sourceTexture;
        private RTHandle _sourceRTHandle;
        private RenderTexture _sourceRT;
        public event System.Action<RenderTexture> OnSourceRTCreated;

        //Destination
        private Material _destinationMaterial;
        private RTHandle _destinationRTHandle;
        public event System.Action<RenderTexture> OnDestinationRTCreated;

        //Output
        private const string _outputTexturePropertyName = "_OutputTex";
        private int _outputTextureID = Shader.PropertyToID(_outputTexturePropertyName);

        public CustomRenderPass(Material sourceMaterial, Material destinationMaterial)
        {
            //Source
            _sourceMaterial = sourceMaterial;
            _sourceTexture = _sourceMaterial.GetTexture(_sourceTextureID);
            _sourceRT = new RenderTexture(_sourceTexture.width, _sourceTexture.height, 0);

            //Destination
            _destinationMaterial = destinationMaterial;

            _textureDescriptor = new RenderTextureDescriptor(_sourceMaterial.GetTexture(_sourceTextureID).width,
                         _sourceMaterial.GetTexture(_sourceTextureID).height,
                         RenderTextureFormat.Default, 0);
            RTHandles.Initialize(_sourceTexture.width, _sourceTexture.height);
        }

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            //Source            
            RenderTexture.active = _sourceRT;
            Graphics.Blit(_sourceTexture, _sourceRT);
            _sourceRTHandle = RTHandles.Alloc(_sourceRT);
            OnSourceRTCreated?.Invoke(_sourceRT);

            //Destination
            RenderingUtils.ReAllocateIfNeeded(ref _destinationRTHandle, _textureDescriptor);
            ConfigureTarget(_destinationRTHandle);
            OnDestinationRTCreated?.Invoke(_destinationRTHandle.rt);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get();
            Blitter.BlitTexture(cmd, _sourceRTHandle, _destinationRTHandle, _destinationMaterial, 0);
            //I still get a black texture here :(
            cmd.SetGlobalTexture(_outputTextureID, _destinationRTHandle);
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
            CommandBufferPool.Release(cmd);
        }

        public void Dispose()
        {

#if UNITY_EDITOR
            if (EditorApplication.isPlaying)
            {
                Object.Destroy(_destinationMaterial);
            }
            else
            {
                Object.DestroyImmediate(_destinationMaterial);
            }
#else
            Object.Destroy(_renderPassMaterial);
#endif
            _destinationRTHandle?.Release();
            _sourceRTHandle?.Release();
        }
    }
}

And here is the result showing the same black texture in the output:

I set the destination shader Graph Settings to Fullscreen with a URP Sample Buffer set to BlitSource, and the destination render texture now has color information in it, but it appears to be recursively rendering into itself…

Still struggling with this one. Anyone have any ideas?

Closing this thread in favor of this new one that is a more concise representation of the problem: cmd.Blit works, how to get Blitter to do the same thing? - Unity Engine - Unity Discussions