Get final, rendered, pixel size of VisualElement ?

I want to draw pixel-perfect (lines and pixels) on a VisualElement.
I did this before in uGUI by drawing on a Texture2D that had the same size in device-pixels as the RectTransform.

The same approach would also work for VisualElements. I can create and assign a Texture to the VisualElement and draw on this texture.
Problem is that the result is scaled because I don’t know the final device-pixel size. As a result, everything is blurry.

How do I find the final, rendered size in device-pixels for a VisualElement?

For example, when I have a VisualElement with width “100px” then these 100 px will be scaled for the device such that it may be more than 100 device-pixels.

Are there other ways to draw pixels on a VisualElement?

2 Likes

If you want the elements to scale you should use %. After the element has been created, you can use VisualElement.worldBound.size (or other worldBound properties) to get the width and height. VisualElement.resolvedStyle might also work but I have not tested it yet. To make sure the element has been created you should subscribe to the GeometryChangedEvent or AttatchToPanelEvent.

VisualElement.worldBound.size and VisualElement.resolvedStyle.width is both given in reference resolution (of the PanelSettings of the UIDocument). I want this in device pixels.

However, I think I could multiply resolvedStyle.width by the factor (device-width / reference-width) to get the actual width in device-pixels.

I guess the UIElements API is always using the reference resolution?

You’re right, the layout values are always using scaled points units. The actual scale is a mix of the PanelSettings scaling options and the device’s dpi values.

Right now, to get the pixel-size of an element, you can multiply the resolvedStyle.width/height by the scaling ratio.

This is a bit tricky to get at the moment, but you can fetch the scaling ratio by casting the element’s panel to a
BaseVisualElementPanel, then access its scaledPixelsPerPoint property.

1 Like

Is there an updated simpler method to get the scaling factor?
I’m having trouble to access the BaseVisualElementPanel properties since its burried in the UIElements namespace.

I can think of two methods to determine the scaling factor without it:

  • Restrict to a scaling mode i.e. scale by width and then calculate the ratio between reference resolution and screen resolution
  • Generate a background visual element that spans the whole screen get its size and combine with the screen size.

Both methods are cumbersome or restrictive. Would love a simpler method.

1 Like

Unfortunately nothing has changed in this area and there’s nothing planned around this subject in the near future.

@Cery1 Here’s what I use:

class PanelHelper
{
    private readonly IPanel _panel;
    private readonly PanelSettings _panelSettings;
    private Vector2 _scalingRatio;
    private int _cacheFrame;
    private float _screenHeight;
    private Vector2 _panelScreenMin;

    public PanelHelper(UIDocument document)
    {
        _panel = document.rootVisualElement.panel;
        _panelSettings = document.panelSettings;
    }

    public Vector2 screenToPanel(Vector2 v)
    {
        return RuntimePanelUtils.ScreenToPanel(_panel, v);
    }

    public Vector2 panelToScreen(Vector2 v)
    {
        if(Time.frameCount != _cacheFrame)
            updateCache();
        Vector2 result = (v - _panelScreenMin) * _scalingRatio;
        result.y = _screenHeight - result.y;
        return result;
    }

    public Rect panelToScreen(Rect r)
    {
        Vector2 TL = panelToScreen(new Vector2(r.xMin, r.yMin));
        Vector2 BR = panelToScreen(new Vector2(r.xMax, r.yMax));
        Vector2 min = new(Mathf.Min(a.x, b.x), Matf.Min(a.y, b.y));
        Vector2 size = new(Mathf.Abs(a.x - b.x), Mathf.Abs(a.y - b.y));
        return new Rect(min, size);
    }

    public float getScaleWeight() => _panelSettings.scaleMode switch
    {
        PanelScaleMode.ConstantPixelSize => .5f,
        PanelScaleMode.ConstantPhysicalSize => .5f,
        PanelScaleMode.ScaleWithScreenSize => _panelSettings.match,
        _ => throw new ArgumentOutOfRangeException()
    };

    public Vector2 getScalingRatio()
    {
        if(Time.frameCount != _cacheFrame)
            updateCache();
        return _scalingRatio;
    }

    private void updateCache()
    {
        _cacheFrame = Time.frameCount;
        Vector2 ssize = new(Screen.width, Screen.height);
        Vector2 stpMin = RuntimePanelUtils.ScreenToPanel(_panel, new Vector2(0, 0));
        Vector2 stpMax = RuntimePanelUtils.ScreenToPanel(_panel, ssize);
        Vector2 stpSize = stpMax - stpMin;
        _screenHeight = ssize.y;
        _panelScreenMin = stpMin;
        _scalingRatio = ssize / stpSize;
    }
}

To get the pixel size of a visual element, use it like this:

Vector2 size = panelHelper.panelToScreen(visualElement.worldBound).size;

Kinda wish Unity would add something like that to RuntimePanelUtils, but I guess there might be contexts in which it doesn’t work or gets more complicated.

4 Likes

a simple solution, if you know that your UI Document is full-screen, would be:
screenToPanelRatio = Screen.width / fullScreenUIDocument.rootVisualElement.resolvedStyle.width;

Then you can find the size of you panel in pixels by multiplying it by this ratio.

Also, don’t forget that the .resolvedStyle will only give you correct values after the on
GeometryChangedEvent callback, just like @Nexer8 has mentioned.

1 Like

Very happy to find this answer, until I discovered BaseRuntimePanel and RuntimePanel are internals :smile:

As @burningmime proposed, I wished it would be supported in the RuntimePanelUtils, at least for the classic overlay use case (I saw the more complex use cases ).

1 Like

So I did this :

var screenBoundsInPanelCoords = RuntimePanelUtils.ScreenToPanel(_button.panel,
   new Vector2(Screen.width, Screen.height));
              
Vector2 buttonPosInPanel = _button.worldBound.center;

Vector2 buttonPosInScreen = new Vector2(buttonPosInPanel.x * Screen.width / screenBoundsInPanelCoords.x,
   Screen.height - buttonPosInPanel.y * Screen.height / screenBoundsInPanelCoords.y);
2 Likes

For my purposes it was enough to RuntimePanelUtils.ScreenToPane(panel,Vector2.one), and then invert that to get the scaling factor

1 Like

What is a and b?

Sorry that was some old code and I may have edited it after posting it here or something.

EDIT: OK; yeah, this is the function from my code…

            public Rect panelToScreen(Rect r)
            {
                Vector2 TL = panelToScreen(new Vector2(r.xMin, r.yMin));
                Vector2 BR = panelToScreen(new Vector2(r.xMax, r.yMax));
                return Utils.absRect(TL, BR);
            }

Which relies on a separate helper method…

        public static Rect absRect(Vector2 a, Vector2 b)
        {
            Vector2 min = new(Math.Min(a.x, b.x), Math.Min(a.y, b.y));
            Vector2 size = new(Math.Abs(a.x - b.x), Math.Abs(a.y - b.y));
            return new Rect(min, size);
        }

But the basic idea is just the same. Get the top-left and bottom-right points in screen space.

1 Like

You’re my life saver, it’s exactly what I needed. Many thanks. :slight_smile: