- Home /
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!
Answer by Incarnadine · Nov 24, 2011 at 09:38 PM
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);
}
Thanks, this is fantastic! I also really appreciate the heavy documentation.
Gr8 code. You save my life
Answer by WillTAtl · Nov 20, 2011 at 04:42 AM
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, thanks for the info. It's true, I'm used to working a little more low-level. :-)
I've used the GUITexture class in the past for overlays, but I'm using a sprite manager with texture atlas to send everything in batches and am looking for a solution I can hook into that pipeline.
Even assu$$anonymous$$g GUITexture were to perform well enough outside of the batching system I'm using, I don't believe it's going to give me the Z-depth sorting I'm after so that it can be both in front of and in some cases occluded by what's going on in my perspective camera scene.
I just ran a quick test and looks like it renders on top ins$$anonymous$$d of merging with my scene elements. $$anonymous$$aybe I'm missing something, but either way, thanks for the suggestion.
oh! I missed that you want elements from the scene to be able to occlude gui elements, sorry!
Hmm. I'm thinking billboarded sprites might be your best bet, but keeping them pixel-perfect in scale at varying depths could be problematic.
A thought, this might be easily solvable with a custom pixel shader, which ignores the transformed z positions and just uses a provided depth value ins$$anonymous$$d?
I should be sleeping, but this got me thinking so I've been diddling about ins$$anonymous$$d. I'm currently using the 2D Toolkit for my sprites management and batching. Reproduced the same issue with 2 cameras that you described, did some testing and seems unity either uses separate depth buffers for ortho and persp. or force-clears the depth buffer when switching between them, so that approach is out.
Dunno what sprite manager you are using, but I've discovered that 2D Toolkit makes this pretty easy. Using a single camera, I attached a sprite to the camera, set it at the desired z-distance, and there's a "make pixel perfect" button in the custom inspector for the sprite component that seems to work perfectly. If you're using something different without this feature, you could presumably accomplish the same thing by tweaking the scale by hand.
Cool, thanks for taking a look. I'm using 2D Toolkit too. :-)
It looks like calling $$anonymous$$akePixelPerfect() every frame does a good job keeping the sprite scaled to the right size as things move. That leaves 2 problems, making sure the sprite is in the correct world position and rotated to face the camera.
For that, I'm not sure if parenting to the camera is the way to go though. I need the sprite to stay above the character's head so would need to override its position each frame if attached to the camera. $$anonymous$$aybe that's not a bad way to pick up the camera rotations I need to keep the sprites oriented, but I was concerned about skewing that could occur from this kind of billboarding.
For billboarding, I'm aware of these 2 techniques:
Orient the sprite to look along the camera forward vector (this is equivalent to matching rotations with the camera). Does a pretty good job when the camera is at about the same height as the sprite, skews when not.
Orient the sprite to look along the vector from sprite to camera. This does a better job when the camera and sprite are at different heights, but it doesn't take the camera forward/look vector into account, so sprites still skew.
I could lerp between the two based on where an object falls in the FOV, but that seems overly complicated. :-)
For now, my camera movement is fairly restricted, so $$anonymous$$akePixelPerfect() + billboarding #1 is giving pretty good results.
Thanks again for the help!
It turns out this didn't work. $$anonymous$$akePixelPerfect() needs some tweaking to be called every frame, but more importantly, my sprites have multiple layers and this shows up or results in Z-fighting when using billboarding. I figured out the depth buffer calculations to render orthographic and will post them in a bit.
Answer by darkhog · Apr 15, 2017 at 09:23 PM
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 () {
}
}
Answer by smoggach · Oct 10, 2014 at 02:11 PM
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;
}
}
Your answer
Follow this Question
Related Questions
Drawing on the same screen with different cameras? 1 Answer
one perspective camera and one orthographic in the same scene 1 Answer
the default camera makes everything look like its moving towards the center? 2 Answers
Can I make a camera that has an orthographic vertical axis, but a perspective on the horzontal axis? 1 Answer