- RenderStreamOutput 을 URP 17 RenderGraph API 로 마이그레이션
(옛 Execute() 도 Compatibility Mode 호환용으로 유지)
- 알파 합성 셰이더 신규: Pre/Post 비교(블룸/글로우) + NiloToon Prepass G + 가우시안 블러
- 알파 채널 별도 Spout 송신 추가 ("Streamingle Spout Alpha Output")
- 그레이스케일 RGB 마스크, A=1
- spout_ndi_normalizer.exe 외부 프로세스 자동 실행/종료 (SpoutNdiLauncher 병합)
- Display 드롭다운 / Vsync / AlwaysOnTop / HideCursor / Realtime / NoActivate 옵션
- exe 가 있으면 강제 종료 후 단일 인스턴스 보장
- 내부 옵션(exe 경로, window size 등)은 [HideInInspector]
- ScreenshotManager 가 RenderStreamOutput 의 합성 결과를 그대로 PNG 저장
- 자체 카메라 렌더/셰이더 관리 제거 → 알파 품질 라이브 출력과 동일
- captureWidth/Height 지정 시 한 프레임 임시 고해상도 렌더 후 원복
- spout_ndi_normalizer.exe 위치: Resources → StreamingAssets/SpoutNdiNormalizer
- URP Asset: Allow Post Process Alpha Output 활성화
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
185 lines
6.4 KiB
C#
185 lines
6.4 KiB
C#
using UnityEngine;
|
|
using System;
|
|
using System.Collections;
|
|
using System.IO;
|
|
|
|
/// <summary>
|
|
/// 스크린샷 캡처 관리.
|
|
/// RenderStreamOutput 이 매 프레임 갱신하는 CaptureTexture/AlphaOutputTexture 를 PNG 로 저장.
|
|
/// captureWidth/Height 가 지정되면 캡처 직전에 카메라 targetTexture 를 임시로 그 해상도로
|
|
/// 변경해 한 프레임 고해상도 렌더 후 원복한다.
|
|
/// 트레이드오프: 그 한 프레임 동안 Spout/NDI 송신도 같은 해상도가 되어 수신측이 잠깐 깜빡임.
|
|
/// </summary>
|
|
[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<string> log;
|
|
private Action<string> logError;
|
|
|
|
public void Initialize(MonoBehaviour host, Action<string> log, Action<string> logError)
|
|
{
|
|
this.host = host;
|
|
this.log = log;
|
|
this.logError = logError;
|
|
|
|
if (renderStream == null)
|
|
renderStream = UnityEngine.Object.FindFirstObjectByType<RenderStreamOutput>();
|
|
|
|
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 을 관리하므로 별도 정리 없음
|
|
}
|
|
}
|