Close

Unity 3D – UI Target Tracker on pointed 3D Object

In this short tutorial, we are going to implement a visual tracker around objects that are pointed to with the mouse cursor. The tracker is an UI element, ie. it’s in the 2D UI canvas space. It draws a scaled rectangle around the objects and adds icons and texts beside that can hold status or symbols.


In order to implement this kind of 3D object tracker that is projected onto the 2D UI canvas, it’s important to understand how Unity implements UI elements in general. The documentation does a good job explaining the concepts of anchors, coordinates and pivot points, so there is no need to do this here.

In terms of game object hierarchy, the entire construct is built like this:

The “UITrackerElement” remains always active in order to receive update events. On this object is attached the script that does the magic. The sub-elements are adapted by the script. The “RectangleImage” is a 9-slice sprite (“Assets/UI Target Selection Box Sprite.psd“) that gets resized to surround the 3D object. The other elements are placed on the right-hand side of the rectangle.

The entire problem resides in finding the rectangle that does visually surround the 3D object when mapping it onto the 2D canvas. The idea behind the logic implemented below is to simply project all points of the 3D bounds to the 2D surface and then take the most outer points in each direction. In the Unity context, “Bounds” are approximations made by Unity about the objects location and its extents, so all word is done for you by the engine. Next, adding a bit of padding distance et voilà…

Note that the way the coordinates are set in the below script only works correctly if anchor points are set to 0 – that’s why it’s enforced in the “Start()” method. Relative position of texts and icons can be modified using the Rect Transform X,Y coordinates, which are conserved.

Lastest version of source code: https://github.com/imifos/unity3d-workspace/UITargetTrackerOnCursor.

Assets/UITargetTracker.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;


public class UITargetTracker : MonoBehaviour {

    // Defines how much % the tracker rectangle is enlarged compared to the 3D screen object bounds
    public float rectanglePaddingMultiplier=0.1f;

    // Here, we get the current cursor selection from
    private ScreenPointerToObjectSelectionMapper selectionManager;
        
    //
    //
    void Start()
    {
        selectionManager = FindObjectOfType<ScreenPointerToObjectSelectionMapper>();

        // Set anchor and pivot points of main and child objects to overwrite bad configuration the user made in the inspector.
        // The Y size of sub-elements other than the rectangle is taken from the inspector and not modified.
        RectTransform rt = this.GetComponent<RectTransform>();
        rt.position = Vector2.zero; // left bottom corner
        rt.anchorMax = Vector2.zero; 
        rt.anchorMin = Vector2.zero; 
        rt.pivot = Vector2.zero;
        
        foreach (Transform child in transform)
        {
            child.gameObject.SetActive(false);

            rt = child.GetComponent<RectTransform>();
            rt.anchorMax = Vector2.zero; // left bottom 
            rt.anchorMin = Vector2.zero; 
            rt.pivot = Vector2.zero;
        }
    }

    // 
    //
    void Update()
    {
        if (selectionManager.selectedObject != null)
        {
            // New tracker rectangle size
            Rect visualRect = TargetObjectBoundsToScreenSpace(selectionManager.selectedObject.GetComponentInChildren<Renderer>());

            // Parent UI component position on the target object visual 2D UI position (anchor is 0,0)
            RectTransform rt = GetComponent<RectTransform>();
            rt.position = new Vector2(visualRect.xMax, visualRect.yMin);
                   
            // Adjust all child elements. The tracker rectangle image to the size of the visual 2D bounds and
            // the other object statically on the side, attacked to the rectangle
            foreach (Transform child in transform)
            {
                // For testing, to see if it's working
                if (child.name == "Text1")
                    child.GetComponent<Text>().text = selectionManager.selectedObject.name;
                
                // Make all child elements visible
                // The parent UI components itself is always active, otherwise it does not receive update events
                child.gameObject.SetActive(true);

                float xPadding = visualRect.width * rectanglePaddingMultiplier;
                float yPadding = visualRect.height * rectanglePaddingMultiplier;
                                
                if (child.name == "RectangleImage")
                {
                    // Resize the tracker sprite rectangle
                    rt = child.GetComponent<RectTransform>();
                    rt.position = new Vector2(visualRect.xMin-xPadding, visualRect.yMin-yPadding);
                    rt.sizeDelta = new Vector2(visualRect.width+xPadding*2, visualRect.height+yPadding*2);
                }
                else {
                    // Reposition the other objects (texts and sprites) beside the tracker rectangle
                    rt = child.GetComponent<RectTransform>();
                    rt.position = new Vector2(visualRect.xMin+ visualRect.width + xPadding , rt.position.y);
                }
            }

        }
        else
        {
            // No selection, no tracker display
            foreach (Transform child in transform)
                child.gameObject.SetActive(false);
        }
    }
        
    /*
     * Project the 3D scene object towards the 2D UI canvas, then take the most left, the most right, the most up and the most 
     * down 2D points of all to determine the outer visual bounds on the UI overlay of the object. This is the size of the 
     * rectangle "visually" surrounding the object when drawn on the UI plane.
     */
    private Rect TargetObjectBoundsToScreenSpace(Renderer r)
    {
        // Object visual rectangle in world space
        Bounds b = r.bounds;
        
        // Calculate the screen space rectangle surrounding the 3D object
        Camera c = Camera.main;
        Rect rect = Rect.zero;
        Vector3 screenSpacePoint;

        screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x + b.extents.x, b.center.y + b.extents.y, b.center.z + b.extents.z));
        AdjustRect(ref rect, screenSpacePoint, true);
        screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x + b.extents.x, b.center.y + b.extents.y, b.center.z - b.extents.z));
        AdjustRect(ref rect, screenSpacePoint);
        screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x + b.extents.x, b.center.y - b.extents.y, b.center.z + b.extents.z));
        AdjustRect(ref rect, screenSpacePoint);
        screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x + b.extents.x, b.center.y - b.extents.y, b.center.z - b.extents.z));
        AdjustRect(ref rect, screenSpacePoint);
        screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x - b.extents.x, b.center.y + b.extents.y, b.center.z + b.extents.z));
        AdjustRect(ref rect, screenSpacePoint);
        screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x - b.extents.x, b.center.y + b.extents.y, b.center.z - b.extents.z));
        AdjustRect(ref rect, screenSpacePoint);
        screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x - b.extents.x, b.center.y - b.extents.y, b.center.z + b.extents.z));
        AdjustRect(ref rect, screenSpacePoint);
        screenSpacePoint = c.WorldToScreenPoint(new Vector3(b.center.x - b.extents.x, b.center.y - b.extents.y, b.center.z - b.extents.z));
        AdjustRect(ref rect, screenSpacePoint);

        return rect;
    }
    
    private void AdjustRect(ref Rect rect,Vector3 pnt,bool firstCall=false) {
        if (firstCall) {
            rect.xMin=pnt.x;
            rect.yMin = pnt.y;
            rect.xMax = pnt.x;
            rect.yMax = pnt.y;
        }
        else {
            rect.xMin = Mathf.Min(rect.xMin, pnt.x);
            rect.yMin = Mathf.Min(rect.yMin, pnt.y);
            rect.xMax = Mathf.Max(rect.xMax, pnt.x);
            rect.yMax = Mathf.Max(rect.yMax, pnt.y);
        }
    }

}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

By continuing to use the site, you agree to the use of cookies. more information

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this.

Close