Search Unity

Test if UI element is visible on screen

Discussion in 'UGUI & TextMesh Pro' started by mimminito, Oct 28, 2014.

  1. mimminito

    mimminito

    Joined:
    Feb 10, 2010
    Posts:
    780
    Hi,

    I am trying to test if a UI element is visible on the screen. Im currently using a Image control, and would like to know if its visible by the camera, or if its outside of its bounds. I cannot seem to find a way to determine this, and as we do not have a renderer component I cannot do a GeometryUtility.TestPlanesAABB test.
    Any help would be appreciated.

    Adam
     
  2. KGC

    KGC

    Joined:
    Oct 2, 2014
    Posts:
    12
    Sorry for necroing an old post, but I had a similar issue today, and could not find a complete solution via Goggle, so I wrote my own based on the fragmented solutions I could find on Unity Answers. Tested on a Canvas set to Screen Space Camera mode.

    Code (CSharp):
    1. bool isFullyVisible = myRectTransform.IsFullyVisibleFrom(myCamera);

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public static class RendererExtensions
    4. {
    5.     /// <summary>
    6.     /// Counts the bounding box corners of the given RectTransform that are visible from the given Camera in screen space.
    7.     /// </summary>
    8.     /// <returns>The amount of bounding box corners that are visible from the Camera.</returns>
    9.     /// <param name="rectTransform">Rect transform.</param>
    10.     /// <param name="camera">Camera.</param>
    11.     private static int CountCornersVisibleFrom(this RectTransform rectTransform, Camera camera)
    12.     {
    13.         Rect screenBounds = new Rect(0f, 0f, Screen.width, Screen.height); // Screen space bounds (assumes camera renders across the entire screen)
    14.         Vector3[] objectCorners = new Vector3[4];
    15.         rectTransform.GetWorldCorners(objectCorners);
    16.  
    17.         int visibleCorners = 0;
    18.         Vector3 tempScreenSpaceCorner; // Cached
    19.         for (var i = 0; i < objectCorners.Length; i++) // For each corner in rectTransform
    20.         {
    21.             tempScreenSpaceCorner = camera.WorldToScreenPoint(objectCorners[i]); // Transform world space position of corner to screen space
    22.             if (screenBounds.Contains(tempScreenSpaceCorner)) // If the corner is inside the screen
    23.             {
    24.                 visibleCorners++;
    25.             }
    26.         }
    27.         return visibleCorners;
    28.     }
    29.  
    30.     /// <summary>
    31.     /// Determines if this RectTransform is fully visible from the specified camera.
    32.     /// Works by checking if each bounding box corner of this RectTransform is inside the cameras screen space view frustrum.
    33.     /// </summary>
    34.     /// <returns><c>true</c> if is fully visible from the specified camera; otherwise, <c>false</c>.</returns>
    35.     /// <param name="rectTransform">Rect transform.</param>
    36.     /// <param name="camera">Camera.</param>
    37.     public static bool IsFullyVisibleFrom(this RectTransform rectTransform, Camera camera)
    38.     {
    39.         return CountCornersVisibleFrom(rectTransform, camera) == 4; // True if all 4 corners are visible
    40.     }
    41.  
    42.     /// <summary>
    43.     /// Determines if this RectTransform is at least partially visible from the specified camera.
    44.     /// Works by checking if any bounding box corner of this RectTransform is inside the cameras screen space view frustrum.
    45.     /// </summary>
    46.     /// <returns><c>true</c> if is at least partially visible from the specified camera; otherwise, <c>false</c>.</returns>
    47.     /// <param name="rectTransform">Rect transform.</param>
    48.     /// <param name="camera">Camera.</param>
    49.     public static bool IsVisibleFrom(this RectTransform rectTransform, Camera camera)
    50.     {
    51.         return CountCornersVisibleFrom(rectTransform, camera) > 0; // True if any corners are visible
    52.     }
    53. }
     
    Last edited: Mar 2, 2017
    syamilsynz, maramak, Rachan and 47 others like this.
  3. Anisoropos

    Anisoropos

    Joined:
    Jul 30, 2012
    Posts:
    102
    Thanks @KGC , your code works like a charm, is well documented and abides to best practices (static helper functions with private functionality and public accessors). :)
     
    drhmiri likes this.
  4. nngafook

    nngafook

    Joined:
    Sep 7, 2013
    Posts:
    5
    @KGC, you're a god!
    Thanks! <3
     
  5. Sir-Gatlin

    Sir-Gatlin

    Joined:
    Jan 18, 2013
    Posts:
    28
    Thank you! this is awesome. Noob question, where would be the best place to implement this? does it have to go into the update method? or is there a better way to listen for it to change?
     
  6. diegoadrada

    diegoadrada

    Joined:
    Nov 27, 2014
    Posts:
    59
    I have the same doubt, because call this from update method doesn't looks like an optimal way.
     
    Sir-Gatlin likes this.
  7. KGC

    KGC

    Joined:
    Oct 2, 2014
    Posts:
    12
    So in our project we use it to check if a UI animation should play. In that case we use a coroutine that checks either every frame or with some time delay (i think its the latter). It really depends. If you need to know with highest precision, then yes, put it in an update function. But you could probably get away with calling it only every 5th frame or so - in that that just use a coroutine :)
     
    Sir-Gatlin likes this.
  8. CalebBarton

    CalebBarton

    Joined:
    Jun 24, 2017
    Posts:
    4
    Thanks for the post KGC. I was just about to develop something similar, but you've taken the hard work out for me.
     
  9. YoungDeveloper

    YoungDeveloper

    Joined:
    Jun 28, 2013
    Posts:
    65
    If you are caching Vector3 tempScreenSpaceCorner then you might especially do it with .Length
     
    diegoadrada likes this.
  10. jjlehtinen

    jjlehtinen

    Joined:
    Aug 7, 2018
    Posts:
    1
    This doesn't seem to take into account the fact that the UI element might be covered by something else, so not actually visible, only if the bounds are contained within the camera bounds?
     
    FunFreighterGames likes this.
  11. Assambra

    Assambra

    Joined:
    May 22, 2017
    Posts:
    2
    @KGC, is it possible that I use the RenderExtension.cs in an open source unity asset, if I specify you as author?
     
  12. KGC

    KGC

    Joined:
    Oct 2, 2014
    Posts:
    12
    Sure, go right ahead :)
     
  13. Biggix

    Biggix

    Joined:
    Dec 8, 2014
    Posts:
    44
    I have faced a similar issue (I needed to know whether a RectTransform is at least partially visible within my camera viewport) and somehow the solution posted by @KGC didn't work for me. I believe this might happen because my RectTransforms were deeply nested inside a couple of ScrollViews so GetWorldCorners might be failing under those conditions.

    In any way, to solve this I resorted to a 3rd party plugin called EasierUI, which provides methods such as GetPosition and GetSize in world units.

    Code (CSharp):
    1. public static bool IsPartlyVisible(this RectTransform rectTransform, Camera camera)
    2.     {
    3.        
    4.         bool result = false;
    5.        
    6.         Vector2 pos = rectTransform.GetPosition(CoordinateSystem.ScreenSpacePixels, true);
    7.         Vector2 size = rectTransform.GetSize(CoordinateSystem.ScreenSpacePixels) * 0.5f;
    8.        
    9.         Vector2[] objectCorners = new Vector2[4];
    10.        
    11.         objectCorners[0] = new Vector2(pos.x - size.x, pos.y - size.y);
    12.         objectCorners[1] = new Vector2(pos.x - size.x, pos.y + size.y);
    13.         objectCorners[2] = new Vector2(pos.x + size.x, pos.y - size.y);
    14.         objectCorners[3] = new Vector2(pos.x + size.x, pos.y + size.y);
    15.        
    16.         Rect screenBounds = new Rect(0f, 0f, Screen.width, Screen.height);
    17.        
    18.         int visibleCorners = 0;
    19.        
    20.         for (var i = 0; i < objectCorners.Length; i++)
    21.         {
    22.             if (screenBounds.Contains(objectCorners[i]))
    23.             {
    24.                 visibleCorners++;
    25.             }
    26.         }
    27.        
    28.         if (visibleCorners>0) // If at least one corner is inside the screen
    29.         {
    30.             result = true;
    31.         }
    32.        
    33.         return result;
    34.     }
     
  14. WorldWideGlide

    WorldWideGlide

    Joined:
    Nov 3, 2013
    Posts:
    26
    I came across this thread and it helped me solve a similar issue that I was having. Here's a similar function that finds how much a UI element is offscreen and returns the offset. You can add this offset to the UI elements transform position to position it back on screen.

    Code (CSharp):
    1. public static Vector3 GetGUIElementOffset(RectTransform rect)
    2.     {
    3.         Rect screenBounds = new Rect(0f, 0f, Screen.width, Screen.height);
    4.         Vector3[] objectCorners = new Vector3[4];
    5.         rect.GetWorldCorners(objectCorners);
    6.  
    7.         var xnew = 0f;
    8.         var ynew = 0f;
    9.         var znew = 0f;
    10.  
    11.         for (int i = 0; i < objectCorners.Length; i++)
    12.         {
    13.             if (objectCorners[i].x < screenBounds.xMin)
    14.             {
    15.                 xnew = screenBounds.xMin - objectCorners[i].x;
    16.             }
    17.             if (objectCorners[i].x > screenBounds.xMax)
    18.             {
    19.                 xnew = screenBounds.xMax - objectCorners[i].x;
    20.             }
    21.             if (objectCorners[i].y < screenBounds.yMin)
    22.             {
    23.                 ynew = screenBounds.yMin - objectCorners[i].y;
    24.             }
    25.             if (objectCorners[i].y > screenBounds.yMax)
    26.             {
    27.                 ynew = screenBounds.yMax - objectCorners[i].y;
    28.             }
    29.         }
    30.  
    31.         return new Vector3(xnew, ynew, znew);
    32.  
    33.     }
     
    jiyarza, niravp_0x, Mortas and 3 others like this.
  15. Alexhator

    Alexhator

    Joined:
    May 23, 2019
    Posts:
    2
    Awesome!
    In my case, what I am trying to do is:

    Having a Scroll, detect if an item is visible or not. So, instead of considering the size of the screen is the size of the scroll. But I have not managed to make it work.

    Any ideas?

    Thank you in advance!
     
  16. Pavlko

    Pavlko

    Joined:
    Dec 23, 2012
    Posts:
    9
    Hello there,

    I successfully used your script @KGC, and I extended it to support RectTransforms in Overlay Canvasses (you just dont pass the Camera parameter).
    I also added a check for .activeInHierarchy to return false.

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public static class RectTransformExtension
    4. {
    5.     /// <summary>
    6.     /// Counts the bounding box corners of the given RectTransform that are visible in screen space.
    7.     /// </summary>
    8.     /// <returns>The amount of bounding box corners that are visible.</returns>
    9.     /// <param name="rectTransform">Rect transform.</param>
    10.     /// <param name="camera">Camera. Leave it null for Overlay Canvasses.</param>
    11.     private static int CountCornersVisibleFrom(this RectTransform rectTransform, Camera camera = null)
    12.     {
    13.         Rect screenBounds = new Rect(0f, 0f, Screen.width, Screen.height); // Screen space bounds (assumes camera renders across the entire screen)
    14.         Vector3[] objectCorners = new Vector3[4];
    15.         rectTransform.GetWorldCorners(objectCorners);
    16.  
    17.         int visibleCorners = 0;
    18.         Vector3 tempScreenSpaceCorner; // Cached
    19.         for (var i = 0; i < objectCorners.Length; i++) // For each corner in rectTransform
    20.         {
    21.             if (camera != null)
    22.                 tempScreenSpaceCorner = camera.WorldToScreenPoint(objectCorners[i]); // Transform world space position of corner to screen space
    23.             else
    24.             {
    25.                 Debug.Log(rectTransform.gameObject.name+" :: "+objectCorners[i].ToString("F2"));
    26.                 tempScreenSpaceCorner = objectCorners[i]; // If no camera is provided we assume the canvas is Overlay and world space == screen space
    27.             }
    28.  
    29.             if (screenBounds.Contains(tempScreenSpaceCorner)) // If the corner is inside the screen
    30.             {
    31.                 visibleCorners++;
    32.             }
    33.         }
    34.         return visibleCorners;
    35.     }
    36.  
    37.     /// <summary>
    38.     /// Determines if this RectTransform is fully visible.
    39.     /// Works by checking if each bounding box corner of this RectTransform is inside the screen space view frustrum.
    40.     /// </summary>
    41.     /// <returns><c>true</c> if is fully visible; otherwise, <c>false</c>.</returns>
    42.     /// <param name="rectTransform">Rect transform.</param>
    43.     /// <param name="camera">Camera. Leave it null for Overlay Canvasses.</param>
    44.     public static bool IsFullyVisibleFrom(this RectTransform rectTransform, Camera camera = null)
    45.     {
    46.         if (!rectTransform.gameObject.activeInHierarchy)
    47.             return false;
    48.  
    49.         return CountCornersVisibleFrom(rectTransform, camera) == 4; // True if all 4 corners are visible
    50.     }
    51.  
    52.     /// <summary>
    53.     /// Determines if this RectTransform is at least partially visible.
    54.     /// Works by checking if any bounding box corner of this RectTransform is inside the screen space view frustrum.
    55.     /// </summary>
    56.     /// <returns><c>true</c> if is at least partially visible; otherwise, <c>false</c>.</returns>
    57.     /// <param name="rectTransform">Rect transform.</param>
    58.     /// <param name="camera">Camera. Leave it null for Overlay Canvasses.</param>
    59.     public static bool IsVisibleFrom(this RectTransform rectTransform, Camera camera = null)
    60.     {
    61.         if (!rectTransform.gameObject.activeInHierarchy)
    62.             return false;
    63.  
    64.         return CountCornersVisibleFrom(rectTransform, camera) > 0; // True if any corners are visible
    65.     }
    66. }
     
  17. mmmshuddup

    mmmshuddup

    Joined:
    Feb 9, 2015
    Posts:
    4
    I tried this in VR and it only partially worked. My canvas starts in the viewport and this extension returns 4 visible corners and as I slow look away it goes down to 3, then 2, and then 1 and then slowly goes back up as I turn around.

    I think this script may have assumed the camera would be static but in my case it's not, the camera viewport is constantly changing as the user looks around, so how can I rectify that?

    I tried using:

    Code (CSharp):
    1. Transform camForward = camera.forward;
    2. Rect screenBounds = new Rect(camForward.x, camForward.y, Screen.width, Screen.height);
    But that hasn't worked. I also tried replacing WorldToScreenPoint with WorldToViewportPoint but that didn't work either.

    EDIT:

    I finally fixed this by checking the z axis like so:

    Code (CSharp):
    1. if (tempScreenSpaceCorner.z >= 0 && screenBounds.Contains(tempScreenSpaceCorner)) {
    2.     visibleCorners++;
    3. }
    if tempScreenSpaceCorner.z is less than 0 then you've totally faced away from the RectTransform!
     
    Last edited: Nov 5, 2019
    AlexanderNox likes this.
  18. Leniaal

    Leniaal

    Joined:
    Nov 7, 2012
    Posts:
    119
    Thanks a lot Pavlko
     
  19. lejean

    lejean

    Joined:
    Jul 4, 2013
    Posts:
    392
    This returns fails if the rectangle is bigger then the canvas and overlaps, but the corners lie outside of it.
    So might not be ideal in all cases
     
    Last edited: Apr 3, 2021
    wondermagic likes this.
  20. leegod

    leegod

    Joined:
    May 5, 2010
    Posts:
    2,476
    I don't know why but this don't return true.

    I tested at my UI children objects generated at Unity scrollview.

    All returns false even if I can see it.
     
    mmmshuddup likes this.
  21. shuskry

    shuskry

    Joined:
    Oct 10, 2015
    Posts:
    462
    Same here....
     
  22. nimrodbens

    nimrodbens

    Joined:
    Feb 14, 2018
    Posts:
    6
    any solution to check if the element is just not inside the visible area of a scroll rect?
     
    mmmshuddup likes this.
  23. GuaxinimDantas

    GuaxinimDantas

    Joined:
    Nov 13, 2017
    Posts:
    1
    I've tried a bunch of things in this thread, but or it wasn't what I was looking for or it wasn't working for me!
    Then I went to UnityDocs and tried other stuff but for me, nothing was working.

    Rect.Overlaps was the most promising function, but even this wasn't resulting in anything useful, so I bruteforced two "world space" Rects and then I used Rect.Overlaps and It worked for my needs.

    I need to metion that all my canvases are "ScreenSpace - Camera"

    Here is the extension
    Code (CSharp):
    1. using UnityEngine;
    2. public static class RectTransformExtensions
    3. {
    4.     /// <summary>
    5.     /// Checks if this RectTransform is overlaping the other RectTransform in the world space [Z Neutral]
    6.     /// </summary>
    7.     /// <param name="overlaping"></param>
    8.     /// The object overlaping
    9.     /// <param name="overlaped"></param>
    10.     /// the object being overlaped
    11.     /// <returns></returns>
    12.     public static bool WorldSpaceOverlaps(this RectTransform overlaping, RectTransform overlaped)
    13.     {
    14.         Vector3[] aux = new Vector3[4]; //Cache
    15.         //Get worldSpace corners and  then creates a Rect considering the first and the third values are diagonal
    16.         overlaping.GetWorldCorners(aux);
    17.         Rect overlapingRect = new Rect(aux[0], (aux[2] - aux[0]));
    18.  
    19.         //Reapeats for the other RectTranform
    20.         overlaped.GetWorldCorners(aux);
    21.         Rect overlapedRect = new Rect(aux[0], (aux[2] - aux[0]));
    22.  
    23.         //Use Rect.Overlaps to do the necessary calculations
    24.         return (overlapedRect.Overlaps(overlapingRect, true));
    25.     }
    26. }
    27.  
    And here is an example on how I used it:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Events;
    3.  
    4. public class InsideRectEvent : MonoBehaviour
    5. {
    6.     [Header("Actions")]
    7.     [SerializeField] private UnityEvent onInsideAction = null;
    8.     [SerializeField] private UnityEvent onOutiseAction = null;
    9.     [Header("Conditionals")]
    10.     [SerializeField] private RectTransform overlaping = null;
    11.     [SerializeField] private RectTransform overlaped = null;
    12.     [SerializeField] string overlapedName = "";
    13.     internal bool isInside { get; private set; } = true;
    14.     private void Start()
    15.     {
    16.         if (overlaping == null)
    17.         {
    18.             overlaping = GetComponent<RectTransform>();
    19.         }
    20.         if (overlaped == null)
    21.         {
    22.             overlaped = GameObject.Find(overlapedName).GetComponent<RectTransform>();
    23.         }
    24.     }
    25.  
    26.     private void Update()
    27.     {
    28.         if (!isInside)
    29.         {
    30.             if (overlaping.WorldSpaceOverlaps(overlaped))
    31.             {
    32.                 isInside = true;
    33.                 onInsideAction?.Invoke();
    34.                 Debug.Log("Inside " + gameObject.name);
    35.             }
    36.         }
    37.         else
    38.         {
    39.             if (!overlaping.WorldSpaceOverlaps(overlaped))
    40.             {
    41.                 isInside = false;
    42.                 onOutiseAction?.Invoke();
    43.                 Debug.Log("Outside "+ gameObject.name);
    44.             }
    45.         }
    46.  
    47.     }
    48. }
    49.  

    Optimizations on your own ;)
     
    Last edited: Dec 21, 2020
    Squize likes this.
  24. shuskry

    shuskry

    Joined:
    Oct 10, 2015
    Posts:
    462
    It's in ... Because when I click where the button is supposed to be, it's work...

    EDIT : When remove the default material to let him "none" ... the image appear now ... !
    Don't know why ... other image have this default material and are visible ...
     
  25. Goty-Metal

    Goty-Metal

    Joined:
    Apr 4, 2020
    Posts:
    168
    Sadly, @KGC code doesn't work on my case for some reason, it always returns false/0 cornerseven if the element is clearly fully visible but luckily @WorldWideGlide solution worked like a charm, and having the offset is super useful, thanks to all by the way great job guys <3
     
  26. w34edrtfg

    w34edrtfg

    Joined:
    Nov 23, 2014
    Posts:
    72
    Very useful!

    I'm leaving a slightly shortened version here:
    Code (CSharp):
    1.  
    2. public static Vector2 GetGUIElementOffset(RectTransform rect) {
    3.         Rect screenBounds = new Rect(0f, 0f, Screen.width, Screen.height);
    4.         Vector3[] objectCorners = new Vector3[4];
    5.         rect.GetWorldCorners(objectCorners);
    6.  
    7.         Vector2 offset = new Vector2(0, 0);
    8.  
    9.         for (int i = 0; i < objectCorners.Length; i++) {
    10.             if (objectCorners[i].x < screenBounds.xMin) {
    11.                 offset.x = screenBounds.xMin - objectCorners[i].x;
    12.             }
    13.             if (objectCorners[i].x > screenBounds.xMax) {
    14.                 offset.x = screenBounds.xMax - objectCorners[i].x;
    15.             }
    16.             if (objectCorners[i].y < screenBounds.yMin) {
    17.                 offset.y = screenBounds.yMin - objectCorners[i].y;
    18.             }
    19.             if (objectCorners[i].y > screenBounds.yMax) {
    20.                 offset.y = screenBounds.yMax - objectCorners[i].y;
    21.             }
    22.         }
    23.  
    24.         return offset;
    25.     }
    26.  
     
    Goty-Metal likes this.
  27. macsimilian

    macsimilian

    Joined:
    Sep 19, 2020
    Posts:
    25
    @KGC Thank you, this worked perfectly out of the box, with my World Space canvas!
     
  28. niravp_0x

    niravp_0x

    Joined:
    Jul 2, 2020
    Posts:
    1
    Thanks mate. Just what I needed to develop my logic further.
     
  29. awsapps

    awsapps

    Joined:
    Jun 15, 2021
    Posts:
    74
    Notice that the solution given by KGC is not fully correct.
    For example, If you take an image big enough to encapsulate the screen, the method " IsVisibleFrom" would return false, because there's no corner displayed on the screen.

     
  30. radiantboy

    radiantboy

    Joined:
    Nov 21, 2012
    Posts:
    1,633
    what is CoordinateSystem?
     
  31. a1creator

    a1creator

    Joined:
    Jan 18, 2021
    Posts:
    24
    THANK YOU THANK YOU THANK YOU THANK YOU!!
    It took me 10 mins to figure out that KGC only made it for world view, thank you for doing my work for me!!
     
    radiantboy likes this.
  32. thanhnguyenkim

    thanhnguyenkim

    Joined:
    May 8, 2015
    Posts:
    4
  33. Yuri_Yuks

    Yuri_Yuks

    Joined:
    Jan 20, 2021
    Posts:
    1
    The simple way:
    Code (CSharp):
    1.       if(RectTransformUtility.RectangleContainsScreenPoint(YourCanvas, YourRect.position))
    2.         {
    3.             Debug.Log("Visible");
    4.         }
    5.         else Debug.Log("Hide");
     
    Alverik and radiantboy like this.
  34. radiantboy

    radiantboy

    Joined:
    Nov 21, 2012
    Posts:
    1,633
    does anyone know a solution that works with screen space canvasses?
     
  35. notunusual

    notunusual

    Joined:
    Jun 13, 2016
    Posts:
    12
  36. Rachan

    Rachan

    Joined:
    Dec 3, 2012
    Posts:
    781
    No need camera Just use this

    Code (CSharp):
    1.  public static bool IsBecomeVisible(this RectTransform rectTransform)
    2.     {
    3.         Vector3[] v = new Vector3[4];
    4.         rectTransform.GetWorldCorners(v);
    5.  
    6.         float maxY = Mathf.Max(v[0].y, v[1].y, v[2].y, v[3].y);
    7.         float minY = Mathf.Min(v[0].y, v[1].y, v[2].y, v[3].y);
    8.  
    9.         if (maxY < 0 || minY > Screen.height)
    10.         {
    11.             return false;
    12.         }
    13.         else
    14.         {
    15.             return true;
    16.         }
    17.     }
    and add them to static class as same as RendererExtension
     
    Last edited: Dec 22, 2023