375 lines
10 KiB
C#
375 lines
10 KiB
C#
#if UNITY_EDITOR
|
||
using System;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
|
||
public class CameraScreenshotTool : EditorWindow
|
||
{
|
||
private enum CaptureMode
|
||
{
|
||
CameraOnly,
|
||
GameView
|
||
}
|
||
|
||
private Camera[] sceneCameras = Array.Empty<Camera>();
|
||
private string[] cameraNames = Array.Empty<string>();
|
||
private int selectedCameraIndex = -1;
|
||
|
||
private Camera targetCamera;
|
||
|
||
private int width = 1920;
|
||
private int height = 1080;
|
||
|
||
private string fileName = "screenshot";
|
||
private string folderName = "Screenshots";
|
||
|
||
private bool addTimestamp = true;
|
||
private bool openFolder = true;
|
||
private bool includeUI = false;
|
||
|
||
private CaptureMode captureMode = CaptureMode.CameraOnly;
|
||
private Vector2 scroll;
|
||
|
||
[MenuItem("Tools/QA/Camera Screenshot Tool")]
|
||
public static void Init()
|
||
{
|
||
var window = GetWindow<CameraScreenshotTool>("Screenshot Tool");
|
||
window.minSize = new Vector2(380, 360);
|
||
window.RefreshCameraList();
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
RefreshCameraList();
|
||
EditorApplication.hierarchyChanged += RefreshCameraList;
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
EditorApplication.hierarchyChanged -= RefreshCameraList;
|
||
}
|
||
|
||
private void OnFocus()
|
||
{
|
||
RefreshCameraList();
|
||
}
|
||
|
||
private void OnGUI()
|
||
{
|
||
scroll = EditorGUILayout.BeginScrollView(scroll);
|
||
|
||
GUILayout.Space(6);
|
||
|
||
DrawCameraSection();
|
||
GUILayout.Space(10);
|
||
|
||
DrawCaptureSection();
|
||
GUILayout.Space(10);
|
||
|
||
DrawResolutionSection();
|
||
GUILayout.Space(10);
|
||
|
||
DrawOutputSection();
|
||
GUILayout.Space(12);
|
||
|
||
DrawCaptureButton();
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
private void DrawCameraSection()
|
||
{
|
||
EditorGUILayout.LabelField("Camera", EditorStyles.boldLabel);
|
||
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
EditorGUI.BeginChangeCheck();
|
||
selectedCameraIndex = EditorGUILayout.Popup("Scene Camera", selectedCameraIndex, cameraNames);
|
||
|
||
if (EditorGUI.EndChangeCheck())
|
||
{
|
||
targetCamera = GetCameraByIndex(selectedCameraIndex);
|
||
}
|
||
|
||
if (GUILayout.Button("Refresh", GUILayout.Width(70)))
|
||
{
|
||
RefreshCameraList();
|
||
}
|
||
}
|
||
|
||
EditorGUI.BeginChangeCheck();
|
||
targetCamera = (Camera)EditorGUILayout.ObjectField("Target Camera", targetCamera, typeof(Camera), true);
|
||
if (EditorGUI.EndChangeCheck())
|
||
{
|
||
SyncDropdownWithTargetCamera();
|
||
}
|
||
|
||
if (targetCamera == null)
|
||
{
|
||
EditorGUILayout.HelpBox("Выбери камеру со сцены.", MessageType.Info);
|
||
}
|
||
else
|
||
{
|
||
EditorGUILayout.HelpBox(
|
||
$"Selected: {targetCamera.name}\n" +
|
||
$"Enabled: {targetCamera.enabled}\n" +
|
||
$"Type: {targetCamera.cameraType}",
|
||
MessageType.None
|
||
);
|
||
}
|
||
}
|
||
|
||
private void DrawCaptureSection()
|
||
{
|
||
EditorGUILayout.LabelField("Capture", EditorStyles.boldLabel);
|
||
|
||
captureMode = (CaptureMode)EditorGUILayout.EnumPopup("Mode", captureMode);
|
||
|
||
includeUI = EditorGUILayout.Toggle("Include UI", includeUI);
|
||
|
||
if (includeUI)
|
||
{
|
||
EditorGUILayout.HelpBox(
|
||
"Overlay UI обычно не попадает в CameraOnly. Для UI-захвата лучше использовать GameView mode.",
|
||
MessageType.Warning
|
||
);
|
||
}
|
||
|
||
if (captureMode == CaptureMode.CameraOnly && includeUI)
|
||
{
|
||
EditorGUILayout.HelpBox(
|
||
"В режиме CameraOnly попадут World Space и часть Screen Space - Camera UI. " +
|
||
"Screen Space - Overlay UI обычно не попадет.",
|
||
MessageType.Info
|
||
);
|
||
}
|
||
|
||
if (captureMode == CaptureMode.GameView)
|
||
{
|
||
EditorGUILayout.HelpBox(
|
||
"GameView mode захватывает итоговую картинку, включая overlay UI, " +
|
||
"но итоговое разрешение зависит от размера GameView.",
|
||
MessageType.Info
|
||
);
|
||
}
|
||
}
|
||
|
||
private void DrawResolutionSection()
|
||
{
|
||
EditorGUILayout.LabelField("Resolution", EditorStyles.boldLabel);
|
||
|
||
width = Mathf.Max(1, EditorGUILayout.IntField("Width", width));
|
||
height = Mathf.Max(1, EditorGUILayout.IntField("Height", height));
|
||
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
if (GUILayout.Button("HD"))
|
||
{
|
||
width = 1280;
|
||
height = 720;
|
||
}
|
||
|
||
if (GUILayout.Button("Full HD"))
|
||
{
|
||
width = 1920;
|
||
height = 1080;
|
||
}
|
||
|
||
if (GUILayout.Button("2K"))
|
||
{
|
||
width = 2560;
|
||
height = 1440;
|
||
}
|
||
|
||
if (GUILayout.Button("4K"))
|
||
{
|
||
width = 3840;
|
||
height = 2160;
|
||
}
|
||
}
|
||
}
|
||
|
||
private void DrawOutputSection()
|
||
{
|
||
EditorGUILayout.LabelField("Output", EditorStyles.boldLabel);
|
||
|
||
fileName = EditorGUILayout.TextField("File Name", fileName);
|
||
folderName = EditorGUILayout.TextField("Folder", folderName);
|
||
addTimestamp = EditorGUILayout.Toggle("Add Timestamp", addTimestamp);
|
||
openFolder = EditorGUILayout.Toggle("Open Folder", openFolder);
|
||
}
|
||
|
||
private void DrawCaptureButton()
|
||
{
|
||
bool canCapture =
|
||
captureMode == CaptureMode.GameView ||
|
||
targetCamera != null;
|
||
|
||
GUI.enabled = canCapture;
|
||
|
||
if (GUILayout.Button("Capture Screenshot", GUILayout.Height(36)))
|
||
{
|
||
Capture();
|
||
}
|
||
|
||
GUI.enabled = true;
|
||
}
|
||
|
||
private void Capture()
|
||
{
|
||
string path = BuildOutputPath();
|
||
|
||
try
|
||
{
|
||
if (captureMode == CaptureMode.GameView)
|
||
{
|
||
CaptureGameView(path);
|
||
}
|
||
else
|
||
{
|
||
CaptureFromCamera(path, targetCamera, includeUI);
|
||
}
|
||
|
||
Debug.Log($"Screenshot saved: {path}");
|
||
AssetDatabase.Refresh();
|
||
|
||
if (openFolder)
|
||
{
|
||
EditorUtility.RevealInFinder(path);
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Debug.LogError($"Screenshot capture failed: {e}");
|
||
}
|
||
}
|
||
|
||
private void CaptureFromCamera(string path, Camera cam, bool withUI)
|
||
{
|
||
if (cam == null)
|
||
throw new InvalidOperationException("Target camera is null.");
|
||
|
||
int w = Mathf.Max(1, width);
|
||
int h = Mathf.Max(1, height);
|
||
|
||
RenderTexture rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32);
|
||
Texture2D tex = new Texture2D(w, h, TextureFormat.RGBA32, false);
|
||
|
||
RenderTexture previousActive = RenderTexture.active;
|
||
RenderTexture previousTarget = cam.targetTexture;
|
||
|
||
try
|
||
{
|
||
// Для CameraOnly UI overlay не поддерживается полноценно.
|
||
// Но world-space и screen-space camera UI могут попасть,
|
||
// если canvas привязан к этой камере.
|
||
cam.targetTexture = rt;
|
||
RenderTexture.active = rt;
|
||
|
||
cam.Render();
|
||
|
||
tex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
|
||
tex.Apply();
|
||
|
||
byte[] bytes = tex.EncodeToPNG();
|
||
File.WriteAllBytes(path, bytes);
|
||
}
|
||
finally
|
||
{
|
||
cam.targetTexture = previousTarget;
|
||
RenderTexture.active = previousActive;
|
||
|
||
DestroyImmediate(rt);
|
||
DestroyImmediate(tex);
|
||
}
|
||
}
|
||
|
||
private void CaptureGameView(string path)
|
||
{
|
||
// В этом режиме includeUI фактически всегда true по смыслу,
|
||
// потому что захватывается итоговый GameView.
|
||
// Разрешение тут зависит от фактического размера GameView.
|
||
ScreenCapture.CaptureScreenshot(path);
|
||
}
|
||
|
||
private string BuildOutputPath()
|
||
{
|
||
string projectPath = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
||
string targetFolder = Path.Combine(projectPath, folderName);
|
||
|
||
if (!Directory.Exists(targetFolder))
|
||
Directory.CreateDirectory(targetFolder);
|
||
|
||
string safeName = string.IsNullOrWhiteSpace(fileName) ? "screenshot" : fileName;
|
||
foreach (char c in Path.GetInvalidFileNameChars())
|
||
safeName = safeName.Replace(c, '_');
|
||
|
||
if (addTimestamp)
|
||
{
|
||
safeName += "_" + DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss");
|
||
}
|
||
|
||
return Path.Combine(targetFolder, safeName + ".png");
|
||
}
|
||
|
||
private void RefreshCameraList()
|
||
{
|
||
sceneCameras = FindObjectsByType<Camera>(FindObjectsSortMode.None)
|
||
.Where(c => c.gameObject.scene.IsValid())
|
||
.OrderBy(c => c.name)
|
||
.ToArray();
|
||
|
||
if (sceneCameras.Length == 0)
|
||
{
|
||
cameraNames = new[] { "No cameras found" };
|
||
selectedCameraIndex = -1;
|
||
targetCamera = null;
|
||
Repaint();
|
||
return;
|
||
}
|
||
|
||
cameraNames = sceneCameras
|
||
.Select(c =>
|
||
{
|
||
string tag = c.CompareTag("MainCamera") ? " [Main]" : "";
|
||
string state = c.enabled ? "" : " [Disabled]";
|
||
return $"{c.name}{tag}{state}";
|
||
})
|
||
.ToArray();
|
||
|
||
if (targetCamera == null)
|
||
{
|
||
targetCamera = sceneCameras.FirstOrDefault(c => c.CompareTag("MainCamera")) ?? sceneCameras[0];
|
||
}
|
||
|
||
SyncDropdownWithTargetCamera();
|
||
Repaint();
|
||
}
|
||
|
||
private void SyncDropdownWithTargetCamera()
|
||
{
|
||
if (targetCamera == null || sceneCameras == null || sceneCameras.Length == 0)
|
||
{
|
||
selectedCameraIndex = -1;
|
||
return;
|
||
}
|
||
|
||
selectedCameraIndex = Array.IndexOf(sceneCameras, targetCamera);
|
||
if (selectedCameraIndex < 0 && sceneCameras.Length > 0)
|
||
{
|
||
selectedCameraIndex = 0;
|
||
targetCamera = sceneCameras[0];
|
||
}
|
||
}
|
||
|
||
private Camera GetCameraByIndex(int index)
|
||
{
|
||
if (sceneCameras == null || index < 0 || index >= sceneCameras.Length)
|
||
return null;
|
||
|
||
return sceneCameras[index];
|
||
}
|
||
}
|
||
#endif |