Adjusting returned WorldToScreenPoint for perspective angled camera

I’m having a very difficult time getting a 3d world point projected onto an angled perspective camera. I’m making an rts game with the “typical” rts camera, that is, it’s angled at about 45 degrees (actually 55 degrees in my case) looking down on the terrain and unit characters.

The problem is that due to the camera’s angle, the projected point (the unit’s pivot point in this case) onto screen space slowly gets more “off” as the unit approaches the edges of the screen. It doesn’t matter what edge it approaches, top, down, left or right, the point will drift off-center relative to the unit from where it was in the center of the screen.

I did some testing, and found that WorldToScreenPoint (and WorldToViewportPoint) work just find IF (big if) I’m using an orthographic projection, or if I’m using a perspective projection with zero angle to it. Which in the latter case, the perspective camera would basically have to be on the ground to see anything worthwhile.

To me, this means the WorldToScreenPoint function works great, but I somehow have to do some math to account for the angle of the camera. What math or vector manipulation can I do to account for the angle of the camera?

The overall goal, is to get a unit’s position in 3d world space, project it onto the screen, and have that point stay exactly the same relative to the unit no matter where the unit is on-screen.

I finally figured it out… i was having a simiar problem…

Quick Description of my problem:
I was using the Canvas to display a sprite ( a healthbar) which floats over all the characters. The problem however, was when i tried to obtain the WorldToScreenPoint it kept giving a result that was slightly off… for example: the healthbar looked a little okay when the character was immediately in front of the camera… but as the character walks to the edge of the camera’s fulstrum, the screens x,y placement becomes more and more incorrect.

Days and days of research and trying different combinations finally showed me that maybe there is a scaling issue which pointed me to look at Canvas / Canvas Scaler / scaling mode: scale with screen

Originally, this worked wonderfully when i had only 1 character and his healthbar stayed stuck to the top of the screen like old classic double dragon games. BUT when i made the decision to have many characters and they all need “floating healthbars”, i didnt come back to re-evaluate whether this option needed to change.

Setting the canvas Scaler to: keep constant pixel size
, fixes the problem and i now have the correct WORLDtoSCREENpoint that i needed! And now the healthbar floats beautifully above the characters…

BUT WAIT, ANOTHER PROBLEM! Now, if the screen resolution is small… the Ui sprite is obsurdly large… and if the screen resolution is high definition then Ui sprite is way too small!

QUESTION: So how do i use the “scale with screen size” mode, but yet also still get back a correct WorldToScreenPoint?

ANSWER: you must take into consideration the overal scaling of the canvas when it is stretched to fit (whatever current resolution that you are using)

INSTEAD OF:

RectTransform myRect = GetComponent<RectTransform>();
Vector2 myPositionOnScreen = Camera.main.WorldToScreenPoint (myOwner);
myRect.anchoredPosition = myPositionOnScreen;

YOU CALCULATE THE OVERALL SCALE FACTOR LIKE THIS:

RectTransform myRect = GetComponent<RectTransform>();
    Vector2 myPositionOnScreen = Camera.main.WorldToScreenPoint (myOwner);

Canvas copyOfMainCanvas = GameObject.Find ("Canvas").GetComponent <Canvas>();
float scaleFactor = copyOfMainCanvas.scaleFactor

Vector2 finalPosition = new Vector2 (myPositionOnScreen.x / scaleFactor , myPositionOnScreen.y / scaleFactor);
myRect.anchoredPosition = finalPosition;

If this helped anyone please log in to give me a thumbs up…

An easy way to solve this problem is to use Unity’s mathematical Plane() class. Planes are defined by a normal and a point, so assuming the surface is the XZ plane passing through the origin, then your normal will be Vector3.up and the point can be Vector3.zero. Rather than Physics.Raycast() you use Plane.Raycast() to find the point. Note if you have a character walking on a surface, it is more accurate/easier to define the plane as it would pass through the character. So if your character was two units high with a pivot point one unit from the ground, you would Vector3(0,1,0) as the point for the plane. Here is a link to an example of a Plane Raycast:

http://forum.unity3d.com/threads/15802-Plane-Raycast-Example

You are wrong in your assumption that WorldToScreenPoint is off in the far edges of the camera view. I tested, just to be sure, by attaching this script to a cube:

public class test : MonoBehaviour {
    void OnGUI() {
		Vector3 screenpos = Camera.main.WorldToScreenPoint(transform.position);
		GUI.Box(new Rect(screenpos.x - 10, Screen.height - (screenpos.y + 10), 20, 20), "X");
	}
}

The camera angles was set to 45, and different angles as well. And it doesn’t matter how far away the cube is from the camera, or how close to the edge of the screen, the X on the screen always appears exactly where the cube appears in the camera.

So WorldToScreenPoint already does all the calculations for you, and it gives you the exact place that the world position appears on the screen.

To anyone else coming across this year later, the correct answer is here:

Just take the function listed by Tomer-Barkan and pass the screen position and distance you would normally pass to ScreenToWorldPoint.