diff --git a/Assets/Resources/Settings/Streamingle Render Pipeline Asset.asset b/Assets/Resources/Settings/Streamingle Render Pipeline Asset.asset index 11b1e2e8b..a5e4f654a 100644 --- a/Assets/Resources/Settings/Streamingle Render Pipeline Asset.asset +++ b/Assets/Resources/Settings/Streamingle Render Pipeline Asset.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f3eb69a4c83b17433d63428649d12ce4c3ea55ffcdd9888f4c106ca9b3d6aba +oid sha256:4d326ded8a0104f2468de0dd7395f91c8ec505783ac22e85295f0d9953b3dda5 size 4686 diff --git a/Assets/Scripts/SpoutOutputScript/RenderStreamOutput.cs b/Assets/Scripts/SpoutOutputScript/RenderStreamOutput.cs index 6d7209640..bb1ba1ae5 100644 --- a/Assets/Scripts/SpoutOutputScript/RenderStreamOutput.cs +++ b/Assets/Scripts/SpoutOutputScript/RenderStreamOutput.cs @@ -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(); if (Sender == null) - { Sender = gameObject.AddComponent(); - 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(); if (NdiSender == null) - { - Debug.Log("NDI 송신 컴포넌트를 생성합니다."); NdiSender = gameObject.AddComponent(); - 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(); + } } 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 { - 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(); + 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(); + 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); } } } diff --git a/Assets/Scripts/SpoutOutputScript/Shaders/StreamingleAlphaCompose.shader b/Assets/Scripts/SpoutOutputScript/Shaders/StreamingleAlphaCompose.shader new file mode 100644 index 000000000..1f0e644e7 --- /dev/null +++ b/Assets/Scripts/SpoutOutputScript/Shaders/StreamingleAlphaCompose.shader @@ -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 +} diff --git a/Assets/Scripts/SpoutOutputScript/Shaders/StreamingleAlphaCompose.shader.meta b/Assets/Scripts/SpoutOutputScript/Shaders/StreamingleAlphaCompose.shader.meta new file mode 100644 index 000000000..faaee268c --- /dev/null +++ b/Assets/Scripts/SpoutOutputScript/Shaders/StreamingleAlphaCompose.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 9d778847d83c98846bc8e7c220f88356 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SpoutOutputScript/Shaders/StreamingleAlphaOnly.shader b/Assets/Scripts/SpoutOutputScript/Shaders/StreamingleAlphaOnly.shader new file mode 100644 index 000000000..dc75544d3 --- /dev/null +++ b/Assets/Scripts/SpoutOutputScript/Shaders/StreamingleAlphaOnly.shader @@ -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 +} diff --git a/Assets/Scripts/SpoutOutputScript/Shaders/StreamingleAlphaOnly.shader.meta b/Assets/Scripts/SpoutOutputScript/Shaders/StreamingleAlphaOnly.shader.meta new file mode 100644 index 000000000..c9733a7f0 --- /dev/null +++ b/Assets/Scripts/SpoutOutputScript/Shaders/StreamingleAlphaOnly.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: 80cfe82978045b341b4aa611843ae050 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/System/ScreenshotManager.cs b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/System/ScreenshotManager.cs index 030e3ae86..059c034c1 100644 --- a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/System/ScreenshotManager.cs +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/System/ScreenshotManager.cs @@ -1,67 +1,54 @@ using UnityEngine; using System; +using System.Collections; using System.IO; /// -/// 스크린샷 캡처 관리 (RGB + 알파채널) +/// 스크린샷 캡처 관리. +/// RenderStreamOutput 이 매 프레임 갱신하는 CaptureTexture/AlphaOutputTexture 를 PNG 로 저장. +/// captureWidth/Height 가 지정되면 캡처 직전에 카메라 targetTexture 를 임시로 그 해상도로 +/// 변경해 한 프레임 고해상도 렌더 후 원복한다. +/// 트레이드오프: 그 한 프레임 동안 Spout/NDI 송신도 같은 해상도가 되어 수신측이 잠깐 깜빡임. /// [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 log; private Action logError; - public void Initialize(Action log, Action logError) + public void Initialize(MonoBehaviour host, Action log, Action logError) { + this.host = host; this.log = log; this.logError = logError; - if (screenshotCamera == null) - { - screenshotCamera = Camera.main; - } + if (renderStream == null) + renderStream = UnityEngine.Object.FindFirstObjectByType(); - 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 을 관리하므로 별도 정리 없음 } } diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/SystemController.cs b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/SystemController.cs index 7235661d6..420acb612 100644 --- a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/SystemController.cs +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/SystemController.cs @@ -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); diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/SystemControllerEditor.uxml b/Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/SystemControllerEditor.uxml index 73632c412..99eeb3b40 100644 --- a/Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/SystemControllerEditor.uxml +++ b/Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/SystemControllerEditor.uxml @@ -29,14 +29,12 @@ - - - + + - - - + + diff --git a/Assets/StreamingAssets.meta b/Assets/StreamingAssets.meta new file mode 100644 index 000000000..f6639edae --- /dev/null +++ b/Assets/StreamingAssets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 125f274339c271348b3ac9e905d1b197 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/SpoutNdiNormalizer.meta b/Assets/StreamingAssets/SpoutNdiNormalizer.meta new file mode 100644 index 000000000..adaa6bae7 --- /dev/null +++ b/Assets/StreamingAssets/SpoutNdiNormalizer.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bd5ef184cfe270f44b04b8d20b5362e5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/SpoutNdiNormalizer/README.md b/Assets/StreamingAssets/SpoutNdiNormalizer/README.md new file mode 100644 index 000000000..363b53c2e --- /dev/null +++ b/Assets/StreamingAssets/SpoutNdiNormalizer/README.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52075f10280b08adb4aa259a72479da0c6bbae7e80705b36346144d8bdb13feb +size 4242 diff --git a/Assets/StreamingAssets/SpoutNdiNormalizer/README.md.meta b/Assets/StreamingAssets/SpoutNdiNormalizer/README.md.meta new file mode 100644 index 000000000..918df58df --- /dev/null +++ b/Assets/StreamingAssets/SpoutNdiNormalizer/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 556dd080c71356944abc6a238e97d8cc +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/StreamingAssets/SpoutNdiNormalizer/spout_ndi_normalizer.exe b/Assets/StreamingAssets/SpoutNdiNormalizer/spout_ndi_normalizer.exe new file mode 100644 index 000000000..c20220776 Binary files /dev/null and b/Assets/StreamingAssets/SpoutNdiNormalizer/spout_ndi_normalizer.exe differ diff --git a/Assets/StreamingAssets/SpoutNdiNormalizer/spout_ndi_normalizer.exe.meta b/Assets/StreamingAssets/SpoutNdiNormalizer/spout_ndi_normalizer.exe.meta new file mode 100644 index 000000000..35759c1fc --- /dev/null +++ b/Assets/StreamingAssets/SpoutNdiNormalizer/spout_ndi_normalizer.exe.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c169e09324be3404dbef88e305616fd4 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: