Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Capture multiple screenshots with different resolutions simultaneously in Unity 3D
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace MultiScreenshotCaptureNamespace
{
internal static class ReflectionExtensions
{
internal static object FetchField( this Type type, string field )
{
return type.GetFieldRecursive( field, true ).GetValue( null );
}
internal static object FetchField( this object obj, string field )
{
return obj.GetType().GetFieldRecursive( field, false ).GetValue( obj );
}
internal static object FetchProperty( this Type type, string property )
{
return type.GetPropertyRecursive( property, true ).GetValue( null, null );
}
internal static object FetchProperty( this object obj, string property )
{
return obj.GetType().GetPropertyRecursive( property, false ).GetValue( obj, null );
}
internal static object CallMethod( this Type type, string method, params object[] parameters )
{
return type.GetMethod( method, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static ).Invoke( null, parameters );
}
internal static object CallMethod( this object obj, string method, params object[] parameters )
{
return obj.GetType().GetMethod( method, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).Invoke( obj, parameters );
}
internal static object CreateInstance( this Type type, params object[] parameters )
{
Type[] parameterTypes;
if( parameters == null )
parameterTypes = null;
else
{
parameterTypes = new Type[parameters.Length];
for( int i = 0; i < parameters.Length; i++ )
parameterTypes[i] = parameters[i].GetType();
}
return CreateInstance( type, parameterTypes, parameters );
}
internal static object CreateInstance( this Type type, Type[] parameterTypes, object[] parameters )
{
return type.GetConstructor( parameterTypes ).Invoke( parameters );
}
private static FieldInfo GetFieldRecursive( this Type type, string field, bool isStatic )
{
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | ( isStatic ? BindingFlags.Static : BindingFlags.Instance );
do
{
FieldInfo fieldInfo = type.GetField( field, flags );
if( fieldInfo != null )
return fieldInfo;
type = type.BaseType;
} while( type != null );
return null;
}
private static PropertyInfo GetPropertyRecursive( this Type type, string property, bool isStatic )
{
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | ( isStatic ? BindingFlags.Static : BindingFlags.Instance );
do
{
PropertyInfo propertyInfo = type.GetProperty( property, flags );
if( propertyInfo != null )
return propertyInfo;
type = type.BaseType;
} while( type != null );
return null;
}
}
public class MultiScreenshotCapture : EditorWindow
{
private enum TargetCamera { GameView = 0, SceneView = 1 };
private class CustomResolution
{
public readonly int width, height;
private int originalIndex, newIndex;
private bool m_isActive;
public bool IsActive
{
get { return m_isActive; }
set
{
if( m_isActive != value )
{
m_isActive = value;
int resolutionIndex;
if( m_isActive )
{
originalIndex = (int) GameView.FetchProperty( "selectedSizeIndex" );
object customSize = GetFixedResolution( width, height );
SizeHolder.CallMethod( "AddCustomSize", customSize );
newIndex = (int) SizeHolder.CallMethod( "IndexOf", customSize ) + (int) SizeHolder.CallMethod( "GetBuiltinCount" );
resolutionIndex = newIndex;
}
else
{
SizeHolder.CallMethod( "RemoveCustomSize", newIndex );
resolutionIndex = originalIndex;
}
GameView.CallMethod( "SizeSelectionCallback", resolutionIndex, null );
GameView.Repaint();
}
}
}
public CustomResolution( int width, int height )
{
this.width = width;
this.height = height;
}
}
[Serializable]
private class SaveData
{
public List<Vector2> resolutions;
public List<bool> resolutionsEnabled;
public bool currentResolutionEnabled;
}
[Serializable]
private class SessionData
{
public List<Vector2> resolutions;
public List<bool> resolutionsEnabled;
public bool currentResolutionEnabled;
public float resolutionMultiplier;
public TargetCamera targetCamera;
public bool captureOverlayUI;
public bool setTimeScaleToZero;
public bool saveAsPNG;
public bool allowTransparentBackground;
public string saveDirectory;
}
private const string SESSION_DATA_PATH = "Library/MSC_Session.json";
private const string TEMPORARY_RESOLUTION_LABEL = "MSC_temp";
private readonly GUILayoutOption GL_WIDTH_25 = GUILayout.Width( 25f );
private readonly GUILayoutOption GL_EXPAND_WIDTH = GUILayout.ExpandWidth( true );
private static object SizeHolder { get { return GetType( "GameViewSizes" ).FetchProperty( "instance" ).FetchProperty( "currentGroup" ); } }
private static EditorWindow GameView { get { return GetWindow( GetType( "GameView" ) ); } }
//private static EditorWindow GameView { get { return (EditorWindow) GetType( "GameView" ).CallMethod( "GetMainGameView" ); } }
private List<Vector2> resolutions = new List<Vector2>() { new Vector2( 1024, 768 ) }; // Not readonly to support serialization
private List<bool> resolutionsEnabled = new List<bool>() { true }; // Same as above
private bool currentResolutionEnabled = true;
private float resolutionMultiplier = 1f;
private TargetCamera targetCamera = TargetCamera.GameView;
private bool captureOverlayUI = false;
private bool setTimeScaleToZero = true;
private float prevTimeScale;
private bool saveAsPNG = true;
private bool allowTransparentBackground = false;
private string saveDirectory;
private Vector2 scrollPos;
private readonly List<CustomResolution> queuedScreenshots = new List<CustomResolution>();
[MenuItem( "Window/Multi Screenshot Capture" )]
private static void Init()
{
MultiScreenshotCapture window = GetWindow<MultiScreenshotCapture>();
window.titleContent = new GUIContent( "Screenshot" );
window.minSize = new Vector2( 325f, 150f );
window.Show();
}
private void Awake()
{
if( File.Exists( SESSION_DATA_PATH ) )
{
SessionData sessionData = JsonUtility.FromJson<SessionData>( File.ReadAllText( SESSION_DATA_PATH ) );
resolutions = sessionData.resolutions;
resolutionsEnabled = sessionData.resolutionsEnabled;
currentResolutionEnabled = sessionData.currentResolutionEnabled;
resolutionMultiplier = sessionData.resolutionMultiplier > 0f ? sessionData.resolutionMultiplier : 1f;
targetCamera = sessionData.targetCamera;
captureOverlayUI = sessionData.captureOverlayUI;
setTimeScaleToZero = sessionData.setTimeScaleToZero;
saveAsPNG = sessionData.saveAsPNG;
allowTransparentBackground = sessionData.allowTransparentBackground;
saveDirectory = sessionData.saveDirectory;
}
}
private void OnDestroy()
{
SessionData sessionData = new SessionData()
{
resolutions = resolutions,
resolutionsEnabled = resolutionsEnabled,
currentResolutionEnabled = currentResolutionEnabled,
resolutionMultiplier = resolutionMultiplier,
targetCamera = targetCamera,
captureOverlayUI = captureOverlayUI,
setTimeScaleToZero = setTimeScaleToZero,
saveAsPNG = saveAsPNG,
allowTransparentBackground = allowTransparentBackground,
saveDirectory = saveDirectory
};
File.WriteAllText( SESSION_DATA_PATH, JsonUtility.ToJson( sessionData ) );
}
private void OnGUI()
{
// In case resolutionsEnabled didn't exist when the latest SessionData was created
if( resolutionsEnabled == null || resolutionsEnabled.Count != resolutions.Count )
{
resolutionsEnabled = new List<bool>( resolutions.Count );
for( int i = 0; i < resolutions.Count; i++ )
resolutionsEnabled.Add( true );
}
scrollPos = EditorGUILayout.BeginScrollView( scrollPos );
GUILayout.BeginHorizontal();
GUILayout.Label( "Resolutions:", GL_EXPAND_WIDTH );
if( GUILayout.Button( "Save" ) )
SaveSettings();
if( GUILayout.Button( "Load" ) )
LoadSettings();
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
GUI.enabled = currentResolutionEnabled;
GUILayout.Label( "Current Resolution", GL_EXPAND_WIDTH );
GUI.enabled = true;
currentResolutionEnabled = EditorGUILayout.Toggle( GUIContent.none, currentResolutionEnabled, GL_WIDTH_25 );
if( GUILayout.Button( "+", GL_WIDTH_25 ) )
{
resolutions.Insert( 0, new Vector2() );
resolutionsEnabled.Insert( 0, true );
}
GUI.enabled = false;
GUILayout.Button( "-", GL_WIDTH_25 );
GUI.enabled = true;
GUILayout.EndHorizontal();
for( int i = 0; i < resolutions.Count; i++ )
{
GUILayout.BeginHorizontal();
GUI.enabled = resolutionsEnabled[i];
resolutions[i] = EditorGUILayout.Vector2Field( GUIContent.none, resolutions[i] );
GUI.enabled = true;
resolutionsEnabled[i] = EditorGUILayout.Toggle( GUIContent.none, resolutionsEnabled[i], GL_WIDTH_25 );
if( GUILayout.Button( "+", GL_WIDTH_25 ) )
{
resolutions.Insert( i + 1, new Vector2() );
resolutionsEnabled.Insert( i + 1, true );
}
if( GUILayout.Button( "-", GL_WIDTH_25 ) )
{
resolutions.RemoveAt( i );
resolutionsEnabled.RemoveAt( i );
i--;
}
GUILayout.EndHorizontal();
}
EditorGUILayout.Space();
resolutionMultiplier = EditorGUILayout.FloatField( "Resolution Multiplier", resolutionMultiplier );
targetCamera = (TargetCamera) EditorGUILayout.EnumPopup( "Target Camera", targetCamera );
EditorGUILayout.Space();
if( targetCamera == TargetCamera.GameView )
{
captureOverlayUI = EditorGUILayout.ToggleLeft( "Capture Overlay UI", captureOverlayUI );
if( captureOverlayUI && EditorApplication.isPlaying )
{
EditorGUI.indentLevel++;
setTimeScaleToZero = EditorGUILayout.ToggleLeft( "Set timeScale to 0 during capture", setTimeScaleToZero );
EditorGUI.indentLevel--;
}
}
saveAsPNG = EditorGUILayout.ToggleLeft( "Save as PNG", saveAsPNG );
if( saveAsPNG && !captureOverlayUI && targetCamera == TargetCamera.GameView )
{
EditorGUI.indentLevel++;
allowTransparentBackground = EditorGUILayout.ToggleLeft( "Allow transparent background", allowTransparentBackground );
if( allowTransparentBackground )
EditorGUILayout.HelpBox( "For transparent background to work, you may need to disable post-processing on the Main Camera.", MessageType.Info );
EditorGUI.indentLevel--;
}
EditorGUILayout.Space();
saveDirectory = PathField( "Save to:", saveDirectory );
EditorGUILayout.Space();
GUI.enabled = queuedScreenshots.Count == 0 && resolutionMultiplier > 0f;
if( GUILayout.Button( "Capture Screenshots" ) )
{
if( string.IsNullOrEmpty( saveDirectory ) )
saveDirectory = Environment.GetFolderPath( Environment.SpecialFolder.DesktopDirectory );
if( currentResolutionEnabled )
CaptureScreenshot( ( targetCamera == TargetCamera.GameView ? Camera.main : SceneView.lastActiveSceneView.camera ).pixelRect.size );
for( int i = 0; i < resolutions.Count; i++ )
{
if( resolutionsEnabled[i] )
CaptureScreenshot( resolutions[i] );
}
if( !captureOverlayUI || targetCamera == TargetCamera.SceneView )
Debug.Log( "<b>Saved screenshots:</b> " + saveDirectory );
else
{
if( EditorApplication.isPlaying && setTimeScaleToZero )
{
prevTimeScale = Time.timeScale;
Time.timeScale = 0f;
}
EditorApplication.update -= CaptureQueuedScreenshots;
EditorApplication.update += CaptureQueuedScreenshots;
}
}
GUI.enabled = true;
EditorGUILayout.EndScrollView();
}
private void CaptureScreenshot( Vector2 resolution )
{
int width = Mathf.RoundToInt( resolution.x * resolutionMultiplier );
int height = Mathf.RoundToInt( resolution.y * resolutionMultiplier );
if( width <= 0 || height <= 0 )
Debug.LogWarning( "Skipped resolution: " + resolution );
else if( !captureOverlayUI || targetCamera == TargetCamera.SceneView )
CaptureScreenshotWithoutUI( width, height );
else
queuedScreenshots.Add( new CustomResolution( width, height ) );
}
private void CaptureQueuedScreenshots()
{
if( queuedScreenshots.Count == 0 )
{
EditorApplication.update -= CaptureQueuedScreenshots;
return;
}
CustomResolution resolution = queuedScreenshots[0];
if( !resolution.IsActive )
{
resolution.IsActive = true;
if( EditorApplication.isPlaying && EditorApplication.isPaused )
EditorApplication.Step(); // Necessary to refresh overlay UI
}
else
{
try
{
CaptureScreenshotWithUI();
}
catch( Exception e )
{
Debug.LogException( e );
}
resolution.IsActive = false;
queuedScreenshots.RemoveAt( 0 );
if( queuedScreenshots.Count == 0 )
{
if( EditorApplication.isPlaying && EditorApplication.isPaused )
EditorApplication.Step(); // Necessary to restore overlay UI
if( EditorApplication.isPlaying && setTimeScaleToZero )
Time.timeScale = prevTimeScale;
Debug.Log( "<b>Saved screenshots:</b> " + saveDirectory );
Repaint();
}
else
{
// Activate the next resolution immediately
CaptureQueuedScreenshots();
}
}
}
private void CaptureScreenshotWithoutUI( int width, int height )
{
Camera camera = targetCamera == TargetCamera.GameView ? Camera.main : SceneView.lastActiveSceneView.camera;
RenderTexture temp = RenderTexture.active;
RenderTexture temp2 = camera.targetTexture;
RenderTexture renderTex = RenderTexture.GetTemporary( width, height, 24 );
Texture2D screenshot = null;
bool allowHDR = camera.allowHDR;
if( saveAsPNG && allowTransparentBackground )
camera.allowHDR = false;
try
{
RenderTexture.active = renderTex;
camera.targetTexture = renderTex;
camera.Render();
screenshot = new Texture2D( renderTex.width, renderTex.height, saveAsPNG && allowTransparentBackground ? TextureFormat.RGBA32 : TextureFormat.RGB24, false );
screenshot.ReadPixels( new Rect( 0, 0, renderTex.width, renderTex.height ), 0, 0, false );
screenshot.Apply( false, false );
File.WriteAllBytes( GetUniqueFilePath( renderTex.width, renderTex.height ), saveAsPNG ? screenshot.EncodeToPNG() : screenshot.EncodeToJPG( 100 ) );
}
finally
{
camera.targetTexture = temp2;
if( saveAsPNG && allowTransparentBackground )
camera.allowHDR = allowHDR;
RenderTexture.active = temp;
RenderTexture.ReleaseTemporary( renderTex );
if( screenshot != null )
DestroyImmediate( screenshot );
}
}
private void CaptureScreenshotWithUI()
{
RenderTexture temp = RenderTexture.active;
RenderTexture renderTex = (RenderTexture) GameView.FetchField( "m_TargetTexture" );
Texture2D screenshot = null;
int width = renderTex.width;
int height = renderTex.height;
try
{
RenderTexture.active = renderTex;
screenshot = new Texture2D( width, height, saveAsPNG && allowTransparentBackground ? TextureFormat.RGBA32 : TextureFormat.RGB24, false );
screenshot.ReadPixels( new Rect( 0, 0, width, height ), 0, 0, false );
if( SystemInfo.graphicsUVStartsAtTop )
{
Color32[] pixels = screenshot.GetPixels32();
for( int i = 0; i < height / 2; i++ )
{
int startIndex0 = i * width;
int startIndex1 = ( height - i - 1 ) * width;
for( int x = 0; x < width; x++ )
{
Color32 color = pixels[startIndex0 + x];
pixels[startIndex0 + x] = pixels[startIndex1 + x];
pixels[startIndex1 + x] = color;
}
}
screenshot.SetPixels32( pixels );
}
screenshot.Apply( false, false );
File.WriteAllBytes( GetUniqueFilePath( width, height ), saveAsPNG ? screenshot.EncodeToPNG() : screenshot.EncodeToJPG( 100 ) );
}
finally
{
RenderTexture.active = temp;
if( screenshot != null )
DestroyImmediate( screenshot );
}
}
private string PathField( string label, string path )
{
GUILayout.BeginHorizontal();
path = EditorGUILayout.TextField( label, path );
if( GUILayout.Button( "o", GL_WIDTH_25 ) )
{
string selectedPath = EditorUtility.OpenFolderPanel( "Choose output directory", "", "" );
if( !string.IsNullOrEmpty( selectedPath ) )
path = selectedPath;
GUIUtility.keyboardControl = 0; // Remove focus from active text field
}
GUILayout.EndHorizontal();
return path;
}
private void SaveSettings()
{
string savePath = EditorUtility.SaveFilePanel( "Choose destination", "", "resolutions", "json" );
if( !string.IsNullOrEmpty( savePath ) )
{
SaveData saveData = new SaveData()
{
resolutions = resolutions,
resolutionsEnabled = resolutionsEnabled,
currentResolutionEnabled = currentResolutionEnabled
};
File.WriteAllText( savePath, JsonUtility.ToJson( saveData, false ) );
}
}
private void LoadSettings()
{
string loadPath = EditorUtility.OpenFilePanel( "Choose save file", "", "json" );
if( !string.IsNullOrEmpty( loadPath ) )
{
SaveData saveData = JsonUtility.FromJson<SaveData>( File.ReadAllText( loadPath ) );
resolutions = saveData.resolutions ?? new List<Vector2>();
resolutionsEnabled = saveData.resolutionsEnabled ?? new List<bool>();
currentResolutionEnabled = saveData.currentResolutionEnabled;
}
}
private string GetUniqueFilePath( int width, int height )
{
string filename = string.Concat( width, "x", height, " {0}", saveAsPNG ? ".png" : ".jpeg" );
int fileIndex = 0;
string path;
do
{
path = Path.Combine( saveDirectory, string.Format( filename, ++fileIndex ) );
} while( File.Exists( path ) );
return path;
}
private static object GetFixedResolution( int width, int height )
{
object sizeType = Enum.Parse( GetType( "GameViewSizeType" ), "FixedResolution" );
return GetType( "GameViewSize" ).CreateInstance( sizeType, width, height, TEMPORARY_RESOLUTION_LABEL );
}
private static Type GetType( string type )
{
return typeof( EditorWindow ).Assembly.GetType( "UnityEditor." + type );
}
}
}
@yasirkula
Copy link
Author

yasirkula commented Mar 19, 2020

How To

Simply create a folder called Editor inside your Project window and add this script inside it. Then, open Window-Multi Screenshot Capture, configure the parameters and hit "Capture Screenshots".

If "Allow transparent background" is enabled, main camera's background color alpha value determines the background opacity (you need to set camera's Clear Flags to Color). You may also need to disable post-processing on the camera.

ss

@WOLKYDJ
Copy link

WOLKYDJ commented Jul 7, 2021

bu oyun içinde çalışıyor mu? çalışıyorsa istediğimiz alanı seçebiliyor muyuz?

@yasirkula
Copy link
Author

yasirkula commented Jul 7, 2021

Oyun editörde oynanırken çalışıyor, build alınan oyunda çalışmıyor. İstenen alanı seçme özelliği maalesef yok.

@bambucci
Copy link

bambucci commented Apr 22, 2022

Hey, I have an issue with your script on Unity 2021.2.10f1.
Is my current version missing something?

image

@yasirkula
Copy link
Author

yasirkula commented Apr 23, 2022

@andrgotin I couldn't reproduce this issue on 2021 LTS. Is there a class named Type in your project? If you're unsure, you can check it by typing Type in one of your scripts, right clicking the word and then clicking "Go to Definition". Regardless, adding using Type = System.Type; to the top should help.

@bambucci
Copy link

bambucci commented Apr 23, 2022

@yasirkula You are absolutely correct. It's my mistake - I named one of my enums Type. After renaming everything is fixed. Thank you for the help and for making such an awesome tool!

@MuhammedResulBilkil
Copy link

MuhammedResulBilkil commented Apr 28, 2022

@yasirkula It works perfect. Thank you for this contribution to the community 👍

In my opinion, It would be much more efficient to work with this: If we could press a Key (Like KeyCode.G) while playing the game in Game View for capturing Images. With this way we don't need to click "Capture Screenshots" button. I think It would be much more efficient because you need to play the game to capture nice view of the game while playing. It is wearying to go from Game View to Screenshot Editor Window.

@yasirkula
Copy link
Author

yasirkula commented Apr 28, 2022

@MuhammedResulBilkil I agree but I couldn't find a way to capture that key input in Play mode. I've called OnGUI every frame and checked if Event.current.type was KeyDown but it didn't return true in Play mode, perhaps keyboard input was eaten (used) by the Game window.

@SiarheiPilat
Copy link

SiarheiPilat commented Jul 4, 2022

Wow!.. This is amazing. What a great script, I've been looking for something like that!

@Leahheartsisthebest
Copy link

Leahheartsisthebest commented Jul 4, 2022

Well Done! It’s Good😉You should Do more.

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