- Home /
How to convert from world space to canvas space?
I have a screen space canvas and I simply want to move a "target" over an object in world space. My world space coordinate is obtained with a raycast and I do a debug draw to make sure it is correct. The problem is none of the following code is giving me the right result.
void Awake()
{
rt = (RectTransform)this.transform;
}
void Update()
{
if (currentSelection == null)
return;
// Translate world position to UI coordinates
Vector3 pos = ((RectTransform)rt.parent).InverseTransformPoint(currentSelection.position);
rt.position = pos; // nether rt or rt.parent works!
}
Answer by Sylos · Nov 24, 2014 at 02:32 PM
I solved this problem by doing the following:
//this is your object that you want to have the UI element hovering over
GameObject WorldObject;
//this is the ui element
RectTransform UI_Element;
//first you need the RectTransform component of your canvas
RectTransform CanvasRect=Canvas.GetComponent<RectTransform>();
//then you calculate the position of the UI element
//0,0 for the canvas is at the center of the screen, whereas WorldToViewPortPoint treats the lower left corner as 0,0. Because of this, you need to subtract the height / width of the canvas * 0.5 to get the correct position.
Vector2 ViewportPosition=Cam.WorldToViewportPoint(WorldObject.transform.position);
Vector2 WorldObject_ScreenPosition=new Vector2(
((ViewportPosition.x*CanvasRect.sizeDelta.x)-(CanvasRect.sizeDelta.x*0.5f)),
((ViewportPosition.y*CanvasRect.sizeDelta.y)-(CanvasRect.sizeDelta.y*0.5f)));
//now you can set the position of the ui element
UI_Element.anchoredPosition=WorldObject_ScreenPosition;
No it's not really performant, and it's not correct. Here is the right code I made :
using UnityEngine;
/// <summary>
/// Place an UI element to a world position
/// </summary>
[RequireComponent(typeof(RectTransform))]
public class PlaceUIElementAtWorldPosition : $$anonymous$$onoBehaviour
{
private RectTransform rectTransform;
private Vector2 uiOffset;
/// <summary>
/// Initiate
/// </summary>
void Start ()
{
// Get the rect transform
this.rectTransform = GetComponent<RectTransform>();
// Calculate the screen offset
this.uiOffset = new Vector2((float)Canvas.sizeDelta.x / 2f, (float)Canvas.sizeDelta.y / 2f);
}
/// <summary>
/// $$anonymous$$ove the UI element to the world position
/// </summary>
/// <param name="objectTransformPosition"></param>
public void $$anonymous$$oveToClickPoint(Vector3 objectTransformPosition)
{
// Get the position on the canvas
Vector2 ViewportPosition = Camera.main.WorldToViewportPoint(objectTransformPosition);
Vector2 proportionalPosition = new Vector2(ViewportPosition.x * Canvas.sizeDelta.x, ViewportPosition.y * Canvas.sizeDelta.y);
// Set the position and remove the screen offset
this.rectTransform.localPosition = proportionalPosition - uiOffset;
}
}
Thanks for this! I was struggling with the problem of how to get $$anonymous$$imap indicators to render in screen space on the $$anonymous$$imap and this worked wonders.
I needed to take 2 things into consideration for my implementation though: 1. If your canvas has a content size fitter, make sure it is set to unconstrained or it may be zero size, causing your icons to render too far up and left. 2. The actual image or text you are trying to render should be a child of the canvas so you can move it relative to the canvas. If you try to move the canvas, the screen space camera mode will just re-center it on the next update.
for me Canvas.GetComponent doesnt work. it says : A field initializer cannot reference the nonstatic field, method, or property `UnityEngine.Component.GetComponent(System.Type)'
$$anonymous$$y two cents: Take in $$anonymous$$d that you might need to change the calculations based on the anchor of the gui element you are drawing, i'm used to work with top-left anchors, so this wasn't correct at all, ins$$anonymous$$d I used this:
// get the initial position for the element
Vector3 character = this.mGameController.Character.transform.position;
Vector2 viewport = this.mGameController.Camera.WorldToViewportPoint (character);
Vector2 screenPosition = new Vector2
(
viewport.x * this.mGameController.GUICanvasRect.sizeDelta.x,
viewport.y * this.mGameController.GUICanvasRect.sizeDelta.y
);
// this is reversed because of the anchor we are using for this element
screenPosition.y = -screenPosition.y;
@RawSpartano Canvas is not a gameObject, take in $$anonymous$$d that GetComponent is only available for gameObjects not Canvas/RecTransform/etc types, so make sure you use a GameObject ins$$anonymous$$d of a Canvas
Just wanted to add that using the sizeDelta only works if your anchors are all grouped together. Otherwise, sizeDelta is not equivalent to the component's size. Good answer though, I ended up going for your solution :)
Answer by leni8ec · Dec 13, 2014 at 02:15 AM
This issue also has another solution:
Vector2 pos = gameObject.transform.position; // get the game object position
Vector2 viewportPoint = Camera.main.WorldToViewportPoint(pos); //convert game object position to VievportPoint
// set MIN and MAX Anchor values(positions) to the same position (ViewportPoint)
rectTransform.anchorMin = viewportPoint;
rectTransform.anchorMax = viewportPoint;
Thanks so much! Was spending hours to get this to work, and this solved it. (Small note on typo, ViewportPosition is called ViewportPoint on line 5 and 6).
This is not as correct as @Sylos answer is, take in $$anonymous$$d that changing the anchor you limit the position where the gui element can move to BUT you don't actually move the element on the GUI canvas, thats why unity is so "slow" when changing these values.
Answer by Stardog · Jan 10, 2015 at 09:05 AM
This is the cleanest way I can find of making a UI element follow a world object's 3D position.
public RectTransform canvasRectT;
public RectTransform healthBar;
public Transform objectToFollow;
void Update()
{
Vector2 screenPoint = RectTransformUtility.WorldToScreenPoint(Camera.main, objectToFollow.position);
healthBar.anchoredPosition = screenPoint - canvasRectT.sizeDelta / 2f;
}
Reading Sylos's answer above helped.
I don't see how RectTransformUtility.WorldToScreenPoint can ever do anything useful, it's static and you don't pass in a reference to a rect so it has no way of knowing what you mean, all it does the the exact same as Camera.main.WorldToScreenPoint (I've tested, several times) would which will work but only if you've got a specific non-scaled canvas setup.
I mean... this works quite well for me, I don't see why I should deal with the chaos up there, is this low performance or sth?
Answer by YoungDeveloper · Aug 20, 2015 at 04:49 PM
I wrote a static function which requires canvas rectransform, camera and vector position. That way you can work with multiple canvas and cameras. Works like a charm.
private Vector2 WorldToCanvasPosition(RectTransform canvas, Camera camera, Vector3 position) {
//Vector position (percentage from 0 to 1) considering camera size.
//For example (0,0) is lower left, middle is (0.5,0.5)
Vector2 temp = camera.WorldToViewportPoint(position);
//Calculate position considering our percentage, using our canvas size
//So if canvas size is (1100,500), and percentage is (0.5,0.5), current value will be (550,250)
temp.x *= canvas.sizeDelta.x;
temp.y *= canvas.sizeDelta.y;
//The result is ready, but, this result is correct if canvas recttransform pivot is 0,0 - left lower corner.
//But in reality its middle (0.5,0.5) by default, so we remove the amount considering cavnas rectransform pivot.
//We could multiply with constant 0.5, but we will actually read the value, so if custom rect transform is passed(with custom pivot) ,
//returned value will still be correct.
temp.x -= canvas.sizeDelta.x * canvas.pivot.x;
temp.y -= canvas.sizeDelta.y * canvas.pivot.y;
return temp;
}
nice solution, I had to remove line 16. and 17. for some reason, then it worked for me!
$$anonymous$$any thanks. I struggled this issue almost a half day, finally found working solution. You saved me, thanks my friend!
Answer by cmberryau · Mar 24, 2015 at 12:58 AM
I extended Sylos's slightly by making it an extension method to UnityEngine.Canvas itself, giving the user the option to use a non-main camera if they'd like:
public static class CanvasExtensions
{
public static Vector2 WorldToCanvas(this Canvas canvas,
Vector3 world_position,
Camera camera = null)
{
if (camera == null)
{
camera = Camera.main;
}
var viewport_position = camera.WorldToViewportPoint(world_position);
var canvas_rect = canvas.GetComponent<RectTransform>();
return new Vector2((viewport_position.x * canvas_rect.sizeDelta.x) - (canvas_rect.sizeDelta.x * 0.5f),
(viewport_position.y * canvas_rect.sizeDelta.y) - (canvas_rect.sizeDelta.y * 0.5f));
}
}
It can be called like so:
// _ui_canvas being the Canvas, _world_point being a point in the world
var rect_transform = _ui_element.GetComponent<RectTransform>();
rect_transform.anchoredPosition = _ui_canvas.WorldToCanvas(_world_point);
You just have to include the namespace the CanvasExtensions class resides in
Be aware that the Vector2 the function returns is calculated for center anchored GUI elements, when using a different anchor you may run into problems when the element is not in the correct position or even out of screen.