Search Unity

camera.WorldToScreenPoint() bug?

Discussion in 'Scripting' started by criistii22, Apr 11, 2011.

  1. criistii22

    criistii22

    Joined:
    Jan 18, 2011
    Posts:
    52
    Hi. I am currently working on a pretty simple mobile game.
    Currently I am stuck on the mentioned function (WorldToScreenPoint). My issue is that, it returns some strange values on the Y axis.

    What I want to do:
    Basically, I need to show an icon on the edge of the screen for each off-screen monster. Since both the main player and all monsters are set on a plane, there should not be any issues.
    The monster's 2D coordinates are calculated like this : Vector2 loc = camera.WorldToScreenPoint(monster.position);
    My problem is that I keep getting some strange values if the monster is to far away from the screen center.
    When the monster spawns, Y is about 4000. It keeps growing until around 6-7000, than it jumps to -3000, at wich point the values are correct until it gets on the screen. If I move away far enough from the monster, the same thing happens again.

    I also attached an image in hope it better explains my issue.

    Any help is greatly appreciated.

    Thanks,
    Cristi
     

    Attached Files:

  2. Satoh

    Satoh

    Joined:
    Feb 17, 2011
    Posts:
    56
    The Y value should be the number of pixels from the bottom of the screen that the monster is... so if it is returning something like 150 when your monster is actual 4000 in the Y axis, that is normal, because it isn't a worldspace point, but a screen point.

    Could you give us a better example... for instance a picture of the actual application running, and the values it returns at that time?
     
  3. criistii22

    criistii22

    Joined:
    Jan 18, 2011
    Posts:
    52
    I realize that the coordinates have the origin in the lower left side of the screen.

    First of all I should clarify that I have a fixed top-down camera, and the entire game is on a plane.. Because of this, when the monster is on screen or above it, Y should have positive values.
    And when the monster is outside the lower part of the screen, the values should be negative. Still I get positive values for certain locations.

    In the attached image, the black dot represents the monster. I am not really sure how screen shots where no monster is visible would help.
     
  4. Satoh

    Satoh

    Joined:
    Feb 17, 2011
    Posts:
    56
    Ah, I see, I misunderstood what the image was depicting. It occurs to me that if your screen is not centered directly parallel with the floor (which it probably isn't) then whatever method checks the screen coordinate of the object, is actually checking from the opposite side of the camera plane, due to the object being so far off the screen.... Allow me to illustrate:
     
    Last edited: Apr 11, 2011
    LouisHong likes this.
  5. criistii22

    criistii22

    Joined:
    Jan 18, 2011
    Posts:
    52
    Oh, I see. Was expecting this is an issue on my side, not unity's. Do you have any suggestion how I could calculate the information I need?
     
  6. Satoh

    Satoh

    Joined:
    Feb 17, 2011
    Posts:
    56
    I'm only assuming that's what is happening. It seems likely to me, but I don't deal with Unity's actual programming directly... You could test it by playtesting and then pausing the playtest when the issue occurs, and then checking the angle of the camera, and the angle of the enemy based on that info.

    Otherwise, posting your coordinate checking script may help shed some light on the issue.

    You could try using world coordinates if you really need to know if the enemy is below the screen like that... it might take a bit of thought... For instance you could test their position relative to your Camera's transform.up rotation... I'm not really sure what you need the info for, so I can't really suggest much at this point.
     
  7. criistii22

    criistii22

    Joined:
    Jan 18, 2011
    Posts:
    52
    My final goal is to create an icon for each off-screen monster. The icons will be on the edge of the screen and point the direction the monster will come from.
     
  8. criistii22

    criistii22

    Joined:
    Jan 18, 2011
    Posts:
    52
    I managed to find some code on the forums that determines weather a Transform is in front or behind the camera, but I still have issues with the enemies behind the camera. I tried projecting their position on the camera's plane, but it still isn't working:

    Code (csharp):
    1.  
    2.             Vector3 heading = e.transform.position - mainCamera.transform.position;
    3.             if (Vector3.Dot(mainCamera.transform.forward, heading) > 0)
    4.             {
    5.                 // Object is in front.
    6.                 enemyScreenCoordinates = mainCamera.WorldToScreenPoint(e.transform.position);
    7.             }
    8.             else
    9.             {
    10.                 Vector3 cameraDown = mainCamera.transform.TransformDirection(Vector3.down);
    11.                 Vector3 projection = Vector3.Project(e.transform.position, cameraDown);
    12.                 enemyScreenCoordinates = mainCamera.WorldToScreenPoint(projection);
    13.             }
    14.  
    Any ideas how to solve this?
     
  9. Kelly G

    Kelly G

    Joined:
    Oct 29, 2010
    Posts:
    35
    Is it necessary to find the screen coordinates when the monster is off the screen? Seems like you just need to know if the monster is on the screen or not.
    You can avoid the problem stated above by comparing strictly world coordinates. So instead of converting the monsters world coordinates to screen coordinates, you can convert the edge of the screen to world coordinates and compare those.


    The camera also has a function that computes viewport to world coordinates (the viewport vector has x and y between 0 and 1 and I think z is distance from the camera). So maybe you can get the location of the icon by checking
    Code (csharp):
    1.  
    2. pseudo code (definitely wont compile):
    3.  
    4.  
    5. if the monster.y  < camera.viewportToWorld(0,0,(distance from camera to ground plane)) then
    6. { //the monster is below the screen draw the icon on that edge of the screen
    7.     icon.y = viewportToWorld(0,0,distance).y;
    8. }
    9. else if the monster.y  > camara.viewportToWorld(1,0,(distance from camera to ground plane)) then
    10. { // the monster is above the screen then draw the icon on the top edge.
    11.     icon.y = viewportToWorld(1,0,distance).y;
    12. }
    13. else
    14. {
    15.  the icon is right on the monster.
    16. }
    17.  
    18. Do the same for X;
    19.  
    20. then rotate the icon to point at the monster using world coordinates. There is a vector3 function that does this.
    21.  
    If you allow the camera to be rotated, you'll have to make some adjustment there too.
     
    Last edited: Apr 14, 2011
  10. criistii22

    criistii22

    Joined:
    Jan 18, 2011
    Posts:
    52
    Sorry for the long inactivity on this subject, but I had to postpone fixing this issue. Now I need to finalize this task.
    I'll recap what I am hoping to achieve: I plan to show an Icon for each monster that is not on screen. Each Icon will indicate the point where the monster enters the screen. For a very good example check minigore on the Appstore.
    I have no issues using WorldToScreenPoint for all monsters in front of the camera. I also manage to determine what monsters are in front and which are behind the camera. However, I am unable to determine the correct position for monsters behind the camera. The way the game is set up, all monsters behind the camera should appear on screen from the lower part of it.
    Any Ideas on how to implement this?
     
  11. Anh-Pham

    Anh-Pham

    Joined:
    Feb 7, 2012
    Posts:
    35
    Same problem here :( Someone can help?
     
  12. davepoo

    davepoo

    Joined:
    May 20, 2015
    Posts:
    2
    I know this is an old thread now, but i came across this recently.
    I implemented the solution by projecting the position of the entity on the camera plane (if it is behind the camera), and this solution does work.
    Below is a version of the code i used to project the point if it is behind the camera.

    Code (csharp):
    1.  
    2. // position = the world position of the entity to be tested
    3. private Vector3 calculateWorldPosition(Vector3 position, Camera camera) {  
    4.    //if the point is behind the camera then project it onto the camera plane
    5.    Vector3 camNormal = camera.transform.forward;
    6.    Vector3 vectorFromCam = position - camera.transform.position;
    7.    float camNormDot = Vector3.Dot (camNormal, vectorFromCam.normalized);
    8.    if (camNormDot <= 0f) {
    9.      //we are beind the camera, project the position on the camera plane
    10.      float camDot = Vector3.Dot (camNormal, vectorFromCam);
    11.      Vector3 proj = (camNormal * camDot * 1.01f);   //small epsilon to keep the position infront of the camera
    12.      position = camera.transform.position + (vectorFromCam - proj);
    13.    }
    14.  
    15.    return position;
    16. }
    17.  
     
    kader1081, AGuang_2, Reiiidev and 7 others like this.
  13. atomicjoe

    atomicjoe

    Joined:
    Apr 10, 2013
    Posts:
    1,869
    Thanks Davepoo! You saved my ass! ;) (pun intended xD )
     
    Last edited: Aug 14, 2016
  14. WiedemannD

    WiedemannD

    Joined:
    Mar 4, 2015
    Posts:
    19
    Either I'm not understanding what values camera.WorldToScreenPoint() is returning, when the object is far behind the camera, or I still think there is a bug.
    In front of the camera everything works splendid, but behind the values just don't flip (is can be read in some posts and would be understandable), but instead I get completely confusing values. In my case with a screenPosition.z < ~ -30 screenPosition.x reaches 0 / flippes from negative to positive, even though the object clearly didn't move that way, relative to the camera. I don't think this behavior can be intended, or if it is there needs to be proper documentation about it.

    Handed in a bug report with the case number 849015

    In my case – I also wanted to show gui indicators for off screen objects – I used the to the camera locally transformed position of the object to calculate the angle etc. instead of using the screenPosition returned by camera.WorldToScreenPoint().

    Code (CSharp):
    1. Matrix4x4 cameraMatrix = mainCamera.transform.worldToLocalMatrix;
    2. Vector3 cameraLocalObjectPosition = cameraMatrix.MultiplyPoint3x4(objectTransform.position);
    3. cameraLocalObjectPosition.z = 0;
    To calculate the rest I used digijin's approach:


    And this works fine for any object position, may it be in front or far behind the cam.
     
    javidsh likes this.
  15. takatok

    takatok

    Joined:
    Aug 18, 2016
    Posts:
    1,496
    Well if your object is in front the camera and is off screen to the left, its X screen position should be negative. As it moves behind the camera it sounds like its also getting closer to the edge of the screen so its negative value is slowly approaching 0. When it crosses the line and goes behind the camera.. the Matrix multiplication that determining the screenPoint from the world Point will effectively act like the camera flipped around 180 degrees and the object's z position now positive instead of negative. However that means the camera's left/right has flipped. So if your width is 800 pixels. and you object is at -10 and approaching the threshold of going behind the camera it will suddenly have its value changed to 810
     
  16. WiedemannD

    WiedemannD

    Joined:
    Mar 4, 2015
    Posts:
    19
    Yes that's what everybody's saying, but it's just not like that.

    I don't know how the values behind the camera are formed, but they are NOT just flipped. In my case the camera is turned by 45 degrees on y, but that shouldn't make a difference, as the whole screen point calculation should be made relative to the camera space anyway.
     
  17. Alturis2

    Alturis2

    Joined:
    Dec 4, 2013
    Posts:
    38
    I found that using the code that davepoo provided works, but does not project the point onto the edges of the screen in a reasonable way for consistent relative direction indication. Here are a couple functions someone can use to make this work better I believe.

    Use this to get the screen point clamped to the front side of the camera (thanks Davepoo)
    Code (CSharp):
    1.  
    2.     public static Vector2 WorldToScreenPointProjected( Camera camera, Vector3 worldPos )
    3.     {
    4.         Vector3 camNormal = camera.transform.forward;
    5.         Vector3 vectorFromCam = worldPos - camera.transform.position;
    6.         float camNormDot = Vector3.Dot( camNormal, vectorFromCam );
    7.         if ( camNormDot <= 0 )
    8.         {
    9.             // we are behind the camera forward facing plane, project the position in front of the plane
    10.             Vector3 proj = ( camNormal * camNormDot * 1.01f );
    11.             worldPos = camera.transform.position + ( vectorFromCam - proj );
    12.         }
    13.  
    14.         return RectTransformUtility.WorldToScreenPoint( camera, worldPos );
    15.     }
    16.  
    Use this to project that screen point to the screen edges in a reasonable way that is more understandable to the player.
    This also returns the angle to set an arrow indicator to if you are using such a thing.
    edgeBuffer is the distance from the edge of the screen you want to clamp it within.
    Code (CSharp):
    1.  
    2.     public static Vector3 ScreenPointEdgeClamp( Vector2 screenPos, float edgeBuffer, out float angleDeg )
    3.     {
    4.         // Take the direction of the screen point from the screen center to push it out to the edge of the screen
    5.         // Use the shortest distance from projecting it along the height and width
    6.         Vector2 screenCenter = new Vector2( Screen.width / 2.0f, Screen.height / 2.0f );
    7.         Vector2 screenDir = ( screenPos - screenCenter ).normalized;
    8.         float angleRad = Mathf.Atan2( screenDir.x, screenDir.y );
    9.         float distHeight = Mathf.Abs( ( screenCenter.y - edgeBuffer ) / Mathf.Cos( angleRad ) );
    10.         float distWidth = Mathf.Abs( ( screenCenter.x - edgeBuffer ) / Mathf.Cos( angleRad + ( Mathf.PI * 0.5f ) ) );
    11.         float dist = Mathf.Min( distHeight, distWidth );
    12.         angleDeg = angleRad * Mathf.Rad2Deg;
    13.         return screenCenter + ( screenDir * dist );
    14.     }
    15.  
     
  18. mrekuc

    mrekuc

    Joined:
    Apr 17, 2009
    Posts:
    116
    @Alturis2 I know this is an old thread. WorldToScreenPointProjected works great.

    ScreenPointEdgeClamp just puts the UI all over the place. How do these work together? I have tried everything I can think of so far but once the world object is in the normal of the camera it still just puts it along the edge instead of where it does when just using WorldToScreenPointProjected.
     
  19. betaFlux

    betaFlux

    Joined:
    Jan 7, 2013
    Posts:
    112
    Have to revive this thread once more. Just wanted to ask if there is a common or easier way to clamp Unity UI indicators to the screen by now? WorldToScreenPointProjected from the post above works almost but if the indicator bound object is exactly behind the camera, the indicator completely disappears from screen instead of moving along the edge.
     
  20. wsmmsbzby

    wsmmsbzby

    Joined:
    Feb 6, 2017
    Posts:
    1
    A less hacky projection code:
    Code (CSharp):
    1. public static Vector3 GetFrontVector(Vector3 position, Camera camera)
    2.         {
    3.             Vector4 vec = camera.worldToCameraMatrix * new Vector4(position.x, position.y, position.z, 1);
    4.             if (vec.z < 0.0f)
    5.             {
    6.                 return position;
    7.             }
    8.             else
    9.             {
    10.                 Matrix4x4 m = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(1, 1, -1));
    11.                 return camera.cameraToWorldMatrix * m * vec;
    12.             }
    13.         }
     
    invadererik likes this.
  21. ChrissTman

    ChrissTman

    Joined:
    Aug 11, 2015
    Posts:
    10
    EDIT: doesn't work as expected, mainly on different scales. My usecase changes the camera's height from 20 to 120 units above the terrain and so it breaks.

    EDIT2: The video posted above is perfect, its implementation work flawless

    Hey, i'm not sure if my solution is actually useful, but the original thread is about a topdown orthographic view. I don't understand the above solutions and this seems more readable tho bit longer.

    Davepoo's code works only while looking straight down, but when camera is rotated there's the unwanted X movement while approaching 0 on the Z axis (after projection).

    There are 2 steps in my solutions.

    1) when the position is offscreen, we don't have to insert it right away into
    WorldToScreenPoint
    .
    We can clamp, constrain and modify the position to still stay off screen, but rather help the function to not make the flip. Our goal is to make the Z axis after projection >= 1

    Code (CSharp):
    1. Vector3 ClampToVector(float max, Vector3 origin, Vector3 toClamp)
    2. {
    3.       return origin + (toClamp - origin).normalized * max;
    4. }
    5.  
    This way the distance from camera to UI_WorldPos is exactly the Max.

    2) By projecting the position into the camera's plane will reveal how much Z deficit we actually have. Unity has a nice helper struct called Plane.

    Code (CSharp):
    1. UnityEngine.Plane plane = new UnityEngine.Plane(camT.forward, camPos);
    2. var zDist = plane.GetDistanceToPoint(worldPos);
    worldPos is what we want to project on screen.
    worldPos += (camT.forward * -(zDist - 1));


    This intruduced a little jump while moving from off-screen to on-screen state.
    So ideal is to project both the corrected world pos and the uncorrected world pos. Measure the distance and interpolate between them.

    (scale is the scale of the canvas while having scaling ON)
    Code (CSharp):
    1. var correctedPos = cam.WorldToScreenPoint(worldPos);
    2. var uncorrectedPos = cam.WorldToScreenPoint(uiPos);
    3. var correctionDiff = Vector2.Distance(correctedPos, uncorrectedPos);
    4. var pos = Vector3.Lerp(uncorrectedPos, correctedPos, correctionDiff / correctionBias * scale);
    And finally clamp and set the projected result on screen.

    Code (CSharp):
    1. //p and p2 are scaling factors
    2. ui.position.x = Mathf.Clamp(pos.x, p, Screen.width - p);
    3. ui.position.y = Mathf.Clamp(pos.y, 0, Screen.height - p2);

    It was 4 lovely hours spent on this feature... Hope i might have at least give someone a perspective on how to solve the issue or similar ones.

    My magic values:
    Max: 5
    Correction bias: 20000
     
    Last edited: Oct 7, 2020
  22. restush96

    restush96

    Joined:
    May 28, 2019
    Posts:
    141
    @Alturis2 add
    Mathf.Round
    on return code of WorldToScreenPointProjected should be perfect.