Refactor : 스크린샷 캡처 RGB(A=1) / RGBA 분리 + sRGB 색공간 일치
- capture_screenshot → RGB24 PNG (알파 정보 자동 제거, 일반 사진) - capture_alpha_screenshot → RGBA32 PNG (배경 투명, OBS/합성 소스용) - HDR/Linear RT → sRGB RT 로 Blit 한 후 ReadPixels — Spout(OBS) 결과와 동일 톤 - 코루틴 기반 임시 고해상도 캡처(captureWidth/Height) 제거 — 카메라 해상도 그대로 → Spout 송신 한 프레임 깜빡임 부작용 사라짐 - screenshot.Initialize() 시그니처 host 인자 제거(코루틴 불필요) - UXML 인스펙터: captureWidth/Height 필드 제거 + 안내 갱신 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ea5f2af2b
commit
09413ccb3b
@ -1,14 +1,13 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 스크린샷 캡처 관리.
|
/// 스크린샷 캡처 관리.
|
||||||
/// RenderStreamOutput 이 매 프레임 갱신하는 CaptureTexture/AlphaOutputTexture 를 PNG 로 저장.
|
/// RenderStreamOutput.CaptureTexture (후처리 + 알파 합성 결과) 를 PNG 로 저장.
|
||||||
/// captureWidth/Height 가 지정되면 캡처 직전에 카메라 targetTexture 를 임시로 그 해상도로
|
/// - CaptureScreenshot() : RGB 만 (알파 = 1, 일반 사진)
|
||||||
/// 변경해 한 프레임 고해상도 렌더 후 원복한다.
|
/// - CaptureAlphaScreenshot() : RGBA 그대로 (배경 투명, 합성 소스용)
|
||||||
/// 트레이드오프: 그 한 프레임 동안 Spout/NDI 송신도 같은 해상도가 되어 수신측이 잠깐 깜빡임.
|
/// 카메라 해상도 그대로 ReadPixels — 별도 임시 RT/카메라 변경 없음.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class ScreenshotManager
|
public class ScreenshotManager
|
||||||
@ -22,22 +21,14 @@ public class ScreenshotManager
|
|||||||
[Tooltip("파일명 앞에 붙을 접두사")]
|
[Tooltip("파일명 앞에 붙을 접두사")]
|
||||||
public string screenshotFilePrefix = "Screenshot";
|
public string screenshotFilePrefix = "Screenshot";
|
||||||
|
|
||||||
[Header("고해상도 캡처 (0 이면 카메라 해상도 사용)")]
|
|
||||||
[Tooltip("캡처 너비. 0 이면 카메라 해상도 그대로")]
|
|
||||||
public int captureWidth = 3840;
|
|
||||||
[Tooltip("캡처 높이. 0 이면 카메라 해상도 그대로")]
|
|
||||||
public int captureHeight = 2160;
|
|
||||||
|
|
||||||
[Tooltip("(하위 호환) 외부 코드가 .screenshotCamera 로 접근하면 RenderStreamOutput.MainCam 반환")]
|
[Tooltip("(하위 호환) 외부 코드가 .screenshotCamera 로 접근하면 RenderStreamOutput.MainCam 반환")]
|
||||||
public Camera screenshotCamera => renderStream != null ? renderStream.MainCam : null;
|
public Camera screenshotCamera => renderStream != null ? renderStream.MainCam : null;
|
||||||
|
|
||||||
private MonoBehaviour host;
|
|
||||||
private Action<string> log;
|
private Action<string> log;
|
||||||
private Action<string> logError;
|
private Action<string> logError;
|
||||||
|
|
||||||
public void Initialize(MonoBehaviour host, Action<string> log, Action<string> logError)
|
public void Initialize(Action<string> log, Action<string> logError)
|
||||||
{
|
{
|
||||||
this.host = host;
|
|
||||||
this.log = log;
|
this.log = log;
|
||||||
this.logError = logError;
|
this.logError = logError;
|
||||||
|
|
||||||
@ -57,78 +48,26 @@ public class ScreenshotManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>RGB 만, 알파는 1 로 저장하는 일반 PNG (평범한 사진)</summary>
|
||||||
public void CaptureScreenshot()
|
public void CaptureScreenshot()
|
||||||
{
|
{
|
||||||
if (!CanCapture()) return;
|
if (renderStream == null || renderStream.CaptureTexture == null)
|
||||||
host.StartCoroutine(CaptureCoroutine(alphaOnly: false));
|
{
|
||||||
|
logError?.Invoke("RenderStreamOutput.CaptureTexture 가 준비되지 않았습니다");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SaveRtAsPng(renderStream.CaptureTexture, GenerateFileName("png"), TextureFormat.RGB24);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>RGBA 모두 저장 — 배경 투명, OBS/합성 소스용</summary>
|
||||||
public void CaptureAlphaScreenshot()
|
public void CaptureAlphaScreenshot()
|
||||||
{
|
{
|
||||||
if (!CanCapture()) return;
|
if (renderStream == null || renderStream.CaptureTexture == null)
|
||||||
host.StartCoroutine(CaptureCoroutine(alphaOnly: true));
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CanCapture()
|
|
||||||
{
|
|
||||||
if (host == null) { logError?.Invoke("ScreenshotManager host(MonoBehaviour) 가 없습니다"); return false; }
|
|
||||||
if (renderStream == null || renderStream.MainCam == null)
|
|
||||||
{
|
{
|
||||||
logError?.Invoke("RenderStreamOutput / MainCam 이 준비되지 않았습니다");
|
logError?.Invoke("RenderStreamOutput.CaptureTexture 가 준비되지 않았습니다");
|
||||||
return false;
|
return;
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerator CaptureCoroutine(bool alphaOnly)
|
|
||||||
{
|
|
||||||
var cam = renderStream.MainCam;
|
|
||||||
bool useTempResolution = captureWidth > 0 && captureHeight > 0;
|
|
||||||
|
|
||||||
RenderTexture tempRT = null;
|
|
||||||
RenderTexture prevTarget = null;
|
|
||||||
|
|
||||||
if (useTempResolution)
|
|
||||||
{
|
|
||||||
prevTarget = cam.targetTexture;
|
|
||||||
var desc = new RenderTextureDescriptor(captureWidth, captureHeight,
|
|
||||||
renderStream.TextureFormat, 24)
|
|
||||||
{
|
|
||||||
msaaSamples = Mathf.Max(1, renderStream.AntiAliasing)
|
|
||||||
};
|
|
||||||
tempRT = new RenderTexture(desc);
|
|
||||||
tempRT.Create();
|
|
||||||
|
|
||||||
cam.targetTexture = tempRT;
|
|
||||||
|
|
||||||
// 1프레임 — RenderStreamOutput.Update 가 새 pixelWidth 감지 → InitializeTextures(고해상도)
|
|
||||||
yield return null;
|
|
||||||
// 1프레임 — 고해상도 RT 에 우리 패스 결과 채워짐
|
|
||||||
yield return new WaitForEndOfFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
var srcRT = alphaOnly ? renderStream.AlphaOutputTexture : renderStream.CaptureTexture;
|
|
||||||
if (srcRT != null)
|
|
||||||
{
|
|
||||||
string fileName = GenerateFileName("png", alphaOnly ? "_Alpha" : "");
|
|
||||||
SaveRtAsPng(srcRT, fileName, alphaOnly ? TextureFormat.RGB24 : TextureFormat.RGBA32);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
logError?.Invoke("캡처 RT 가 준비되지 않았습니다");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (useTempResolution)
|
|
||||||
{
|
|
||||||
cam.targetTexture = prevTarget;
|
|
||||||
if (tempRT != null)
|
|
||||||
{
|
|
||||||
tempRT.Release();
|
|
||||||
UnityEngine.Object.Destroy(tempRT);
|
|
||||||
}
|
|
||||||
// 한 프레임 더 대기 — RenderStreamOutput.Update 가 원래 해상도로 복귀
|
|
||||||
yield return null;
|
|
||||||
}
|
}
|
||||||
|
SaveRtAsPng(renderStream.CaptureTexture, GenerateFileName("png", "_Alpha"), TextureFormat.RGBA32);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenScreenshotFolder()
|
public void OpenScreenshotFolder()
|
||||||
@ -148,11 +87,18 @@ public class ScreenshotManager
|
|||||||
{
|
{
|
||||||
string fullPath = Path.Combine(screenshotSavePath, fileName);
|
string fullPath = Path.Combine(screenshotSavePath, fileName);
|
||||||
RenderTexture prevActive = RenderTexture.active;
|
RenderTexture prevActive = RenderTexture.active;
|
||||||
|
RenderTexture sRgbRT = null;
|
||||||
Texture2D tex = null;
|
Texture2D tex = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
RenderTexture.active = rt;
|
// HDR/Linear RT → sRGB RT 로 Blit. ReadPixels 는 색공간 변환을 안 하므로 이걸 거치지
|
||||||
|
// 않으면 PNG 가 Linear 값을 sRGB 슬롯에 그대로 담아 OBS(Spout 결과)와 다르게 보임.
|
||||||
|
sRgbRT = RenderTexture.GetTemporary(rt.width, rt.height, 0,
|
||||||
|
RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||||||
|
Graphics.Blit(rt, sRgbRT);
|
||||||
|
|
||||||
|
RenderTexture.active = sRgbRT;
|
||||||
tex = new Texture2D(rt.width, rt.height, format, false);
|
tex = new Texture2D(rt.width, rt.height, format, false);
|
||||||
tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
|
tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
|
||||||
tex.Apply();
|
tex.Apply();
|
||||||
@ -167,6 +113,7 @@ public class ScreenshotManager
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
RenderTexture.active = prevActive;
|
RenderTexture.active = prevActive;
|
||||||
|
if (sRgbRT != null) RenderTexture.ReleaseTemporary(sRgbRT);
|
||||||
if (tex != null) UnityEngine.Object.Destroy(tex);
|
if (tex != null) UnityEngine.Object.Destroy(tex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(this, Log, LogError);
|
screenshot.Initialize(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,12 +29,10 @@
|
|||||||
<!-- 스크린샷 -->
|
<!-- 스크린샷 -->
|
||||||
<ui:VisualElement class="section">
|
<ui:VisualElement class="section">
|
||||||
<ui:Foldout text="스크린샷" value="true" class="section-foldout">
|
<ui:Foldout text="스크린샷" value="true" class="section-foldout">
|
||||||
<ui:HelpBox message-type="Info" text="알파 합성 결과는 RenderStreamOutput 컴포넌트의 CaptureTexture / AlphaOutputTexture 를 그대로 PNG 로 저장합니다. 해상도/셰이더/블러 설정은 RenderStreamOutput 에서 조절."/>
|
<ui:HelpBox message-type="Info" text="RenderStreamOutput.CaptureTexture 를 카메라 해상도로 PNG 저장. capture_screenshot = RGB만 (A=1, 일반 사진) capture_alpha_screenshot = RGBA (배경 투명, 합성 소스) 알파 합성 강도/블러는 RenderStreamOutput 에서 조절."/>
|
||||||
<uie:PropertyField binding-path="screenshot.renderStream" label="RenderStreamOutput" tooltip="비어있으면 씬에서 자동 검색"/>
|
<uie:PropertyField binding-path="screenshot.renderStream" label="RenderStreamOutput" tooltip="비어있으면 씬에서 자동 검색"/>
|
||||||
<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.captureWidth" label="캡처 너비" tooltip="0 이면 카메라 해상도 사용. >0 이면 한 프레임 동안 카메라를 그 해상도로 변경 (Spout 도 잠깐 같은 해상도가 됨)"/>
|
|
||||||
<uie:PropertyField binding-path="screenshot.captureHeight" label="캡처 높이" tooltip="0 이면 카메라 해상도 사용"/>
|
|
||||||
</ui:Foldout>
|
</ui:Foldout>
|
||||||
</ui:VisualElement>
|
</ui:VisualElement>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user