Get a (GUI) 2d-arrow to turn to a specific position in 3d-space (similar to a compass)

I've been fiddling with this for a couple of days now, trying out both GUI-skins and 3d-objects but can't seem to get it working properly. I've tried out this solution http://forum.unity3d.com/threads/43967-compass-on-GUI-rotate-with-camera! but it doesn't really apply to what I'm trying to achieve.

So what I'm trying to do is an arrow as GUI that moves around a pivot in the center of the screen, looking at new waypoints. The directions can be on all axises as the player is a flying object (which can roll, turn and elevate in all angles) but the arrow should always remain flat towards the camera but rotate around its z-axis towards the active waypoint. When the object is close enough in angle (when the player looks directly at it) it's supposed to fade out. The big problem is to get the arrow to look at the object with the correct rotation and still remain flat to the camera.

This could also be used as a damage direction notifier (where the bullet came from and hit the player) for instance in an FPS, showing hits as a clock around a pivot centered to the screen. How would you solve such a thing? I've been looking into WorldToScreenPoint and angles, but can't figure it out - the more I dig the more confused I get.

alt text

I've gone completely blind here, a little help would be much appreciated!

Edit: This is what I've come up with so far:

var cam : Camera;
private var targetPos : Vector3;
private var screenMiddle : Vector3;
private var angle : float = 0.0;

function Update() {
    targetPos = cam.WorldToScreenPoint (CheckpointScript.nextCheckpoint.transform.position);
    screenMiddle = Vector3(Screen.width/2, Screen.height/2, 0); 
    angle = Mathf.Atan2(targetPos.x-screenMiddle.x, targetPos.y-screenMiddle.y) * 180 / Mathf.PI;
    print(angle);
}

This prints out the angle between the object and the target (which is set in the CheckpointScript). My problem is to set the eulerAngles so the arrow only rotates on one axis (to remain flat towards the camera). Besides from having a blonde moment here, what am I supposed to do?

Working with rotations can be really tricky especially when you have to work with 2d to 3d coordinates. If someone want to implement this here's a quick version:

var cam : Camera; //Camera to use
var target : Transform; //Target to point at (you could set this to any gameObject dynamically)
private var targetPos : Vector3; //Target position on screen
private var screenMiddle : Vector3; //Middle of the screen

function Update() {
    //Get the targets position on screen into a Vector3
    targetPos = cam.WorldToScreenPoint (target.transform.position);
    //Get the middle of the screen into a Vector3
    screenMiddle = Vector3(Screen.width/2, Screen.height/2, 0); 
    //Compute the angle from screenMiddle to targetPos
    var tarAngle = (Mathf.Atan2(targetPos.x-screenMiddle.x,Screen.height-targetPos.y-screenMiddle.y) * Mathf.Rad2Deg)+90;
    if (tarAngle < 0) tarAngle +=360;

    //Calculate the angle from the camera to the target
    var targetDir = target.transform.position - cam.transform.position;
    var forward = cam.transform.forward;
    var angle = Vector3.Angle(targetDir, forward);

    //If the angle exceeds 90deg inverse the rotation to point correctly
    if(angle<90){
        transform.localRotation = Quaternion.Euler(-tarAngle,90,270);
    } else {
        transform.localRotation = Quaternion.Euler(tarAngle,270,90);
    }

}

The GUI in 3d-space has to be put relative to the camera at all times (child to main camera or a camera only seeing the GUI-element rendered on top of the main camera). Preferably the last mentioned as you mostly wouldn't want the GUI to fall behind objects in 3d-space. Big thanks to The_r0nin and the tarAngle computation.

Here's how I make an airplane rotate based on the direction of the mouse pointer from the center of the screen. This should be able to be modified to do what you want (use a WorldToScreenPoint to get your point, or just use the direction based on subtracting Vector3s):

    screenVec = Vector3(Input.mousePosition.x - Global.sCenter.x, Global.sHeight - Input.mousePosition.y - Global.sCenter.y, 0);

    tarAngle= (Mathf.Atan2(screenVec.y,screenVec.x) * Mathf.Rad2Deg)+90;
if (tarAngle < 0) tarAngle +=360;

    if (tarAngle != 0) transform.RotateAround(transform.position,transform.forward, -tarAngle);

Global.sCenter is the screen center pixel coordinates. I simply find the angle between the mouse pointer and the center of the screen (using Atan2) and then roll the target using RotateAround the z-axis of the transform. You could roll your object around its z-axis using the relative x & y coordinate difference...

If you use a 3D arrow and place it in front of the camera you can use the Transform.LookAt() function to set its facing direction. If you use the camera's up vector is should keep the arrow's up vector the same as the camera's up vector.

void Update()
{
    arrowObject.transform.LookAt( waypoint.transform, cam.transform.up );
}

I projected the distance of my target onto a point roughly the same distance away but in front of my camera, and then found the angle from that projected point’s up vector required to make it rotate and point towards the target.

Vector3 up = targetTransform.up;
float dist = Vector3.Magnitude(targetTransform.position - camera.position);
Vector3 over = targetTransform.position - (camera.position + camera.forward * dist);
float ang = Vector3.Angle(up, over);
if (over.x < 0)
  this.transform.localRotation = Quaternion.Euler(0, 0, ang);
else
  this.transform.localRotation = Quaternion.Euler(0, 0, -ang);

Here’s a sccript I found online, it allows to to rotate any GUI element around a pivot; I have used it as a player marker myself.

using UnityEngine;

[ExecuteInEditMode()] 
public class RotatableGuiItem : MonoBehaviour
{
	public Texture2D texture = null;
	public float angle = 0;
	public Vector2 size = new Vector2(128, 128);
	
	//this will overwrite the items position
	public AlignmentScreenpoint ScreenpointToAlign = AlignmentScreenpoint.TopLeft;
	public Vector2 relativePosition = new Vector2(0, 0);
	
	Vector2 pos = new Vector2(0, 0);
	
	Rect rect;
	Vector2 pivot;
	
	void Start() 
	{
		UpdateSettings();
	}
	
	public void UpdateSettings()
	{
		Vector2 cornerPos = new Vector2(0, 0);
		
		//overwrite the items position
		switch (ScreenpointToAlign)
		{
		case AlignmentScreenpoint.TopLeft:
			cornerPos =new Vector2(0, 0);
			break;
		case AlignmentScreenpoint.TopMiddle:
			cornerPos =new Vector2(Screen.width/2, 0);
			break;
		case AlignmentScreenpoint.TopRight:
			cornerPos = new Vector2(Screen.width, 0);
			break;
		case AlignmentScreenpoint.LeftMiddle:
			cornerPos = new Vector2(0, Screen.height / 2);
			break;
		case AlignmentScreenpoint.RightMiddle:
			cornerPos = new Vector2(Screen.width, Screen.height / 2);
			break;
		case AlignmentScreenpoint.BottomLeft:
			cornerPos = new Vector2(0, Screen.height);
			break;
		case AlignmentScreenpoint.BottomMiddle:
			cornerPos = new Vector2(Screen.width/2, Screen.height);
			break;
		case AlignmentScreenpoint.BottomRight:
			cornerPos = new Vector2(Screen.width, Screen.height);
			break;
		default:
			break;
		}
		
		pos = cornerPos + relativePosition;
		rect = new Rect(pos.x - size.x * 0.5f, pos.y - size.y * 0.5f, size.x, size.y);
		pivot = new Vector2(rect.xMin + rect.width * 0.5f, rect.yMin + rect.height * 0.5f);
	}
	
	void OnGUI() {

		GUI.depth = 1;

		if (Application.isEditor)
		{
			UpdateSettings();
		}
		Matrix4x4 matrixBackup = GUI.matrix;
		GUIUtility.RotateAroundPivot(angle, pivot);
		GUI.DrawTexture(rect, texture);
		GUI.matrix = matrixBackup;
	}
	
	public enum AlignmentScreenpoint {
		TopLeft, TopMiddle, TopRight,
		LeftMiddle, RightMiddle,
		BottomLeft, BottomMiddle, BottomRight
	}
}