티스토리 뷰

개발/UE5

[Rendering] First Person Rendering

LazySonic 2025. 9. 3. 14:59

참고 발표

First-Person Rendering in Unreal Engine 5.6 | Unreal Fest Orlando 2025

 

언리얼 엔진에서 1인칭 솔루션을 제공하는 이유

도전과제

더보기

1. 씬 클리핑

 

 

2. Field of View

 

3. Fake Legs

4. Lighting, Shadow

5. Reflections

도전과제 요약정리


기존 해결방법

에셋 기반으로 해결 하던 방식

Headless Mesh Floating Arms


렌더링 기법을 통해 해결 하던 방식

1. Compositing

1인칭과 3인칭 각각 따로 그린 뒤 씬을 합치는 방법을 고안해봤으나,

해당 방법에서는 빛과 그림자를 결합하는 과정에서 해결 불가능한 문제가 생기며, 비용 또한 높다는 단점이 있다.

 

2. Seperate Render Pass

1인칭 메시를 먼저 그린 뒤 뎁스 스텐실과 같은 장치를 통해 3인칭 뷰 정보가 이를 덮어 씌우지 못 하도록 그리는 방법이다.

이 방법은 꽤 괜찮지만, 1인칭 메시가 Depth 정보를 모두 덮어 씌우기 때문에 Depth 기반의 Post Process를 사용하지 못 하는 문제가 발생한다.

 

또한, 이 문제는 반투명 객체나 파티클과의 결합에서도 문제를 야기하기 때문에 일반적인 게임에선 사용하기 어렵다.

 

3. Custom Projection Matrix

1인칭 FOV 데이터를 분리하여 1인칭에 맞는 메트릭스를 별도로 구성, 이를 기반으로 1인칭 메시들을 그려 빛과 그림자가 드리우게 하고 3인칭 이후에 글려 클리핑을 방지하도록 하는 방법이다.

다만, Fake Legs과 Legs의 지면 클리핑 문제는 해결해야 하는 문제이며 별도의 매트릭스를 관리해야한다는 점에서 다소 복잡성이 높다.

그렇지만, 언리얼엔진은 이 방법이 그렇게 어려운건 아니라 생각해 이 아이디어를 바탕으로 1인칭 FOV 데이터를 분리, 별도의 메트릭스를 구성하여 렌더링하도록 하는 방법을 채택했다.

 

언리얼에서 제공하는 솔루션

Cliping

Full Scale Geometry Scaled Geometry

전체 사이즈 지오메트리로 라이팅을 연산하는 경우 굉장히 정확하게 그려짐으로, 대부분의 상황에 제대로 보인다.

다만, 클리핑 되는 상황이 발생하는 경우 실제로 클리핑 되어야 하는 안쪽의 경우 어둡기 때문에 사진과 같이 그림자 지게 보인다.

다소 부정확한 라이팅이 일반적으로 사용되어 이따금씩 플리커링 처럼 그림자가 생기는 이슈가 있을 수 있다.

다만, 클리핑되는 지역에서도 정상적인 느낌으로 그림자를 줄 수 있다는 장점이 있다.

 

위 두 상황을 적절히 타개하기 위해 언리얼 엔진에서는 Scaling and Morphing을 적용하기로 했다.

 

수식 설명에 필요한 기본 수학 지식

(추가 작성 예정)

행렬변환

역행렬

프로젝션 행렬

 

솔루션에 사용되는 수식 설명

FOV Correction

M * V * P_fp = M * 'T' * V * P = M * V * P_fp * P^-1 * V^-1 * V * P

 

우리는 여기서 변환행렬 'T'를 구하고자 한다.

 

유도 과정

M*V*P_fp = M*T*V*P => M 역행렬을 양쪽에 곱함 => V*P_fp = T*V*P

양쪽에 V역행렬을 곱함=>P_fp = T*P => P 역행렬을 곱함 => T = P_fp * P^-1

=> M*V*P_fp = M * (P_fp*P^-1) * V * P

 

※ 그럼 여기서 왜 V^-1*V를 중간에 넣어주는가?

 

그 이유는 T 연산 자체가 카메라 공간(V)에서 이뤄져야 하는 연산이기 때문이다. 따라서 M * V 로 카메라 공간으로 이동 후 T 연산 한 결과물은 카메라 공간상의 결과물이 됩니다. 이를 다시 월드로 돌리기 위해 V^-1을 뒤에 곱해줍니다.
이 결과물이 M*V*P_fp*P^-1*V^-1*V*P가 됩니다. 여기서 V^-1*V가 항등행렬이라 당황스러울 수 있지만 흐름을 따라가면 말한대로 공간상의 정의를 설명하기 위한 중요 요소가 됩니다.

고로 T = V*P_fp*P*V^-1이 됩니다.

 

With Scaling

S = ScaleMatrix(s,s,s)

T = V*P_fp*P^-1*S*V^-1

 

Simplify

P_fp*P^-1=(f,f,1)

위와 같이 정리가 가능한 이유는

프로젝션 행렬의 경우 위와 같은 행렬을 갖고 (Theta는 FOV) 이는 FOv에 따라 x,y 축으로 확대 축소가 이뤄진다는 뜻 이다.

여기서 P와 P_fp의 차이는 FOV의 값 차이 뿐이다. 이에 따라 P_fp에 P 역행렬을 곱하면 기저

 

f = tan(FOV/2) / tan(FOV_fp/2)

 

Final Transform

T = V*ScaleMatrix(f*s, f*s, f*s)*V^-1

위 과정에 의한 Final Transform을 구성하는 로직은 SceneView.h, FViewMatrices::Init 내 FirstPersonTransform 연산부에서 확인 가능합니다.

 

이렇게 구성된 FirstPersonTransform은 

FViewInfo::TUniquePtr<FViewUniformShaderParameters> CachedViewUniformShaderParameters

위 URV 구조체에 아래와 같이 등록되며

 

#define VIEW_UNIFORM_BUFFER_MEMBER_TABLE

VIEW_UNIFORM_BUFFER_MEMBER_PER_VIEW(FMatrix44f, FirstPersonTransform)
VIEW_UNIFORM_BUFFER_MEMBER_PER_VIEW(FMatrix44f, PrevFirstPersonTransform)

 

 

URV로 등록된 FisrtPersonTransform을 각종 쉐이더의 VertexPass 에서 'MaterialTemplate.ush' 에 있는 'ApplyMaterialFirstPersonTransform' 함수를 통해 WorldPositionWithWPO로 변환한다.

관련 로직은 'BasePassVertexShader.usf' 에서 확인 가능하다.

 

Fake Legs

위에서 이야기했던 내용에 따르면 1인칭 메시가 그려지는 공간을 왜곡함으로써 클리핑과 조명 렌더링에 대한 영역을 해결해 나갔다는 것을 알 수 있다.

그렇다면 팔이 아니라 1인칭 다른 바디 파츠에 대해선 어떻게 처리해야 할까?

위와 같다면, 다리와 몸통 같은 경우도 공간이 왜곡되어 바닥과 일치하지 않는 장애가 생기진 않을까?

영상속 답변은 '생긴다'. 이에, 솔루션으로 제공한 기능이 'World Space Interpolation' 마테리얼 노드다.

해당 노드는 'MaterialTemplate.ush' 내에서 다음과 같이 3인칭 공간과 1인칭 공간 사이를 보간하여  WPO를 구성하는데 사용된다.

void ApplyMaterialFirstPersonTransform(FMaterialVertexParameters Parameters, inout float3 WorldPositionWithWPO)
{
#if SUPPORT_FIRST_PERSON_RENDERING
	BRANCH
	if (IsFirstPerson(Parameters))
	{
		const float LerpAlpha = GetMaterialFirstPersonInterpolationAlpha(Parameters);
		const float3 FirstPersonPosition = TransformToFirstPerson(WorldPositionWithWPO, LerpAlpha);
		WorldPositionWithWPO = FirstPersonPosition;
	}
#endif
}

이 노드를 이용해 실질적인 Fake Legs를 구현한 샘플은 FPS Sample에서 다음과 같이 확인할 수 있다.

의미는 스피어 마스크에 시각적 장애가 없는 임의의 상수 Scale을 넣어 절대 월드 위치와 1인칭 공간상의 절대 위치 차를 보간 알파값으로 등록함으로써 시각 아티펙트를 수정한다.

 

Casting Scene Shadows

오로지 그림자만 캐스팅하고 프리미티브는 숨긴다.

1인칭 그림자는 별도 쉐도우 맵을 그리고 씬과 융화된다.

관련한 로직은 'SceneRendering.h'와 'ShadowSetup.cpp', 'ShadowSceneRenderer.cpp' 를 참고하면 도움이 된다.

- ShadowSetup에서 FSortedShadowMap 내 VirtualShadowMapShadows를 1인칭 여부 확인 후 생성한다.

- ShadowSceneRenderer에서 1인칭 그림자인지 확인 후 

 

HWRT Reflections

 

 

실무 세팅 및 적용

[업무에 쫓겨 이후 작성 예정]