UI elements on main camera shaking

Hi, I have a problem with my UI elements.

I used to have 2 cameras in my project, one for everything that’s not UI and one for UI elements.

I needed to use a UI Camera because my project uses a Pixel Perfect camera component and Screen Space - Overlay didn’t scale the UI when played in resolutions that require frame cropping and having the UI in the main camera makes the UI elements shake.

Now recently I implemented URP into my project and since its a 2D project I cannot use multiple cameras anymore.

So I am forced to use UI on the main camera again which makes my UI elements shake.

Gif here.
gratefulhealthyaxolotl

The shaking doesn’t happen when Canvas is set to Screen Space - Overlay or when Pixel Perfect camera component is turned off but neither is viable for my Pixel art project since that causes issues with scaling.

Anyone know how to tackle it?
Thank you.

Bump.

Seem that I cannot use Screen Space - Overlay because, from what I can tell, It’s tied to the game window so that doesn’t work with a Pixel Perfect camera since the “game view” is not the entire window sometimes.

And since I cannot use multiple cameras that means I’m forced to use Screen Space - Camera which is still shaking.

EDIT: UI elements aren’t shaking when Pixel Perfect camera component is turned off, the game view just doesn’t scale correctly.

Can’t think of any solution to this …anyone?

Bump, please anyone?

(EDIT: Not ideal, forcing a lower resolution sometimes. Better solution below.)

Bump,

so I have tried a couple of approaches.

First I tried to play around with the values in the Canvas Scaler script, manually setting screen size that affects the scale factor. The idea was that I would “trick” the Canvas set to Screen Space - Overlay into thinking that the screen size in for example 800x600 is actually 640x360 so the overlay would correctly fit the game camera. Seem tho that the screen size that affects the scale factor in SetScaleFactor(scaleFactor); doesn’t determine the position, only size, since it just made the UI smaller. If someone knows how to force my intended effect please let me know.

Secondly I know that the Screen Space - Overlay works correctly if the screen size is a multiple of 640x360, I thought that restricting the game to work only in those resolutions would fix my problem. So I made a
ForceResolution script that forces the game to run in “viable” resolutions.

The idea is that I get the highest resolution the monitor supports and set my game resolution to that, then i check the current screen resolution and set the “viable” resolution (e.g. a 800x600 screen fits a 640x360 but 1366x768 can fit a 1280x720 resolution).

This approach “works” but…

Now you might want to sit down for this.
Code:

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

public class ForceResolution : MonoBehaviour
{
    public TextMeshProUGUI res;
    public TextMeshProUGUI setGreen;
    void Start()
    {
        Resolution[] resArray = Screen.resolutions;
        Screen.SetResolution(resArray[resArray.Length-1].width, resArray[resArray.Length-1].height,true);

        res.text = Screen.currentResolution.width.ToString() + ", " + Screen.currentResolution.height.ToString();
      
        if (Display.main.systemWidth >= 640 && Display.main.systemWidth < 1280)
        {
            Screen.SetResolution(640, 360, true);
            setGreen.text = "640, 360";
        }
        else if (Display.main.systemWidth >= 1280 && Display.main.systemWidth < 1920)
        {
            Screen.SetResolution(1280, 720, true);
            setGreen.text = "1280, 720";

        }
        else if (Display.main.systemWidth >= 1920 && Display.main.systemWidth < 2560)
        {
            Screen.SetResolution(1920, 1080, true);
            setGreen.text = "1920, 1080";
        }
        else if (Display.main.systemWidth >= 2560 && Display.main.systemWidth < 3840)
        {
            Screen.SetResolution(2560, 1440, true);
            setGreen.text = "2560, 1440";
        }
        else if (Display.main.systemWidth >= 3840)
        {          
            Screen.SetResolution(3840, 2160, true);
            setGreen.text = "3840, 2160";
        }    
    }
    private void Update()
    {
        res.text = Screen.currentResolution.width.ToString() + ", " + Screen.currentResolution.height.ToString();
    }
}

Now I know that this isn’t viable or possibly even legal but I have been sitting on this issue for over 2 weeks.

This takes care of the issue with UI being outside of game view and it is not shaking since its on the Screen Space - Overlay but the thought that this all could be fixed if I somehow could got rid of the shaking on the Screen Space - Camera really keeps me up at night.

If anyone can help me out with this issue, please, do let me know.

New year, new me so I figured I’d solve the issue and I did.

My fix above was viable but not ideal since I’m forcing a lower resolution sometimes.
The game looks worse on a 800x600 monitors when running at a 640x360 resolution - 800x600 with frame cropping looks way better.

So I returned to my prior idea

I was trying to edit the Canvas and Canvas Scaler scripts to get the intended effect but I didn’t realize I could just add another Canvas as a child and edit its Rect Transform to fit my UI so the parent Canvas can stay in Screen Space - Overlay.

Calculating the width and height of the child Canvas is easy, the reference resolution is 640x360 so the parent Canvas is always that size given that the game is running in a resolution that is a multiple of my reference resolution (e.g. a 640x360 screen equals a W:640 and H:360 Canvas, a 1280x720 screen equals a W:640 and H:360 Canvas but the Scale is now X:2 and Y:2) and if the resolution is odd then the dimensions of the parent Canvas are larger by the exact amount that the child Canvas needs to be moved by.

(e.g. #1; a 1280x800 screen equals a W:640 and H:400 Canvas and 400-360=40 so the child Canvas needs to be edited by 40 pixels, 20 from the top and 20 from the bottom. e.g. #2; 800x600 screen equals a W:800 and H:600 Canvas, 800-640=160 & 600-360=240 so the child Canvas needs to be edited by 160 pixels horizontally and 240 pixels vertically.)

So I added a UiCanvasScaler script to my child Canvas that calculates and edits its size based on the dimensions of the parent Canvas like so: thisRect.sizeDelta = new Vector2(640 - parentCanvas.rect.width, 360 - parentCanvas.rect.height);
There are 3 hiccups with this approach that I encountered so far:

  1. Even when triggered on Awake() and with the script being high on the Script Execution Order the scaling is visually noticeable. When you start the scene, all the UI elements are on the edges of the screen and after a couple of frames they snap into position. My solution for this is having the Canvas start in Screen Space - Camera and switch Screen Space - Overlay with a function. I implement a Fade in to black animation at the beginning of every scene lasting 60 frames so I call the function 27 frames in. Works for me, hides the scaling.

  2. The Fade in animation. It used to be animation in the parent Canvas but the scaling problem mentioned affected this animation since its supposed to cover the entire screen (I just use a black Panel and change its alpha value in the animator) but the scaling of the Canvas made it scale to a smaller size for the fraction of a second making a part of the scene visible.

    Here you can see the Fade in panel and UI not being scaled properly to fit the screen yet. This is frame 1 of the animation and the scaling takes approximately 10 frames. This only happens on resolutions that aren’t a multiple of the reference resolution, otherwise the scaling is instant. My solution for this is another Canvas that doesn’t get scaled or anything, it is set to Screen Space - Overlay and just plays the Fade in and Fade out animations correctly and covers the not yet correctly scaled UI for the first couple of frames before it gets scaled.

  3. The outside of the Camera view. Stuff that is outside the camera view stays there. After the Fade animation plays and everything is in order, if anything exits the camera view it continues onto the black borders and stays there along with an odd afterimage effect. I have a pop-up in game that shows the amount of coins the player has collected every time he picks one up. This object then slides down under the camera view and gets disabled but with the parent Canvas being the size of the entire screen it stays on it now.

    This is easily fixed a Rect Mask 2D on the child Canvas.
    In the issue #1 I said:

Now this is fixed with the solution I wrote there but they would also stay on the screen.
If the Canvas was in Screen Space - Overlay the UI elements would start outside of the camera view and their artifacts would stay there even after their correct scaling.

This doesn’t happen when I just switch from Camera to Overlay because they never enter the outside of the camera view but its still worth mentioning. The only instance when this happens is when switching screen resolutions in the editor. In-game I’m thinking that playing the Fade in/out animation when changing resolution would fix this since the artifacts would get covered by the black color in the Panel element, not implemented yet tho.

So all 3 hiccups I encountered so far have a solution and my UI elements are no longer shaking so I suppose the issue is Solved for now.

2 Likes

I’m facing a similar issue now having switched to URP, making my second Pixel Perfect UI Camera useless.

I was thinking an alternative to your method would be to grab the Pixel Ratio from the PixelPerfectCamera component, set the Canvas Scaler to use Constant Pixel Size. Then it looks like the PixelPerfectCamera component that ships with URP outputs the current Pixel Ratio.

// PixelPerfectCamera.cs

/// <summary>
        /// Ratio of the rendered Sprites compared to their original size (readonly).
        /// </summary>
        public int pixelRatio
        {
            get
            {
                if (m_CinemachineCompatibilityMode)
                {
                    if (m_UpscaleRT)
                        return m_Internal.zoom * m_Internal.cinemachineVCamZoom;
                    else
                        return m_Internal.cinemachineVCamZoom;
                }
                else
                {
                    return m_Internal.zoom;
                }
            }
        }

And then on game load to upsize the UI canvas according to this ratio.

1 Like