0
\$\begingroup\$

I'm using a Cinemachine Virtual Camera in a 2D project to follow around a target. The ground / background is a Unity Tilemap GameObject.

Animated example showing shimmering in grass tiles as a character walks

As you can see in the gif above (or on Imgur), when following around the player (a 24x24 sprite), the background tiles seem to warp or shimmer a bit.

I've tried to script all types of solutions to adjust the Virtual Camera transform position and hopefully snap/move it "correctly" to no avail.

I don't even know for sure that the source of the issue is with the camera setup. I'm running out of solutions to something that seems like a pretty straightforward and very common scenario. I've created a sample project illustrating the issue which can be downloaded here.

Using Unity 2017.3.1f1 Personal. The background sprites are 32x32, with a PPU of 32. No compression, Point (no filter), rendered using a material with Shader: Sprites/Default, and Pixel snap.

Cinemachine Virtual Cam is set to Ortho lens size 16, Body: Framing Transposer with default settings.

Thank you so much for any suggestions or tips!!!

It feels similar to what's being described here with sub-pixel movement but I don't know for sure, and the solution in that blog post seems like it should be unnecessary (but again - maybe not).

I've created camera scripts and attached them to the virtual camera as follows:

float maxPixelHeight = 32;
CinemachineVirtualCamera vcam;

public void Awake()
    {
        float scale = Screen.height / maxPixelHeight;
        float orthographicSize = (Screen.height / scale) / 2f;
        vcam = GetComponent<CinemachineVirtualCamera>();
        vcam.m_Lens.OrthographicSize = orthographicSize;
}

    public void Update()
    {
        Vector3 tempPos = vcam.transform.position;
      Vector3 newPos = new Vector3(Mathf.RoundToInt(tempPos.x), Mathf.RoundToInt(tempPos.y), Mathf.RoundToInt(tempPos.z));
        vcam.transform.position = tempPos;
    }

I've also tried:

public void Update()
    {
        Vector3 tempPos = vcam.transform.position;
Vector3 newPos = new Vector3((float)System.Math.Round((decimal)tempPos.x, 2) , (float)System.Math.Round((decimal)tempPos.y, 2), (float)System.Math.Round((decimal)tempPos.z, 2));
        vcam.transform.position = newPos;
    }

and something like:

public void Update()
    {
        Vector3 tempPos = vcam.transform.position;
        vcam.transform.position = new Vector3(RoundToNearestPixel(tempPos.x), RoundToNearestPixel(tempPos.y), RoundToNearestPixel(tempPos.z));    
    }

    public float RoundToNearestPixel(float unityUnits)
    {
        float valueInPixels = unityUnits * maxPixelHeight;
        valueInPixels = Mathf.Round(valueInPixels);
        float roundedUnityUnits = valueInPixels * (1 / maxPixelHeight);
        return roundedUnityUnits;
    }
\$\endgroup\$

1 Answer 1

8
\$\begingroup\$

Note that Cinemachine now has a Pixel Perfect extension that automates the kind of camera calculations I describe further down in this answer. Consider using that, and check the rest of this answer for some intuition about what it's doing and how it fixes the problem.


This is something a lot of people get wrong about pixel art rendering, and you'll find a lot of bad advice online - saying the problem is snapping, that you need to adjust the positions of objects to eliminate shimmering.

That's not actually the problem or solution for this particular warping artifact. Nearest neighbour / point sampling already snaps display positions to the nearest sprite texel for you. So if you're seeing shimmering or warping within a single sprite, it's not snapping that's to blame.

(Snapping sprite or camera positions can help with a different effect: when you have a sprite as a whole that jitters between adjacent pixel positions, or two sprites that appear to vibrate relative to one another, because nearest neighbour filtering decides to snap one whole sprite to the left and the other whole sprite to the right. But that's a topic for another question...)

See this blown up example: even when the mushroom moves smoothly to non-snapped pixel positions, nearest neighbour sampling steps the whole sprite in full-pixel increments, with no shimmer/warping.

Example of 16x16 image displayed at 16x16, moving smoothly

The problem is scale.

Example of 16x16 image displayed at 17x17, warping & shimmering

Here's the same sprite, stretched from 16x16 texels in the sprite sheet to cover 17x17 pixels in the screen grid. Nearest neighbour filtering still snaps each pixel to the closest texel sample from the sprite, but because the scale is no longer an integer, this snapping sometimes snaps left in one part of the image, while snapping right in another part of the image, creating localized stretching & compression of doubled/skipped lines of texels.

Here's another example. Both Mario sprites are moving at the same rate, occupying the same sub-pixel positions (no extra snapping script applied). But the one on top has been scaled 1% larger than its native size, creating these warping/rippling artifacts.

Example of two Marios, one with integer scaling (bottom) one with non-integer scaling (top, rippling)

So, where does this scaling come from?

If your PPU setting is 32, and your orthographic camera size is 16, then you're showing 32 (16x2) units of the world vertically, 32 sprite texels apiece, for 32x32 = 1024 texels vertically.

So if your window is exactly 1024 pixels tall (or 2048, or 3072, or 4096...), then each texel in your sprite will map to exactly 1 screen pixel (or 2, or 3, or 4...)

If your window is any other size, you'll have a fractional number of screen pixels per sprite pixel, leading to these fractional scaling artifacts.

To solve this, you need to do some combination of:

  • Changing the orthographic size of your camera

    (eg. 11.25 @ 720p or 16.875 @ 1080p will give 1:1 scaling with 32 PPU, larger screens will see more of the world)

  • Adding padding / letterboxing / pillarboxing to the edges of your screen

    (eg. Using the above, but cropping the scene in 1080p so that the amount of the scene displayed is the same. The extra space can be filled with UI or decorative borders)

  • Rendering at a lower resolution and scaling up by different integer ratios depending on your display size

(eg. rendering to a 360p render target with an orthographic height of 5.625, then scaling it up 2x for 720p, or 3x for 1080p)

  • Swapping your assets for alternates at a different resolution

    (eg. Drawing an alternate set of sprites at 48x48, 48 PPU, and swapping to them when rendering to a 1080p screen so you see the same area as in 720p)

The goal is to use some combination of settings so that each sprite texel maps to an integer number of screen pixels for your target resolution.

I'm borrowing from a few previous answers I've written on this topic, so you might find those useful for some further details & links:

\$\endgroup\$
1
  • \$\begingroup\$ Thank you tremendously @DMGregory. I'm going to go through this and attempt a fix. I appreciate the lengthy and detailed explanation and hope to pay it forward in the community. \$\endgroup\$
    – JaredH
    Apr 1, 2018 at 3:28

You must log in to answer this question.

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