Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Last active October 24, 2024 05:11
Show Gist options
  • Save yasirkula/fba5c7b5280aa90cdb66a68c4005b52d to your computer and use it in GitHub Desktop.
Save yasirkula/fba5c7b5280aa90cdb66a68c4005b52d 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 );
}
}
}
@yasirkula
Copy link
Author

@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

@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

@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

@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

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

@Leahheartsisthebest
Copy link

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