Created
November 29, 2023 22:09
-
-
Save rtlsilva/79aa29fc97e3282a005d2bc3090c2fe0 to your computer and use it in GitHub Desktop.
A utility class that takes snapshots of GameObjects by rendering them to textures
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using UniRx; | |
using UnityEngine; | |
using UnityEngine.Rendering; | |
using UnityEngine.UI; | |
using MyNamespace.Extensions; | |
namespace MyNamespace.Utils | |
{ | |
/// <summary> | |
/// Utility class that takes snapshots of content by rendering it to textures. | |
/// </summary> | |
public static class GameObjectSnapshotRenderer | |
{ | |
/// <summary> | |
/// Returns an observable that directly renders the given <see cref="Camera"/> to a screen-sized <see cref="Texture2D"/>. | |
/// </summary> | |
/// <param name="camera">The camera to use for rendering.</param> | |
/// <param name="captureStage">The position in the camera's rendering pipeline where the content will be captured to a texture. | |
/// Consult Unity's documentation on built-in rendering pipelines and command buffers to verify which steps are valid./param> | |
public static IObservable<Texture2D> RenderToTexture(Camera camera, CameraEvent captureStage = CameraEvent.AfterEverything) | |
{ | |
return RenderToTexture(camera, Screen.width, Screen.height, captureStage); | |
} | |
/// <summary> | |
/// Returns an observable that directly renders the given <see cref="Camera"/> to a <see cref="Texture2D"/> of the given dimensions. | |
/// </summary> | |
/// <param name="camera">The camera to use for rendering.</param> | |
/// <param name="textureWidth">The pixel width of the capture.</param> | |
/// <param name="textureHeight">The pixel height of the capture.</param> | |
/// <param name="captureStage">The position in the camera's rendering pipeline where the content will be captured to a texture. | |
/// Consult Unity's documentation on built-in rendering pipelines and command buffers to verify which steps are valid./param> | |
public static IObservable<Texture2D> RenderToTexture(Camera camera, int textureWidth, int textureHeight, CameraEvent captureStage = CameraEvent.AfterEverything) | |
{ | |
return Observable.NextFrame() | |
.Select(_ => RenderCameraToTexture(camera, textureWidth, textureHeight, captureStage)); | |
} | |
/// <summary> | |
/// Returns an observable that renders the given <see cref="RectTransform"/> to a screen-sized texture. | |
/// <para>The rendering is done off-screen and the target is given a frame to perform layout updates before rendering.</para> | |
/// </summary> | |
/// <param name="renderSetup"> An action that executes before rendering. Takes the root transform that is created for this operation | |
/// so elements may be added to it and configured.</param> | |
public static IObservable<Texture2D> RenderToTexture(Action<RectTransform> renderSetup = null) | |
{ | |
// Set up the studio | |
var photoStudio = new GameObject(nameof(GameObjectSnapshotRenderer), typeof(RectTransform), typeof(Canvas)); | |
var photoStudioTransform = photoStudio.RectTransform(); | |
var photoStudioCanvas = photoStudio.GetComponent<Canvas>(); | |
var photoStudioCanvasScaler = photoStudioCanvas.GetOrAddComponent<CanvasScaler>(); | |
var screenshotCamera = SetupCamera("Screenshot Camera", photoStudioTransform, Color.clear, cullingMask: LayerMask.GetMask("OffscreenRendering")); | |
photoStudioTransform.SetParent(null); | |
photoStudioTransform.Stretch(); | |
photoStudioTransform.anchoredPosition3D = Vector3.zero.WithX(10000); | |
photoStudioTransform.SetDefaultScale(); | |
// we need something assigned here so the canvas doesn't act as an overlay canvas in the frame before rendering, | |
// as that would cause it to draw over everything and not respect its offscreen position | |
photoStudioCanvas.worldCamera = screenshotCamera; | |
photoStudioCanvas.renderMode = RenderMode.ScreenSpaceCamera; | |
photoStudioCanvas.planeDistance = 0; | |
photoStudioCanvasScaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; | |
photoStudioCanvasScaler.referenceResolution = new Vector2(Screen.width, Screen.height); | |
photoStudioCanvasScaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; | |
photoStudioCanvasScaler.matchWidthOrHeight = 1f; | |
renderSetup?.Invoke(photoStudioTransform); | |
photoStudio.SelfAndDescendants().ForEach(go => go.layer = LayerMask.NameToLayer("OffscreenRendering")); | |
// this fits the camera to the canvas so we can render non-screen-sized canvases to non-screen-sized textures | |
// (in other words, the canvas' width and height directly correspond to the output size when the uniform scale of the canvas is 1) | |
var renderWidth = photoStudioTransform.GetWidth(); | |
var renderHeight = photoStudioTransform.GetHeight(); | |
screenshotCamera.aspect = renderWidth / renderHeight; | |
screenshotCamera.orthographicSize = renderHeight * 0.5f; | |
return RenderToTexture(screenshotCamera, (int)renderWidth, (int)renderHeight) | |
.Finally(() => GameObject.Destroy(photoStudio)); | |
} | |
/// <summary> | |
/// Renders the specified <see cref="Camera"/> to a texture. | |
/// </summary> | |
/// <param name="camera">The camera that will render the content that will be captured to a texture.</param> | |
/// <param name="captureStage">The position in the camera's rendering pipeline where the content will be captured to a texture. | |
/// Consult Unity's documentation on built-in rendering pipelines and command buffers to verify which steps are valid./param> | |
/// <returns>A <see cref="Texture2D"/> with the camera's rendered contents.</returns> | |
private static Texture2D RenderCameraToTexture(Camera camera, int textureWidth, int textureHeight, CameraEvent captureStage = CameraEvent.AfterEverything) | |
{ | |
// Prep the film | |
var screenCopyTexture = RenderTexture.GetTemporary(textureWidth, textureHeight, 32, RenderTextureFormat.ARGB32); | |
if (screenCopyTexture == null) | |
Debug.LogError("Could not obtain temporary render texture"); | |
var outputTextureId = new RenderTargetIdentifier(screenCopyTexture); | |
var activeRenderTexture = RenderTexture.active; | |
RenderTexture.active = screenCopyTexture; | |
GL.Clear(true, true, Color.clear); | |
RenderTexture.active = activeRenderTexture; | |
// Snap a shot | |
camera.forceIntoRenderTexture = true; | |
if (captureStage == CameraEvent.AfterEverything) | |
{ | |
// if we want the final rendering result, simply assign the output to the camera | |
if (camera.targetTexture != null) camera.targetTexture.Release(); | |
camera.targetTexture = screenCopyTexture; | |
camera.Render(); | |
camera.targetTexture = null; | |
} | |
else | |
{ | |
// for captures of intermediate rendering steps, use command buffers | |
var commandBuffer = BuildCameraCopyCommandBuffer(camera, outputTextureId); | |
camera.AddCommandBuffer(captureStage, commandBuffer); | |
camera.Render(); | |
camera.RemoveCommandBuffer(captureStage, commandBuffer); | |
commandBuffer.Release(); | |
} | |
camera.forceIntoRenderTexture = false; | |
// Develop the film into a texture | |
var output = screenCopyTexture.ToTexture2D(); | |
RenderTexture.ReleaseTemporary(screenCopyTexture); | |
return output; | |
} | |
/// <summary> | |
/// Creates a <see cref="Camera"/> with the given name, parent transform and background color and other predefined configurations. | |
/// </summary> | |
/// <param name="cameraName">The name of the game object that will hold the camera component.</param> | |
/// <param name="parent">The transform that the camera will be parented to.</param> | |
/// <param name="backgroundColor">The clear color of the camera's background.</param> | |
/// <param name="cameraClearFlags">The camera's clear flags. Defaults to <see cref="CameraClearFlags.SolidColor"/>.</param> | |
/// <param name="isOrthographic">Whether this is an ortographic camera.</param> | |
private static Camera SetupCamera(string cameraName, Transform parent, | |
Color backgroundColor, | |
LayerMask cullingMask = default, | |
CameraClearFlags cameraClearFlags = CameraClearFlags.SolidColor, | |
bool isOrthographic = true) | |
{ | |
var camera = new GameObject(cameraName).AddComponent<Camera>(); | |
if (parent != null) | |
camera.transform.SetParent(parent, false); | |
camera.cullingMask = cullingMask; | |
camera.clearFlags = cameraClearFlags; | |
camera.backgroundColor = backgroundColor; | |
camera.orthographic = isOrthographic; | |
camera.nearClipPlane = 0; | |
camera.depth = -100; | |
camera.allowMSAA = false; | |
camera.forceIntoRenderTexture = true; | |
camera.enabled = false; // we'll call Render() manually, so the component can be disabled | |
return camera; | |
} | |
/// <summary> | |
/// Constructs a <see cref="CommandBuffer"/> that will directly copy the given <see cref="Camera"/>'s rendered content | |
/// to the output render target. | |
/// </summary> | |
/// <param name="camera">The camera that will provide input.</param> | |
/// <param name="outputRenderTarget">The output render target.</param> | |
/// <returns>A command buffer to be executed or added to a camera.</returns> | |
private static CommandBuffer BuildCameraCopyCommandBuffer(Camera camera, RenderTargetIdentifier outputRenderTarget) | |
{ | |
var sourceTexture = camera.allowMSAA ? BuiltinRenderTextureType.CameraTarget : BuiltinRenderTextureType.CurrentActive; | |
int screenCopyId = Shader.PropertyToID("_MainTex"); | |
var commandBuffer = new CommandBuffer(); | |
commandBuffer.name = "Copy Screen"; | |
// Command buffers do not appear to set _MainTex properly on the default blit shader when using a temporary RT, so we blit the screen to _MainTex first | |
commandBuffer.Blit(sourceTexture, screenCopyId); | |
commandBuffer.Blit(screenCopyId, outputRenderTarget, new Material(Shader.Find("Hidden/BlitCopy"))); | |
return commandBuffer; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment