Skip to main content
Bounty Awarded with 100 reputation awarded by Charles Nough
added 379 characters in body
Source Link
PepeOjeda
  • 893
  • 3
  • 7

EDIT: AlrightAlright, I sort of lost an entire afternoon to this, but I believe I've got it nailed down now.

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

[CreateAssetMenu]
public class GammaUIFix : ScriptableRendererFeature
{
    // We are not allowed to access the camera target from rendererFeature, only inside the pass
    // so, in order to tell the pass to use the camera color target as source/target, we can use this enum
    public enum RTHandleType { Custom, CameraColor }

    [SerializeField] LayerMask layerMask;
    [SerializeField] Material toGammaMat;
    [SerializeField] Material toLinearMat;
    RTHandle intermediateRT;
    RTHandle customDepthAndStencil;

    public GammaUIFix()
    { }

    public override void Create()
    {
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (renderingData.cameraData.cameraType != CameraType.Game)
            return;
            
        RenderTextureDescriptor textureDescriptorcolorDescriptor = new(Screen.width, Screen.height, RenderTextureFormat.Default, 0);
        RenderingUtils.ReAllocateIfNeeded(ref intermediateRT, colorDescriptor, name: "intermediate");
        RenderTextureDescriptor depthDescriptor = new(Screen.width, Screen.height, RenderTextureFormat.DefaultDepth, 032);
        RenderingUtils.ReAllocateIfNeeded(ref intermediateRTcustomDepthAndStencil, textureDescriptordepthDescriptor, name: "customDepth");

        BlitPass toGamma = new(new BlitPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            material = toGammaMat,
            sourceType = RTHandleType.CameraColor,

            targetType = RTHandleType.Custom,
            target = intermediateRT
        });

        DrawRenderersPass drawUI = new(new DrawRenderersPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            layerMask = layerMask,
            target = intermediateRT,
            depthAndStencil = customDepthAndStencil
        });

        BlitPass toLinear = new(new BlitPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            material = toLinearMat,

            sourceType = RTHandleType.Custom,
            source = intermediateRT,

            targetType = RTHandleType.CameraColor
        });

        renderer.EnqueuePass(toGamma);
        renderer.EnqueuePass(drawUI);
        renderer.EnqueuePass(toLinear);
    }

    protected override void Dispose(bool disposing)
    {
        if (intermediateRT != null && disposing?.Release();
            intermediateRTcustomDepthAndStencil?.Release();
    }

    //Draws all renderers of a specific layer into target RTHandle, even if that layer is culled by the camera
    class DrawRenderersPass : ScriptableRenderPass
    {
        Settings settings;
        List<ShaderTagId> forwardOnlyShaderTagIds = new()
        {
            new ShaderTagId("SRPDefaultUnlit"),
            new ShaderTagId("UniversalForward"),
            new ShaderTagId("UniversalForwardOnly"), // Legacy shaders (do not have a gbuffer pass) are considered forward-only for backward compatibility
        };


        public DrawRenderersPass(Settings _settings)
        {
            this.settings = _settings;
            renderPassEvent = _settings.renderPassEvent;
        }

        public override void ConfigureOnCameraSetup(CommandBuffer cmd, RenderTextureDescriptorref cameraTextureDescriptorRenderingData renderingData)
        {
            base.Configure(cmd, cameraTextureDescriptor);
            ConfigureTarget(settings.target, settings.depthAndStencil);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("Draw Renderers Pass");

            DrawingSettings drawingSettings = CreateDrawingSettings(forwardOnlyShaderTagIds, ref renderingData, SortingCriteria.CommonTransparent);
            FilteringSettings filteringSettings = new(RenderQueueRange.all, settings.layerMask);
            RenderStateBlock renderStateBlock = new(RenderStateMask.Nothing);

            renderingData.cameraData.camera.TryGetCullingParameters(out ScriptableCullingParameters cullingParameters);
            cullingParameters.cullingMask = (uint)(int)settings.layerMask;
            context.DrawRenderers(
                context.Cull(ref cullingParameters),
                ref drawingSettings,
                ref filteringSettings,
                ref renderStateBlock);


            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
            CommandBufferPool.Release(cmd);
        }

        public override void FrameCleanup(CommandBuffer cmd)
        {
            if (cmd == null)
                throw new ArgumentNullException("cmd");


            base.FrameCleanup(cmd);
        }

        [System.Serializable]
        public class Settings
        {
            public RenderPassEvent renderPassEvent;
            public RTHandle target;
            public RTHandle depthAndStencil;
            public LayerMask layerMask;
        }
    }

    //blits from source to target using the provided material
    class BlitPass : ScriptableRenderPass
    {
        Settings settings;


        public BlitPass(Settings _settings)
        {
            this.settings = _settings;
            renderPassEvent = _settings.renderPassEvent;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("Blit Pass");
            RTHandle source;
            if (settings.sourceType == RTHandleType.CameraColor)
                source = renderingData.cameraData.renderer.cameraColorTargetHandle;
            else
                source = settings.source;

            RTHandle target;
            if (settings.targetType == RTHandleType.CameraColor)
                target = renderingData.cameraData.renderer.cameraColorTargetHandle;
            else
                target = settings.target;

            //This seems to happen when selecting certain objects in the inspector? I have no idea, man
            if (source.rt == null || target.rt == null)
                return;

            if (source == target)
            {
                Debug.LogError("Cannot blit from source to itself! Use an intermediate target");
                return;
            }

            Blitter.BlitCameraTexture(cmd, source, target, settings.material, 0);
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
            CommandBufferPool.Release(cmd);
        }

        public override void FrameCleanup(CommandBuffer cmd)
        {
            if (cmd == null)
                throw new ArgumentNullException("cmd");
            base.FrameCleanup(cmd);
        }

        [System.Serializable]
        public class Settings
        {
            public RenderPassEvent renderPassEvent;
            public Material material;

            public RTHandleType sourceType;
            public RTHandle source;

            public RTHandleType targetType;
            public RTHandle target;
        }
    }

    //-------------------------------------------------------------------------
}

Although I should mention at one point I got a mysterious persistent error aboutAlthough I should mention at one point I got a mysterious persistent error about the RTHandle being null that then disappeared upon restarting the editor. I don't know what that was about, and cannot guarantee it won't happen again :(

EDIT (again): Fixed the pesky RTHandle being null that then disappeared upon restartingerrors and got masks to work by adding a custom depth/stencil buffer to the editorDrawRenderers pass. I don't know what that was about, and cannot guarantee it won't happen again :(

EDIT: Alright, I sort of lost an entire afternoon to this, but I believe I've got it nailed down now.

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

[CreateAssetMenu]
public class GammaUIFix : ScriptableRendererFeature
{
    // We are not allowed to access the camera target from rendererFeature, only inside the pass
    // so, in order to tell the pass to use the camera color target as source/target, we can use this enum
    public enum RTHandleType { Custom, CameraColor }

    [SerializeField] LayerMask layerMask;
    [SerializeField] Material toGammaMat;
    [SerializeField] Material toLinearMat;
    RTHandle intermediateRT;

    public GammaUIFix()
    { }

    public override void Create()
    {
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        RenderTextureDescriptor textureDescriptor = new(Screen.width,
            Screen.height, RenderTextureFormat.Default, 0);
        RenderingUtils.ReAllocateIfNeeded(ref intermediateRT, textureDescriptor);

        BlitPass toGamma = new(new BlitPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            material = toGammaMat,
            sourceType = RTHandleType.CameraColor,

            targetType = RTHandleType.Custom,
            target = intermediateRT
        });

        DrawRenderersPass drawUI = new(new DrawRenderersPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            layerMask = layerMask,
            target = intermediateRT
        });

        BlitPass toLinear = new(new BlitPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            material = toLinearMat,

            sourceType = RTHandleType.Custom,
            source = intermediateRT,

            targetType = RTHandleType.CameraColor
        });

        renderer.EnqueuePass(toGamma);
        renderer.EnqueuePass(drawUI);
        renderer.EnqueuePass(toLinear);
    }

    protected override void Dispose(bool disposing)
    {
        if (intermediateRT != null && disposing)
            intermediateRT.Release();
    }

    //Draws all renderers of a specific layer into target RTHandle, even if that layer is culled by the camera
    class DrawRenderersPass : ScriptableRenderPass
    {
        Settings settings;
        List<ShaderTagId> forwardOnlyShaderTagIds = new()
        {
            new ShaderTagId("SRPDefaultUnlit"),
            new ShaderTagId("UniversalForward"),
            new ShaderTagId("UniversalForwardOnly"), // Legacy shaders (do not have a gbuffer pass) are considered forward-only for backward compatibility
        };


        public DrawRenderersPass(Settings _settings)
        {
            this.settings = _settings;
            renderPassEvent = _settings.renderPassEvent;
        }

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            base.Configure(cmd, cameraTextureDescriptor);
            ConfigureTarget(settings.target);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("Draw Renderers Pass");

            DrawingSettings drawingSettings = CreateDrawingSettings(forwardOnlyShaderTagIds, ref renderingData, SortingCriteria.CommonTransparent);
            FilteringSettings filteringSettings = new(RenderQueueRange.all, settings.layerMask);
            RenderStateBlock renderStateBlock = new(RenderStateMask.Nothing);

            renderingData.cameraData.camera.TryGetCullingParameters(out ScriptableCullingParameters cullingParameters);
            cullingParameters.cullingMask = (uint)(int)settings.layerMask;
            context.DrawRenderers(
                context.Cull(ref cullingParameters),
                ref drawingSettings,
                ref filteringSettings,
                ref renderStateBlock);


            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }

        public override void FrameCleanup(CommandBuffer cmd)
        {
            if (cmd == null)
                throw new ArgumentNullException("cmd");


            base.FrameCleanup(cmd);
        }

        [System.Serializable]
        public class Settings
        {
            public RenderPassEvent renderPassEvent;
            public RTHandle target;
            public LayerMask layerMask;
        }
    }

    //blits from source to target using the provided material
    class BlitPass : ScriptableRenderPass
    {
        Settings settings;


        public BlitPass(Settings _settings)
        {
            this.settings = _settings;
            renderPassEvent = _settings.renderPassEvent;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("Blit Pass");
            RTHandle source;
            if (settings.sourceType == RTHandleType.CameraColor)
                source = renderingData.cameraData.renderer.cameraColorTargetHandle;
            else
                source = settings.source;

            RTHandle target;
            if (settings.targetType == RTHandleType.CameraColor)
                target = renderingData.cameraData.renderer.cameraColorTargetHandle;
            else
                target = settings.target;


            if (source == target)
            {
                Debug.LogError("Cannot blit from source to itself! Use an intermediate target");
                return;
            }

            Blitter.BlitCameraTexture(cmd, source, target, settings.material, 0);
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }

        public override void FrameCleanup(CommandBuffer cmd)
        {
            if (cmd == null)
                throw new ArgumentNullException("cmd");
            base.FrameCleanup(cmd);
        }

        [System.Serializable]
        public class Settings
        {
            public RenderPassEvent renderPassEvent;
            public Material material;

            public RTHandleType sourceType;
            public RTHandle source;

            public RTHandleType targetType;
            public RTHandle target;
        }
    }

    //-------------------------------------------------------------------------
}

Although I should mention at one point I got a mysterious persistent error about the RTHandle being null that then disappeared upon restarting the editor. I don't know what that was about, and cannot guarantee it won't happen again :(

Alright, I sort of lost an entire afternoon to this, but I believe I've got it nailed down now.

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

[CreateAssetMenu]
public class GammaUIFix : ScriptableRendererFeature
{
    // We are not allowed to access the camera target from rendererFeature, only inside the pass
    // so, in order to tell the pass to use the camera color target as source/target, we can use this enum
    public enum RTHandleType { Custom, CameraColor }

    [SerializeField] LayerMask layerMask;
    [SerializeField] Material toGammaMat;
    [SerializeField] Material toLinearMat;
    RTHandle intermediateRT;
    RTHandle customDepthAndStencil;

    public GammaUIFix()
    { }

    public override void Create()
    {
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (renderingData.cameraData.cameraType != CameraType.Game)
            return;
            
        RenderTextureDescriptor colorDescriptor = new(Screen.width, Screen.height, RenderTextureFormat.Default, 0);
        RenderingUtils.ReAllocateIfNeeded(ref intermediateRT, colorDescriptor, name: "intermediate");
        RenderTextureDescriptor depthDescriptor = new(Screen.width, Screen.height, RenderTextureFormat.Depth, 32);
        RenderingUtils.ReAllocateIfNeeded(ref customDepthAndStencil, depthDescriptor, name: "customDepth");

        BlitPass toGamma = new(new BlitPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            material = toGammaMat,
            sourceType = RTHandleType.CameraColor,

            targetType = RTHandleType.Custom,
            target = intermediateRT
        });

        DrawRenderersPass drawUI = new(new DrawRenderersPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            layerMask = layerMask,
            target = intermediateRT,
            depthAndStencil = customDepthAndStencil
        });

        BlitPass toLinear = new(new BlitPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            material = toLinearMat,

            sourceType = RTHandleType.Custom,
            source = intermediateRT,

            targetType = RTHandleType.CameraColor
        });

        renderer.EnqueuePass(toGamma);
        renderer.EnqueuePass(drawUI);
        renderer.EnqueuePass(toLinear);
    }

    protected override void Dispose(bool disposing)
    {
        intermediateRT?.Release();
        customDepthAndStencil?.Release();
    }

    //Draws all renderers of a specific layer into target RTHandle, even if that layer is culled by the camera
    class DrawRenderersPass : ScriptableRenderPass
    {
        Settings settings;
        List<ShaderTagId> forwardOnlyShaderTagIds = new()
        {
            new ShaderTagId("SRPDefaultUnlit"),
            new ShaderTagId("UniversalForward"),
            new ShaderTagId("UniversalForwardOnly"), // Legacy shaders (do not have a gbuffer pass) are considered forward-only for backward compatibility
        };


        public DrawRenderersPass(Settings _settings)
        {
            this.settings = _settings;
            renderPassEvent = _settings.renderPassEvent;
        }

        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
        {
            ConfigureTarget(settings.target, settings.depthAndStencil);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("Draw Renderers Pass");

            DrawingSettings drawingSettings = CreateDrawingSettings(forwardOnlyShaderTagIds, ref renderingData, SortingCriteria.CommonTransparent);
            FilteringSettings filteringSettings = new(RenderQueueRange.all, settings.layerMask);
            RenderStateBlock renderStateBlock = new(RenderStateMask.Nothing);

            renderingData.cameraData.camera.TryGetCullingParameters(out ScriptableCullingParameters cullingParameters);
            cullingParameters.cullingMask = (uint)(int)settings.layerMask;
            context.DrawRenderers(
                context.Cull(ref cullingParameters),
                ref drawingSettings,
                ref filteringSettings,
                ref renderStateBlock);


            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
            CommandBufferPool.Release(cmd);
        }

        [System.Serializable]
        public class Settings
        {
            public RenderPassEvent renderPassEvent;
            public RTHandle target;
            public RTHandle depthAndStencil;
            public LayerMask layerMask;
        }
    }

    //blits from source to target using the provided material
    class BlitPass : ScriptableRenderPass
    {
        Settings settings;


        public BlitPass(Settings _settings)
        {
            this.settings = _settings;
            renderPassEvent = _settings.renderPassEvent;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("Blit Pass");
            RTHandle source;
            if (settings.sourceType == RTHandleType.CameraColor)
                source = renderingData.cameraData.renderer.cameraColorTargetHandle;
            else
                source = settings.source;

            RTHandle target;
            if (settings.targetType == RTHandleType.CameraColor)
                target = renderingData.cameraData.renderer.cameraColorTargetHandle;
            else
                target = settings.target;

            //This seems to happen when selecting certain objects in the inspector? I have no idea, man
            if (source.rt == null || target.rt == null)
                return;

            if (source == target)
            {
                Debug.LogError("Cannot blit from source to itself! Use an intermediate target");
                return;
            }

            Blitter.BlitCameraTexture(cmd, source, target, settings.material, 0);
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
            CommandBufferPool.Release(cmd);
        }

        [System.Serializable]
        public class Settings
        {
            public RenderPassEvent renderPassEvent;
            public Material material;

            public RTHandleType sourceType;
            public RTHandle source;

            public RTHandleType targetType;
            public RTHandle target;
        }
    }

    //-------------------------------------------------------------------------
}

Although I should mention at one point I got a mysterious persistent error about the RTHandle being null that then disappeared upon restarting the editor. I don't know what that was about, and cannot guarantee it won't happen again :(

EDIT (again): Fixed the pesky RTHandle errors and got masks to work by adding a custom depth/stencil buffer to the DrawRenderers pass.

added 9733 characters in body
Source Link
PepeOjeda
  • 893
  • 3
  • 7

You could try using a custom shaderEDIT: Alright, I sort of lost an entire afternoon to this, but I believe I've got it nailed down now.

the image looking correct!

The problem comes down to the fact that correctsdoing arithmetic on colors is a finicky thing, because the result entirely depends on the color space being used. In this case, interpolating (blending, with the alpha for your) between two colors in gamma space will produce a perceptually different color than blending in linear space, even if both colors being interpolated are perceptually identical.

So, the color that photoshop outputs is the result of interpolating in gamma space. To get the same color in Unity, you need to get everything into gamma space and have the interpolation take place there.

That's why moving the texture to linear color was not working correctly: both the scene render and the texture were in the same color space, sure, but not in the correct one!

So, the solution is to take four steps:

  • Render everything except UI normally.
  • Blit this image into gamma space.
  • Render the UI without applying gamma correction.
  • Blit the result into linear space so things look normal.

The first step just requires removing the UI elementslayer from the cullingMask of the camera, and Unity handles the rest. I believe this should work

The meat and potatoes of the process, then, is a render feature that creates three passes for the remaining steps:

Shaderusing "Unlit/GammaCorrected"System;
{using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering.Universal;
using UnityEngine.Rendering;
using PropertiesUnityEngine.Rendering.Universal;

[CreateAssetMenu]
public class GammaUIFix : ScriptableRendererFeature
{
    // We are not _MainTexallowed ("Texture"to access the camera target from rendererFeature, 2D)only =inside "white"the pass
    // so, in order to tell the pass to use the camera color target as source/target, we can use this enum
    public enum RTHandleType { Custom, CameraColor } 

    }[SerializeField] LayerMask layerMask;
    SubShader[SerializeField] Material toGammaMat;
    [SerializeField] Material toLinearMat;
    RTHandle intermediateRT;

    public GammaUIFix()
    { }

    public override void Create()
 Tags   { 
 "RenderType"="Transparent"   } 

    public override void AddRenderPasses(ScriptableRenderer Blendrenderer, SrcAlpharef OneMinusSrcAlphaRenderingData renderingData)
    {
        LODRenderTextureDescriptor 100textureDescriptor = new(Screen.width,
            Screen.height, RenderTextureFormat.Default, 0);
        PassRenderingUtils.ReAllocateIfNeeded(ref intermediateRT, textureDescriptor);

        BlitPass toGamma = new(new BlitPass.Settings()
        {
            CGPROGRAMrenderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            #pragmamaterial vertex= verttoGammaMat,
            #pragmasourceType fragment= fragRTHandleType.CameraColor,

            #includetargetType "UnityCG= RTHandleType.cginc"Custom,
            target = intermediateRT
        });

    struct appdata   DrawRenderersPass drawUI = new(new DrawRenderersPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            layerMask = layerMask,
            target = intermediateRT
        });

        BlitPass toLinear = new(new BlitPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
  float4 vertex : POSITION;       material = toLinearMat,

            sourceType = RTHandleType.Custom,
  float2 uv : TEXCOORD0;       source = intermediateRT,

            targetType = RTHandleType.CameraColor
        });

        renderer.EnqueuePass(toGamma);
    struct v2f   renderer.EnqueuePass(drawUI);
        renderer.EnqueuePass(toLinear);
    }

    protected override void Dispose(bool disposing)
    {
        if (intermediateRT != null && disposing)
   float2 uv : TEXCOORD0;      intermediateRT.Release();
    }

    //Draws all renderers of a specific layer into float4target vertexRTHandle, even if that layer is culled by the camera
    class DrawRenderersPass : SV_POSITION;ScriptableRenderPass
    {
        Settings settings;
        List<ShaderTagId> forwardOnlyShaderTagIds = new()
        {
            new ShaderTagId("SRPDefaultUnlit"),
            new ShaderTagId("UniversalForward"),
            new ShaderTagId("UniversalForwardOnly"), // Legacy shaders (do not have a gbuffer pass) are considered forward-only for backward compatibility
        };


        public DrawRenderersPass(Settings _settings)
        {
            sampler2Dthis.settings _MainTex;= _settings;
            float4renderPassEvent _MainTex_ST;= _settings.renderPassEvent;
        }

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
    v2f vert   {
            base.Configure(appdatacmd, vcameraTextureDescriptor);
            ConfigureTarget(settings.target);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("Draw v2fRenderers o;Pass");

            DrawingSettings drawingSettings = CreateDrawingSettings(forwardOnlyShaderTagIds, oref renderingData, SortingCriteria.vertexCommonTransparent);
            FilteringSettings filteringSettings = UnityObjectToClipPosnew(vRenderQueueRange.vertexall, settings.layerMask);
            RenderStateBlock renderStateBlock = new(RenderStateMask.Nothing);

  o          renderingData.uvcameraData.camera.TryGetCullingParameters(out ScriptableCullingParameters cullingParameters);
            cullingParameters.cullingMask = TRANSFORM_TEX(vuint)(int)settings.uvlayerMask;
            context.DrawRenderers(
                context.Cull(ref cullingParameters), 
 _MainTex               ref drawingSettings,
                ref filteringSettings,
                ref renderStateBlock); 


            context.ExecuteCommandBuffer(cmd);
    return o;
       CommandBufferPool.Release(cmd);
        }

        public override void FrameCleanup(CommandBuffer fixed4cmd)
 frag       {
            if (v2fcmd i== null) 
 : SV_Target              throw new ArgumentNullException("cmd");


            base.FrameCleanup(cmd);
        }

        [System.Serializable]
        public class Settings
        {
            public RenderPassEvent renderPassEvent;
            public RTHandle target;
            public LayerMask layerMask;
        }
    }

    //blits samplefrom source to target using the textureprovided material
    class BlitPass : ScriptableRenderPass
    {
        Settings settings;


   fixed4 col    public BlitPass(Settings _settings)
        {
            this.settings = tex2D_settings;
            renderPassEvent = _settings.renderPassEvent;
        }

        public override void Execute(_MainTexScriptableRenderContext context, iref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.uvGet("Blit Pass");
            RTHandle source;
   col         if (settings.sourceType == RTHandleType.CameraColor)
                source = powrenderingData.cameraData.renderer.cameraColorTargetHandle;
            else
                source = settings.source;

            RTHandle target;
            if (col,settings.targetType 1/2== RTHandleType.2CameraColor)
                target = renderingData.cameraData.renderer.cameraColorTargetHandle;
            else
                target = settings.target;


            if (source == target)
            {
                Debug.LogError("Cannot blit from source to itself! Use an intermediate target");
                returnreturn;
 col;           }

            Blitter.BlitCameraTexture(cmd, source, target, settings.material, 0);
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        } 

        public override void FrameCleanup(CommandBuffer ENDCGcmd)
        {
            if (cmd == null)
                throw new ArgumentNullException("cmd");
            base.FrameCleanup(cmd);
        }

        [System.Serializable]
        public class Settings
        {
            public RenderPassEvent renderPassEvent;
            public Material material;

            public RTHandleType sourceType;
            public RTHandle source;

            public RTHandleType targetType;
            public RTHandle target;
        }
    } 

    //-------------------------------------------------------------------------
}

The only important line thereI'm not going to give a blow by blow, because I don't think the code itself is col = pow(col, 1/2.2);very mysterious. There are some slightly confusing details in how you need to interact with the API, which appliesI guess, but the gamma correctiongeneral idea should be clear. Everything else is

You will also need two shaders for the gamma there-and-back-again:

ToGamma

Shader "CustomEffects/ToGamma"
{
    HLSLINCLUDE
    
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        // The Blit.hlsl file provides the vertex shader (Vert),
        // the input structure (Attributes), and the output structure (Varyings)
        #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
        #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"

        float4 ToGamma (Varyings input) : SV_Target
        {
            float3 color = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, input.texcoord).rgb;
            return float4(LinearToSRGB(color), 1);
        }

    ENDHLSL
    
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}
        LOD 100
        ZTest Always ZWrite Off Cull Off
        Pass
        {
            Name "ToGamma"

            HLSLPROGRAM
            
            #pragma vertex Vert
            #pragma fragment ToGamma
            
            ENDHLSL
        }
    }
}

ToLinear

Shader "CustomEffects/ToLinear"
{
    HLSLINCLUDE
    
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        // The Blit.hlsl file provides the vertex shader (Vert),
        // the input structure (Attributes), and the output structure (Varyings)
        #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
        #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"

        float4 ToLinear (Varyings input) : SV_Target
        {
            float3 color = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, input.texcoord).rgb;
            return float4(SRGBToLinear(color), 1);
        }
    
    ENDHLSL
    
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}
        LOD 100
        ZTest Always ZWrite Off Cull Off
        Pass
        {
            Name "ToLinear"

            HLSLPROGRAM
            
            #pragma vertex Vert
            #pragma fragment ToLinear
            
            ENDHLSL
        }
    }
}

Once you have all that, you just boilerplateset up the render feature and you are done! the render feature in the inspector, with the UI layer selected and a reference to the ToGamma and ToLinear materials

Although I should mention at one point I got a mysterious persistent error about the RTHandle being null that then disappeared upon restarting the editor. I don't know what that was about, and cannot guarantee it won't happen again :(

You could try using a custom shader that corrects the alpha for your UI elements. I believe this should work:

Shader "Unlit/GammaCorrected"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                col = pow(col, 1/2.2);
                return col;
            }
            ENDCG
        }
    }
}

The only important line there is col = pow(col, 1/2.2);, which applies the gamma correction. Everything else is just boilerplate.

EDIT: Alright, I sort of lost an entire afternoon to this, but I believe I've got it nailed down now.

the image looking correct!

The problem comes down to the fact that doing arithmetic on colors is a finicky thing, because the result entirely depends on the color space being used. In this case, interpolating (blending, with the alpha) between two colors in gamma space will produce a perceptually different color than blending in linear space, even if both colors being interpolated are perceptually identical.

So, the color that photoshop outputs is the result of interpolating in gamma space. To get the same color in Unity, you need to get everything into gamma space and have the interpolation take place there.

That's why moving the texture to linear color was not working correctly: both the scene render and the texture were in the same color space, sure, but not in the correct one!

So, the solution is to take four steps:

  • Render everything except UI normally.
  • Blit this image into gamma space.
  • Render the UI without applying gamma correction.
  • Blit the result into linear space so things look normal.

The first step just requires removing the UI layer from the cullingMask of the camera, and Unity handles the rest.

The meat and potatoes of the process, then, is a render feature that creates three passes for the remaining steps:

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

[CreateAssetMenu]
public class GammaUIFix : ScriptableRendererFeature
{
    // We are not allowed to access the camera target from rendererFeature, only inside the pass
    // so, in order to tell the pass to use the camera color target as source/target, we can use this enum
    public enum RTHandleType { Custom, CameraColor } 

    [SerializeField] LayerMask layerMask;
    [SerializeField] Material toGammaMat;
    [SerializeField] Material toLinearMat;
    RTHandle intermediateRT;

    public GammaUIFix()
    { }

    public override void Create()
    { 
    } 

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        RenderTextureDescriptor textureDescriptor = new(Screen.width,
            Screen.height, RenderTextureFormat.Default, 0);
        RenderingUtils.ReAllocateIfNeeded(ref intermediateRT, textureDescriptor);

        BlitPass toGamma = new(new BlitPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            material = toGammaMat,
            sourceType = RTHandleType.CameraColor,

            targetType = RTHandleType.Custom,
            target = intermediateRT
        });

        DrawRenderersPass drawUI = new(new DrawRenderersPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            layerMask = layerMask,
            target = intermediateRT
        });

        BlitPass toLinear = new(new BlitPass.Settings()
        {
            renderPassEvent = RenderPassEvent.AfterRenderingTransparents,
            material = toLinearMat,

            sourceType = RTHandleType.Custom,
            source = intermediateRT,

            targetType = RTHandleType.CameraColor
        });

        renderer.EnqueuePass(toGamma);
        renderer.EnqueuePass(drawUI);
        renderer.EnqueuePass(toLinear);
    }

    protected override void Dispose(bool disposing)
    {
        if (intermediateRT != null && disposing)
            intermediateRT.Release();
    }

    //Draws all renderers of a specific layer into target RTHandle, even if that layer is culled by the camera
    class DrawRenderersPass : ScriptableRenderPass
    {
        Settings settings;
        List<ShaderTagId> forwardOnlyShaderTagIds = new()
        {
            new ShaderTagId("SRPDefaultUnlit"),
            new ShaderTagId("UniversalForward"),
            new ShaderTagId("UniversalForwardOnly"), // Legacy shaders (do not have a gbuffer pass) are considered forward-only for backward compatibility
        };


        public DrawRenderersPass(Settings _settings)
        {
            this.settings = _settings;
            renderPassEvent = _settings.renderPassEvent;
        }

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            base.Configure(cmd, cameraTextureDescriptor);
            ConfigureTarget(settings.target);
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("Draw Renderers Pass");

            DrawingSettings drawingSettings = CreateDrawingSettings(forwardOnlyShaderTagIds, ref renderingData, SortingCriteria.CommonTransparent);
            FilteringSettings filteringSettings = new(RenderQueueRange.all, settings.layerMask);
            RenderStateBlock renderStateBlock = new(RenderStateMask.Nothing);

            renderingData.cameraData.camera.TryGetCullingParameters(out ScriptableCullingParameters cullingParameters);
            cullingParameters.cullingMask = (uint)(int)settings.layerMask;
            context.DrawRenderers(
                context.Cull(ref cullingParameters), 
                ref drawingSettings,
                ref filteringSettings,
                ref renderStateBlock); 


            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        }

        public override void FrameCleanup(CommandBuffer cmd)
        {
            if (cmd == null) 
                throw new ArgumentNullException("cmd");


            base.FrameCleanup(cmd);
        }

        [System.Serializable]
        public class Settings
        {
            public RenderPassEvent renderPassEvent;
            public RTHandle target;
            public LayerMask layerMask;
        }
    }

    //blits from source to target using the provided material
    class BlitPass : ScriptableRenderPass
    {
        Settings settings;


        public BlitPass(Settings _settings)
        {
            this.settings = _settings;
            renderPassEvent = _settings.renderPassEvent;
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            CommandBuffer cmd = CommandBufferPool.Get("Blit Pass");
            RTHandle source;
            if (settings.sourceType == RTHandleType.CameraColor)
                source = renderingData.cameraData.renderer.cameraColorTargetHandle;
            else
                source = settings.source;

            RTHandle target;
            if (settings.targetType == RTHandleType.CameraColor)
                target = renderingData.cameraData.renderer.cameraColorTargetHandle;
            else
                target = settings.target;


            if (source == target)
            {
                Debug.LogError("Cannot blit from source to itself! Use an intermediate target");
                return;
            }

            Blitter.BlitCameraTexture(cmd, source, target, settings.material, 0);
            context.ExecuteCommandBuffer(cmd);
            CommandBufferPool.Release(cmd);
        } 

        public override void FrameCleanup(CommandBuffer cmd)
        {
            if (cmd == null)
                throw new ArgumentNullException("cmd");
            base.FrameCleanup(cmd);
        }

        [System.Serializable]
        public class Settings
        {
            public RenderPassEvent renderPassEvent;
            public Material material;

            public RTHandleType sourceType;
            public RTHandle source;

            public RTHandleType targetType;
            public RTHandle target;
        }
    } 

    //-------------------------------------------------------------------------
}

I'm not going to give a blow by blow, because I don't think the code itself is very mysterious. There are some slightly confusing details in how you need to interact with the API, I guess, but the general idea should be clear.

You will also need two shaders for the gamma there-and-back-again:

ToGamma

Shader "CustomEffects/ToGamma"
{
    HLSLINCLUDE
    
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        // The Blit.hlsl file provides the vertex shader (Vert),
        // the input structure (Attributes), and the output structure (Varyings)
        #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
        #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"

        float4 ToGamma (Varyings input) : SV_Target
        {
            float3 color = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, input.texcoord).rgb;
            return float4(LinearToSRGB(color), 1);
        }

    ENDHLSL
    
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}
        LOD 100
        ZTest Always ZWrite Off Cull Off
        Pass
        {
            Name "ToGamma"

            HLSLPROGRAM
            
            #pragma vertex Vert
            #pragma fragment ToGamma
            
            ENDHLSL
        }
    }
}

ToLinear

Shader "CustomEffects/ToLinear"
{
    HLSLINCLUDE
    
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        // The Blit.hlsl file provides the vertex shader (Vert),
        // the input structure (Attributes), and the output structure (Varyings)
        #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
        #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"

        float4 ToLinear (Varyings input) : SV_Target
        {
            float3 color = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, input.texcoord).rgb;
            return float4(SRGBToLinear(color), 1);
        }
    
    ENDHLSL
    
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}
        LOD 100
        ZTest Always ZWrite Off Cull Off
        Pass
        {
            Name "ToLinear"

            HLSLPROGRAM
            
            #pragma vertex Vert
            #pragma fragment ToLinear
            
            ENDHLSL
        }
    }
}

Once you have all that, you just set up the render feature and you are done! the render feature in the inspector, with the UI layer selected and a reference to the ToGamma and ToLinear materials

Although I should mention at one point I got a mysterious persistent error about the RTHandle being null that then disappeared upon restarting the editor. I don't know what that was about, and cannot guarantee it won't happen again :(

Source Link
PepeOjeda
  • 893
  • 3
  • 7

You could try using a custom shader that corrects the alpha for your UI elements. I believe this should work:

Shader "Unlit/GammaCorrected"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                col = pow(col, 1/2.2);
                return col;
            }
            ENDCG
        }
    }
}

The only important line there is col = pow(col, 1/2.2);, which applies the gamma correction. Everything else is just boilerplate.