1
\$\begingroup\$

I have an issue that is driving me crazy. All these lines are 2 in height, but when moving around the screen they change thickness. As you can see the top line is thinner.

enter image description here

What can be done to make sure thin lines always keep the same height/width?

Canvas settings:

enter image description here

EDIT: This is not unique to thin lines, basically due to scaling any element can be the same height/width but appear differently, but Im assuming this can be fixed since several games have pixel perfect UI.

\$\endgroup\$
6
  • \$\begingroup\$ This is what happens when you take UI designed for one screen size and stretch it to a different one with a canvas scaler. It applies automatic scaling that necessarily changes the size of some elements. If the destination resolution isn't an integer multiple of the source resolution, then some rows of pixels get stretched/doubled and some don't, just due to making the best fit with rounding. This simple scaling rule doesn't know out of the box that your line sprites are special and should get some different scaling rule applied. \$\endgroup\$
    – DMGregory
    Aug 20, 2021 at 11:23
  • \$\begingroup\$ @DMGregory that makes sense, but automatic scaling isnt really optional? There must be some way to fix this and keep the scaling functionality no? \$\endgroup\$
    – Majs
    Aug 20, 2021 at 11:32
  • \$\begingroup\$ There are two notions here that this comment conflates: "It's necessary for my game to work with multiple target resolutions" (yes) "...that means I am limited to using this one particular automated scaling behaviour or no scaling at all" (no, that does not logically follow. Unity gives you a much wider toolkit for controlling your rendering than just this single scaling component). Users can post answers below to demonstrate potential fixes for your use case. \$\endgroup\$
    – DMGregory
    Aug 20, 2021 at 11:37
  • \$\begingroup\$ Does your UI camera have AA(anti-aliasing)? \$\endgroup\$ Aug 28, 2021 at 19:23
  • \$\begingroup\$ Do you observe this in the editor or in the build? \$\endgroup\$
    – Serg
    Aug 28, 2021 at 19:40

3 Answers 3

1
+100
\$\begingroup\$

You can draw lines with a height of exactly 2 pixels by using a Canvas with Constant Pixel Size.

  • Create an Image UI object in the Hierarchy
  • Select the Canvas parent object
    • In the Canvas Component:
      • Set Render Mode to Screen Space - Overlay
      • Enable the Pixel Perfect tickbox
    • In the Canvas Scaler Component:
      • Set UI Scale Mode to Constant Pixel Size

Inspector preview

  • Select the Image child object
    • In the Rect Transform Component:
      • Change the Height value to 2
      • Change all other settings according to your needs
    • In the Image Component:
      • Set the Source Image to None
      • Set the Color for the horizontal bar to render
      • Disable the Raycast Target tickbox

Inspector preview

The result is a horizontal line with a given height and colour (width can be defined as a value or adjusted by using anchors), and its size never glitches when moved around the screen.

On the other hand, you are working with explicit pixel dimensions rather than a reference screen and autoscaling; thus, different screen sizes may result in the same UI element having a different ratio with respect to other elements.

\$\endgroup\$
3
  • \$\begingroup\$ Ah, basically this method would disable proper scaling to different resolutions yes? \$\endgroup\$
    – Majs
    Aug 29, 2021 at 12:28
  • \$\begingroup\$ Yes, it does. Pixel perfect scaling works well with pixel art assets: scaling artefacts occur at the pixel level of the screen and aren't noticeable. I guess you want to scale UI width and height, but keep borders and (some) icons at their original size. In such a case, I would go with the Constant Pixel Size option and take advantage of layout anchors to automatically resize some elements. Unfortunately, you can't nest Canvases with different scaling options (child Canvases inherit from the parent), but you can use separate Canvases for UI elements with different scaling behaviours. \$\endgroup\$
    – liggiorgio
    Aug 29, 2021 at 15:12
  • \$\begingroup\$ Okay, well Ill accept since its probably as close as Ill get to an answer, but I dont see my self removing scaling. \$\endgroup\$
    – Majs
    Aug 29, 2021 at 18:34
3
\$\begingroup\$

The issue, as other folks have pointed out, is that Unity is doing ~the best it can with the information it has. Because the reference size of your UI and the actual size of the screen are different, Unity must either squish or expand lines of pixels to make them fit. It's functionally impossible to guarantee your reference resolution is always a clean multiple of the screen resolution because people have essentially every screen resolution imaginable and you gotta pick one reference resolution to author your game in (unless you have a LOT of time on your hands ;) So, if there's no clean scaling factor, Unity must either anti-alias (which makes things blurry; generally bad for UI) or make some pixel lines thicker and some thinner (the problem outlined in the original post).

All that said, I think there is a better answer; we just need to be able to express the intent. I think what you actually want here is two things:

  1. To have all the lines agree on how thick they should be
  2. To be able to assert a minimum size, so thin lines never vanish

It turns out, achieving both of those things isn't that hard. So, for instance, we can go from this (note that those lines are the same 'width' in my reference resolution, but I got unlucky with their scaling):

Broken scaling of lines

to this (if I just say 'be the same size')

Rounded scaling of lines

to this (if I say 'be the same size and never less than 4 pixels width')

Rounded scaling with min of lines

It requires adding a component to your object and telling it what things should be the same, with this interface:

Pixel scaler interface

Essentially, if you just check the 'control XX' button, it will ensure that if they were the same size in your reference resolution, they'll be the same size on screen. If you also give a > 0 'min XX', then it will further enforce that the on screen size is never less than that - which can help prevent your lines disappearing.

Here is the code for the thing. Note that I'm assuming you have a pixel perfect camera (probably the right choice for UI). I'm doing my testing in a Screen Space - Overlay render mode; I suspect it will work fine for Screen Space - Camera too. And, of course, I've got a Canvas Scaler set to Scale With Screen Size... which is sorta the whole point of the question ;)

public class PixelSizeCorrector : MonoBehaviour
{
    // For reference: UiManager's properties are:
    // public int ReferenceWidth => (int)(this.Canvas.GetComponent<CanvasScaler>().referenceResolution.x);
    // public float ScaledWidth => this.Canvas.pixelRect.width;
    // NOTE: I'm assuming your scaling is controlled by width here; this is a setting on the CanvasScaler... if you're using something else, you might need to calc this differently.
    // public float ScaledToReferenceMultiplier => this.ReferenceWidth / this.ScaledWidth;

    public bool ControlWidth;
    public bool ControlHeight;
    public int MinWidth;
    public int MinHeight;

    protected override void OnEnabling()
    {
        base.OnEnabling();

        var rectTrans = this.GetComponent<RectTransform>();
        if (this.ControlWidth)
        {
            this.RationalizeScaling(rectTrans, rectTrans.rect.width, this.MinWidth, RectTransform.Axis.Horizontal, "Width");
        }

        if (this.ControlHeight)
        {
            this.RationalizeScaling(rectTrans, rectTrans.rect.height, this.MinHeight, RectTransform.Axis.Vertical, "Height");
        }
    }

    private void RationalizeScaling(RectTransform rectTrans, float referenceDimension, float minInPixels, RectTransform.Axis axisName, string dimensionName)
    {
        // First determine how many pixels we're actually going to be
        var scaledDimension = referenceDimension * UiManager.Instance.ReferenceToScaledMultiplier;

        // Now determine our target (it's either a consistent rounding or, if a min was requested and we're below it, that min)
        float targetScaledDimension = Mathf.RoundToInt(scaledDimension);
        if (minInPixels > 0 && targetScaledDimension < minInPixels) targetScaledDimension = minInPixels;
        // Backsolve for what the original reference dimension needs to be to give us the targetScaledDimension
        var targetReferenceDimension = targetScaledDimension * UiManager.Instance.ScaledToReferenceMultiplier;

        rectTrans.SetSizeWithCurrentAnchors(axisName, targetReferenceDimension);

        //Logger.ErrorLog($"{dimensionName} -> Reference: {referenceDimension}, Scaled: {scaledDimension}, targetScaled: {targetScaledDimension}, targetRef: {targetReferenceDimension}, refToScaledMult: {UiManager.Instance.ReferenceToScaledMultiplier}");
    }
}

Do keep in mind that if something else (like a Layout Group) is controlling the dimensions of your object, it will likely overwrite what this script is doing. So, you might need to hook this up to control whatever is controlling the dimension (like a LayoutElement). If there's any interest, I'm probably going to do that myself and am happy to share.

Hope this helps folks who are struggling with this issue too!

\$\endgroup\$
0
\$\begingroup\$

It seems that the root of the problem is not in Unity or any other rendering system, but in the fact that the image on the monitors is still made from pixels. Therefore, if you don't create a user interface for a specific device with a constant resolution you won't be able to guarantee 100% correct representation of the image on the display with a different resolution (except for the cases when such a resolution is always greater than the original one and its multiplier is always a whole number).

Just imagine you have 2x2 pixels monitor (or Canvas in your case) and 1 pixel image on it at (0, 0). What happens if you try to display the same canvas on a 3x3 monitor? The canvas will be scaled by 1.5 and the image will occupy 1.5 pixels. This is exactly where visual artifacts begin to appear. The rendering system will try to make some approximation but it is technically impossible to display anything on half a pixel without loss of quality (or at least I can't imagine how it could be made).

My point here is that visual artifacts are not some kind of error, quite the contrary they are to somehow compensate for the inability to reliably display the image on different resolutions. Hence it is impossible to completely get rid of the artifacts, but you can try to make them less noticeable.

On different resolutions, the same lines may correspond more or less exactly to an integer number of pixels, so they may look like they have different heights or brightness despite the system's efforts to hide this.

For example, this image has 1px white lines and 2px colored lines:

enter image description here

in a different resolution it can be displayed like this (I intentionally scaled it to make the artifacts more noticeable):

enter image description here

Approximation/anti-aliasing algorithms hide the imperfection of displays quite well when it comes to large objects, but in your case they will hardly be able to make it unnoticeable to the eye when the lines are only a couple of pixels thick, are located strictly horizontally (collinear to the display pixels), and are deliberately made contrasting to the background - all this taken together makes it much easier for the eye to compare them and to notice any difference. Therefore, I don't think that you will be able to achieve an acceptable result using such approach (which is applied by default), unless you want to avoid a design with very thin lines.

Another option could be (as suggested in the @liggiorgio answer) using the Pixel Perfect option along with Constant Pixel Size. This can give a good result if you are ready for some compromises, for example, to give up scaling and smooth movement (if you move the element by 0.5 pixels you will get height artifacts back), but for the UI it could be a decent solution since the requirements for animations in the UI are not so high especially if it's just kind of a dialog moving/dragging. Still, I can't see any point in using images to create lines (as @liggiorgio suggests) except for keeping them aligned with pixels when moving, but this may also make them out of sync with other elements of the background image which would look like jittering. Instead, you can move UI elements by whole pixels using RectTransform without any need to resort to such tricks.

What about scaling - you can customize CanvasScaler so that it will scale only by whole numbers. IOW your UI can be scaled (without getting artifacts) only when the ratio of a new resolution to the reference one gives an integer number. In your case (if you are only interested in horizontal lines) you can only take into account the height values ratio. You can find an example of CanvasScaler customization here and adapt it to your needs.


So, at the end once again: I am not an expert in this matter and I have not even had to solve a similar problem for the UI, so all my reasoning is mostly theoretical.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .