Files
Fishing2/Packages/cn.tuanjie.codely.bridge/Editor/Tools/ManageScreenshot.cs
2026-03-09 17:50:20 +08:00

663 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Codely.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityTcp.Editor.Helpers; // For Response class
namespace UnityTcp.Editor.Tools
{
/// <summary>
/// Handles screenshot capture operations within Unity Editor.
/// </summary>
public static class ManageScreenshot
{
// --- Main Handler ---
// Define the list of valid actions
private static readonly List<string> ValidActions = new List<string>
{
"capture",
"capture_main_camera",
"capture_specific_camera",
"capture_scene_camera"
};
public static object HandleCommand(JObject @params)
{
string action = @params["action"]?.ToString().ToLower();
if (string.IsNullOrEmpty(action))
{
return Response.Error("Action parameter is required.");
}
// Check if the action is valid before switching
if (!ValidActions.Contains(action))
{
string validActionsList = string.Join(", ", ValidActions);
return Response.Error(
$"Unknown action: '{action}'. Valid actions are: {validActionsList}"
);
}
try
{
switch (action)
{
case "capture":
return CaptureGameView(@params);
case "capture_main_camera":
return CaptureMainCamera(@params);
case "capture_specific_camera":
return CaptureSpecificCamera(@params);
case "capture_scene_camera":
return CaptureSceneCamera(@params);
default:
string validActionsListDefault = string.Join(", ", ValidActions);
return Response.Error(
$"Unknown action: '{action}'. Valid actions are: {validActionsListDefault}"
);
}
}
catch (Exception e)
{
Debug.LogError($"[ManageScreenshot] Action '{action}' failed: {e}");
return Response.Error(
$"Internal error processing action '{action}': {e.Message}"
);
}
}
// --- Action Implementations ---
/// <summary>
/// Menu item to capture screenshot from game view
/// </summary>
public static void CaptureScreenshotMenuItem()
{
CaptureGameViewAndSave();
}
private static object CaptureGameView(JObject @params)
{
string customPath = @params["path"]?.ToString();
string customFilename = @params["filename"]?.ToString();
int? width = @params["width"]?.ToObject<int?>();
int? height = @params["height"]?.ToObject<int?>();
try
{
string savedPath = CaptureGameViewAndSave(customPath, customFilename, width, height);
if (savedPath != null)
{
return Response.Success(
"Screenshot captured successfully from Game view.",
new { path = savedPath, camera = "Game View" }
);
}
else
{
return Response.Error("Failed to capture screenshot from Game view.");
}
}
catch (Exception e)
{
return Response.Error($"Failed to capture screenshot from Game view: {e.Message}");
}
}
private static object CaptureMainCamera(JObject @params)
{
string customPath = @params["path"]?.ToString();
string customFilename = @params["filename"]?.ToString();
int? width = @params["width"]?.ToObject<int?>();
int? height = @params["height"]?.ToObject<int?>();
try
{
string savedPath = CaptureAndSave(customPath, customFilename, width, height);
if (savedPath != null)
{
return Response.Success(
"Screenshot captured successfully from main camera.",
new { path = savedPath, camera = "Main Camera" }
);
}
else
{
return Response.Error("Failed to capture screenshot from main camera.");
}
}
catch (Exception e)
{
return Response.Error($"Failed to capture screenshot: {e.Message}");
}
}
private static object CaptureSpecificCamera(JObject @params)
{
string cameraName = @params["cameraName"]?.ToString();
string customPath = @params["path"]?.ToString();
string customFilename = @params["filename"]?.ToString();
int? width = @params["width"]?.ToObject<int?>();
int? height = @params["height"]?.ToObject<int?>();
if (string.IsNullOrEmpty(cameraName))
{
return Response.Error("'cameraName' parameter is required for capture_specific_camera action.");
}
try
{
Camera targetCamera = GameObject.Find(cameraName)?.GetComponent<Camera>();
if (targetCamera == null)
{
// Try finding by name in all cameras
Camera[] cameras = UnityEngine.Object.FindObjectsOfType<Camera>();
foreach (Camera cam in cameras)
{
if (cam.name.Equals(cameraName, StringComparison.OrdinalIgnoreCase))
{
targetCamera = cam;
break;
}
}
}
if (targetCamera == null)
{
return Response.Error($"Camera '{cameraName}' not found in the scene.");
}
string savedPath = CaptureAndSave(customPath, customFilename, width, height, targetCamera);
if (savedPath != null)
{
return Response.Success(
$"Screenshot captured successfully from camera '{cameraName}'.",
new { path = savedPath, camera = cameraName }
);
}
else
{
return Response.Error($"Failed to capture screenshot from camera '{cameraName}'.");
}
}
catch (Exception e)
{
return Response.Error($"Failed to capture screenshot from camera '{cameraName}': {e.Message}");
}
}
private static object CaptureSceneCamera(JObject @params)
{
string customPath = @params["path"]?.ToString();
string customFilename = @params["filename"]?.ToString();
int? width = @params["width"]?.ToObject<int?>();
int? height = @params["height"]?.ToObject<int?>();
try
{
// Get the active scene view camera
SceneView sceneView = SceneView.lastActiveSceneView;
if (sceneView == null)
{
return Response.Error("No active Scene view found. Please ensure a Scene view is open.");
}
Camera sceneCamera = sceneView.camera;
if (sceneCamera == null)
{
return Response.Error("Scene view camera not found.");
}
string savedPath = CaptureAndSave(customPath, customFilename, width, height, sceneCamera, "SceneCamera");
if (savedPath != null)
{
return Response.Success(
"Screenshot captured successfully from Scene camera.",
new { path = savedPath, camera = "Scene Camera" }
);
}
else
{
return Response.Error("Failed to capture screenshot from Scene camera.");
}
}
catch (Exception e)
{
return Response.Error($"Failed to capture screenshot from Scene camera: {e.Message}");
}
}
/// <summary>
/// Function to capture Game view screenshot and save to file
/// </summary>
public static string CaptureGameViewAndSave(string customPath = null, string customFilename = null, int? width = null, int? height = null)
{
try
{
// Generate filename with GameView prefix
string filename;
if (!string.IsNullOrEmpty(customFilename))
{
filename = customFilename.EndsWith(".png") ? customFilename : customFilename + ".png";
}
else
{
filename = $"GameView-{System.DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.png";
}
// Determine save path
string savePath;
if (!string.IsNullOrEmpty(customPath))
{
// Ensure directory exists
Directory.CreateDirectory(customPath);
savePath = Path.Combine(customPath, filename);
}
else
{
// Use project path with Screenshots folder
string projectPath = Path.GetDirectoryName(Application.dataPath); // Get project root (parent of Assets)
string screenshotsFolder = Path.Combine(projectPath, "Screenshots");
// Create Screenshots folder if it doesn't exist
if (!Directory.Exists(screenshotsFolder))
{
Directory.CreateDirectory(screenshotsFolder);
Debug.Log($"Created Screenshots folder at: {screenshotsFolder}");
}
savePath = Path.Combine(screenshotsFolder, filename);
}
// Try different capture methods based on mode and availability
Texture2D screenshot = null;
// Always prioritize GameView reflection to capture what user actually sees
// This ensures screenshots match the GameView dimensions, multi-camera setups, and post-processing
screenshot = CaptureGameViewInEditMode(width, height);
if (screenshot == null)
{
// Fallback to main camera rendering if GameView is not accessible
Camera mainCamera = Camera.main;
if (mainCamera != null)
{
screenshot = CaptureScreenByRenderTexture(mainCamera, width, height);
}
}
if (screenshot == null)
{
Debug.LogError("Failed to capture screenshot using all available methods!");
return null;
}
// ReadPixels from RenderTexture often returns bottom-up (OpenGL origin); flip so saved PNG is top-down
FlipTextureVertically(screenshot);
// Encode to PNG and save
byte[] bytes = screenshot.EncodeToPNG();
UnityEngine.Object.DestroyImmediate(screenshot); // Free GPU memory (must use DestroyImmediate in edit mode)
File.WriteAllBytes(savePath, bytes);
Debug.Log("Game view screenshot saved to: " + savePath);
return savePath;
}
catch (System.Exception e)
{
Debug.LogError($"Failed to capture game view screenshot: {e.Message}");
return null;
}
}
/// <summary>
/// Capture screenshot from camera using RenderTexture approach
/// </summary>
private static Texture2D CaptureScreenByRenderTexture(Camera camera, int? width = null, int? height = null)
{
if (camera == null)
{
Debug.LogError("Camera is null!");
return null;
}
try
{
// Use custom dimensions or default to reasonable size
int captureWidth = width ?? 1920;
int captureHeight = height ?? 1080;
Rect rect = new Rect(0, 0, captureWidth, captureHeight);
// Create a RenderTexture object
RenderTexture rt = new RenderTexture((int)rect.width, (int)rect.height, 24);
// Store original target texture to restore later
RenderTexture originalTarget = camera.targetTexture;
// Temporarily set the camera's targetTexture to rt, and manually render the camera
camera.targetTexture = rt;
camera.Render();
// Activate this rt, and read pixels from it.
RenderTexture previousActive = RenderTexture.active;
RenderTexture.active = rt;
Texture2D screenShot = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGB24, false);
// Note: At this time, it reads pixels from RenderTexture.active
screenShot.ReadPixels(rect, 0, 0);
screenShot.Apply();
// Reset related parameters so the camera continues to display on screen
camera.targetTexture = originalTarget;
RenderTexture.active = previousActive;
UnityEngine.Object.DestroyImmediate(rt);
return screenShot;
}
catch (System.Exception e)
{
Debug.LogError($"Failed to capture screenshot using RenderTexture: {e.Message}");
return null;
}
}
/// <summary>
/// Flips the texture vertically in-place. Use before saving when the image is upside-down.
/// ReadPixels from RenderTexture uses bottom-left origin (e.g. OpenGL); PNG expects top-left,
/// so screenshots from GameView/camera RT can appear flipped. Call this before EncodeToPNG to correct.
/// </summary>
private static void FlipTextureVertically(Texture2D tex)
{
if (tex == null || tex.height <= 1) return;
int w = tex.width;
int h = tex.height;
Color[] pixels = tex.GetPixels();
for (int y = 0; y < h / 2; y++)
{
int top = y * w;
int bottom = (h - 1 - y) * w;
for (int x = 0; x < w; x++)
{
Color tmp = pixels[top + x];
pixels[top + x] = pixels[bottom + x];
pixels[bottom + x] = tmp;
}
}
tex.SetPixels(pixels);
tex.Apply();
}
/// <summary>
/// Capture Game view using GameView's internal render texture via reflection
/// Works in both Edit and Play modes to capture what the user actually sees.
/// Note: ReadPixels from RT may yield bottom-up image (OpenGL); save path flips vertically before PNG.
/// </summary>
private static Texture2D CaptureGameViewInEditMode(int? width = null, int? height = null)
{
try
{
// Get active GameView
var gameView = GetActiveGameView();
if (gameView == null)
{
Debug.LogWarning("Game View not found. Falling back to camera rendering.");
return CaptureWithCameraRenderingFallback(width, height);
}
// Try to get GameView's internal render texture via reflection
// Different Unity versions may use different property/field names
RenderTexture gameViewRT = null;
var gameViewType = gameView.GetType();
// Try property "targetTexture" (some Unity versions)
var textureProperty = gameViewType.GetProperty("targetTexture",
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
if (textureProperty != null)
{
gameViewRT = textureProperty.GetValue(gameView, null) as RenderTexture;
}
// Try field "m_TargetTexture" (some Unity versions)
if (gameViewRT == null)
{
var textureField = gameViewType.GetField("m_TargetTexture",
BindingFlags.NonPublic | BindingFlags.Instance);
if (textureField != null)
{
gameViewRT = textureField.GetValue(gameView) as RenderTexture;
}
}
// Try method "GetMainPlayModeView" or similar approaches
if (gameViewRT == null)
{
var renderDocField = gameViewType.GetField("m_RenderTexture",
BindingFlags.NonPublic | BindingFlags.Instance);
if (renderDocField != null)
{
gameViewRT = renderDocField.GetValue(gameView) as RenderTexture;
}
}
if (gameViewRT == null)
{
Debug.LogWarning("GameView render texture not accessible. Falling back to camera rendering.");
return CaptureWithCameraRenderingFallback(width, height);
}
RenderTexture activeRT = RenderTexture.active;
// Determine final dimensions
int finalWidth = width ?? gameViewRT.width;
int finalHeight = height ?? gameViewRT.height;
// Create temporary RenderTexture to read pixels
RenderTexture tempRT;
// If custom dimensions are specified and different from game view, resize
if ((width.HasValue || height.HasValue) && (finalWidth != gameViewRT.width || finalHeight != gameViewRT.height))
{
tempRT = RenderTexture.GetTemporary(finalWidth, finalHeight, 0, gameViewRT.format);
// Scale the game view texture to the desired size
Graphics.Blit(gameViewRT, tempRT);
}
else
{
// Use game view dimensions
tempRT = RenderTexture.GetTemporary(gameViewRT.width, gameViewRT.height, 0, gameViewRT.format);
Graphics.Blit(gameViewRT, tempRT);
}
RenderTexture.active = tempRT;
Texture2D screenshot = new Texture2D(tempRT.width, tempRT.height, TextureFormat.RGB24, false);
screenshot.ReadPixels(new Rect(0, 0, tempRT.width, tempRT.height), 0, 0);
screenshot.Apply();
RenderTexture.active = activeRT;
RenderTexture.ReleaseTemporary(tempRT);
return screenshot;
}
catch (System.Exception e)
{
Debug.LogError($"Failed to capture game view using reflection: {e.Message}. Falling back to camera rendering.");
return CaptureWithCameraRenderingFallback(width, height);
}
}
/// <summary>
/// Get the active Game View window using reflection
/// </summary>
private static EditorWindow GetActiveGameView()
{
try
{
System.Type gameViewType = System.Type.GetType("UnityEditor.GameView,UnityEditor");
if (gameViewType == null)
return null;
// Try to get the focused game view first
EditorWindow focusedWindow = EditorWindow.focusedWindow;
if (focusedWindow != null && focusedWindow.GetType() == gameViewType)
return focusedWindow;
// If no focused game view, get any game view that exists
EditorWindow[] gameViews = Resources.FindObjectsOfTypeAll(gameViewType) as EditorWindow[];
if (gameViews != null && gameViews.Length > 0)
return gameViews[0];
return null;
}
catch (System.Exception e)
{
Debug.LogWarning($"Could not get Game View: {e.Message}");
return null;
}
}
/// <summary>
/// Fallback method using camera rendering when GameView access fails
/// </summary>
private static Texture2D CaptureWithCameraRenderingFallback(int? width = null, int? height = null)
{
try
{
int captureWidth = width ?? Screen.width;
int captureHeight = height ?? Screen.height;
// Create render texture
RenderTexture rt = new RenderTexture(captureWidth, captureHeight, 24);
// Get all cameras and render them to the render texture
Camera[] cameras = Camera.allCameras;
RenderTexture previousActive = RenderTexture.active;
RenderTexture.active = rt;
// Clear the render texture
GL.Clear(true, true, Color.black);
// Render all active cameras to the render texture in order
foreach (Camera cam in cameras)
{
if (cam != null && cam.enabled && cam.gameObject.activeInHierarchy)
{
RenderTexture prevTarget = cam.targetTexture;
cam.targetTexture = rt;
cam.Render();
cam.targetTexture = prevTarget;
}
}
// Read pixels from render texture
Texture2D screenshot = new Texture2D(captureWidth, captureHeight, TextureFormat.RGB24, false);
screenshot.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0);
screenshot.Apply();
// Cleanup
RenderTexture.active = previousActive;
UnityEngine.Object.DestroyImmediate(rt);
return screenshot;
}
catch (System.Exception e)
{
Debug.LogError($"Camera rendering fallback failed: {e.Message}");
return null;
}
}
/// <summary>
/// Helper method to resize a texture
/// </summary>
private static Texture2D ResizeTexture(Texture2D source, int targetWidth, int targetHeight)
{
RenderTexture rt = RenderTexture.GetTemporary(targetWidth, targetHeight);
RenderTexture.active = rt;
Graphics.Blit(source, rt);
Texture2D result = new Texture2D(targetWidth, targetHeight);
result.ReadPixels(new Rect(0, 0, targetWidth, targetHeight), 0, 0);
result.Apply();
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(rt);
return result;
}
/// <summary>
/// Function to capture screenshot and save to file
/// </summary>
public static string CaptureAndSave(string customPath = null, string customFilename = null, int? width = null, int? height = null, Camera specificCamera = null, string cameraPrefix = null)
{
// Get the camera to use
Camera cam = specificCamera ?? Camera.main;
if (cam == null)
{
Debug.LogError("No main camera found!");
return null;
}
// Use the new RenderTexture approach
Texture2D screenshot = CaptureScreenByRenderTexture(cam, width, height);
if (screenshot == null)
{
Debug.LogError("Failed to capture screenshot using RenderTexture!");
return null;
}
// Encode to PNG
byte[] bytes = screenshot.EncodeToPNG();
UnityEngine.Object.DestroyImmediate(screenshot);
// Generate filename with camera prefix
string filename;
if (!string.IsNullOrEmpty(customFilename))
{
filename = customFilename.EndsWith(".png") ? customFilename : customFilename + ".png";
}
else
{
string prefix = cameraPrefix ?? (specificCamera != null ? specificCamera.name : "MainCamera");
filename = $"{prefix}-{System.DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.png";
}
// Determine save path
string savePath;
if (!string.IsNullOrEmpty(customPath))
{
// Ensure directory exists
Directory.CreateDirectory(customPath);
savePath = Path.Combine(customPath, filename);
}
else
{
// Use project path with Screenshots folder
string projectPath = Path.GetDirectoryName(Application.dataPath); // Get project root (parent of Assets)
string screenshotsFolder = Path.Combine(projectPath, "Screenshots");
// Create Screenshots folder if it doesn't exist
if (!Directory.Exists(screenshotsFolder))
{
Directory.CreateDirectory(screenshotsFolder);
Debug.Log($"Created Screenshots folder at: {screenshotsFolder}");
}
savePath = Path.Combine(screenshotsFolder, filename);
}
// Save to file
File.WriteAllBytes(savePath, bytes);
Debug.Log("Screenshot saved to: " + savePath);
return savePath;
}
}
}