Draw orthographic visuals (e.g. health bar) in perspective scene?

I would like to draw a 2D health bar above my characters that doesn’t change in perspective or scale as the 3D camera zooms and pans (so regular billboards aren’t what I’m after).

The general approach I tried was:

  • Setup a main camera with perspective projection, depth only clear flag, near/far clip planes .3 : 1000, camera depth -1.
  • Setup a GUI camera with orthographic projection, don’t clear flag, size 50, near/far clip planes 0 : 100, camera depth 0.
  • Get a world position vector for the health bar prefab at a spot above character position
  • Convert the perspective position vector to viewport coordinates via mainCamera.WorldToViewportPoint()
  • Convert the viewport point back to a world point via guiCamera.ViewportToWorldPoint()

This works OK as far as X/Y, but the ortho visuals are always rendered on top of the perspective visuals in terms of Z order. I want elements in my perspective scene to occlude these. I assumed the depth buffer would take care of this during drawing, but maybe I’m not getting consistent Z values between the perspective and ortho cameras.

Any suggestions on the best way to achieve the effect? Thanks!

Here is the solution I arrived at which does the job of rendering ortho into a perspective scene.

  • Setup a main camera with perspective projection, depth only clear flag, near/far clip planes 1: 100, camera depth -1.
  • Setup a GUI camera with orthographic projection, don’t clear flag, size 100, near/far clip planes 1 : 100, camera depth 0.
  • Get a world position vector for the health bar prefab at a spot above character position
  • Convert the perspective position vector to normalized viewport coordinates via mainCamera.WorldToNormalizedViewportPoint() (see below)
  • Convert the viewport point back to a world point via guiCamera.NormalizedViewportToWorldPoint() (see below)

Camera Extensions:

public static class CameraExtensions
{
	/// [summary]
	/// The resulting value of z' is normalized between the values of -1 and 1, 
	/// where the near plane is at -1 and the far plane is at 1. Values outside of 
	/// this range correspond to points which are not in the viewing frustum, and 
	/// shouldn't be rendered.
	/// 
	/// See: http://en.wikipedia.org/wiki/Z-buffering
	/// [/summary]
	/// [param name="camera"]
	/// The camera to use for conversion.
	/// [/param]
	/// [param name="point"]
	/// The point to convert.
	/// [/param]
	/// [returns]
	/// A world point converted to view space and normalized to values between -1 and 1.
	/// [/returns]
	public static Vector3 WorldToNormalizedViewportPoint(this Camera camera, Vector3 point)
	{
		// Use the default camera matrix to normalize XY, 
		// but Z will be distance from the camera in world units
		point = camera.WorldToViewportPoint(point);

 		if(camera.isOrthoGraphic)
		{
			// Convert world units into a normalized Z depth value
			// based on orthographic projection
			point.z = (2 * (point.z - camera.nearClipPlane) / (camera.farClipPlane - camera.nearClipPlane)) - 1f;
		}
		else
		{
			// Convert world units into a normalized Z depth value
			// based on perspective projection
			point.z = ((camera.farClipPlane + camera.nearClipPlane) / (camera.farClipPlane - camera.nearClipPlane))
				+ (1/point.z) * (-2 * camera.farClipPlane * camera.nearClipPlane / (camera.farClipPlane - camera.nearClipPlane));
		}
		
		return point;
	}
	
	/// [summary]
	/// Takes as input a normalized viewport point with values between -1 and 1,
	/// and outputs a point in world space according to the given camera.
	/// [/summary]
	/// [param name="camera"]
	/// The camera to use for conversion.
	/// [/param]
	/// [param name="point"]
	/// The point to convert.
	/// [/param]
	/// [returns]
	/// A normalized viewport point converted to world space according to the given camera.
	/// [/returns]
	public static Vector3 NormalizedViewportToWorldPoint(this Camera camera, Vector3 point)
	{
		if(camera.isOrthoGraphic)
		{
			// Convert normalized Z depth value into world units
			// based on orthographic projection
			point.z = (point.z + 1f) * (camera.farClipPlane - camera.nearClipPlane) * 0.5f + camera.nearClipPlane;
		}
		else
		{
			// Convert normalized Z depth value into world units
			// based on perspective projection
			point.z = ((-2 * camera.farClipPlane * camera.nearClipPlane) / (camera.farClipPlane - camera.nearClipPlane)) /
				(point.z - ((camera.farClipPlane + camera.nearClipPlane) / (camera.farClipPlane - camera.nearClipPlane)));
		}

		// Use the default camera matrix which expects normalized XY but world unit Z 
		return camera.ViewportToWorldPoint(point);
	}
}

Usage:

void LateUpdate()
{		
	var position = perspectiveCamera.WorldToNormalizedViewportPoint(worldTransform.position);
	objectTransform.position = orthographicCamera.NormalizedViewportToWorldPoint(position);
}

I’m guessing you’re used to more low-level, DIY programming tools? Unity has this sort of functionality built-in for you! Place to start looking is the GUITexture class. Some tips to get you started.

There are two basic ways to deal with positioning GUITextures. If you want them to scale and position themselves relative to the view, you can just set the object’s transform position and scale, and they will resize automatically with the window. Note, in this case, that GUITextures treat their object’s transform as being in Viewport Space, which is normalized, meaning (0,0) is the bottom-left corner and (1,1) the top-right, regardless of the resolution or aspect ratio of the game screen. This approach is quick and easy, but it will give wonky results if your aspect ratios change.

If you want per-pixel control and no scaling of the GUITextures, you’ll want to set the pixelInset values of the GUITexture. These are in Screen space, meaning bottom-left is (0,0), and top-right corner is (Screen.width-1,Screen.height-1). You’ll want to set the transform position and scale to (0,0,0), and then just set up the GUITexture component’s pixelInset, which will then directly control the position and size of the sprite in pixels.

:edit: forgot to mention, either way, you control relative depth of GUITextures with the transform’s z position. GUITextures will always render in front of any objects in world space, with the highest z value being on top, regardless of the camera direction or setup. Camera’s clip planes are ignored for GUITextures as well.

Hi. You can use sorting layers and sorting orders on any renderer.

Here’s a script I wrote to help with meshes. This also works with particles.

public class MeshSortingOrder : MonoBehaviour {

    public string layerName;
    public int order;

    private MeshRenderer rend;
	void Awake()
    {
        rend = GetComponent<MeshRenderer>();
        rend.sortingLayerName = layerName;
        rend.sortingOrder = order;
    }

    public void OnValidate()
    {
        rend = GetComponent<MeshRenderer>();
        rend.sortingLayerName = layerName;
        rend.sortingOrder = order;
    }
}

I’m using the following script that can be used to get exact thing you want. No additional camera necessary, unless you want the bars to be “always on top”.

“cam” is the game object containing main camera. Then check orientate/scale depending on what you want to achieve (scale gives constant scale regardless of distance, orientate gives billboard behavior). You can also adjust scale multiplier (“objectScale”) if the object turns out too big/too small.

Apply to the healthbar object/healthbar ui canvas.

using UnityEngine;
using System.Collections;

public class CanvasTripod : MonoBehaviour {
	public GameObject cam;
	public float objectScale = 1.0f; 
	public bool orientate =true;
	public bool scale = true;
	private Vector3 initialScale; 
	// Use this for initialization
	void Start () {
		initialScale = transform.localScale; 
	}
	void Update(){
		//billboarding the canvas
		if (orientate){
			transform.LookAt(transform.position + cam.transform.rotation * Vector3.back, cam.transform.rotation * Vector3.up);
			this.transform.Rotate(0,180,0);
		}
		//making it properly scaled
		if (scale) {
			Plane plane = new Plane(cam.transform.forward, cam.transform.position); 
			float dist = plane.GetDistanceToPoint(transform.position); 
			transform.localScale = initialScale * dist * objectScale; 
		}
	}
	// Update is called once per frame
	void LateUpdate () {
		
		
	}
}