[CustomRendererFeature] Rendering Transparent Obejcts

Context:

I’m developing a 2.5D game that combines Sprites and MeshRenderers.
i want to copy the screen to a texture, then use that texture on a transparent object for effects like a heat wave effect.

While Unity’s built-in Opaque Texture feature can copy the screen after the DrawOpaqueObjects pass, I need to copy the screen after transparent objects are rendered.

Current Implementation:

To achieve this, I’ve created a custom RendererFeature that performs two main tasks after the DrawTransparentObjects pass:

  • 1- Copies the screen to a global texture named _SceneColorAfterTransparent
  • 2- Re-renders all transparent objects, including those in a special “RenderAfterTransparent” layer

Here’s the code:

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;
using System.Collections.Generic;

public class CopyColorAfterTransparentFeature : ScriptableRendererFeature
{
    [System.Serializable]
    private class Settings
    {
        [SerializeField] internal RenderPassEvent passEvent = RenderPassEvent.AfterRenderingTransparents;

        [Header("CopyColor Settings")]
        [SerializeField] internal Downsampling downsampling;

        [Header("Rendering Settings")]
        [SerializeField] internal string[] shaderTagStrings = new string[]
            {"UniversalForward", "UniversalForwardOnly", "SRPDefaultUnlit"};
    }
    [SerializeField] private Settings settings;

    private CopyColorPass copyColorPass;
    private RenderPass renderPass;

    public const string propertyName = "_SceneColorAfterTransparent";
    private static readonly int TextureID = Shader.PropertyToID(propertyName);

    public override void Create()
    {
        copyColorPass = new CopyColorPass(settings.downsampling)
        { renderPassEvent = settings.passEvent };

        List<ShaderTagId> shaderTagIds = new();
        for (int i = 0; i < settings.shaderTagStrings.Length; i++)
            shaderTagIds.Add(new ShaderTagId(settings.shaderTagStrings[i]));

        renderPass = new RenderPass(shaderTagIds, -1)
        { renderPassEvent = settings.passEvent + 2 };
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(copyColorPass);
        renderer.EnqueuePass(renderPass);
    }

    private class CopyColorPass : ScriptableRenderPass
    {
        private readonly Downsampling downsampling;
        internal CopyColorPass(Downsampling downsampling) => this.downsampling = downsampling;

        private class PassData
        {
            internal TextureHandle copySourceTexture;
            internal TextureHandle destinationTexture;
        }

        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            using (var builder = renderGraph.AddRasterRenderPass<PassData>
                ("CopyColor_AfterTransparent", out var passData))
            {
                var resourceData = frameData.Get<UniversalResourceData>();
                passData.copySourceTexture = resourceData.activeColorTexture;

                int factor = downsampling switch
                {
                    Downsampling._2xBilinear => 2,
                    Downsampling._4xBox or Downsampling._4xBilinear => 4,
                    _ => 1
                };

                var cameraData = frameData.Get<UniversalCameraData>();
                RenderTextureDescriptor desc = cameraData.cameraTargetDescriptor;
                desc.msaaSamples = 1;
                desc.depthBufferBits = 0;
                desc.width /= factor;
                desc.height /= factor;

                FilterMode filterMode = downsampling == Downsampling.None ? FilterMode.Point : FilterMode.Bilinear;
                passData.destinationTexture = UniversalRenderer.CreateRenderGraphTexture(
                    renderGraph, desc, "CopyTexture", false, filterMode);

                builder.UseTexture(passData.copySourceTexture);
                builder.SetRenderAttachment(passData.destinationTexture, 0);

                builder.SetGlobalTextureAfterPass(passData.destinationTexture, TextureID);

                builder.AllowPassCulling(false);
                builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
                {
                    Blitter.BlitTexture(context.cmd, data.copySourceTexture,
                        new Vector4(1, 1, 0, 0), 0, false);
                });
            }
        }
    }

    private class RenderPass : ScriptableRenderPass
    {
        private readonly List<ShaderTagId> shaderTagIdList;
        private readonly LayerMask layerMask;
        internal RenderPass(List<ShaderTagId> shaderTagIds, LayerMask layerMask)
        {
            shaderTagIdList = shaderTagIds;
            this.layerMask = layerMask;
        }

        internal class PassData { internal RendererListHandle rendererList; }

        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameContext)
        {
            using (var builder = renderGraph.AddRasterRenderPass<PassData>("Re-RenderTransparentes", out var passData))
            {
                var renderingData = frameContext.Get<UniversalRenderingData>();
                var cameraData = frameContext.Get<UniversalCameraData>();
                var lightData = frameContext.Get<UniversalLightData>();

                var drawSettings = RenderingUtils.CreateDrawingSettings(
                    shaderTagIdList, renderingData, cameraData, lightData, SortingCriteria.CommonTransparent);

                var filterSettings = new FilteringSettings(RenderQueueRange.transparent, layerMask);
                var rendererListParams = new RendererListParams(renderingData.cullResults, drawSettings, filterSettings);

                passData.rendererList = renderGraph.CreateRendererList(rendererListParams);
                builder.UseRendererList(passData.rendererList);

                var resourceData = frameContext.Get<UniversalResourceData>();
                builder.SetRenderAttachment(resourceData.activeColorTexture, 0);
                builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture, AccessFlags.Write);

                builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
                    context.cmd.DrawRendererList(data.rendererList));
            }
        }
    }
}

Setup:

Current Results:

The implementation works as intended in most cases:
a transparent object with the distortion shader surrounded by opaque and transparent objects a transparent object in the center on a bigger transparent object with the distortion shader

The main issue occurs when an object’s center point is closer to the camera - it gets rendered first. This appears to be related to the rendering order of transparent objects.
sad gif :(

Final note:

i’m not using another camera with a RenderTexture for performance reasons.

Resources that helped me (I’m very new to Unity’s rendering pipeline): Unity Documentation | This Repo (it has more problems + uses the old API)

Can anyone help me understand what’s causing this rendering order issue and how to fix it?

Fixed it my self !

happy gif :D

The fix was using an opaque object for the effect, and then re-render transparents.

New Code (only the edited parts):

private class Settings
{
    // New: (in the settings) Layer mask for the effect object
    [[SerializeField] internal LayerMask layerMask;  
}

private RenderPass renderOpaquePass;    // New: For rendering the effect object
private RenderPass renderTransparentPass;  // Existing: For re-rendering transparents

public override void Create()
{
    // ...

    // New: Create opaque pass for the effect object
    renderOpaquePass = new RenderPass(shaderTagIds, SortingCriteria.CommonOpaque,
        RenderQueueRange.opaque, settings.layerMask)
    { renderPassEvent = settings.passEvent + 2 };

    // Modified
    renderTransparentPass = new RenderPass(shaderTagIds, SortingCriteria.CommonTransparent,
        RenderQueueRange.transparent, -1)
    { renderPassEvent = settings.passEvent + 3 };
}

public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
    renderer.EnqueuePass(copyColorPass);
    renderer.EnqueuePass(renderOpaquePass);
    renderer.EnqueuePass(renderTransparentPass);
}

// Updated RenderPass to handle both opaque and transparent objects
private class RenderPass
{
    // Added sorting criteria and render queue parameters
    internal RenderPass(List<ShaderTagId> shaderTagIdList, SortingCriteria sortingCriteria,
        RenderQueueRange renderQueue, LayerMask layerMask)
    {
        this.shaderTagIdList = shaderTagIdList;
        this.sortingCriteria = sortingCriteria;
        this.renderQueue = renderQueue;
        this.layerMask = layerMask;
    }
    // ... rest of the RenderPass implementation
}

Edit: Don’t forget to remove the layermask of the effect from the OpaqueLayerMask in the UniversalRendererData

IDK if this is the best way to do it, so if you have any ideas, don’t hesitate to share them!

1 Like