Skip to content

Instantly share code, notes, and snippets.

@mickdekkers
Last active March 8, 2024 19:02
  • Star 49 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save mickdekkers/5c3c62539c057010d4497f9865060e20 to your computer and use it in GitHub Desktop.
Take snapshot images of Prefabs and GameObjects in Unity using Render Textures
using UnityEditor;
using UnityEngine;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
// Object rendering code based on Dave Carlile's "Create a GameObject Image Using Render Textures" post
// Link: http://crappycoding.com/2014/12/create-gameobject-image-using-render-textures/
/// <summary>
/// Takes snapshot images of prefabs and GameObject instances, and provides methods to save them as PNG files.
/// </summary>
public class SnapshotCamera : MonoBehaviour {
// This disables the "never assigned" warning.
// These fields will be assigned by the factory.
#pragma warning disable 0649
/// <summary>
/// The Camera used internally by the SnapshotCamera.
/// </summary>
private Camera cam;
/// <summary>
/// The layer on which the SnapshotCamera takes snapshots.
/// </summary>
private int layer;
#pragma warning restore 0649
/// <summary>
/// The default position offset applied to objects when none is specified.
/// </summary>
public Vector3 defaultPositionOffset = new Vector3(0, 0, 1);
/// <summary>
/// The default rotation applied to objects when none is specified.
/// </summary>
public Vector3 defaultRotation = new Vector3(345.8529f, 313.8297f, 14.28433f);
/// <summary>
/// The default scale applied to objects when none is specified.
/// </summary>
public Vector3 defaultScale = new Vector3(1, 1, 1);
// This private constructor serves to ensure only the factory can produce new instances.
private SnapshotCamera () { }
/// <summary>
/// Factory method which sets up and configures a new SnapshotCamera, then returns it.
/// </summary>
/// <param name="layer">The name of the layer on which to take snapshots.</param>
/// <param name="name">The name that will be given to the new GameObject the SnapshotCamera will be attached to.</param>
/// <returns>A new SnapshotCamera, ready for use.</returns>
public static SnapshotCamera MakeSnapshotCamera (string layer, string name = "Snapshot Camera")
{
return MakeSnapshotCamera(LayerMask.NameToLayer(layer), name);
}
/// <summary>
/// Factory method which sets up and configures a new SnapshotCamera, then returns it.
/// </summary>
/// <param name="layer">The layer number of the layer on which to take snapshots.</param>
/// <param name="name">The name that will be given to the new GameObject the SnapshotCamera will be attached to.</param>
/// <returns>A new SnapshotCamera, ready for use.</returns>
public static SnapshotCamera MakeSnapshotCamera (int layer = 5, string name = "Snapshot Camera")
{
if (layer < 0 || layer > 31)
throw new ArgumentOutOfRangeException("layer", "layer argument must specify a valid layer between 0 and 31");
// Create a new GameObject to hold the camera
GameObject snapshotCameraGO = new GameObject(name);
// Add a Camera component to the GameObject
Camera cam = snapshotCameraGO.AddComponent<Camera>();
// Configure the Camera
cam.cullingMask = 1 << layer;
cam.orthographic = true;
cam.orthographicSize = 1;
cam.clearFlags = CameraClearFlags.SolidColor;
cam.backgroundColor = Color.clear;
cam.nearClipPlane = 0.1f;
cam.enabled = false;
// Add a SnapshotCamera component to the GameObject
SnapshotCamera snapshotCamera = snapshotCameraGO.AddComponent<SnapshotCamera>();
// Set the SnapshotCamera's cam and layer fields
snapshotCamera.cam = cam;
snapshotCamera.layer = layer;
// Return the SnapshotCamera
return snapshotCamera;
}
#region PNG saving
/// <summary>
/// Sanitizes a filename string by replacing illegal characters with underscores.
/// </summary>
/// <param name="dirty">The unsanitized filename string.</param>
/// <returns>A sanitized filename string with illegal characters replaced with underscores.</returns>
private static string SanitizeFilename (string dirty)
{
string invalidFileNameChars = Regex.Escape(new string(Path.GetInvalidFileNameChars()));
string invalidRegStr = string.Format(@"([{0}]*\.+$)|([{0}]+)", invalidFileNameChars);
return Regex.Replace(dirty, invalidRegStr, "_");
}
/// <summary>
/// Saves a byte array of PNG data as a PNG file.
/// </summary>
/// <param name="bytes">The PNG data to write to a file.</param>
/// <param name="filename">The name of the file. This will be the current timestamp if not specified.</param>
/// <param name="directory">The directory in which to save the file. This will be the game/Snapshots directory if not specified.</param>
/// <returns>A FileInfo pointing to the created PNG file</returns>
public static FileInfo SavePNG (byte[] bytes, string filename = "", string directory = "")
{
directory = directory != "" ? Directory.CreateDirectory(directory).FullName : Directory.CreateDirectory(Path.Combine(Application.dataPath, "../Snapshots")).FullName;
filename = filename != "" ? SanitizeFilename(filename) + ".png" : System.DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss-ffff") + ".png";
string filepath = Path.Combine(directory, filename);
File.WriteAllBytes(filepath, bytes);
return new FileInfo(filepath);
}
/// <summary>
/// Saves a Texture2D as a PNG file.
/// </summary>
/// <param name="tex">The Texture2D to write to a file.</param>
/// <param name="filename">The name of the file. This will be the current timestamp if not specified.</param>
/// <param name="directory">The directory in which to save the file. This will be the game/Snapshots directory if not specified.</param>
/// <returns>A FileInfo pointing to the created PNG file</returns>
public static FileInfo SavePNG (Texture2D tex, string filename = "", string directory = "")
{
return SavePNG(tex.EncodeToPNG(), filename, directory);
}
#endregion
#region Object preparation
/// <summary>
/// This stores the a state (layers, position, rotation, and scale) of a GameObject, and provides a method to restore it.
/// </summary>
private struct GameObjectStateSnapshot
{
private GameObject gameObject;
private Vector3 position;
private Quaternion rotation;
private Vector3 scale;
private Dictionary<GameObject, int> layers;
/// <summary>
/// Store the current state (layers, position, rotation, and scale) of a GameObject
/// </summary>
/// <param name="gameObject">The GameObject whose state to store.</param>
public GameObjectStateSnapshot (GameObject gameObject)
{
this.gameObject = gameObject;
this.position = gameObject.transform.position;
this.rotation = gameObject.transform.rotation;
this.scale = gameObject.transform.localScale;
this.layers = new Dictionary<GameObject, int>();
foreach (Transform t in gameObject.GetComponentsInChildren<Transform>(true))
{
this.layers.Add(t.gameObject, t.gameObject.layer);
}
}
/// <summary>
/// Restore the gameObject to the state stored in this GameObjectStateSnapshot.
/// </summary>
public void Restore ()
{
this.gameObject.transform.position = this.position;
this.gameObject.transform.rotation = this.rotation;
this.gameObject.transform.localScale = this.scale;
foreach (KeyValuePair<GameObject, int> entry in this.layers)
{
entry.Key.layer = entry.Value;
}
}
}
/// <summary>
/// Set the layers of the GameObject and all its children to the SnapshotCamera's snapshot layer so the SnapshotCamera can see it.
/// </summary>
/// <param name="gameObject">The GameObject apply the layer modifications to.</param>
private void SetLayersRecursively (GameObject gameObject)
{
foreach (Transform transform in gameObject.GetComponentsInChildren<Transform>(true))
transform.gameObject.layer = layer;
}
/// <summary>
/// Prepares an instantiated GameObject for taking a snapshot by setting its layers and applying the specified position offset, rotation, and scale to it.
/// </summary>
/// <param name="prefab">The instantiated GameObject to prepare.</param>
/// <param name="positionOffset">The position offset relative to the SnapshotCamera to apply to the gameObject.</param>
/// <param name="rotation">The rotation to apply to the gameObject.</param>
/// <param name="scale">The scale to apply to the gameObject.</param>
/// <returns>A GameObjectStateSnapshot containing the state of the gameObject prior to modifying its layers, position, rotation, and scale.</returns>
private GameObjectStateSnapshot PrepareObject (GameObject gameObject, Vector3 positionOffset, Quaternion rotation, Vector3 scale)
{
GameObjectStateSnapshot goss = new GameObjectStateSnapshot(gameObject);
gameObject.transform.position = transform.position + positionOffset;
gameObject.transform.rotation = rotation;
gameObject.transform.localScale = scale;
SetLayersRecursively(gameObject);
return goss;
}
/// <summary>
/// Prepares a prefab for taking a snapshot by creating an instance, setting its layers and applying the specified position offset, rotation, and scale to it.
/// </summary>
/// <param name="prefab">The prefab to prepare.</param>
/// <param name="positionOffset">The position offset relative to the SnapshotCamera to apply to the prefab.</param>
/// <param name="rotation">The rotation to apply to the prefab.</param>
/// <param name="scale">The scale to apply to the prefab.</param>
/// <returns>A prefab instance ready for taking a snapshot.</returns>
private GameObject PreparePrefab (GameObject prefab, Vector3 positionOffset, Quaternion rotation, Vector3 scale)
{
GameObject gameObject = GameObject.Instantiate(prefab, transform.position + positionOffset, rotation) as GameObject;
gameObject.transform.localScale = scale;
SetLayersRecursively(gameObject);
return gameObject;
}
#endregion
#region TakeObjectSnapshot
/// <summary>
/// Takes a snapshot of an instantiated GameObject and returns it as a Texture2D.
///
/// Uses a completely transparent background and
/// applies the default position offset, rotation and scale to the gameObject while taking the snapshot, and restores them afterwards.
/// </summary>
/// <param name="gameObject">The instantiated GameObject to snapshot.</param>
/// <param name="width">The width of the snapshot image.</param>
/// <param name="height">The height of the snapshot image.</param>
/// <returns>A Texture2D containing the captured snapshot.</returns>
public Texture2D TakeObjectSnapshot (GameObject gameObject, int width = 128, int height = 128)
{
return TakeObjectSnapshot(gameObject, Color.clear, defaultPositionOffset, Quaternion.Euler(defaultRotation), defaultScale, width, height);
}
/// <summary>
/// Takes a snapshot of an instantiated GameObject and returns it as a Texture2D.
///
/// Applies the default position offset, rotation and scale to the gameObject while taking the snapshot, and restores them afterwards.
/// </summary>
/// <param name="gameObject">The instantiated GameObject to snapshot.</param>
/// <param name="backgroundColor">The background color of the snapshot. Can be transparent.</param>
/// <param name="width">The width of the snapshot image.</param>
/// <param name="height">The height of the snapshot image.</param>
/// <returns>A Texture2D containing the captured snapshot.</returns>
public Texture2D TakeObjectSnapshot (GameObject gameObject, Color backgroundColor, int width = 128, int height = 128)
{
return TakeObjectSnapshot (gameObject, backgroundColor, defaultPositionOffset, Quaternion.Euler(defaultRotation), defaultScale, width, height);
}
/// <summary>
/// Takes a snapshot of an instantiated GameObject and returns it as a Texture2D.
///
/// Uses a completely transparent background.
/// </summary>
/// <param name="gameObject">The instantiated GameObject to snapshot.</param>
/// <param name="positionOffset">The position offset relative to the SnapshotCamera that will be applied to the gameObject while taking the snapshot. Its position will be restored after taking the snapshot.</param>
/// <param name="rotation">The rotation that will be applied to the gameObject while taking the snapshot. Its rotation will be restored after taking the snapshot.</param>
/// <param name="scale">The scale that will be applied to the gameObject while taking the snapshot. Its scale will be restored after taking the snapshot.</param>
/// <param name="width">The width of the snapshot image.</param>
/// <param name="height">The height of the snapshot image.</param>
/// <returns>A Texture2D containing the captured snapshot.</returns>
public Texture2D TakeObjectSnapshot (GameObject gameObject, Vector3 positionOffset, Quaternion rotation, Vector3 scale, int width = 128, int height = 128)
{
return TakeObjectSnapshot(gameObject, Color.clear, positionOffset, rotation, scale, width, height);
}
/// <summary>
/// Takes a snapshot of an instantiated GameObject and returns it as a Texture2D.
/// </summary>
/// <param name="gameObject">The instantiated GameObject to snapshot.</param>
/// <param name="backgroundColor">The background color of the snapshot. Can be transparent.</param>
/// <param name="positionOffset">The position offset relative to the SnapshotCamera that will be applied to the gameObject while taking the snapshot. Its position will be restored after taking the snapshot.</param>
/// <param name="rotation">The rotation that will be applied to the gameObject while taking the snapshot. Its rotation will be restored after taking the snapshot.</param>
/// <param name="scale">The scale that will be applied to the gameObject while taking the snapshot. Its scale will be restored after taking the snapshot.</param>
/// <param name="width">The width of the snapshot image.</param>
/// <param name="height">The height of the snapshot image.</param>
/// <returns>A Texture2D containing the captured snapshot.</returns>
public Texture2D TakeObjectSnapshot (GameObject gameObject, Color backgroundColor, Vector3 positionOffset, Quaternion rotation, Vector3 scale, int width = 128, int height = 128)
{
if (gameObject == null)
throw new ArgumentNullException("gameObject");
else if (gameObject.scene.name == null)
throw new ArgumentException("gameObject parameter must be an instantiated GameObject! If you want to use a prefab directly, use TakePrefabSnapshot instead.", "gameObject");
// Prepare the gameObject and save its current state so we can restore it later
GameObjectStateSnapshot previousState = PrepareObject(gameObject, positionOffset, rotation, scale);
// Take a snapshot
Texture2D snapshot = TakeSnapshot(backgroundColor, width, height);
// Restore the gameObject to its previous state
previousState.Restore();
// Return the snapshot
return snapshot;
}
#endregion
#region TakePrefabSnapshot
/// <summary>
/// Takes a snapshot of a prefab and returns it as a Texture2D.
///
/// Uses a completely transparent background and
/// applies the default position offset, rotation and scale to the prefab while taking the snapshot.
/// </summary>
/// <param name="prefab">The prefab to snapshot.</param>
/// <param name="width">The width of the snapshot image.</param>
/// <param name="height">The height of the snapshot image.</param>
/// <returns>A Texture2D containing the captured snapshot.</returns>
public Texture2D TakePrefabSnapshot (GameObject prefab, int width = 128, int height = 128)
{
return TakePrefabSnapshot(prefab, Color.clear, defaultPositionOffset, Quaternion.Euler(defaultRotation), defaultScale, width, height);
}
/// <summary>
/// Takes a snapshot of a prefab and returns it as a Texture2D.
///
/// Applies the default position offset, rotation and scale to the prefab while taking the snapshot.
/// </summary>
/// <param name="prefab">The prefab to snapshot.</param>
/// <param name="backgroundColor">The background color of the snapshot. Can be transparent.</param>
/// <param name="width">The width of the snapshot image.</param>
/// <param name="height">The height of the snapshot image.</param>
/// <returns>A Texture2D containing the captured snapshot.</returns>
public Texture2D TakePrefabSnapshot (GameObject prefab, Color backgroundColor, int width = 128, int height = 128)
{
return TakePrefabSnapshot(prefab, backgroundColor, defaultPositionOffset, Quaternion.Euler(defaultRotation), defaultScale, width, height);
}
/// <summary>
/// Takes a snapshot of a prefab and returns it as a Texture2D.
///
/// Uses a completely transparent background.
/// </summary>
/// <param name="prefab">The prefab to snapshot.</param>
/// <param name="positionOffset">The position offset relative to the SnapshotCamera that will be applied to the prefab while taking the snapshot.</param>
/// <param name="rotation">The rotation that will be applied to the prefab while taking the snapshot.</param>
/// <param name="scale">The scale that will be applied to the prefab while taking the snapshot.</param>
/// <param name="width">The width of the snapshot image.</param>
/// <param name="height">The height of the snapshot image.</param>
/// <returns>A Texture2D containing the captured snapshot.</returns>
public Texture2D TakePrefabSnapshot (GameObject prefab, Vector3 positionOffset, Quaternion rotation, Vector3 scale, int width = 128, int height = 128)
{
return TakePrefabSnapshot(prefab, Color.clear, positionOffset, rotation, scale, width, height);
}
/// <summary>
/// Takes a snapshot of a prefab and returns it as a Texture2D.
/// </summary>
/// <param name="prefab">The prefab to snapshot.</param>
/// <param name="backgroundColor">The background color of the snapshot. Can be transparent.</param>
/// <param name="positionOffset">The position offset relative to the SnapshotCamera that will be applied to the prefab while taking the snapshot.</param>
/// <param name="rotation">The rotation that will be applied to the prefab while taking the snapshot.</param>
/// <param name="scale">The scale that will be applied to the prefab while taking the snapshot.</param>
/// <param name="width">The width of the snapshot image.</param>
/// <param name="height">The height of the snapshot image.</param>
/// <returns>A Texture2D containing the captured snapshot.</returns>
public Texture2D TakePrefabSnapshot (GameObject prefab, Color backgroundColor, Vector3 positionOffset, Quaternion rotation, Vector3 scale, int width = 128, int height = 128)
{
if (prefab == null)
throw new ArgumentNullException("prefab");
else if (prefab.scene.name != null)
throw new ArgumentException("prefab parameter must be a prefab! If you want to use an instance, use TakeObjectSnapshot instead.", "prefab");
// Prepare an instance of the prefab
GameObject instance = PreparePrefab(prefab, positionOffset, rotation, scale);
// Take a snapshot
Texture2D snapshot = TakeSnapshot(backgroundColor, width, height);
// Destroy the instance we created
DestroyImmediate(instance);
// Return the snapshot
return snapshot;
}
#endregion
/// <summary>
/// Takes a snapshot of whatever is in front of the camera and within the camera's culling mask and returns it as a Texture2D.
/// </summary>
/// <param name="backgroundColor">The background color to apply to the camera before taking the snapshot.</param>
/// <param name="width">The width of the snapshot image.</param>
/// <param name="height">The height of the snapshot image.</param>
/// <returns>A Texture2D containing the captured snapshot.</returns>
private Texture2D TakeSnapshot(Color backgroundColor, int width, int height)
{
// Set the background color of the camera
cam.backgroundColor = backgroundColor;
// Get a temporary render texture and render the camera
cam.targetTexture = RenderTexture.GetTemporary(width, height, 24);
cam.Render();
// Activate the temporary render texture
RenderTexture previouslyActiveRenderTexture = RenderTexture.active;
RenderTexture.active = cam.targetTexture;
// Extract the image into a new texture without mipmaps
Texture2D texture = new Texture2D(cam.targetTexture.width, cam.targetTexture.height, TextureFormat.ARGB32, false);
texture.ReadPixels(new Rect(0, 0, cam.targetTexture.width, cam.targetTexture.height), 0, 0);
texture.Apply(false);
// Reactivate the previously active render texture
RenderTexture.active = previouslyActiveRenderTexture;
// Clean up after ourselves
cam.targetTexture = null;
RenderTexture.ReleaseTemporary(cam.targetTexture);
// Return the texture
return texture;
}
}
This file has been truncated, but you can view the full file.
@AndBje
Copy link

AndBje commented Nov 25, 2018

Excellent code and many kudos for taking your time to write and publish it for all of us!

@z3nth10n
Copy link

z3nth10n commented Apr 4, 2019

Your implementation is great but it has an issue and is that the orthographicSize variable from Camera doesn't fit the Renderer bounds.

@caochao
Copy link

caochao commented Nov 12, 2019

haha, so many code comments

@davwen
Copy link

davwen commented Apr 14, 2020

Can I use this commercially?

@mickdekkers
Copy link
Author

mickdekkers commented Apr 14, 2020

@davwen Sure! I forgot to add a license to the gist initially but consider it MIT licensed :)

@slokuge
Copy link

slokuge commented May 7, 2020

Thanks for this ! Helped me out a lot on a school project

@SFoster84
Copy link

Hello, thanks for providing this example! Currently, if I take a picture of the prefab and set a resolution of like 1440x1080, I get a very small prefab in the center of the big picture. Is there a way to have it zoom in on the prefab?

@Vaupell
Copy link

Vaupell commented Apr 23, 2021

Excellent, forked it for some customization 👯

@neoneper
Copy link

neoneper commented Apr 8, 2022

Hey guy! How to use it?

@schniggo1994
Copy link

schniggo1994 commented Apr 21, 2022

Hi, thx for the code. For a 1x1x1 its really nice, but when i change one side to for example 5, it still takes a snapshot of a 1 x 1 x 1 cube. Somebody know why?

@SoFizzticated
Copy link

Hey guy! How to use it?

Any luck or hints? Quite the newbie at unity

@DrCord
Copy link

DrCord commented Mar 27, 2023

Very simple Implementation to help others use this:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SnapshotItem : MonoBehaviour
{
    private SnapshotCamera snapshotCamera;
    public GameObject gameObjectToSnapshot;

    void Start()
    {
        snapshotCamera = SnapshotCamera.MakeSnapshotCamera(0);
        Texture2D snapshot = snapshotCamera.TakeObjectSnapshot(gameObjectToSnapshot);
        SnapshotCamera.SavePNG(snapshot);
    }
}

@tuyenduy
Copy link

Hey guy! How to use it?

  1. Unzip, copy contents in Assests folder to your project's folder "Snapshot" etc. You may need to adjust materials to use your Render Pipeline.
  2. Open sample scene "Testscene" inside, you can add your own prefabs.. You may need to change Input to use new Input System, like this

// SnapshotCameraTest.cs
using UnityEngine.InputSystem;
update function:
if (Keyboard.current.spaceKey.wasPressedThisFrame)
//if (Input.GetKeyUp(KeyCode.Space))

  1. Create a layer name it "SnapshotLayer", change Camera, the test cubes and your prefabs to this layer.
  2. Run scene, take snapshot of test cube by hitting Space. Snapshots are saved in Project-folder/Snapshots.

Hope this helps

@Goldenflamer
Copy link

well done ty

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment