Skip to content

Render Texture Does Not Capture Overlay Canvas UI in Screenshot – Unity

While building out my Ultimate Screenshot Tool for the Unity asset store, I came across an issue where canvases in overlay render mode would not display in my screenshots. After some digging, I found out this is a fairly common issue with many screenshot tools. My solution ended up being a bit brutish, but it works well. There’s also some tricks to it, so I thought I’d share it.

The Solution

My solution was simply to grab all the canvases and switch the overlay canvases to camera mode during the screenshot. Camera mode simply puts the canvas on a specific camera instead of the overall screen. It ends up performing roughly the same as overlay mode if set up properly.

To set it up properly, first you’ll need to grab the right camera. You’ll want a camera that layers on top of the others with a viewport is the same size as the screen. If you’re just using one camera, that camera will work. Then to get the canvas to appear in front of everything else on that camera, you’ll want to set its plane distance to your camera’s near clip plane. (The near clip plane is the closest distance where the camera will still render the object.) If you use the z-position within your UI (rather than just order with the hierarchy layering) than you may need to increase this value slightly. I used an extra 10% of the near clip plane for some margin of safety. You’re welcome to experiment with your own values.

Now to the trick I mentioned. To get it working on exactly the frame it’s set up on, the order of operations does matter for some reason. I switched around the order for code aesthetic reasons at one point and actually broke it! But don’t worry, if you leave it as is, it’ll work fine.

I was rather overzealous in preserving original camera state just to cover corner cases where users where switching between render modes for some reason. There’s no reason why restoring the original camera or plane distance would otherwise matter as overlay canvases don’t use those variables.

The Code

using System.Collections.Generic;

using UnityEngine;
using UnityEngine.SceneManagement;

public static class CanvasesAdjuster
{
    static Canvas[] allCanvases;

    static bool[] canvasModified;
    static Camera[] originalRenderCameras;
    static float[] originalCanvasPlaneDistances;

    public static void SetupIfNecessary()
    {
        bool setupRequired = false;
        if (allCanvases == null)
            setupRequired = true;
        else
        {
            foreach (Canvas canvas in allCanvases)
            {
                if (canvas == null)
                {
                    setupRequired = true;
                    break;
                }
            }
        }

        if (setupRequired)
            Setup();
    }

    static void Setup()
    {
        List<GameObject> rootGameObjects = new List<GameObject>();
        Scene scene = SceneManager.GetActiveScene();
        scene.GetRootGameObjects(rootGameObjects);

        allCanvases = new Canvas[0];
        foreach (GameObject rootGameObject in rootGameObjects)
            allCanvases = allCanvases.CombineWith(rootGameObject.GetComponentsInChildren<Canvas>(true));
    }

    public static bool AnyOverlayCameras()
    {
        SetupIfNecessary;

        foreach (Canvas canvas in allCanvases)
        {
            if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
                return true;
        }

        return false;
    }

    public static void ForceCameraRenderMode(Camera camera)
    {
        SetupIfNecessary();

        canvasModified = new bool[allCanvases.Length];
        originalRenderCameras = new Camera[allCanvases.Length];
        originalCanvasPlaneDistances = new float[allCanvases.Length];
        for (int i = 0; i < allCanvases.Length; ++i)
        {
            if (allCanvases[i].renderMode == RenderMode.ScreenSpaceOverlay)
            {
                canvasModified[i] = true;
                originalRenderCameras[i] = allCanvases[i].worldCamera;
                originalCanvasPlaneDistances[i] = allCanvases[i].planeDistance;

                // Order of operations is apparently important here
                allCanvases[i].renderMode = RenderMode.ScreenSpaceCamera;
                allCanvases[i].planeDistance = camera.nearClipPlane * 1.1f;
                allCanvases[i].worldCamera = camera;
            }
        }
    }

    public static void RestoreOriginalRenderModes()
    {
        for (int i = 0; i < allCanvases.Length; ++i)
        {
            if (canvasModified[i])
            {
                allCanvases[i].worldCamera = originalRenderCameras[i];
                allCanvases[i].planeDistance = originalCanvasPlaneDistances[i];

                allCanvases[i].renderMode = RenderMode.ScreenSpaceOverlay;
            }
        }
    }

    public static T[] CombineWith<T>(this T[] arrayA, T[] arrayB)
    {
        T[] newArray = new T[arrayA.Length + arrayB.Length];
        for (int i = 0; i < arrayA.Length; ++i)
            newArray[i] = arrayA[i];
        for (int i = 0; i < arrayB.Length; ++i)
            newArray[arrayA.Length + i] = arrayB[i];
        return newArray;
    }
}

The above code is provided under the MIT license. Feel free to use it in any project without worrying about attribution.

Everything Else

And that’s it! Just swap those canvases into camera mode before your screenshot and swap them back when you’re done. Let me know if you have any issues with it. Note: You will have to handle DontDestroyOnLoad objects separately as they are not returned as RootGameObjects in the scene.

If this all seems like a bit of a hassle or you’d prefer to skip the headaches of the various editor/build corner cases (and trust me there are more than you’d think), check out my Ultimate Screenshot Tool on the Unity asset store. It’s got loads of super convenient functionality to take brilliant screenshots.

Published inCode ExamplesDevelopment Tips

Be First to Comment

    Leave a Reply

    Your email address will not be published. Required fields are marked *