using UnityEngine; using System; using System.Collections; using System.IO; /// /// 스크린샷 캡처 관리. /// RenderStreamOutput 이 매 프레임 갱신하는 CaptureTexture/AlphaOutputTexture 를 PNG 로 저장. /// captureWidth/Height 가 지정되면 캡처 직전에 카메라 targetTexture 를 임시로 그 해상도로 /// 변경해 한 프레임 고해상도 렌더 후 원복한다. /// 트레이드오프: 그 한 프레임 동안 Spout/NDI 송신도 같은 해상도가 되어 수신측이 잠깐 깜빡임. /// [Serializable] public class ScreenshotManager { [Tooltip("RenderStreamOutput 컴포넌트 (비어있으면 씬에서 자동 검색)")] public RenderStreamOutput renderStream; [Tooltip("스크린샷 저장 경로 (비어있으면 프로젝트 루트의 Screenshots/)")] public string screenshotSavePath = ""; [Tooltip("파일명 앞에 붙을 접두사")] public string screenshotFilePrefix = "Screenshot"; [Header("고해상도 캡처 (0 이면 카메라 해상도 사용)")] [Tooltip("캡처 너비. 0 이면 카메라 해상도 그대로")] public int captureWidth = 3840; [Tooltip("캡처 높이. 0 이면 카메라 해상도 그대로")] public int captureHeight = 2160; [Tooltip("(하위 호환) 외부 코드가 .screenshotCamera 로 접근하면 RenderStreamOutput.MainCam 반환")] public Camera screenshotCamera => renderStream != null ? renderStream.MainCam : null; private MonoBehaviour host; private Action log; private Action logError; public void Initialize(MonoBehaviour host, Action log, Action logError) { this.host = host; this.log = log; this.logError = logError; if (renderStream == null) renderStream = UnityEngine.Object.FindFirstObjectByType(); if (renderStream == null) logError?.Invoke("RenderStreamOutput 컴포넌트를 찾을 수 없습니다 — 알파 합성 캡처 불가"); if (string.IsNullOrEmpty(screenshotSavePath)) screenshotSavePath = Path.Combine(Application.dataPath, "..", "Screenshots"); if (!Directory.Exists(screenshotSavePath)) { Directory.CreateDirectory(screenshotSavePath); log?.Invoke($"Screenshots 폴더 생성됨: {screenshotSavePath}"); } } public void CaptureScreenshot() { if (!CanCapture()) return; host.StartCoroutine(CaptureCoroutine(alphaOnly: false)); } public void CaptureAlphaScreenshot() { if (!CanCapture()) return; host.StartCoroutine(CaptureCoroutine(alphaOnly: true)); } private bool CanCapture() { if (host == null) { logError?.Invoke("ScreenshotManager host(MonoBehaviour) 가 없습니다"); return false; } if (renderStream == null || renderStream.MainCam == null) { logError?.Invoke("RenderStreamOutput / MainCam 이 준비되지 않았습니다"); return false; } return true; } private IEnumerator CaptureCoroutine(bool alphaOnly) { var cam = renderStream.MainCam; bool useTempResolution = captureWidth > 0 && captureHeight > 0; RenderTexture tempRT = null; RenderTexture prevTarget = null; if (useTempResolution) { prevTarget = cam.targetTexture; var desc = new RenderTextureDescriptor(captureWidth, captureHeight, renderStream.TextureFormat, 24) { msaaSamples = Mathf.Max(1, renderStream.AntiAliasing) }; tempRT = new RenderTexture(desc); tempRT.Create(); cam.targetTexture = tempRT; // 1프레임 — RenderStreamOutput.Update 가 새 pixelWidth 감지 → InitializeTextures(고해상도) yield return null; // 1프레임 — 고해상도 RT 에 우리 패스 결과 채워짐 yield return new WaitForEndOfFrame(); } var srcRT = alphaOnly ? renderStream.AlphaOutputTexture : renderStream.CaptureTexture; if (srcRT != null) { string fileName = GenerateFileName("png", alphaOnly ? "_Alpha" : ""); SaveRtAsPng(srcRT, fileName, alphaOnly ? TextureFormat.RGB24 : TextureFormat.RGBA32); } else { logError?.Invoke("캡처 RT 가 준비되지 않았습니다"); } if (useTempResolution) { cam.targetTexture = prevTarget; if (tempRT != null) { tempRT.Release(); UnityEngine.Object.Destroy(tempRT); } // 한 프레임 더 대기 — RenderStreamOutput.Update 가 원래 해상도로 복귀 yield return null; } } public void OpenScreenshotFolder() { if (Directory.Exists(screenshotSavePath)) { System.Diagnostics.Process.Start(screenshotSavePath); log?.Invoke($"저장 폴더 열기: {screenshotSavePath}"); } else { logError?.Invoke($"저장 폴더가 존재하지 않습니다: {screenshotSavePath}"); } } private void SaveRtAsPng(RenderTexture rt, string fileName, TextureFormat format) { string fullPath = Path.Combine(screenshotSavePath, fileName); RenderTexture prevActive = RenderTexture.active; Texture2D tex = null; try { RenderTexture.active = rt; tex = new Texture2D(rt.width, rt.height, format, false); tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); tex.Apply(); File.WriteAllBytes(fullPath, tex.EncodeToPNG()); log?.Invoke($"스크린샷 저장 완료: {fullPath} ({rt.width}x{rt.height})"); } catch (Exception e) { logError?.Invoke($"스크린샷 저장 실패 ({fileName}): {e.Message}"); } finally { RenderTexture.active = prevActive; if (tex != null) UnityEngine.Object.Destroy(tex); } } private string GenerateFileName(string extension, string suffix = "") { string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); return $"{screenshotFilePrefix}{suffix}_{timestamp}.{extension}"; } public void Cleanup() { // RenderStreamOutput 이 모든 RT/Material lifecycle 을 관리하므로 별도 정리 없음 } }