Refactor: 카메라 블렌딩을 UI 오버레이 방식으로 변경

- 렌더 패스 기반 블렌딩 제거 (CameraBlendRendererFeature, CameraBlend.shader)
- UI RawImage 기반 CameraBlendOverlay 추가
- 스냅샷/실시간 블렌딩 모두 UI 오버레이 방식으로 통합
- 메인 카메라의 실제 위치를 저장하여 화면 튀는 문제 수정

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
user 2026-01-21 09:51:36 +09:00
parent 8179f17b0d
commit 5087ebc5af
10 changed files with 250 additions and 538 deletions

View File

@ -280,8 +280,7 @@ public class CameraManager : MonoBehaviour, IController
[SerializeField] private bool useRealtimeBlend = true; [SerializeField] private bool useRealtimeBlend = true;
// 블렌드용 렌더 텍스처와 카메라 // 블렌드용 렌더 텍스처와 카메라
private RenderTexture blendRenderTexture; private RenderTexture prevCameraRenderTexture; // 블렌딩용 이전 카메라 렌더 텍스처
private RenderTexture prevCameraRenderTexture; // 실시간 블렌딩용 이전 카메라 렌더 텍스처
private Camera blendCamera; private Camera blendCamera;
private CinemachineCamera currentCamera; private CinemachineCamera currentCamera;
@ -350,13 +349,11 @@ public class CameraManager : MonoBehaviour, IController
} }
// 블렌드 리소스 정리 // 블렌드 리소스 정리
CameraBlendController.Reset(); if (prevCameraRenderTexture != null)
if (blendRenderTexture != null)
{ {
blendRenderTexture.Release(); prevCameraRenderTexture.Release();
Destroy(blendRenderTexture); Destroy(prevCameraRenderTexture);
blendRenderTexture = null; prevCameraRenderTexture = null;
} }
if (blendCamera != null) if (blendCamera != null)
@ -785,7 +782,12 @@ public class CameraManager : MonoBehaviour, IController
if (blendCoroutine != null) if (blendCoroutine != null)
{ {
StopCoroutine(blendCoroutine); StopCoroutine(blendCoroutine);
CameraBlendController.EndBlend(); // UI 오버레이 숨기기
var overlay = CameraBlendOverlay.Instance;
if (overlay != null)
{
overlay.Hide();
}
} }
blendCoroutine = StartCoroutine(BlendTransitionCoroutine(index, customBlendTime ?? blendTime)); blendCoroutine = StartCoroutine(BlendTransitionCoroutine(index, customBlendTime ?? blendTime));
@ -923,38 +925,6 @@ public class CameraManager : MonoBehaviour, IController
} }
/// <summary>
/// 블렌드용 렌더 텍스처 생성/갱신
/// </summary>
private void EnsureBlendRenderTexture()
{
int width = Screen.width;
int height = Screen.height;
// 이미 적절한 크기의 텍스처가 있으면 재사용
if (blendRenderTexture != null &&
blendRenderTexture.width == width &&
blendRenderTexture.height == height)
{
return;
}
// 기존 텍스처 해제
if (blendRenderTexture != null)
{
blendRenderTexture.Release();
Destroy(blendRenderTexture);
}
// 새 렌더 텍스처 생성 - 색상 전용 (depth 없음), 리니어 색공간에서 올바른 블렌딩
var descriptor = new RenderTextureDescriptor(width, height, RenderTextureFormat.ARGB32, 0); // depth = 0
descriptor.sRGB = true; // sRGB 텍스처로 설정하여 리니어 파이프라인과 일치
blendRenderTexture = new RenderTexture(descriptor);
blendRenderTexture.name = "CameraBlendRT";
blendRenderTexture.Create();
}
/// <summary> /// <summary>
/// 크로스 디졸브 블렌드 전환 코루틴 /// 크로스 디졸브 블렌드 전환 코루틴
/// </summary> /// </summary>
@ -971,16 +941,15 @@ public class CameraManager : MonoBehaviour, IController
} }
/// <summary> /// <summary>
/// 스냅샷 블렌딩 (이전 화면 정지) /// 스냅샷 블렌딩 (이전 화면 정지) - UI 오버레이 방식
/// </summary> /// </summary>
private IEnumerator SnapshotBlendTransitionCoroutine(int targetIndex, float duration) private IEnumerator SnapshotBlendTransitionCoroutine(int targetIndex, float duration)
{ {
isBlending = true; isBlending = true;
// 블렌드 텍스처 준비 // 메인 카메라의 현재 위치/회전/FOV를 저장
EnsureBlendRenderTexture(); Camera mainCamera = Camera.main;
if (mainCamera == null)
if (blendRenderTexture == null)
{ {
SetImmediate(targetIndex); SetImmediate(targetIndex);
isBlending = false; isBlending = false;
@ -988,80 +957,14 @@ public class CameraManager : MonoBehaviour, IController
yield break; yield break;
} }
// 렌더 패스에서 현재 프레임(포스트 프로세싱 적용 후)을 캡처하도록 요청 Vector3 prevCameraPosition = mainCamera.transform.position;
CameraBlendController.RequestCapture(blendRenderTexture); Quaternion prevCameraRotation = mainCamera.transform.rotation;
float prevCameraFOV = mainCamera.fieldOfView;
// 캡처가 완료될 때까지 대기 (다음 프레임 렌더링 후)
yield return new WaitForEndOfFrame();
yield return null; // 렌더 패스가 실행될 때까지 한 프레임 더 대기
// 캡처 완료 확인
if (!CameraBlendController.CaptureReady)
{
CameraBlendController.EndBlend();
SetImmediate(targetIndex);
isBlending = false;
blendCoroutine = null;
yield break;
}
// 블렌딩 시작 - BlendAmount = 1 (이전 카메라만 보임)
CameraBlendController.StartBlendAfterCapture();
CameraBlendController.BlendAmount = 1f;
// 카메라 전환 (이제 BlendAmount=1이므로 캡처된 이전 화면만 보임)
SetImmediate(targetIndex);
// 블렌드 진행: 1 → 0 (이전 카메라가 서서히 사라지고 새 카메라가 드러남)
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
// 부드러운 이징 적용 (SmoothStep)
t = t * t * (3f - 2f * t);
// 1에서 0으로: 이전 카메라가 서서히 사라짐
CameraBlendController.BlendAmount = 1f - t;
yield return null;
}
CameraBlendController.BlendAmount = 0f;
// 블렌딩 종료
CameraBlendController.EndBlend();
isBlending = false;
blendCoroutine = null;
}
/// <summary>
/// 실시간 블렌딩 (두 카메라 동시 렌더링)
/// 이전 카메라 위치를 저장해두고 그 위치에서 렌더링, 서서히 사라지면서 새 카메라가 드러남
/// </summary>
private IEnumerator RealtimeBlendTransitionCoroutine(int targetIndex, float duration)
{
isBlending = true;
// 이전 카메라 프리셋 저장
var prevPreset = currentPreset;
if (prevPreset == null || prevPreset.virtualCamera == null)
{
SetImmediate(targetIndex);
isBlending = false;
blendCoroutine = null;
yield break;
}
// SetImmediate 호출 전에 이전 카메라의 위치/회전/FOV를 저장
Vector3 prevCameraPosition = prevPreset.virtualCamera.transform.position;
Quaternion prevCameraRotation = prevPreset.virtualCamera.transform.rotation;
float prevCameraFOV = prevPreset.virtualCamera.Lens.FieldOfView;
// 블렌드 텍스처 준비 // 블렌드 텍스처 준비
EnsureBlendRenderTexture();
EnsurePrevCameraRenderTexture(); EnsurePrevCameraRenderTexture();
if (blendRenderTexture == null || prevCameraRenderTexture == null) if (prevCameraRenderTexture == null)
{ {
SetImmediate(targetIndex); SetImmediate(targetIndex);
isBlending = false; isBlending = false;
@ -1079,34 +982,122 @@ public class CameraManager : MonoBehaviour, IController
yield break; yield break;
} }
// UI 오버레이 준비
var overlay = CameraBlendOverlay.GetOrCreate();
// 블렌드 카메라를 현재 메인 카메라 위치로 설정하고 렌더링 (스냅샷 캡처)
blendCamera.transform.SetPositionAndRotation(prevCameraPosition, prevCameraRotation);
blendCamera.fieldOfView = prevCameraFOV;
blendCamera.targetTexture = prevCameraRenderTexture;
blendCamera.Render();
// UI 오버레이 표시 (알파 1 = 완전 불투명)
overlay.Show(prevCameraRenderTexture);
overlay.SetAlpha(1f);
// 새 카메라로 전환
SetImmediate(targetIndex);
// 블렌드 진행: 알파 1 → 0 (스냅샷이 서서히 사라지고 새 카메라가 드러남)
float elapsed = 0f;
while (elapsed < duration)
{
// 다음 프레임까지 대기
yield return null;
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
// 부드러운 이징 적용 (SmoothStep)
t = t * t * (3f - 2f * t);
// 알파를 1에서 0으로: 스냅샷이 서서히 사라짐
overlay.SetAlpha(1f - t);
}
// 블렌딩 종료
overlay.Hide();
isBlending = false;
blendCoroutine = null;
}
/// <summary>
/// 실시간 블렌딩 (UI 오버레이 방식)
/// 이전 카메라 위치를 저장해두고 그 위치에서 렌더링, UI로 표시하고 서서히 사라지게 함
/// </summary>
private IEnumerator RealtimeBlendTransitionCoroutine(int targetIndex, float duration)
{
isBlending = true;
// 이전 카메라 프리셋 저장
var prevPreset = currentPreset;
if (prevPreset == null || prevPreset.virtualCamera == null)
{
SetImmediate(targetIndex);
isBlending = false;
blendCoroutine = null;
yield break;
}
// 메인 카메라의 현재 위치/회전/FOV를 저장 (Cinemachine이 적용한 실제 위치)
Camera mainCamera = Camera.main;
if (mainCamera == null)
{
SetImmediate(targetIndex);
isBlending = false;
blendCoroutine = null;
yield break;
}
Vector3 prevCameraPosition = mainCamera.transform.position;
Quaternion prevCameraRotation = mainCamera.transform.rotation;
float prevCameraFOV = mainCamera.fieldOfView;
// 블렌드 텍스처 준비
EnsurePrevCameraRenderTexture();
if (prevCameraRenderTexture == null)
{
SetImmediate(targetIndex);
isBlending = false;
blendCoroutine = null;
yield break;
}
// 블렌드 카메라 준비
EnsureBlendCamera();
if (blendCamera == null)
{
SetImmediate(targetIndex);
isBlending = false;
blendCoroutine = null;
yield break;
}
// UI 오버레이 준비
var overlay = CameraBlendOverlay.GetOrCreate();
// 블렌드 카메라를 이전 카메라 위치로 설정하고 첫 프레임 렌더링 // 블렌드 카메라를 이전 카메라 위치로 설정하고 첫 프레임 렌더링
blendCamera.transform.SetPositionAndRotation(prevCameraPosition, prevCameraRotation); blendCamera.transform.SetPositionAndRotation(prevCameraPosition, prevCameraRotation);
blendCamera.fieldOfView = prevCameraFOV; blendCamera.fieldOfView = prevCameraFOV;
blendCamera.targetTexture = prevCameraRenderTexture; blendCamera.targetTexture = prevCameraRenderTexture;
// Camera.Render()로 렌더링 (URP에서도 포스트 프로세싱 적용됨)
blendCamera.Render(); blendCamera.Render();
Graphics.Blit(prevCameraRenderTexture, blendRenderTexture);
// 실시간 블렌딩 시작 (BlendAmount = 1에서 시작, 이전 카메라가 100% 보임) // UI 오버레이 표시 (알파 1 = 완전 불투명)
CameraBlendController.StartRealtimeBlend(blendRenderTexture); overlay.Show(prevCameraRenderTexture);
CameraBlendController.BlendAmount = 1f; overlay.SetAlpha(1f);
// 새 카메라로 전환 (메인 카메라는 이제 새 위치에서 렌더링됨) // 새 카메라로 전환 (메인 카메라는 이제 새 위치에서 렌더링됨)
SetImmediate(targetIndex); SetImmediate(targetIndex);
// 블렌드 진행: 1 → 0 (이전 카메라 화면이 서서히 사라지고 새 카메라가 드러남) // 블렌드 진행: 알파 1 → 0 (이전 카메라 화면이 서서히 사라)
float elapsed = 0f; float elapsed = 0f;
while (elapsed < duration) while (elapsed < duration)
{ {
// 이전 카메라 시점에서 먼저 렌더링 (메인 카메라 렌더링 전에) // 이전 카메라 시점에서 렌더링
blendCamera.transform.SetPositionAndRotation(prevCameraPosition, prevCameraRotation); blendCamera.transform.SetPositionAndRotation(prevCameraPosition, prevCameraRotation);
blendCamera.fieldOfView = prevCameraFOV; blendCamera.fieldOfView = prevCameraFOV;
blendCamera.targetTexture = prevCameraRenderTexture; blendCamera.targetTexture = prevCameraRenderTexture;
// Camera.Render()로 렌더링 (URP에서도 포스트 프로세싱 적용됨)
blendCamera.Render(); blendCamera.Render();
Graphics.Blit(prevCameraRenderTexture, blendRenderTexture);
// 다음 프레임까지 대기 // 다음 프레임까지 대기
yield return null; yield return null;
@ -1115,14 +1106,12 @@ public class CameraManager : MonoBehaviour, IController
float t = Mathf.Clamp01(elapsed / duration); float t = Mathf.Clamp01(elapsed / duration);
// 부드러운 이징 적용 (SmoothStep) // 부드러운 이징 적용 (SmoothStep)
t = t * t * (3f - 2f * t); t = t * t * (3f - 2f * t);
// 1에서 0으로: 이전 카메라가 서서히 사라짐 // 알파를 1에서 0으로: 이전 카메라가 서서히 사라짐
CameraBlendController.BlendAmount = 1f - t; overlay.SetAlpha(1f - t);
} }
CameraBlendController.BlendAmount = 0f;
// 블렌딩 종료 // 블렌딩 종료
CameraBlendController.EndBlend(); overlay.Hide();
isBlending = false; isBlending = false;
blendCoroutine = null; blendCoroutine = null;

View File

@ -1,39 +0,0 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: CameraBlend
m_Shader: {fileID: 4800000, guid: 76944497fc2bcdd479fc435a5d4d8447, type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _MainTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _PrevTex:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _BlendAmount: 0.000075280666
m_Colors: []
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: f563d7f0c3fc17647ad56388fb936427
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,57 +0,0 @@
Shader "Streamingle/CameraBlend"
{
Properties
{
_PrevTex ("Previous Camera", 2D) = "white" {}
_BlendAmount ("Blend Amount", Range(0, 1)) = 0
}
SubShader
{
Tags
{
"RenderType" = "Opaque"
"RenderPipeline" = "UniversalPipeline"
}
Pass
{
Name "CameraBlend"
ZTest Always
ZWrite Off
Cull 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"
TEXTURE2D(_PrevTex);
SAMPLER(sampler_PrevTex);
float _BlendAmount;
float4 Frag(Varyings input) : SV_Target
{
float2 uv = input.texcoord;
// 현재 카메라 (Blitter에서 전달됨) - 동일한 샘플러 사용
float4 currentColor = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, uv);
// 이전 카메라 (캡처된 텍스처) - 동일한 Linear 샘플러 사용
float4 prevColor = SAMPLE_TEXTURE2D(_PrevTex, sampler_LinearClamp, uv);
// 리니어 공간에서 블렌딩 (BlendAmount: 1 = 이전, 0 = 현재)
// 이전 카메라(B)가 위에 덮이고 서서히 사라짐
float4 finalColor = lerp(currentColor, prevColor, _BlendAmount);
return finalColor;
}
ENDHLSL
}
}
FallBack Off
}

View File

@ -1,9 +0,0 @@
fileFormatVersion: 2
guid: 76944497fc2bcdd479fc435a5d4d8447
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,290 +0,0 @@
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;
/// <summary>
/// 카메라 전환 시 크로스 디졸브 블렌딩을 위한 URP Renderer Feature
/// Unity 6 Render Graph API 사용
/// </summary>
public class CameraBlendRendererFeature : ScriptableRendererFeature
{
[System.Serializable]
public class Settings
{
// AfterRenderingPostProcessing - 모든 포스트 프로세싱 적용 후 블렌딩
public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
public Material blendMaterial;
}
public Settings settings = new Settings();
private CameraBlendRenderPass blendPass;
public override void Create()
{
blendPass = new CameraBlendRenderPass(settings);
blendPass.renderPassEvent = settings.renderPassEvent;
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (settings.blendMaterial == null)
return;
// 게임 카메라만 처리
if (renderingData.cameraData.cameraType != CameraType.Game)
return;
// 캡처 요청이 있거나 블렌딩 중일 때 패스 추가
if (CameraBlendController.CaptureRequested ||
(CameraBlendController.IsBlending && CameraBlendController.BlendTexture != null))
{
renderer.EnqueuePass(blendPass);
}
}
protected override void Dispose(bool disposing)
{
blendPass?.Dispose();
}
}
public class CameraBlendRenderPass : ScriptableRenderPass
{
private CameraBlendRendererFeature.Settings settings;
private static readonly int BlendAmountProperty = Shader.PropertyToID("_BlendAmount");
private static readonly int PrevTexProperty = Shader.PropertyToID("_PrevTex");
private class PassData
{
public Material blendMaterial;
public float blendAmount;
public RenderTexture blendTexture;
public TextureHandle sourceTexture;
public TextureHandle destinationTexture;
}
private class CapturePassData
{
public TextureHandle sourceTexture;
public RenderTexture targetTexture;
}
public CameraBlendRenderPass(CameraBlendRendererFeature.Settings settings)
{
this.settings = settings;
profilingSampler = new ProfilingSampler("Camera Blend Pass");
ConfigureInput(ScriptableRenderPassInput.Color);
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
if (settings.blendMaterial == null)
return;
var resourceData = frameData.Get<UniversalResourceData>();
var cameraData = frameData.Get<UniversalCameraData>();
// 백버퍼인 경우 스킵
if (resourceData.isActiveTargetBackBuffer)
{
return;
}
var source = resourceData.activeColorTexture;
// source가 유효한지 확인
if (!source.IsValid())
{
return;
}
// 캡처 요청 처리 - 현재 프레임(포스트 프로세싱 적용 후)을 캡처
if (CameraBlendController.CaptureRequested && CameraBlendController.BlendTexture != null)
{
// UnsafePass를 사용하여 RenderTexture에 직접 복사
using (var builder = renderGraph.AddUnsafePass<CapturePassData>("Camera Blend Capture", out var captureData, profilingSampler))
{
captureData.sourceTexture = source;
captureData.targetTexture = CameraBlendController.BlendTexture;
builder.UseTexture(source, AccessFlags.Read);
builder.AllowPassCulling(false);
builder.SetRenderFunc((CapturePassData data, UnsafeGraphContext context) =>
{
// NativeCommandBuffer를 통해 Blit 수행
var nativeCmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
nativeCmd.Blit(data.sourceTexture, data.targetTexture);
CameraBlendController.CaptureReady = true;
});
}
return; // 캡처만 하고 블렌딩은 다음 프레임부터
}
// 블렌딩이 활성화되지 않았으면 스킵
if (!CameraBlendController.IsBlending || CameraBlendController.BlendTexture == null)
return;
// cameraColorDesc 사용 - 카메라 색상 텍스처 설명자
var cameraTargetDesc = renderGraph.GetTextureDesc(resourceData.cameraColor);
cameraTargetDesc.name = "_CameraBlendDestination";
cameraTargetDesc.clearBuffer = false;
var destination = renderGraph.CreateTexture(cameraTargetDesc);
// 블렌딩 패스
using (var builder = renderGraph.AddRasterRenderPass<PassData>("Camera Blend", out var passData, profilingSampler))
{
passData.blendMaterial = settings.blendMaterial;
passData.blendAmount = CameraBlendController.BlendAmount;
passData.blendTexture = CameraBlendController.BlendTexture;
passData.sourceTexture = source;
builder.UseTexture(source, AccessFlags.Read);
builder.SetRenderAttachment(destination, 0, AccessFlags.Write);
builder.AllowPassCulling(false);
builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
{
data.blendMaterial.SetFloat(BlendAmountProperty, data.blendAmount);
data.blendMaterial.SetTexture(PrevTexProperty, data.blendTexture);
Blitter.BlitTexture(context.cmd, data.sourceTexture, new Vector4(1, 1, 0, 0), data.blendMaterial, 0);
});
}
// 결과를 source로 복사
using (var builder = renderGraph.AddRasterRenderPass<PassData>("Camera Blend Copy Back", out var copyData, profilingSampler))
{
copyData.sourceTexture = destination;
builder.UseTexture(destination, AccessFlags.Read);
builder.SetRenderAttachment(source, 0, AccessFlags.Write);
builder.AllowPassCulling(false);
builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
{
Blitter.BlitTexture(context.cmd, data.sourceTexture, new Vector4(1, 1, 0, 0), 0, false);
});
}
}
[System.Obsolete("This rendering path is for compatibility mode only (when Render Graph is disabled). Use Render Graph API instead.", false)]
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
// Legacy path
}
public void Dispose()
{
}
}
/// <summary>
/// 카메라 블렌딩을 제어하는 정적 컨트롤러
/// </summary>
public static class CameraBlendController
{
private static bool isBlending = false;
private static float blendAmount = 1f;
private static RenderTexture blendTexture;
private static bool captureRequested = false;
private static bool captureReady = false;
private static bool isRealtimeMode = false;
public static bool IsBlending
{
get => isBlending;
set => isBlending = value;
}
public static float BlendAmount
{
get => blendAmount;
set => blendAmount = Mathf.Clamp01(value);
}
public static RenderTexture BlendTexture
{
get => blendTexture;
set => blendTexture = value;
}
public static bool CaptureRequested
{
get => captureRequested;
set => captureRequested = value;
}
public static bool CaptureReady
{
get => captureReady;
set => captureReady = value;
}
public static bool IsRealtimeMode
{
get => isRealtimeMode;
set => isRealtimeMode = value;
}
/// <summary>
/// 다음 프레임에서 현재 화면을 캡처하도록 요청합니다. (스냅샷 모드)
/// </summary>
public static void RequestCapture(RenderTexture targetTexture)
{
blendTexture = targetTexture;
captureRequested = true;
captureReady = false;
isRealtimeMode = false;
}
/// <summary>
/// 캡처가 완료된 후 블렌딩을 시작합니다. (스냅샷 모드)
/// BlendAmount = 1에서 시작 (이전 카메라 100% 보임)
/// </summary>
public static void StartBlendAfterCapture()
{
if (captureReady && blendTexture != null)
{
blendAmount = 1f;
isBlending = true;
captureRequested = false;
}
}
/// <summary>
/// 실시간 블렌딩을 시작합니다. (매 프레임 이전 카메라 렌더링)
/// BlendAmount = 1에서 시작 (이전 카메라 100% 보임)
/// </summary>
public static void StartRealtimeBlend(RenderTexture texture)
{
blendTexture = texture;
blendAmount = 1f;
isBlending = true;
isRealtimeMode = true;
captureRequested = false;
captureReady = false;
}
public static void StartBlend(RenderTexture texture)
{
blendTexture = texture;
blendAmount = 0f;
isBlending = true;
}
public static void EndBlend()
{
isBlending = false;
blendAmount = 1f;
blendTexture = null;
captureRequested = false;
captureReady = false;
isRealtimeMode = false;
}
public static void Reset()
{
EndBlend();
}
}

View File

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 14b4e395bb7cf744d87b2c1885014709

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2 fileFormatVersion: 2
guid: 634dec0a07bb460468b7e86f02bbc5b9 guid: 055b59bee33be324c88ea76f3ea4786b
folderAsset: yes folderAsset: yes
DefaultImporter: DefaultImporter:
externalObjects: {} externalObjects: {}

View File

@ -0,0 +1,126 @@
using UnityEngine;
using UnityEngine.UI;
namespace Streamingle
{
/// <summary>
/// 카메라 블렌딩용 UI 오버레이
/// RawImage 하나로 이전 카메라 화면을 표시하고 서서히 사라지게 함
/// </summary>
public class CameraBlendOverlay : MonoBehaviour
{
private static CameraBlendOverlay instance;
public static CameraBlendOverlay Instance => instance;
private Canvas canvas;
private RawImage rawImage;
private CanvasGroup canvasGroup;
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
SetupUI();
}
private void SetupUI()
{
// Canvas 설정
canvas = gameObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 9999; // 최상단에 표시
// CanvasScaler 추가 (화면 크기 맞춤)
var scaler = gameObject.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
scaler.screenMatchMode = CanvasScaler.ScreenMatchMode.MatchWidthOrHeight;
scaler.matchWidthOrHeight = 0.5f;
// GraphicRaycaster는 추가하지 않음 (클릭 방해 안함)
// RawImage 생성
GameObject imageObj = new GameObject("BlendImage");
imageObj.transform.SetParent(transform, false);
rawImage = imageObj.AddComponent<RawImage>();
rawImage.raycastTarget = false; // 레이캐스트 비활성화
// 화면 전체를 덮도록 설정
RectTransform rectTransform = rawImage.rectTransform;
rectTransform.anchorMin = Vector2.zero;
rectTransform.anchorMax = Vector2.one;
rectTransform.offsetMin = Vector2.zero;
rectTransform.offsetMax = Vector2.zero;
// CanvasGroup 추가 (알파 조절용)
canvasGroup = imageObj.AddComponent<CanvasGroup>();
canvasGroup.blocksRaycasts = false;
canvasGroup.interactable = false;
// 초기에는 숨김
Hide();
}
/// <summary>
/// 오버레이 표시 및 텍스처 설정
/// </summary>
public void Show(RenderTexture texture)
{
if (rawImage == null) return;
rawImage.texture = texture;
rawImage.enabled = true;
canvasGroup.alpha = 1f;
}
/// <summary>
/// 알파값 설정 (0 = 투명, 1 = 불투명)
/// </summary>
public void SetAlpha(float alpha)
{
if (canvasGroup == null) return;
canvasGroup.alpha = alpha;
}
/// <summary>
/// 오버레이 숨김
/// </summary>
public void Hide()
{
if (rawImage == null) return;
rawImage.enabled = false;
rawImage.texture = null;
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
}
}
/// <summary>
/// 싱글톤 인스턴스 생성 (없으면 자동 생성)
/// </summary>
public static CameraBlendOverlay GetOrCreate()
{
if (instance != null) return instance;
GameObject obj = new GameObject("CameraBlendOverlay");
return obj.AddComponent<CameraBlendOverlay>();
}
private void OnDestroy()
{
if (instance == this)
{
instance = null;
}
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c072c526296e95d4bb6a11943cb2018b