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;
|
||||||
using UnityEngine.Rendering;
|
using UnityEngine.Rendering;
|
||||||
using UnityEngine.Rendering.Universal;
|
using UnityEngine.Rendering.Universal;
|
||||||
|
using UnityEngine.Rendering.RenderGraphModule;
|
||||||
|
using UnityEngine.Rendering.RenderGraphModule.Util;
|
||||||
using Klak.Spout;
|
using Klak.Spout;
|
||||||
using Klak.Ndi;
|
using Klak.Ndi;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Debug = UnityEngine.Debug;
|
||||||
|
|
||||||
// 출력 방식 열거형
|
// 출력 방식 열거형
|
||||||
public enum OutputMethod
|
public enum OutputMethod
|
||||||
@ -14,6 +19,16 @@ public enum OutputMethod
|
|||||||
Both
|
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
|
public class RenderStreamOutput : MonoBehaviour
|
||||||
{
|
{
|
||||||
[Header("출력 설정")]
|
[Header("출력 설정")]
|
||||||
@ -32,99 +47,212 @@ public class RenderStreamOutput : MonoBehaviour
|
|||||||
[Tooltip("Spout 송신 이름")]
|
[Tooltip("Spout 송신 이름")]
|
||||||
public string spoutSenderName = "Streamingle Spout Output";
|
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 설정")]
|
[Header("NDI 설정")]
|
||||||
[Tooltip("NDI 송신 컴포넌트")]
|
[Tooltip("NDI 송신 컴포넌트")]
|
||||||
public NdiSender NdiSender;
|
public NdiSender NdiSender;
|
||||||
[Tooltip("NDI 송신 이름")]
|
[Tooltip("NDI 송신 이름")]
|
||||||
public string ndiSenderName = "Streamingle NDI Output";
|
public string ndiSenderName = "Streamingle NDI Output";
|
||||||
|
|
||||||
[Header("쉐이더 설정")]
|
|
||||||
[Tooltip("렌더링 결과를 처리할 쉐이더 머티리얼")]
|
|
||||||
public Material ShaderContral;
|
|
||||||
|
|
||||||
[Header("텍스처 설정")]
|
[Header("텍스처 설정")]
|
||||||
[Tooltip("텍스처 포맷 설정")]
|
[Tooltip("텍스처 포맷 설정")]
|
||||||
public RenderTextureFormat TextureFormat = RenderTextureFormat.DefaultHDR;
|
public RenderTextureFormat TextureFormat = RenderTextureFormat.DefaultHDR;
|
||||||
[Tooltip("깊이 버퍼 비트 수")]
|
|
||||||
public int DepthBuffer = 24;
|
|
||||||
[Tooltip("안티앨리어싱 레벨 (1, 2, 4, 8)")]
|
[Tooltip("안티앨리어싱 레벨 (1, 2, 4, 8)")]
|
||||||
[Range(1, 8)]
|
[Range(1, 8)]
|
||||||
public int AntiAliasing = 1;
|
public int AntiAliasing = 1;
|
||||||
|
|
||||||
[HideInInspector] public CustomRenderTexture ShaderTexture = null;
|
[Header("프레임 제한")]
|
||||||
[HideInInspector] public RenderTexture ShaderCameraTexture = null;
|
[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 screenWidth;
|
||||||
private int screenHeight;
|
private int screenHeight;
|
||||||
|
|
||||||
// 이전 출력 방식 저장
|
|
||||||
private OutputMethod previousOutputMethod;
|
private OutputMethod previousOutputMethod;
|
||||||
// 이전 알파 채널 설정 저장
|
|
||||||
private bool previousKeepAlpha;
|
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()
|
private void Awake()
|
||||||
{
|
{
|
||||||
// 컴포넌트 자동 할당
|
|
||||||
MainCam = MainCam == null ? Camera.main : MainCam;
|
MainCam = MainCam == null ? Camera.main : MainCam;
|
||||||
|
|
||||||
// 초기 설정 저장
|
|
||||||
previousOutputMethod = outputMethod;
|
previousOutputMethod = outputMethod;
|
||||||
previousKeepAlpha = keepAlpha;
|
previousKeepAlpha = keepAlpha;
|
||||||
|
previousTargetFrameRate = targetFrameRate;
|
||||||
|
|
||||||
// 출력 컴포넌트 초기화
|
ApplyFrameRate();
|
||||||
|
EnsureMaterial();
|
||||||
|
EnsureAlphaOnlyMaterial();
|
||||||
InitializeOutputComponents();
|
InitializeOutputComponents();
|
||||||
|
|
||||||
InitializeTextures();
|
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()
|
private void InitializeOutputComponents()
|
||||||
{
|
{
|
||||||
// Spout 송신 컴포넌트 초기화
|
|
||||||
if (outputMethod == OutputMethod.Spout || outputMethod == OutputMethod.Both)
|
if (outputMethod == OutputMethod.Spout || outputMethod == OutputMethod.Both)
|
||||||
{
|
{
|
||||||
if (Sender == null)
|
if (Sender == null)
|
||||||
{
|
{
|
||||||
Sender = GetComponent<SpoutSender>();
|
Sender = GetComponent<SpoutSender>();
|
||||||
if (Sender == null)
|
if (Sender == null)
|
||||||
{
|
|
||||||
Sender = gameObject.AddComponent<SpoutSender>();
|
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 (outputMethod == OutputMethod.NDI || outputMethod == OutputMethod.Both)
|
||||||
{
|
{
|
||||||
if (NdiSender == null)
|
if (NdiSender == null)
|
||||||
{
|
{
|
||||||
NdiSender = GetComponent<NdiSender>();
|
NdiSender = GetComponent<NdiSender>();
|
||||||
if (NdiSender == null)
|
if (NdiSender == null)
|
||||||
{
|
|
||||||
Debug.Log("NDI 송신 컴포넌트를 생성합니다.");
|
|
||||||
NdiSender = gameObject.AddComponent<NdiSender>();
|
NdiSender = gameObject.AddComponent<NdiSender>();
|
||||||
if (NdiSender != null)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AlphaSender == null)
|
||||||
{
|
{
|
||||||
NdiSender.ndiName = ndiSenderName;
|
// 같은 GameObject 에 두 번째 SpoutSender 추가
|
||||||
NdiSender.keepAlpha = keepAlpha;
|
AlphaSender = gameObject.AddComponent<SpoutSender>();
|
||||||
NdiSender.captureMethod = Klak.Ndi.CaptureMethod.Texture;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogError("NDI 송신 컴포넌트 생성 실패.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEnable()
|
private void OnEnable()
|
||||||
{
|
{
|
||||||
InitializeScriptablePass();
|
InitializeScriptablePasses();
|
||||||
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
|
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,77 +263,76 @@ public class RenderStreamOutput : MonoBehaviour
|
|||||||
|
|
||||||
private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
|
private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
|
||||||
{
|
{
|
||||||
if (camera == MainCam)
|
if (camera != MainCam)
|
||||||
{
|
return;
|
||||||
|
|
||||||
var data = camera.GetUniversalAdditionalCameraData();
|
var data = camera.GetUniversalAdditionalCameraData();
|
||||||
data.scriptableRenderer.EnqueuePass(m_ScriptablePass);
|
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()
|
private void InitializeTextures()
|
||||||
{
|
{
|
||||||
// 카메라가 null인 경우 체크
|
|
||||||
if (MainCam == null)
|
if (MainCam == null)
|
||||||
{
|
{
|
||||||
Debug.LogError("MainCam이 설정되지 않았습니다.");
|
Debug.LogError("MainCam이 설정되지 않았습니다.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShaderContral이 null인 경우 체크
|
|
||||||
if (ShaderContral == null)
|
|
||||||
{
|
|
||||||
Debug.LogError("ShaderContral 머티리얼이 설정되지 않았습니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 카메라의 실제 해상도 사용
|
|
||||||
screenWidth = MainCam.pixelWidth;
|
screenWidth = MainCam.pixelWidth;
|
||||||
screenHeight = MainCam.pixelHeight;
|
screenHeight = MainCam.pixelHeight;
|
||||||
|
|
||||||
// ShaderTexture 초기화 - 메모리 누수 방지를 위한 확실한 정리
|
var desc = new RenderTextureDescriptor(screenWidth, screenHeight, TextureFormat, 0)
|
||||||
if (ShaderTexture != null)
|
|
||||||
{
|
{
|
||||||
ShaderTexture.Release();
|
msaaSamples = Mathf.Max(1, AntiAliasing)
|
||||||
DestroyImmediate(ShaderTexture); // Destroy 대신 DestroyImmediate 사용
|
};
|
||||||
}
|
|
||||||
ShaderTexture = new CustomRenderTexture(screenWidth, screenHeight, TextureFormat);
|
|
||||||
ShaderTexture.material = ShaderContral;
|
|
||||||
ShaderTexture.updateMode = CustomRenderTextureUpdateMode.Realtime;
|
|
||||||
ShaderTexture.antiAliasing = AntiAliasing;
|
|
||||||
|
|
||||||
// ShaderCameraTexture 초기화
|
ReleaseRT(ref CaptureTexture, ref m_CaptureHandle);
|
||||||
if (ShaderCameraTexture != null)
|
CaptureTexture = new RenderTexture(desc);
|
||||||
{
|
CaptureTexture.Create();
|
||||||
ShaderCameraTexture.Release();
|
m_CaptureHandle = RTHandles.Alloc(CaptureTexture, transferOwnership: false);
|
||||||
DestroyImmediate(ShaderCameraTexture); // Destroy 대신 DestroyImmediate 사용
|
|
||||||
}
|
|
||||||
ShaderCameraTexture = new RenderTexture(screenWidth, screenHeight, DepthBuffer, TextureFormat);
|
|
||||||
ShaderCameraTexture.Create();
|
|
||||||
ShaderCameraTexture.antiAliasing = AntiAliasing;
|
|
||||||
|
|
||||||
// ShaderCameraTexture를 ShaderContral의 _MainTex에 할당
|
ReleaseRT(ref m_PreColorTexture, ref m_PreColorHandle);
|
||||||
if (ShaderContral != null)
|
m_PreColorTexture = new RenderTexture(desc);
|
||||||
{
|
m_PreColorTexture.Create();
|
||||||
ShaderContral.SetTexture("_MainTex", ShaderCameraTexture);
|
m_PreColorHandle = RTHandles.Alloc(m_PreColorTexture, transferOwnership: false);
|
||||||
UpdateShaderScreenSize();
|
|
||||||
}
|
ReleaseRT(ref AlphaOutputTexture, ref m_AlphaOutputHandle);
|
||||||
|
AlphaOutputTexture = new RenderTexture(desc);
|
||||||
|
AlphaOutputTexture.Create();
|
||||||
|
m_AlphaOutputHandle = RTHandles.Alloc(AlphaOutputTexture, transferOwnership: false);
|
||||||
|
|
||||||
// 출력 방식에 따라 텍스처 할당
|
|
||||||
UpdateOutputSources();
|
UpdateOutputSources();
|
||||||
|
InitializeScriptablePasses();
|
||||||
|
}
|
||||||
|
|
||||||
// m_ScriptablePass 재초기화
|
private static void ReleaseRT(ref RenderTexture rt, ref RTHandle handle)
|
||||||
InitializeScriptablePass();
|
{
|
||||||
|
if (handle != null)
|
||||||
|
{
|
||||||
|
handle.Release();
|
||||||
|
handle = null;
|
||||||
|
}
|
||||||
|
if (rt != null)
|
||||||
|
{
|
||||||
|
rt.Release();
|
||||||
|
DestroyImmediate(rt);
|
||||||
|
rt = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateOutputSources()
|
private void UpdateOutputSources()
|
||||||
{
|
{
|
||||||
// Spout 출력 설정
|
|
||||||
if (outputMethod == OutputMethod.Spout || outputMethod == OutputMethod.Both)
|
if (outputMethod == OutputMethod.Spout || outputMethod == OutputMethod.Both)
|
||||||
{
|
{
|
||||||
if (Sender != null)
|
if (Sender != null)
|
||||||
{
|
{
|
||||||
Sender.enabled = true;
|
Sender.enabled = true;
|
||||||
Sender.sourceTexture = ShaderTexture;
|
Sender.sourceTexture = CaptureTexture;
|
||||||
Sender.spoutName = spoutSenderName;
|
Sender.spoutName = spoutSenderName;
|
||||||
Sender.keepAlpha = keepAlpha;
|
Sender.keepAlpha = keepAlpha;
|
||||||
Sender.captureMethod = Klak.Spout.CaptureMethod.Texture;
|
Sender.captureMethod = Klak.Spout.CaptureMethod.Texture;
|
||||||
@ -216,20 +343,18 @@ public class RenderStreamOutput : MonoBehaviour
|
|||||||
Sender.enabled = false;
|
Sender.enabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NDI 출력 설정
|
|
||||||
if (outputMethod == OutputMethod.NDI || outputMethod == OutputMethod.Both)
|
if (outputMethod == OutputMethod.NDI || outputMethod == OutputMethod.Both)
|
||||||
{
|
{
|
||||||
if (NdiSender != null)
|
if (NdiSender != null)
|
||||||
{
|
{
|
||||||
NdiSender.enabled = true;
|
NdiSender.enabled = true;
|
||||||
NdiSender.sourceTexture = ShaderTexture;
|
NdiSender.sourceTexture = CaptureTexture;
|
||||||
NdiSender.ndiName = ndiSenderName;
|
NdiSender.ndiName = ndiSenderName;
|
||||||
NdiSender.keepAlpha = keepAlpha;
|
NdiSender.keepAlpha = keepAlpha;
|
||||||
NdiSender.captureMethod = Klak.Ndi.CaptureMethod.Texture;
|
NdiSender.captureMethod = Klak.Ndi.CaptureMethod.Texture;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// NDI 송신 컴포넌트가 없으면 다시 초기화 시도
|
|
||||||
InitializeOutputComponents();
|
InitializeOutputComponents();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -237,27 +362,41 @@ public class RenderStreamOutput : MonoBehaviour
|
|||||||
{
|
{
|
||||||
NdiSender.enabled = false;
|
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_PreCapturePass = new PreCapturePass(m_PreColorHandle)
|
||||||
m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
|
{
|
||||||
|
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()
|
private void Update()
|
||||||
{
|
{
|
||||||
// 화면 크기 변경 체크 - 매 프레임마다 Screen.width/height 접근은 비용이 있음
|
|
||||||
// 변경이 자주 일어나지 않으므로 더 효율적인 방법으로 변경
|
|
||||||
if (MainCam != null && (screenWidth != MainCam.pixelWidth || screenHeight != MainCam.pixelHeight))
|
if (MainCam != null && (screenWidth != MainCam.pixelWidth || screenHeight != MainCam.pixelHeight))
|
||||||
{
|
{
|
||||||
screenWidth = MainCam.pixelWidth;
|
|
||||||
screenHeight = MainCam.pixelHeight;
|
|
||||||
InitializeTextures();
|
InitializeTextures();
|
||||||
UpdateShaderScreenSize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 설정이 변경되었는지 확인
|
|
||||||
if (previousOutputMethod != outputMethod || previousKeepAlpha != keepAlpha)
|
if (previousOutputMethod != outputMethod || previousKeepAlpha != keepAlpha)
|
||||||
{
|
{
|
||||||
previousOutputMethod = outputMethod;
|
previousOutputMethod = outputMethod;
|
||||||
@ -265,87 +404,247 @@ public class RenderStreamOutput : MonoBehaviour
|
|||||||
InitializeOutputComponents();
|
InitializeOutputComponents();
|
||||||
UpdateOutputSources();
|
UpdateOutputSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (previousTargetFrameRate != targetFrameRate)
|
||||||
|
{
|
||||||
|
previousTargetFrameRate = targetFrameRate;
|
||||||
|
ApplyFrameRate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateShaderScreenSize()
|
PushMaterialParams();
|
||||||
{
|
|
||||||
if (ShaderContral != null)
|
|
||||||
{
|
|
||||||
ShaderContral.SetVector("_Resolution", new Vector4(screenWidth, screenHeight, 0, 0));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
{
|
{
|
||||||
// m_ScriptablePass 정리 (내부 텍스처 참조 해제)
|
m_PreCapturePass = null;
|
||||||
m_ScriptablePass = 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(m_AlphaComposeMaterial);
|
||||||
DestroyImmediate(ShaderCameraTexture);
|
m_AlphaComposeMaterial = null;
|
||||||
ShaderCameraTexture = null; // 참조 제거
|
|
||||||
}
|
}
|
||||||
if (ShaderTexture != null)
|
if (m_AlphaOnlyMaterial != null)
|
||||||
{
|
{
|
||||||
ShaderTexture.Release();
|
DestroyImmediate(m_AlphaOnlyMaterial);
|
||||||
DestroyImmediate(ShaderTexture);
|
m_AlphaOnlyMaterial = null;
|
||||||
ShaderTexture = null; // 참조 제거
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AlphaRecodingRenderPass : ScriptableRenderPass
|
KillNormalizer();
|
||||||
{
|
|
||||||
private RenderTexture m_ShaderCameraTexture;
|
|
||||||
private Material m_ShaderContral;
|
|
||||||
private CustomRenderTexture m_ShaderTexture;
|
|
||||||
|
|
||||||
public AlphaRecodingRenderPass(RenderTexture shaderCameraTexture, Material shaderContral, CustomRenderTexture shaderTexture)
|
|
||||||
{
|
|
||||||
m_ShaderCameraTexture = shaderCameraTexture;
|
|
||||||
m_ShaderContral = shaderContral;
|
|
||||||
m_ShaderTexture = shaderTexture;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("This method is obsolete. Use the new Render Graph API instead.")]
|
// ───────────────────────── Spout/NDI Normalizer (외부 exe) ─────────────────────────
|
||||||
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
|
|
||||||
|
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
|
try
|
||||||
{
|
{
|
||||||
if (m_ShaderCameraTexture == null || m_ShaderContral == null || !m_ShaderCameraTexture.IsCreated())
|
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;
|
return;
|
||||||
|
|
||||||
CommandBuffer cmd = CommandBufferPool.Get("Alpha Recoding Pass");
|
var resourceData = frameData.Get<UniversalResourceData>();
|
||||||
|
if (resourceData == null)
|
||||||
|
return;
|
||||||
|
|
||||||
try
|
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))
|
||||||
{
|
{
|
||||||
// Update cameraColorTargetHandle usage
|
builder.AllowPassCulling(false);
|
||||||
var colorTarget = renderingData.cameraData.renderer.cameraColorTargetHandle;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 최종 렌더링 결과를 가져옵니다
|
// CaptureTexture 의 알파를 그레이스케일 RGB 로 변환해서 AlphaOutputTexture 로 출력
|
||||||
RenderTargetIdentifier source = colorTarget;
|
public class AlphaOutputPass : ScriptableRenderPass
|
||||||
cmd.Blit(source, m_ShaderCameraTexture);
|
|
||||||
|
|
||||||
// ShaderCameraTexture를 ShaderContral의 _MainTex에 할당
|
|
||||||
m_ShaderContral.SetTexture("_MainTex", m_ShaderCameraTexture);
|
|
||||||
|
|
||||||
// ShaderTexture 업데이트 (필요한 경우)
|
|
||||||
if (m_ShaderTexture != null)
|
|
||||||
{
|
{
|
||||||
m_ShaderTexture.Update();
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.ExecuteCommandBuffer(cmd);
|
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
{
|
||||||
CommandBufferPool.Release(cmd);
|
if (m_SourceHandle == null || m_DestHandle == null || m_Material == null)
|
||||||
}
|
return;
|
||||||
}
|
|
||||||
catch (System.Exception e)
|
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))
|
||||||
{
|
{
|
||||||
Debug.LogError($"Alpha Recoding 렌더 패스 실행 중 오류 발생: {e.Message}");
|
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 UnityEngine;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 스크린샷 캡처 관리 (RGB + 알파채널)
|
/// 스크린샷 캡처 관리.
|
||||||
|
/// RenderStreamOutput 이 매 프레임 갱신하는 CaptureTexture/AlphaOutputTexture 를 PNG 로 저장.
|
||||||
|
/// captureWidth/Height 가 지정되면 캡처 직전에 카메라 targetTexture 를 임시로 그 해상도로
|
||||||
|
/// 변경해 한 프레임 고해상도 렌더 후 원복한다.
|
||||||
|
/// 트레이드오프: 그 한 프레임 동안 Spout/NDI 송신도 같은 해상도가 되어 수신측이 잠깐 깜빡임.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class ScreenshotManager
|
public class ScreenshotManager
|
||||||
{
|
{
|
||||||
[Tooltip("스크린샷 해상도 (기본: 4K)")]
|
[Tooltip("RenderStreamOutput 컴포넌트 (비어있으면 씬에서 자동 검색)")]
|
||||||
public int screenshotWidth = 3840;
|
public RenderStreamOutput renderStream;
|
||||||
|
|
||||||
[Tooltip("스크린샷 해상도 (기본: 4K)")]
|
[Tooltip("스크린샷 저장 경로 (비어있으면 프로젝트 루트의 Screenshots/)")]
|
||||||
public int screenshotHeight = 2160;
|
|
||||||
|
|
||||||
[Tooltip("스크린샷 저장 경로 (비어있으면 바탕화면)")]
|
|
||||||
public string screenshotSavePath = "";
|
public string screenshotSavePath = "";
|
||||||
|
|
||||||
[Tooltip("파일명 앞에 붙을 접두사")]
|
[Tooltip("파일명 앞에 붙을 접두사")]
|
||||||
public string screenshotFilePrefix = "Screenshot";
|
public string screenshotFilePrefix = "Screenshot";
|
||||||
|
|
||||||
[Tooltip("알파 채널 추출용 셰이더")]
|
[Header("고해상도 캡처 (0 이면 카메라 해상도 사용)")]
|
||||||
public Shader alphaShader;
|
[Tooltip("캡처 너비. 0 이면 카메라 해상도 그대로")]
|
||||||
|
public int captureWidth = 3840;
|
||||||
|
[Tooltip("캡처 높이. 0 이면 카메라 해상도 그대로")]
|
||||||
|
public int captureHeight = 2160;
|
||||||
|
|
||||||
[Tooltip("NiloToon Prepass 버퍼 텍스처 이름")]
|
[Tooltip("(하위 호환) 외부 코드가 .screenshotCamera 로 접근하면 RenderStreamOutput.MainCam 반환")]
|
||||||
public string niloToonPrepassBufferName = "_NiloToonPrepassBufferTex";
|
public Camera screenshotCamera => renderStream != null ? renderStream.MainCam : null;
|
||||||
|
|
||||||
[Tooltip("촬영할 카메라 (비어있으면 메인 카메라 사용)")]
|
|
||||||
public Camera screenshotCamera;
|
|
||||||
|
|
||||||
[Tooltip("알파 채널 블러 반경 (0 = 블러 없음, 1.0 = 약한 블러)")]
|
|
||||||
[Range(0f, 3f)]
|
|
||||||
public float alphaBlurRadius = 1.0f;
|
|
||||||
|
|
||||||
[NonSerialized]
|
|
||||||
private Material alphaMaterial;
|
|
||||||
|
|
||||||
|
private MonoBehaviour host;
|
||||||
private Action<string> log;
|
private Action<string> log;
|
||||||
private Action<string> logError;
|
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.log = log;
|
||||||
this.logError = logError;
|
this.logError = logError;
|
||||||
|
|
||||||
if (screenshotCamera == null)
|
if (renderStream == null)
|
||||||
{
|
renderStream = UnityEngine.Object.FindFirstObjectByType<RenderStreamOutput>();
|
||||||
screenshotCamera = Camera.main;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alphaShader == null)
|
if (renderStream == null)
|
||||||
{
|
logError?.Invoke("RenderStreamOutput 컴포넌트를 찾을 수 없습니다 — 알파 합성 캡처 불가");
|
||||||
alphaShader = Shader.Find("Hidden/AlphaFromNiloToon");
|
|
||||||
if (alphaShader == null)
|
|
||||||
{
|
|
||||||
logError?.Invoke("알파 셰이더를 찾을 수 없습니다: Hidden/AlphaFromNiloToon");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(screenshotSavePath))
|
if (string.IsNullOrEmpty(screenshotSavePath))
|
||||||
{
|
|
||||||
screenshotSavePath = Path.Combine(Application.dataPath, "..", "Screenshots");
|
screenshotSavePath = Path.Combine(Application.dataPath, "..", "Screenshots");
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists(screenshotSavePath))
|
if (!Directory.Exists(screenshotSavePath))
|
||||||
{
|
{
|
||||||
@ -72,113 +59,75 @@ public class ScreenshotManager
|
|||||||
|
|
||||||
public void CaptureScreenshot()
|
public void CaptureScreenshot()
|
||||||
{
|
{
|
||||||
if (screenshotCamera == null)
|
if (!CanCapture()) return;
|
||||||
{
|
host.StartCoroutine(CaptureCoroutine(alphaOnly: false));
|
||||||
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}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CaptureAlphaScreenshot()
|
public void CaptureAlphaScreenshot()
|
||||||
{
|
{
|
||||||
if (screenshotCamera == null)
|
if (!CanCapture()) return;
|
||||||
{
|
host.StartCoroutine(CaptureCoroutine(alphaOnly: true));
|
||||||
logError?.Invoke("촬영할 카메라가 설정되지 않았습니다!");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alphaShader == null)
|
private bool CanCapture()
|
||||||
{
|
{
|
||||||
logError?.Invoke("알파 셰이더가 설정되지 않았습니다!");
|
if (host == null) { logError?.Invoke("ScreenshotManager host(MonoBehaviour) 가 없습니다"); return false; }
|
||||||
return;
|
if (renderStream == null || renderStream.MainCam == null)
|
||||||
|
{
|
||||||
|
logError?.Invoke("RenderStreamOutput / MainCam 이 준비되지 않았습니다");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
string fileName = GenerateFileName("png", "_Alpha");
|
private IEnumerator CaptureCoroutine(bool alphaOnly)
|
||||||
string fullPath = Path.Combine(screenshotSavePath, fileName);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
RenderTexture rt = new RenderTexture(screenshotWidth, screenshotHeight, 24);
|
var cam = renderStream.MainCam;
|
||||||
RenderTexture currentRT = screenshotCamera.targetTexture;
|
bool useTempResolution = captureWidth > 0 && captureHeight > 0;
|
||||||
|
|
||||||
screenshotCamera.targetTexture = rt;
|
RenderTexture tempRT = null;
|
||||||
screenshotCamera.Render();
|
RenderTexture prevTarget = null;
|
||||||
|
|
||||||
Texture niloToonPrepassBuffer = Shader.GetGlobalTexture(niloToonPrepassBufferName);
|
if (useTempResolution)
|
||||||
|
|
||||||
if (niloToonPrepassBuffer == null)
|
|
||||||
{
|
{
|
||||||
logError?.Invoke($"NiloToon Prepass 버퍼를 찾을 수 없습니다: {niloToonPrepassBufferName}");
|
prevTarget = cam.targetTexture;
|
||||||
screenshotCamera.targetTexture = currentRT;
|
var desc = new RenderTextureDescriptor(captureWidth, captureHeight,
|
||||||
UnityEngine.Object.Destroy(rt);
|
renderStream.TextureFormat, 24)
|
||||||
return;
|
{
|
||||||
|
msaaSamples = Mathf.Max(1, renderStream.AntiAliasing)
|
||||||
|
};
|
||||||
|
tempRT = new RenderTexture(desc);
|
||||||
|
tempRT.Create();
|
||||||
|
|
||||||
|
cam.targetTexture = tempRT;
|
||||||
|
|
||||||
|
// 1프레임 — RenderStreamOutput.Update 가 새 pixelWidth 감지 → InitializeTextures(고해상도)
|
||||||
|
yield return null;
|
||||||
|
// 1프레임 — 고해상도 RT 에 우리 패스 결과 채워짐
|
||||||
|
yield return new WaitForEndOfFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alphaMaterial == null)
|
var srcRT = alphaOnly ? renderStream.AlphaOutputTexture : renderStream.CaptureTexture;
|
||||||
|
if (srcRT != null)
|
||||||
{
|
{
|
||||||
alphaMaterial = new Material(alphaShader);
|
string fileName = GenerateFileName("png", alphaOnly ? "_Alpha" : "");
|
||||||
|
SaveRtAsPng(srcRT, fileName, alphaOnly ? TextureFormat.RGB24 : TextureFormat.RGBA32);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logError?.Invoke("캡처 RT 가 준비되지 않았습니다");
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderTexture alphaRT = new RenderTexture(screenshotWidth, screenshotHeight, 0, RenderTextureFormat.ARGB32);
|
if (useTempResolution)
|
||||||
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}");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
{
|
||||||
logError?.Invoke($"알파 스크린샷 촬영 실패: {e.Message}");
|
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 = "")
|
private string GenerateFileName(string extension, string suffix = "")
|
||||||
{
|
{
|
||||||
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||||
@ -203,9 +179,6 @@ public class ScreenshotManager
|
|||||||
|
|
||||||
public void Cleanup()
|
public void Cleanup()
|
||||||
{
|
{
|
||||||
if (alphaMaterial != null)
|
// RenderStreamOutput 이 모든 RT/Material lifecycle 을 관리하므로 별도 정리 없음
|
||||||
{
|
|
||||||
UnityEngine.Object.Destroy(alphaMaterial);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,7 @@ public class SystemController : MonoBehaviour
|
|||||||
optiTrack.Initialize(Log, LogError);
|
optiTrack.Initialize(Log, LogError);
|
||||||
facialMotion.Initialize(Log, LogError);
|
facialMotion.Initialize(Log, LogError);
|
||||||
motionRecording.Initialize(optiTrack, Log, LogError);
|
motionRecording.Initialize(optiTrack, Log, LogError);
|
||||||
screenshot.Initialize(Log, LogError);
|
screenshot.Initialize(this, Log, LogError);
|
||||||
clothSimulation.Initialize(Log, LogError);
|
clothSimulation.Initialize(Log, LogError);
|
||||||
avatarHead.Initialize(Log, LogError);
|
avatarHead.Initialize(Log, LogError);
|
||||||
retargetingRemote.Initialize(Log, LogError);
|
retargetingRemote.Initialize(Log, LogError);
|
||||||
|
|||||||
@ -29,14 +29,12 @@
|
|||||||
<!-- 스크린샷 -->
|
<!-- 스크린샷 -->
|
||||||
<ui:VisualElement class="section">
|
<ui:VisualElement class="section">
|
||||||
<ui:Foldout text="스크린샷" value="true" class="section-foldout">
|
<ui:Foldout text="스크린샷" value="true" class="section-foldout">
|
||||||
<uie:PropertyField binding-path="screenshot.screenshotCamera" label="카메라" tooltip="촬영할 카메라 (비어있으면 메인 카메라)"/>
|
<ui:HelpBox message-type="Info" text="알파 합성 결과는 RenderStreamOutput 컴포넌트의 CaptureTexture / AlphaOutputTexture 를 그대로 PNG 로 저장합니다. 해상도/셰이더/블러 설정은 RenderStreamOutput 에서 조절."/>
|
||||||
<uie:PropertyField binding-path="screenshot.screenshotWidth" label="너비"/>
|
<uie:PropertyField binding-path="screenshot.renderStream" label="RenderStreamOutput" tooltip="비어있으면 씬에서 자동 검색"/>
|
||||||
<uie:PropertyField binding-path="screenshot.screenshotHeight" label="높이"/>
|
|
||||||
<uie:PropertyField binding-path="screenshot.screenshotSavePath" label="저장 경로" tooltip="비어있으면 프로젝트 루트/Screenshots"/>
|
<uie:PropertyField binding-path="screenshot.screenshotSavePath" label="저장 경로" tooltip="비어있으면 프로젝트 루트/Screenshots"/>
|
||||||
<uie:PropertyField binding-path="screenshot.screenshotFilePrefix" label="파일 접두사"/>
|
<uie:PropertyField binding-path="screenshot.screenshotFilePrefix" label="파일 접두사"/>
|
||||||
<uie:PropertyField binding-path="screenshot.alphaShader" label="알파 셰이더"/>
|
<uie:PropertyField binding-path="screenshot.captureWidth" label="캡처 너비" tooltip="0 이면 카메라 해상도 사용. >0 이면 한 프레임 동안 카메라를 그 해상도로 변경 (Spout 도 잠깐 같은 해상도가 됨)"/>
|
||||||
<uie:PropertyField binding-path="screenshot.niloToonPrepassBufferName" label="NiloToon 버퍼 이름"/>
|
<uie:PropertyField binding-path="screenshot.captureHeight" label="캡처 높이" tooltip="0 이면 카메라 해상도 사용"/>
|
||||||
<uie:PropertyField binding-path="screenshot.alphaBlurRadius" label="알파 블러 반경"/>
|
|
||||||
</ui:Foldout>
|
</ui:Foldout>
|
||||||
</ui:VisualElement>
|
</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