Pixel-Perfect Virtual Camera – Unity3D

UPDATE: ALL THESE AND MORE CAN BE FOUND ON MY NEW SITE http://www.returnofthebrain.co.uk/

Note – This tutorial was made using Unity 5. But it’ll probably work on 4 as well. It won’t work for 3 unless you have pro as RenderTexture is pro-only in version 3.

Pixel-Perfect games can be a bit of a pain in the arse in Unity sometimes, particularly when your target resolution is quite low. It’s very easy to get badly stretched images or ‘pixel-walking’ where some pixels don’t seem to be the same size as others.

pixelwalking

Might be kinda hard to tell from the GIF above, but you should be able to see how the pixels distort when the camera moves across them.

Luckily, there’s a few things we can do about this.

We’re going to build a small but pretty handy virtual camera rig which will handle rendering our game at the resolution our assets were built for, and then scaling the resulting image properly to fit within a multiple of that target resolution. Ensuring that we get a crisp pixel-perfect view of our game!

Scene Setup

We need to set up a couple of things before we write our scaling script. First, our scene’s main camera needs to be adjusted to fit the target resolution of our assets.

It should be set to ‘Orthographic’ projection if it’s not already, and you’ll probably want to set it to clear to a solid color, and ensure that colors alpha value is set to 255, sometimes Unity likes to make it transparent.

The most important thing we need to set here is ‘Size’ setting, which controls our cameras orthographic size. It’s a little unintuitive at first, but this size represents half of the desired vertical resolution, in world units.

In this example, I’m aiming for a target resolution of 160×144, same resolution as the Gameboy. So, the orthographic size of our camera should be half of 144, or 72.

Now. Bare in mind that this is in world units, and so assumes that you’ve imported your assets with a unit-to-pixel ratio of 1. Which makes them pretty huge, and may break your game if you intend to rely on any physics calculations. The default is set to 100. If you’ve imported with any other size than 1, you’ll need divide the orthographic size by that.

orthographicSize = (verticalResolution/2) / pixelsPerUnit

For our example, half of 144 is 72. 72 divided by our pixelsPerUnit ratio (32 in this example) gives us an orthographic size of 2.25.

Last thing we need to do here is hop over to our project tab and create two things, a RenderTexture called rt_VirtualScreen and a material called m_VirtualScreen. The RenderTexture should be the dimensions of your target resolution (so here, it’s 160×144 to match my Gameboy resolution) Drag the RenderTexture into the TargetTexture slot of our camera.

gamepixels.gif

You should be greeted by a warning telling you that there are no cameras rendering, this means our camera is now rendering directly to that RenderTexture, and we can move onto the next step.

Update: You’ll want to make sure the RenderTexture rt_VirtualScreen has some settings applied. Ensure that Anti-Aliasing is set to ‘None’ and Filter Mode is set to ‘Point’ otherwise the scaled image will still be filtered later on!

point filtering.gif

Creating a UI to house our Virtual Screen

Create a UICanvas object inside of our scene,and a new Camera object called UICamera, attach this to the UICanvas root object. Leave the settings as default.

Next, create a RawImage element, and drag our rt_VirtualScreen onto its Texture slot. Drag the material into the Material slot and make sure Pixel Snap is enabled on the material.

This will already look a lot better, but we want to write some script that will scale the image properly.

Create a script called PixelPerfectScaleUI and attach it to the RawImage element.

Best-Fit Script

We’re going to allow the user to choose between three different scale modes.

  • Best Fit: This is the most accurate mode, and our aim here is to scale the image so that each pixel is scaled by a multiple of two.
  • Maintain Aspect Ratio: This mode will stretch the image to fit either the height or width of the screen, maintaining the correct aspect ratio.
  • Stretch to Fit: I don’t know why some people still want this mode, but they do! So I’ve included it anyway. It will still look better this way than it will through a regular camera setup.

The [ExecuteInEditMode] tag will allow us to preview our script without having to enter play mode.

The update function stores the current screen height, and will only re-size the screen if that changes, so that we aren’t doing it every frame. If it detects a change we will use a switch statement to determine which of our scale modes to apply.

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]
public class PixelPerfectScaleUI : MonoBehaviour
{
	public enum FitModes { BEST_FIT, MAINTAIN_ASPECT, SCALE_TO_FIT }
	public FitModes fitMode = FitModes.BEST_FIT;

    public float gameHorizontalPixels;
    public float gameVerticalPixels;
    float minimumMultiplier = 1;

    private float screenPixelsY = 0;

        void Update () {
	    if(screenPixelsY != (float)Screen.height)
            {
		switch(fitMode){
		    case FitModes.BEST_FIT:
			BestFit();
			break;
		    case FitModes.MAINTAIN_ASPECT:
			MaintainAspectFit();
			break;
		    case FitModes.SCALE_TO_FIT:
			ScaleToFit();
			break;
		}
            }
	}

Remember to set ‘screenVerticalPixels’ and ‘screenHorizontalPixels’ in the inspector, they should be the resolution you want to render your game at and should match the size of your RenderTexture.

BestFit is the meat of our script, and the most complex part.

Update: 02/10/2016 – Updated this script to be a bit more flexible and remove the while loop which is a bit dangerous in an update function! On the advice of my good buddy @salamander3

targetHeight represents our games render height (so again for our Gameboy example, this would be 144 pixels), multiplier represents how much we’re going to scale up by, and screenPixelsY is the current height of the viewport.

Multiplier starts at 2 because our game is so small that it’s going to be very rare that we would need to display it at original size (after-all, scaling up properly is the whole reason we’re writing this script!)

Next we work out the current multiplier based on the size of the screen, and step it down to a multiple of 2. If the result is less than 2, we force it to be minimumMultiplier (which should either be 1 or 2 depending on how small your original resolution is)

Next thing we do is work out the aspect ratio of our game by dividing the game width by the game height. As we know our height, we can get our UI component’s RectTransform, and set that height. Then we can set the width to our new height multiplied by the aspect ratio.

best_fit

    private void BestFit() {
	float targetHeight = gameVerticalPixels;
	float multiplier = minimumMultiplier;

	multiplier = screenPixelsY / targetHeight;
        multiplier -= multiplier % 2;
        if(multiplier < 2){
            multiplier = minimumMultiplier;
        }

        float aspect = gameHorizontalPixels / gameVerticalPixels;
        float height = gameVerticalPixels * multiplier;
        float width = height * aspect;

</code><code>	RectTransform rt = gameObject.GetComponent();
	rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, height);
	rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, width);
    }

Next up is a much easier script, but it results in a slightly less accurate result that scales as best it can to the screen size without distorting the aspect ratio.

First thing we do is find out our games aspect by dividing width by height. Then we create two float variables which will store our width and height.

If our screen width is less than our screen height, we should fit our image to the screen width. Which means setting our width to match the screen, and then dividing width by our aspect ratio to find the correct height.

Otherwise, we scale it to fit the height, by setting our height to match the screen and our width to the height of the screen multiplied by the aspect. This will ensure our entire image stays on-screen at all times.

Then we simply apply these values to our RectTransform as previously.

maintain

    private void MaintainAspectFit() {
        float aspect = gameHorizontalPixels / gameVerticalPixels;

        float targetWidth;
        float targetHeight;

        if(Screen.width < Screen.height){
	    // fit to width
	    targetWidth = Screen.width;
	    targetHeight = Screen.width / aspect;
        } else {
	    / fit to height
	    targetHeight = Screen.height;
	    targetWidth = Screen.height * aspect;
        }

        RectTransform rt = gameObject.GetComponent();
        rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, targetHeight);
        rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, targetWidth);
    }

Lastly is our simple scale-to-fit option. This one’s really easy! Just apply the screen height and width to our RectTransform. Done!

scaletofit

    private void ScaleToFit() {
	RectTransform rt = gameObject.GetComponent();
	rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, Screen.height);
	rt.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, Screen.width);
    }
}

This gives us a much better way of rendering pixel-perfect games and, if necessary, stretching them to fit in a much cleaner way than simply using a default camera.

There are a couple of drawbacks here however. First, you now need to manage this system alongside your UI if you intend to use the Unity UI system. Secondly, this technique works best with texture sheets that are power-of-two (square) sizes. Using odd sizes you can’t guarantee you won’t still see some artifacting.

I’m using this exact setup to make Gameboy style games at the moment. Give it a try for yourself!

Advertisements
This entry was posted in Tutorials, Unity3D and tagged . Bookmark the permalink.

10 Responses to Pixel-Perfect Virtual Camera – Unity3D

  1. ed says:

    so where does screenVerticalPixels come from?

    Like

    • diablobasher says:

      It’s whatever resolution you want to use as your render resolution. It’s a public variable that you set in the inspector.

      I’ll update the tutorial to make that clearer.

      edit: Oh! ‘screenVerticalPixels’ should actually be gameVerticalPixels! I’ve updated that. Cheers.

      Like

  2. Pingback: Senior Capstone: Moving back to familiar territory! | College Student by Day, Game Developer also by Day.

  3. Guy says:

    Are there some settings I should be changing with the UI canvas or the UI camera or anything at all? I have pixel snap on the canvas and materials but no matter how the Raw Image gets scaled, it doesn’t scale pixel perfect. If I double the resolution of the Raw Image, it doesn’t create 2×2 pixels but scales and then AA’s the image.

    Like

  4. Mike Sabillon says:

    Hello! awesome post. I’ve got a question.. i am making a pixel perfect game and i am having trouble with making the ui/hud pixel perfect with the canvas, do you know a way to make ui images/sprites snap perfectly with screen pixels?

    Like

    • diablobasher says:

      Hi Mike, sorry for the late response.

      UI/HUD for pixel perfect games is a massive pain in the ass. Your main option is to render it like the rest of the game, using this camera, in which case clicking on things/interacting with things becomes difficult as you’ll have to project the mouse click based on the point it hits on the render quad.

      Otherwise I’m not sure how I’d do it, sorry! For UI-light games this is fine, but for a more UI-heavy game, you may want to find a better solution.

      Like

  5. baba says:

    I don’t understand your condition, you’re not setting screenPixelsY to the Screen.height anywhere, it’s 0 so it will always return true.

    Did you mean to add “screenPixelsY = (float)Screen.height;” right after the switch?

    Thanks for the tutorial!

    Like

  6. Pingback: RETURN OF THE BRAIN

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s