TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역]Brief Analysis of UE5 Rendering Pipeline

jplee 2023. 9. 19. 17:07

역자의 안부.
최근 외부 컨설팅 일을 하고 있습니다만... CGI 쪽은 다량의 GPU 파티클을 날리지 않는 이상은(뭐 심지어 그때는 GPU 단에서 CPU 쪽으로 처리 쓰레드를 바꿔주면 되는..) 최적화에 대한 이슈등에서 한 발짝 물러서 있는 입장이 되어 있습니다.
그럼에도 분명 다시 중원(그러니까 게임필드)에 다시 등장 할 때는 최적화에 대한 여러가지 처리, 이해, 경험 등이 매우 중요 하겠지요.  어찌 되었든 최근 CGI 쪽 컨설팅 외에 게임쪽 일을 다시 하게 되었네요.
중국에서는 매우 심각한 첼린지들이 산 넘어 산 이었고 엔진팀 동료들과 많은 대화를 하면서 해결 해 나갔습니다만... 귀국 후에 게임 업계를 보면 그에 비할 만한 자이언트 첼리지는 없어 보이는 것이 사실입니다. 그래서 어쩌면 현타가 올 수도 있는데... 이러하든 저러하든... 또 노션을 뒤지다 보니... 같이 볼만한 토픽 어카이브가 있어서 포스팅 해 봅니다. 매우 기본적인 것이지만 혹시 아티스트라면 한번 정도 읽어 봐 주시는 건 어떨가 합니다.
시대가 바뀌었고 모든 데이터의 개연성이 복잡해 졌고 개발 난위도는 리니지 2 때에 비해 많이 복잡해 졌으니까 이제는 아티스트들도 VBO 가 뭔지는 알아야 하고 GPU INSTANCE CONST BUFFER 가 뭔지... 관심을 갖을 시대가 되었다고 생각 합니다.
 
이 글의 원문도 유실 했습니다.... 원문을 쓴 블로거에게 좋은 경험과 인사이트를 공유 해 주신 것에 감사 드립니다.


 
언리얼 엔진 5의 렌더링 파이프라인에 대한 간략한 분석입니다. 이 글은 렌더독의 캡처 결과를 기반으로 작성되었습니다. 저는 **에픽의 개발자가 아니기 때문에 이 글에는 실수가 있을 수 있습니다. 또한 내용은 에픽의 실제 아이디어가 아닌 제 개인적인 이해를 바탕으로 작성되었습니다. 오해의 소지가 있다면 용서해 주세요. 더 나은 설명이 있거나 실수를 지적하고 싶으시면 댓글을 달아주세요. 

(저는 영어 원어민이 아니기 때문에 언어적 실수가 있을 수 있습니다.)
 

준비

여기서는 디버그 프레임을 캡처하는 방법을 설명하겠습니다. UE4에서 렌더 문서 사용법에 익숙하다면 이 부분을 건너뛰어도 됩니다. 

UE5 의 한 프레임을 디버그 정보로 캡처하려면 다음과 같이 해야 합니다:

1. 렌더 문서 다운로드: https://renderdoc.org/
2. UE5의 플러그인 세팅에서 Render Doc 플러그인을 활성화합니다.
플러그인을 활성화한 후 엔진을 재시작해야 합니다.

Engine/Config 폴더에서 ConsoleVariables.ini를 수정합니다. 제 컴퓨터의 경로는 C:\Program Files\Epic Games\UE_5.0EA\Engine\Config입니다.
이 두 줄 앞에서 ';'을 제거해야 합니다. 아래 코드블럭을 참고 해 보세요.

; Uncomment when running with a graphical debugger (but not when profiling)
r.Shaders.Optimize=0
; When this is enabled, shaders will have extra debugging info. This could change patch sizes, uniqueness, etc and will recompile the shaders
r.Shaders.KeepDebugInfo=1

엔진을 시작하기 위해 바로 가기 또는 패치 파일을 만듭니다. 저는 이 단축키를 만듭니다

"C:\Program Files\Epic Games\UE_5.0EA\Engine\Binaries\Win64\UnrealEditor.exe" "C:\Users\LUO\Documents\Unreal Projects\UE5TestProject\UE5TestProject.uproject" -d3ddebug

하나는 테스트 프로젝트의 경로이고 다른 하나는 '-d3ddebug' 매개변수를 추가해야 합니다.

그런 다음 단축키 또는 bat 파일을 사용하여 엔진을 시작하세요.
셰이더가 플래그를 디버그하도록 처음 활성화하는 경우 시간이 오래 걸릴 수 있습니다. 

한 프레임 캡처
    1. 기본 씬에서 일부 항목을 변경할 수 있습니다. 나나이트 메시를 하나 이상 추가하는 것이 좋습니다.
    2. 이 버튼을 눌러 한 프레임을 캡처합니다.
 

렌더링에 대한 켑처 도큐먼트가 자동으로 열리고 해당 캡처 데이터를 로드할 수 있습니다.
 

 

Render Pipeline Minimap

다음은 전체 파이프라인에 대한 간략한 맵입니다. 

나나이트 렌더링은 논리적으로 기존의 디퍼드 렌더링 패스와 관련이 없습니다.

요약하자면, 새로운 UE5 파이프라인에는 다음과 같은 새로운 기능이 포함되어 있습니다:

1. 새로운 GPU 기반 컬링 시스템
2. 나나이트 
3. 루멘 조명.

이 글에서 이 세 가지에 대한 제 추측을 말씀드리겠습니다.
 
 

GPU 기반 컬링

씬 인스턴스 데이터 구조
GPU 주도 컬링을 구현하기 위해 UE5 는 씬의 표현을 GPU 측에 저장해야 합니다.  우리가 다루는 핵심 데이터 구조체는 'GPUScene_InstanceData'이며, 이 구조체에 데이터가 들어 있습니다.

struct FInstanceSceneData
{
	float4x4 LocalToWorld;
	float4x4 PrevLocalToWorld;
	float4x4 WorldToLocal;
	float4   NonUniformScale;
	float4   InvNonUniformScaleAndDeterminantSign;
	float3   LocalBoundsCenter;
	uint     PrimitiveId;
	float3   LocalBoundsExtent;
	uint     LastUpdateSceneFrameNumber;
	uint     NaniteRuntimeResourceID;
	uint     NaniteHierarchyOffset;
	bool     NaniteHasImposter;
	// TODO: bool     CastShadows;
	float    PerInstanceRandom;
	float4   LightMapAndShadowMapUVBias;
	bool     ValidInstance;
};

 

Data Updating

입력: 인스턴스 업로드 버퍼
출력: GPUScene_InstanceData
게임 로직은 CPU 측에서 실행되므로 변환 정보의 업데이트도 CPU 측에서 발생합니다.
UE5에는 이전의 CPU 기반 병렬 처리를 사용한 컬링 시스템과 비교하여 새로운 GPU 기반 컬링 시스템이 추가되었습니다.
CPU 측에서는 업데이트 데이터를 준비합니다. 그리고 컴퓨트 쉐이더를 사용하여 해당 정보에 기반하여 GPUScene_Instance 데이터를 업데이트합니다.

Instance Culling

 
Input: GPUScene_InstanceData
Output: InstanceCulling.VisibleInstanceFlags
인스턴스 컬링은 GPU 장면 업데이트 이후에 발생합니다. 이 단계에서는 각 인스턴스의 바운딩 박스를 가져와 프러스텀 컬링을 수행합니다.

InstanceCulling.VisibleInstanceFlags 버퍼는 패킹된 비트 버퍼로, 인스턴스 가시성 정보를 단일 비트로 담고 있습니다.
 
 

Nanite

Nanite는 UE5의 새로운 유명한 기능으로, UE가 많은 폴리곤을 렌더링할 수 있게 합니다.
주의하세요, Nanite는 DX12의 Mesh Shader나 tessellation을 사용하지 않습니다.
Nanite의 아이디어에 대한 공식 설명을 기다리는 중이기 때문에 Nanite에 대한 자세한 설명은 할 수 없습니다. 그래서 이 부분에서는 렌더링 분석을 기반으로 한 제 개인적인 이해만 설명하겠습니다.
한 개의 Nanite 메시의 렌더링은 가시성 버퍼 생성과 G-버퍼 방출로 나누어져 있습니다.
Nanite 렌더링은 여러 부분으로 분할되어 있으므로, 먼저 미니맵을 제공하겠습니다:

나나이트가 화면에 메시의 대략적인 모양으로 투명한 직사각형 카드를 생성한다고 생각하면 됩니다. 그리고 뎁스, 스텐실, G-버퍼를 생성하기 위해 텍스처를 생성합니다.

Visibility Buffer Building

나나이트 프리 패스는 디퍼드 렌더링의 G-버퍼 빌드 패스처럼 작동하지만 출력은 비저빌리티 버퍼일 뿐이며 하드웨어와 소프트웨어의 두 가지 래스터화 경로가 포함되어 있습니다.

Visibility Buffer

G 버퍼 기반 지연 렌더링의 G 버퍼와는 달리 가시성 버퍼는 매우 간단합니다. 여기에는 트라이앵글을 참조할 수 있는 정보와 몇 가지 추가 정보가 포함된 트라이앵글 인덱스가 포함되어 있습니다.

void UnpackVisPixel(
	UlongType Pixel,
	out uint DepthInt,
	out uint VisibleClusterIndex, 
	out uint TriIndex
	)
{
	const uint2 Unpacked = UnpackUlongType(Pixel);
	VisibleClusterIndex = Unpacked.x >> 7;
	TriIndex = Unpacked.x & 0x7F;
	DepthInt = Unpacked.y;

	VisibleClusterIndex--;
}

가시성 버퍼는 다음과 같습니다:
 

Visibility Buffer Rasterize

나나이트는 비저빌리티 버퍼의 래스터라이즈를 하드웨어와 소프트웨어의 두 가지 경로로 분할했습니다. 그 이유는 쿼드 오버드로 또는 쿼드 오버셰이딩을 피하기 위해서입니다.
쿼드 오버드로에 대해 잘 모르신다면 구글에서 '쿼드 오버드로'를 검색해 보세요. 작은 삼각형(화면에서 2x2 픽셀보다 작은)이 많으면 성능이 정말 나빠집니다.
따라서 나나이트는 큰 트라이앵글의 경우 하드웨어를 선택하고 마이크로 폴리곤의 경우 소프트웨어 래스터라이저를 선택합니다.
래스터라이저에 대한 자세한 내용은 Rasterizer.usf의 MicropolyRasterize를 확인하세요.
(나나이트의 래스터라이즈 코드를 이해하지 못해서 죄송합니다. 용서해주세요)
 
역자 각주
쿼드 오버드로우 ( Quad overdraw )

쿼드 오버드로는 앞서 언급한 문제입니다. 보시다시피 일반적인 오버드로를 보여줍니다. 쿼드 오버드로 문제뿐만이 아닙니다. 반투명 오브젝트를 여러 개 뒤에 넣으면 흰색으로 변합니다.
문제가 있다면, 여기로 이동하여... "풍경" → "모드 관리" → 컴포넌트 "선택" 도구로 이동하세요. 트라이앵글이 너무 많다고 생각되는 컴포넌트를 선택하고 "세부 정보" 탭에서 "LOD 바이어스"로 이동하여 예를 들어 1 또는 2 또는 더 높게 설정합니다. 보시다시피 사용되는 버텍스의 양이 줄어든 것을 확인할 수 있습니다. 이 방법은 정말 중요한 영역을 제외하고 랜드스케이프에서 트라이앵글의 양을 줄이는 데 매우 좋은 방법입니다.
( 출처 : https://unrealartoptimization.github.io/book/profiling/view-modes/ )

 

Nanite to GBuffer

가시성 버퍼를 사용하면 원본 트라이앵글 데이터에 액세스할 수 있으므로 머티리얼 정보도 얻을 수 있습니다. 다음 단계는 기존의 디퍼드 파이프라인에서 G-버퍼에 쓰는 것입니다.

나나이트는 다음과 같이 저해상도(32 x 20) 머티리얼 인포 텍스처를 사용하여 화면을 향한 2D 카드 세트를 생성합니다:

The 2D cards are like this:

네, 나나이트 결과물은 2D 스프라이트처럼 렌더링됩니다. 둠에 등장하는 적들을 기억하시나요?

해상도는 낮지만 가시성 버퍼를 기반으로 선명한 가장자리를 얻을 수 있으므로 모든 것이 괜찮을 것입니다.

그런 다음 마스킹된 스프라이트이므로 디퍼드 파이프라인에서 했던 것처럼 직접 렌더링할 수 있습니다:

Lumen

새로운 GI 솔루션인 Lumen은 스크린 스페이스 레이 트레이싱과 사인드 디스턴스 필드 트레이싱을 포함한 혼합된 글로벌 일루미네이션 시스템입니다. 공식 기술 세부 페이지를 읽는 것을 정말로 추천합니다: https://docs.unrealengine.com/5.0/en-US/RenderingFeatures/Lumen/TechOverview/
공식 페이지에는 이미 기본 개념을 이해하기에 충분한 정보가 포함되어 있으므로, 여기서는 너무 많은 시간을 소비하지 않겠습니다.
Lumen은 GI를 두 부분으로 나눕니다. 가까운 거리에서는 화면 공간 추적을 사용하여 더 나은 결과를 얻을 것입니다. 그리고 다른 부분에서는 Lumen은 장면을 나타내는 카드 세트를 구축한 다음 빠른 추적을 위해 부호화된 거리 필드를 가속 구조로 사용할 것입니다.

전체 과정은 여러 단계로 구성되어 있으며, 다음과 같이 요약하였습니다:

  1. 카드 구성하기
  2. 카드의 텍셀에서 추적하여 카드의 라디오시티 업데이트하기
  3. LumenReflections 단계에서 SDF로 카드 추적하기

그래서, 네, 조명을 빠르게 변경할 때 카드의 라디오시티가 여러 프레임에 걸쳐 업데이트되므로 대기 시간이 발생합니다.
 

루멘 씬 조명

이 단계에서는 루멘이 카드의 모양을 업데이트하고 방사도를 계산하며 투명한 조명을 준비합니다.
카드의 모양은 거리에 따라 변화합니다:

또한 표면 텍스처 해상도도 변경됩니다. 
 

루멘에는 아틀라스 텍스처 세트가 포함되어 있으며 가상 텍스처 시스템처럼 작동합니다.

루멘 간접 조명 및 AO

그런 다음 루멘은 동적 및 정적 객체에 대한 간접 조명과 AO를 생성하기 위해 프로세스를 두 부분으로 나눕니다: 화면 공간 프로브 패스 및 반사 패스.
(알림: 아직 이러한 패스의 세부 사항을 작업 중이므로 많은 세부 사항을 논의하지 못한 점 양해 부탁드립니다)
먼저, 화면 공간 추적을 수행합니다:
 

결과는 정말로 노이즈가 많습니다.
그런 다음, Lumen은 메시의 SDF를 추적합니다.
 

그런 다음, 복셀을 추적합니다:
 

Reflection solver pass 후에 결과는 다음과 같습니다:
 

마지막으로, Temporal reprojection을 통해 시간 정보를 재사용하면 결과물은 매우 부드럽고 노이즈가 완화 될 것입니다:
 

SDF(부호화된 거리 필드)를 사용한 트레이싱

레이 트레이싱을 훨씬 빠르게 만들기 위해서는 거대한 빈 공간을 건너뛰는 것이 핵심 아이디어입니다. 그래서 Lumen은 이를 달성하기 위해 부호화된 거리 필드를 사용합니다. 공식 문서에서 언급한 대로, 이는 전역 거리 필드와 메시 거리 필드로 구성됩니다.

각 위치의 SDF에는 가장 가까운 표면까지의 거리가 포함되어 있습니다. 따라서 이 거리는 사실상 무언가에 충돌하지 않고 레이가 전진할 수 있는 '안전 거리'를 나타냅니다. 또한 큰 빈 공간의 경우 거리가 훨씬 더 크므로 레이는 작은 반복으로 해당 공간을 건너뛸 것입니다.
SDF를 사용한 추적에 대한 자세한 내용은 Epic의 DFAO 및 DFGI 문서를 확인하십시오:
https://docs.unrealengine.com/4.26/en-US/BuildingWorlds/LightingAndShadows/MeshDistanceFields/
더 많은 코드 세부 정보는 다음을 확인하십시오:

  • SDF 추적 부분 : LumenTracingCommon.ush의 RayTraceSingleMeshSDF 함수.
  • SDF 추적 결과를 얻은 후 카드 데이터를 가져오는 방법 : LumenTracingCommon.ush의 SampleLumenMeshCards.

Reference

  1. https://docs.unrealengine.com/5.0/en-US/RenderingFeatures/
  2. https://unrealartoptimization.github.io/book/pipelines/pixel/