Skip to content

Instantly share code, notes, and snippets.

@SiarheiPilat
Forked from yasirkula/MultiScreenshotCapture.cs
Last active January 13, 2024 11:55
Show Gist options
  • Save SiarheiPilat/c24b94a88e3ff5cf7ab3675da6c23e9d to your computer and use it in GitHub Desktop.
Save SiarheiPilat/c24b94a88e3ff5cf7ab3675da6c23e9d to your computer and use it in GitHub Desktop.
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 )
{
Camera camera = targetCamera == TargetCamera.GameView ? Camera.main : SceneView.lastActiveSceneView.camera;
CaptureScreenshot( new Vector2( camera.pixelWidth / camera.rect.width, camera.pixelHeight / camera.rect.height ) );
}
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
{
// If Game window's render resolution hasn't changed yet (can happen in play mode on newer Unity versions), wait for it to refresh.
// Not checking resolution equality direclty because Unity may change the resolution slightly (e.g. it clamps min resolution to 10x10
// and if the Main Camera's Viewport Rect isn't full-screen, it'll be subject to floating point imprecision)
RenderTexture renderTex = (RenderTexture) GameView.FetchField( "m_TargetTexture" );
if( Vector2.Distance( new Vector2( resolution.width, resolution.height ), new Vector2( renderTex.width, renderTex.height ) ) > 15f )
return;
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 );
}
}
}
{"resolutions":[{"x":1290.0,"y":2796.0},{"x":1284.0,"y":2778.0},{"x":1242.0,"y":2688.0},{"x":1242.0,"y":2208.0},{"x":2048.0,"y":2732.0}],"resolutionsEnabled":[true,true,true,true,true],"currentResolutionEnabled":false}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment