Refactor : Spout/NDI 출력 파이프라인 통합 + 알파 합성 + Normalizer 통합
- 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>
This commit is contained in:
parent
3f30d5672a
commit
9ea5f2af2b
BIN
Assets/Resources/Settings/Streamingle Render Pipeline Asset.asset
(Stored with Git LFS)
BIN
Assets/Resources/Settings/Streamingle Render Pipeline Asset.asset
(Stored with Git LFS)
Binary file not shown.
@ -1,9 +1,14 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using UnityEngine.Rendering.RenderGraphModule;
|
||||
using UnityEngine.Rendering.RenderGraphModule.Util;
|
||||
using Klak.Spout;
|
||||
using Klak.Ndi;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
// 출력 방식 열거형
|
||||
public enum OutputMethod
|
||||
@ -14,6 +19,16 @@ public enum OutputMethod
|
||||
Both
|
||||
}
|
||||
|
||||
// Normalizer 출력 디스플레이 (음수 = windowed, 0~ = borderless fullscreen monitor index)
|
||||
public enum NormalizerDisplay
|
||||
{
|
||||
Windowed = -1,
|
||||
Display1 = 0,
|
||||
Display2 = 1,
|
||||
Display3 = 2,
|
||||
Display4 = 3,
|
||||
}
|
||||
|
||||
public class RenderStreamOutput : MonoBehaviour
|
||||
{
|
||||
[Header("출력 설정")]
|
||||
@ -32,99 +47,212 @@ public class RenderStreamOutput : MonoBehaviour
|
||||
[Tooltip("Spout 송신 이름")]
|
||||
public string spoutSenderName = "Streamingle Spout Output";
|
||||
|
||||
[Header("Spout 알파 별도 송신")]
|
||||
[Tooltip("알파 전용 Spout 송신 컴포넌트 (자동 생성)")]
|
||||
public SpoutSender AlphaSender;
|
||||
[Tooltip("알파 전용 Spout 송신 이름")]
|
||||
public string alphaSpoutSenderName = "Streamingle Spout Alpha Output";
|
||||
[Tooltip("Hidden/Streamingle/AlphaOnly 셰이더")]
|
||||
public Shader alphaOnlyShader;
|
||||
|
||||
[Header("NDI 설정")]
|
||||
[Tooltip("NDI 송신 컴포넌트")]
|
||||
public NdiSender NdiSender;
|
||||
[Tooltip("NDI 송신 이름")]
|
||||
public string ndiSenderName = "Streamingle NDI Output";
|
||||
|
||||
[Header("쉐이더 설정")]
|
||||
[Tooltip("렌더링 결과를 처리할 쉐이더 머티리얼")]
|
||||
public Material ShaderContral;
|
||||
|
||||
[Header("텍스처 설정")]
|
||||
[Tooltip("텍스처 포맷 설정")]
|
||||
public RenderTextureFormat TextureFormat = RenderTextureFormat.DefaultHDR;
|
||||
[Tooltip("깊이 버퍼 비트 수")]
|
||||
public int DepthBuffer = 24;
|
||||
[Tooltip("안티앨리어싱 레벨 (1, 2, 4, 8)")]
|
||||
[Range(1, 8)]
|
||||
public int AntiAliasing = 1;
|
||||
|
||||
[HideInInspector] public CustomRenderTexture ShaderTexture = null;
|
||||
[HideInInspector] public RenderTexture ShaderCameraTexture = null;
|
||||
[Header("프레임 제한")]
|
||||
[Tooltip("타겟 프레임레이트 (-1: 제한 없음). VSync 는 자동으로 꺼짐")]
|
||||
public int targetFrameRate = 75;
|
||||
|
||||
private AlphaRecodingRenderPass m_ScriptablePass;
|
||||
[Header("Spout/NDI Normalizer")]
|
||||
[Tooltip("외부 정규화 exe 자동 실행. Unity 출력의 페이싱 jitter 를 외부 프로세스로 정규화해 안정적 cadence 송신")]
|
||||
public bool normalizerEnabled = true;
|
||||
[Tooltip("출력 디스플레이 — 풀스크린 모니터 또는 Windowed")]
|
||||
public NormalizerDisplay normalizerDisplay = NormalizerDisplay.Display2;
|
||||
[Tooltip("Normalizer 출력 cadence (Hz)")]
|
||||
public float normalizerFps = 60f;
|
||||
[Tooltip("Normalizer VSync (런타임에 V 키로 토글 가능)")]
|
||||
public bool normalizerVsync = true;
|
||||
[Tooltip("Normalizer 윈도우를 항상 최상위로 유지 (런타임에 T 키로 토글 가능)")]
|
||||
public bool normalizerAlwaysOnTop = false;
|
||||
[Tooltip("Normalizer 윈도우 위에 커서가 있을 때 숨김 (런타임에 C 키로 토글 가능)")]
|
||||
public bool normalizerHideCursor = false;
|
||||
[Tooltip("⚠ 헤드리스 렌더팜 전용 — TIME_CRITICAL + Pro Audio MMCSS 스케줄링. Unity 가 foreground 일 때 켜면 Unity 자체가 starvation 됨. 평소엔 끄세요")]
|
||||
public bool normalizerRealtime = false;
|
||||
[Tooltip("Normalizer 창이 클릭으로 focus 를 가져가지 않게 함. Unity Editor 와 같이 작업할 때 권장 — 클릭이 통과되어 Unity 가 foreground 유지. 단, 런타임 핫키(F11/V/T/C/P/ESC 등) 비활성")]
|
||||
public bool normalizerNoActivate = true;
|
||||
|
||||
// ──────── 내부 옵션 (사용자가 만지지 않도록 숨김) ────────
|
||||
[HideInInspector] public string normalizerExeRelativePath = "SpoutNdiNormalizer/spout_ndi_normalizer.exe";
|
||||
[HideInInspector] public string normalizerExeAbsolutePath = "";
|
||||
[HideInInspector] public int normalizerWindowWidth = 1280;
|
||||
[HideInInspector] public int normalizerWindowHeight = 720;
|
||||
[HideInInspector] public bool normalizerShowConsole = false;
|
||||
|
||||
[Header("알파 합성")]
|
||||
[Tooltip("Hidden/Streamingle/AlphaCompose 셰이더")]
|
||||
public Shader alphaComposeShader;
|
||||
[Tooltip("cameraColor.a 를 알파로 사용하는 강도. NiloToon 환경에서는 cameraColor.a 가 1 로 차서 보통 0 권장")]
|
||||
[Range(0f, 4f)]
|
||||
public float sourceAlphaGain = 0.0f;
|
||||
[Tooltip("후처리(블룸/글로우 등) 가 추가한 빛을 알파로 변환하는 강도")]
|
||||
[Range(0f, 20f)]
|
||||
public float bloomAlphaGain = 5.0f;
|
||||
[Tooltip("NiloToon Prepass G 채널 알파 강도 (0 = 사용 안 함)")]
|
||||
[Range(0f, 4f)]
|
||||
public float prepassAlphaGain = 1.0f;
|
||||
[Tooltip("Prepass 알파 외곽 가우시안 블러 반경 (텍셀)")]
|
||||
[Range(0f, 5f)]
|
||||
public float alphaBlurRadius = 1.0f;
|
||||
|
||||
[HideInInspector] public RenderTexture CaptureTexture = null;
|
||||
[HideInInspector] public RenderTexture AlphaOutputTexture = null;
|
||||
private RenderTexture m_PreColorTexture = null;
|
||||
|
||||
private RTHandle m_CaptureHandle;
|
||||
private RTHandle m_PreColorHandle;
|
||||
private RTHandle m_AlphaOutputHandle;
|
||||
|
||||
private Material m_AlphaComposeMaterial;
|
||||
private Material m_AlphaOnlyMaterial;
|
||||
private PreCapturePass m_PreCapturePass;
|
||||
private ComposePass m_ComposePass;
|
||||
private AlphaOutputPass m_AlphaOutputPass;
|
||||
|
||||
private Process m_NormalizerProc;
|
||||
|
||||
// 화면 크기 체크를 위한 변수
|
||||
private int screenWidth;
|
||||
private int screenHeight;
|
||||
|
||||
// 이전 출력 방식 저장
|
||||
private OutputMethod previousOutputMethod;
|
||||
// 이전 알파 채널 설정 저장
|
||||
private bool previousKeepAlpha;
|
||||
private int previousTargetFrameRate;
|
||||
|
||||
private static readonly int s_SourceAlphaGainID = Shader.PropertyToID("_SourceAlphaGain");
|
||||
private static readonly int s_BloomAlphaGainID = Shader.PropertyToID("_BloomAlphaGain");
|
||||
private static readonly int s_PrepassAlphaGainID = Shader.PropertyToID("_PrepassAlphaGain");
|
||||
private static readonly int s_AlphaBlurRadiusID = Shader.PropertyToID("_AlphaBlurRadius");
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// 컴포넌트 자동 할당
|
||||
MainCam = MainCam == null ? Camera.main : MainCam;
|
||||
|
||||
// 초기 설정 저장
|
||||
previousOutputMethod = outputMethod;
|
||||
previousKeepAlpha = keepAlpha;
|
||||
previousTargetFrameRate = targetFrameRate;
|
||||
|
||||
// 출력 컴포넌트 초기화
|
||||
ApplyFrameRate();
|
||||
EnsureMaterial();
|
||||
EnsureAlphaOnlyMaterial();
|
||||
InitializeOutputComponents();
|
||||
|
||||
InitializeTextures();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (normalizerEnabled)
|
||||
{
|
||||
try { LaunchNormalizer(); }
|
||||
catch (Exception e) { Debug.LogError($"[RenderStreamOutput] normalizer launch failed: {e}"); }
|
||||
}
|
||||
}
|
||||
|
||||
private void OnApplicationQuit() { KillNormalizer(); }
|
||||
|
||||
private void EnsureAlphaOnlyMaterial()
|
||||
{
|
||||
if (alphaOnlyShader == null)
|
||||
alphaOnlyShader = Shader.Find("Hidden/Streamingle/AlphaOnly");
|
||||
|
||||
if (alphaOnlyShader == null)
|
||||
{
|
||||
Debug.LogError("AlphaOnly 셰이더를 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_AlphaOnlyMaterial == null)
|
||||
m_AlphaOnlyMaterial = new Material(alphaOnlyShader) { hideFlags = HideFlags.HideAndDontSave };
|
||||
}
|
||||
|
||||
private void ApplyFrameRate()
|
||||
{
|
||||
if (targetFrameRate > 0)
|
||||
{
|
||||
QualitySettings.vSyncCount = 0;
|
||||
Application.targetFrameRate = targetFrameRate;
|
||||
}
|
||||
else
|
||||
{
|
||||
Application.targetFrameRate = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureMaterial()
|
||||
{
|
||||
if (alphaComposeShader == null)
|
||||
alphaComposeShader = Shader.Find("Hidden/Streamingle/AlphaCompose");
|
||||
|
||||
if (alphaComposeShader == null)
|
||||
{
|
||||
Debug.LogError("AlphaCompose 셰이더를 찾을 수 없습니다. Always Included Shaders 에 추가하거나 인스펙터에 할당하세요.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_AlphaComposeMaterial == null)
|
||||
m_AlphaComposeMaterial = new Material(alphaComposeShader) { hideFlags = HideFlags.HideAndDontSave };
|
||||
|
||||
PushMaterialParams();
|
||||
}
|
||||
|
||||
private void PushMaterialParams()
|
||||
{
|
||||
if (m_AlphaComposeMaterial == null) return;
|
||||
m_AlphaComposeMaterial.SetFloat(s_SourceAlphaGainID, sourceAlphaGain);
|
||||
m_AlphaComposeMaterial.SetFloat(s_BloomAlphaGainID, bloomAlphaGain);
|
||||
m_AlphaComposeMaterial.SetFloat(s_PrepassAlphaGainID, prepassAlphaGain);
|
||||
m_AlphaComposeMaterial.SetFloat(s_AlphaBlurRadiusID, alphaBlurRadius);
|
||||
}
|
||||
|
||||
private void InitializeOutputComponents()
|
||||
{
|
||||
// Spout 송신 컴포넌트 초기화
|
||||
if (outputMethod == OutputMethod.Spout || outputMethod == OutputMethod.Both)
|
||||
{
|
||||
if (Sender == null)
|
||||
{
|
||||
Sender = GetComponent<SpoutSender>();
|
||||
if (Sender == null)
|
||||
{
|
||||
Sender = gameObject.AddComponent<SpoutSender>();
|
||||
Sender.spoutName = spoutSenderName;
|
||||
Sender.keepAlpha = keepAlpha;
|
||||
Sender.captureMethod = Klak.Spout.CaptureMethod.Texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
// NDI 송신 컴포넌트 초기화
|
||||
|
||||
if (outputMethod == OutputMethod.NDI || outputMethod == OutputMethod.Both)
|
||||
{
|
||||
if (NdiSender == null)
|
||||
{
|
||||
NdiSender = GetComponent<NdiSender>();
|
||||
if (NdiSender == null)
|
||||
{
|
||||
Debug.Log("NDI 송신 컴포넌트를 생성합니다.");
|
||||
NdiSender = gameObject.AddComponent<NdiSender>();
|
||||
if (NdiSender != null)
|
||||
{
|
||||
NdiSender.ndiName = ndiSenderName;
|
||||
NdiSender.keepAlpha = keepAlpha;
|
||||
NdiSender.captureMethod = Klak.Ndi.CaptureMethod.Texture;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("NDI 송신 컴포넌트 생성 실패.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (AlphaSender == null)
|
||||
{
|
||||
// 같은 GameObject 에 두 번째 SpoutSender 추가
|
||||
AlphaSender = gameObject.AddComponent<SpoutSender>();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
InitializeScriptablePass();
|
||||
InitializeScriptablePasses();
|
||||
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
|
||||
}
|
||||
|
||||
@ -135,77 +263,76 @@ public class RenderStreamOutput : MonoBehaviour
|
||||
|
||||
private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
|
||||
{
|
||||
if (camera == MainCam)
|
||||
{
|
||||
var data = camera.GetUniversalAdditionalCameraData();
|
||||
data.scriptableRenderer.EnqueuePass(m_ScriptablePass);
|
||||
}
|
||||
if (camera != MainCam)
|
||||
return;
|
||||
|
||||
var data = camera.GetUniversalAdditionalCameraData();
|
||||
if (m_PreCapturePass != null)
|
||||
data.scriptableRenderer.EnqueuePass(m_PreCapturePass);
|
||||
if (m_ComposePass != null)
|
||||
data.scriptableRenderer.EnqueuePass(m_ComposePass);
|
||||
if (m_AlphaOutputPass != null)
|
||||
data.scriptableRenderer.EnqueuePass(m_AlphaOutputPass);
|
||||
}
|
||||
|
||||
private void InitializeTextures()
|
||||
{
|
||||
// 카메라가 null인 경우 체크
|
||||
if (MainCam == null)
|
||||
{
|
||||
Debug.LogError("MainCam이 설정되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ShaderContral이 null인 경우 체크
|
||||
if (ShaderContral == null)
|
||||
{
|
||||
Debug.LogError("ShaderContral 머티리얼이 설정되지 않았습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 카메라의 실제 해상도 사용
|
||||
screenWidth = MainCam.pixelWidth;
|
||||
screenHeight = MainCam.pixelHeight;
|
||||
|
||||
// ShaderTexture 초기화 - 메모리 누수 방지를 위한 확실한 정리
|
||||
if (ShaderTexture != null)
|
||||
var desc = new RenderTextureDescriptor(screenWidth, screenHeight, TextureFormat, 0)
|
||||
{
|
||||
ShaderTexture.Release();
|
||||
DestroyImmediate(ShaderTexture); // Destroy 대신 DestroyImmediate 사용
|
||||
}
|
||||
ShaderTexture = new CustomRenderTexture(screenWidth, screenHeight, TextureFormat);
|
||||
ShaderTexture.material = ShaderContral;
|
||||
ShaderTexture.updateMode = CustomRenderTextureUpdateMode.Realtime;
|
||||
ShaderTexture.antiAliasing = AntiAliasing;
|
||||
msaaSamples = Mathf.Max(1, AntiAliasing)
|
||||
};
|
||||
|
||||
// ShaderCameraTexture 초기화
|
||||
if (ShaderCameraTexture != null)
|
||||
{
|
||||
ShaderCameraTexture.Release();
|
||||
DestroyImmediate(ShaderCameraTexture); // Destroy 대신 DestroyImmediate 사용
|
||||
}
|
||||
ShaderCameraTexture = new RenderTexture(screenWidth, screenHeight, DepthBuffer, TextureFormat);
|
||||
ShaderCameraTexture.Create();
|
||||
ShaderCameraTexture.antiAliasing = AntiAliasing;
|
||||
ReleaseRT(ref CaptureTexture, ref m_CaptureHandle);
|
||||
CaptureTexture = new RenderTexture(desc);
|
||||
CaptureTexture.Create();
|
||||
m_CaptureHandle = RTHandles.Alloc(CaptureTexture, transferOwnership: false);
|
||||
|
||||
// ShaderCameraTexture를 ShaderContral의 _MainTex에 할당
|
||||
if (ShaderContral != null)
|
||||
{
|
||||
ShaderContral.SetTexture("_MainTex", ShaderCameraTexture);
|
||||
UpdateShaderScreenSize();
|
||||
}
|
||||
ReleaseRT(ref m_PreColorTexture, ref m_PreColorHandle);
|
||||
m_PreColorTexture = new RenderTexture(desc);
|
||||
m_PreColorTexture.Create();
|
||||
m_PreColorHandle = RTHandles.Alloc(m_PreColorTexture, transferOwnership: false);
|
||||
|
||||
ReleaseRT(ref AlphaOutputTexture, ref m_AlphaOutputHandle);
|
||||
AlphaOutputTexture = new RenderTexture(desc);
|
||||
AlphaOutputTexture.Create();
|
||||
m_AlphaOutputHandle = RTHandles.Alloc(AlphaOutputTexture, transferOwnership: false);
|
||||
|
||||
// 출력 방식에 따라 텍스처 할당
|
||||
UpdateOutputSources();
|
||||
InitializeScriptablePasses();
|
||||
}
|
||||
|
||||
// m_ScriptablePass 재초기화
|
||||
InitializeScriptablePass();
|
||||
private static void ReleaseRT(ref RenderTexture rt, ref RTHandle handle)
|
||||
{
|
||||
if (handle != null)
|
||||
{
|
||||
handle.Release();
|
||||
handle = null;
|
||||
}
|
||||
if (rt != null)
|
||||
{
|
||||
rt.Release();
|
||||
DestroyImmediate(rt);
|
||||
rt = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOutputSources()
|
||||
{
|
||||
// Spout 출력 설정
|
||||
if (outputMethod == OutputMethod.Spout || outputMethod == OutputMethod.Both)
|
||||
{
|
||||
if (Sender != null)
|
||||
{
|
||||
Sender.enabled = true;
|
||||
Sender.sourceTexture = ShaderTexture;
|
||||
Sender.sourceTexture = CaptureTexture;
|
||||
Sender.spoutName = spoutSenderName;
|
||||
Sender.keepAlpha = keepAlpha;
|
||||
Sender.captureMethod = Klak.Spout.CaptureMethod.Texture;
|
||||
@ -216,20 +343,18 @@ public class RenderStreamOutput : MonoBehaviour
|
||||
Sender.enabled = false;
|
||||
}
|
||||
|
||||
// NDI 출력 설정
|
||||
if (outputMethod == OutputMethod.NDI || outputMethod == OutputMethod.Both)
|
||||
{
|
||||
if (NdiSender != null)
|
||||
{
|
||||
NdiSender.enabled = true;
|
||||
NdiSender.sourceTexture = ShaderTexture;
|
||||
NdiSender.sourceTexture = CaptureTexture;
|
||||
NdiSender.ndiName = ndiSenderName;
|
||||
NdiSender.keepAlpha = keepAlpha;
|
||||
NdiSender.captureMethod = Klak.Ndi.CaptureMethod.Texture;
|
||||
}
|
||||
else
|
||||
{
|
||||
// NDI 송신 컴포넌트가 없으면 다시 초기화 시도
|
||||
InitializeOutputComponents();
|
||||
}
|
||||
}
|
||||
@ -237,27 +362,41 @@ public class RenderStreamOutput : MonoBehaviour
|
||||
{
|
||||
NdiSender.enabled = false;
|
||||
}
|
||||
|
||||
// 알파 전용 Spout 송신 (항상 활성)
|
||||
if (AlphaSender != null)
|
||||
{
|
||||
AlphaSender.enabled = true;
|
||||
AlphaSender.sourceTexture = AlphaOutputTexture;
|
||||
AlphaSender.spoutName = alphaSpoutSenderName;
|
||||
AlphaSender.keepAlpha = false;
|
||||
AlphaSender.captureMethod = Klak.Spout.CaptureMethod.Texture;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeScriptablePass()
|
||||
private void InitializeScriptablePasses()
|
||||
{
|
||||
m_ScriptablePass = new AlphaRecodingRenderPass(ShaderCameraTexture, ShaderContral, ShaderTexture);
|
||||
m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
|
||||
m_PreCapturePass = new PreCapturePass(m_PreColorHandle)
|
||||
{
|
||||
renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing
|
||||
};
|
||||
m_ComposePass = new ComposePass(m_CaptureHandle, m_PreColorHandle, m_PreColorTexture, m_AlphaComposeMaterial)
|
||||
{
|
||||
renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing
|
||||
};
|
||||
m_AlphaOutputPass = new AlphaOutputPass(m_CaptureHandle, m_AlphaOutputHandle, m_AlphaOnlyMaterial)
|
||||
{
|
||||
renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing + 1
|
||||
};
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 화면 크기 변경 체크 - 매 프레임마다 Screen.width/height 접근은 비용이 있음
|
||||
// 변경이 자주 일어나지 않으므로 더 효율적인 방법으로 변경
|
||||
if (MainCam != null && (screenWidth != MainCam.pixelWidth || screenHeight != MainCam.pixelHeight))
|
||||
{
|
||||
screenWidth = MainCam.pixelWidth;
|
||||
screenHeight = MainCam.pixelHeight;
|
||||
InitializeTextures();
|
||||
UpdateShaderScreenSize();
|
||||
}
|
||||
|
||||
// 설정이 변경되었는지 확인
|
||||
if (previousOutputMethod != outputMethod || previousKeepAlpha != keepAlpha)
|
||||
{
|
||||
previousOutputMethod = outputMethod;
|
||||
@ -265,87 +404,247 @@ public class RenderStreamOutput : MonoBehaviour
|
||||
InitializeOutputComponents();
|
||||
UpdateOutputSources();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateShaderScreenSize()
|
||||
{
|
||||
if (ShaderContral != null)
|
||||
if (previousTargetFrameRate != targetFrameRate)
|
||||
{
|
||||
ShaderContral.SetVector("_Resolution", new Vector4(screenWidth, screenHeight, 0, 0));
|
||||
previousTargetFrameRate = targetFrameRate;
|
||||
ApplyFrameRate();
|
||||
}
|
||||
|
||||
PushMaterialParams();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// m_ScriptablePass 정리 (내부 텍스처 참조 해제)
|
||||
m_ScriptablePass = null;
|
||||
m_PreCapturePass = null;
|
||||
m_ComposePass = null;
|
||||
m_AlphaOutputPass = null;
|
||||
|
||||
if (ShaderCameraTexture != null)
|
||||
ReleaseRT(ref CaptureTexture, ref m_CaptureHandle);
|
||||
ReleaseRT(ref m_PreColorTexture, ref m_PreColorHandle);
|
||||
ReleaseRT(ref AlphaOutputTexture, ref m_AlphaOutputHandle);
|
||||
|
||||
if (m_AlphaComposeMaterial != null)
|
||||
{
|
||||
ShaderCameraTexture.Release();
|
||||
DestroyImmediate(ShaderCameraTexture);
|
||||
ShaderCameraTexture = null; // 참조 제거
|
||||
DestroyImmediate(m_AlphaComposeMaterial);
|
||||
m_AlphaComposeMaterial = null;
|
||||
}
|
||||
if (ShaderTexture != null)
|
||||
if (m_AlphaOnlyMaterial != null)
|
||||
{
|
||||
ShaderTexture.Release();
|
||||
DestroyImmediate(ShaderTexture);
|
||||
ShaderTexture = null; // 참조 제거
|
||||
DestroyImmediate(m_AlphaOnlyMaterial);
|
||||
m_AlphaOnlyMaterial = null;
|
||||
}
|
||||
|
||||
KillNormalizer();
|
||||
}
|
||||
}
|
||||
|
||||
public class AlphaRecodingRenderPass : ScriptableRenderPass
|
||||
{
|
||||
private RenderTexture m_ShaderCameraTexture;
|
||||
private Material m_ShaderContral;
|
||||
private CustomRenderTexture m_ShaderTexture;
|
||||
// ───────────────────────── Spout/NDI Normalizer (외부 exe) ─────────────────────────
|
||||
|
||||
public AlphaRecodingRenderPass(RenderTexture shaderCameraTexture, Material shaderContral, CustomRenderTexture shaderTexture)
|
||||
private string ResolveNormalizerExePath()
|
||||
{
|
||||
m_ShaderCameraTexture = shaderCameraTexture;
|
||||
m_ShaderContral = shaderContral;
|
||||
m_ShaderTexture = shaderTexture;
|
||||
if (!string.IsNullOrEmpty(normalizerExeAbsolutePath))
|
||||
return normalizerExeAbsolutePath;
|
||||
var rel = normalizerExeRelativePath.Replace('/', Path.DirectorySeparatorChar);
|
||||
return Path.Combine(Application.streamingAssetsPath, rel);
|
||||
}
|
||||
|
||||
[Obsolete("This method is obsolete. Use the new Render Graph API instead.")]
|
||||
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
|
||||
public void LaunchNormalizer()
|
||||
{
|
||||
var exe = ResolveNormalizerExePath();
|
||||
if (!File.Exists(exe))
|
||||
{
|
||||
Debug.LogError($"[RenderStreamOutput] normalizer exe not found: {exe}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 기존 자식 프로세스 + orphan 모두 종료 후 새로 실행 (단일 인스턴스 보장)
|
||||
KillNormalizer();
|
||||
KillExternalNormalizers(exe);
|
||||
|
||||
var args = BuildNormalizerArgs();
|
||||
var psi = new ProcessStartInfo(exe, args)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = !normalizerShowConsole,
|
||||
WindowStyle = normalizerShowConsole ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden,
|
||||
WorkingDirectory = Path.GetDirectoryName(exe),
|
||||
};
|
||||
|
||||
m_NormalizerProc = Process.Start(psi);
|
||||
if (m_NormalizerProc == null)
|
||||
{
|
||||
Debug.LogError("[RenderStreamOutput] normalizer Process.Start returned null");
|
||||
return;
|
||||
}
|
||||
Debug.Log($"[RenderStreamOutput] normalizer launched PID {m_NormalizerProc.Id}\n exe={exe}\n args={args}");
|
||||
}
|
||||
|
||||
public void KillNormalizer()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (m_ShaderCameraTexture == null || m_ShaderContral == null || !m_ShaderCameraTexture.IsCreated())
|
||||
return;
|
||||
|
||||
CommandBuffer cmd = CommandBufferPool.Get("Alpha Recoding Pass");
|
||||
|
||||
try
|
||||
if (m_NormalizerProc != null && !m_NormalizerProc.HasExited)
|
||||
{
|
||||
// Update cameraColorTargetHandle usage
|
||||
var colorTarget = renderingData.cameraData.renderer.cameraColorTargetHandle;
|
||||
|
||||
// 최종 렌더링 결과를 가져옵니다
|
||||
RenderTargetIdentifier source = colorTarget;
|
||||
cmd.Blit(source, m_ShaderCameraTexture);
|
||||
|
||||
// ShaderCameraTexture를 ShaderContral의 _MainTex에 할당
|
||||
m_ShaderContral.SetTexture("_MainTex", m_ShaderCameraTexture);
|
||||
|
||||
// ShaderTexture 업데이트 (필요한 경우)
|
||||
if (m_ShaderTexture != null)
|
||||
{
|
||||
m_ShaderTexture.Update();
|
||||
}
|
||||
|
||||
context.ExecuteCommandBuffer(cmd);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CommandBufferPool.Release(cmd);
|
||||
m_NormalizerProc.Kill();
|
||||
m_NormalizerProc.WaitForExit(2000);
|
||||
}
|
||||
}
|
||||
catch (System.Exception e)
|
||||
catch (Exception e) { Debug.LogWarning($"[RenderStreamOutput] normalizer kill: {e.Message}"); }
|
||||
m_NormalizerProc = null;
|
||||
}
|
||||
|
||||
private string BuildNormalizerArgs()
|
||||
{
|
||||
string Q(string s) => "\"" + s.Replace("\"", "\\\"") + "\"";
|
||||
|
||||
var parts = new System.Collections.Generic.List<string>
|
||||
{
|
||||
Debug.LogError($"Alpha Recoding 렌더 패스 실행 중 오류 발생: {e.Message}");
|
||||
"--sender", Q(spoutSenderName),
|
||||
"--fps", normalizerFps.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture),
|
||||
"--width", normalizerWindowWidth.ToString(),
|
||||
"--height", normalizerWindowHeight.ToString(),
|
||||
"--parent-pid", Process.GetCurrentProcess().Id.ToString(),
|
||||
};
|
||||
int monitorIndex = (int)normalizerDisplay;
|
||||
if (monitorIndex >= 0) { parts.Add("--monitor"); parts.Add(monitorIndex.ToString()); }
|
||||
if (!normalizerVsync) parts.Add("--no-vsync");
|
||||
if (normalizerAlwaysOnTop) parts.Add("--topmost");
|
||||
if (normalizerHideCursor) parts.Add("--hide-cursor");
|
||||
if (normalizerRealtime) parts.Add("--realtime");
|
||||
if (normalizerNoActivate) parts.Add("--no-activate");
|
||||
return string.Join(" ", parts);
|
||||
}
|
||||
|
||||
private static void KillExternalNormalizers(string exePath)
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(exePath);
|
||||
try
|
||||
{
|
||||
foreach (var p in Process.GetProcessesByName(name))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!p.HasExited)
|
||||
{
|
||||
p.Kill();
|
||||
p.WaitForExit(2000);
|
||||
Debug.Log($"[RenderStreamOutput] killed orphan normalizer PID {p.Id}");
|
||||
}
|
||||
}
|
||||
catch (Exception e) { Debug.LogWarning($"[RenderStreamOutput] kill orphan: {e.Message}"); }
|
||||
finally { p.Dispose(); }
|
||||
}
|
||||
}
|
||||
catch { /* best effort */ }
|
||||
}
|
||||
}
|
||||
|
||||
// 후처리 전 cameraColor 를 PreColorTexture 로 백업
|
||||
public class PreCapturePass : ScriptableRenderPass
|
||||
{
|
||||
private RTHandle m_PreColorHandle;
|
||||
|
||||
public PreCapturePass(RTHandle preColorHandle)
|
||||
{
|
||||
m_PreColorHandle = preColorHandle;
|
||||
}
|
||||
|
||||
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
|
||||
{
|
||||
if (m_PreColorHandle == null)
|
||||
return;
|
||||
|
||||
var resourceData = frameData.Get<UniversalResourceData>();
|
||||
if (resourceData == null)
|
||||
return;
|
||||
|
||||
TextureHandle source = resourceData.activeColorTexture;
|
||||
if (!source.IsValid())
|
||||
return;
|
||||
|
||||
TextureHandle destination = renderGraph.ImportTexture(m_PreColorHandle);
|
||||
using (var builder = renderGraph.AddBlitPass(source, destination, Vector2.one, Vector2.zero,
|
||||
passName: "Streamingle Pre-PostProcess Capture", returnBuilder: true))
|
||||
{
|
||||
builder.AllowPassCulling(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CaptureTexture 의 알파를 그레이스케일 RGB 로 변환해서 AlphaOutputTexture 로 출력
|
||||
public class AlphaOutputPass : ScriptableRenderPass
|
||||
{
|
||||
private RTHandle m_SourceHandle;
|
||||
private RTHandle m_DestHandle;
|
||||
private Material m_Material;
|
||||
|
||||
public AlphaOutputPass(RTHandle sourceHandle, RTHandle destHandle, Material material)
|
||||
{
|
||||
m_SourceHandle = sourceHandle;
|
||||
m_DestHandle = destHandle;
|
||||
m_Material = material;
|
||||
}
|
||||
|
||||
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
|
||||
{
|
||||
if (m_SourceHandle == null || m_DestHandle == null || m_Material == null)
|
||||
return;
|
||||
|
||||
TextureHandle source = renderGraph.ImportTexture(m_SourceHandle);
|
||||
TextureHandle destination = renderGraph.ImportTexture(m_DestHandle);
|
||||
|
||||
var blitParams = new RenderGraphUtils.BlitMaterialParameters(source, destination, m_Material, 0);
|
||||
using (var builder = renderGraph.AddBlitPass(blitParams,
|
||||
passName: "Streamingle Alpha-Only Output", returnBuilder: true))
|
||||
{
|
||||
builder.AllowPassCulling(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 후처리 후 cameraColor + PreColorTexture + NiloToonPrepass.g 합성 → CaptureTexture
|
||||
public class ComposePass : ScriptableRenderPass
|
||||
{
|
||||
private RTHandle m_CaptureHandle;
|
||||
private RTHandle m_PreColorHandle;
|
||||
private RenderTexture m_PreColorRT;
|
||||
private Material m_Material;
|
||||
private static readonly int s_PreColorTexID = Shader.PropertyToID("_PreColorTex");
|
||||
|
||||
public ComposePass(RTHandle captureHandle, RTHandle preColorHandle, RenderTexture preColorRT, Material material)
|
||||
{
|
||||
m_CaptureHandle = captureHandle;
|
||||
m_PreColorHandle = preColorHandle;
|
||||
m_PreColorRT = preColorRT;
|
||||
m_Material = material;
|
||||
}
|
||||
|
||||
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
|
||||
{
|
||||
if (m_CaptureHandle == null || m_PreColorHandle == null || m_Material == null)
|
||||
return;
|
||||
|
||||
var resourceData = frameData.Get<UniversalResourceData>();
|
||||
if (resourceData == null)
|
||||
return;
|
||||
|
||||
TextureHandle source = resourceData.activeColorTexture;
|
||||
if (!source.IsValid())
|
||||
return;
|
||||
|
||||
TextureHandle destination = renderGraph.ImportTexture(m_CaptureHandle);
|
||||
TextureHandle preColor = renderGraph.ImportTexture(m_PreColorHandle);
|
||||
|
||||
// 머티리얼에 PreColor 텍스처 직접 바인딩 (외부 RT 라 lifetime 안전).
|
||||
// RenderGraph 는 UseTexture 로 PreCapturePass→ComposePass 순서만 보장.
|
||||
m_Material.SetTexture(s_PreColorTexID, m_PreColorRT);
|
||||
|
||||
var blitParams = new RenderGraphUtils.BlitMaterialParameters(source, destination, m_Material, 0);
|
||||
using (var builder = renderGraph.AddBlitPass(blitParams,
|
||||
passName: "Streamingle Alpha Compose", returnBuilder: true))
|
||||
{
|
||||
builder.UseTexture(preColor);
|
||||
builder.AllowPassCulling(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
// 후처리 후 RGB + 합성 알파를 출력하는 풀스크린 Blit 셰이더.
|
||||
//
|
||||
// 알파 = saturate(prepassAlpha + bloomAlpha)
|
||||
// prepassAlpha = NiloToon Prepass G 채널 (캐릭터 외곽, AA 포함)
|
||||
// bloomAlpha = saturate((postLum - preLum) * _BloomAlphaGain) — 후처리가 추가한 빛
|
||||
//
|
||||
// _PreColorTex 가 비어있으면 bloomAlpha = 0 (Prepass 만 사용)
|
||||
// _NiloToonPrepassBufferTex 가 비어있으면 prepassAlpha = 0 (Bloom diff 만 사용)
|
||||
Shader "Hidden/Streamingle/AlphaCompose"
|
||||
{
|
||||
Properties
|
||||
{
|
||||
_SourceAlphaGain ("Source Alpha Gain (cameraColor.a)", Range(0, 4)) = 1.0
|
||||
_BloomAlphaGain ("Bloom Alpha Gain", Range(0, 20)) = 5.0
|
||||
_PrepassAlphaGain ("Prepass Alpha Gain", Range(0, 4)) = 1.0
|
||||
_AlphaBlurRadius ("Alpha Blur Radius (texels)", Range(0, 5)) = 1.0
|
||||
}
|
||||
|
||||
SubShader
|
||||
{
|
||||
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
|
||||
|
||||
Pass
|
||||
{
|
||||
ZWrite Off ZTest Always Cull Off
|
||||
Blend Off
|
||||
|
||||
HLSLPROGRAM
|
||||
#pragma vertex Vert
|
||||
#pragma fragment Frag
|
||||
|
||||
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
|
||||
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
|
||||
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
|
||||
|
||||
// _BlitTexture 는 Blit.hlsl 가 이미 선언함 (후처리 후 cameraColor)
|
||||
TEXTURE2D(_PreColorTex); // 후처리 전 cameraColor
|
||||
SAMPLER(sampler_PreColorTex);
|
||||
|
||||
TEXTURE2D(_NiloToonPrepassBufferTex); // NiloToon 글로벌
|
||||
SAMPLER(sampler_NiloToonPrepassBufferTex);
|
||||
|
||||
float _SourceAlphaGain;
|
||||
float _BloomAlphaGain;
|
||||
float _PrepassAlphaGain;
|
||||
float _AlphaBlurRadius;
|
||||
|
||||
// 3x3 가우시안 가중치 (1/4 1/8 1/16) — Prepass G 외곽 부드럽게
|
||||
half SamplePrepassAlpha(float2 uv)
|
||||
{
|
||||
if (_AlphaBlurRadius <= 0.001)
|
||||
return SAMPLE_TEXTURE2D(_NiloToonPrepassBufferTex,
|
||||
sampler_NiloToonPrepassBufferTex, uv).g;
|
||||
|
||||
float2 t = (1.0 / _ScreenParams.xy) * _AlphaBlurRadius;
|
||||
|
||||
half a = 0;
|
||||
a += SAMPLE_TEXTURE2D(_NiloToonPrepassBufferTex, sampler_NiloToonPrepassBufferTex, uv).g * 0.25;
|
||||
a += SAMPLE_TEXTURE2D(_NiloToonPrepassBufferTex, sampler_NiloToonPrepassBufferTex, uv + float2( 0, t.y)).g * 0.125;
|
||||
a += SAMPLE_TEXTURE2D(_NiloToonPrepassBufferTex, sampler_NiloToonPrepassBufferTex, uv + float2( 0, -t.y)).g * 0.125;
|
||||
a += SAMPLE_TEXTURE2D(_NiloToonPrepassBufferTex, sampler_NiloToonPrepassBufferTex, uv + float2( t.x, 0)).g * 0.125;
|
||||
a += SAMPLE_TEXTURE2D(_NiloToonPrepassBufferTex, sampler_NiloToonPrepassBufferTex, uv + float2(-t.x, 0)).g * 0.125;
|
||||
a += SAMPLE_TEXTURE2D(_NiloToonPrepassBufferTex, sampler_NiloToonPrepassBufferTex, uv + float2( t.x, t.y)).g * 0.0625;
|
||||
a += SAMPLE_TEXTURE2D(_NiloToonPrepassBufferTex, sampler_NiloToonPrepassBufferTex, uv + float2(-t.x, t.y)).g * 0.0625;
|
||||
a += SAMPLE_TEXTURE2D(_NiloToonPrepassBufferTex, sampler_NiloToonPrepassBufferTex, uv + float2( t.x, -t.y)).g * 0.0625;
|
||||
a += SAMPLE_TEXTURE2D(_NiloToonPrepassBufferTex, sampler_NiloToonPrepassBufferTex, uv + float2(-t.x, -t.y)).g * 0.0625;
|
||||
return a;
|
||||
}
|
||||
|
||||
half4 Frag(Varyings input) : SV_Target
|
||||
{
|
||||
float2 uv = input.texcoord;
|
||||
|
||||
half4 postSample = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv);
|
||||
half3 postRgb = postSample.rgb;
|
||||
half sourceAlpha = postSample.a * _SourceAlphaGain;
|
||||
|
||||
half3 preRgb = SAMPLE_TEXTURE2D(_PreColorTex, sampler_PreColorTex, uv).rgb;
|
||||
|
||||
// 후처리가 추가한 luminance (블룸/글로우/플레어 등)
|
||||
half postLum = Luminance(postRgb);
|
||||
half preLum = Luminance(preRgb);
|
||||
half bloomAlpha = saturate((postLum - preLum) * _BloomAlphaGain);
|
||||
|
||||
// NiloToon Prepass G 채널 알파 (캐릭터 외곽, 가우시안 블러로 AA)
|
||||
half prepassAlpha = SamplePrepassAlpha(uv) * _PrepassAlphaGain;
|
||||
|
||||
half alpha = saturate(sourceAlpha + prepassAlpha + bloomAlpha);
|
||||
|
||||
return half4(postRgb, alpha);
|
||||
}
|
||||
ENDHLSL
|
||||
}
|
||||
}
|
||||
Fallback Off
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d778847d83c98846bc8e7c220f88356
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -0,0 +1,30 @@
|
||||
// CaptureTexture 의 알파를 그레이스케일 RGB 로 출력. 알파는 1 고정.
|
||||
// 검정 = 알파 0, 흰색 = 알파 1
|
||||
Shader "Hidden/Streamingle/AlphaOnly"
|
||||
{
|
||||
SubShader
|
||||
{
|
||||
Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
|
||||
|
||||
Pass
|
||||
{
|
||||
ZWrite Off ZTest Always Cull Off
|
||||
Blend Off
|
||||
|
||||
HLSLPROGRAM
|
||||
#pragma vertex Vert
|
||||
#pragma fragment Frag
|
||||
|
||||
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
|
||||
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
|
||||
|
||||
half4 Frag(Varyings input) : SV_Target
|
||||
{
|
||||
half a = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, input.texcoord).a;
|
||||
return half4(a, a, a, 1.0);
|
||||
}
|
||||
ENDHLSL
|
||||
}
|
||||
}
|
||||
Fallback Off
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80cfe82978045b341b4aa611843ae050
|
||||
ShaderImporter:
|
||||
externalObjects: {}
|
||||
defaultTextures: []
|
||||
nonModifiableTextures: []
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -1,67 +1,54 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// 스크린샷 캡처 관리 (RGB + 알파채널)
|
||||
/// 스크린샷 캡처 관리.
|
||||
/// RenderStreamOutput 이 매 프레임 갱신하는 CaptureTexture/AlphaOutputTexture 를 PNG 로 저장.
|
||||
/// captureWidth/Height 가 지정되면 캡처 직전에 카메라 targetTexture 를 임시로 그 해상도로
|
||||
/// 변경해 한 프레임 고해상도 렌더 후 원복한다.
|
||||
/// 트레이드오프: 그 한 프레임 동안 Spout/NDI 송신도 같은 해상도가 되어 수신측이 잠깐 깜빡임.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ScreenshotManager
|
||||
{
|
||||
[Tooltip("스크린샷 해상도 (기본: 4K)")]
|
||||
public int screenshotWidth = 3840;
|
||||
[Tooltip("RenderStreamOutput 컴포넌트 (비어있으면 씬에서 자동 검색)")]
|
||||
public RenderStreamOutput renderStream;
|
||||
|
||||
[Tooltip("스크린샷 해상도 (기본: 4K)")]
|
||||
public int screenshotHeight = 2160;
|
||||
|
||||
[Tooltip("스크린샷 저장 경로 (비어있으면 바탕화면)")]
|
||||
[Tooltip("스크린샷 저장 경로 (비어있으면 프로젝트 루트의 Screenshots/)")]
|
||||
public string screenshotSavePath = "";
|
||||
|
||||
[Tooltip("파일명 앞에 붙을 접두사")]
|
||||
public string screenshotFilePrefix = "Screenshot";
|
||||
|
||||
[Tooltip("알파 채널 추출용 셰이더")]
|
||||
public Shader alphaShader;
|
||||
[Header("고해상도 캡처 (0 이면 카메라 해상도 사용)")]
|
||||
[Tooltip("캡처 너비. 0 이면 카메라 해상도 그대로")]
|
||||
public int captureWidth = 3840;
|
||||
[Tooltip("캡처 높이. 0 이면 카메라 해상도 그대로")]
|
||||
public int captureHeight = 2160;
|
||||
|
||||
[Tooltip("NiloToon Prepass 버퍼 텍스처 이름")]
|
||||
public string niloToonPrepassBufferName = "_NiloToonPrepassBufferTex";
|
||||
|
||||
[Tooltip("촬영할 카메라 (비어있으면 메인 카메라 사용)")]
|
||||
public Camera screenshotCamera;
|
||||
|
||||
[Tooltip("알파 채널 블러 반경 (0 = 블러 없음, 1.0 = 약한 블러)")]
|
||||
[Range(0f, 3f)]
|
||||
public float alphaBlurRadius = 1.0f;
|
||||
|
||||
[NonSerialized]
|
||||
private Material alphaMaterial;
|
||||
[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(Action<string> log, Action<string> logError)
|
||||
public void Initialize(MonoBehaviour host, Action<string> log, Action<string> logError)
|
||||
{
|
||||
this.host = host;
|
||||
this.log = log;
|
||||
this.logError = logError;
|
||||
|
||||
if (screenshotCamera == null)
|
||||
{
|
||||
screenshotCamera = Camera.main;
|
||||
}
|
||||
if (renderStream == null)
|
||||
renderStream = UnityEngine.Object.FindFirstObjectByType<RenderStreamOutput>();
|
||||
|
||||
if (alphaShader == null)
|
||||
{
|
||||
alphaShader = Shader.Find("Hidden/AlphaFromNiloToon");
|
||||
if (alphaShader == null)
|
||||
{
|
||||
logError?.Invoke("알파 셰이더를 찾을 수 없습니다: Hidden/AlphaFromNiloToon");
|
||||
}
|
||||
}
|
||||
if (renderStream == null)
|
||||
logError?.Invoke("RenderStreamOutput 컴포넌트를 찾을 수 없습니다 — 알파 합성 캡처 불가");
|
||||
|
||||
if (string.IsNullOrEmpty(screenshotSavePath))
|
||||
{
|
||||
screenshotSavePath = Path.Combine(Application.dataPath, "..", "Screenshots");
|
||||
}
|
||||
|
||||
if (!Directory.Exists(screenshotSavePath))
|
||||
{
|
||||
@ -72,113 +59,75 @@ public class ScreenshotManager
|
||||
|
||||
public void CaptureScreenshot()
|
||||
{
|
||||
if (screenshotCamera == null)
|
||||
{
|
||||
logError?.Invoke("촬영할 카메라가 설정되지 않았습니다!");
|
||||
return;
|
||||
}
|
||||
|
||||
string fileName = GenerateFileName("png");
|
||||
string fullPath = Path.Combine(screenshotSavePath, fileName);
|
||||
|
||||
try
|
||||
{
|
||||
RenderTexture rt = new RenderTexture(screenshotWidth, screenshotHeight, 24);
|
||||
RenderTexture currentRT = screenshotCamera.targetTexture;
|
||||
|
||||
screenshotCamera.targetTexture = rt;
|
||||
screenshotCamera.Render();
|
||||
|
||||
RenderTexture.active = rt;
|
||||
Texture2D screenshot = new Texture2D(screenshotWidth, screenshotHeight, TextureFormat.RGB24, false);
|
||||
screenshot.ReadPixels(new Rect(0, 0, screenshotWidth, screenshotHeight), 0, 0);
|
||||
screenshot.Apply();
|
||||
|
||||
byte[] bytes = screenshot.EncodeToPNG();
|
||||
File.WriteAllBytes(fullPath, bytes);
|
||||
|
||||
screenshotCamera.targetTexture = currentRT;
|
||||
RenderTexture.active = null;
|
||||
rt.Release();
|
||||
UnityEngine.Object.Destroy(rt);
|
||||
UnityEngine.Object.Destroy(screenshot);
|
||||
|
||||
log?.Invoke($"스크린샷 저장 완료: {fullPath}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logError?.Invoke($"스크린샷 촬영 실패: {e.Message}");
|
||||
}
|
||||
if (!CanCapture()) return;
|
||||
host.StartCoroutine(CaptureCoroutine(alphaOnly: false));
|
||||
}
|
||||
|
||||
public void CaptureAlphaScreenshot()
|
||||
{
|
||||
if (screenshotCamera == null)
|
||||
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("촬영할 카메라가 설정되지 않았습니다!");
|
||||
return;
|
||||
logError?.Invoke("RenderStreamOutput / MainCam 이 준비되지 않았습니다");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (alphaShader == null)
|
||||
private IEnumerator CaptureCoroutine(bool alphaOnly)
|
||||
{
|
||||
var cam = renderStream.MainCam;
|
||||
bool useTempResolution = captureWidth > 0 && captureHeight > 0;
|
||||
|
||||
RenderTexture tempRT = null;
|
||||
RenderTexture prevTarget = null;
|
||||
|
||||
if (useTempResolution)
|
||||
{
|
||||
logError?.Invoke("알파 셰이더가 설정되지 않았습니다!");
|
||||
return;
|
||||
}
|
||||
|
||||
string fileName = GenerateFileName("png", "_Alpha");
|
||||
string fullPath = Path.Combine(screenshotSavePath, fileName);
|
||||
|
||||
try
|
||||
{
|
||||
RenderTexture rt = new RenderTexture(screenshotWidth, screenshotHeight, 24);
|
||||
RenderTexture currentRT = screenshotCamera.targetTexture;
|
||||
|
||||
screenshotCamera.targetTexture = rt;
|
||||
screenshotCamera.Render();
|
||||
|
||||
Texture niloToonPrepassBuffer = Shader.GetGlobalTexture(niloToonPrepassBufferName);
|
||||
|
||||
if (niloToonPrepassBuffer == null)
|
||||
prevTarget = cam.targetTexture;
|
||||
var desc = new RenderTextureDescriptor(captureWidth, captureHeight,
|
||||
renderStream.TextureFormat, 24)
|
||||
{
|
||||
logError?.Invoke($"NiloToon Prepass 버퍼를 찾을 수 없습니다: {niloToonPrepassBufferName}");
|
||||
screenshotCamera.targetTexture = currentRT;
|
||||
UnityEngine.Object.Destroy(rt);
|
||||
return;
|
||||
}
|
||||
msaaSamples = Mathf.Max(1, renderStream.AntiAliasing)
|
||||
};
|
||||
tempRT = new RenderTexture(desc);
|
||||
tempRT.Create();
|
||||
|
||||
if (alphaMaterial == null)
|
||||
{
|
||||
alphaMaterial = new Material(alphaShader);
|
||||
}
|
||||
cam.targetTexture = tempRT;
|
||||
|
||||
RenderTexture alphaRT = new RenderTexture(screenshotWidth, screenshotHeight, 0, RenderTextureFormat.ARGB32);
|
||||
alphaMaterial.SetTexture("_MainTex", rt);
|
||||
alphaMaterial.SetTexture("_AlphaTex", niloToonPrepassBuffer);
|
||||
alphaMaterial.SetFloat("_BlurRadius", alphaBlurRadius);
|
||||
|
||||
Graphics.Blit(rt, alphaRT, alphaMaterial);
|
||||
|
||||
RenderTexture.active = alphaRT;
|
||||
Texture2D screenshot = new Texture2D(screenshotWidth, screenshotHeight, TextureFormat.RGBA32, false);
|
||||
screenshot.ReadPixels(new Rect(0, 0, screenshotWidth, screenshotHeight), 0, 0);
|
||||
screenshot.Apply();
|
||||
|
||||
byte[] bytes = screenshot.EncodeToPNG();
|
||||
File.WriteAllBytes(fullPath, bytes);
|
||||
|
||||
screenshotCamera.targetTexture = currentRT;
|
||||
RenderTexture.active = null;
|
||||
rt.Release();
|
||||
UnityEngine.Object.Destroy(rt);
|
||||
alphaRT.Release();
|
||||
UnityEngine.Object.Destroy(alphaRT);
|
||||
UnityEngine.Object.Destroy(screenshot);
|
||||
|
||||
log?.Invoke($"알파 스크린샷 저장 완료: {fullPath}");
|
||||
// 1프레임 — RenderStreamOutput.Update 가 새 pixelWidth 감지 → InitializeTextures(고해상도)
|
||||
yield return null;
|
||||
// 1프레임 — 고해상도 RT 에 우리 패스 결과 채워짐
|
||||
yield return new WaitForEndOfFrame();
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
var srcRT = alphaOnly ? renderStream.AlphaOutputTexture : renderStream.CaptureTexture;
|
||||
if (srcRT != null)
|
||||
{
|
||||
logError?.Invoke($"알파 스크린샷 촬영 실패: {e.Message}");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,6 +144,33 @@ public class ScreenshotManager
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
@ -203,9 +179,6 @@ public class ScreenshotManager
|
||||
|
||||
public void Cleanup()
|
||||
{
|
||||
if (alphaMaterial != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(alphaMaterial);
|
||||
}
|
||||
// RenderStreamOutput 이 모든 RT/Material lifecycle 을 관리하므로 별도 정리 없음
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ public class SystemController : MonoBehaviour
|
||||
optiTrack.Initialize(Log, LogError);
|
||||
facialMotion.Initialize(Log, LogError);
|
||||
motionRecording.Initialize(optiTrack, Log, LogError);
|
||||
screenshot.Initialize(Log, LogError);
|
||||
screenshot.Initialize(this, Log, LogError);
|
||||
clothSimulation.Initialize(Log, LogError);
|
||||
avatarHead.Initialize(Log, LogError);
|
||||
retargetingRemote.Initialize(Log, LogError);
|
||||
|
||||
@ -29,14 +29,12 @@
|
||||
<!-- 스크린샷 -->
|
||||
<ui:VisualElement class="section">
|
||||
<ui:Foldout text="스크린샷" value="true" class="section-foldout">
|
||||
<uie:PropertyField binding-path="screenshot.screenshotCamera" label="카메라" tooltip="촬영할 카메라 (비어있으면 메인 카메라)"/>
|
||||
<uie:PropertyField binding-path="screenshot.screenshotWidth" label="너비"/>
|
||||
<uie:PropertyField binding-path="screenshot.screenshotHeight" label="높이"/>
|
||||
<ui:HelpBox message-type="Info" text="알파 합성 결과는 RenderStreamOutput 컴포넌트의 CaptureTexture / AlphaOutputTexture 를 그대로 PNG 로 저장합니다. 해상도/셰이더/블러 설정은 RenderStreamOutput 에서 조절."/>
|
||||
<uie:PropertyField binding-path="screenshot.renderStream" label="RenderStreamOutput" tooltip="비어있으면 씬에서 자동 검색"/>
|
||||
<uie:PropertyField binding-path="screenshot.screenshotSavePath" label="저장 경로" tooltip="비어있으면 프로젝트 루트/Screenshots"/>
|
||||
<uie:PropertyField binding-path="screenshot.screenshotFilePrefix" label="파일 접두사"/>
|
||||
<uie:PropertyField binding-path="screenshot.alphaShader" label="알파 셰이더"/>
|
||||
<uie:PropertyField binding-path="screenshot.niloToonPrepassBufferName" label="NiloToon 버퍼 이름"/>
|
||||
<uie:PropertyField binding-path="screenshot.alphaBlurRadius" label="알파 블러 반경"/>
|
||||
<uie:PropertyField binding-path="screenshot.captureWidth" label="캡처 너비" tooltip="0 이면 카메라 해상도 사용. >0 이면 한 프레임 동안 카메라를 그 해상도로 변경 (Spout 도 잠깐 같은 해상도가 됨)"/>
|
||||
<uie:PropertyField binding-path="screenshot.captureHeight" label="캡처 높이" tooltip="0 이면 카메라 해상도 사용"/>
|
||||
</ui:Foldout>
|
||||
</ui:VisualElement>
|
||||
|
||||
|
||||
8
Assets/StreamingAssets.meta
Normal file
8
Assets/StreamingAssets.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 125f274339c271348b3ac9e905d1b197
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/StreamingAssets/SpoutNdiNormalizer.meta
Normal file
8
Assets/StreamingAssets/SpoutNdiNormalizer.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd5ef184cfe270f44b04b8d20b5362e5
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/StreamingAssets/SpoutNdiNormalizer/README.md
(Stored with Git LFS)
Normal file
BIN
Assets/StreamingAssets/SpoutNdiNormalizer/README.md
(Stored with Git LFS)
Normal file
Binary file not shown.
7
Assets/StreamingAssets/SpoutNdiNormalizer/README.md.meta
Normal file
7
Assets/StreamingAssets/SpoutNdiNormalizer/README.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 556dd080c71356944abc6a238e97d8cc
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c169e09324be3404dbef88e305616fd4
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
x
Reference in New Issue
Block a user