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}"); } } }