using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
///
/// 에디터 스크린샷 유틸리티 — Game/Scene 뷰 캡처, 슈퍼 해상도, 알파 지원, 히스토리 관리
///
public class ScreenshotToolWindow : EditorWindow
{
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private const string PrefsPrefix = "ScreenshotTool_";
// ── Settings ──
private string saveFolderPath;
private string filePrefix = "Screenshot";
private int superSize = 1;
private bool captureAlpha;
private bool autoOpenFolder;
private bool includeTimestamp = true;
private int selectedSourceIndex; // 0=Game, 1=SceneView
// ── UI Elements ──
private TextField saveFolderField;
private TextField prefixField;
private SliderInt superSizeSlider;
private Label superSizeValueLabel;
private Toggle alphaToggle;
private Toggle autoOpenToggle;
private Toggle timestampToggle;
private Label resolutionInfoLabel;
private VisualElement historyContainer;
private Label historyEmptyLabel;
private ScrollView historyScroll;
// ── History ──
private readonly List history = new List();
private const int MaxHistory = 30;
[Serializable]
private class ScreenshotEntry
{
public string path;
public string timestamp;
public int width;
public int height;
public long fileSizeBytes;
}
[MenuItem("Tools/Utilities/스크린샷 도구 &F12")]
public static void ShowWindow()
{
var window = GetWindow("스크린샷 도구");
window.minSize = new Vector2(380, 520);
}
private void OnEnable()
{
LoadPrefs();
}
private void OnDisable()
{
SavePrefs();
}
public void CreateGUI()
{
var root = rootVisualElement;
root.AddToClassList("tool-root");
var commonUss = AssetDatabase.LoadAssetAtPath(CommonUssPath);
if (commonUss != null) root.styleSheets.Add(commonUss);
// ── Title ──
var title = new Label("스크린샷 도구") { name = "title" };
title.AddToClassList("tool-title");
root.Add(title);
// ══════════════════════════════════════════
// Settings Section
// ══════════════════════════════════════════
var settingsSection = CreateSection("캡처 설정");
root.Add(settingsSection);
// Source
var sourceRow = CreateRow();
sourceRow.Add(new Label("캡처 소스") { style = { minWidth = 100 } });
var sourceDropdown = new DropdownField(new List { "Game View", "Scene View" }, selectedSourceIndex);
sourceDropdown.RegisterValueChangedCallback(evt =>
{
selectedSourceIndex = sourceDropdown.index;
UpdateResolutionInfo();
});
sourceDropdown.style.flexGrow = 1;
sourceRow.Add(sourceDropdown);
settingsSection.Add(sourceRow);
// Super Size
var superSizeRow = CreateRow();
superSizeRow.Add(new Label("배율 (Super Size)") { style = { minWidth = 100 } });
superSizeSlider = new SliderInt(1, 8) { value = superSize };
superSizeSlider.style.flexGrow = 1;
superSizeValueLabel = new Label($"x{superSize}") { style = { minWidth = 30, unityTextAlign = TextAnchor.MiddleRight } };
superSizeSlider.RegisterValueChangedCallback(evt =>
{
superSize = evt.newValue;
superSizeValueLabel.text = $"x{superSize}";
UpdateResolutionInfo();
});
superSizeRow.Add(superSizeSlider);
superSizeRow.Add(superSizeValueLabel);
settingsSection.Add(superSizeRow);
// Resolution Info
resolutionInfoLabel = new Label("") { style = { color = new Color(0.65f, 0.7f, 0.98f), fontSize = 11, marginLeft = 4, marginTop = 2 } };
settingsSection.Add(resolutionInfoLabel);
UpdateResolutionInfo();
// Alpha
alphaToggle = new Toggle("알파(투명) 배경 캡처") { value = captureAlpha };
alphaToggle.RegisterValueChangedCallback(evt => captureAlpha = evt.newValue);
alphaToggle.style.marginTop = 4;
settingsSection.Add(alphaToggle);
// ══════════════════════════════════════════
// Save Settings Section
// ══════════════════════════════════════════
var saveSection = CreateSection("저장 설정");
root.Add(saveSection);
// Save folder
var folderRow = CreateRow();
folderRow.Add(new Label("저장 경로") { style = { minWidth = 100 } });
saveFolderField = new TextField { value = saveFolderPath };
saveFolderField.style.flexGrow = 1;
saveFolderField.RegisterValueChangedCallback(evt => saveFolderPath = evt.newValue);
var browseBtn = new Button(() =>
{
var selected = EditorUtility.OpenFolderPanel("스크린샷 저장 폴더", saveFolderPath, "");
if (!string.IsNullOrEmpty(selected))
{
saveFolderPath = selected;
saveFolderField.value = selected;
}
}) { text = "..." };
browseBtn.style.width = 30;
folderRow.Add(saveFolderField);
folderRow.Add(browseBtn);
saveSection.Add(folderRow);
// Prefix
var prefixRow = CreateRow();
prefixRow.Add(new Label("파일 접두사") { style = { minWidth = 100 } });
prefixField = new TextField { value = filePrefix };
prefixField.style.flexGrow = 1;
prefixField.RegisterValueChangedCallback(evt => filePrefix = evt.newValue);
prefixRow.Add(prefixField);
saveSection.Add(prefixRow);
// Timestamp toggle
timestampToggle = new Toggle("파일명에 타임스탬프 포함") { value = includeTimestamp };
timestampToggle.RegisterValueChangedCallback(evt => includeTimestamp = evt.newValue);
saveSection.Add(timestampToggle);
// Auto-open
autoOpenToggle = new Toggle("캡처 후 폴더 열기") { value = autoOpenFolder };
autoOpenToggle.RegisterValueChangedCallback(evt => autoOpenFolder = evt.newValue);
saveSection.Add(autoOpenToggle);
// ══════════════════════════════════════════
// Capture Buttons
// ══════════════════════════════════════════
var btnRow = CreateRow();
btnRow.style.marginTop = 8;
btnRow.style.marginBottom = 4;
var captureBtn = new Button(CaptureScreenshot) { text = "캡처 (F12)" };
captureBtn.AddToClassList("btn-primary");
captureBtn.style.flexGrow = 2;
captureBtn.style.height = 36;
captureBtn.style.fontSize = 14;
btnRow.Add(captureBtn);
var quickBurstBtn = new Button(CaptureBurst) { text = "연속 캡처 (3장)" };
quickBurstBtn.style.flexGrow = 1;
quickBurstBtn.style.height = 36;
quickBurstBtn.style.marginLeft = 4;
SetBorderRadius(quickBurstBtn, 4);
quickBurstBtn.style.backgroundColor = new Color(0.15f, 0.15f, 0.15f);
btnRow.Add(quickBurstBtn);
root.Add(btnRow);
// ══════════════════════════════════════════
// History Section
// ══════════════════════════════════════════
var historySection = CreateSection("캡처 기록");
historySection.style.flexGrow = 1;
root.Add(historySection);
var historyHeader = CreateRow();
historyHeader.style.justifyContent = Justify.SpaceBetween;
historyHeader.style.marginBottom = 4;
historyHeader.Add(new Label("최근 캡처") { style = { unityFontStyleAndWeight = FontStyle.Bold, fontSize = 12 } });
var clearHistoryBtn = new Button(() => { history.Clear(); RefreshHistory(); }) { text = "기록 삭제" };
clearHistoryBtn.AddToClassList("btn-danger");
historyHeader.Add(clearHistoryBtn);
historySection.Add(historyHeader);
historyScroll = new ScrollView(ScrollViewMode.Vertical);
historyScroll.style.flexGrow = 1;
historySection.Add(historyScroll);
historyEmptyLabel = new Label("캡처 기록이 없습니다.");
historyEmptyLabel.AddToClassList("list-empty");
historyScroll.Add(historyEmptyLabel);
historyContainer = new VisualElement();
historyScroll.Add(historyContainer);
RefreshHistory();
}
// ══════════════════════════════════════════════════════
// Capture Logic
// ══════════════════════════════════════════════════════
private void CaptureScreenshot()
{
EnsureSaveFolder();
string filePath = BuildFilePath();
if (selectedSourceIndex == 0)
CaptureGameView(filePath);
else
CaptureSceneView(filePath);
}
private void CaptureBurst()
{
EnsureSaveFolder();
for (int i = 0; i < 3; i++)
{
string filePath = BuildFilePath($"_burst{i + 1}");
if (selectedSourceIndex == 0)
CaptureGameView(filePath);
else
CaptureSceneView(filePath);
}
}
private void CaptureGameView(string filePath)
{
try
{
if (captureAlpha)
{
CaptureGameViewWithAlpha(filePath);
}
else
{
ScreenCapture.CaptureScreenshot(filePath, superSize);
// ScreenCapture is async, poll for file
EditorApplication.delayCall += () => WaitForFile(filePath, 0);
}
}
catch (Exception e)
{
Debug.LogError($"[스크린샷 도구] Game View 캡처 실패: {e.Message}");
}
}
private void CaptureGameViewWithAlpha(string filePath)
{
var cam = Camera.main;
if (cam == null)
{
Debug.LogError("[스크린샷 도구] Main Camera를 찾을 수 없습니다. Camera.main이 설정되어 있는지 확인하세요.");
return;
}
int w = cam.pixelWidth * superSize;
int h = cam.pixelHeight * superSize;
var rt = RenderTexture.GetTemporary(w, h, 24, RenderTextureFormat.ARGB32);
var prevRT = cam.targetTexture;
var prevClearFlags = cam.clearFlags;
var prevBgColor = cam.backgroundColor;
cam.targetTexture = rt;
cam.clearFlags = CameraClearFlags.SolidColor;
cam.backgroundColor = new Color(0, 0, 0, 0);
cam.Render();
var tex = new Texture2D(w, h, TextureFormat.ARGB32, false);
RenderTexture.active = rt;
tex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
tex.Apply();
RenderTexture.active = null;
cam.targetTexture = prevRT;
cam.clearFlags = prevClearFlags;
cam.backgroundColor = prevBgColor;
RenderTexture.ReleaseTemporary(rt);
byte[] bytes = tex.EncodeToPNG();
DestroyImmediate(tex);
File.WriteAllBytes(filePath, bytes);
OnCaptureComplete(filePath);
}
private void CaptureSceneView(string filePath)
{
var sceneView = SceneView.lastActiveSceneView;
if (sceneView == null)
{
Debug.LogError("[스크린샷 도구] 활성화된 Scene View를 찾을 수 없습니다.");
return;
}
try
{
var cam = sceneView.camera;
int w = (int)sceneView.position.width * superSize;
int h = (int)sceneView.position.height * superSize;
var format = captureAlpha ? RenderTextureFormat.ARGB32 : RenderTextureFormat.Default;
var rt = RenderTexture.GetTemporary(w, h, 24, format);
cam.targetTexture = rt;
if (captureAlpha)
{
cam.clearFlags = CameraClearFlags.SolidColor;
cam.backgroundColor = new Color(0, 0, 0, 0);
}
cam.Render();
var texFormat = captureAlpha ? TextureFormat.ARGB32 : TextureFormat.RGB24;
var tex = new Texture2D(w, h, texFormat, false);
RenderTexture.active = rt;
tex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
tex.Apply();
RenderTexture.active = null;
cam.targetTexture = null;
RenderTexture.ReleaseTemporary(rt);
byte[] bytes = tex.EncodeToPNG();
DestroyImmediate(tex);
File.WriteAllBytes(filePath, bytes);
OnCaptureComplete(filePath);
}
catch (Exception e)
{
Debug.LogError($"[스크린샷 도구] Scene View 캡처 실패: {e.Message}");
}
}
private void WaitForFile(string filePath, int attempt)
{
if (File.Exists(filePath))
{
OnCaptureComplete(filePath);
}
else if (attempt < 30) // ~3 seconds
{
int next = attempt + 1;
EditorApplication.delayCall += () => WaitForFile(filePath, next);
}
else
{
Debug.LogWarning($"[스크린샷 도구] 파일 생성 대기 시간 초과: {filePath}");
}
}
private void OnCaptureComplete(string filePath)
{
var info = new FileInfo(filePath);
if (!info.Exists) return;
// Read dimensions from PNG header instead of loading full texture
int w = 0, h = 0;
try
{
using (var fs = File.OpenRead(filePath))
using (var reader = new BinaryReader(fs))
{
// Skip PNG signature (8 bytes) + IHDR chunk length (4 bytes) + chunk type (4 bytes)
reader.ReadBytes(16);
// Width and height are big-endian 4-byte integers
byte[] wb = reader.ReadBytes(4);
byte[] hb = reader.ReadBytes(4);
if (BitConverter.IsLittleEndian) { Array.Reverse(wb); Array.Reverse(hb); }
w = BitConverter.ToInt32(wb, 0);
h = BitConverter.ToInt32(hb, 0);
}
}
catch { /* fallback: dimensions unknown */ }
var entry = new ScreenshotEntry
{
path = filePath,
timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
width = w,
height = h,
fileSizeBytes = info.Length
};
history.Insert(0, entry);
if (history.Count > MaxHistory) history.RemoveRange(MaxHistory, history.Count - MaxHistory);
RefreshHistory();
string sizeStr = FormatFileSize(info.Length);
Debug.Log($"[스크린샷 도구] 저장 완료: {filePath} ({w}x{h}, {sizeStr})");
if (autoOpenFolder)
EditorUtility.RevealInFinder(filePath);
}
// ══════════════════════════════════════════════════════
// History UI
// ══════════════════════════════════════════════════════
private void RefreshHistory()
{
if (historyContainer == null) return;
historyContainer.Clear();
if (historyEmptyLabel != null)
historyEmptyLabel.style.display = history.Count == 0 ? DisplayStyle.Flex : DisplayStyle.None;
foreach (var entry in history)
{
var item = new VisualElement();
item.AddToClassList("list-item");
var header = CreateRow();
header.style.justifyContent = Justify.SpaceBetween;
header.style.alignItems = Align.Center;
string fileName = Path.GetFileName(entry.path);
var nameLabel = new Label(fileName) { style = { unityFontStyleAndWeight = FontStyle.Bold, fontSize = 11, flexShrink = 1 } };
nameLabel.style.overflow = Overflow.Hidden;
nameLabel.style.textOverflow = TextOverflow.Ellipsis;
header.Add(nameLabel);
var btnGroup = new VisualElement { style = { flexDirection = FlexDirection.Row, flexShrink = 0 } };
var openBtn = new Button(() => EditorUtility.RevealInFinder(entry.path)) { text = "폴더" };
openBtn.style.fontSize = 10;
openBtn.style.paddingLeft = 6;
openBtn.style.paddingRight = 6;
openBtn.style.height = 20;
SetBorderRadius(openBtn, 3);
openBtn.style.marginRight = 2;
btnGroup.Add(openBtn);
var copyPathBtn = new Button(() =>
{
EditorGUIUtility.systemCopyBuffer = entry.path;
Debug.Log($"[스크린샷 도구] 경로 복사됨: {entry.path}");
}) { text = "복사" };
copyPathBtn.style.fontSize = 10;
copyPathBtn.style.paddingLeft = 6;
copyPathBtn.style.paddingRight = 6;
copyPathBtn.style.height = 20;
SetBorderRadius(copyPathBtn, 3);
btnGroup.Add(copyPathBtn);
header.Add(btnGroup);
item.Add(header);
string detail = $"{entry.timestamp}";
if (entry.width > 0)
detail += $" | {entry.width}x{entry.height}";
detail += $" | {FormatFileSize(entry.fileSizeBytes)}";
var detailLabel = new Label(detail) { style = { fontSize = 10, color = new Color(0.58f, 0.64f, 0.72f), marginTop = 2 } };
item.Add(detailLabel);
historyContainer.Add(item);
}
}
// ══════════════════════════════════════════════════════
// Helpers
// ══════════════════════════════════════════════════════
private void EnsureSaveFolder()
{
if (string.IsNullOrWhiteSpace(saveFolderPath))
saveFolderPath = Path.Combine(Application.dataPath, "..", "Screenshots");
if (!Directory.Exists(saveFolderPath))
Directory.CreateDirectory(saveFolderPath);
}
private string BuildFilePath(string suffix = "")
{
string name = string.IsNullOrWhiteSpace(filePrefix) ? "Screenshot" : filePrefix;
if (includeTimestamp)
name += $"_{DateTime.Now:yyyyMMdd_HHmmss}";
name += suffix;
name += ".png";
return Path.Combine(saveFolderPath, name);
}
private void UpdateResolutionInfo()
{
if (resolutionInfoLabel == null) return;
int w, h;
if (selectedSourceIndex == 0)
{
// Game View
var gameViewSize = Handles.GetMainGameViewSize();
w = (int)gameViewSize.x;
h = (int)gameViewSize.y;
}
else
{
// Scene View
var sv = SceneView.lastActiveSceneView;
if (sv != null)
{
w = (int)sv.position.width;
h = (int)sv.position.height;
}
else
{
resolutionInfoLabel.text = "Scene View를 찾을 수 없음";
return;
}
}
int outW = w * superSize;
int outH = h * superSize;
resolutionInfoLabel.text = $"출력 해상도: {outW} x {outH} px";
}
private VisualElement CreateSection(string title)
{
var section = new VisualElement();
section.AddToClassList("section");
var foldout = new Foldout { text = title, value = true };
foldout.AddToClassList("section-foldout");
section.Add(foldout);
return section;
}
private VisualElement CreateRow()
{
return new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginTop = 3, marginBottom = 3 } };
}
private static void SetBorderRadius(VisualElement el, float radius)
{
el.style.borderTopLeftRadius = radius;
el.style.borderTopRightRadius = radius;
el.style.borderBottomLeftRadius = radius;
el.style.borderBottomRightRadius = radius;
}
private string FormatFileSize(long bytes)
{
if (bytes < 1024) return $"{bytes} B";
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
return $"{bytes / (1024.0 * 1024.0):F1} MB";
}
// ══════════════════════════════════════════════════════
// Prefs Persistence
// ══════════════════════════════════════════════════════
private void LoadPrefs()
{
saveFolderPath = EditorPrefs.GetString(PrefsPrefix + "SaveFolder", Path.Combine(Application.dataPath, "..", "Screenshots"));
filePrefix = EditorPrefs.GetString(PrefsPrefix + "Prefix", "Screenshot");
superSize = EditorPrefs.GetInt(PrefsPrefix + "SuperSize", 1);
captureAlpha = EditorPrefs.GetBool(PrefsPrefix + "Alpha", false);
autoOpenFolder = EditorPrefs.GetBool(PrefsPrefix + "AutoOpen", false);
includeTimestamp = EditorPrefs.GetBool(PrefsPrefix + "Timestamp", true);
selectedSourceIndex = EditorPrefs.GetInt(PrefsPrefix + "Source", 0);
}
private void SavePrefs()
{
EditorPrefs.SetString(PrefsPrefix + "SaveFolder", saveFolderPath);
EditorPrefs.SetString(PrefsPrefix + "Prefix", filePrefix);
EditorPrefs.SetInt(PrefsPrefix + "SuperSize", superSize);
EditorPrefs.SetBool(PrefsPrefix + "Alpha", captureAlpha);
EditorPrefs.SetBool(PrefsPrefix + "AutoOpen", autoOpenFolder);
EditorPrefs.SetBool(PrefsPrefix + "Timestamp", includeTimestamp);
EditorPrefs.SetInt(PrefsPrefix + "Source", selectedSourceIndex);
}
// ══════════════════════════════════════════════════════
// Global Hotkey (F12)
// ══════════════════════════════════════════════════════
[MenuItem("Tools/Utilities/Game View 빠른 캡처 _F12")]
private static void QuickCapture()
{
// 윈도우가 열려 있으면 그 설정을 사용, 없으면 기본값
var windows = Resources.FindObjectsOfTypeAll();
if (windows.Length > 0)
{
windows[0].CaptureScreenshot();
}
else
{
// 기본 설정으로 빠른 캡처
string folder = EditorPrefs.GetString(PrefsPrefix + "SaveFolder", Path.Combine(Application.dataPath, "..", "Screenshots"));
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
string prefix = EditorPrefs.GetString(PrefsPrefix + "Prefix", "Screenshot");
string name = $"{prefix}_{DateTime.Now:yyyyMMdd_HHmmss}.png";
string path = Path.Combine(folder, name);
int ss = EditorPrefs.GetInt(PrefsPrefix + "SuperSize", 1);
ScreenCapture.CaptureScreenshot(path, ss);
Debug.Log($"[스크린샷 도구] 빠른 캡처: {path}");
}
}
}