- 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>
651 lines
23 KiB
C#
651 lines
23 KiB
C#
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
|
|
{
|
|
None,
|
|
Spout,
|
|
NDI,
|
|
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("출력 설정")]
|
|
[Tooltip("출력 방식 선택 (Spout, NDI, 또는 둘 다)")]
|
|
public OutputMethod outputMethod = OutputMethod.Spout;
|
|
[Tooltip("알파 채널 유지")]
|
|
public bool keepAlpha = true;
|
|
|
|
[Header("카메라 설정")]
|
|
[Tooltip("렌더링 소스로 사용할 카메라")]
|
|
public Camera MainCam;
|
|
|
|
[Header("Spout 설정")]
|
|
[Tooltip("Spout 송신 컴포넌트")]
|
|
public SpoutSender Sender;
|
|
[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 RenderTextureFormat TextureFormat = RenderTextureFormat.DefaultHDR;
|
|
[Tooltip("안티앨리어싱 레벨 (1, 2, 4, 8)")]
|
|
[Range(1, 8)]
|
|
public int AntiAliasing = 1;
|
|
|
|
[Header("프레임 제한")]
|
|
[Tooltip("타겟 프레임레이트 (-1: 제한 없음). VSync 는 자동으로 꺼짐")]
|
|
public int targetFrameRate = 75;
|
|
|
|
[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()
|
|
{
|
|
if (outputMethod == OutputMethod.Spout || outputMethod == OutputMethod.Both)
|
|
{
|
|
if (Sender == null)
|
|
{
|
|
Sender = GetComponent<SpoutSender>();
|
|
if (Sender == null)
|
|
Sender = gameObject.AddComponent<SpoutSender>();
|
|
}
|
|
}
|
|
|
|
if (outputMethod == OutputMethod.NDI || outputMethod == OutputMethod.Both)
|
|
{
|
|
if (NdiSender == null)
|
|
{
|
|
NdiSender = GetComponent<NdiSender>();
|
|
if (NdiSender == null)
|
|
NdiSender = gameObject.AddComponent<NdiSender>();
|
|
}
|
|
}
|
|
|
|
if (AlphaSender == null)
|
|
{
|
|
// 같은 GameObject 에 두 번째 SpoutSender 추가
|
|
AlphaSender = gameObject.AddComponent<SpoutSender>();
|
|
}
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
InitializeScriptablePasses();
|
|
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
|
|
}
|
|
|
|
private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
|
|
{
|
|
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()
|
|
{
|
|
if (MainCam == null)
|
|
{
|
|
Debug.LogError("MainCam이 설정되지 않았습니다.");
|
|
return;
|
|
}
|
|
|
|
screenWidth = MainCam.pixelWidth;
|
|
screenHeight = MainCam.pixelHeight;
|
|
|
|
var desc = new RenderTextureDescriptor(screenWidth, screenHeight, TextureFormat, 0)
|
|
{
|
|
msaaSamples = Mathf.Max(1, AntiAliasing)
|
|
};
|
|
|
|
ReleaseRT(ref CaptureTexture, ref m_CaptureHandle);
|
|
CaptureTexture = new RenderTexture(desc);
|
|
CaptureTexture.Create();
|
|
m_CaptureHandle = RTHandles.Alloc(CaptureTexture, transferOwnership: false);
|
|
|
|
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();
|
|
}
|
|
|
|
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()
|
|
{
|
|
if (outputMethod == OutputMethod.Spout || outputMethod == OutputMethod.Both)
|
|
{
|
|
if (Sender != null)
|
|
{
|
|
Sender.enabled = true;
|
|
Sender.sourceTexture = CaptureTexture;
|
|
Sender.spoutName = spoutSenderName;
|
|
Sender.keepAlpha = keepAlpha;
|
|
Sender.captureMethod = Klak.Spout.CaptureMethod.Texture;
|
|
}
|
|
}
|
|
else if (Sender != null)
|
|
{
|
|
Sender.enabled = false;
|
|
}
|
|
|
|
if (outputMethod == OutputMethod.NDI || outputMethod == OutputMethod.Both)
|
|
{
|
|
if (NdiSender != null)
|
|
{
|
|
NdiSender.enabled = true;
|
|
NdiSender.sourceTexture = CaptureTexture;
|
|
NdiSender.ndiName = ndiSenderName;
|
|
NdiSender.keepAlpha = keepAlpha;
|
|
NdiSender.captureMethod = Klak.Ndi.CaptureMethod.Texture;
|
|
}
|
|
else
|
|
{
|
|
InitializeOutputComponents();
|
|
}
|
|
}
|
|
else if (NdiSender != null)
|
|
{
|
|
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 InitializeScriptablePasses()
|
|
{
|
|
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()
|
|
{
|
|
if (MainCam != null && (screenWidth != MainCam.pixelWidth || screenHeight != MainCam.pixelHeight))
|
|
{
|
|
InitializeTextures();
|
|
}
|
|
|
|
if (previousOutputMethod != outputMethod || previousKeepAlpha != keepAlpha)
|
|
{
|
|
previousOutputMethod = outputMethod;
|
|
previousKeepAlpha = keepAlpha;
|
|
InitializeOutputComponents();
|
|
UpdateOutputSources();
|
|
}
|
|
|
|
if (previousTargetFrameRate != targetFrameRate)
|
|
{
|
|
previousTargetFrameRate = targetFrameRate;
|
|
ApplyFrameRate();
|
|
}
|
|
|
|
PushMaterialParams();
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
m_PreCapturePass = null;
|
|
m_ComposePass = null;
|
|
m_AlphaOutputPass = null;
|
|
|
|
ReleaseRT(ref CaptureTexture, ref m_CaptureHandle);
|
|
ReleaseRT(ref m_PreColorTexture, ref m_PreColorHandle);
|
|
ReleaseRT(ref AlphaOutputTexture, ref m_AlphaOutputHandle);
|
|
|
|
if (m_AlphaComposeMaterial != null)
|
|
{
|
|
DestroyImmediate(m_AlphaComposeMaterial);
|
|
m_AlphaComposeMaterial = null;
|
|
}
|
|
if (m_AlphaOnlyMaterial != null)
|
|
{
|
|
DestroyImmediate(m_AlphaOnlyMaterial);
|
|
m_AlphaOnlyMaterial = null;
|
|
}
|
|
|
|
KillNormalizer();
|
|
}
|
|
|
|
// ───────────────────────── Spout/NDI Normalizer (외부 exe) ─────────────────────────
|
|
|
|
private string ResolveNormalizerExePath()
|
|
{
|
|
if (!string.IsNullOrEmpty(normalizerExeAbsolutePath))
|
|
return normalizerExeAbsolutePath;
|
|
var rel = normalizerExeRelativePath.Replace('/', Path.DirectorySeparatorChar);
|
|
return Path.Combine(Application.streamingAssetsPath, rel);
|
|
}
|
|
|
|
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_NormalizerProc != null && !m_NormalizerProc.HasExited)
|
|
{
|
|
m_NormalizerProc.Kill();
|
|
m_NormalizerProc.WaitForExit(2000);
|
|
}
|
|
}
|
|
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>
|
|
{
|
|
"--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);
|
|
}
|
|
}
|
|
}
|