[언리얼엔진] 7. CCTV 기능 구현

2026. 4. 9. 18:36·Unreal Engine 프로젝트/VR 공포게임

결과물

보이시나요 .. 부드럽게 송출되는 CCTV의 화면이 .. 추가로 보이실지 모르겠지만, 좌측 상단에 프레임이 표시되는데요. VR말고 PC 상에서는 무려 230 FPS이 나온답니다.

(가운데 가로막고 있는 물체는 Hand 메시입니다. VR 장비를 착용하지 않은 상태라서 제어가 안 되는 상태라 그렇습니다.)

 


 

구현 내용

1. ACCTV.h / .cpp

SceneCaptureComponent를 활용하여 특정 장소의 모습을 실시간으로 캡쳐하려고 합니다. 다만, 실시간으로 캡쳐를 진행하는 만큼 렌더링의 비용이 발생하고, 이는 VR 환경에서 치명적으로 작용하기 때문에 SceneCaptureComponent의 최적화도 진행해주었습니다.
총 4개의 SceneCaptureComponent를 사용하여 4개의 장소를 캡쳐하고자 합니다. 플레이어의 카메라 + 4대의 CCTV이므로 렌더링 비용이 어마어마할 겁니다. 하지만, 현재 비추는 장소의 CCTV만 캡쳐를 진행하고, 나머지 CCTV는 캡쳐를 비활성화 하는 방식을 사용하여 최적화를 진행했습니다. 추가로, 따로 설정을 하지 않으면 SceneCaptureComponent가 Tick마다 캡쳐를 진행하기 때문에 타이머 방식을 활용해서 일정 시간마다 캡쳐를 진행하도록 구현했습니다.

ACCTV.h

더보기
#pragma once

#include "CoreMinimal.h"
#include "Components/SceneCaptureComponent2D.h"
#include "GameFramework/Actor.h"
#include "CCTV.generated.h"

class UCameraComponent;
class USceneCaptureComponent2D;
class UTextureRenderTarget2D;

UCLASS()
class VIRTUALREALITY_API ACCTV : public AActor
{
	GENERATED_BODY()

public:
	ACCTV();
	virtual void BeginPlay() override;

	
// Member Function	
public:
	/** SceneCaptureComponent의 캡처 활성화 여부를 설정합니다. */
	void SetCaptureEnabled(bool bEnabled);

private:
	void CaptureScene();
	
	
// Component Section	
protected:
	/** 씬 캡쳐 컴포넌트가 어떤 장면을 캡쳐하는지 볼 수 있게 하는 카메라입니다. 이 카메라는 씬 캡처 컴포넌트와 동일한 위치에 배치되어야 합니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수")
	TObjectPtr<UCameraComponent> FakeCameraComponent;

	/** 씬 캡처 컴포넌트입니다. 이 컴포넌트가 장면을 캡처하여 텍스처로 렌더링합니다. */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수")
	TObjectPtr<USceneCaptureComponent2D> SceneCaptureComponent;
	
	
// Variable Section
protected:
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수|수치")
	float CaptureInterval = 1.0f;

private:
	FTimerHandle CaptureTimerHandle;

	
// Getter, Setter Section
public:
	FORCEINLINE UTextureRenderTarget2D* GetRenderTarget() const { return SceneCaptureComponent ? SceneCaptureComponent->TextureTarget : nullptr; };
};

ACCTV.cpp

더보기
#include "CCTV.h"
#include "Camera/CameraComponent.h"
#include "Components/SceneCaptureComponent2D.h"
#include "Engine/TextureRenderTarget2D.h"
#include "VirtualReality.h"

ACCTV::ACCTV()
{
	FakeCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("FakeCameraComponent"));
	FakeCameraComponent->SetupAttachment(GetRootComponent());

	SceneCaptureComponent = CreateDefaultSubobject<USceneCaptureComponent2D>(TEXT("SceneCaptureComponent"));
	SceneCaptureComponent->SetupAttachment(FakeCameraComponent);

	// 기본적으로 캡처를 비활성화하여 불필요한 렌더링 비용을 절약합니다.
	SceneCaptureComponent->bCaptureEveryFrame = false;
	SceneCaptureComponent->bCaptureOnMovement = false;
	SceneCaptureComponent->ShowFlags.SetMotionBlur(false);
	SceneCaptureComponent->ShowFlags.SetEyeAdaptation(false);
	SceneCaptureComponent->ShowFlags.SetLocalExposure(false);
	SceneCaptureComponent->ShowFlags.SetPostProcessMaterial(false);
	SceneCaptureComponent->ShowFlags.SetToneCurve(false);
	SceneCaptureComponent->ShowFlags.SetTonemapper(false);
	SceneCaptureComponent->ShowFlags.SetBloom(false);
	SceneCaptureComponent->ShowFlags.SetAmbientOcclusion(false);
	SceneCaptureComponent->ShowFlags.SetDynamicShadows(false);
	SceneCaptureComponent->ShowFlags.SetFog(false);
	SceneCaptureComponent->ShowFlags.SetLensFlares(false);
	SceneCaptureComponent->ShowFlags.SetAntiAliasing(false);
	SceneCaptureComponent->ShowFlags.SetDepthOfField(false);
	SceneCaptureComponent->LODDistanceFactor = 10.0f;
	SceneCaptureComponent->SetActive(false);
}

void ACCTV::BeginPlay()
{
	Super::BeginPlay();
	
	CaptureScene();
}

void ACCTV::SetCaptureEnabled(bool bEnabled)
{
	if (bEnabled)
	{
		GetWorldTimerManager().SetTimer(
			CaptureTimerHandle,
			this,
			&ThisClass::CaptureScene,
			CaptureInterval,
			true,
			true
		);	
	}
	else
	{
		GetWorldTimerManager().ClearTimer(CaptureTimerHandle);
	}

	LOG(TEXT("CCTV 캡처 상태가 [%s]로 변경되었습니다."), bEnabled ? TEXT("활성화") : TEXT("비활성화"));
}

void ACCTV::CaptureScene()
{
	// 씬을 수동으로 캡처합니다.
	SceneCaptureComponent->CaptureScene();
}

 


 

2. AMonitor.h / .cpp

CCTV가 캡쳐하는 장면을 렌더링하는 책임을 가지는 클래스입니다. Material Instance를 통해 화면에 송출하는 기능을 수행합니다.

AMonitor.h

더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Monitor.generated.h"

class ACCTV;
class UPointLightComponent;
class UStaticMeshComponent;
class UMaterialInterface;
class UMaterialInstanceDynamic;
class UInputAction;

UCLASS()
class VIRTUALREALITY_API AMonitor : public AActor
{
	GENERATED_BODY()

	
// Lifecycle Section	
public:
	AMonitor();
	virtual void BeginPlay() override;
	
	
// Member Function	
public:
	UFUNCTION(BlueprintCallable)
	void SwitchToCCTV(int32 Index);
	
private:
	void SwitchToNextCCTV();
	void SwitchToPrevCCTV();
	void CollectCCTVs();
	void SetActiveCCTV(int32 Index);
	void DeactivateAllCCTVs();

	
// Component Section	
protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
	TObjectPtr<USceneComponent> MonitorRoot;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
	TObjectPtr<UStaticMeshComponent> MonitorMesh;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
	TObjectPtr<UStaticMeshComponent> ScreenMesh;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "변수|컴포넌트")
	TObjectPtr<UPointLightComponent> ScreenLight;
	
	
// Variable Section	
protected:
	/** 렌더 타겟을 표시할 화면 머티리얼입니다. */
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수")
	TObjectPtr<UMaterialInterface> ScreenMaterial;

	/** 렌더 타겟 텍스처를 연결할 머티리얼 파라미터 이름입니다. */
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "변수")
	FName RenderTargetParameterName = TEXT("RenderTarget");

private:
	UPROPERTY()
	TObjectPtr<UMaterialInstanceDynamic> ScreenMaterialInstance;

	TArray<TObjectPtr<ACCTV>> RegisteredCCTVs;
	int32 ActiveCCTVIndex = -1;


// Getter, Setter Section
public:
	FORCEINLINE int32 GetActiveCCTVIndex() const { return ActiveCCTVIndex; }\
	
};

AMonitor.cpp

더보기
#include "Monitor.h"
#include "Components/StaticMeshComponent.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Actor/CCTV.h"
#include "EngineUtils.h"
#include "VirtualReality.h"
#include "Components/PointLightComponent.h"

AMonitor::AMonitor()
{
	MonitorRoot = CreateDefaultSubobject<USceneComponent>("MonitorRoot");
	SetRootComponent(MonitorRoot);
	
	MonitorMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MonitorMesh"));
	MonitorMesh->SetupAttachment(MonitorRoot);
	MonitorMesh->SetRelativeScale3D(FVector(1.0f, 2.5f, 2.5f));
	
	ScreenMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ScreenMesh"));
	ScreenMesh->SetupAttachment(MonitorMesh);
	ScreenMesh->SetRelativeLocation(FVector(4.0f, 0.0f, 31.6f));
	ScreenMesh->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
	ScreenMesh->SetRelativeScale3D(FVector(0.6f, 0.001f, 0.14f));
	
	ScreenLight = CreateDefaultSubobject<UPointLightComponent>(TEXT("ScreenLight"));
	ScreenLight->SetupAttachment(ScreenMesh);
	ScreenLight->SetIntensityUnits(ELightUnits::Unitless);
	ScreenLight->SetVisibility(false);
	
	static ConstructorHelpers::FObjectFinder<UStaticMesh> SM_Monitor(TEXT("/Game/AtmosphericHouse/Meshes/Meshes_props/Electronics/SM_Computer_screen"));
	if (SM_Monitor.Succeeded())
	{
		MonitorMesh->SetStaticMesh(SM_Monitor.Object);
	}
}

void AMonitor::BeginPlay()
{
	Super::BeginPlay();

	// 화면용 다이나믹 머티리얼 인스턴스를 생성합니다.
	if (ScreenMaterial)
	{
		ScreenMaterialInstance = UMaterialInstanceDynamic::Create(ScreenMaterial, this);
		ScreenMesh->SetMaterial(0, ScreenMaterialInstance);
	}

	// 월드 내 모든 CCTV를 수집합니다.
	CollectCCTVs();
}

void AMonitor::CollectCCTVs()
{
	RegisteredCCTVs.Reset();

	for (ACCTV* CCTV : TActorRange<ACCTV>(GetWorld()))
	{
		RegisteredCCTVs.Emplace(CCTV);
	}

	LOG(TEXT("CCTV %d개가 수집되었습니다."), RegisteredCCTVs.Num());
}

void AMonitor::SetActiveCCTV(int32 Index)
{
	// 이전 활성 CCTV를 비활성화합니다.
	if (RegisteredCCTVs.IsValidIndex(ActiveCCTVIndex))
	{
		RegisteredCCTVs[ActiveCCTVIndex]->SetCaptureEnabled(false);
	}

	// 새 CCTV를 활성화합니다.
	ActiveCCTVIndex = Index;
	RegisteredCCTVs[ActiveCCTVIndex]->SetCaptureEnabled(true);

	LOG(TEXT("활성 CCTV가 인덱스 %d로 변경되었습니다."), ActiveCCTVIndex);
}

void AMonitor::DeactivateAllCCTVs()
{
	for (TObjectPtr<ACCTV>& CCTV : RegisteredCCTVs)
	{
		if (CCTV)
		{
			CCTV->SetCaptureEnabled(false);
		}
	}

	ActiveCCTVIndex = -1;

	LOG(TEXT("모든 CCTV가 비활성화되었습니다."));
}

void AMonitor::SwitchToCCTV(int32 Index)
{
	if (RegisteredCCTVs.IsEmpty()) return;

	ScreenLight->SetVisibility(true);

	// 인덱스를 CCTV 배열 범위 내에서 순환시킵니다.
	const int32 ClampedIndex = (Index % RegisteredCCTVs.Num() + RegisteredCCTVs.Num()) % RegisteredCCTVs.Num();

	// 활성 CCTV를 전환합니다.
	SetActiveCCTV(ClampedIndex);

	// 전환된 CCTV의 렌더 타겟을 화면 머티리얼에 적용합니다.
	if (UTextureRenderTarget2D* RenderTarget = RegisteredCCTVs[ClampedIndex]->GetRenderTarget())
	{
		ScreenMaterialInstance->SetTextureParameterValue(RenderTargetParameterName, RenderTarget);
	}

	LOG(TEXT("모니터가 CCTV 인덱스 %d로 전환되었습니다."), ClampedIndex);
}

void AMonitor::SwitchToNextCCTV()
{
	// 다음 인덱스로 전환합니다. SwitchToCCTV 내부에서 순환 처리됩니다.
	SwitchToCCTV(ActiveCCTVIndex + 1);
}

void AMonitor::SwitchToPrevCCTV()
{
	// 이전 인덱스로 전환합니다. SwitchToCCTV 내부에서 순환 처리됩니다.
	SwitchToCCTV(ActiveCCTVIndex - 1);
}

 


 

3. Render Target Texture

SceneCaptureComponent가 캡쳐한 장면을 저장하기 위한 텍스처입니다. Size X, Size Y에 따라 캡쳐한 장면의 해상도가 좌우됩니다.

 


 

4. Monitor Material

 이 VR 게임의 핵심이라고 할 수 있는 모니터의 머티리얼을 구현했습니다. Render Target Texture 기반 머티리얼로, SceneCaptureComponent가 캡쳐한 장면을 흑백 필터링 및 Lens Distortion 효과가 적용된 상태로 송출하는 기능을 수행합니다.

Lens Distortion 효과
흑백 및 Noise 필터 효과

 


 

마무리

CCTV 로직을 직접 구현해보니, 역시 언리얼은 무엇보다 최적화가 중요하다는 사실을 새삼 깨닫게 되었네요. 유니티로 2D 게임 개발할 때에는 프레임 드랍 걱정은 전혀 없었는데, 언리얼은 언제나 프레임 걱정을 하면서 개발을 진행하는 것 같아요 ㅎㅎ 이게 또 언리얼만의 묘미가 아닐까 싶습니다 ! 덕분에 SceneCaptureComponent의 최적화 로직에 대한 스킬을 얻게 되었습니다 ! 나이수 !

 

 

 

 

 

'Unreal Engine 프로젝트 > VR 공포게임' 카테고리의 다른 글

[언리얼엔진] 9. Floating Dust 파티클 구현  (0) 2026.04.09
[언리얼 엔진] 8. 채널 전환 버튼 구현  (0) 2026.04.09
[언리얼엔진] 6. VR Hand 애니메이션 제작  (0) 2026.04.09
[언리얼엔진] 5. VR 최적화  (0) 2026.04.09
[언리얼엔진] 4. VR 레버 구현  (2) 2026.04.08
'Unreal Engine 프로젝트/VR 공포게임' 카테고리의 다른 글
  • [언리얼엔진] 9. Floating Dust 파티클 구현
  • [언리얼 엔진] 8. 채널 전환 버튼 구현
  • [언리얼엔진] 6. VR Hand 애니메이션 제작
  • [언리얼엔진] 5. VR 최적화
Meoyoung's Development Logs
Meoyoung's Development Logs
내가 보려고 만든 블로그
  • Meoyoung's Development Logs
    이게뭐영
    Meoyoung's Development Logs
  • 전체
    오늘
    어제
    • 분류 전체보기 (289)
      • Unreal Engine 프로젝트 (36)
        • 더 퍼스트 버서커 : 카잔 (16)
        • VR 공포게임 (13)
        • Paper-ZD (7)
      • 언리얼 엔진 (72)
        • GAS (10)
        • 트러블슈팅 (27)
        • 캐릭터 (2)
        • VR (1)
        • Lighting (2)
        • 멀티스레드 (2)
        • Lyra (1)
      • C++ (31)
        • 문법 정리 (8)
        • [서적] Fundamental C++ 프로그래밍 .. (5)
        • [서적] 이것이 C++이다 (11)
        • [서적] Effective C++ (7)
      • 게임잼 (3)
      • 강의 (36)
        • [강의] 이득우의 언리얼 프로그래밍 Part1 (13)
        • [강의] 이득우의 언리얼 프로그래밍 Part2 (2)
        • [강의] 이득우의 언리얼 프로그래밍 Part3 (12)
        • [강의] 소울라이크 개발 A-Z (4)
        • [강의] Udemy-2D (5)
      • C# (1)
        • [서적] 이것이 C#이다 (1)
      • 코딩테스트 (26)
        • 프로그래머스 (6)
        • 알고리듬 (13)
        • 자료구조 (7)
      • 컴퓨터 과학 (27)
        • 운영체제 (11)
        • 데이터베이스 (0)
        • 디자인패턴 (0)
        • 자료구조 (5)
        • 네트워크 (0)
        • 컴퓨터구조 (11)
      • 면접준비 (0)
        • C++ (0)
        • 운영체제 (0)
        • 자료구조 (0)
      • 기타 (48)
        • [팀프로젝트] The Fourth Descenda.. (5)
        • GetOutOf (15)
        • [개인프로젝트] FPS 구현 맛보기 (5)
        • [서적] 인생 언리얼5 (4)
        • 스파르타코딩클럽 (15)
        • 객체지향프로그래밍 (2)
        • 컴퓨터회로 (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    버블정렬
    참가후기
    경북게임잼
    셸정렬
    자료구조
    게임개발
    쉘정렬
    선택정렬
    게임잼
    알고리즘
    삽입정렬
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Meoyoung's Development Logs
[언리얼엔진] 7. CCTV 기능 구현
상단으로

티스토리툴바