Using RenderTexture to render sprites smoothly

Update: This is now available on the asset store: Unity Asset Store - The Best Assets for Game Making

I’ve been playing with 2D characters lately, and I have always been quite unhappy with the way texture filtering and camera anti-alias effects look. I thought I’d share my approach to get rid of this.

With point filtering on the textures, it always looks twitchy:

It gets a bit better with bilinear filtering and/or anti-alias camera effects, but I still don’t like it. The results still look a bit too blurry for me.

So I have been playing with a RenderTexture instead, and the result looks quite promising:

You can already see that the result is much smoother.

The way I did that was to have the main camera render its image into a RenderTexture that is double the screen’s width/height. This RenderTexture has an anti-alias level of 2 on it. Then that RenderTexture is directly blitted onto the screen. I will show the scripts further down in this post.

Now, if you look closely in the above animation, you can still see some twitchiness. This can be improved even further by switching to bilinear texture filtering, which is okay in my case:

This looks really smooth for my taste.

(Try opening up the animations in different browser tabs and switch between them. Also note how the text in the Canvas gets also more crisp.)

So this is how I did it:

The main camera is the usual setup of an orthographic camera. There’s nothing special. I then have a second camera with a few scripts on it:

Note that this camera is switched off so that it won’t interfere while out of play mode. It will get activated in play mode by the SetupRenderTexture script. Also, this camera’s settings don’t really matter, because it doesn’t render anywhere. For calculations regarding Canvas, especially clicking, the size should be the same as the main camera.

The scripts that I use:

The SetupRenderTexture script sets up a render texture for the main camera to use. It also enables the second camera that will blit the render texture onto the screen:

/// <summary>
/// Sets up a render texture and instructs the main camera to use it.
/// </summary>
public class SetupRenderTexture : MonoBehaviour {
    [SerializeField]
    private int antialias;

    private void OnEnable() {
        // create render texture double the width/height of the screen
        RenderTexture renderTexture = new RenderTexture(
            Screen.width * 2, Screen.height * 2, 0, RenderTextureFormat.ARGB32);
        renderTexture.antiAliasing = antialias;
        renderTexture.Create();

        // instruct main camera to render to texture
        Camera.main.targetTexture = renderTexture;

        // activate camera that blits render texture to screen
        GetComponent<Camera>().enabled = true;
    }
}

The BlitRenderTexture script simply blits the render texture of the main camera onto the screen. This automatically downscales the render texture’s contents.

/// <summary>
/// Blits the main camera's render texture directly to the screen.
/// </summary>
public class BlitRenderTexture : MonoBehaviour {
    private Camera mainCamera;

    private void OnEnable() {
        mainCamera = Camera.main;
    }

    private void OnRenderImage(RenderTexture source, RenderTexture dest) {
        // blit render texture directly to screen
        Graphics.Blit(mainCamera.targetTexture, (RenderTexture) null);
    }
}

The SetupCanvasScaling script modifies all CanvasScalers in the scene so that scale factors are preserved. It also modifies all MyGraphicRaycasters in the scene to use the correct camera for calculations.

/// <summary>
/// Modifies all CanvasScalers in the scene to double their scale factors.
/// </summary>
public class SetupCanvasScaling : MonoBehaviour {
    private void OnEnable() {
        CanvasScaler[] canvasScalers = FindObjectsOfType<CanvasScaler>();
        for (int i = 0; i < canvasScalers.Length; i++) {
            CanvasScaler canvasScaler = canvasScalers[i];
            switch (canvasScaler.uiScaleMode) {
                case CanvasScaler.ScaleMode.ConstantPixelSize:
                    canvasScaler.scaleFactor *= 2f;
                    break;
                case CanvasScaler.ScaleMode.ConstantPhysicalSize:
                    Debug.LogError("cannot use " + typeof(SetupCanvasScaling).Name +
                        " script with a Canvas in 'constant physical size' mode");
                    break;
                case CanvasScaler.ScaleMode.ScaleWithScreenSize:
                    canvasScaler.referenceResolution /= 2f;
                    break;
            }
        }

        Camera camera = GetComponent<Camera>();
        MyGraphicRaycaster[] graphicRaycasters = FindObjectsOfType<MyGraphicRaycaster>();
        for (int i = 0; i < graphicRaycasters.Length; i++) {
            graphicRaycasters[i].raycastCamera = camera;
        }
    }
}

Finally, all GraphicRaycasters on Canvases in the scene need to be removed and replaced with MyGraphicRaycaster. This script does the same as the GraphicRaycaster, but uses the correct camera for calculations when clicking in the scene.

/// <summary>
/// A graphic raycaster that works like the original GraphicRaycaster,
/// but uses a different camera.
/// </summary>
public class MyGraphicRaycaster : GraphicRaycaster {
    [NonSerialized]
    public Camera raycastCamera;

    public override Camera eventCamera {
        get {
            return raycastCamera;
        }
    }
}

Of course there are also drawbacks with my approach:

  • Actual rendered content is four times as big. This is simply a result of the render texture being four times as big as the screen size.

  • One additional draw call to blit the render texture onto the screen. This shouldn’t be too hard for 2D games.

4 Likes

I’ve now played around some more with Canvases in the scene, and it turned out that the GraphicRaycaster got confused, so that clicks were off. The solution is to use a custom raycaster that uses the correct camera for calculations. I’ve updated the OP accordingly, adding a new script, and enhancing the SetupCanvasScaling script.

Hmm, it looks like it doesn’t work so well yet with the new UI. When the camera is still, everything works as expected, but when the camera is moving, the UI seems to be lagging a frame behind. So instead of staying exactly at the same position each frame, it will jump around depending on the camera’s position delta. I’ve already played around with this for some time now (including changing the script execution order to move BlitRenderTexture to the very bottom) - but no joy so far.

Ah, the jumpiness problem seems to occur because I’ve been using the main camera to also render the UI (“Screen Space - Camera” setting in the Canvas.) If I use yet another dedicated UI camera that never moves, the problem goes away. Of course the MyGraphicRaycaster cannot use that other camera anymore because it moves away from the UI camera. This problem again can be worked around with YET another camera that always stays where the UI camera is. (Note that this new camera can stay deactivated, it won’t be used for rendering.)

So I guess that’s all we need to do here. The whole setup isn’t too light, but it’s not really complicated once you get the hang of it. I think I will sort things out a bit and update the OP again once that’s done.

1 Like

I’ve decided to clean and package this up and make it available on the asset store: Unity Asset Store - The Best Assets for Game Making

2 Likes