Test if UI element is visible on screen

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

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.

Usage

bool isFullyVisible = myRectTransform.IsFullyVisibleFrom(myCamera);

RendererExtensions.cs

using UnityEngine;

public static class RendererExtensions
{
    /// <summary>
    /// Counts the bounding box corners of the given RectTransform that are visible from the given Camera in screen space.
    /// </summary>
    /// <returns>The amount of bounding box corners that are visible from the Camera.</returns>
    /// <param name="rectTransform">Rect transform.</param>
    /// <param name="camera">Camera.</param>
    private static int CountCornersVisibleFrom(this RectTransform rectTransform, Camera camera)
    {
        Rect screenBounds = new Rect(0f, 0f, Screen.width, Screen.height); // Screen space bounds (assumes camera renders across the entire screen)
        Vector3[] objectCorners = new Vector3[4];
        rectTransform.GetWorldCorners(objectCorners);

        int visibleCorners = 0;
        Vector3 tempScreenSpaceCorner; // Cached
        for (var i = 0; i < objectCorners.Length; i++) // For each corner in rectTransform
        {
            tempScreenSpaceCorner = camera.WorldToScreenPoint(objectCorners[i]); // Transform world space position of corner to screen space
            if (screenBounds.Contains(tempScreenSpaceCorner)) // If the corner is inside the screen
            {
                visibleCorners++;
            }
        }
        return visibleCorners;
    }

    /// <summary>
    /// Determines if this RectTransform is fully visible from the specified camera.
    /// Works by checking if each bounding box corner of this RectTransform is inside the cameras screen space view frustrum.
    /// </summary>
    /// <returns><c>true</c> if is fully visible from the specified camera; otherwise, <c>false</c>.</returns>
    /// <param name="rectTransform">Rect transform.</param>
    /// <param name="camera">Camera.</param>
    public static bool IsFullyVisibleFrom(this RectTransform rectTransform, Camera camera)
    {
        return CountCornersVisibleFrom(rectTransform, camera) == 4; // True if all 4 corners are visible
    }

    /// <summary>
    /// Determines if this RectTransform is at least partially visible from the specified camera.
    /// Works by checking if any bounding box corner of this RectTransform is inside the cameras screen space view frustrum.
    /// </summary>
    /// <returns><c>true</c> if is at least partially visible from the specified camera; otherwise, <c>false</c>.</returns>
    /// <param name="rectTransform">Rect transform.</param>
    /// <param name="camera">Camera.</param>
    public static bool IsVisibleFrom(this RectTransform rectTransform, Camera camera)
    {
        return CountCornersVisibleFrom(rectTransform, camera) > 0; // True if any corners are visible
    }
}
50 Likes

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). :slight_smile:

1 Like

@KGC , you’re a god!
Thanks! <3

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?

I have the same doubt, because call this from update method doesn’t looks like an optimal way.

1 Like

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 :slight_smile:

1 Like

Thanks for the post KGC. I was just about to develop something similar, but you’ve taken the hard work out for me.

If you are caching Vector3 tempScreenSpaceCorner then you might especially do it with .Length

1 Like

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?

2 Likes

@KGC , is it possible that I use the RenderExtension.cs in an open source unity asset, if I specify you as author?

Sure, go right ahead :slight_smile:

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.

public static bool IsPartlyVisible(this RectTransform rectTransform, Camera camera)
    {
       
        bool result = false;
       
        Vector2 pos = rectTransform.GetPosition(CoordinateSystem.ScreenSpacePixels, true);
        Vector2 size = rectTransform.GetSize(CoordinateSystem.ScreenSpacePixels) * 0.5f;
       
        Vector2[] objectCorners = new Vector2[4];
       
        objectCorners[0] = new Vector2(pos.x - size.x, pos.y - size.y);
        objectCorners[1] = new Vector2(pos.x - size.x, pos.y + size.y);
        objectCorners[2] = new Vector2(pos.x + size.x, pos.y - size.y);
        objectCorners[3] = new Vector2(pos.x + size.x, pos.y + size.y);
       
        Rect screenBounds = new Rect(0f, 0f, Screen.width, Screen.height);
       
        int visibleCorners = 0;
       
        for (var i = 0; i < objectCorners.Length; i++)
        {
            if (screenBounds.Contains(objectCorners[i]))
            {
                visibleCorners++;
            }
        }
       
        if (visibleCorners>0) // If at least one corner is inside the screen
        {
            result = true;
        }
       
        return result;
    }

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.

public static Vector3 GetGUIElementOffset(RectTransform rect)
    {
        Rect screenBounds = new Rect(0f, 0f, Screen.width, Screen.height);
        Vector3[] objectCorners = new Vector3[4];
        rect.GetWorldCorners(objectCorners);

        var xnew = 0f;
        var ynew = 0f;
        var znew = 0f;

        for (int i = 0; i < objectCorners.Length; i++)
        {
            if (objectCorners[i].x < screenBounds.xMin)
            {
                xnew = screenBounds.xMin - objectCorners[i].x;
            }
            if (objectCorners[i].x > screenBounds.xMax)
            {
                xnew = screenBounds.xMax - objectCorners[i].x;
            }
            if (objectCorners[i].y < screenBounds.yMin)
            {
                ynew = screenBounds.yMin - objectCorners[i].y;
            }
            if (objectCorners[i].y > screenBounds.yMax)
            {
                ynew = screenBounds.yMax - objectCorners[i].y;
            }
        }

        return new Vector3(xnew, ynew, znew);

    }
6 Likes

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!

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.

using UnityEngine;

public static class RectTransformExtension
{
    /// <summary>
    /// Counts the bounding box corners of the given RectTransform that are visible in screen space.
    /// </summary>
    /// <returns>The amount of bounding box corners that are visible.</returns>
    /// <param name="rectTransform">Rect transform.</param>
    /// <param name="camera">Camera. Leave it null for Overlay Canvasses.</param>
    private static int CountCornersVisibleFrom(this RectTransform rectTransform, Camera camera = null)
    {
        Rect screenBounds = new Rect(0f, 0f, Screen.width, Screen.height); // Screen space bounds (assumes camera renders across the entire screen)
        Vector3[] objectCorners = new Vector3[4];
        rectTransform.GetWorldCorners(objectCorners);

        int visibleCorners = 0;
        Vector3 tempScreenSpaceCorner; // Cached
        for (var i = 0; i < objectCorners.Length; i++) // For each corner in rectTransform
        {
            if (camera != null)
                tempScreenSpaceCorner = camera.WorldToScreenPoint(objectCorners[i]); // Transform world space position of corner to screen space
            else
            {
                Debug.Log(rectTransform.gameObject.name+" :: "+objectCorners[i].ToString("F2"));
                tempScreenSpaceCorner = objectCorners[i]; // If no camera is provided we assume the canvas is Overlay and world space == screen space
            }

            if (screenBounds.Contains(tempScreenSpaceCorner)) // If the corner is inside the screen
            {
                visibleCorners++;
            }
        }
        return visibleCorners;
    }

    /// <summary>
    /// Determines if this RectTransform is fully visible.
    /// Works by checking if each bounding box corner of this RectTransform is inside the screen space view frustrum.
    /// </summary>
    /// <returns><c>true</c> if is fully visible; otherwise, <c>false</c>.</returns>
    /// <param name="rectTransform">Rect transform.</param>
    /// <param name="camera">Camera. Leave it null for Overlay Canvasses.</param>
    public static bool IsFullyVisibleFrom(this RectTransform rectTransform, Camera camera = null)
    {
        if (!rectTransform.gameObject.activeInHierarchy)
            return false;

        return CountCornersVisibleFrom(rectTransform, camera) == 4; // True if all 4 corners are visible
    }

    /// <summary>
    /// Determines if this RectTransform is at least partially visible.
    /// Works by checking if any bounding box corner of this RectTransform is inside the screen space view frustrum.
    /// </summary>
    /// <returns><c>true</c> if is at least partially visible; otherwise, <c>false</c>.</returns>
    /// <param name="rectTransform">Rect transform.</param>
    /// <param name="camera">Camera. Leave it null for Overlay Canvasses.</param>
    public static bool IsVisibleFrom(this RectTransform rectTransform, Camera camera = null)
    {
        if (!rectTransform.gameObject.activeInHierarchy)
            return false;

        return CountCornersVisibleFrom(rectTransform, camera) > 0; // True if any corners are visible
    }
}
7 Likes

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:

Transform camForward = camera.forward;
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:

if (tempScreenSpaceCorner.z >= 0 && screenBounds.Contains(tempScreenSpaceCorner)) {
    visibleCorners++;
}

if tempScreenSpaceCorner.z is less than 0 then you’ve totally faced away from the RectTransform!

1 Like

Thanks a lot Pavlko

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

1 Like

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.

1 Like