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
}
}
}
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â:
- Put your UI on a Layer called UI (if it isnât already).
- Duplicate your Main Camera and call it UI Camera.
- Parent your UI Camera to the Main Camera.
- Remove scripts like camera control, post effects and audio listeners from the UI Camera.
- In the Main Camera Culling Mask turn off UI
- In the UI Camera turn everything off but UI.
- In the UI Camera change Clear Flags to Depth only
- In the UI Camera change the Depth to something higher than the value on your Main Camera.
- Then in your Canvas set the Event Camera to the UI Camera.
This image is of the Scene view
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;
}
}
}
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
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
}
}
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
}
}
}
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;
}
}
}
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;
}
}
}
}
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
}
}
}