210 lines
8.4 KiB
C#
210 lines
8.4 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace io.github.hatayama.uLoopMCP
|
|
{
|
|
/// <summary>
|
|
/// Utility class for capturing any EditorWindow to a Texture2D.
|
|
/// Uses InternalEditorUtilityBridge to access Unity internal GrabPixels method.
|
|
/// </summary>
|
|
public static class EditorWindowCaptureUtility
|
|
{
|
|
/// <summary>
|
|
/// Find all EditorWindows matching the given name (title bar text).
|
|
/// </summary>
|
|
/// <param name="windowName">Window name displayed in the title bar (e.g., "Console", "Inspector")</param>
|
|
/// <param name="matchMode">Matching mode: exact, prefix, or contains (all case-insensitive)</param>
|
|
/// <returns>Array of matching EditorWindows (empty if none found)</returns>
|
|
public static EditorWindow[] FindWindowsByName(string windowName, WindowMatchMode matchMode = WindowMatchMode.exact)
|
|
{
|
|
if (string.IsNullOrEmpty(windowName))
|
|
{
|
|
return Array.Empty<EditorWindow>();
|
|
}
|
|
|
|
List<EditorWindow> matchingWindows = new List<EditorWindow>();
|
|
EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll<EditorWindow>();
|
|
foreach (EditorWindow window in allWindows)
|
|
{
|
|
if (window.titleContent == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
string title = window.titleContent.text;
|
|
bool isMatch = matchMode switch
|
|
{
|
|
WindowMatchMode.exact => title.Equals(windowName, StringComparison.OrdinalIgnoreCase),
|
|
WindowMatchMode.prefix => title.StartsWith(windowName, StringComparison.OrdinalIgnoreCase),
|
|
WindowMatchMode.contains => title.Contains(windowName, StringComparison.OrdinalIgnoreCase),
|
|
_ => title.Equals(windowName, StringComparison.OrdinalIgnoreCase)
|
|
};
|
|
|
|
if (isMatch)
|
|
{
|
|
matchingWindows.Add(window);
|
|
}
|
|
}
|
|
|
|
return matchingWindows.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Capture an EditorWindow to a Texture2D asynchronously.
|
|
/// Waits for 2 frames after showing the window to ensure it is fully rendered.
|
|
/// </summary>
|
|
/// <param name="window">The EditorWindow to capture</param>
|
|
/// <param name="resolutionScale">Resolution scale (0.1 to 1.0)</param>
|
|
/// <param name="ct">Cancellation token</param>
|
|
/// <returns>Captured Texture2D, or null if capture failed</returns>
|
|
public static async Task<Texture2D?> CaptureWindowAsync(EditorWindow window, float resolutionScale, CancellationToken ct)
|
|
{
|
|
if (window == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
window.ShowTab();
|
|
await EditorDelay.DelayFrame(2, ct);
|
|
return CaptureWindowInternal(window, resolutionScale);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal capture logic shared by sync and async methods.
|
|
/// </summary>
|
|
private static Texture2D? CaptureWindowInternal(EditorWindow window, float resolutionScale)
|
|
{
|
|
float scale = EditorGUIUtility.pixelsPerPoint;
|
|
int width = Mathf.RoundToInt(window.position.width * scale);
|
|
int height = Mathf.RoundToInt(window.position.height * scale);
|
|
|
|
if (width <= 0 || height <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// For Linear color space, disable the sRGB flag to prevent double gamma conversion
|
|
RenderTextureDescriptor descriptor = new RenderTextureDescriptor(width, height, RenderTextureFormat.ARGB32, 24);
|
|
if (QualitySettings.activeColorSpace == ColorSpace.Linear)
|
|
{
|
|
descriptor.sRGB = false;
|
|
}
|
|
RenderTexture rt = RenderTexture.GetTemporary(descriptor);
|
|
|
|
InternalEditorUtilityBridge.CaptureEditorWindow(window, rt);
|
|
|
|
RenderTexture previousActive = RenderTexture.active;
|
|
RenderTexture.active = rt;
|
|
|
|
Texture2D texture = new Texture2D(width, height, TextureFormat.RGB24, false);
|
|
texture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
|
|
texture.Apply();
|
|
|
|
RenderTexture.active = previousActive;
|
|
RenderTexture.ReleaseTemporary(rt);
|
|
|
|
if (!Mathf.Approximately(resolutionScale, 1.0f))
|
|
{
|
|
texture = ApplyResolutionScaling(texture, resolutionScale);
|
|
}
|
|
|
|
return texture;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get a list of all open EditorWindow names.
|
|
/// </summary>
|
|
/// <returns>Array of window names</returns>
|
|
public static string[] GetOpenWindowNames()
|
|
{
|
|
EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll<EditorWindow>();
|
|
List<string> names = new List<string>();
|
|
|
|
foreach (EditorWindow window in allWindows)
|
|
{
|
|
if (window.titleContent != null && !string.IsNullOrEmpty(window.titleContent.text))
|
|
{
|
|
names.Add(window.titleContent.text);
|
|
}
|
|
}
|
|
|
|
return names.ToArray();
|
|
}
|
|
|
|
// Captures game rendering by reading GameView's composited RenderTexture (PlayMode only).
|
|
// Contains all cameras + Screen Space Overlay Canvas, without tab bar or borders.
|
|
// Coordinate mapping to simulate-mouse:
|
|
// sim_x = image_x / resolutionScale, sim_y = image_y / resolutionScale + YOffset
|
|
// where YOffset = Screen.height - RenderTexture.height (returned in the tuple).
|
|
public static async Task<(Texture2D? texture, int yOffset)> CaptureGameRenderingAsync(float resolutionScale, CancellationToken ct)
|
|
{
|
|
Debug.Assert(UnityEditor.EditorApplication.isPlaying, "CaptureGameRenderingAsync requires PlayMode");
|
|
|
|
// Wait for the game camera to complete at least one full render cycle after any state change
|
|
await EditorDelay.DelayFrame(2, ct);
|
|
|
|
RenderTexture rt = GameViewBridge.GetRenderTexture();
|
|
if (rt == null)
|
|
{
|
|
Debug.LogWarning("[EditorWindowCaptureUtility] GameView RenderTexture is not available");
|
|
return (null, 0);
|
|
}
|
|
|
|
int yOffset = (int)Handles.GetMainGameViewSize().y - rt.height;
|
|
|
|
// RenderTexture uses bottom-left origin; flip vertically for standard top-left image format
|
|
RenderTextureDescriptor flipDescriptor = new RenderTextureDescriptor(rt.width, rt.height, rt.format, 0);
|
|
if (QualitySettings.activeColorSpace == ColorSpace.Linear)
|
|
{
|
|
flipDescriptor.sRGB = false;
|
|
}
|
|
RenderTexture flipped = RenderTexture.GetTemporary(flipDescriptor);
|
|
Graphics.Blit(rt, flipped, new Vector2(1f, -1f), new Vector2(0f, 1f));
|
|
|
|
RenderTexture previousActive = RenderTexture.active;
|
|
RenderTexture.active = flipped;
|
|
|
|
Texture2D texture = new Texture2D(rt.width, rt.height, TextureFormat.RGB24, false);
|
|
texture.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
|
|
texture.Apply();
|
|
|
|
RenderTexture.active = previousActive;
|
|
RenderTexture.ReleaseTemporary(flipped);
|
|
|
|
if (!Mathf.Approximately(resolutionScale, 1.0f))
|
|
{
|
|
texture = ApplyResolutionScaling(texture, resolutionScale);
|
|
}
|
|
|
|
return (texture, yOffset);
|
|
}
|
|
|
|
private static Texture2D ApplyResolutionScaling(Texture2D originalTexture, float scale)
|
|
{
|
|
int newWidth = Mathf.RoundToInt(originalTexture.width * scale);
|
|
int newHeight = Mathf.RoundToInt(originalTexture.height * scale);
|
|
|
|
Texture2D scaledTexture = new Texture2D(newWidth, newHeight, originalTexture.format, false);
|
|
|
|
RenderTexture rt = RenderTexture.GetTemporary(newWidth, newHeight);
|
|
Graphics.Blit(originalTexture, rt);
|
|
|
|
RenderTexture.active = rt;
|
|
scaledTexture.ReadPixels(new Rect(0, 0, newWidth, newHeight), 0, 0);
|
|
scaledTexture.Apply();
|
|
RenderTexture.active = null;
|
|
|
|
RenderTexture.ReleaseTemporary(rt);
|
|
UnityEngine.Object.DestroyImmediate(originalTexture);
|
|
|
|
return scaledTexture;
|
|
}
|
|
}
|
|
}
|
|
|