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.