(HDRP) Custom Pass - Per-Renderer Solution

I’ve been trying to achieve a simple highlight effect in HDRP that outlines and fills objects in two passes, but I’ve been struggling with the implementation. I would appreciate some help with the following questions:

  1. I need highlighting to be on a per-renderer basis, rather than per-layer (rendering layer masks are fine).

I created a custom pass that allocates a buffer and draws individual renderers into it using the command buffer from the custom pass context, and while that concept works perfectly for what I need, it’s been very buggy and limited. I can’t seem to do any depth testing, backfaces aren’t culled, and there’s an issue where alpha is fully opaque on scene start/load, but fixes itself after re-saving the scene.

        protected override void Setup(ScriptableRenderContext RenderContext, CommandBuffer CMD)
        {
            highlightShaderFullscreen = Shader.Find("Fullscreen/Highlight");
            highlightShaderRenderer = Shader.Find("Fullscreen/HighlightRenderer");

            fullscreenMaterial = CoreUtils.CreateEngineMaterial(highlightShaderFullscreen);
            rendererMaterial = CoreUtils.CreateEngineMaterial(highlightShaderRenderer);

            highlightBuffer = RTHandles.Alloc
            (
                Vector2.one, TextureXR.slices, dimension: TextureXR.dimension,
                colorFormat: GraphicsFormat.B8G8R8A8_SRGB,
                useDynamicScale: true, name: "Highlight Buffer"
            );

            highlightDepthBuffer = RTHandles.Alloc
            (
                Vector2.one, TextureXR.slices, dimension: TextureXR.dimension,
                colorFormat: GraphicsFormat.R16_UInt, useDynamicScale: true,
                name: "Highlight Depth Buffer", depthBufferBits: DepthBits.Depth16
            );
        }

        protected override void Execute(CustomPassContext CTX)
        {
            CoreUtils.SetRenderTarget(CTX.cmd, highlightBuffer, highlightDepthBuffer, ClearFlag.All);

            highlightRenderers.ForEach(Renderer =>
            {
                for (int i = 0; i < Renderer.sharedMaterials.Length; i++)
                {
                    CTX.cmd.DrawRenderer(Renderer, rendererMaterial, i);
                }
            });

            CTX.propertyBlock.SetTexture("_HighlightBuffer", highlightBuffer);
            CTX.propertyBlock.SetInt("_OutlineIterations", outlineIterations);
            CTX.propertyBlock.SetFloat("_OutlineIntensity", outlineIntensity);
            CTX.propertyBlock.SetFloat("_OutlineWidth", outlineWidth);

            CoreUtils.DrawFullScreen(CTX.cmd, fullscreenMaterial, CTX.cameraColorBuffer, CTX.cameraDepthBuffer, shaderPassId: 0,
                properties: CTX.propertyBlock);
        }

All the traditional methods for drawing renderers in a custom pass require a special object layer, which doesn’t work for my case. I’ve read that you can use ScriptableRenderContext.DrawRenderers to filter with a rendering layer mask, but I couldn’t understand how to properly invoke it.

Is it possible to achieve a per-renderer custom pass?

  1. As a bonus and slightly less important question, is it possible to create depth-based outlines, so that objects in front have their outline rendered properly instead of merging with the other? (like the example on the right):

I imagine that’d require something along the lines of applying fullscreen effect and writing depth on each renderer individually, depth testing and combining results into the buffer, but I have no idea where to even begin with this concept or if it’s even possible. Some directions would be great :slight_smile:

Thank you very much for your time!

Using:

  • Unity 2022.3.48f1
  • HDRP 14.0.11

Hello,

What might help is specifying the shader pass index in your DrawRenderer call. By default it renders all the pass of the shaders which might not work depending on which kind of shaders is used.

To render objects in an offscreen buffer, I’d recommend an unlit shader or shader specially made to render objects in a single pass like mentioned here: Create a Custom Pass GameObject | High Definition RP | 17.0.3

I think there are some custom pass examples in this repo that could help you create your effect: GitHub - alelievr/HDRP-Custom-Passes: A bunch of custom passes made for HDRP especially the outline and TIPS effect that contains an edge detection code.

Regarding the use of the renderingLayerMask, you could try to call the DrawRenderers function in the CustomPassUtils, the last parameter allows you to filter objects using their rendering layer mask:
https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@17.0/api/UnityEngine.Rendering.HighDefinition.CustomPassUtils.html#UnityEngine_Rendering_HighDefinition_CustomPassUtils_DrawRenderers_UnityEngine_Rendering_HighDefinition_CustomPassContext__UnityEngine_LayerMask_UnityEngine_Rendering_HighDefinition_CustomPass_RenderQueueType_UnityEngine_Material_System_Int32_UnityEngine_Rendering_RenderStateBlock_UnityEngine_Rendering_SortingCriteria_System_UInt32_

Hey Antoinel, thanks a lot for your response.

I’ve tried specifying the shader pass before, without much success. I’ve revisited it after your suggestion though, and these are my findings. For context, I’m using HDRP Unlit authored in Shader Graph for the renderers.

  • No shader pass other than -1 works. However, if I remove the depth buffer from SetRenderTarget, I’m able to properly use ForwardOnly, and this fixes the backface culling issue. Almost perfect, except I still can’t do depth testing like Draw Renderers, nor create a depth buffer(?).

  • I’ve nailed down the alpha issue to CoreUtils.CreateEngineMaterial. Passing a reference to a material asset, even the direct one from shader graph, fixes the issue. But creating the material causes the undesired behavior with alpha. I tried debugging the differences between the two but they’re entirely identical, so I wonder what’s going on there. Maybe something isn’t initializing in time?

  • In regards to renderingLayerMask, it’s not an option in HDRP 14, so it’s a no-go for me. :frowning:

So definitely nudged in the right direction, with depth testing remaining the biggest issue. Any ideas?

You can customize the lit shader to include another for example outline pass, and disable/enable the pass in code. Then you can render all the material with outline pass enable in custom pass using render list.

For the HDRP Unlit shader, you should be using the “ForwardOnly” pass, you can find its index using Material.FindPass(“ForwardOnly”).

The problem you have with the depth test might come from the depth comparison mode in the shader, can you try to override it in the state block of the DrawRenderers? Like in this example: Unity - Scripting API: RenderStateBlock
The correct value should be: depth write to true and depth compare to less equal.

With HDRP 14, the rendering layer mask is inside the FilteringSettings which you need to provide if you manually call DrawRenderers() from the ScriptableRenderContext: Unity - Scripting API: FilteringSettings

Still no dice with RenderStateBlock. I used CustomPassUtils.DrawRenderers, but I can’t get depth testing to work. It works fine when using the built-in Draw Renderers Pass.

        protected override void Execute(CustomPassContext CTX)
        {
            if (RendererMaterial == null)
                return;

            CoreUtils.SetRenderTarget(CTX.cmd, highlightBuffer, ClearFlag.Color);

            var renderState = new RenderStateBlock(RenderStateMask.Everything);
            renderState.depthState = new DepthState(true, CompareFunction.LessEqual);

            CustomPassUtils.DrawRenderers(CTX, debugLayer, overrideMaterial: RendererMaterial, overrideRenderState: renderState);

            CTX.propertyBlock.SetTexture("_HighlightBuffer", highlightBuffer);
            CTX.propertyBlock.SetInt("_OutlineIterations", outlineIterations);
            CTX.propertyBlock.SetFloat("_OutlineIntensity", outlineIntensity);
            CTX.propertyBlock.SetFloat("_OutlineWidth", outlineWidth);

            CoreUtils.DrawFullScreen(CTX.cmd, fullscreenMaterial, CTX.cameraColorBuffer, CTX.cameraDepthBuffer, shaderPassId: 0, properties: CTX.propertyBlock);
        }