Blit material color works in editor and certain builds but not others

Hello all, been trying to fix this problem for two days now and thought I’d try asking here. I have a ScriptableRenderPass that takes a SpriteDefault material, and is using that material to blit its color. I’m using this to basically create a fade in / out effect, the kind you might have seen on old NES games. However, while it works in the Unity editor, and the Windows build works on my Windows desktop PC, the Mac build works on one of my Macbooks, but not the other…

Here is the code for the renderer feature and scriptable render pass below. I left in the comments that I put in to help debug (particularly in the Execute() function), so I can confirm that on the Mac build in question, the renderer handlers, material, shader etc are indeed not null.

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

namespace Rendering_RendererFeature
{

    public class TintColorRenderPassFeature : ScriptableRendererFeature
    {
        public class TintColorRenderPass : ScriptableRenderPass
        {

            public RenderTargetIdentifier SourceRendererTarget;
            private Material _TheMaterial;
            private RenderTargetHandle _TempRenderTargetHandle;
            private bool _enabled = false;

            private static TintColorRenderPass _Instance;

            public TintColorRenderPass(Material NewMaterial)
            {
                this._TheMaterial = NewMaterial;

                this._TempRenderTargetHandle.Init("TemporaryColorTextureToExample");

                _Instance = this;
            }


            public static TintColorRenderPass getInstance()
            {
                return _Instance;
            }


            public bool isEnabled()
            {
                return this._enabled;
            }


            public void enable()
            {
                this._enabled = true;
                // Debug.Log("<color=#00FF00> TINT COLOR RENDER PASS ENABLED</color>");
            }


            public void disable()
            {
                this._enabled = false;
                // Debug.Log("<color=#FF0000> TINT COLOR RENDER PASS DISABLED</color>");
            }

            // This method is called before executing the render pass.
            // It can be used to configure render targets and their clear state. Also to create temporary render target textures.
            // When empty this render pass will render to the active camera render target.
            // You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>.
            // The render pipeline will ensure target setup and clearing happens in a performant manner.
            public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
            {
                // When a pass is enqueued (in the RenderPassFeature) all the ScriptableRenderer passes run in
                // a specific order, some calls are consecutive with each pass and some calls are not. You can't
                // guarantee that the source target you set before the passes are executed exist or will be the same
                // target by the time your execute is called. You should assign the target every time in Configure or
                // OnCameraSetup.
                this.SourceRendererTarget = renderingData.cameraData.renderer.cameraColorTarget;
            }

            // Here you can implement the rendering logic.
            // Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
            // https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
            // You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
            public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
            {
                Debug.Log("<color=cyan>RENDERING W/ TINT COLOR RENDER PASS</color>");
                if (this._TheMaterial != null) {
                    Debug.Log("<color=cyan>TINT COLOR RENDER PASS has material!</color>");
                    if (this._TheMaterial.shader != null) {
                        Debug.Log("<color=cyan>TINT COLOR RENDER PASS has material SHADER!</color>");
                    } else {
                        Debug.Log("<color=red>TINT COLOR RENDER PASS is missing material SHADER!</color>");
                    }
                } else {
                    Debug.Log("<color=red>TINT COLOR RENDER PASS is MISSING material!</color>");
                }

                if (this._TempRenderTargetHandle != null) {
                    Debug.Log("<color=cyan>Found TempRenderTargetHandle!</color>");
                } else {
                    Debug.Log("<color=red>DID NOT find TempRenderTargetHandle!</color>");
                }

                // We can give the command buffer any name we want (easier to see on Frame Debug list)
                // This command buffer holds the list of rendering commands to execute.
                CommandBuffer TheCommandBuffer = CommandBufferPool.Get("TintColorRenderPass");
                TheCommandBuffer.Clear();

                // Can't blit from / to same target, so need to use temporary render target in-between
                TheCommandBuffer.GetTemporaryRT(this._TempRenderTargetHandle.id, renderingData.cameraData.cameraTargetDescriptor);

                Blit(TheCommandBuffer, this.SourceRendererTarget, this._TempRenderTargetHandle.Identifier(), this._TheMaterial);
                Blit(TheCommandBuffer, this._TempRenderTargetHandle.Identifier(), this.SourceRendererTarget);

                context.ExecuteCommandBuffer(TheCommandBuffer);

                TheCommandBuffer.Clear();
                CommandBufferPool.Release(TheCommandBuffer);
            }


            // called after Execute, use it to clean up anything allocated in Configure
            public override void FrameCleanup(CommandBuffer cmd)
            {
                cmd.ReleaseTemporaryRT(this._TempRenderTargetHandle.id);
            }

            // Cleanup any allocated resources that were created during the execution of this render pass.
            public override void OnCameraCleanup(CommandBuffer cmd)
            {
            }

            public void setColor(Color NewColor)
            {
                // this is for shader
                // this._TheMaterial.SetColor("_TintColor", NewColor);

                // this for SpriteDefault
                this._TheMaterial.SetColor("_Color", NewColor);
            }
        } // end TintColorRenderPass

        [System.Serializable]
        public class RenderPassSettings {
            public Material MaterialToUse = null;
        }

        public RenderPassSettings CustomSettings = new RenderPassSettings();

        TintColorRenderPass m_ScriptablePass;

        /// <inheritdoc/>
        public override void Create()
        {
            m_ScriptablePass = new TintColorRenderPass(this.CustomSettings.MaterialToUse);

            // Configures where the render pass should be injected.
            // m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
            m_ScriptablePass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
        }

        // Here you can inject one or multiple render passes in the renderer.
        // This method is called when setting up the renderer once per-camera.
        // Called every frame once per-camera
        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            if (!this.m_ScriptablePass.isEnabled()) {
                return;
            }
          
            // m_ScriptablePass.SourceRendererTarget = renderer.cameraColorTarget;
            renderer.EnqueuePass(m_ScriptablePass);
        }


        public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
        {
            m_ScriptablePass.SourceRendererTarget = renderer.cameraColorTarget;
        }

    } // end TintColorRenderPassFeature
}

To use this, I’m basically enabling / disabling the TintColorRenderPass from another class (UIUtilities, pasted below), and then calling setColor at intervals to get the staggered fading effect I’d like.

using System.Collections;
using Player;
using ResourceLoad;
using UnityEngine;
using static Rendering_RendererFeature.TintColorRenderPassFeature;


namespace GameUI
{

    // Monobehavior so we can use Invoke / StartCoroutine etc
    public class UIUtilities : MonoBehaviour
    {

        private Color _FadeOutStep1Color = new Color(0.5f, 0.0f, 1.0f);
        private Color _FadeOutStep2Color = new Color(0.0f, 0.0f, 0.75f);
        private Color _FadeOutStep3Color = new Color(0.0f, 0.0f, 0.25f);
        private Color _FadeOutColor = Color.black;
        private Color _FadeInColor = Color.white;

        private static float _TIME_BETWEEN_FADE_COLORS = 0.11f;
        private static float _TIME_BETWEEN_FADE_COLORS_QUICK = 0.05f;

        private static UIUtilities _Instance;


        public void Awake()
        {
            _Instance = this;

            Cursor.visible = false;
        }


        public void Start()
        {
          
        }


        public static UIUtilities getInstance()
        {
            return _Instance;
        }


        public void OnApplicationQuit()
        {
            this.doResetLogic();
        }


        public float fadeOutWithDelay(float delay)
        {
            return this._fadeOutWithDelay(delay, _TIME_BETWEEN_FADE_COLORS);
        }


        public float fadeInWithDelay(float delay)
        {
            return this._fadeInWithDelay(delay, _TIME_BETWEEN_FADE_COLORS);
        }


        public float fadeOutWithDelayQuick(float delay)
        {
            return this._fadeOutWithDelay(delay, _TIME_BETWEEN_FADE_COLORS_QUICK);
        }


        public float fadeInWithDelayQuick(float delay)
        {
            return this._fadeInWithDelay(delay, _TIME_BETWEEN_FADE_COLORS_QUICK);
        }


        public IEnumerator fadeToColor(Color NewColor, float delay)
        {
            yield return new WaitForSeconds(delay);

            TintColorRenderPass TheRenderPass = TintColorRenderPass.getInstance();
            TheRenderPass.setColor(NewColor);

            yield return null;
        }


        public void enableTintColorRenderPass()
        {
            TintColorRenderPass TheRenderPass = TintColorRenderPass.getInstance();
            TheRenderPass.enable();
        }


        public void disableTintColorRenderPass()
        {
            TintColorRenderPass TheRenderPass = TintColorRenderPass.getInstance();
            TheRenderPass.disable();
        }


        public void doResetLogic()
        {
            TintColorRenderPass TheRenderPass = TintColorRenderPass.getInstance();
            TheRenderPass.setColor(Color.white);
        }


        private float _fadeOutWithDelay(float delay, float timeBeweenFadeColors)
        {
            this.enableTintColorRenderPass();

            // Color Magenta = new Color(1.0f, 0.0f, 1.0f, 1.0f);
            float totalDelay = delay;
            totalDelay += timeBeweenFadeColors;
            totalDelay += timeBeweenFadeColors * 2;
            totalDelay += timeBeweenFadeColors * 3;

            StartCoroutine(fadeToColor(this._FadeOutStep1Color, (delay * Time.timeScale)));
            StartCoroutine(fadeToColor(this._FadeOutStep2Color, (delay + timeBeweenFadeColors) * Time.timeScale));
            StartCoroutine(fadeToColor(this._FadeOutStep3Color, (delay + timeBeweenFadeColors * 2) * Time.timeScale));
            StartCoroutine(fadeToColor(this._FadeOutColor, (delay + timeBeweenFadeColors * 3) * Time.timeScale));
          
            // if (disableColorTintAfterFade) {
            //     Invoke("disableTintColorRenderPass", (totalDelay + 0.5f) * Time.timeScale);
            // }

            // Invoker.InvokeDelayed(disableTintColorRenderPass, totalDelay + 0.5f);

            // must include the extra time delay from disableTintColorRenderPass
            return (totalDelay + 0.15f) * Time.timeScale;
        }


        private float _fadeInWithDelay(float delay, float timeBeweenFadeColors)
        {
            this.enableTintColorRenderPass();

            // Color Magenta = new Color(1.0f, 0.0f, 1.0f, 1.0f);
            float totalDelay = delay;
            totalDelay += timeBeweenFadeColors;
            totalDelay += timeBeweenFadeColors * 2;
            totalDelay += timeBeweenFadeColors * 3;

            StartCoroutine(fadeToColor(this._FadeOutStep3Color, (delay * Time.timeScale)));
            StartCoroutine(fadeToColor(this._FadeOutStep2Color, (delay + timeBeweenFadeColors) * Time.timeScale));
            StartCoroutine(fadeToColor(this._FadeOutStep1Color, (delay + timeBeweenFadeColors * 2) * Time.timeScale));
            StartCoroutine(fadeToColor(this._FadeInColor, (delay + timeBeweenFadeColors * 3) * Time.timeScale));

            Invoke("disableTintColorRenderPass", (totalDelay + 0.15f) * Time.timeScale);
          
            // Invoker.InvokeDelayed(disableTintColorRenderPass, totalDelay + 0.5f);

            // must include the extra time delay from disableTintColorRenderPass
            return (totalDelay + 0.15f) * Time.timeScale;
        }
    }


}

So as I said, the result is that it works perfectly in the editor, as well as on builds for my Windows desktop and my older Macbook from 2017. However, it’s not working on another Macbook I got in 2021. On this one, whenever I blit, it’s just causing the screen to go completely black, instead of tinting the color it’s supposed to be. When I take out the blit lines, it obviously doesn’t work, but the screen doesn’t go black, so I’ve narrowed it down to those two lines (but obviously the solution could lie anywhere).

Here’s a couple of recordings of how it should look… apologies for the video downloads, but I couldn’t think of an easier way to display it for those who might be interested.

https://cdn.discordapp.com/attachments/698253713487167539/1171977477988569108/tint-color-render-pass-example.mov?ex=655ea415&is=654c2f15&hm=dde5b85d6af78577ccef045a533df50845a2424ef5dab9e0abe31cb595a6c6ea&

vs. how it looks on the problem Macbook - note the screen just goes black when it’s supposed to be fading:

https://cdn.discordapp.com/attachments/698253713487167539/1171978324201971812/tint-color-render-pass-broken.mov?ex=655ea4de&is=654c2fde&hm=0535128a7c017b12f3f34b32d735b9b1cfc6825d16fa000c241479419e7850ad&

If it helps, my project is 2D, and I’m using the Universal Render Pipeline.

Any ideas for things I can try? It’s frustrating that it is working in both editor and Mac and Windows builds, just not this one particular Macbook. I haven’t seen any errors on the build logs. Having said that, I’m a total noob with all this render pass stuff, as well as shaders, so any help would be appreciated.

I am aware that some of the way the render pass class is doing stuff is deprecated, and maybe I should be using the RTHandle class, but I tried that and couldn’t get the fade working even in the Editor. I wrote this a while ago and I’m not even sure where I got the original code from, but only noticed the problem recently as I started testing more on actual builds.

Well, it took a few days but I found a solution! This thread here URP 2D Renderer custom feature led me to this github repo for a blit render pass that gave me what I needed: GitHub - Cyanilux/URP_BlitRenderFeature: Blit Render Feature for Universal RP's Forward Renderer. Set specific source/destination via camera source, ID string or RenderTexture asset. Also options for _InverseView matrix and _CameraNormalsTexture generation..

Specifically it seems like the code in this blit render pass takes into account whatever version of Unity you’re using and makes the blit accordingly. In all my searches over the past few days, I had a lot of trouble figuring out what the right way was to do this fade-blit for my project which is:

  1. 2D
  2. Using the URP
  3. Using a simple material if possible (and just setting the color on said material).

Going by the posts on that thread, and my searches in general, it seems like I’m not the only one having trouble figuring the proper way to do a blit for 2D. Would be nice to have some straightforward documentation as all the official Unity examples for this kind of thing seem to be in 3D.

Anyway, I’m very thankful to have found this as my blit-fade now works on my Mac builds. Hopefully it’s helpful to more people in the future.