역자 주.
요 몇일동안은 아트팀을 직접 지원하는 셰이더 함수작업을 했습니다. 엔진 소스를 수정해야 할 필요가 없는 수요 부터 우선순위를 올려서 작업중이죠. 최대한 엔진 소스를 고치지 않는 선에서 시각적으로 인가 할 수 있는 수준으로 구현하는 것이 가장 올바른 선택이지않나 생각합니다. 특히 이런 사용엔진을 쓰는 경우에는 말이죠. ( 우린 소니 산타모니카 스튜디오가 아니니까요... 하하 ) 하지만 특정 아바타 클래스들을 위한 심층적인 수요를 만족하기 위해서는 소스코드에 접근해야 하는 경우가 분명히 필요 합니다. 또는 그 외에 더 깊은 효과를 구현하기 위해서도 그렇고 최적화를 달성하면서도 그렇고 말이죠... 분명히 RDG를 잘 이해하고 사용해도 사용하는 것이 바람직해 보입니다. 그래서 아무튼... 또 내일 출근 하면서 1시간 정도 소요되는 지하철 출근시간을 알차게 보내기 위해서 이렇게 RDG 101 애픽게임즈가 몇년 전 발표했던 PPT 를 주석번역 추가하여 올려봅니다.
이 슬라이드 자료는 언리얼 엔진의 새로운 렌더링 종속성 그래프 시스템(이하 RDG) 사용법을 간략하게 설명합니다.
RDG 아키텍처를 이해하려면 먼저 언리얼 엔진에서 셰이더 파라미터가 어떻게 노출되는지 이해해야 합니다. 셰이더 파라미터 시스템은 RDG 설계의 기본 구성 요소입니다.
먼저 가상의 셰이더 소스 파일에 전역적으로 선언된 다음 셰이더 입력 세트를 살펴봅니다.
이상적으로는 이러한 셰이더 입력을 C++ 구조체에 매핑하여 사용자가 채우고 컬렉션으로 제출하는 것이 좋습니다. 안타깝게도 이러한 표현은 작성하기는 쉽지만 셰이더 컴파일러 런타임에서 유효성을 검사할 수 없습니다.
언리얼 엔진은 매크로 시스템을 사용하여 셰이더 파라미터 구조체를 정의합니다. 이 표현은 비교적 쉽게 작성할 수 있으며 셰이더 컴파일러를 위한 리플렉션 정보를 자동으로 생성합니다.
셰이더 파라미터 매크로 시스템의 특징은 컴파일 타임 리플렉션 메타데이터를 자동으로 생성한다는 점입니다. 모든 사용자는 런타임에 셰이더 파라미터 구조체의 내용을 통과하여 멤버별 정보에 액세스할 수 있습니다. 메타데이터는 공통 코드(예: RHI/RDG)에서 구조체의 보이드* 포인터 표현을 순회하고 리소스를 추출하는 데 필요합니다.
매크로 시스템의 또 다른 기능은 셰이더 데이터의 자동 정렬입니다. 언리얼 엔진은 이식성을 위해 셰이더 플랫폼에 구애받지 않는 데이터 정렬 규칙을 사용합니다.주요 규칙은 각 멤버의 크기가 2의 다음 제곱으로 정렬되지만, 4바이트보다 큰 경우에만 정렬된다는 것입니다. 예를 들어 포인터는 8바이트로 정렬됩니다(32비트 플랫폼에서도);Float, uint32, int32는 4바이트 정렬입니다;FVector2D, FIntPoint는 8바이트로 정렬됩니다;FVector와 FVector4는 16바이트로 정렬됩니다.각 멤버의 자동 정렬은 위의 설명에 나와 있는 것처럼 필연적으로 Padding을 생성합니다.
Padding을 최소화하거나 제거하기 위해 구조체를 구성하는 것도 고려해 보세요. 파라미터의 순서는 셰이더 소스에 어떤 식으로든 영향을 미치지 않으므로 GPU에 업로드되는 바이트 수를 줄일 수 있는 위험 부담이 적은 변경입니다. 위의 예제에서 'float World'를 위로 이동하면 FVector2D ViewportSize( a three float 구조체)에 필요한 4바이트 Padding 영역에 위치할 수 있습니다. 다시 말하지만, 이 three float 벡터는 16바이트(또는 4개의 플로트)로 정렬되므로 싱글 플로트 멤버를 네 번째 플로트 슬롯으로 이동하면 padding이 제거됩니다.
위의 코드 스크랩은 빽빽하게 채워진 셰이더 파라미터 구조체를 보여줍니다. 각 FVector {x, y, z} 뒤에는 16바이트 메모리 슬롯을 채우기 위한 실수(float)가 있습니다.
과거에는 셰이더 작성자가 느슨한 플로트를 4폭의 커다란 벡터에 수동으로 패킹해야 했습니다. Padding 규칙을 준수하는 한 SHADER_PARAMETER_STRUCT 시스템을 사용할 때는 이 작업이 필요하지 않습니다. 이 데이터를 느슨하게 유지하면 일반적인 파라미터 벡터(예: MyFeatureParams.{x, y, z, w})를 피할 수 있어 가독성이 향상됩니다. 이는 셰이더 소스 파일에서도 마찬가지이며, 친숙한 이름으로 느슨한 파라미터를 간단히 선언할 수 있습니다.파라미터 배열은 예외입니다. 시스템은 파라미터를 단일 배열로 명시적으로 병합하지 않으므로 사용자가 직접 병합해야 합니다.
이제 셰이더에 대한 셰이더 파라미터 구조가 생겼으니 셰이더 자체를 정의해야 합니다. 기존 셰이더 프레임워크에 새 셰이더 파라미터 시스템을 사용하는 방법은 간단합니다.
먼저 셰이더 클래스에 SHADER_USE_PARAMETER_STRUCT() 매크로를 추가합니다.
이렇게 하면 클래스의 생성자가 구현되고 셰이더가 선언된 셰이더 파라미터 구조체를 사용하도록 구성됩니다.
레이트레이싱 셰이더는 RHI에서 처리하는 방식에 약간의 차이가 있기 때문에 대신 SHADER_USE_ROOT_PARAMETER_STRUCT()를 사용해야 합니다.
이 요구 사항은 일시적인 것으로 향후 셰이더 시스템 개정에서 제거될 예정입니다.
구성이 완료되면 셰이더는 클래스에서 FParameters 멤버를 찾습니다.
이 멤버는 사용하려는 셰이더 파라미터 구조체 유형에 할당해야 합니다. 당연히 셰이더 파라미터 구조체는 셰이더당 하나만 허용됩니다.
셰이더 파라미터 구조체를 효과적으로 구성하는 방법을 보여주는 다음 슬라이드를 기대해 주세요.
이 두 단계만 거치면 셰이더 파라미터 구조체 시스템 사용을 시작할 수 있습니다.
클래스는 셰이더 파라미터를 자동으로 반영하고 바인딩합니다.
셰이더 컴파일러는 이 구조체를 기준으로 바인딩의 유효성도 검사합니다. 예를 들어 C++의 유형은 이제 자동으로 실패하는 대신 유용한 오류 메시지를 생성합니다.
파라미터 구조체와 1대1 관계가 있는 셰이더의 모범 사례는 클래스에서 직접 파라미터를 (FParameter로) 선언하는 것입니다. 가장 간단하고 명확한 접근 방식입니다.
이제 셰이더가 셰이더 파라미터 구조체로 구성되었습니다. 이제 이 파라미터에 값을 할당하고 RHI에 푸시해야 합니다.먼저 FParameters 유형은 단순한 구조체이므로 인스턴스화하여 채울 수 있습니다. 이제 디버거에서 구조체의 전체 내용을 볼 수 있으므로 디버깅이 간단해집니다.새로운 디자인의 가장 큰 차별점은 셰이더 파라미터 설정이 셰이더 클래스에서 분리되었다는 점입니다.
파라미터 설정을 셰이더 클래스 내부에 상용구와 함께 포함하지 않고 상위 레벨의 패스 코드로 이동하여 명확성을 높였습니다. 이렇게 하면 데이터의 흐름이 더 자연스러워져 읽기 및 디버깅이 더 쉬워집니다.
셰이더 파라미터 시스템은 파라미터를 RHI 명령 목록으로 푸시하는 헬퍼 함수인 SetShaderParameters를 제공합니다.
이를 통해 디버거에서 셰이더 파라미터 값을 검사할 수 있는 명확한 위치를 제공합니다.이 함수는 사용 중인 특정 셰이더 순열에 대해 구조체의 내용을 검증합니다. 필요한 리소스가 null 로 남아 있으면 오류가 발생합니다. 참고로 구조체의 모든 멤버는 유형에 따라 기본값이 null, 0 또는 기본 생성자로 초기화됩니다.이는 또한 순열이 리소스를 사용하지 않는 경우 리소스를 안전하게 무시할 수 있음을 의미합니다.
유니폼 버퍼는 동일한 셰이더 파라미터 구조체 모델을 활용하지만, 파라미터를 '전역'으로 선언하는 전용 매크로가 있습니다.
글로벌 셰이더 파라미터 구조는 셰이더 코드에 자동으로 반영되는 추가 기능(및 오버헤드)과 함께 일부 HLSL 구문 특수성을 처리하는 기능이 있습니다.
셰이더 파라미터 구조체는 SHADER_PARAMETER_STRUCT_REF() 변형을 통해 균일 버퍼를 참조할 수 있습니다. 이 셰이더 바인딩 이름은 IMPLEMENT_GLOBAL_SHADER_PARAMETER_STRUCT()에 의해 정의되므로 참조된 균일 버퍼의 멤버 이름은 C++에서만 사용됩니다.마지막으로, 전역으로 정의된 바인딩 이름은 하나뿐이므로 한 번에 하나의 균일 버퍼 인스턴스만 바인딩할 수 있습니다.
언리얼 엔진의 셰이더 파라미터 시스템에 대한 이해를 바탕으로 이제 RDG의 기본을 소개해 드리겠습니다.
그래프 렌더링 API는 Builder 에서 시작됩니다. 이 Builder Interface 는 리소스 및 패스의 즉시 모드 선언을 통해 그래프 설정을 용이하게 합니다. 그래프는 설정 단계를 통해 점진적으로 빌드됩니다. 모든 설정이 완료되면 그래프가 실행되고, 그래프는 모든 패스를 종속성 정렬 순서대로 컴파일하고 호출합니다.
무엇보다도 이 설계는 GPU 작업의 효율적인 스케줄링과 보다 적극적인 메모리 관리를 가능하게 합니다.
API는 사용하기 쉽습니다. 그래프 빌더 인스턴스를 정의하고 설정 작업을 수행한 후 실행하기만 하면 됩니다. 빌더는 단일 스레드로 실행되며(항상 렌더 스레드에서!) 실행 사이에 상태가 보존되지 않습니다. 전체 그래프는 매 프레임마다 다시 빌드됩니다.진정한 의미의 전체 프레임 최적화를 위해서는 전체 엔진을 동일한 그래프 빌더 인스턴스를 사용하도록 포팅하는 것이 궁극적인 목표입니다. 이 작업은 시간이 걸립니다. 기존 코드를 '포팅'하거나 새 렌더링 코드를 작성할 때 선호하는 패턴은 로컬 그래프 빌더 인스턴스를 정의하고 이를 수동으로 실행하는 것입니다. 엔진의 섹션이 포팅되면 모두 동일한 빌더 인스턴스로 병합됩니다.
새 텍스처를 만들려면 그래프 빌더에서 CreateTexture를 호출하기만 하면 됩니다. 그러면 그래프 빌더 인스턴스의 수명 동안 유효한 RDG 텍스처 핸들이 반환됩니다. 물리적 GPU 리소스가 즉시 할당되지 않기 때문에 별도의 유형이 필요합니다. RDG 포인터는 이를 사용하는 각 패스에 대해 유효성이 보장되는 미래 리소스에 대한 핸들을 나타냅니다. 리소스가 패스에 연결되는 방법에 대한 자세한 내용은 곧 다루겠습니다. 위 코드에서는 그래프에 텍스처를 선언하기만 했습니다.
마찬가지로 빌더를 사용하여 리소스(텍스처 또는 버퍼)의 뷰를 생성할 수 있습니다. 위의 코드 예제는 지정된 밉 레벨에서 텍스처의 UAV를 생성합니다.
SRV도 지원되며 동일한 일반 생성 패턴을 따릅니다.
RDG 리소스를 패스와 연결(즉, 생성 또는 소비)하기 위해 _RDG_ 셰이더 파라미터로 선언된 수정된 SHADER_PARAMETER_STRUCT를 사용합니다. 이 셰이더 파라미터의 변형은 C++ RDG 리소스 포인터를 받아들입니다. 그래프는 패스에서 이 구조체를 탐색하고 패스의 관련 용도에 맞는 RDG 리소스를 등록합니다. 그래프가 패스에서 리소스가 어떻게 사용되는지에 따라 리소스의 수명을 추론할 수 있기 때문에 메모리 관리의 이점이 발휘되는 곳입니다.
위의 코드를 보면 왜 다른 '패스' 파라미터 구조체가 아닌 셰이더 파라미터 구조체를 사용하여 패스 파라미터를 표현하는지 의문을 가질 수 있습니다. 이는 셰이더와 1대1인 패스(엔진에서 가장 빈번하게 사용되는 패스 유형)를 쉽게 작성하기 위한 의도적인 디자인 결정이었습니다. 위의 셰이더 파라미터 구조체는 두 가지 용도로 사용됩니다. 패스 설정: 거의 모든 리소스가 셰이더를 통해 생성/소비되므로 셰이더 메타데이터 리소스가 패스에서 어떻게 사용될지(예: 읽기 대 쓰기) 알 수 있습니다. 이 정보는 리소스 전환에 매우 중요합니다. 파라미터 값을 명령 목록으로 설정하기: 패스 내의 셰이더는 패스 리소스를 '추출'할 필요 없이 전체 구조체를 바인딩하기만 하면 됩니다. RDG 리소스는 RHI 셰이더 파라미터 바인딩 코드에 의해 자동으로 역참조됩니다.
패스/리소스 연결을 셰이더 파라미터 구조체에 결합하면 단일 셰이더 패스를 작성하는 것이 간단해집니다. 상용구가 대폭 감소하여 전체 코드베이스의 유지보수성이 향상됩니다.
요약하자면 셰이더 파라미터 구조체는 RDG 리소스로 작업할 수 있는 확장을 제공합니다. RDG 패스는 이 셰이더 파라미터 구조체를 제공받고, 이 구조체는 패스 리소스를 설정하는 데 필요한 모든 정보를 탐색하고 추출합니다.
그래프에 패스를 추가하기 위해 빌더는 AddPass 함수를 제공합니다. 이 함수는 그래프 실행 중에 호출되는 람다를 받습니다. 람다는 RHI에 작업을 디스패치하기 위한 RHI 명령 목록을 제공합니다. 이 패스는 셰이더 파라미터 구조체를 받아들이고, 이 구조체의 모든 _RDG_ 파라미터를 반영합니다. 다른 모든 셰이더 파라미터는 무시됩니다.
각 RDG 패스는 외부 GPU 프로파일러뿐만 아니라 ProfileGPU 명령에 노출되는 형식화된 이름을 허용합니다. 패스를 고유하게 식별할 수 있는 충분한 정보를 입력하는 것이 좋습니다. 이 정보는 성능상의 이유로 릴리스 빌드에서 삭제된다는 점에 유의하세요.
앞서 설명한 것처럼 그래프는 패스가 액세스할 RDG 리소스를 알아야 하며, 이는 패스 파라미터 구조체에서 가져옵니다. 하지만 그래프 실행이 지연되기 때문에 이 구조체는 그래프의 수명에 맞게 할당되어야 합니다. 그래프 빌더 인터페이스는 그래프의 수명에 최적화된 AllocParameters 함수를 제공합니다. 이 객체에는 PassParameters 명명 규칙을 사용하는 것이 좋습니다.
패스 매개변수 구조체 및 패스에 관한 몇 가지 규칙: 패스 매개변수 인스턴스는 AddPass를 통해 패스에 연결되면 변경할 수 없는 것으로 간주됩니다. 패스는 포인터의 소유권(파괴 포함)을 가정합니다. 하나의 패스만 패스 매개변수 인스턴스를 사용할 수 있습니다(1대1이어야 함). 여러 패스를 지원하려면 각각에 대해 고유한 인스턴스를 인스턴스화해야 합니다. 패스 매개변수 인스턴스를 할당하고 패스에 연결하지 않는 것은 유효하지 않습니다. 패스 람다에서 RDG 유형의 기본 RHI 리소스에만 액세스할 수 있으며, 해당 RDG 유형이 패스와 연결된 패스 매개변수 인스턴스에서 선언된 경우에만 가능합니다.
이러한 조건을 위반하면 RDG 유효성 검사 계층에서 오류를 발생시킵니다.
실행 중에 그래프는 제공된 패스 실행 람다 함수를 호출합니다. 여기에서 패스에 대한 모든 RHI 제출 로직이 실행됩니다. 패스 파라미터에 선언된 모든 RDG 리소스는 이 함수 중에 완전히 할당되어 사용 가능한 것으로 간주됩니다(즉, 기본 RHI 유형에 액세스해도 안전합니다).
람다 캡처 규칙에 대한 참고 사항: UE4 C++ 표준은 [=] 사용을 금지하고 있으며, 람다 실행이 지연되기 때문에 [&] 사용도 불가능합니다. 대신 멤버를 개별적으로 명시적으로 캡처해야 합니다.
래스터 파이프라인을 사용하는 패스를 제작할 때는 패스 파라미터 구조체에 RENDER_TARGET_BINDING_SLOTS() 매크로를 추가하세요. 이렇게 하면 패스에 의해 선택될 렌더 타깃 및 뎁스 스텐실에 대한 입력이 노출됩니다. 이 데이터는 셰이더에서 무시됩니다.
렌더 타깃은 바인딩 배열로 노출됩니다. 각 바인딩은 로드/저장 작업과 함께 RDG 텍스처를 받아들입니다.
마찬가지로 뎁스 스텐실은 별도의 바인딩으로 노출됩니다. 또한 뎁스 및 스텐실에 대한 로드/저장 작업을 별도로 수행할 수 있을 뿐만 아니라 RDG 텍스처도 사용할 수 있습니다. 또한 텍스처의 읽기 또는 읽기-쓰기 여부를 지정할 수 있습니다.
픽셀 셰이더에 UAV를 바인딩할 수도 있습니다.
렌더 그래프는 현재 텍스처 할당을 제어하기 위해 IPooledRenderTarget 인터페이스를 활용합니다. 기존 리소스를 그래프로 가져와야 할 때가 있습니다(특히 RDG 변환 프로세스 중). 빌더는 기존 렌더 타깃에 의해 지원되는 RDG 텍스처 인스턴스를 반환하는 RegisterExternalTexture를 노출합니다.
풀링된 렌더 타깃 포인터는 FRDGTexture에서 추출할 수도 있습니다. 이를 통해 그래프 호출 전반에 걸쳐 리소스의 내용을 보존할 수 있습니다.
그러나 그래프가 실행될 때까지 추출이 지연되는데, 이는 그래프 내 리소스의 수명에 따라 실행 중에 리소스가 할당될 수 있기 때문입니다.
따라서 API는 그래프가 실행될 때 채워질 포인터를 제공할 수 있는 QueueTextureExtraction을 노출합니다.
API는 버퍼와 뷰를 생성하는 메서드를 자연스럽게 노출합니다.
버퍼는 셰이더에서 SHADER_PARAMETER_RDG_BUFFER_SRV()를 사용하여 SRV를 통해서만 읽을 수 있습니다.
간접 그리기/디스패치 버퍼는 셰이더에서 직접 사용하지 않는다는 점에서 약간 독특합니다. 대신 패스 파라미터에 RDG 버퍼로 선언한 다음 패스에서 직접 RHI 간접 그리기 버퍼를 사용하면 됩니다.
RDG는 텍스처 시각화 툴에 FRDGTexture를 자동으로 노출합니다. 모든 쓰기 작업이 기록됩니다. 콘솔에서 CreateTexture()에 지정한 디버그 이름을 직접 입력하기만 하면 표시됩니다.
동일한 디버그 이름으로 새 텍스처를 수정하거나 재정의하는 패스가 여러 개 있는 경우 캡처 패스는 모든 패스를 캡처하지만 마지막 캡처 인스턴스만 표시합니다.
N 구문을 사용하여 시각화할 리소스의 버전을 선택할 수 있습니다.
지연 실행의 가장 큰 단점은 디버깅을 어렵게 만든다는 것입니다. 패스 실행 중에 문제가 발생하면 문제의 원인이 설정 단계에 있을 수 있습니다. 하지만 설정 스택 프레임은 이미 오래 전에 사라졌기 때문에 모든 중간 설정 정보를 잃게 됩니다. 설정 코드에 중단점을 설정할 수도 있지만, 문제가 허위로 발생하거나 패스가 일반적인 실행 경로(예: 다운샘플 작업)일 수 있습니다.
이 문제를 해결하기 위해 RDG는 즉시 모드로 실행할 수 있습니다. 이 모드는 추가 패스 중에 패스를 즉시 실행합니다. 이 모드는 전체 설정 프로세스 동안 모든 할당에 대한 참조를 유지해야 하므로 많은 메모리를 사용합니다(리소스가 사용될지 여부는 알 수 없습니다!). 하지만 실행 중에 중단점을 설정하고 문제가 발생한 부분을 명확하게 콜 스택으로 확인할 수 있다는 점에서 디버깅의 이점이 매우 큽니다. 이 모드는 시작 시 또는 런타임에 활성화할 수 있습니다.
이 예제에서는 뎁스 오브 필드 IndirectScatter 패스에서 중단점에 도달했습니다. 즉시 모드를 활성화하면 문제를 조사하기 위해 설정 단계에서 준비된 ConvolutionTextures 데이터 구조의 내용을 찾아볼 수 있습니다.
흔히 저지르는 실수는 필수 셰이더 파라미터를 비워 두는 것입니다. 앞서 언급했듯이 SetShaderParameters 함수는 필요한 셰이더 파라미터가 모두 있는지 확인합니다. 하지만 이 대신 설정 시 이러한 문제를 잡아내는 것이 좋습니다. 따라서 패스에 추가하기 전에 모든 파라미터가 있는지 확인할 수 있도록 ValidateShaderParameters가 제공됩니다.
RDG는 그래프 설정과 관련된 문제뿐만 아니라 성능 문제도 잡아낼 수 있는 풍부한 유효성 검사 계층을 제공합니다. 더 많은 비용이 드는 유효성 검사는 명령줄을 통해 또는 런타임 중에 활성화할 수 있는 CVar 뒤에 숨겨져 있습니다. 유효성 검사의 일반적인 사례 중 하나는 선언되었지만 패스에서 실제로 사용되지 않는 리소스를 포착하는 것입니다.
AddPass는 그래프 실행 함수에 의해 실제로 사용되지 않더라도 전달 매개변수 구조체에서 발견된 모든 RDG 리소스를 처리합니다! 여기에는 메모리 할당 및 배리어/레이아웃 전환 수행과 같이 비용이 많이 드는 작업이 포함됩니다. 불필요한 비용을 없애려면 패스에 실제로 사용되는 리소스만 제공하는 것이 중요합니다. 셰이더 파라미터는 기본값이 0으로 초기화되므로 가장 간단한 해결책은 사용 조건에 따라 브랜치 할당을 통해 필요한 리소스를 화이트리스트에 추가하는 것입니다(위 예시).
마찬가지로 동일한 로직을 사용하여 특정 조건이 충족될 때 사용되지 않는 리소스를 무효화할 수도 있습니다.
대부분의 경우 패스는 단일 컴퓨팅 또는 픽셀 셰이더 연산을 래핑하는 데 그칩니다. 셰이더에는 여러 순열이 포함되어 있기 때문에 사용하지 않는 리소스를 블랙리스트에 추가하기 위해 패스 매개변수를 직접 조정해야 하는 유지 관리의 악몽이 발생할 수 있습니다. 대신 RDG는 이 작업을 대신 수행할 수 있는 유틸리티 함수를 제공합니다: ClearUnusedGraphResources입니다. 이 함수는 하나의 셰이더를 가져와서 셰이더가 사용하지 않는 모든 리소스를 무효화합니다. 이렇게 하면 패스에서 해당 리소스가 완전히 제거되어 그래프를 통해 리소스를 추적하는 데 드는 비용이 줄어듭니다.
이 유틸리티 함수는 모든 리소스를 무효화하기 전에 자동으로 ValidateShaderParameters를 호출합니다. 이렇게 하면 사용하지 않는 리소스를 무효화하기 전에 필요한 모든 리소스가 있는지 확인하여 혼동을 피할 수 있습니다.
컴퓨팅 셰이더와 같은 일반적인 경우 유틸리티 함수를 사용하면 많은 상용구를 제거할 수 있습니다. 상용구가 적다는 것은 복사-붙여넣기 버그가 줄어들고 유지 관리가 쉬워진다는 뜻입니다.
여담이지만, 셰이더를 디버깅할 때 텍스처에서 구식 printf 디버깅이 필요할 때가 있습니다. 이 기능은 C++에서 한 번 설정하고 셰이더 코드에서 조건부로 활성화할 수 있습니다!
기본 아이디어는 선택적 RDG 텍스처와 관련 UAV를 생성한 다음, 기능 개발 과정에서 디버깅하려는 각 패스에 바인딩하는 것입니다. 그런 다음 셰이더 코드에서 텍스처 리소스의 존재를 조건부로 선언하고 원하는 내용을 작성할 수 있습니다. 리소스가 활성화되어 있지 않으면 시스템이 자동으로 컬링합니다. 텍스처 시각화 툴을 사용하여 커스텀 텍스처의 내용을 확인할 수 있습니다!
또 다른 방법론의 예로, 단일 텍스처가 지원하는 최대 4개 이상의 텍스처 채널에서 작동하는 패스를 생각해 보겠습니다. 이 설정 중 일부를 추상화하여 구현에서 단일 컬렉션으로 취급하도록 하면 좋을 것입니다.
이를 위해 이러한 리소스를 셰이더 파라미터 구조로 그룹화하고 이를 초기화하는 메서드를 만들 수 있습니다. 이러한 함수를 구현하면 추가 텍스처 레퍼런스를 포함할지 여부에 대한 세부 사항을 추상화할 수 있습니다.
텍스처의 구조는 네이티브 RDG 리소스처럼 작동합니다. 설정 코드의 복잡성이 별도의 함수로 추상화되어 보다 모듈화된 코드가 생성됩니다.
셰이더 파라미터 구조체는 중첩될 수 있습니다. C++에서는 중첩을 구조체 구성이라고 합니다. 셰이더 코드에서는 구조체 멤버 앞에 밑줄로 구분된 구조체 이름을 붙여야 합니다. 이 예제에서는 모든 입력 텍스처를 수동으로 나열하는 대신 구조체를 참조하기만 하면 됩니다.
리소스 구조에는 특정한 것이 없습니다. 모든 셰이더 파라미터는 중첩될 수 있습니다. 권장되는 패턴은 여러 패스 간에 공유되는 공통 셰이더 파라미터를 식별하는 것입니다. 이러한 파라미터는 자체 구조체로 추출하여 한 번 채운 다음 복사할 수 있습니다. 공통 파라미터를 설정하는 단일 코드 경로를 사용하면 유지 관리 비용을 줄일 수 있습니다.
C++ 측에서 자식 구조를 중첩하되 모든 셰이더 이름을 부모 스코프에 흡수하는 것도 가능합니다. 위 예시에서는 단편적인 파라미터 구조체를 빌드한 다음 셰이더의 파라미터 구조체로 구성했습니다. 셰이더 측에서 불필요한 명명 접두사가 생성되는 것을 방지하기 위해 SHADER_PARAMETER_STRUCT_INCLUDE 변형이 사용됩니다. 이렇게 하면 셰이더 이름이 전역 범위로 평탄화되어 구조 접두사가 제거됩니다. 이는 C++에서 업데이트 빈도를 구성할 때 매우 유용합니다. 예를 들어 동일한 공통 셰이더 파라미터를 참조하는 일련의 패스가 있을 수 있습니다. 공통 파라미터를 포함하면 패스 설정의 시작 부분에서 빌드하고 간단히 복사할 수 있습니다.
중첩 시스템은 대부분의 요구 사항을 지원할 수 있을 만큼 유연합니다. 기능의 요구사항(예: 코드 중복, 장황함 등)에 따라 판단력을 발휘하여 파라미터를 구성하세요. 언리얼 엔진은 이를 달성하는 데 필요한 툴을 제공합니다.
앞서 리소스를 FDOFGatherInputTextures를 사용하여 구조체로 구성하는 것이 얼마나 편리한지 소개했습니다. 뎁스 오브 필드 셰이더 중 하나가 이 리소스 구조의 각 텍스처에 대해 밉 체인을 생성해야 한다는 것이 밝혀졌습니다. 이 시스템은 이 사용 사례를 지원하기 위해 C++ 측에서 셰이더 파라미터 구조의 배열을 지원합니다. C++에서는 직관적인 for 루프를 사용하여 레이어당 모든 UAV를 설정하는 것이 훨씬 더 편리해집니다. 셰이더 코드는 구조 중첩과 같은 이유로 그다지 훌륭하지는 않지만, 적어도 C++에서는 이러한 제약이 없습니다.
GPU 디버깅 툴에서 타이밍을 더 잘 구성하기 위해 코드에 이벤트 범위를 추가하여 그 안에서 생성된 모든 패스를 포함할 수 있습니다.
이벤트는 성능과 관련된 정보와 결합하면 훨씬 더 유용합니다. 예를 들어 뎁스 오브 필드는 알파 채널을 지원해야 할 수 있습니다. 이렇게 하면 버퍼 레이아웃이 변경되고 텍스처 가져오기가 증가합니다. 하지만 이 정보를 각 패스에 넣으면 노이즈가 많이 발생할 수 있습니다. 대신 전체 스코프에 저장할 수 있는 정보입니다.
UE4 는 그리기 이벤트 외에도 버킷에서 GPU 타이밍 통계를 추적합니다. 이는 `stat gpu` 콘솔 명령으로 표시됩니다.
GPU 통계 지원은 스코프 형태로만 RDG 에 통합되어 있습니다. 패스 GPU 타이밍은 패스 구성 시 가장 안쪽 스코프로 집계됩니다.
먼저 스크린 패스란 무엇인가요? 기본적으로 텍스처 입력을 읽고 텍스처 출력을 쓰는 패스라고 할 수 있습니다. 이 정의는 엔진의 대부분의 패스에 적용되며 문제 공간의 틀을 잡는 데 도움이 됩니다. 이 프레임워크는 주로 픽셀 셰이더 패스를 대상으로 하지만, 특정 컴포넌트는 계산 패스에도 적용할 수 있습니다. 네이밍에 대한 참고 사항: "포스트 프로세스" 대신 "스크린 패스" 규칙을 선택했습니다. 이것이 더 일반적이라고 생각했습니다. 예를 들어 서브서피스 스캐터링은 라이팅 컴포지션 단계에서 실행되므로 엄밀히 말해 포스트 프로세스가 아닙니다.
앞의 정의로 무장하고 스크린 패스의 기본 요구 사항을 고려해 보겠습니다. 먼저 셰이더에는 텍스처에 대한 정보(예: 픽셀 단위의 범위 또는 UV 좌표의 뷰포트 영역)가 필요할 수 있습니다. 다음으로, 대부분의 경우 스크린 패스는 단일 픽셀 또는 계산 셰이더만 필요합니다. 픽셀 셰이더에 대한 중요한 고려 사항은 VR용 HMD 숨김 영역 메시로 렌더링할지, 아니면 단순히 전체 화면 트라이앵글로 렌더링할지 여부입니다. 마지막으로, 나중에 자세히 살펴보겠지만 출력 텍스처의 뷰포트에 렌더링하거나 입력 텍스처의 뷰포트에서 샘플을 가져올 수 있는 유연성이 필요합니다. 이를 위한 사용 사례로는 분할 화면이나 VR, 동적 해상도 스케일링 등이 있습니다. 궁극적으로 스크린 패스 프레임워크는 이러한 문제를 해결하기 위해 존재합니다.
스크린 패스 프레임워크는 텍스처 뷰포트를 명시적으로 정의합니다. 이 데이터 구조는 픽셀 단위로 지정된 텍스처 범위 내에서 방향이 지정된 직사각형을 나타냅니다. 뷰포트를 UV 좌표로 표현하고 뷰포트 간에 매핑하기 위해서는 이 두 가지가 모두 필요합니다. 다음 슬라이드에서 살펴보겠지만 프레임워크에서 셰이더 파라미터를 도출하고, 스크린 패스를 위한 입력 및 출력 뷰포트를 정의하고, 뷰포트 간 UV 트랜스폼을 도출하는 데 사용됩니다.
텍스처 뷰포트의 필요성을 이해하려면 입력 및 출력 텍스처가 하나인 패스를 생각해 보세요. 여기서 중요한 점은 이 두 뷰포트가 서로 다를 수 있다는 것입니다. 분할 화면이 이에 대한 명백한 예입니다. 분할 화면 뷰포트에서 읽고, 패스의 중간 체인을 처리한 다음, 그 결과를 다시 분할 화면 뷰포트에 합성해야 할 수도 있습니다. 이러한 세부 사항은 셰이더 코드에서 최대한 추상화하는 것이 가장 이상적입니다.
고급 시나리오에서는 텍스처 뷰포트가 입력마다 다를 수도 있습니다. 예를 들어 모션 블러에서 뎁스와 속도는 하나의 뷰포트를 공유하지만, 씬 컬러는 TAA에서 업샘플링 되었으므로 두 번째 뷰포트가 있고, 마지막으로 속도 타일 분류 텍스처는 세 번째 뷰포트가 있습니다. 요컨대, 이러한 뷰포트 공간 간에 UV 좌표를 쉽게 매핑할 수 있는 방법이 필요합니다.
셰이더 파라미터 문제를 해결하기 위해 프레임워크는 텍스처 뷰포트에서 파생된 새로운 셰이더 파라미터 구조체를 정의합니다. 이 구조는 텍스처의 범위 및 역 범위, 픽셀 또는 UV 좌표의 뷰포트 사각형 등과 같은 정보를 제공합니다. 패스에 대한 여러 입력이 동일한 텍스처 뷰포트를 공유하는 경향이 있으므로 설계상 특정 텍스처 인스턴스와 결합되지 않습니다. 대신 패스에 필요한 만큼 정의할 수 있습니다.
텍스처 뷰포트 파라미터를 사용하려면 이전 슬라이드에서 살펴본 패스 파라미터 구조체의 멤버로 추가하기만 하면 됩니다. 텍스처 뷰포트 인스턴스에서 바로 파라미터를 인스턴스화할 수 있습니다.
셰이더 측면에서 프레임워크는 HLSL에서 텍스처 뷰포트 파라미터를 정의하는 매크로를 제공합니다. 따라서 개별 멤버를 일일이 추가할 필요가 없습니다. 이 매크로는 ScreenPass.ush에 정의되어 있습니다.
픽셀 셰이더 작업을 더 쉽게 하기 위해 프레임워크에는 픽셀 셰이더 패스를 렌더 그래프에 직접 추가할 수 있는 유틸리티 함수가 포함되어 있습니다. HMD 메시를 사용할지, 전체 화면 트라이앵글을 사용할지 등의 세부 사항을 추상화합니다. 입력 및 출력 텍스처 뷰포트를 제공하면 구현에서 RHI 뷰포트 설정과 UV 좌표 생성을 자동으로 처리합니다. 수동으로 패스를 설정하고 명령 목록에 제출해야 하는 경우와 같이 더 많은 제어가 필요한 경우 이 함수의 다른 저수준 변형이 있습니다.
마지막으로 이 프레임워크는 한 뷰포트 공간에서 다른 뷰포트 공간으로 UV 좌표를 매핑하는 간단한 트랜스폼 유형을 제공합니다. 인스턴스화하려면 소스 및 대상 뷰포트를 전달하기만 하면 됩니다. 셰이더 코드에서 스케일/편향 인자를 소스 UV 좌표에 적용합니다. 셰이더에서 SCREEN_PASS_TEXTURE_VIEWPORT_TRANSFORM 매크로를 사용하여 트랜스폼을 빠르게 정의할 수 있습니다.
'TECH.ART.FLOW.IO' 카테고리의 다른 글
[번역]FASTBuild로 UE4 및 UE5 컴파일하기 (0) | 2023.12.18 |
---|---|
바이트덴스 조석광년 SGR 테크아트팀 구조. (0) | 2023.12.14 |
[번역]Why Talking About Render Graphs (1) | 2023.12.12 |
[번역]Tone Mapping (0) | 2023.12.04 |
[번역/정리]만화 얼굴 그림자 매핑 생성 렌더링 원리 (1) | 2023.12.01 |