World Space canvas on top of "everything" ?

Is it possible to render a world space canvas on top of everything? Example, health bars on top of all 3D objects.

You’re going to need a custom shader for your World Space UI objects that has it’s Render Order to Overlay and has ZTest turned off.

This is a copy of the Default UI shader with the necessary changes. Should do the trick. Just make a material with this shader, and apply it to everything you want drawn over the top of geometry in your WorldSpace UI.

Shader "UI/Default_OverlayNoZTest"
{
	Properties
	{
		[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
		_Color ("Tint", Color) = (1,1,1,1)
		
		_StencilComp ("Stencil Comparison", Float) = 8
		_Stencil ("Stencil ID", Float) = 0
		_StencilOp ("Stencil Operation", Float) = 0
		_StencilWriteMask ("Stencil Write Mask", Float) = 255
		_StencilReadMask ("Stencil Read Mask", Float) = 255

		_ColorMask ("Color Mask", Float) = 15
	}

	SubShader
	{
		Tags
		{ 
			"Queue"="Overlay" 
			"IgnoreProjector"="True" 
			"RenderType"="Transparent" 
			"PreviewType"="Plane"
			"CanUseSpriteAtlas"="True"
		}
		
		Stencil
		{
			Ref [_Stencil]
			Comp [_StencilComp]
			Pass [_StencilOp] 
			ReadMask [_StencilReadMask]
			WriteMask [_StencilWriteMask]
		}

		Cull Off
		Lighting Off
		ZWrite Off
		ZTest Off
		Blend SrcAlpha OneMinusSrcAlpha
		ColorMask [_ColorMask]

		Pass
		{
		CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"
			
			struct appdata_t
			{
				float4 vertex   : POSITION;
				float4 color    : COLOR;
				float2 texcoord : TEXCOORD0;
			};

			struct v2f
			{
				float4 vertex   : SV_POSITION;
				fixed4 color    : COLOR;
				half2 texcoord  : TEXCOORD0;
			};
			
			fixed4 _Color;

			v2f vert(appdata_t IN)
			{
				v2f OUT;
				OUT.vertex = mul(UNITY_MATRIX_MVP, IN.vertex);
				OUT.texcoord = IN.texcoord;
#ifdef UNITY_HALF_TEXEL_OFFSET
				OUT.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1);
#endif
				OUT.color = IN.color * _Color;
				return OUT;
			}

			sampler2D _MainTex;

			fixed4 frag(v2f IN) : SV_Target
			{
				half4 color = tex2D(_MainTex, IN.texcoord) * IN.color;
				clip (color.a - 0.01);
				return color;
			}
		ENDCG
		}
	}
}
1 Like

If you have a lot of UI elements visually attached to game elements then you are going to want to use world space. But if you want your UI to look like an overlay. You’re going to want to render your game and the UI separately and then composite them together.

Simple explanation: click to add a new camera, set depth to a high number. Any world space Canvas items (eg, health bars), simply click to use that camera; also set “order in layer” to a high number. That’s it.

In detail, using a layer so you can do them “all at once”:

  1. Put your UI on a Layer called UI (if it isn’t already).
  2. Duplicate your Main Camera and call it UI Camera.
  3. Parent your UI Camera to the Main Camera.
  4. Remove scripts like camera control, post effects and audio listeners from the UI Camera.
  5. In the Main Camera Culling Mask turn off UI
  6. In the UI Camera turn everything off but UI.
  7. In the UI Camera change Clear Flags to Depth only
  8. In the UI Camera change the Depth to something higher than the value on your Main Camera.
  9. Then in your Canvas set the Event Camera to the UI Camera.

This image is of the Scene view

39681-2015-01-28-09-19-49-unity-shipyardunity-murphy-sta.png

While this is what it looks like in game (please ignore the test graphics)

Other answers involve copying Unity’s built-in UI shader and then overriding the ZTest value. Because Unity added a property for setting ZTest to the UI shaders, you can actually solve this with just a line of code. That way, you can always use Unity’s latest shaders without having to hard code anything.

Here’s an example script showing how to override the ZTest value so your UI always renders on top. Attach it to your canvas and it should update the UI elements automatically.

In a nutshell, I’m just making a copy of Unity’s default rendering material, then changing the value of ZTest on the copy, then applying it back to the graphic. This should work for any UI graphic - text, etc. (Note: this doesn’t work for TextMeshPro specifically - they are missing the keyword needed, and instead have a separate shader called ‘Overlay’ that does the same thing.)

[ExecuteInEditMode] //Disable if you don't care about previewing outside of play mode
public class WorldSpaceOverlayUI : MonoBehaviour
{
    private const string shaderTestMode = "unity_GUIZTestMode"; //The magic property we need to set
    [SerializeField] UnityEngine.Rendering.CompareFunction desiredUIComparison = UnityEngine.Rendering.CompareFunction.Always; //If you want to try out other effects
    [Tooltip("Set to blank to automatically populate from the child UI elements")]
    [SerializeField] Graphic[] uiElementsToApplyTo;

    //Allows us to reuse materials
    private Dictionary<Material, Material> materialMappings = new Dictionary<Material, Material>();

    protected virtual void Start()
    {
        if (uiElementsToApplyTo.Length == 0)
        {
            uiElementsToApplyTo = gameObject.GetComponentsInChildren<Graphic>();
        }

        foreach (var graphic in uiElementsToApplyTo)
        {
            Material material = graphic.materialForRendering;
            if (material == null)
            {
                Debug.LogError($"{nameof(WorldSpaceOverlayUI)}: skipping target without material {graphic.name}.{graphic.GetType().Name}");
                continue;
            }

            if (!materialMappings.TryGetValue(material, out Material materialCopy))
            {
                materialCopy = new Material(material);
                materialMappings.Add(material, materialCopy);
            }

            materialCopy.SetInt(shaderTestMode, (int)desiredUIComparison);
            graphic.material = materialCopy;
        }
    }
}

Based on Julien-Lynge 's answer I’ve created this script - CustomZTestUI.cs · GitHub


It works in the same way of just overriding the Z Test but does it using the built-in Unity way for overriding UI materials, and it runs much less often so it should perform better.


In-line script (same as Gist):

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Rendering;

namespace Glitchers.UserInterface
{
	public class CustomZTestUI : MonoBehaviour, IMaterialModifier
	{
		#region Serialized
		[Tooltip("LessEqual is 'normal'. Always is overlay. Never is hide.")]
		public CompareFunction comparison = CompareFunction.LessEqual;
		#endregion

		#region Variables
		private Graphic m_Graphic;
		private Material m_RenderMaterial;
		#endregion

		#region Properties
		private const string _propertyKey = "unity_GUIZTestMode";
		private static int? _propertyID = null;
		private static int PropertyID
		{
			get
			{
				if(_propertyID.HasValue==false)
				{
					_propertyID = Shader.PropertyToID(_propertyKey);
				}
				return _propertyID.Value;
			}
		}
		#endregion

		#region Lifecycle
		private void Awake()
		{
			m_Graphic = GetComponent<Graphic>();
		}

		private void OnEnable()
		{
			SetDirty();
		}

		private void OnDisable()
		{
			SetDirty();
		}

#if UNITY_EDITOR
		private void OnValidate()
		{
			if(m_Graphic==null)
			{
				m_Graphic = GetComponent<Graphic>();
			}

			SetDirty();
		}
#endif
		#endregion

		#region Methods
		private void SetDirty()
		{
			if(m_Graphic!=null)
			{
				m_Graphic.SetMaterialDirty();
			}
		}
		#endregion

		#region IMaterialModifier
		Material IMaterialModifier.GetModifiedMaterial(Material baseMaterial)
		{
#if UNITY_EDITOR
			if( Application.isPlaying == false )
			{
				return baseMaterial;
			}
#endif

			if(m_RenderMaterial==null)
			{
				m_RenderMaterial = new Material(baseMaterial)
				{
					name = string.Format("{0} CustomZTestUI", baseMaterial.name),
					hideFlags = HideFlags.HideAndDontSave
				};
			}
			
			m_RenderMaterial.SetInt(PropertyID, (int)comparison);
			
			return m_RenderMaterial;
		}
		#endregion
	}
}

This is a really old post but I want to post something since I’ve encountered this myself.

If you are using URP steps 1-6 will be the same but once you get to steps 7-8 you can instead change the UI camera to an “Overlay” camera on the cam settings and then go back to your main camera and at the very bottom that says camera stacking just click the plus and then add your UI camera.

UI camera stacking documentation here:
https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@10.2/manual/camera-stacking.html

Sometimes you need the canvas to be in world space. For example, your character is moving around and there is a thermometer/etc which “moves with the character”, turning and so on.

If you DO NOT need the UI to be in world space, the answer is triviai:

Setting the Canvas to Render itself as an Overlay on the Camera and not in World Space.

(If you need the UI in world space, this is irrelevant.)

Ok, try this one: [ui-defaultfontdrawontop.zip][1]
The shader from DanSuperGP had a bug in it which was messing up the colors. This is a copy of UI/Default Font shader which only needed one thing fixed: ZTest set to ‘off’.

. . . . . . . .

EDIT: for some reason ZIPs are forbidden.
Here is the complete text:

Shader "UI/Default Font Draw On Top" {
	Properties {
		_MainTex ("Font Texture", 2D) = "white" {}
		_Color ("Text Color", Color) = (1,1,1,1)
		
		_StencilComp ("Stencil Comparison", Float) = 8
		_Stencil ("Stencil ID", Float) = 0
		_StencilOp ("Stencil Operation", Float) = 0
		_StencilWriteMask ("Stencil Write Mask", Float) = 255
		_StencilReadMask ("Stencil Read Mask", Float) = 255

		_ColorMask ("Color Mask", Float) = 15
	}

	SubShader {

		Tags 
		{
			"Queue"="Transparent"
			"IgnoreProjector"="True"
			"RenderType"="Transparent"
			"PreviewType"="Plane"
		}
		
		Stencil
		{
			Ref [_Stencil]
			Comp [_StencilComp]
			Pass [_StencilOp] 
			ReadMask [_StencilReadMask]
			WriteMask [_StencilWriteMask]
		}
		
		Lighting Off 
		Cull Off 
		ZTest Off
		ZWrite Off 
		Blend SrcAlpha OneMinusSrcAlpha
		ColorMask [_ColorMask]

		Pass 
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"

			struct appdata_t {
				float4 vertex : POSITION;
				fixed4 color : COLOR;
				float2 texcoord : TEXCOORD0;
			};

			struct v2f {
				float4 vertex : SV_POSITION;
				fixed4 color : COLOR;
				float2 texcoord : TEXCOORD0;
			};

			sampler2D _MainTex;
			uniform float4 _MainTex_ST;
			uniform fixed4 _Color;
			
			v2f vert (appdata_t v)
			{
				v2f o;
				o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
				o.color = v.color * _Color;
				o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
#ifdef UNITY_HALF_TEXEL_OFFSET
				o.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1);
#endif
				return o;
			}

			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 col = i.color;
				col.a *= tex2D(_MainTex, i.texcoord).a;
				clip (col.a - 0.01);
				return col;
			}
			ENDCG 
		}
	}
}

Expanding answer by @Julien-Lynge

Add this script to your canvas (works in playmode, requires adding script only to canvas):

using UnityEngine;
using UnityEngine.UI;     
public class DrawGUIOnTop : MonoBehaviour {
 
    public UnityEngine.Rendering.CompareFunction comparison = UnityEngine.Rendering.CompareFunction.Always;
 
    public bool apply = false;
 
    private void Start()
    {
        if (apply)
        {
            apply = false;
            Graphic[] images = GetComponentsInChildren<Graphic>();
            foreach (var image in images)
            {
                Material existingGlobalMat = image.materialForRendering;
                Material updatedMaterial = new Material(existingGlobalMat);
                updatedMaterial.SetInt("unity_GUIZTestMode", (int) comparison);
                image.material = updatedMaterial;
            }
        }
    }
}
1 Like

Update of @Julien-Lynge solution that includes TextMeshPro on a canvas.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

[ExecuteInEditMode] //Disable if you don't care about previewing outside of play mode
public class WorldSpaceOverlayUI : MonoBehaviour
{
    private const string shaderTestMode = "unity_GUIZTestMode"; //The magic property we need to set
    [SerializeField] UnityEngine.Rendering.CompareFunction desiredUIComparison = UnityEngine.Rendering.CompareFunction.Always; //If you want to try out other effects
    [Tooltip("Set to blank to automatically populate from the child UI elements")]
    [SerializeField] Graphic[] uiGraphicsToApplyTo;
    [Tooltip("Set to blank to automatically populate from the child UI elements")]
    [SerializeField] TextMeshProUGUI[] uiTextsToApplyTo;
    //Allows us to reuse materials
    private Dictionary<Material, Material> materialMappings = new Dictionary<Material, Material>();
    protected virtual void Start()
    {
        if (uiGraphicsToApplyTo.Length == 0)
        {
            uiGraphicsToApplyTo = gameObject.GetComponentsInChildren<Graphic>();
        }
        if (uiTextsToApplyTo.Length == 0)
        {
            uiTextsToApplyTo = gameObject.GetComponentsInChildren<TextMeshProUGUI>();
        }
        foreach (var graphic in uiGraphicsToApplyTo)
        {
            Material material = graphic.materialForRendering;
            if (material == null)
            {
                Debug.LogError($"{nameof(WorldSpaceOverlayUI)}: skipping target without material {graphic.name}.{graphic.GetType().Name}");
                continue;
            }
            if (!materialMappings.TryGetValue(material, out Material materialCopy))
            {
                materialCopy = new Material(material);
                materialMappings.Add(material, materialCopy);
            }
            materialCopy.SetInt(shaderTestMode, (int)desiredUIComparison);
            graphic.material = materialCopy;
        }
        foreach (var text in uiTextsToApplyTo)
        {
            Material material = text.fontMaterial;
            if (material == null)
            {
                Debug.LogError($"{nameof(WorldSpaceOverlayUI)}: skipping target without material {text.name}.{text.GetType().Name}");
                continue;
            }
            if (!materialMappings.TryGetValue(material, out Material materialCopy))
            {
                materialCopy = new Material(material);
                materialMappings.Add(material, materialCopy);
            }
            materialCopy.SetInt(shaderTestMode, (int)desiredUIComparison);
            text.fontMaterial = materialCopy;
        }
    }
}

I was trying to make this work in VR and the only changes to the first answer is that using OVR and player prefab you have to put the UICamera in the trackingspace(the parent of the main camera) not as a child of the main camera which makes the whole UI layer move with gaze direction.,Heyo, been trying to make this method work in VR mode using OVR and player prefab, had some frustrating moments but if you put the UICamera under the trackingspace(the parent of the main camera) in the hierarchy not as a child of the camera it works! Just letting people know since i found no fix for this online

Not sure if this will work for you but could attach a canvas to each game object, if you mean you want to use it to overlay 20 over 3d, I did that for health bars in a game. If you want to show 3d in the canvas you can create another camera and use a render texture, you can use a make to make it any shape you want as well…

The other solution that is the best one from Julien-Lynge does not work with the newer Unity (2021+) and TextMeshPro.

If using the URP: Follow this tutorial here: Unity: Canvas World Space on Top of Everything (UI Camera) - YouTube
Then use the highlighted comment for how to modify the process.

Share modified script so it works for VR

Single Pass Instanced rendering

Shader "UI/Default_OverlayNoZTest"
{
    Properties
    {
        [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
        _Color("Tint", Color) = (1,1,1,1)
        _StencilComp("Stencil Comparison", Float) = 8
        _Stencil("Stencil ID", Float) = 0
        _StencilOp("Stencil Operation", Float) = 0
        _StencilWriteMask("Stencil Write Mask", Float) = 255
        _StencilReadMask("Stencil Read Mask", Float) = 255

        _CullMode ("Cull Mode", Float) = 0
        _ColorMask("Color Mask", Float) = 15
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Overlay"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
            //"PreviewType" = "Plane"
            //"CanUseSpriteAtlas" = "True"
        }

        Stencil
        {
            Ref[_Stencil]
            Comp[_StencilComp]
            Pass[_StencilOp]
            ReadMask[_StencilReadMask]
            WriteMask[_StencilWriteMask]
        }

        Cull [_CullMode]
        ZWrite Off
        Lighting Off
        Fog
        {
            Mode Off
        }
        ZTest Always
        Blend One OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata_t
            {
                UNITY_VERTEX_INPUT_INSTANCE_ID
                float4 vertex : POSITION;
                float4 color : COLOR;
                float2 texcoord : TEXCOORD0;
            };

            struct v2f
            {
                UNITY_VERTEX_INPUT_INSTANCE_ID
			    UNITY_VERTEX_OUTPUT_STEREO
                float4 vertex : SV_POSITION;
                fixed4 color : COLOR;
                half2 texcoord : TEXCOORD0;
            };

            fixed4 _Color;
            fixed4 _TextureSampleAdd; //Added for font color support

            v2f vert(appdata_t IN)
            {
                v2f OUT;

                UNITY_INITIALIZE_OUTPUT(v2f, OUT);
			    UNITY_SETUP_INSTANCE_ID(IN);
			    UNITY_TRANSFER_INSTANCE_ID(IN, OUT);
			    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                
                OUT.vertex = UnityObjectToClipPos(IN.vertex);
                OUT.texcoord = IN.texcoord;
                #ifdef UNITY_HALF_TEXEL_OFFSET
        OUT.vertex.xy += (_ScreenParams.zw - 1.0)*float2(-1,1);
                #endif
                OUT.color = IN.color * _Color;
                return OUT;
            }

            sampler2D _MainTex;

            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
                //Added for font color support
                clip(color.a - 0.01);
                return color;
            }
            ENDCG
        }
    }
}

I fixed this in 3 seconds.check this youtube video.