Skip to content

📊 언리얼 엔진 디퍼드 렌더링 파이프라인 구조도

mermaid
graph TD
    subgraph UE_Deferred_Pipeline ["언리얼 엔진 디퍼드 렌더링 파이프라인 (UE Deferred Pipeline)"]
        InitView["1. InitViews (가시성 판단 & 프리징 컬링)"]
        PrePass["2. PrePass / Early-Z (깊이 버퍼 사전 기록)"]
        BasePass["3. BasePass (GBuffer 렌더링: Normal, Roughness, BaseColor, Specular)"]
        Shadow["4. Shadow Depths & CSM (가상 섀도우 맵 / 캐스케이드 섀도우 맵)"]
        LightPass["5. Lights & Direct Lighting (타일/클러스터 기반 디퍼드 라이팅)"]
        GI["6. Global Illumination & Reflections (Lumen / Ray Tracing / SSR)"]
        SSS["7. Subsurface Scattering (화면 공간 분리형 SSS 가우시안 블러)"]
        Translucent["8. Translucent Rendering (전방향 투명도 렌더링)"]
        PostProcess["9. Post-Processing (톤 매핑, 템포럴 슈퍼 해상도 TSR, 블룸, DoF)"]
        
        InitView --> PrePass
        PrePass --> BasePass
        BasePass --> Shadow
        Shadow --> LightPass
        LightPass --> GI
        GI --> SSS
        SSS --> Translucent
        Translucent --> PostProcess
    end

언리얼 렌더링 시스템 분석(04) - 디퍼드 렌더링 파이프라인

🌐 원문 링크: 剖析虚幻渲染体系(04)- 延迟渲染管线 (cnblogs.com/timlly)
📅 원문 발행일: 2021-05-05 | ✍️ 저자: Timlly (00 / )

💡 시리즈 분류: Unreal Engine Rendering국내 업계 표준 용어 감수 및 수식/도해 복원 적용 완료


이 문서에서는 UE의 FDeferredShadingSceneRenderer의 구체적인 구현 프로세스와 관련 프로세스를 주로 설명합니다. 여기에는 주요 렌더링 기술인 Deferred Shading(Deferred Shading)이 포함됩니다. 포워드 렌더링과는 다른 단계와 디자인을 가진 렌더링 경로입니다.

FDeferredShadingSceneRenderer를 설명하기 전에, UE 구현을 더 잘 이해할 수 있도록 먼저 렌더링 기술, 포워드 렌더링 및 그 변형에 대해 설명하겠습니다. 보다 구체적으로 이 글은 주로 다음과 같은 내용을 담고 있습니다.

  • 렌더 경로

포워드 렌더링 및 그 변형

  • 디퍼드 렌더링 및 그 변형

  • FDeferredShadingSceneRenderer(디퍼드 렌더링 장면 렌더러)

InitView

-프리패스

-베이스패스

-라이팅패스

  • 반투명패스

  • 포스트 프로세스

  • 디퍼드 렌더링의 최적화와 미래

4.1.2 기본 개념

이 기사에 포함된 렌더링의 기본 개념과 분석 중 일부는 다음과 같습니다.

개념 약어 중국어 번역 분석하다

렌더 타겟 RT 렌더 타겟, 렌더 텍스처 색상 쓰기 및 혼합의 대상으로 사용할 수 있는 GPU 메모리에 있는 텍스처 버퍼입니다.

기하학적 버퍼 GBuffer 기하학 버퍼 디퍼드 셰이딩 기술을 위한 기하학적 데이터 저장 컨테이너인 특수 렌더 타겟입니다.

렌더링 경로

  • 렌더링 경로 텍스처를 렌더링하기 위해 장면 객체를 렌더링하는 데 사용되는 단계 및 기술입니다.

앞으로 음영처리

  • 포워드 렌더링 디퍼드 셰이딩 경로보다 더 간단하고 편리한 일반적이고 자연스럽게 지원되는 렌더링 경로입니다.

4.2 디퍼드 렌더링 기술

4.2.1 디퍼드 렌더링 소개

디퍼드 렌더링, 디퍼드 렌더링은 Michael Deering이 1988년에 쓴 논문 The Triangle Processor and Normal Vector Shader: a VLSI system for highperformancegraphics에서 처음 제안되었습니다. 스크린 스페이스 쉐이딩(Screen Space Shading)을 기반으로 한 기술이며, 가장 널리 사용되는 렌더링 패스(Rendering Path) 기술이기도 합니다. 이와 유사한 것이 Forward Shading입니다.

핵심 아이디어는 장면의 객체 드로잉을 지오메트리 패스와 조명 패스라는 두 가지 패스로 분리하는 것입니다. 그 목적은 계산 집약적인 조명 패스를 지연시키고 이를 객체 수 및 조명 수에서 분리하여 셰이딩 효율성을 향상시키는 것입니다. 현재 주류 렌더러 및 상용 엔진에서 광범위하고 충분한 지원을 제공합니다.

4.2.2 디퍼드 렌더링 기술

디퍼드 렌더링은 지오메트리 패스(Geometry Pass, UE에서는 이를 베이스 패스라고 함)와 조명 패스(Lighting Pass)라는 두 가지 주요 패스가 있다는 점에서 포워드 렌더링과 다릅니다. 렌더링 프로세스는 아래 그림에 나와 있습니다.

디퍼드 렌더링에는 두 개의 렌더링 패스가 있습니다. 첫 번째는 장면의 객체 정보를 여러 GBuffer로 래스터라이제이션하는 지오메트리 채널입니다. 두 번째는 GBuffer의 기하학적 정보를 사용하여 각 픽셀의 조명 결과를 계산하는 조명 단계입니다.

두 채널의 각각의 프로세스와 기능은 다음과 같습니다.

  • 기하학 채널

이 단계에서는 장면의 모든 불투명 객체(Opaque)와 Masked 객체가 조명이 없는 재질로 렌더링된 다음 객체의 기하학적 정보가 해당 렌더링 텍스처(렌더 대상)에 기록됩니다.

물체의 기하학적 정보에는 다음이 포함됩니다.

  1. 위치(위치, 깊이일 수도 있습니다. 깊이와 화면 공간 uv가 있는 한 위치를 재구성할 수 있기 때문입니다.)

  2. 정상.

  3. 재료 매개변수(확산 색상, 방출 색상, 반사 스케일, 반사 강도, AO, 러프니스, 음영 모델 등).

기하학 채널의 구현 의사 코드는 다음과 같습니다:

cpp
void RenderBasePass()
{
    SetupGBuffer(); // 설정(Set)데이터 버퍼 。
    
    // 순회(Traverse/Iterate)의 반투명(Translucent)
    for each(Object in OpaqueAndMaskedObjectsInScene)
    {
        SetUnlitMaterial(Object);    // 설정(Set)라이팅 의 머티리얼(Material)
        DrawObjectToGBuffer(Object); // 렌더링(Render)Object의 정보 까지 GBuffer,GPU래스터라이제이션 내에서 。
    }
}

위의 렌더링 후에는 이러한 불투명 객체의 색상, 노멀, 깊이, AO 등과 같은 GBuffer 정보를 얻을 수 있습니다(아래 그림).

디퍼드 렌더링 기술은 지오메트리 채널 이후 객체의 GBuffer 정보를 획득하며, 왼쪽 상단이 위치, 오른쪽 상단이 노멀, 왼쪽 하단이 색상(기본 색상), 오른쪽 하단이 하이라이트입니다.

조명 계산은 지오메트리 채널 단계에서 수행되지 않으므로 많은 중복 조명 계산 논리가 제거된다는 사실에 특별한 주의를 기울여야 합니다. 즉, 가려진 객체에 대한 많은 조명 계산을 피하고 오버드로를 피하는 것을 의미합니다.

  • 조명 채널

조명 단계에서는 지오메트리 채널 단계에서 생성된 GBuffer 데이터를 사용하여 조명 계산을 수행합니다. 핵심 로직은 동일한 수의 렌더링 해상도로 픽셀을 순회하고 각 픽셀의 UV 좌표에 따라 GBuffer에서 픽셀의 기하학적 정보를 얻어 조명 계산을 수행하는 것입니다. 의사코드는 다음과 같습니다.

cpp
void RenderLightingPass()
{
    BindGBuffer(); // 바인딩(Bind)데이터 버퍼 。
    SetupRenderTarget(); // 설정(Set)렌더링(Render)텍스처(Texture)。
    
    // 순회(Traverse/Iterate)RT의 픽셀(Pixel)
    for each(pixel in RenderTargetPixels)
    {
        // 페치/가져오기(Fetch)GBuffer데이터 。
        pixelData = GetPixelDataFromGBuffer(pixel.uv);
        // 누적 합산(Accumulate)컬러
        color = 0;    
        // 순회(Traverse/Iterate),개 의 라이팅 계산/산출(Calculate)결과 누적 합산(Accumulate)까지 컬러 내에서 。
        for each(light in Lights)
        {
            color += CalculateLightColor(light, pixelData);
        }
        // 기록/쓰기(Write)컬러 까지 RT。
        WriteColorToRenderTarget(color);
    }
}

그중 CalculateLightColor는 Gouraud, Lambert, Phong, Blinn-Phong, BRDF, BTDF, BSSRDF 및 기타 조명 모델을 사용할 수 있습니다.

  • 디퍼드 렌더링의 장점과 단점

가장 시간이 많이 걸리는 조명 계산은 포스트 프로세스 단계로 지연되므로 장면의 객체 수와 분리되고 렌더 대상 크기에만 관련됩니다. 복잡도는 O(Nlight×WRT×HRT)입니다. 따라서 디퍼드 렌더링은 복잡한 장면과 광원이 많은 장면을 처리하는 데 더 적합하며 종종 매우 좋은 성능 향상을 달성할 수 있습니다.

그러나 몇 가지 단점도 있습니다. 기하학적 정보를 그리기 위해 채널이 하나 더 필요한 경우 멀티 렌더링 텍스처(MRT) 지원, 더 많은 CPU 및 GPU 메모리 사용량, 더 높은 대역폭 요구 사항, 제한된 머티리얼 렌더링 유형, MSAA와 같은 하드웨어 앤티앨리어싱 사용의 어려움, 흐릿한 그림 등이 필요합니다. 또한 간단한 장면을 처리할 때는 렌더링 성능이 향상되지 않을 수 있습니다.

4.2.3 디퍼드 렌더링의 변형

디퍼드 렌더링은 다양한 플랫폼과 API에 대해 다양한 최적화 및 개선 기술을 사용할 수 있으므로 결과적으로 다양한 변형이 발생할 수 있습니다. 다음은 몇 가지 변형입니다.

4.2.3.1 지연 조명(Light Pre-Pass)

**디퍼드 라이팅(Deferred Lighting)**은 Light Pre-Pass라고도 합니다. Deferred Shading과의 차이점은 세 가지 패스가 필요하다는 것입니다.

  1. 첫 번째 패스는 Geometry Pass라고 합니다. 각 픽셀 조명 계산에 필요한 기하학적 속성(노멀, 깊이)만 GBuffer로 출력됩니다.

  2. 두 번째 패스는 Lighting Pass라고 합니다. 광원 속성(예: Normal*LightDir, LightColor, Specular)을 LBuffer(Light Buffer, 광원 버퍼)에 저장합니다.

광원 버퍼는 광원 데이터와 레이아웃을 저장합니다.

  1. 세 번째 패스는 Secondary Geometry Pass라고 합니다. GBuffer 및 LBuffer의 데이터를 얻고 각 픽셀에 필요한 데이터를 재구성하여 조명을 계산하고 조명 계산을 수행합니다.

Deferred Shading에 비해 Deferred 조명의 장점은 필요한 G-Buffer 크기가 대폭 줄어들어 더 많은 재질 유형을 표현할 수 있고 MSAA 등을 더 잘 지원할 수 있다는 것입니다. 단점은 장면을 두 번 그려야 하며 Draw Call이 추가된다는 것입니다.

추가적으로 Deferred Lighting의 최적화된 버전이 있는데, 이는 위와 약간 다릅니다. 자세한 내용은 라이트 프리패스 문서를 참고하세요.

4.2.3.2 타일 기반 디퍼드 렌더링(TBDR)

타일 기반 디퍼드 렌더링은 타일 기반 렌더링, 즉 줄여서 TBDR으로 번역됩니다. 핵심 아이디어는 렌더링 텍스처를 일반 사각형(타일이라고 함)으로 나눈 다음 사각형 경계 상자를 사용하여 타일에서 쓸모 없는 광원을 제거하고 유효한 광원 목록만 남겨 실제 조명 계산에서 유효하지 않은 광원 계산 양을 줄이는 것입니다.

구현 다이어그램과 구체적인 단계는 다음과 같습니다.

  1. 렌더링 텍스처를 동일한 면적의 작은 블록(타일)으로 나눕니다. 위의 그림 (b)를 참조하세요.

타일은 균일하게 고정된 크기를 갖지 않으며 다양한 플랫폼 아키텍처 및 렌더러에 따라 다릅니다. 그러나 일반적으로 2의 N승이며, 길이와 너비가 반드시 같지는 않습니다. 16x16, 32x32, 64x64 등이 될 수 있습니다. 너무 작거나 너무 커서는 안 됩니다. 그렇지 않으면 최적화 효과가 분명하지 않습니다. PowerVR GPU는 일반적으로 32x32를 사용하는 반면 ARM Mali GPU는 16x16을 사용합니다.

  1. 타일 내의 깊이 범위를 기반으로 경계 상자를 계산합니다.

TBDR의 각 타일 내 깊이 범위는 다를 수 있으므로 다양한 크기의 경계 상자를 얻을 수 있습니다.

  1. Tile의 Bounding Box와 Light의 Bounding Box를 기준으로 교차를 수행합니다.

감쇠가 없는 방향성 조명 외에도 다른 유형의 광원도 위치 및 감쇠를 기반으로 경계 상자를 계산할 수 있습니다.

  1. 분리된 조명을 버리고 타일에 효과적인 조명 목록을 얻습니다. 위의 이미지 (c)를 참조하세요.

  2. 모든 타일을 탐색하고 각 타일의 작업 광원 인덱스 목록을 얻은 다음 타일에 있는 모든 픽셀의 조명 결과를 계산합니다.

TBDR은 많은 쓸모 없는 광원을 폐기하고 많은 비효율적인 조명 계산을 피할 수 있기 때문에 모바일 GPU 아키텍처에 널리 채택되어 하드웨어 가속을 기반으로 TBDR을 형성합니다.

PowerVR의 TBDR 아키텍처는 즉시 모드 아키텍처와 비교하여 클리핑 후 래스터라이제이션 전 타일링 단계를 추가하고 온칩 깊이 버퍼 및 컬러 버퍼를 추가하여 깊이와 색상에 더 빠르게 액세스합니다.

다음 그림은 PowerVR Rogue 제품군의 Series7XT 시리즈 GPU 및 하드웨어 아키텍처의 개략도입니다.

4.2.3.3 클러스터링된 디퍼드 렌더링

클러스터형 디퍼드 렌더링은 클러스터형 디퍼드 렌더링입니다. TBDR과의 차이점은 깊이를 더 세밀하게 분할함으로써 깊이 범위가 크게 점프할 때(중간에 유효 픽셀이 없는 경우) TBDR로 인해 발생하는 광원 클리핑 효율이 감소하는 문제를 피할 수 있다는 점입니다.

Clustered Deferred Rendering의 핵심 아이디어는 깊이를 특정 방식으로 여러 부분으로 세분화하여 각 클러스터의 경계 상자를 보다 정확하게 계산한 다음 깊이가 불연속적일 때 광원 클리핑 효율성이 저하되는 것을 방지하기 위해 광원을 보다 정확하게 클리핑하는 것입니다.

위 그림의 클러스터링 방식을 암시적 클러스터링 방식이라고 합니다. 실제로 깊이를 더욱 정확하게 세분화하고 실제 깊이 범위로 각 계열의 경계 상자를 계산할 수 있는 명시적 클러스터링 방법이 있습니다.

명시적 깊이 클러스터링은 각 클러스터의 경계 상자를 더 정확하게 찾습니다.

다음 그림은 Tiled, Implicit 및 Explicit 깊이 분할 방법의 비교 차트입니다.

빨간색은 타일형 차단 방법, 녹색은 암시적 클러스터링 방법, 파란색은 명시적 클러스터링 방법입니다. 명시적 클러스터링을 통해 얻은 경계 상자가 더 정확하고 경계 상자가 더 작아서 광원 클리핑 효율성이 향상되는 것을 볼 수 있습니다.

Clustered Deferred 방법을 사용하면 엄마는 다음 화면 O_O를 렌더링할 때 컴퓨터가 멈추고 프레임이 떨어지는 것에 대해 더 이상 걱정할 필요가 없습니다.

4.2.3.4 분리된 디퍼드 렌더링

분리된 디퍼드 렌더링(분리된 디퍼드 렌더링)은 최적화된 디퍼드 렌더링 기술로 Gabor Liktor와 다른 사람들이 하드웨어 래스터라이제이션를 위한 분리된 지연 음영 논문에서 처음 제안했습니다. 핵심 아이디어는 셰이딩 샘플링 데이터(가시성과 무관)를 저장하기 위한 컴팩트한 지오메트리 버퍼를 추가하고, 메모이제이션 캐시(아래 그림)를 사용하여 계산량을 압축하고, 확률론적 래스터라이제이션의 셰이딩 재사용률을 향상시키며, AA 소비를 줄여 셰이딩 효율성을 향상시키고 디퍼드 렌더링에서 MSAA를 사용하기 어려운 문제를 해결하는 것입니다.

위 그림 왼쪽: 표면의 한 지점에서 시간이 지남에 따라 화면에 투사된 지점의 변화를 나타내는 개략도입니다. t2-t0이 충분히 작다고 가정하면 화면에 투영된 지점은 동일한 지점에 있으므로 이전 셰이딩 결과를 재사용할 수 있습니다. 위 오른쪽: 분리된 디퍼드 렌더링은 메모 캐시 버퍼를 사용하여 이전 셰이딩 결과를 재사용하여 객체의 좌표를 화면 공간에 매핑합니다. 동일한 UV라면 직접 조명이 아닌 직접 메모이제이션 캐시 버퍼의 결과를 가져옵니다.

분리된 디퍼드 렌더링 사용 효과에 대한 개요입니다. 위: 4배 슈퍼샘플링으로 렌더링된 뎁스 오브 필드(DoF). 중간: SSPP(픽셀당 음영 처리); 하단: 카메라에서 캡처한 장면.

4.2.3.5 가시성 버퍼

가시성 버퍼지연 텍스처링과 매우 유사합니다. Deferred Lighting의 더욱 과감한 개선안입니다. 핵심 아이디어는 GBuffer 사용량을 줄이기 위해(큰 GBuffer 사용량은 큰 대역폭과 높은 에너지 소비를 의미함) GBuffer를 렌더링하는 대신 Visibility Buffer를 렌더링하는 것입니다. 가시성 버퍼에는 삼각형과 인스턴스 ID만 저장됩니다. 이러한 속성을 사용하면 조명 계산 단계(셰이딩) 중에 실제로 필요한 버텍스 어트리뷰트과 텍스처 속성이 UAV와 바인드리스 텍스처에서 각각 읽혀지고, 밉맵은 UV 차이를 기반으로 자체적으로 계산됩니다(아래 그림).

GBuffer 및 Visibility Buffer 렌더링 파이프라인의 비교 다이어그램. 후자는 가시성 단계에서 전자의 Gemotry Pass를 대체합니다. 이 단계에서는 4바이트 버퍼로 압축할 수 있는 삼각형과 인스턴스 ID만 기록하므로 메모리 사용량이 크게 줄어듭니다.

이 방법은 버퍼 점유를 줄일 수 있지만 바인드 없는 텍스처 지원이 필요하며 GPU 캐시에 적합하지 않습니다(인접 픽셀의 삼각형 및 인스턴스 ID가 크게 점프하여 캐시의 공간적 지역성이 감소함).

4.2.3.6 잘 정리된 타일 조명 목록

Fine Pruned Tiled Light Lists는 최적화된 타일 음영 처리입니다. 객체와 광원의 교차점을 계산하기 위한 두 가지 패스가 있다는 점에서 기존 타일 렌더링과 다릅니다.

첫 번째 패스는 화면 공간에서 광원의 AABB를 계산한 다음 객체의 AABB로 대략적인 판단을 내립니다.

두 번째 패스는 타일별로 광원(AABB 아님)의 실제 모양과 정확하게 교차합니다.

잘게 잘라낸 타일 조명 절단 다이어그램을 나열합니다. 위 사진은 대충 크롭한 결과이고, 아래 사진은 정밀 크롭한 결과입니다. 일반적으로 거친 작물의 수가 정밀한 작물의 수보다 많지만 속도가 더 빠르다는 것을 알 수 있습니다. 정밀한 자르기는 이를 기반으로 보다 정확한 교차를 수행하므로 분리된 광원을 추가로 폐기하고 많은 광원 수를 줄이며 음영 계산 입력을 방지합니다.

이러한 Tiled Shading은 불규칙한 광원을 구현할 수 있으며, Compute Shader를 사용하여 구현할 수 있습니다. 콘솔 게임 "Rise of the Tomb Raider"에도 사용되었습니다.

4.2.3.7 지연된 속성 보간 음영

Visibility Buffer와 유사하게 Deferred Attribute Interpolation Shading은 기존 GBuffer의 메모리가 너무 높은 문제도 해결합니다. Deferred의 메모리 소모를 줄이기 위해 GBuffer 정보를 저장하지 않고 삼각형 정보를 저장한 후 보간법을 통해 계산하는 방식을 제공합니다. 이 방법은 Visibility Buffer를 사용하여 중복된 삼각형 정보를 제거하여 비디오 메모리 사용량을 더욱 줄일 수도 있습니다.

지연 속성 보간 셰이딩 알고리즘 다이어그램. 첫 번째 패스는 가시성 버퍼를 생성합니다. 두 번째 패스에서는 가시성을 사용하여 모든 픽셀이 메모 캐시에 단 하나의 삼각형 정보만 기록되도록 합니다. 여기서 메모 캐시는 삼각형 ID와 실제 주소를 기록하는 매핑 버퍼입니다. 최종 패스는 속성 보간을 위해 각 삼각형의 화면 공간의 편도함수를 계산합니다.

이 방법은 GI와 같은 저주파 조명을 별도로 계산하여 셰이딩 소비를 줄이고 MSAA를 활성화할 수도 있습니다.

4.2.3.8 지연된 거친 픽셀 음영

Deferred Coarse Pixel Shading은 화면 공간의 조명 계산 단계에서 기존 디퍼드 렌더링의 높은 소비 문제를 해결하기 위해 제안되었습니다. GBuffer 생성 시 추가로 ddx, ddy를 생성한 후 Compute Shader를 사용하여 특정 크기의 블록에 대해 변화가 미미한 영역을 찾아내고, 저주파 셰이딩(가변 비율 셰이딩과 유사하게 여러 픽셀이 하나의 계산을 공유함)을 사용하여 셰이딩 효율성을 높이고 소비를 줄입니다.

컴퓨트 셰이더의 지연된 거친 픽셀 셰이딩 렌더링 메커니즘 다이어그램. 핵심 단계는 ddx와 ddy를 계산하여 변화가 적은 픽셀을 찾고, 착색 빈도를 줄이고, 재사용률을 높이는 것입니다.

이 방법을 사용하여 렌더링한 후 다음 두 장면을 렌더링할 때 성능이 크게 향상되었습니다.

Deferred Coarse Pixel Shading을 사용하여 렌더링된 두 장면. 왼쪽이 발전소, 오른쪽이 스폰자입니다.

구체적인 데이터는 다음과 같습니다.

발전소 시나리오는 50%-60%, Sponza 시나리오는 25%-37%를 절약할 수 있습니다.

이 접근 방식은 화면 공간 후처리에도 작동합니다.

4.2.3.9 지연된 적응형 컴퓨팅 셰이딩

Deferred Adaptive Compute Shading의 핵심 아이디어는 화면 픽셀을 특정 방식으로 서로 다른 세분성의 5개 레벨 픽셀 블록으로 나누어 인접한 레벨에서 직접 보간할지 또는 모양을 변경할지 결정하는 것입니다.

Deferred Adaptive Compute Shading의 5가지 수준과 해당 보간 다이어그램. 각 레벨에 새로 추가된 픽셀(노란색으로 표시)에 대해 이전 레벨의 주변 픽셀을 고려하여 프레임 버퍼와 GBuffer의 로컬 분산을 추정하여 인접한 픽셀에서 보간할지 아니면 직접 색상을 지정할지 결정합니다.

알고리즘 프로세스는 다음과 같습니다.

  • 첫 번째 레벨은 1/16(4x4 블록이 사용되므로) 픽셀을 렌더링합니다.

  • 2층부터 5층까지 이동하며 다음 단계에 따라 레벨별로 색상을 지정합니다.

이전 레벨의 인접한 4개 픽셀의 유사성(유사성 기준)을 계산합니다.

  • 유사도 기준이 특정 임계값보다 작으면 해당 픽셀이 주변 픽셀과 충분히 유사한 것으로 간주하고 직접 보간을 통해 현재 픽셀의 결과를 얻습니다.

  • 그렇지 않은 경우 해당 픽셀과 주변 픽셀의 차이가 너무 크다는 의미이므로 셰이딩 계산을 활성화합니다.

이 방법은 UE4의 다양한 장면을 렌더링할 때 평균 제곱 오차(RMSE), 최대 신호 대 잡음비(PSNR), 평균 구조적 유사성(MSSIM)에서 좋은 지표를 얻을 수 있습니다. (아래 사진)

UE4의 일부 장면을 렌더링할 때 DACS 방법은 다양한 셰이딩 속도에서 좋은 이미지 렌더링 표시기를 얻을 수 있습니다.

동일한 장면과 그림을 렌더링할 때 체커보드 쉐이딩 방식과 비교했을 때 DACS의 평균 제곱 오차(RMSE)는 동일 시간에 전자의 21.5%에 불과합니다. 동일한 화질(MSSIM)에서 DACS의 시간은 4.22배 빠릅니다.

4.2.4 포워드 렌더링 및 그 변형

4.2.4.1 포워드 렌더링

포워드 셰이딩은 가장 간단하고 가장 널리 지원되는 렌더링 기술입니다. 구현 아이디어는 장면의 모든 개체를 탐색하고, 각 개체에 대해 그리기 품질을 한 번 호출하고, 렌더링 파이프라인을 통해 이를 래스터라이제이션하고, 렌더링 텍스처에 쓰는 것입니다. 구현 의사 코드는 다음과 같습니다.

cpp
// 순회(Traverse/Iterate)RT의 픽셀(Pixel)
for each(object in ObjectsInScene)
{
    color = 0;
    // 순회(Traverse/Iterate),개 의 라이팅 계산/산출(Calculate)결과 누적 합산(Accumulate)까지 컬러 내에서 。
    for each(light in Lights)
    {
        color += CalculateLightColor(light, object);
    }
    // 기록/쓰기(Write)컬러 까지 RT。
    WriteColorToRenderTarget(color);
}

렌더링 프로세스의 개략도는 다음과 같습니다.

시간 복잡도는 O(gfl)입니다. 여기서:

  • g는 장면의 객체 수를 나타냅니다.

  • f는 색칠된 조각의 수를 나타냅니다.

  • l은 장면의 광원 수를 나타냅니다.

이 간단한 기술은 GPU 등장 이후 하드웨어에서 자연스럽게 지원되어 왔으며 매우 다양한 응용 분야를 가지고 있습니다. 장점은 구현이 간단하고 다중 패스 렌더링이 필요하지 않으며 MRT 지원이 필요하지 않으며 MSAA 앤티앨리어싱을 완벽하게 켤 수 있다는 것입니다.

물론 단점도 있습니다. 광원 수가 많은 크고 복잡한 장면의 렌더링을 지원할 수 없습니다. 중복된 계산이 많고 과도한 계산이 필요합니다.

4.2.4.2 포워드 렌더링의 변형

  • 포워드+ 렌더링

**포워드+**는 타일드 포워드 렌더링이라고도 합니다. 포워드 렌더링 광원의 수를 늘리기 위해 광원 컬링 단계를 추가합니다. 패스에는 깊이 프리패스, 라이트 컬링 패스, 셰이딩 패스의 세 가지 패스가 있습니다.

라이트 컬링 패스는 타일의 디퍼드 렌더링과 유사합니다. 화면을 여러 개의 Tile로 나누고, 각 Tile과 광원을 교차시키며, Tile의 광원 목록에 유효한 광원을 적어서 Shading 단계에서 광원의 수를 줄입니다.

Forward+는 거리 원뿔의 연장으로 인해 기하학적 경계에서 위양성(SAT(분리 축 정리)을 통해 개선할 수 있는 위양성)이 발생하는 문제가 있습니다.

  • 클러스터 포워드 렌더링

클러스터 포워드 렌더링은 클러스터 디퍼드 렌더링과 유사합니다. 화면 공간을 동일한 타일로 나누고 깊이를 클러스터로 세분화한 다음 광원을 보다 세밀하게 자릅니다. 알고리즘은 유사하므로 여기서는 설명하지 않습니다.

  • 볼륨 타일링 포워드 렌더링

볼륨 타일드 포워드 렌더링은 타일드 및 클러스터러의 포워드 렌더링을 확장한 기술로 장면에서 지원하는 광원 수를 늘리는 것을 목표로 합니다. 논문의 저자는 400만 개의 광원이 있는 장면을 30FPS에서 실시간으로 렌더링할 수 있다고 믿습니다.

이는 다음 단계로 구성됩니다.

  1. 초기화 단계:

1.1 그리드(볼륨 타일)의 크기를 계산합니다. 타일 크기 (tx,ty)와 화면 해상도 (w,h)를 고려하면 화면 분할 수 (Sx,Sy)를 계산할 수 있습니다.

(Sx,Sy)=(|wtx|, |hty|)

깊이 방향의 분할 수는 다음과 같습니다.

S_Z = \Bigg|\frac{\log(Z_{far}/Z_{near})}{\log \big(1+\frac{2\tan{\theta}{S_y}\big)}\Bigg|

1.2 각 볼륨 타일의 AABB를 계산합니다. 아래 그림과 결합하면 각 타일의 AABB 경계는 다음과 같이 계산됩니다.

knear=Znear(1+2tan(θ)Sy)kkfar=Znear(1+2tan(θ)Sy)k+1pmin=(Sxi, Syj)pmax=(Sx(i+1), Sy(j+1))
  1. 업데이트 단계:

2.1 깊이 프리패스. 반투명하지 않은 물체의 깊이만 기록됩니다.

2.2 활성화된 타일을 표시합니다.

2.3 타일 목록을 생성하고 압축합니다.

2.4 타일에 광원을 할당합니다. 각 스레드 그룹은 활성화된 볼륨 타일을 실행하고 타일의 AABB를 사용하여 장면의 모든 광원과 교차하고(BVH를 사용하여 교차 수를 줄일 수 있음) 교차하는 광원 인덱스를 해당 타일의 광원 목록에 기록합니다(각 타일의 광원 데이터는 광원 목록의 시작 위치와 광원 수입니다).

2.5 색칠. 이 단계는 이전 방법과 특별히 다르지 않습니다.

복셀 타일을 기반으로 한 렌더링은 대규모 광원의 렌더링을 만족시킬 수 있지만 Draw Call 및 자기 유사 복셀 타일(Self-Similar Volume Tiles, 카메라에 가까운 복셀은 매우 작고 먼 복셀은 상당히 큽니다)이 증가하는 문제도 있습니다.

4.2.5 렌더링 경로 요약

4.2.5.1 디퍼드 렌더링과 순방향 렌더링

디퍼드 렌더링과 정방향 렌더링의 비교는 아래 표에 나와 있습니다.

포워드 렌더링 디퍼드 렌더링

시나리오 복잡성 단순한 복잡한

지원되는 광원 수 덜 많이

시간 복잡성O(gfl)O(fl)

앤티앨리어싱 MSAA, FXAA, SMAA TAA, SMAA

비디오 메모리 및 대역폭 더 낮은 더 높은

패스 수 덜 많이

지하철도 요구 사항 없음 요청

오버드로 심각한 피하는 것이 좋다

재료 유형 많이 덜

이미지 품질 명확하고 우수한 안티앨리어싱 효과 흐릿하고 앤티앨리어싱 효과가 손상됨

투명 개체 불투명한 물체, 마스크된 물체, 반투명한 물체 불투명 개체, 마스크된 개체, 반투명 개체를 지원하지 않음

화면 해상도 낮음 높은

하드웨어 요구 사항 낮음, 하드웨어 장치의 거의 100%를 포괄함 더 높음, MRT 지원 필요, Shader Model 3.0+ 필요

구현 복잡성 낮음 높은

포스트 프로세스 노멀, 깊이 등의 정보가 필요한 포스트 프로세스 지원 불가 SSAO, SSR, SSGI 등 노멀, 깊이 등의 정보가 필요한 후처리를 지원합니다.

다음은 몇 가지 추가 설명입니다.

  • 디퍼드 렌더링이 MSAA를 지원하지 않는 이유는 조명을 수행하기 전에 MSAA가 포함된 RT를 해결해야 하기 때문입니다(MSAA의 RT는 가중치에 따라 계산되어 최종 색상을 계산함). 물론 Resolve 없이 직접 셰이딩을 입력할 수도 있지만 이로 인해 비디오 메모리와 대역폭이 갑자기 증가하게 됩니다.

  • 디퍼드 렌더링 사진이 포워드 렌더링보다 흐릿한 이유는 지오메트리 채널과 조명 채널이라는 두 가지 래스터라이제이션를 거쳤기 때문입니다. 이는 두 번의 이산화를 수행하는 것과 동일하며 두 가지 신호 손실이 발생하고 이는 후속 재구성에 영향을 미쳐 최종 사진의 분산이 확장됩니다. 또한 디퍼드 렌더링에서는 TAA를 앤티앨리어싱 기술로 채택합니다. TAA의 핵심 아이디어는 공간 샘플링을 시간 샘플링으로 변환하는 것입니다. 그러나 타임 샘플링은 부정확한 경우가 많고 여러 가지 사소한 문제가 있어 사진의 분산이 다시 확대되어 고스팅, 모아레, 깜박임, 회색 사진이 생성될 수 있습니다. UE를 이용하여 디퍼드 렌더링을 기본으로 켜서 만든 게임 화면이 다소 불투명하게 느껴지는 이유이기도 합니다.

일부 포스트 프로세스(고대비 보존, 톤매핑) 및 타겟 AA 기술(SMAA, SSAA, DLSS)을 통해 이미지 품질 흐림 문제를 완화할 수 있지만, 어떤 경우에도 동일한 소비로 포워드 렌더링의 이미지 품질 효과를 달성하기는 어렵습니다.

  • 디퍼드 렌더링은 반투명 객체를 지원하지 않으므로 반투명 객체를 구체적으로 렌더링하려면 최종 단계에서 포워드 렌더링 단계를 추가해야 하는 경우가 많습니다.

  • 현 단계(2021년 상반기)에서는 PC 컴퓨터와 호스트 기기에서는 디퍼드 렌더링이 첫 번째 렌더링 기술인 반면, 모바일 기기에서는 포워드 렌더링이 선호되는 렌더링 기술이다. 시간이 흐르고 하드웨어가 발전함에 따라 디퍼드 렌더링이 곧 모바일 플랫폼으로 승격될 것이라고 믿습니다.

  • 머티리얼 렌더링 유형의 경우 디퍼드 렌더링은 다양한 셰이딩 모델을 기하학적 데이터로 GBuffer에 작성하여 머티리얼 및 조명 셰이딩 유형을 확장할 수 있지만 GBuffer의 대역폭에 의해서도 제한됩니다. 1바이트라면 256개의 머티리얼 셰이딩 유형을 지원할 수 있습니다.

UE는 셰이딩 모델 ID를 저장하기 위해 4비트를 사용하며 최대 16개의 셰이딩 모델만 지원할 수 있습니다. 저자는 UE의 shading 유형을 확장하려고 시도한 적이 있으며 저장을 위해 추가 GBuffer를 열지 않는 한 나머지 4비트가 차지하는 것을 발견했습니다. 그러나 더 많은 머티리얼 유형을 위해 추가 GBuffer를 갖는 것은 비용 효율적이지 않습니다.

  • 디퍼드 렌더링을 위한 MSAA와 관련하여 일부 문서(연기 렌더링의 다중 샘플 앤티앨리어싱, 지연 렌더러에 대한 다중 샘플 앤티앨리어싱 최적화)에서는 디퍼드 렌더링 파이프라인에서 MSAA, 샘플링 전략, 데이터 구조 및 대역폭 최적화를 지원하는 방법을 설명합니다.

연기 렌더링의 다중 샘플 앤티앨리어싱은 디퍼드 렌더링에서 MSAA의 알고리즘 다이어그램을 제안했습니다. 핵심 아이디어는 표준 GBuffer를 저장하는 것 외에도 Geometry Pass가 GBuffer에 해당하는 확장 데이터도 추가한다는 것입니다. 이러한 확장 데이터에는 Lighting Pass 셰이딩에 사용하기 위해 다중 샘플링 픽셀이 필요한 MSAA 데이터만 포함되어 있습니다. 이는 MSAA의 요구 사항을 충족할 뿐만 아니라 GBuffer 사용량도 줄일 수 있습니다.

4.2.5.2 디퍼드 렌더링 변형 비교

이 섹션에서는 주로 디퍼드 렌더링과 일반적인 변형을 비교합니다. 아래 표를 참조하세요.

연기됨 타일링된 지연 클러스터링된 지연

광원의 수 약간 더 많이 많이

대역폭 소비 매우 높다 높은 약간 더 높음

차단 방법 없음 화면 공간 화면 공간 + 깊이

구현 복잡성 약간 더 높음 높은 매우 높다

적용 가능한 시나리오 많은 물체와 광원이 있는 장면 서로 겹치지 않는 여러 객체와 여러 로컬 광원이 있는 장면 객체가 많은 장면, 서로 겹치지 않는 여러 로컬 광원, 시야 공간의 거리가 먼 장면

4.3 지연된 셰이딩 장면 렌더러

4.3.1 FSceneRenderer

cpp
FSceneRenderer는 UE 씬 렌더러의 상위 클래스입니다. 이는 UE 렌더링 시스템의 두뇌이자 엔진입니다. 이는 전체 렌더링 시스템에서 중요한 역할을 합니다. 주로 장면을 처리 및 렌더링하고 RHI 레이어에 대한 렌더링 지침을 생성하는 데 사용됩니다. 부분적인 정의와 분석은 다음과 같습니다.
cpp
// Engine\Source\Runtime\Renderer\Private\SceneRendering.h

// 렌더링(Render)
class FSceneRenderer
{
public:
    FScene* Scene; // 렌더링(Render)의
    FSceneViewFamily ViewFamily; // 렌더링(Render)의 뷰(View)(저장(Save)렌더링(Render)의 view)。
    TArray<FViewInfo> Views; // 렌더링(Render)의 view인스턴스 。

    FMeshElementCollector MeshCollector; //
    FMeshElementCollector RayTracingCollector; //

    // 라이트(Light)정보
    TArray<FVisibleLightInfo,SceneRenderingAllocator> VisibleLightInfos;

    // 그림자(Shadow)의 데이터
    TArray<FParallelMeshDrawCommandPass*, SceneRenderingAllocator> DispatchedShadowDepthPasses;
    FSortedShadowMaps SortedShadowsForShadowDepthPass;

    // 플래그
    bool bHasRequestedToggleFreeze;
    bool bUsedPrecomputedVisibility;

    // 을(를) 활용하여 그림자(Shadow)의 포인트 라이트(Light)리스트 (r.SupportPointLightWholeSceneShadows)
    TArray<FName, SceneRenderingAllocator> UsedWholeScenePointLightNames;

    // 플랫폼Level정보
    ERHIFeatureLevel::Type FeatureLevel;
    EShaderPlatform ShaderPlatform;
    
    (......)

public:
    FSceneRenderer(const FSceneViewFamily* InViewFamily,FHitProxyConsumer* HitProxyConsumer);
    virtual ~FSceneRenderer();

    // FSceneRenderer인터페이스 (※ 주의: 는 구현 및 추상화 인터페이스 )

    // 렌더링(Render)
    virtual void Render(FRHICommandListImmediate& RHICmdList) = 0;
    virtual void RenderHitProxies(FRHICommandListImmediate& RHICmdList) {}

    // 렌더링(Render)인스턴스
    static FSceneRenderer* CreateSceneRenderer(const FSceneViewFamily* InViewFamily, FHitProxyConsumer* HitProxyConsumer);
    void PrepareViewRectsForRendering();

#if WITH_MGPU // GPU지원
    void ComputeViewGPUMasks(FRHIGPUMask RenderTargetGPUMask);
#endif

    // 갱신(Update)개 view의 렌더링(Render)텍스처(Texture)의 결과
    void DoCrossGPUTransfers(FRHICommandListImmediate& RHICmdList, FRHIGPUMask RenderTargetGPUMask);

    // 차폐/오클루전 인터페이스 및 데이터
    bool DoOcclusionQueries(ERHIFeatureLevel::Type InFeatureLevel) const;
    void BeginOcclusionTests(FRHICommandListImmediate& RHICmdList, bool bRenderQueries);
    static FGraphEventRef OcclusionSubmittedFence[FOcclusionQueryHelpers::MaxBufferedOcclusionFrames];
    void FenceOcclusionTests(FRHICommandListImmediate& RHICmdList);
    void WaitOcclusionTests(FRHICommandListImmediate& RHICmdList);
    bool ShouldDumpMeshDrawCommandInstancingStats() const { return bDumpMeshDrawCommandInstancingStats; }
    
    static FGlobalBoundShaderState OcclusionTestBoundShaderState;
    static bool ShouldCompositeEditorPrimitives(const FViewInfo& View);

    // 렌더링(Render)실행(Execute) 및 로써 및
    static void WaitForTasksClearSnapshotsAndDeleteSceneRenderer(FRHICommandListImmediate& RHICmdList, FSceneRenderer* SceneRenderer, bool bWaitForTasks = true);
    static void DelayWaitForTasksClearSnapshotsAndDeleteSceneRenderer(FRHICommandListImmediate& RHICmdList, FSceneRenderer* SceneRenderer);
    
    // 인터페이스
    static FIntPoint ApplyResolutionFraction(...);
    static FIntPoint QuantizeViewRectMin(const FIntPoint& ViewRectMin);
    static FIntPoint GetDesiredInternalBufferSize(const FSceneViewFamily& ViewFamily);
    static ISceneViewFamilyScreenPercentage* ForkScreenPercentageInterface(...);
    
    static int32 GetRefractionQuality(const FSceneViewFamily& ViewFamily);
    
protected:
    (......)

#if WITH_MGPU // GPU지원
    FRHIGPUMask AllViewsGPUMask;
    FRHIGPUMask GetGPUMaskForShadow(FProjectedShadowInfo* ProjectedShadowInfo) const;
#endif

    // ----렌더링(Render)의 인터페이스 ----
    
    // --렌더링(Render)프로세스 및 MeshPass인터페이스 --
    void OnStartRender(FRHICommandListImmediate& RHICmdList);
    void RenderFinish(FRHICommandListImmediate& RHICmdList);
    void SetupMeshPass(FViewInfo& View, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, FViewCommands& ViewCommands);
    void GatherDynamicMeshElements(...);
    void RenderDistortion(FRHICommandListImmediate& RHICmdList);
    void InitFogConstants();
    bool ShouldRenderTranslucency(ETranslucencyPass::Type TranslucencyPass) const;
    void RenderCustomDepthPassAtLocation(FRHICommandListImmediate& RHICmdList, int32 Location);
    void RenderCustomDepthPass(FRHICommandListImmediate& RHICmdList);
    void RenderPlanarReflection(class FPlanarReflectionSceneProxy* ReflectionSceneProxy);
    void InitSkyAtmosphereForViews(FRHICommandListImmediate& RHICmdList);
    void RenderSkyAtmosphereLookUpTables(FRHICommandListImmediate& RHICmdList);
    void RenderSkyAtmosphere(FRHICommandListImmediate& RHICmdList);
    void RenderSkyAtmosphereEditorNotifications(FRHICommandListImmediate& RHICmdList);
    
    // ---그림자(Shadow)인터페이스 ---
    void InitDynamicShadows(FRHICommandListImmediate& RHICmdList, FGlobalDynamicIndexBuffer& DynamicIndexBuffer, FGlobalDynamicVertexBuffer& DynamicVertexBuffer, FGlobalDynamicReadBuffer& DynamicReadBuffer);
    bool RenderShadowProjections(FRHICommandListImmediate& RHICmdList, const FLightSceneInfo* LightSceneInfo, IPooledRenderTarget* ScreenShadowMaskTexture, IPooledRenderTarget* ScreenShadowMaskSubPixelTexture, bool bProjectingForForwardShading, bool bMobileModulatedProjections, const struct FHairStrandsVisibilityViews* InHairVisibilityViews);
    TRefCountPtr<FProjectedShadowInfo> GetCachedPreshadow(...);
    void CreatePerObjectProjectedShadow(...);
    void SetupInteractionShadows(...);
    void AddViewDependentWholeSceneShadowsForView(...);
    void AllocateShadowDepthTargets(FRHICommandListImmediate& RHICmdList);
    void AllocatePerObjectShadowDepthTargets(FRHICommandListImmediate& RHICmdList, ...);
    void AllocateCachedSpotlightShadowDepthTargets(FRHICommandListImmediate& RHICmdList, ...);
    void AllocateCSMDepthTargets(FRHICommandListImmediate& RHICmdList, ...);
    void AllocateRSMDepthTargets(FRHICommandListImmediate& RHICmdList, ...);
    void AllocateOnePassPointLightDepthTargets(FRHICommandListImmediate& RHICmdList, ...);
    void AllocateTranslucentShadowDepthTargets(FRHICommandListImmediate& RHICmdList, ...);
    bool CheckForProjectedShadows(const FLightSceneInfo* LightSceneInfo) const;
    void GatherShadowPrimitives(...);
    void RenderShadowDepthMaps(FRHICommandListImmediate& RHICmdList);
    void RenderShadowDepthMapAtlases(FRHICommandListImmediate& RHICmdList);
    void CreateWholeSceneProjectedShadow(FLightSceneInfo* LightSceneInfo, ...);
    void UpdatePreshadowCache(FSceneRenderTargets& SceneContext);
    void InitProjectedShadowVisibility(FRHICommandListImmediate& RHICmdList);    
    void GatherShadowDynamicMeshElements(FGlobalDynamicIndexBuffer& DynamicIndexBuffer, FGlobalDynamicVertexBuffer& DynamicVertexBuffer, FGlobalDynamicReadBuffer& DynamicReadBuffer);
    
    // --라이트(Light)인터페이스 --
    static void GetLightNameForDrawEvent(const FLightSceneProxy* LightProxy, FString& LightNameWithLevel);
    static void GatherSimpleLights(const FSceneViewFamily& ViewFamily, ...);
    static void SplitSimpleLightsByView(const FSceneViewFamily& ViewFamily, ...);

    // --인터페이스 --
    void PreVisibilityFrameSetup(FRHICommandListImmediate& RHICmdList);
    void ComputeViewVisibility(FRHICommandListImmediate& RHICmdList, ...);
    void PostVisibilityFrameSetup(FILCUpdatePrimTaskData& OutILCTaskData);
    
    // --인터페이스 --
    void GammaCorrectToViewportRenderTarget(FRHICommandList& RHICmdList, const FViewInfo* View, float OverrideGamma);
    FRHITexture* GetMultiViewSceneColor(const FSceneRenderTargets& SceneContext) const;
    void UpdatePrimitiveIndirectLightingCacheBuffers();
    bool ShouldRenderSkyAtmosphereEditorNotifications();
    void ResolveSceneColor(FRHICommandList& RHICmdList);

    (......)
};
cpp
FSceneRenderer는 게임 스레드의 FRendererModule::BeginRenderingViewFamily에 의해 생성 및 초기화된 다음 렌더링 스레드에 전달됩니다. 렌더링 스레드는 FSceneRenderer::Render()를 호출합니다. 렌더링 반환 후 FSceneRenderer 인스턴스가 삭제됩니다. 즉, FSceneRenderer는 매 프레임마다 생성되고 소멸됩니다. 특정 프로세스의 도식 코드는 다음과 같습니다.
cpp
void UGameEngine::Tick( float DeltaSeconds, bool bIdleMode )
{
    UGameEngine::RedrawViewports()
    {
        void FViewport::Draw( bool bShouldPresent)
        {
            void UGameViewportClient::Draw()
            {
                // 계산/산출(Calculate)ViewFamily、View의
                ULocalPlayer::CalcSceneView();
                // 렌더링(Render)
                FRendererModule::BeginRenderingViewFamily(..., FSceneViewFamily* ViewFamily)
                {
                    FScene* const Scene = ViewFamily->Scene->GetRenderScene();
                    
                    World->SendAllEndOfFrameUpdates();
                    // 생성(Create)렌더링(Render)
                    FSceneRenderer* SceneRenderer = FSceneRenderer::CreateSceneRenderer(ViewFamily, ...);
                    // 렌더링(Render)스레드(Thread)드로우/렌더(Draw).
                    ENQUEUE_RENDER_COMMAND(FDrawSceneCommand)(
                    [SceneRenderer](FRHICommandListImmediate& RHICmdList)
                    {
                        RenderViewFamily_RenderThread(RHICmdList, SceneRenderer)
                        {
                            (......)
                            // 호출(Call)렌더링(Render)의 드로우/렌더(Draw)인터페이스 .
                            SceneRenderer->Render(RHICmdList);
                            (......)
                            // SceneRenderer의 인스턴스 。
                            FSceneRenderer::WaitForTasksClearSnapshotsAndDeleteSceneRenderer(RHICmdList, SceneRenderer)
                            {
                                // 인스턴스 렌더링(Render)。
                                RHICmdList.ImmediateFlush(EImmediateFlushType::WaitForOutstandingTasksOnly);
                                // 그림자(Shadow)Pass。
                                for (int32 PassIndex = 0; PassIndex < SceneRenderer->DispatchedShadowDepthPasses.Num(); ++PassIndex)
                                {
                                    SceneRenderer->DispatchedShadowDepthPasses[PassIndex]->WaitForTasksAndEmpty();
                                }
                                (......)
                                // SceneRenderer인스턴스 。
                                delete SceneRenderer;
                                (......)
                            }
                        }
                        FlushPendingDeleteRHIResources_RenderThread();
                    });
                }
}}}}
cpp
FSceneRenderer에는 FMobileSceneRenderer와 FDeferredShadingSceneRenderer라는 두 가지 하위 클래스가 있습니다.

FMobileSceneRenderer는 기본적으로 포워드 렌더링 프로세스를 사용하는 모바일 플랫폼용 씬 렌더러입니다.

FDeferredShadingSceneRenderer는 디퍼드 셰이딩 씬 렌더러라고 불리지만 실제로는 포워드 렌더링과 디퍼드 렌더링을 포함한 두 가지 렌더링 경로를 통합합니다. 이는 PC 및 호스트 플랫폼의 기본 장면 렌더러입니다(저자도 처음 접했을 때 이 꿀 이름 때문에 혼란스러워했습니다).

**다음 장에서는 FDeferredShadingSceneRenderer의 지연 렌더링 경로에 중점을 두고 UE의 지연 렌더링 파이프라인 프로세스와 메커니즘을 설명합니다. **

4.3.2 FDeferredShadingSceneRenderer

FDeferredShadingSceneRenderer는 PC 및 호스트 플랫폼용 UE의 씬 렌더러입니다. 디퍼드 렌더링(Deferred Shading) 경로의 논리를 구현합니다. 부분 정의 및 선언은 다음과 같습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\DeferredShadingRenderer.h

class FDeferredShadingSceneRenderer : public FSceneRenderer
{
public:
    // EarlyZ
    EDepthDrawingMode EarlyZPassMode;
    bool bEarlyZPassMovable;
    bool bDitheredLODTransitionsUseStencil;
    int32 StencilLODMode = 0;
    
    FComputeFenceRHIRef TranslucencyLightingVolumeClearEndFence;

    // 함수
    FDeferredShadingSceneRenderer(const FSceneViewFamily* InViewFamily,FHitProxyConsumer* HitProxyConsumer);

    // 인터페이스
    void ClearView(FRHICommandListImmediate& RHICmdList);
    void ClearGBufferAtMaxZ(FRHICommandList& RHICmdList);
    void ClearLPVs(FRHICommandListImmediate& RHICmdList);
    void UpdateLPVs(FRHICommandListImmediate& RHICmdList);

    // PrePass인터페이스
    bool RenderPrePass(FRHICommandListImmediate& RHICmdList, TFunctionRef<void()> AfterTasksAreStarted);
    bool RenderPrePassHMD(FRHICommandListImmediate& RHICmdList);
    void RenderPrePassView(FRHICommandList& RHICmdList, ...);
    bool RenderPrePassViewParallel(const FViewInfo& View, ...);
    void PreRenderDitherFill(FRHIAsyncComputeCommandListImmediate& RHICmdList, ...);
    void PreRenderDitherFill(FRHICommandListImmediate& RHICmdList, ...);
    void RenderPrePassEditorPrimitives(FRHICommandList& RHICmdList, ...);

    // basepass인터페이스
    bool RenderBasePass(FRHICommandListImmediate& RHICmdList, ...);
    void RenderBasePassViewParallel(FViewInfo& View, ...);
    bool RenderBasePassView(FRHICommandListImmediate& RHICmdList, ...);
    
    // skypass인터페이스
    void RenderSkyPassViewParallel(FRHICommandListImmediate& ParentCmdList, ...);
    bool RenderSkyPassView(FRHICommandListImmediate& RHICmdList, ...);

    (......)

    // GBuffer & Texture
    void CopySingleLayerWaterTextures(FRHICommandListImmediate& RHICmdList, ...);
    static void BeginRenderingWaterGBuffer(FRHICommandList& RHICmdList, ...);
    void FinishWaterGBufferPassAndResolve(FRHICommandListImmediate& RHICmdList, ...);
    
    // 렌더링(Render)
    bool RenderSingleLayerWaterPass(FRHICommandListImmediate& RHICmdList, ...);
    bool RenderSingleLayerWaterPassView(FRHICommandListImmediate& RHICmdList, ...);
    void RenderSingleLayerWaterReflections(FRHICommandListImmediate& RHICmdList, ...);

    // 렌더링(Render)프로세스
    // 렌더링(Render)
    virtual void Render(FRHICommandListImmediate& RHICmdList) override;
    void RenderFinish(FRHICommandListImmediate& RHICmdList);
    
    // 렌더링(Render)인터페이스
    bool RenderHzb(FRHICommandListImmediate& RHICmdList);
    void RenderOcclusion(FRHICommandListImmediate& RHICmdList);
    void FinishOcclusion(FRHICommandListImmediate& RHICmdList);
    virtual void RenderHitProxies(FRHICommandListImmediate& RHICmdList) override;

private:
    // 정적 데이터 (을(를) 활용하여 pass의 초기화(Initialize))
    static FGraphEventRef TranslucencyTimestampQuerySubmittedFence[FOcclusionQueryHelpers::MaxBufferedOcclusionFrames + 1];
    static FGlobalDynamicIndexBuffer DynamicIndexBufferForInitViews;
    static FGlobalDynamicIndexBuffer DynamicIndexBufferForInitShadows;
    static FGlobalDynamicVertexBuffer DynamicVertexBufferForInitViews;
    static FGlobalDynamicVertexBuffer DynamicVertexBufferForInitShadows;
    static TGlobalResource<FGlobalDynamicReadBuffer> DynamicReadBufferForInitViews;
    static TGlobalResource<FGlobalDynamicReadBuffer> DynamicReadBufferForInitShadows;

    // 인터페이스
    void PreVisibilityFrameSetup(FRHICommandListImmediate& RHICmdList);

    // 초기화(Initialize)view데이터
    bool InitViews(FRHICommandListImmediate& RHICmdList, ...);
    void InitViewsPossiblyAfterPrepass(FRHICommandListImmediate& RHICmdList, ...);

    void SetupSceneReflectionCaptureBuffer(FRHICommandListImmediate& RHICmdList);
    void UpdateTranslucencyTimersAndSeparateTranslucencyBufferSize(FRHICommandListImmediate& RHICmdList);
    void CreateIndirectCapsuleShadows();

    // 렌더링(Render)
    bool RenderFog(FRHICommandListImmediate& RHICmdList, ...);
    void RenderViewFog(FRHICommandList& RHICmdList, const FViewInfo& View, ...);

    // 、、、거리、
    void RenderAtmosphere(FRHICommandListImmediate& RHICmdList, ...);
    void RenderDebugSkyAtmosphere(FRHICommandListImmediate& RHICmdList);
    void RenderDiffuseIndirectAndAmbientOcclusion(FRHICommandListImmediate& RHICmdList);
    void RenderDeferredReflectionsAndSkyLighting(FRHICommandListImmediate& RHICmdList, ...);
    void RenderDeferredReflectionsAndSkyLightingHair(FRHICommandListImmediate& RHICmdList, ...);
    void RenderDFAOAsIndirectShadowing(FRHICommandListImmediate& RHICmdList,...);
    bool RenderDistanceFieldLighting(FRHICommandListImmediate& RHICmdList,...);
    void RenderDistanceFieldAOScreenGrid(FRHICommandListImmediate& RHICmdList, ...);
    void RenderMeshDistanceFieldVisualization(FRHICommandListImmediate& RHICmdList, ...);

    // ----Tiled라이트(Light)인터페이스 ----
    // 계산/산출(Calculate)Tile의 Tile데이터 ,로써 클리핑/클램핑(Clipping)Tile을(를) 활용하여 의 라이트(Light)。오직 을(를) 활용하여 forward shading、clustered deferred shading 및 활성화(Enable)Surface lighting의 반투명(Translucent)。
    void ComputeLightGrid(FRHICommandListImmediate& RHICmdList, ...);
    bool CanUseTiledDeferred() const;
    bool ShouldUseTiledDeferred(int32 NumTiledDeferredLights) const;
    bool ShouldUseClusteredDeferredShading() const;
    bool AreClusteredLightsInLightGrid() const;
    void AddClusteredDeferredShadingPass(FRHICommandListImmediate& RHICmdList, ...);
    void RenderTiledDeferredLighting(FRHICommandListImmediate& RHICmdList, const TArray<FSortedLightSceneInfo, SceneRenderingAllocator>& SortedLights, ...);
    void GatherAndSortLights(FSortedLightSetSceneInfo& OutSortedLights);
    void RenderLights(FRHICommandListImmediate& RHICmdList, ...);
    void RenderLightArrayForOverlapViewmode(FRHICommandListImmediate& RHICmdList, ...);
    void RenderStationaryLightOverlap(FRHICommandListImmediate& RHICmdList);

    // ----렌더링(Render)라이트(Light)(、간접광(Indirect Light)、、정적 、)----
    void RenderLight(FRHICommandList& RHICmdList, ...);
    void RenderLightsForHair(FRHICommandListImmediate& RHICmdList, ...);
    void RenderLightForHair(FRHICommandList& RHICmdList, ...);
    void RenderSimpleLightsStandardDeferred(FRHICommandListImmediate& RHICmdList, ...);
    void ClearTranslucentVolumeLighting(FRHICommandListImmediate& RHICmdListViewIndex, ...);
    void InjectAmbientCubemapTranslucentVolumeLighting(FRHICommandList& RHICmdList, ...);
    void ClearTranslucentVolumePerObjectShadowing(FRHICommandList& RHICmdList, const int32 ViewIndex);
    void AccumulateTranslucentVolumeObjectShadowing(FRHICommandList& RHICmdList, ...);
    void InjectTranslucentVolumeLighting(FRHICommandListImmediate& RHICmdList, ...);
    void InjectTranslucentVolumeLightingArray(FRHICommandListImmediate& RHICmdList, ...);
    void InjectSimpleTranslucentVolumeLightingArray(FRHICommandListImmediate& RHICmdList, ...);
    void FilterTranslucentVolumeLighting(FRHICommandListImmediate& RHICmdList, ...);
    
    // Light Function
    bool RenderLightFunction(FRHICommandListImmediate& RHICmdList, ...);
    bool RenderPreviewShadowsIndicator(FRHICommandListImmediate& RHICmdList, ...);
    bool RenderLightFunctionForMaterial(FRHICommandListImmediate& RHICmdList, ...);
    
    // 반투명(Translucent)pass
    void RenderViewTranslucency(FRHICommandListImmediate& RHICmdList, ...);
    void RenderViewTranslucencyParallel(FRHICommandListImmediate& RHICmdList, ...);
    void BeginTimingSeparateTranslucencyPass(FRHICommandListImmediate& RHICmdList, const FViewInfo& View);
    void EndTimingSeparateTranslucencyPass(FRHICommandListImmediate& RHICmdList, const FViewInfo& View);
    void BeginTimingSeparateTranslucencyModulatePass(FRHICommandListImmediate& RHICmdList, ...);
    void EndTimingSeparateTranslucencyModulatePass(FRHICommandListImmediate& RHICmdList, ...);
    void SetupDownsampledTranslucencyViewParameters(FRHICommandListImmediate& RHICmdList, ...);
    void ConditionalResolveSceneColorForTranslucentMaterials(FRHICommandListImmediate& RHICmdList, ...);
    void RenderTranslucency(FRHICommandListImmediate& RHICmdList, bool bDrawUnderwaterViews = false);
    void RenderTranslucencyInner(FRHICommandListImmediate& RHICmdList, ...);

    //
    void RenderLightShaftOcclusion(FRHICommandListImmediate& RHICmdList, FLightShaftsOutput& Output);
    void RenderLightShaftBloom(FRHICommandListImmediate& RHICmdList);

    // 벨로시티 버퍼(Velocity Buffer)
    bool ShouldRenderVelocities() const;
    void RenderVelocities(FRHICommandListImmediate& RHICmdList, ...);
    void RenderVelocitiesInner(FRHICommandListImmediate& RHICmdList, ...);

    // 렌더링(Render)인터페이스
    bool RenderLightMapDensities(FRHICommandListImmediate& RHICmdList);
    bool RenderDebugViewMode(FRHICommandListImmediate& RHICmdList);
    void UpdateDownsampledDepthSurface(FRHICommandList& RHICmdList);
    void DownsampleDepthSurface(FRHICommandList& RHICmdList, ...);
    void CopyStencilToLightingChannelTexture(FRHICommandList& RHICmdList);

    // ----그림자(Shadow)렌더링(Render)인터페이스 ----
    void CreatePerObjectProjectedShadow(...);
    bool InjectReflectiveShadowMaps(FRHICommandListImmediate& RHICmdList, ...);
    bool RenderCapsuleDirectShadows(FRHICommandListImmediate& RHICmdList, ...) const;
    void SetupIndirectCapsuleShadows(FRHICommandListImmediate& RHICmdList, ...) const;
    void RenderIndirectCapsuleShadows(FRHICommandListImmediate& RHICmdList, ...) const;
    void RenderCapsuleShadowsForMovableSkylight(FRHICommandListImmediate& RHICmdList, ...) const;
    bool RenderShadowProjections(FRHICommandListImmediate& RHICmdList, ...);
    void RenderForwardShadingShadowProjections(FRHICommandListImmediate& RHICmdList, ...);

    //
    bool ShouldRenderVolumetricFog() const;
    void SetupVolumetricFog();
    void RenderLocalLightsForVolumetricFog(...);
    void RenderLightFunctionForVolumetricFog(...);
    void VoxelizeFogVolumePrimitives(...);
    void ComputeVolumetricFog(FRHICommandListImmediate& RHICmdList);
    void VisualizeVolumetricLightmap(FRHICommandListImmediate& RHICmdList);

    // 반사(Reflection)
    void RenderStandardDeferredImageBasedReflections(FRHICommandListImmediate& RHICmdList, ...);
    bool HasDeferredPlanarReflections(const FViewInfo& View) const;
    void RenderDeferredPlanarReflections(FRDGBuilder& GraphBuilder, ...);
    bool ShouldDoReflectionEnvironment() const;
    
    // 거리AO 및 그림자(Shadow)
    bool ShouldRenderDistanceFieldAO() const;
    bool ShouldPrepareForDistanceFieldShadows() const;
    bool ShouldPrepareForDistanceFieldAO() const;
    bool ShouldPrepareForDFInsetIndirectShadow() const;
    bool ShouldPrepareDistanceFieldScene() const;
    bool ShouldPrepareGlobalDistanceField() const;
    bool ShouldPrepareHeightFieldScene() const;
    void UpdateGlobalDistanceFieldObjectBuffers(FRHICommandListImmediate& RHICmdList);
    void UpdateGlobalHeightFieldObjectBuffers(FRHICommandListImmediate& RHICmdList);
    void AddOrRemoveSceneHeightFieldPrimitives(bool bSkipAdd = false);
    void PrepareDistanceFieldScene(FRHICommandListImmediate& RHICmdList, bool bSplitDispatch);
    void CopySceneCaptureComponentToTarget(FRHICommandListImmediate& RHICmdList);
    void SetupImaginaryReflectionTextureParameters(...);
    
    // 레이 트레이싱(Ray Tracing)인터페이스
    void RenderRayTracingDeferredReflections(...);
    void RenderRayTracingShadows(...);
    void RenderRayTracingStochasticRectLight(FRHICommandListImmediate& RHICmdList, ...);
    void CompositeRayTracingSkyLight(FRHICommandListImmediate& RHICmdList, ...);
    bool RenderRayTracingGlobalIllumination(FRDGBuilder& GraphBuilder, ...);
    void RenderRayTracingGlobalIlluminationBruteForce(FRDGBuilder& GraphBuilder, ...);
    void RayTracingGlobalIlluminationCreateGatherPoints(FRDGBuilder& GraphBuilder, ...);
    void RenderRayTracingGlobalIlluminationFinalGather(FRDGBuilder& GraphBuilder, ...);
    void RenderRayTracingAmbientOcclusion(FRDGBuilder& GraphBuilder, ...);
    (......)

    // 는 활성화(Enable)라이트(Light)
    bool bClusteredShadingLightsInLightGrid;
};

위에서 볼 수 있듯이 FDeferredShadingSceneRenderer에는 주로 메시 패스, 광원, 그림자, 레이 트레이싱, 반사, 가시성과 같은 여러 주요 인터페이스가 포함되어 있습니다. 그중 가장 중요한 인터페이스는 바로 FDeferredShadingSceneRenderer::Render입니다. FDeferredShadingSceneRenderer의 주요 렌더링 입구입니다. 주요 프로세스와 중요한 인터페이스의 호출은 모두 내부에서 직접 또는 간접적으로 발생합니다. FDeferredShadingSceneRenderer::Render의 로직을 세분화하면 다음과 같은 주요 단계로 나눌 수 있습니다:

무대 분석하다

FScene::UpdateAllPrimitiveSceneInfos 모든 기본 정보를 GPU로 업데이트합니다. GPUScene이 활성화되면 2차원 텍스처 또는 StructureBuffer가 기본 정보를 저장하는 데 사용됩니다.

FSceneRenderTargets::할당 필요한 경우(해상도 변경, API 트리거) 장면의 렌더링 텍스처를 재배포하여 해당 뷰를 렌더링할 수 있을 만큼 충분히 큰 크기를 보장합니다.

InitView 여러 가지 방법을 잘라서 프리미티브의 가시성을 초기화하고, 보이는 동적 그림자를 설정하고, 필요한 경우 그림자 프러스텀를 세계(전체 필드 그림자 및 객체별 그림자)와 교차시킵니다.

프리패스/깊이 전용 패스 불투명한 물체의 깊이를 렌더링하는 데 사용되는 고급 깊이 패스입니다. 이 패스는 깊이만 기록하고 색상은 기록하지 않습니다. 쓰기 깊이에는 플랫폼과 기능 수준에 따라 비활성화, 폐색만, 전체 깊이의 세 가지 모드가 있습니다. 일반적으로 하드웨어의 Early-Z 기술을 활성화하고 Base Pass의 렌더링 효율성을 향상시킬 수 있도록 Hierarchical-Z를 생성하는 데 사용됩니다.

기본 패스 **이전 장에서 언급한 기하학적 채널입니다.**노멀, 깊이, 색상, AO, 러프니스, 메탈릭 등을 포함하여 불투명한 개체(불투명 및 마스크 재질)를 렌더링하는 데 사용되는 기하학적 정보입니다. 이 기하학적 정보는 여러 GBuffer에 기록됩니다. 이 단계에서는 동적 광원의 기여도가 계산되지 않지만 라이트맵과 스카이라이트의 기여도는 계산됩니다.

오클루전 쿼리 발행/BeginOcclusionTests 폐색 렌더링을 켜면 이 프레임의 렌더링 폐색 데이터가 다음 프레임의 InitViews 단계에서 폐색 제거에 사용됩니다. 이 단계에서는 주로 객체의 바운딩 박스를 렌더링에 사용하며, 유사한 객체의 바운딩 박스도 패킹하여 Draw Calls를 줄일 수 있습니다.

조명이 단계는 이전 장에서 언급한 조명 패스이기도 하며, 표준 디퍼드 렌더링과 블록 디퍼드 렌더링이 혼합되어 있습니다. 그림자를 켜는 광원의 그림자 맵이 계산됩니다. 화면 공간 픽셀에 대한 각 조명의 기여도 계산되어 장면 색상에 누적됩니다. 또한 반투명 조명 볼륨에 대한 광원의 기여도도 계산됩니다.

안개 화면 공간의 불투명 표면 픽셀에 대한 안개와 대기의 영향을 계산합니다.

반투명 반투명 객체의 렌더링 단계. 모든 반투명 객체는 오프스크린 렌더링 텍스처(오프스크린 렌더 타겟, 코드에서는 별도의 반투명 렌더 타겟이라고 함)에 먼 곳에서 가까운 곳(뷰 공간)으로 하나씩 그려지며, 그런 다음 별도의 패스를 사용하여 조명 결과를 올바르게 계산하고 혼합합니다.

포스트 프로세스 포스트 프로세스 단계. GBuffer가 필요하지 않은 Bloom, Tone Mapping, Gamma Correction 등과 GBuffer가 필요한 SSR, SSAO, SSGI 등이 포함됩니다. 이 단계에서는 반투명 렌더 텍스처를 최종 장면 색상에 혼합합니다.

위의 내용은 프로세스의 일부에 대한 간략한 목록이지만 전부는 아닙니다. RenderDoc 도구를 사용하여 프레임을 캡처하거나 명령줄 profilegpu를 사용하여 UE의 각 프레임에 대한 자세한 렌더링 프로세스를 볼 수 있습니다.

UE 콘솔 명령줄 profilegpu가 실행된 후 팝업되는 GPU Visualizer 창을 통해 장면의 각 프레임을 렌더링하는 단계와 기간을 명확하게 확인할 수 있습니다.

렌더링 분석 도구인 RenderDoc을 사용하여 캡처한 UE의 프레임입니다.

다음 장에서는 위의 순서와 관련된 렌더링 프로세스와 최적화를 하나씩 분석합니다. 분석된 코드는 주로 FDeferredShadingSceneRenderer::Render 구현에 집중되어 있습니다. FDeferredShadingSceneRenderer::Render 덩굴을 따라 UE 씬 렌더링에 관련된 다양한 멜론을 터치해 보세요.

4.3.3 FScene::UpdateAllPrimitiveSceneInfos

FScene::UpdateAllPrimitiveSceneInfos에 대한 호출은 FDeferredShadingSceneRenderer::Render의 첫 번째 줄에서 발생합니다.

cpp
// Engine\Source\Runtime\Renderer\Private\DeferredShadingRenderer.cpp

void FDeferredShadingSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
    Scene->UpdateAllPrimitiveSceneInfos(RHICmdList, true);
    
    (......)
}

FScene::UpdateAllPrimitiveSceneInfos의 주요 기능은 CPU 측의 프리미티브 데이터를 삭제, 추가, 업데이트하고 GPU 측에 동기화하는 것입니다. GPU 메타데이터에는 두 가지 방법이 있습니다.

  • 각 프리미티브에는 고유한 균일 버퍼가 있습니다. 셰이더에서 프리미티브의 데이터에 액세스해야 하는 경우 프리미티브의 균일 버퍼에서 이를 가져올 수 있습니다. 이 구조는 간단하고 이해하기 쉬우며 모든 FeatureLevel 장치와 호환됩니다. 그러나 CPU와 GPU의 IO가 증가하고 GPU의 캐시 적중률이 감소합니다.

  • Texture2D 또는 StructuredBuffer의 GPU 장면을 사용하면 모든 기본 요소의 데이터가 규칙에 따라 여기에 배치됩니다. 셰이더의 프리미티브 데이터에 액세스해야 하는 경우 GPU 장면의 해당 위치에서 데이터를 읽어야 합니다. 구현하기 어렵고 이해하기 어려운 SM5 지원이 필요하지만 CPU와 GPU의 IO를 줄이고, GPU 캐시 적중률을 향상시키며, 레이 트레이싱과 GPU 기반 파이프라인을 더 잘 지원할 수 있습니다.

위의 액세스 방법은 다르지만 셰이더에 캡슐화되어 있습니다. 사용자는 어떤 형태의 Buffer인지 구별할 필요가 없으며, 다음 방법만 사용하면 됩니다.

cpp
GetPrimitiveData(PrimitiveId).xxx;

여기서 xxx는 기본 요소의 속성 이름입니다. 액세스 가능한 특정 속성은 다음과 같습니다.

cpp
// Engine\Shaders\Private\SceneData.ush

struct FPrimitiveSceneData
{
    float4x4 LocalToWorld;
    float4 InvNonUniformScaleAndDeterminantSign;
    float4 ObjectWorldPositionAndRadius;
    float4x4 WorldToLocal;
    float4x4 PreviousLocalToWorld;
    float4x4 PreviousWorldToLocal;
    float3 ActorWorldPosition;
    float UseSingleSampleShadowFromStationaryLights;
    float3 ObjectBounds;
    float LpvBiasMultiplier;
    float DecalReceiverMask;
    float PerObjectGBufferData;
    float UseVolumetricLightmapShadowFromStationaryLights;
    float DrawsVelocity;
    float4 ObjectOrientation;
    float4 NonUniformScale;
    float3 LocalObjectBoundsMin;
    uint LightingChannelMask;
    float3 LocalObjectBoundsMax;
    uint LightmapDataIndex;
    float3 PreSkinnedLocalBoundsMin;
    int SingleCaptureIndex;
    float3 PreSkinnedLocalBoundsMax;
    uint OutputVelocity;
    float4 CustomPrimitiveData[NUM_CUSTOM_PRIMITIVE_DATA];
};

각 그래픽 요소별로 접근할 수 있는 데이터가 여전히 많고, 차지하는 비디오 메모리도 상당하다는 것을 알 수 있다. 각 그래픽 요소는 약 576바이트를 차지합니다. 장면에 10,000개의 그래픽 요소가 있고(게임 장면에서 매우 일반적임) 패딩을 무시하면 이러한 그래픽 요소 데이터의 총량은 약 550만 개에 이릅니다.

집에 더 가까이 가서 C++ 레이어로 돌아가서 GPU 장면의 정의를 살펴보겠습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\ScenePrivate.h

class FGPUScene
{
public:
    // 는 갱신(Update)데이터 ,을(를) 활용하여 ,성능 。
    bool bUpdateAllPrimitives;

    // 갱신(Update)데이터 의 .
    TArray<int32> PrimitivesToUpdate;
    // 의 bit,의 bit로 1테이블 갱신(Update)(PrimitivesToUpdate 내에서 ).
    TBitArray<> PrimitivesMarkedToUpdate;

    // 의 GPU데이터 구조체 , 로써 는 TextureBuffer 또는 Texture2D, 그러나 오직 내에서 생성(Create) 및 , 모바일 플랫폼(Mobile)에 의해 Mobile.UseGPUSceneTexture변수.
    FRWBufferStructured PrimitiveBuffer;
    FTextureRWBuffer2D PrimitiveTexture;
    // 의 buffer
    FScatterUploadBuffer PrimitiveUploadBuffer;
    FScatterUploadBuffer PrimitiveUploadViewBuffer;

    // 라이팅
    FGrowOnlySpanAllocator    LightmapDataAllocator;
    FRWBufferStructured        LightmapDataBuffer;
    FScatterUploadBuffer    LightmapUploadBuffer;
};

이 섹션의 시작 부분에 있는 주제로 돌아가 FScene::UpdateAllPrimitiveSceneInfos 프로세스를 계속 분석합니다.

cpp
// Engine\Source\Runtime\Renderer\Private\RendererScene.cpp

void FScene::UpdateAllPrimitiveSceneInfos(FRHICommandListImmediate& RHICmdList, bool bAsyncCreateLPIs)
{
    TArray<FPrimitiveSceneInfo*> RemovedLocalPrimitiveSceneInfos(RemovedPrimitiveSceneInfos.Array());
    RemovedLocalPrimitiveSceneInfos.Sort(FPrimitiveArraySortKey());

    TArray<FPrimitiveSceneInfo*> AddedLocalPrimitiveSceneInfos(AddedPrimitiveSceneInfos.Array());
    AddedLocalPrimitiveSceneInfos.Sort(FPrimitiveArraySortKey());

    TSet<FPrimitiveSceneInfo*> DeletedSceneInfos;
    DeletedSceneInfos.Reserve(RemovedLocalPrimitiveSceneInfos.Num());

    (......)

    // 처리(Process)
    {
        (......)

        while (RemovedLocalPrimitiveSceneInfos.Num())
        {
            // 까지 포인트
            int StartIndex = RemovedLocalPrimitiveSceneInfos.Num() - 1;
            SIZE_T InsertProxyHash = RemovedLocalPrimitiveSceneInfos[StartIndex]->Proxy->GetTypeHash();

            while (StartIndex > 0 && RemovedLocalPrimitiveSceneInfos[StartIndex - 1]->Proxy->GetTypeHash() == InsertProxyHash)
            {
                StartIndex--;
            }

            (......)

            {
                SCOPED_NAMED_EVENT(FScene_SwapPrimitiveSceneInfos, FColor::Turquoise);

                for (int CheckIndex = StartIndex; CheckIndex < RemovedLocalPrimitiveSceneInfos.Num(); CheckIndex++)
                {
                    int SourceIndex = RemovedLocalPrimitiveSceneInfos[CheckIndex]->PackedIndex;

                    for (int TypeIndex = BroadIndex; TypeIndex < TypeOffsetTable.Num(); TypeIndex++)
                    {
                        FTypeOffsetTableEntry& NextEntry = TypeOffsetTable[TypeIndex];
                        int DestIndex = --NextEntry.Offset; //decrement and prepare swap 

                        // , PrimitiveSceneProxies 및 TypeOffsetTable, 로써 의 또는 회 .
                        // example swap chain of removing X 
                        // PrimitiveSceneProxies[0,0,0,6,X,6,6,6,2,2,2,2,1,1,1,7,4,8]
                        // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,X,2,2,2,1,1,1,7,4,8]
                        // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,X,1,1,1,7,4,8]
                        // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,1,1,1,X,7,4,8]
                        // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,1,1,1,7,X,4,8]
                        // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,1,1,1,7,4,X,8]
                        // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,1,1,1,7,4,8,X]

                        if (DestIndex != SourceIndex)
                        {
                            checkfSlow(DestIndex > SourceIndex, TEXT("Corrupted Prefix Sum [%d, %d]"), DestIndex, SourceIndex);
                            Primitives[DestIndex]->PackedIndex = SourceIndex;
                            Primitives[SourceIndex]->PackedIndex = DestIndex;

                            TArraySwapElements(Primitives, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveTransforms, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveSceneProxies, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveBounds, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveFlagsCompact, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveVisibilityIds, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveOcclusionFlags, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveComponentIds, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveVirtualTextureFlags, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveVirtualTextureLod, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveOcclusionBounds, DestIndex, SourceIndex);
                            TBitArraySwapElements(PrimitivesNeedingStaticMeshUpdate, DestIndex, SourceIndex);

                            AddPrimitiveToUpdateGPU(*this, SourceIndex);
                            AddPrimitiveToUpdateGPU(*this, DestIndex);

                            SourceIndex = DestIndex;
                        }
                    }
                }
            }

            const int PreviousOffset = BroadIndex > 0 ? TypeOffsetTable[BroadIndex - 1].Offset : 0;
            const int CurrentOffset = TypeOffsetTable[BroadIndex].Offset;

            if (CurrentOffset - PreviousOffset == 0)
            {
                // remove empty OffsetTable entries e.g.
                // TypeOffsetTable[3,8,12,15,15,17,18]
                // TypeOffsetTable[3,8,12,15,17,18]
                TypeOffsetTable.RemoveAt(BroadIndex);
            }

            (......)

            for (int RemoveIndex = StartIndex; RemoveIndex < RemovedLocalPrimitiveSceneInfos.Num(); RemoveIndex++)
            {
                int SourceIndex = RemovedLocalPrimitiveSceneInfos[RemoveIndex]->PackedIndex;
                check(SourceIndex >= (Primitives.Num() - RemovedLocalPrimitiveSceneInfos.Num() + StartIndex));
                Primitives.Pop();
                PrimitiveTransforms.Pop();
                PrimitiveSceneProxies.Pop();
                PrimitiveBounds.Pop();
                PrimitiveFlagsCompact.Pop();
                PrimitiveVisibilityIds.Pop();
                PrimitiveOcclusionFlags.Pop();
                PrimitiveComponentIds.Pop();
                PrimitiveVirtualTextureFlags.Pop();
                PrimitiveVirtualTextureLod.Pop();
                PrimitiveOcclusionBounds.Pop();
                PrimitivesNeedingStaticMeshUpdate.RemoveAt(PrimitivesNeedingStaticMeshUpdate.Num() - 1);
            }

            CheckPrimitiveArrays();

            for (int RemoveIndex = StartIndex; RemoveIndex < RemovedLocalPrimitiveSceneInfos.Num(); RemoveIndex++)
            {
                FPrimitiveSceneInfo* PrimitiveSceneInfo = RemovedLocalPrimitiveSceneInfos[RemoveIndex];
                FScopeCycleCounter Context(PrimitiveSceneInfo->Proxy->GetStatId());
                int32 PrimitiveIndex = PrimitiveSceneInfo->PackedIndex;
                PrimitiveSceneInfo->PackedIndex = INDEX_NONE;

                if (PrimitiveSceneInfo->Proxy->IsMovable())
                {
                    // Remove primitive's motion blur information.
                    VelocityData.RemoveFromScene(PrimitiveSceneInfo->PrimitiveComponentId);
                }

                // Unlink the primitive from its shadow parent.
                PrimitiveSceneInfo->UnlinkAttachmentGroup();

                // Unlink the LOD parent info if valid
                PrimitiveSceneInfo->UnlinkLODParentComponent();

                // Flush virtual textures touched by primitive
                PrimitiveSceneInfo->FlushRuntimeVirtualTexture();

                // Remove the primitive from the scene.
                PrimitiveSceneInfo->RemoveFromScene(true);

                // Update the primitive that was swapped to this index
                AddPrimitiveToUpdateGPU(*this, PrimitiveIndex);

                DistanceFieldSceneData.RemovePrimitive(PrimitiveSceneInfo);

                DeletedSceneInfos.Add(PrimitiveSceneInfo);
            }
            RemovedLocalPrimitiveSceneInfos.RemoveAt(StartIndex, RemovedLocalPrimitiveSceneInfos.Num() - StartIndex);
        }
    }
    
    // 처리(Process)추가(Add)
    {
        CSV_SCOPED_TIMING_STAT_EXCLUSIVE(AddPrimitiveSceneInfos);
        SCOPED_NAMED_EVENT(FScene_AddPrimitiveSceneInfos, FColor::Green);
        SCOPE_CYCLE_COUNTER(STAT_AddScenePrimitiveRenderThreadTime);
        if (AddedLocalPrimitiveSceneInfos.Num())
        {
            Primitives.Reserve(Primitives.Num() + AddedLocalPrimitiveSceneInfos.Num());
            PrimitiveTransforms.Reserve(PrimitiveTransforms.Num() + AddedLocalPrimitiveSceneInfos.Num());
            PrimitiveSceneProxies.Reserve(PrimitiveSceneProxies.Num() + AddedLocalPrimitiveSceneInfos.Num());
            PrimitiveBounds.Reserve(PrimitiveBounds.Num() + AddedLocalPrimitiveSceneInfos.Num());
            PrimitiveFlagsCompact.Reserve(PrimitiveFlagsCompact.Num() + AddedLocalPrimitiveSceneInfos.Num());
            PrimitiveVisibilityIds.Reserve(PrimitiveVisibilityIds.Num() + AddedLocalPrimitiveSceneInfos.Num());
            PrimitiveOcclusionFlags.Reserve(PrimitiveOcclusionFlags.Num() + AddedLocalPrimitiveSceneInfos.Num());
            PrimitiveComponentIds.Reserve(PrimitiveComponentIds.Num() + AddedLocalPrimitiveSceneInfos.Num());
            PrimitiveVirtualTextureFlags.Reserve(PrimitiveVirtualTextureFlags.Num() + AddedLocalPrimitiveSceneInfos.Num());
            PrimitiveVirtualTextureLod.Reserve(PrimitiveVirtualTextureLod.Num() + AddedLocalPrimitiveSceneInfos.Num());
            PrimitiveOcclusionBounds.Reserve(PrimitiveOcclusionBounds.Num() + AddedLocalPrimitiveSceneInfos.Num());
            PrimitivesNeedingStaticMeshUpdate.Reserve(PrimitivesNeedingStaticMeshUpdate.Num() + AddedLocalPrimitiveSceneInfos.Num());
        }

        while (AddedLocalPrimitiveSceneInfos.Num())
        {
            int StartIndex = AddedLocalPrimitiveSceneInfos.Num() - 1;
            SIZE_T InsertProxyHash = AddedLocalPrimitiveSceneInfos[StartIndex]->Proxy->GetTypeHash();

            while (StartIndex > 0 && AddedLocalPrimitiveSceneInfos[StartIndex - 1]->Proxy->GetTypeHash() == InsertProxyHash)
            {
                StartIndex--;
            }

            for (int AddIndex = StartIndex; AddIndex < AddedLocalPrimitiveSceneInfos.Num(); AddIndex++)
            {
                FPrimitiveSceneInfo* PrimitiveSceneInfo = AddedLocalPrimitiveSceneInfos[AddIndex];
                Primitives.Add(PrimitiveSceneInfo);
                const FMatrix LocalToWorld = PrimitiveSceneInfo->Proxy->GetLocalToWorld();
                PrimitiveTransforms.Add(LocalToWorld);
                PrimitiveSceneProxies.Add(PrimitiveSceneInfo->Proxy);
                PrimitiveBounds.AddUninitialized();
                PrimitiveFlagsCompact.AddUninitialized();
                PrimitiveVisibilityIds.AddUninitialized();
                PrimitiveOcclusionFlags.AddUninitialized();
                PrimitiveComponentIds.AddUninitialized();
                PrimitiveVirtualTextureFlags.AddUninitialized();
                PrimitiveVirtualTextureLod.AddUninitialized();
                PrimitiveOcclusionBounds.AddUninitialized();
                PrimitivesNeedingStaticMeshUpdate.Add(false);

                const int SourceIndex = PrimitiveSceneProxies.Num() - 1;
                PrimitiveSceneInfo->PackedIndex = SourceIndex;

                AddPrimitiveToUpdateGPU(*this, SourceIndex);
            }

            bool EntryFound = false;
            int BroadIndex = -1;
            //broad phase search for a matching type
            for (BroadIndex = TypeOffsetTable.Num() - 1; BroadIndex >= 0; BroadIndex--)
            {
                // example how the prefix sum of the tails could look like
                // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,2,1,1,1,7,4,8]
                // TypeOffsetTable[3,8,12,15,16,17,18]

                if (TypeOffsetTable[BroadIndex].PrimitiveSceneProxyType == InsertProxyHash)
                {
                    EntryFound = true;
                    break;
                }
            }

            //new type encountered
            if (EntryFound == false)
            {
                BroadIndex = TypeOffsetTable.Num();
                if (BroadIndex)
                {
                    FTypeOffsetTableEntry Entry = TypeOffsetTable[BroadIndex - 1];
                    //adding to the end of the list and offset of the tail (will will be incremented once during the while loop)
                    TypeOffsetTable.Push(FTypeOffsetTableEntry(InsertProxyHash, Entry.Offset));
                }
                else
                {
                    //starting with an empty list and offset zero (will will be incremented once during the while loop)
                    TypeOffsetTable.Push(FTypeOffsetTableEntry(InsertProxyHash, 0));
                }
            }

            {
                SCOPED_NAMED_EVENT(FScene_SwapPrimitiveSceneInfos, FColor::Turquoise);

                for (int AddIndex = StartIndex; AddIndex < AddedLocalPrimitiveSceneInfos.Num(); AddIndex++)
                {
                    int SourceIndex = AddedLocalPrimitiveSceneInfos[AddIndex]->PackedIndex;

                    for (int TypeIndex = BroadIndex; TypeIndex < TypeOffsetTable.Num(); TypeIndex++)
                    {
                        FTypeOffsetTableEntry& NextEntry = TypeOffsetTable[TypeIndex];
                        int DestIndex = NextEntry.Offset++; //prepare swap and increment

                        // example swap chain of inserting a type of 6 at the end
                        // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,2,1,1,1,7,4,8,6]
                        // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,1,1,1,7,4,8,2]
                        // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,2,1,1,7,4,8,1]
                        // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,2,1,1,1,4,8,7]
                        // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,2,1,1,1,7,8,4]
                        // PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,2,1,1,1,7,4,8]

                        if (DestIndex != SourceIndex)
                        {
                            checkfSlow(SourceIndex > DestIndex, TEXT("Corrupted Prefix Sum [%d, %d]"), SourceIndex, DestIndex);
                            Primitives[DestIndex]->PackedIndex = SourceIndex;
                            Primitives[SourceIndex]->PackedIndex = DestIndex;

                            TArraySwapElements(Primitives, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveTransforms, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveSceneProxies, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveBounds, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveFlagsCompact, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveVisibilityIds, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveOcclusionFlags, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveComponentIds, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveVirtualTextureFlags, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveVirtualTextureLod, DestIndex, SourceIndex);
                            TArraySwapElements(PrimitiveOcclusionBounds, DestIndex, SourceIndex);
                            TBitArraySwapElements(PrimitivesNeedingStaticMeshUpdate, DestIndex, SourceIndex);

                            AddPrimitiveToUpdateGPU(*this, DestIndex);
                        }
                    }
                }
            }

            CheckPrimitiveArrays();

            for (int AddIndex = StartIndex; AddIndex < AddedLocalPrimitiveSceneInfos.Num(); AddIndex++)
            {
                FPrimitiveSceneInfo* PrimitiveSceneInfo = AddedLocalPrimitiveSceneInfos[AddIndex];
                FScopeCycleCounter Context(PrimitiveSceneInfo->Proxy->GetStatId());
                int32 PrimitiveIndex = PrimitiveSceneInfo->PackedIndex;

                // Add the primitive to its shadow parent's linked list of children.
                // Note: must happen before AddToScene because AddToScene depends on LightingAttachmentRoot
                PrimitiveSceneInfo->LinkAttachmentGroup();
            }

            {
                SCOPED_NAMED_EVENT(FScene_AddPrimitiveSceneInfoToScene, FColor::Turquoise);
                if (GIsEditor)
                {
                    FPrimitiveSceneInfo::AddToScene(RHICmdList, this, TArrayView<FPrimitiveSceneInfo*>(&AddedLocalPrimitiveSceneInfos[StartIndex], AddedLocalPrimitiveSceneInfos.Num() - StartIndex), true);
                }
                else
                {
                    const bool bAddToDrawLists = !(CVarDoLazyStaticMeshUpdate.GetValueOnRenderThread());
                    if (bAddToDrawLists)
                    {
                        FPrimitiveSceneInfo::AddToScene(RHICmdList, this, TArrayView<FPrimitiveSceneInfo*>(&AddedLocalPrimitiveSceneInfos[StartIndex], AddedLocalPrimitiveSceneInfos.Num() - StartIndex), true, true, bAsyncCreateLPIs);
                    }
                    else
                    {
                        FPrimitiveSceneInfo::AddToScene(RHICmdList, this, TArrayView<FPrimitiveSceneInfo*>(&AddedLocalPrimitiveSceneInfos[StartIndex], AddedLocalPrimitiveSceneInfos.Num() - StartIndex), true, false, bAsyncCreateLPIs);

                        for (int AddIndex = StartIndex; AddIndex < AddedLocalPrimitiveSceneInfos.Num(); AddIndex++)
                        {
                            FPrimitiveSceneInfo* PrimitiveSceneInfo = AddedLocalPrimitiveSceneInfos[AddIndex];
                            PrimitiveSceneInfo->BeginDeferredUpdateStaticMeshes();
                        }
                    }
                }
            }

            for (int AddIndex = StartIndex; AddIndex < AddedLocalPrimitiveSceneInfos.Num(); AddIndex++)
            {
                FPrimitiveSceneInfo* PrimitiveSceneInfo = AddedLocalPrimitiveSceneInfos[AddIndex];
                int32 PrimitiveIndex = PrimitiveSceneInfo->PackedIndex;

                if (PrimitiveSceneInfo->Proxy->IsMovable() && GetFeatureLevel() > ERHIFeatureLevel::ES3_1)
                {
                    // We must register the initial LocalToWorld with the velocity state. 
                    // In the case of a moving component with MarkRenderStateDirty() called every frame, UpdateTransform will never happen.
                    VelocityData.UpdateTransform(PrimitiveSceneInfo, PrimitiveTransforms[PrimitiveIndex], PrimitiveTransforms[PrimitiveIndex]);
                }

                AddPrimitiveToUpdateGPU(*this, PrimitiveIndex);
                bPathTracingNeedsInvalidation = true;

                DistanceFieldSceneData.AddPrimitive(PrimitiveSceneInfo);

                // Flush virtual textures touched by primitive
                PrimitiveSceneInfo->FlushRuntimeVirtualTexture();

                // Set LOD parent information if valid
                PrimitiveSceneInfo->LinkLODParentComponent();

                // Update scene LOD tree
                SceneLODHierarchy.UpdateNodeSceneInfo(PrimitiveSceneInfo->PrimitiveComponentId, PrimitiveSceneInfo);
            }
            AddedLocalPrimitiveSceneInfos.RemoveAt(StartIndex, AddedLocalPrimitiveSceneInfos.Num() - StartIndex);
        }
    }
    
    // 갱신(Update)변환(Transform)모멘트(Moment)
    {
        CSV_SCOPED_TIMING_STAT_EXCLUSIVE(UpdatePrimitiveTransform);
        SCOPED_NAMED_EVENT(FScene_AddPrimitiveSceneInfos, FColor::Yellow);
        SCOPE_CYCLE_COUNTER(STAT_UpdatePrimitiveTransformRenderThreadTime);

        TArray<FPrimitiveSceneInfo*> UpdatedSceneInfosWithStaticDrawListUpdate;
        TArray<FPrimitiveSceneInfo*> UpdatedSceneInfosWithoutStaticDrawListUpdate;
        UpdatedSceneInfosWithStaticDrawListUpdate.Reserve(UpdatedTransforms.Num());
        UpdatedSceneInfosWithoutStaticDrawListUpdate.Reserve(UpdatedTransforms.Num());

        for (const auto& Transform : UpdatedTransforms)
        {
            FPrimitiveSceneProxy* PrimitiveSceneProxy = Transform.Key;
            if (DeletedSceneInfos.Contains(PrimitiveSceneProxy->GetPrimitiveSceneInfo()))
            {
                continue;
            }
            check(PrimitiveSceneProxy->GetPrimitiveSceneInfo()->PackedIndex != INDEX_NONE);

            const FBoxSphereBounds& WorldBounds = Transform.Value.WorldBounds;
            const FBoxSphereBounds& LocalBounds = Transform.Value.LocalBounds;
            const FMatrix& LocalToWorld = Transform.Value.LocalToWorld;
            const FVector& AttachmentRootPosition = Transform.Value.AttachmentRootPosition;
            FScopeCycleCounter Context(PrimitiveSceneProxy->GetStatId());

            FPrimitiveSceneInfo* PrimitiveSceneInfo = PrimitiveSceneProxy->GetPrimitiveSceneInfo();
            const bool bUpdateStaticDrawLists = !PrimitiveSceneProxy->StaticElementsAlwaysUseProxyPrimitiveUniformBuffer();
            if (bUpdateStaticDrawLists)
            {
                UpdatedSceneInfosWithStaticDrawListUpdate.Push(PrimitiveSceneInfo);
            }
            else
            {
                UpdatedSceneInfosWithoutStaticDrawListUpdate.Push(PrimitiveSceneInfo);
            }

            PrimitiveSceneInfo->FlushRuntimeVirtualTexture();

            // Remove the primitive from the scene at its old location
            // (note that the octree update relies on the bounds not being modified yet).
            PrimitiveSceneInfo->RemoveFromScene(bUpdateStaticDrawLists);

            if (PrimitiveSceneInfo->Proxy->IsMovable() && GetFeatureLevel() > ERHIFeatureLevel::ES3_1)
            {
                VelocityData.UpdateTransform(PrimitiveSceneInfo, LocalToWorld, PrimitiveSceneProxy->GetLocalToWorld());
            }

            // Update the primitive transform.
            PrimitiveSceneProxy->SetTransform(LocalToWorld, WorldBounds, LocalBounds, AttachmentRootPosition);
            PrimitiveTransforms[PrimitiveSceneInfo->PackedIndex] = LocalToWorld;

            if (!RHISupportsVolumeTextures(GetFeatureLevel())
                && (PrimitiveSceneProxy->IsMovable() || PrimitiveSceneProxy->NeedsUnbuiltPreviewLighting() || PrimitiveSceneProxy->GetLightmapType() == ELightmapType::ForceVolumetric))
            {
                PrimitiveSceneInfo->MarkIndirectLightingCacheBufferDirty();
            }

            AddPrimitiveToUpdateGPU(*this, PrimitiveSceneInfo->PackedIndex);

            DistanceFieldSceneData.UpdatePrimitive(PrimitiveSceneInfo);

            // If the primitive has static mesh elements, it should have returned true from ShouldRecreateProxyOnUpdateTransform!
            check(!(bUpdateStaticDrawLists && PrimitiveSceneInfo->StaticMeshes.Num()));
        }

        // Re-add the primitive to the scene with the new transform.
        if (UpdatedSceneInfosWithStaticDrawListUpdate.Num() > 0)
        {
            FPrimitiveSceneInfo::AddToScene(RHICmdList, this, UpdatedSceneInfosWithStaticDrawListUpdate, true, true, bAsyncCreateLPIs);
        }

        if (UpdatedSceneInfosWithoutStaticDrawListUpdate.Num() > 0)
        {
            FPrimitiveSceneInfo::AddToScene(RHICmdList, this, UpdatedSceneInfosWithoutStaticDrawListUpdate, false, true, bAsyncCreateLPIs);
            for (FPrimitiveSceneInfo* PrimitiveSceneInfo : UpdatedSceneInfosWithoutStaticDrawListUpdate)
            {
                PrimitiveSceneInfo->FlushRuntimeVirtualTexture();
            }
        }

        if (AsyncCreateLightPrimitiveInteractionsTask && AsyncCreateLightPrimitiveInteractionsTask->GetTask().HasPendingPrimitives())
        {
            check(GAsyncCreateLightPrimitiveInteractions);
            AsyncCreateLightPrimitiveInteractionsTask->StartBackgroundTask();
        }

        for (const auto& Transform : OverridenPreviousTransforms)
        {
            FPrimitiveSceneInfo* PrimitiveSceneInfo = Transform.Key;
            VelocityData.OverridePreviousTransform(PrimitiveSceneInfo->PrimitiveComponentId, Transform.Value);
        }
    }

    (......)
}

요약하자면 FScene::UpdateAllPrimitiveSceneInfos의 기능은 프리미티브를 삭제, 추가하고 변환 행렬, 사용자 정의 데이터, 거리 필드 데이터 등을 포함한 프리미티브의 모든 데이터를 업데이트하는 것입니다.

흥미롭게도 프리미티브를 삭제하거나 추가할 때 순서가 지정된 PrimitiveSceneProxies와 TypeOffsetTable이 결합되어 요소 작업 중 이동 또는 교환 횟수를 줄일 수 있다는 점은 영리합니다. 교환 프로세스는 주석 처리된 코드에 명확하게 설명되어 있습니다.

cpp
// :회 의 까지 타입/유형 의 ,까지 리스트
// PrimitiveSceneProxies[0,0,0,6,X,6,6,6,2,2,2,2,1,1,1,7,4,8]
// PrimitiveSceneProxies[0,0,0,6,6,6,6,6,X,2,2,2,1,1,1,7,4,8]
// PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,X,1,1,1,7,4,8]
// PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,1,1,1,X,7,4,8]
// PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,1,1,1,7,X,4,8]
// PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,1,1,1,7,4,X,8]
// PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,1,1,1,7,4,8,X]

// 추가(Add):추가(Add)의 리스트 ,회 및 타입/유형 의 。
// PrimitiveSceneProxies[0,0,0,6,6,6,6,6,2,2,2,2,1,1,1,7,4,8,6]
// PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,1,1,1,7,4,8,2]
// PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,2,1,1,7,4,8,1]
// PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,2,1,1,1,4,8,7]
// PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,2,1,1,1,7,8,4]
// PrimitiveSceneProxies[0,0,0,6,6,6,6,6,6,2,2,2,2,1,1,1,7,4,8]

또한 추가 또는 삭제되었거나 데이터가 변경된 모든 기본 인덱스에 대해 AddPrimitiveToUpdateGPU가 호출되어 업데이트할 목록에 추가됩니다.

cpp
// Engine\Source\Runtime\Renderer\Private\GPUScene.cpp

void AddPrimitiveToUpdateGPU(FScene& Scene, int32 PrimitiveId)
{
    if (UseGPUScene(GMaxRHIShaderPlatform, Scene.GetFeatureLevel()))
    { 
        if (PrimitiveId + 1 > Scene.GPUScene.PrimitivesMarkedToUpdate.Num())
        {
            const int32 NewSize = Align(PrimitiveId + 1, 64);
            Scene.GPUScene.PrimitivesMarkedToUpdate.Add(0, NewSize - Scene.GPUScene.PrimitivesMarkedToUpdate.Num());
        }

        // 갱신(Update)의 추가(Add)PrimitivesToUpdate 및 PrimitivesMarkedToUpdate 내에서
        if (!Scene.GPUScene.PrimitivesMarkedToUpdate[PrimitiveId])
        {
            Scene.GPUScene.PrimitivesToUpdate.Add(PrimitiveId);
            Scene.GPUScene.PrimitivesMarkedToUpdate[PrimitiveId] = true;
        }
    }
}

GPUScene의 PrimitivesToUpdate 및 PrimitivesMarkedToUpdate가 업데이트해야 하는 모든 기본 인덱스를 수집한 후 FDeferredShadingSceneRenderer::Render의 InitViews 이후 GPU에 동기화됩니다.

cpp
void FDeferredShadingSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
    // 갱신(Update)GPUScene의 데이터
    Scene->UpdateAllPrimitiveSceneInfos(RHICmdList, true);

    (......)

    // 초기화(Initialize)View의 데이터
    bDoInitViewAftersPrepass = InitViews(RHICmdList, BasePassDepthStencilAccess, ILCTaskData, UpdateViewCustomDataEvents);

    (......)

    // 동기화 CPU의 GPUScene까지 GPU.
    UpdateGPUScene(RHICmdList, *Scene);
    
    (......)
}

특정 동기화 논리를 보려면 UpdateGPUScene을 입력하세요.

cpp
// Engine\Source\Runtime\Renderer\Private\GPUScene.cpp

void UpdateGPUScene(FRHICommandListImmediate& RHICmdList, FScene& Scene)
{
    // 에 따라 의 GPUScene저장(Store)타입/유형 호출(Call)의 인터페이스 。
    if (GPUSceneUseTexture2D(Scene.GetShaderPlatform()))
    {
        UpdateGPUSceneInternal<FTextureRWBuffer2D>(RHICmdList, Scene);
    }
    else
    {
        UpdateGPUSceneInternal<FRWBufferStructured>(RHICmdList, Scene);
    }
}

// ResourceType는 캡슐화 FTextureRWBuffer2D 및 FRWBufferStructured의 스텐실 타입/유형
template<typename ResourceType>
void UpdateGPUSceneInternal(FRHICommandListImmediate& RHICmdList, FScene& Scene)
{
    if (UseGPUScene(GMaxRHIShaderPlatform, Scene.GetFeatureLevel()))
    {
        // 는 데이터 동기화 ?
        if (GGPUSceneUploadEveryFrame || Scene.GPUScene.bUpdateAllPrimitives)
        {
            for (int32 Index : Scene.GPUScene.PrimitivesToUpdate)
            {
                Scene.GPUScene.PrimitivesMarkedToUpdate[Index] = false;
            }
            Scene.GPUScene.PrimitivesToUpdate.Reset();

            for (int32 i = 0; i < Scene.Primitives.Num(); i++)
            {
                Scene.GPUScene.PrimitivesToUpdate.Add(i);
            }

            Scene.GPUScene.bUpdateAllPrimitives = false;
        }

        bool bResizedPrimitiveData = false;
        bool bResizedLightmapData = false;

        // 페치/가져오기(Fetch)GPU의 리소스 : GPUScene.PrimitiveBuffer 또는 GPUScene.PrimitiveTexture
        ResourceType* MirrorResourceGPU = GetMirrorGPU<ResourceType>(Scene);
        // 만약 리소스 해상도/크기 , 이면 확장 . 는 256, 2의 N회 .
        {
            const uint32 SizeReserve = FMath::RoundUpToPowerOfTwo( FMath::Max( Scene.Primitives.Num(), 256 ) );
            // 만약 리소스 해상도/크기 , 생성(Create)리소스 , 리소스 의 데이터 까지 리소스 .
            bResizedPrimitiveData = ResizeResourceIfNeeded(RHICmdList, *MirrorResourceGPU, SizeReserve * sizeof(FPrimitiveSceneShaderData::Data), TEXT("PrimitiveData"));
        }

        {
            const uint32 SizeReserve = FMath::RoundUpToPowerOfTwo( FMath::Max( Scene.GPUScene.LightmapDataAllocator.GetMaxSize(), 256 ) );
            bResizedLightmapData = ResizeResourceIfNeeded(RHICmdList, Scene.GPUScene.LightmapDataBuffer, SizeReserve * sizeof(FLightmapSceneShaderData::Data), TEXT("LightmapData"));
        }

        const int32 NumPrimitiveDataUploads = Scene.GPUScene.PrimitivesToUpdate.Num();

        int32 NumLightmapDataUploads = 0;

        if (NumPrimitiveDataUploads > 0)
        {
            // 갱신(Update)의 데이터 까지 PrimitiveUploadBuffer 내에서 .에 의해 Buffer의 해상도/크기 최댓값(Max), 따라서 회 의 Buffer해상도/크기 , 만약 해상도/크기 , .
            const int32 MaxPrimitivesUploads = GetMaxPrimitivesUpdate(NumPrimitiveDataUploads, FPrimitiveSceneShaderData::PrimitiveDataStrideInFloat4s);
            for (int32 PrimitiveOffset = 0; PrimitiveOffset < NumPrimitiveDataUploads; PrimitiveOffset += MaxPrimitivesUploads)
            {
                SCOPED_DRAW_EVENTF(RHICmdList, UpdateGPUScene, TEXT("UpdateGPUScene PrimitivesToUpdate and Offset = %u %u"), NumPrimitiveDataUploads, PrimitiveOffset);

                Scene.GPUScene.PrimitiveUploadBuffer.Init(MaxPrimitivesUploads, sizeof(FPrimitiveSceneShaderData::Data), true, TEXT("PrimitiveUploadBuffer"));

                // 개 해상도/크기 의 회 , 개 데이터 까지 PrimitiveUploadBuffer 내에서 .
                for (int32 IndexUpdate = 0; (IndexUpdate < MaxPrimitivesUploads) && ((IndexUpdate + PrimitiveOffset) < NumPrimitiveDataUploads); ++IndexUpdate)
                {
                    int32 Index = Scene.GPUScene.PrimitivesToUpdate[IndexUpdate + PrimitiveOffset];
                    // PrimitivesToUpdate may contain a stale out of bounds index, as we don't remove update request on primitive removal from scene.
                    if (Index < Scene.PrimitiveSceneProxies.Num())
                    {
                        FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy = Scene.PrimitiveSceneProxies[Index];
                        NumLightmapDataUploads += PrimitiveSceneProxy->GetPrimitiveSceneInfo()->GetNumLightmapDataEntries();

                        FPrimitiveSceneShaderData PrimitiveSceneData(PrimitiveSceneProxy);
                        Scene.GPUScene.PrimitiveUploadBuffer.Add(Index, &PrimitiveSceneData.Data[0]);
                    }
                    Scene.GPUScene.PrimitivesMarkedToUpdate[Index] = false;
                }

                // 변환(Transform/Convert)UAV의 상태 까지 Compute, 로써 데이터 GPU데이터 .
                if (bResizedPrimitiveData)
                {
                    RHICmdList.TransitionResource(EResourceTransitionAccess::ERWBarrier, EResourceTransitionPipeline::EComputeToCompute, MirrorResourceGPU->UAV);
                }
                else
                {
                    RHICmdList.TransitionResource(EResourceTransitionAccess::EWritable, EResourceTransitionPipeline::EGfxToCompute, MirrorResourceGPU->UAV);
                }

                // 데이터 까지 GPU.
                {
                    Scene.GPUScene.PrimitiveUploadBuffer.ResourceUploadTo(RHICmdList, *MirrorResourceGPU, true);
                }
            }

            // 변환(Transform/Convert)UAV의 상태 까지 Gfx, 로써 렌더링(Render)shader을(를) 활용하여 .
            RHICmdList.TransitionResource(EResourceTransitionAccess::EReadable, EResourceTransitionPipeline::EComputeToGfx, MirrorResourceGPU->UAV);
        }

        // MirrorResourceGPU데이터 .
        if (GGPUSceneValidatePrimitiveBuffer && (Scene.GPUScene.PrimitiveBuffer.NumBytes > 0 || Scene.GPUScene.PrimitiveTexture.NumBytes > 0))
        {
            //UE_LOG(LogRenderer, Warning, TEXT("r.GPUSceneValidatePrimitiveBuffer enabled, doing slow readback from GPU"));
            uint32 Stride = 0;
            FPrimitiveSceneShaderData* InstanceBufferCopy = (FPrimitiveSceneShaderData*)(FPrimitiveSceneShaderData*)LockResource(*MirrorResourceGPU, Stride);

            const int32 TotalNumberPrimitives = Scene.PrimitiveSceneProxies.Num();
            int32 MaxPrimitivesUploads = GetMaxPrimitivesUpdate(TotalNumberPrimitives, FPrimitiveSceneShaderData::PrimitiveDataStrideInFloat4s);
            for (int32 IndexOffset = 0; IndexOffset < TotalNumberPrimitives; IndexOffset += MaxPrimitivesUploads)
            {
                for (int32 Index = 0; (Index < MaxPrimitivesUploads) && ((Index + IndexOffset) < TotalNumberPrimitives); ++Index)
                {
                    FPrimitiveSceneShaderData PrimitiveSceneData(Scene.PrimitiveSceneProxies[Index + IndexOffset]);

                    for (int i = 0; i < FPrimitiveSceneShaderData::PrimitiveDataStrideInFloat4s; i++)
                    {
                        check(PrimitiveSceneData.Data[i] == InstanceBufferCopy[Index].Data[i]);
                    }
                }
                InstanceBufferCopy += Stride / sizeof(FPrimitiveSceneShaderData);
            }

            UnlockResourceGPUScene(*MirrorResourceGPU);
        }

        (......)
    }
}

위의 코드와 관련하여 언급할 가치가 있는 것은 다음과 같습니다.

  • 너무 작은 크기로 인해 GPU 리소스가 다시 생성되는 것을 줄이기 위해 GPU Scene의 리소스 증가(보유) 전략은 256 이상이며 2의 N승을 위로 가져옵니다(즉, 두 배). 이 전략은 std::Vector와 다소 유사합니다.

  • UE의 기본 RHI는 DirectX11을 기반으로 합니다. 리소스를 변환할 때 ResourceTransitionPipeline에는 Gfx와 Compute의 두 가지 유형만 있으며 DirectX12의 Copy 유형은 무시됩니다.

cpp
// Engine\Source\Runtime\RHI\Public\RHI.h

enum class EResourceTransitionPipeline
{
    EGfxToCompute,
    EComputeToGfx,
    EGfxToGfx,
    EComputeToCompute,    
};

4.3.4 InitViews

InitView는 GPU 장면이 업데이트된 후 즉시 실행됩니다. 가시성 결정, 장면 그래프 메타데이터 및 태그 수집, 가시적 그리드 명령 생성, 패스 렌더링에 필요한 데이터 초기화 등 많은 렌더링 로직을 처리하며 중요합니다. 코드는 다음과 같습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp

bool FDeferredShadingSceneRenderer::InitViews(FRHICommandListImmediate& RHICmdList, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, struct FILCUpdatePrimTaskData& ILCTaskData, FGraphEventArray& UpdateViewCustomDataEvents)
{
    // 생성(Create)설정(Set)스테이지 .
    PreVisibilityFrameSetup(RHICmdList);

    RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);

    // 초기화(Initialize)
    {
        if (Scene->FXSystem && Scene->FXSystem->RequiresEarlyViewUniformBuffer() && Views.IsValidIndex(0))
        {
            // 개 view의 RHI리소스 초기화(Initialize).
            Views[0].InitRHIResources();
            Scene->FXSystem->PostInitViews(RHICmdList, Views[0].ViewUniformBuffer, Views[0].AllowGPUParticleUpdate() && !ViewFamily.EngineShowFlags.HitProxies);
        }
    }
    
    // 생성(Create).
    FViewVisibleCommandsPerView ViewCommandsPerView;
    ViewCommandsPerView.SetNum(Views.Num());

    // 계산/산출(Calculate).
    ComputeViewVisibility(RHICmdList, BasePassDepthStencilAccess, ViewCommandsPerView, DynamicIndexBufferForInitViews, DynamicVertexBufferForInitViews, DynamicReadBufferForInitViews);

    RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);

    // 그림자(Shadow)
    CreateIndirectCapsuleShadows();
    RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);

    // 초기화(Initialize).
    if (ShouldRenderSkyAtmosphere(Scene, ViewFamily.EngineShowFlags))
    {
        InitSkyAtmosphereForViews(RHICmdList);
    }

    // 생성(Create)설정(Set)스테이지 .
    PostVisibilityFrameSetup(ILCTaskData);
    RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);

    (......)

    // 는 Prepass초기화(Initialize)view,에 의해 GDoInitViewsLightingAfterPrepass,GDoInitViewsLightingAfterPrepassr.DoInitViewsLightingAfterPrepass。
    bool bDoInitViewAftersPrepass = !!GDoInitViewsLightingAfterPrepass;
    if (!bDoInitViewAftersPrepass)
    {
        InitViewsPossiblyAfterPrepass(RHICmdList, ILCTaskData, UpdateViewCustomDataEvents);
    }

    {
        // 초기화(Initialize)view의 uniform buffer 및 RHI리소스 .
        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
        {
            FViewInfo& View = Views[ViewIndex];

            if (View.ViewState)
            {
                if (!View.ViewState->ForwardLightingResources)
                {
                    View.ViewState->ForwardLightingResources.Reset(new FForwardLightingViewResources());
                }

                View.ForwardLightingResources = View.ViewState->ForwardLightingResources.Get();
            }
            else
            {
                View.ForwardLightingResourcesStorage.Reset(new FForwardLightingViewResources());
                View.ForwardLightingResources = View.ForwardLightingResourcesStorage.Get();
            }

#if RHI_RAYTRACING
            View.IESLightProfileResource = View.ViewState ? &View.ViewState->IESLightProfileResources : nullptr;
#endif
            // Set the pre-exposure before initializing the constant buffers.
            if (View.ViewState)
            {
                View.ViewState->UpdatePreExposure(View);
            }

            // Initialize the view's RHI resources.
            View.InitRHIResources();
        }
    }

    //
    SetupVolumetricFog();
    
    // 렌더링(Render)이벤트 .
    OnStartRender(RHICmdList);

    return bDoInitViewAftersPrepass;
}

위의 코드는 많은 로직을 수행하지 않는 것처럼 보이지만 실제로는 위의 일부 중요한 인터페이스에 많은 로직이 분산되어 있습니다. 먼저 PreVisibilityFrameSetup을 분석해 보겠습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp

void FDeferredShadingSceneRenderer::PreVisibilityFrameSetup(FRHICommandListImmediate& RHICmdList)
{
    // Possible stencil dither optimization approach
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
    {
        FViewInfo& View = Views[ViewIndex];
        View.bAllowStencilDither = bDitheredLODTransitionsUseStencil;
    }

    FSceneRenderer::PreVisibilityFrameSetup(RHICmdList);
}

void FSceneRenderer::PreVisibilityFrameSetup(FRHICommandListImmediate& RHICmdList)
{
    // RHI렌더링(Render).
    RHICmdList.BeginScene();

    {
        static auto CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.DoLazyStaticMeshUpdate"));
        const bool DoLazyStaticMeshUpdate = (CVar->GetInt() && !GIsEditor);

        // 레이턴시 의 정적 갱신(Update).
        if (DoLazyStaticMeshUpdate)
        {
            QUICK_SCOPE_CYCLE_COUNTER(STAT_PreVisibilityFrameSetup_EvictionForLazyStaticMeshUpdate);
            static int32 RollingRemoveIndex = 0;
            static int32 RollingPassShrinkIndex = 0;
            if (RollingRemoveIndex >= Scene->Primitives.Num())
            {
                RollingRemoveIndex = 0;
                RollingPassShrinkIndex++;
                if (RollingPassShrinkIndex >= UE_ARRAY_COUNT(Scene->CachedDrawLists))
                {
                    RollingPassShrinkIndex = 0;
                }
                // Periodically shrink the SparseArray containing cached mesh draw commands which we are causing to be regenerated with UpdateStaticMeshes
                Scene->CachedDrawLists[RollingPassShrinkIndex].MeshDrawCommands.Shrink();
            }
            const int32 NumRemovedPerFrame = 10;
            TArray<FPrimitiveSceneInfo*, TInlineAllocator<10>> SceneInfos;
            for (int32 NumRemoved = 0; NumRemoved < NumRemovedPerFrame && RollingRemoveIndex < Scene->Primitives.Num(); NumRemoved++, RollingRemoveIndex++)
            {
                SceneInfos.Add(Scene->Primitives[RollingRemoveIndex]);
            }
            FPrimitiveSceneInfo::UpdateStaticMeshes(RHICmdList, Scene, SceneInfos, false);
        }
    }

    // 변환(Transform/Convert)Skin Cache
    RunGPUSkinCacheTransition(RHICmdList, Scene, EGPUSkinCacheTransition::FrameSetup);

    // 초기화(Initialize)Groom
    if (IsHairStrandsEnable(Scene->GetShaderPlatform()) && Views.Num() > 0)
    {
        const EWorldType::Type WorldType = Views[0].Family->Scene->GetWorld()->WorldType;
        const FShaderDrawDebugData* ShaderDrawData = &Views[0].ShaderDrawData;
        auto ShaderMap = GetGlobalShaderMap(FeatureLevel);
        RunHairStrandsInterpolation(RHICmdList, WorldType, Scene->GetGPUSkinCache(), ShaderDrawData, ShaderMap, EHairStrandsInterpolationType::SimulationStrands, nullptr);
    }

    //
    if (Scene->FXSystem && Views.IsValidIndex(0))
    {
        Scene->FXSystem->PreInitViews(RHICmdList, Views[0].AllowGPUParticleUpdate() && !ViewFamily.EngineShowFlags.HitProxies);
    }

    (......)

    // 설정(Set)모션 블러(Motion Blur)파라미터 (TAA의 처리(Process))
    for(int32 ViewIndex = 0;ViewIndex < Views.Num();ViewIndex++)
    {
        FViewInfo& View = Views[ViewIndex];
        FSceneViewState* ViewState = View.ViewState;

        check(View.VerifyMembersChecks());

        // Once per render increment the occlusion frame counter.
        if (ViewState)
        {
            ViewState->OcclusionFrameCounter++;
        }

        // HighResScreenshot should get best results so we don't do the occlusion optimization based on the former frame
        extern bool GIsHighResScreenshot;
        const bool bIsHitTesting = ViewFamily.EngineShowFlags.HitProxies;
        // Don't test occlusion queries in collision viewmode as they can be bigger then the rendering bounds.
        const bool bCollisionView = ViewFamily.EngineShowFlags.CollisionVisibility || ViewFamily.EngineShowFlags.CollisionPawn;
        if (GIsHighResScreenshot || !DoOcclusionQueries(FeatureLevel) || bIsHitTesting || bCollisionView)
        {
            View.bDisableQuerySubmissions = true;
            View.bIgnoreExistingQueries = true;
        }
        FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);

        // set up the screen area for occlusion
        float NumPossiblePixels = SceneContext.UseDownsizedOcclusionQueries() && IsValidRef(SceneContext.GetSmallDepthSurface()) ? 
            (float)View.ViewRect.Width() / SceneContext.GetSmallColorDepthDownsampleFactor() * (float)View.ViewRect.Height() / SceneContext.GetSmallColorDepthDownsampleFactor() :
            View.ViewRect.Width() * View.ViewRect.Height();
        View.OneOverNumPossiblePixels = NumPossiblePixels > 0.0 ? 1.0f / NumPossiblePixels : 0.0f;

        // Still need no jitter to be set for temporal feedback on SSR (it is enabled even when temporal AA is off).
        check(View.TemporalJitterPixels.X == 0.0f);
        check(View.TemporalJitterPixels.Y == 0.0f);
        
        // Cache the projection matrix b        
        // Cache the projection matrix before AA is applied
        View.ViewMatrices.SaveProjectionNoAAMatrix();

        if (ViewState)
        {
            check(View.bStatePrevViewInfoIsReadOnly);
            View.bStatePrevViewInfoIsReadOnly = ViewFamily.bWorldIsPaused || ViewFamily.EngineShowFlags.HitProxies || bFreezeTemporalHistories;

            ViewState->SetupDistanceFieldTemporalOffset(ViewFamily);

            if (!View.bStatePrevViewInfoIsReadOnly && !bFreezeTemporalSequences)
            {
                ViewState->FrameIndex++;
            }

            if (View.OverrideFrameIndexValue.IsSet())
            {
                ViewState->FrameIndex = View.OverrideFrameIndexValue.GetValue();
            }
        }
        
        // Subpixel jitter for temporal AA
        int32 CVarTemporalAASamplesValue = CVarTemporalAASamples.GetValueOnRenderThread();

        bool bTemporalUpsampling = View.PrimaryScreenPercentageMethod == EPrimaryScreenPercentageMethod::TemporalUpscale;
        
        // Apply a sub pixel offset to the view.
        if (View.AntiAliasingMethod == AAM_TemporalAA && ViewState && (CVarTemporalAASamplesValue > 0 || bTemporalUpsampling) && View.bAllowTemporalJitter)
        {
            float EffectivePrimaryResolutionFraction = float(View.ViewRect.Width()) / float(View.GetSecondaryViewRectSize().X);

            // Compute number of TAA samples.
            int32 TemporalAASamples = CVarTemporalAASamplesValue;
            {
                if (Scene->GetFeatureLevel() < ERHIFeatureLevel::SM5)
                {
                    // Only support 2 samples for mobile temporal AA.
                    TemporalAASamples = 2;
                }
                else if (bTemporalUpsampling)
                {
                    // When doing TAA upsample with screen percentage < 100%, we need extra temporal samples to have a
                    // constant temporal sample density for final output pixels to avoid output pixel aligned converging issues.
                    TemporalAASamples = float(TemporalAASamples) * FMath::Max(1.f, 1.f / (EffectivePrimaryResolutionFraction * EffectivePrimaryResolutionFraction));
                }
                else if (CVarTemporalAASamplesValue == 5)
                {
                    TemporalAASamples = 4;
                }

                TemporalAASamples = FMath::Clamp(TemporalAASamples, 1, 255);
            }

            // Compute the new sample index in the temporal sequence.
            int32 TemporalSampleIndex            = ViewState->TemporalAASampleIndex + 1;
            if(TemporalSampleIndex >= TemporalAASamples || View.bCameraCut)
            {
                TemporalSampleIndex = 0;
            }

            // Updates view state.
            if (!View.bStatePrevViewInfoIsReadOnly && !bFreezeTemporalSequences)
            {
                ViewState->TemporalAASampleIndex          = TemporalSampleIndex;
                ViewState->TemporalAASampleIndexUnclamped = ViewState->TemporalAASampleIndexUnclamped+1;
            }

            // 에 따라 의 TAA샘플링 및 파라미터 , 및 계산/산출(Calculate)의 샘플링파라미터 .
            float SampleX, SampleY;
            if (Scene->GetFeatureLevel() < ERHIFeatureLevel::SM5)
            {
                float SamplesX[] = { -8.0f/16.0f, 0.0/16.0f };
                float SamplesY[] = { /* - */ 0.0f/16.0f, 8.0/16.0f };
                check(TemporalAASamples == UE_ARRAY_COUNT(SamplesX));
                SampleX = SamplesX[ TemporalSampleIndex ];
                SampleY = SamplesY[ TemporalSampleIndex ];
            }
            else if (View.PrimaryScreenPercentageMethod == EPrimaryScreenPercentageMethod::TemporalUpscale)
            {
                // Uniformly distribute temporal jittering in [-.5; .5], because there is no longer any alignement of input and output pixels.
                SampleX = Halton(TemporalSampleIndex + 1, 2) - 0.5f;
                SampleY = Halton(TemporalSampleIndex + 1, 3) - 0.5f;

                View.MaterialTextureMipBias = -(FMath::Max(-FMath::Log2(EffectivePrimaryResolutionFraction), 0.0f) ) + CVarMinAutomaticViewMipBiasOffset.GetValueOnRenderThread();
                View.MaterialTextureMipBias = FMath::Max(View.MaterialTextureMipBias, CVarMinAutomaticViewMipBias.GetValueOnRenderThread());
            }
            else if( CVarTemporalAASamplesValue == 2 )
            {
                // 2xMSAA
                // Pattern docs: http://msdn.microsoft.com/en-us/library/windows/desktop/ff476218(v=vs.85).aspx
                //   N.
                //   .S
                float SamplesX[] = { -4.0f/16.0f, 4.0/16.0f };
                float SamplesY[] = { -4.0f/16.0f, 4.0/16.0f };
                check(TemporalAASamples == UE_ARRAY_COUNT(SamplesX));
                SampleX = SamplesX[ TemporalSampleIndex ];
                SampleY = SamplesY[ TemporalSampleIndex ];
            }
            else if( CVarTemporalAASamplesValue == 3 )
            {
                // 3xMSAA
                //   A..
                //   ..B
                //   .C.
                // Rolling circle pattern (A,B,C).
                float SamplesX[] = { -2.0f/3.0f,  2.0/3.0f,  0.0/3.0f };
                float SamplesY[] = { -2.0f/3.0f,  0.0/3.0f,  2.0/3.0f };
                check(TemporalAASamples == UE_ARRAY_COUNT(SamplesX));
                SampleX = SamplesX[ TemporalSampleIndex ];
                SampleY = SamplesY[ TemporalSampleIndex ];
            }
            else if( CVarTemporalAASamplesValue == 4 )
            {
                // 4xMSAA
                // Pattern docs: http://msdn.microsoft.com/en-us/library/windows/desktop/ff476218(v=vs.85).aspx
                //   .N..
                //   ...E
                //   W...
                //   ..S.
                // Rolling circle pattern (N,E,S,W).
                float SamplesX[] = { -2.0f/16.0f,  6.0/16.0f, 2.0/16.0f, -6.0/16.0f };
                float SamplesY[] = { -6.0f/16.0f, -2.0/16.0f, 6.0/16.0f,  2.0/16.0f };
                check(TemporalAASamples == UE_ARRAY_COUNT(SamplesX));
                SampleX = SamplesX[ TemporalSampleIndex ];
                SampleY = SamplesY[ TemporalSampleIndex ];
            }
            else if( CVarTemporalAASamplesValue == 5 )
            {
                // Compressed 4 sample pattern on same vertical and horizontal line (less temporal flicker).
                // Compressed 1/2 works better than correct 2/3 (reduced temporal flicker).
                //   . N .
                //   W . E
                //   . S .
                // Rolling circle pattern (N,E,S,W).
                float SamplesX[] = {  0.0f/2.0f,  1.0/2.0f,  0.0/2.0f, -1.0/2.0f };
                float SamplesY[] = { -1.0f/2.0f,  0.0/2.0f,  1.0/2.0f,  0.0/2.0f };
                check(TemporalAASamples == UE_ARRAY_COUNT(SamplesX));
                SampleX = SamplesX[ TemporalSampleIndex ];
                SampleY = SamplesY[ TemporalSampleIndex ];
            }
            else
            {
                float u1 = Halton( TemporalSampleIndex + 1, 2 );
                float u2 = Halton( TemporalSampleIndex + 1, 3 );

                // Generates samples in normal distribution
                // exp( x^2 / Sigma^2 )
                    
                static auto CVar = IConsoleManager::Get().FindConsoleVariable(TEXT("r.TemporalAAFilterSize"));
                float FilterSize = CVar->GetFloat();

                // Scale distribution to set non-unit variance
                // Variance = Sigma^2
                float Sigma = 0.47f * FilterSize;

                // Window to [-0.5, 0.5] output
                // Without windowing we could generate samples far away on the infinite tails.
                float OutWindow = 0.5f;
                float InWindow = FMath::Exp( -0.5 * FMath::Square( OutWindow / Sigma ) );
                    
                // Box-Muller transform
                float Theta = 2.0f * PI * u2;
                float r = Sigma * FMath::Sqrt( -2.0f * FMath::Loge( (1.0f - u1) * InWindow + u1 ) );
                    
                SampleX = r * FMath::Cos( Theta );
                SampleY = r * FMath::Sin( Theta );
            }

            View.TemporalJitterSequenceLength = TemporalAASamples;
            View.TemporalJitterIndex = TemporalSampleIndex;
            View.TemporalJitterPixels.X = SampleX;
            View.TemporalJitterPixels.Y = SampleY;

            View.ViewMatrices.HackAddTemporalAAProjectionJitter(FVector2D(SampleX * 2.0f / View.ViewRect.Width(), SampleY * -2.0f / View.ViewRect.Height()));
        }

        // Setup a new FPreviousViewInfo from current frame infos.
        FPreviousViewInfo NewPrevViewInfo;
        {
            NewPrevViewInfo.ViewMatrices = View.ViewMatrices;
        }

        // 초기화(Initialize)view state
        if ( ViewState )
        {
            // update previous frame matrices in case world origin was rebased on this frame
            if (!View.OriginOffsetThisFrame.IsZero())
            {
                ViewState->PrevFrameViewInfo.ViewMatrices.ApplyWorldOffset(View.OriginOffsetThisFrame);
            }

            // determine if we are initializing or we should reset the persistent state
            const float DeltaTime = View.Family->CurrentRealTime - ViewState->LastRenderTime;
            const bool bFirstFrameOrTimeWasReset = DeltaTime < -0.0001f || ViewState->LastRenderTime < 0.0001f;
            const bool bIsLargeCameraMovement = IsLargeCameraMovement(
                View,
                ViewState->PrevFrameViewInfo.ViewMatrices.GetViewMatrix(),
                ViewState->PrevFrameViewInfo.ViewMatrices.GetViewOrigin(),
                45.0f, 10000.0f);
            const bool bResetCamera = (bFirstFrameOrTimeWasReset || View.bCameraCut || bIsLargeCameraMovement || View.bForceCameraVisibilityReset);
            
            (......)

            if (bResetCamera)
            {
                View.PrevViewInfo = NewPrevViewInfo;

                // PT: If the motion blur shader is the last shader in the post-processing chain then it is the one that is
                //     adjusting for the viewport offset.  So it is always required and we can't just disable the work the
                //     shader does.  The correct fix would be to disable the effect when we don't need it and to properly mark
                //     the uber-postprocessing effect as the last effect in the chain.

                View.bPrevTransformsReset = true;
            }
            else
            {
                View.PrevViewInfo = ViewState->PrevFrameViewInfo;
            }

            // Replace previous view info of the view state with this frame, clearing out references over render target.
            if (!View.bStatePrevViewInfoIsReadOnly)
            {
                ViewState->PrevFrameViewInfo = NewPrevViewInfo;
            }

            // If the view has a previous view transform, then overwrite the previous view info for the _current_ frame.
            if (View.PreviousViewTransform.IsSet())
            {
                // Note that we must ensure this transform ends up in ViewState->PrevFrameViewInfo else it will be used to calculate the next frame's motion vectors as well
                View.PrevViewInfo.ViewMatrices.UpdateViewMatrix(View.PreviousViewTransform->GetTranslation(), View.PreviousViewTransform->GetRotation().Rotator());
            }

            // detect conditions where we should reset occlusion queries
            if (bFirstFrameOrTimeWasReset || 
                ViewState->LastRenderTime + GEngine->PrimitiveProbablyVisibleTime < View.Family->CurrentRealTime ||
                View.bCameraCut ||
                View.bForceCameraVisibilityReset ||
                IsLargeCameraMovement(
                    View, 
                    ViewState->PrevViewMatrixForOcclusionQuery, 
                    ViewState->PrevViewOriginForOcclusionQuery, 
                    GEngine->CameraRotationThreshold, GEngine->CameraTranslationThreshold))
            {
                View.bIgnoreExistingQueries = true;
                View.bDisableDistanceBasedFadeTransitions = true;
            }

            // Turn on/off round-robin occlusion querying in the ViewState
            static const auto CVarRROCC = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("vr.RoundRobinOcclusion"));
            const bool bEnableRoundRobin = CVarRROCC ? (CVarRROCC->GetValueOnAnyThread() != false) : false;
            if (bEnableRoundRobin != ViewState->IsRoundRobinEnabled())
            {
                ViewState->UpdateRoundRobin(bEnableRoundRobin);
                View.bIgnoreExistingQueries = true;
            }

            ViewState->PrevViewMatrixForOcclusionQuery = View.ViewMatrices.GetViewMatrix();
            ViewState->PrevViewOriginForOcclusionQuery = View.ViewMatrices.GetViewOrigin();
                
            (......)

            // we don't use DeltaTime as it can be 0 (in editor) and is computed by subtracting floats (loses precision over time)
            // Clamp DeltaWorldTime to reasonable values for the purposes of motion blur, things like TimeDilation can make it very small
            // Offline renders always control the timestep for the view and always need the timescales calculated.
            if (!ViewFamily.bWorldIsPaused || View.bIsOfflineRender)
            {
                ViewState->UpdateMotionBlurTimeScale(View);
            }
            

            ViewState->PrevFrameNumber = ViewState->PendingPrevFrameNumber;
            ViewState->PendingPrevFrameNumber = View.Family->FrameNumber;

            // This finishes the update of view state
            ViewState->UpdateLastRenderTime(*View.Family);

            ViewState->UpdateTemporalLODTransition(View);
        }
        else
        {
            // Without a viewstate, we just assume that camera has not moved.
            View.PrevViewInfo = NewPrevViewInfo;
        }
    }

    // 설정(Set)글로벌 지터링 파라미터 및 uniform buffer.
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
    {
        FViewInfo& View = Views[ViewIndex];

        FDitherUniformShaderParameters DitherUniformShaderParameters;
        DitherUniformShaderParameters.LODFactor = View.GetTemporalLODTransition();
        View.DitherFadeOutUniformBuffer = FDitherUniformBufferRef::CreateUniformBufferImmediate(DitherUniformShaderParameters, UniformBuffer_SingleFrame);

        DitherUniformShaderParameters.LODFactor = View.GetTemporalLODTransition() - 1.0f;
        View.DitherFadeInUniformBuffer = FDitherUniformBufferRef::CreateUniformBufferImmediate(DitherUniformShaderParameters, UniformBuffer_SingleFrame);
    }
}

PreVisibilityFrameSetup이 정적 그리드, Groom, SkinCache, 특수 효과, TAA, ViewState 등과 같은 많은 초기화 작업을 수행한 것을 볼 수 있습니다. 그런 다음 계속해서 ComputeViewVisibility를 분석합니다.

cpp
// Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp

void FSceneRenderer::ComputeViewVisibility(FRHICommandListImmediate& RHICmdList, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, FViewVisibleCommandsPerView& ViewCommandsPerView, FGlobalDynamicIndexBuffer& DynamicIndexBuffer, FGlobalDynamicVertexBuffer& DynamicVertexBuffer, FGlobalDynamicReadBuffer& DynamicReadBuffer)
{
    // 할당(Allocate)라이트(Light)정보 리스트 。
    if (Scene->Lights.GetMaxIndex() > 0)
    {
        VisibleLightInfos.AddZeroed(Scene->Lights.GetMaxIndex());
    }

    int32 NumPrimitives = Scene->Primitives.Num();
    float CurrentRealTime = ViewFamily.CurrentRealTime;

    FPrimitiveViewMasks HasDynamicMeshElementsMasks;
    HasDynamicMeshElementsMasks.AddZeroed(NumPrimitives);

    FPrimitiveViewMasks HasViewCustomDataMasks;
    HasViewCustomDataMasks.AddZeroed(NumPrimitives);

    FPrimitiveViewMasks HasDynamicEditorMeshElementsMasks;

    if (GIsEditor)
    {
        HasDynamicEditorMeshElementsMasks.AddZeroed(NumPrimitives);
    }

    const bool bIsInstancedStereo = (Views.Num() > 0) ? (Views[0].IsInstancedStereoPass() || Views[0].bIsMobileMultiViewEnabled) : false;
    UpdateReflectionSceneData(Scene);

    // 갱신(Update)판정/확인 의 정적 .
    {
        Scene->ConditionalMarkStaticMeshElementsForUpdate();

        TArray<FPrimitiveSceneInfo*> UpdatedSceneInfos;
        for (TSet<FPrimitiveSceneInfo*>::TIterator It(Scene->PrimitivesNeedingStaticMeshUpdateWithoutVisibilityCheck); It; ++It)
        {
            FPrimitiveSceneInfo* Primitive = *It;
            if (Primitive->NeedsUpdateStaticMeshes())
            {
                UpdatedSceneInfos.Add(Primitive);
            }
        }
        if (UpdatedSceneInfos.Num() > 0)
        {
            FPrimitiveSceneInfo::UpdateStaticMeshes(RHICmdList, Scene, UpdatedSceneInfos);
        }
        Scene->PrimitivesNeedingStaticMeshUpdateWithoutVisibilityCheck.Reset();
    }

    // 초기화(Initialize)view의 데이터 .
    uint8 ViewBit = 0x1;
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex, ViewBit <<= 1)
    {
        STAT(NumProcessedPrimitives += NumPrimitives);

        FViewInfo& View = Views[ViewIndex];
        FViewCommands& ViewCommands = ViewCommandsPerView[ViewIndex];
        FSceneViewState* ViewState = (FSceneViewState*)View.State;

        // Allocate the view's visibility maps.
        View.PrimitiveVisibilityMap.Init(false,Scene->Primitives.Num());
        // we don't initialized as we overwrite the whole array (in GatherDynamicMeshElements)
        View.DynamicMeshEndIndices.SetNumUninitialized(Scene->Primitives.Num());
        View.PrimitiveDefinitelyUnoccludedMap.Init(false,Scene->Primitives.Num());
        View.PotentiallyFadingPrimitiveMap.Init(false,Scene->Primitives.Num());
        View.PrimitiveFadeUniformBuffers.AddZeroed(Scene->Primitives.Num());
        View.PrimitiveFadeUniformBufferMap.Init(false, Scene->Primitives.Num());
        View.StaticMeshVisibilityMap.Init(false,Scene->StaticMeshes.GetMaxIndex());
        View.StaticMeshFadeOutDitheredLODMap.Init(false,Scene->StaticMeshes.GetMaxIndex());
        View.StaticMeshFadeInDitheredLODMap.Init(false,Scene->StaticMeshes.GetMaxIndex());
        View.StaticMeshBatchVisibility.AddZeroed(Scene->StaticMeshBatchVisibility.GetMaxIndex());
        View.PrimitivesLODMask.Init(FLODMask(), Scene->Primitives.Num());

        View.PrimitivesCustomData.Init(nullptr, Scene->Primitives.Num());

        // We must reserve to prevent realloc otherwise it will cause memory leak if we Execute In Parallel
        const bool WillExecuteInParallel = FApp::ShouldUseThreadingForPerformance() && CVarParallelInitViews.GetValueOnRenderThread() > 0;
        View.PrimitiveCustomDataMemStack.Reserve(WillExecuteInParallel ? FMath::CeilToInt(((float)View.PrimitiveVisibilityMap.Num() / (float)FRelevancePrimSet<int32>::MaxInputPrims)) + 1 : 1);

        View.AllocateCustomDataMemStack();

        View.VisibleLightInfos.Empty(Scene->Lights.GetMaxIndex());

        View.DirtyIndirectLightingCacheBufferPrimitives.Reserve(Scene->Primitives.Num());

        // 생성(Create)라이트(Light)정보 .
        for(int32 LightIndex = 0;LightIndex < Scene->Lights.GetMaxIndex();LightIndex++)
        {
            if( LightIndex+2 < Scene->Lights.GetMaxIndex() )
            {
                if (LightIndex > 2)
                {
                    FLUSH_CACHE_LINE(&View.VisibleLightInfos(LightIndex-2));
                }
            }
            new(View.VisibleLightInfos) FVisibleLightViewInfo();
        }

        View.PrimitiveViewRelevanceMap.Empty(Scene->Primitives.Num());
        View.PrimitiveViewRelevanceMap.AddZeroed(Scene->Primitives.Num());

        const bool bIsParent = ViewState && ViewState->IsViewParent();
        if ( bIsParent )
        {
            ViewState->ParentPrimitives.Empty();
        }

        if (ViewState)
        {
            // 페치/가져오기(Fetch)압축해제/언패킹(Unpacking)이전 프레임(Previous Frame)의 차폐/오클루전 데이터 .
            View.PrecomputedVisibilityData = ViewState->GetPrecomputedVisibilityData(View, Scene);
        }
        else
        {
            View.PrecomputedVisibilityData = NULL;
        }

        if (View.PrecomputedVisibilityData)
        {
            bUsedPrecomputedVisibility = true;
        }

        bool bNeedsFrustumCulling = true;

#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
        if( ViewState )
        {
            //
            if(ViewState->bIsFrozen)
            {
                bNeedsFrustumCulling = false;
                for (FSceneBitArray::FIterator BitIt(View.PrimitiveVisibilityMap); BitIt; ++BitIt)
                {
                    if (ViewState->FrozenPrimitives.Contains(Scene->PrimitiveComponentIds[BitIt.GetIndex()]))
                    {
                        BitIt.GetValue() = true;
                    }
                }
            }
        }
#endif

        // 클리핑/클램핑(Clipping).
        if (bNeedsFrustumCulling)
        {
            // Update HLOD transition/visibility states to allow use during distance culling
            FLODSceneTree& HLODTree = Scene->SceneLODHierarchy;
            if (HLODTree.IsActive())
            {
                QUICK_SCOPE_CYCLE_COUNTER(STAT_ViewVisibilityTime_HLODUpdate);
                HLODTree.UpdateVisibilityStates(View);
            }
            else
            {
                HLODTree.ClearVisibilityState(View);
            }

            int32 NumCulledPrimitivesForView;
            const bool bUseFastIntersect = (View.ViewFrustum.PermutedPlanes.Num() == 8) && CVarUseFastIntersect.GetValueOnRenderThread();
            if (View.CustomVisibilityQuery && View.CustomVisibilityQuery->Prepare())
            {
                if (CVarAlsoUseSphereForFrustumCull.GetValueOnRenderThread())
                {
                    NumCulledPrimitivesForView = bUseFastIntersect ? FrustumCull<true, true, true>(Scene, View) : FrustumCull<true, true, false>(Scene, View);
                }
                else
                {
                    NumCulledPrimitivesForView = bUseFastIntersect ? FrustumCull<true, false, true>(Scene, View) : FrustumCull<true, false, false>(Scene, View);
                }
            }
            else
            {
                if (CVarAlsoUseSphereForFrustumCull.GetValueOnRenderThread())
                {
                    NumCulledPrimitivesForView = bUseFastIntersect ? FrustumCull<false, true, true>(Scene, View) : FrustumCull<false, true, false>(Scene, View);
                }
                else
                {
                    NumCulledPrimitivesForView = bUseFastIntersect ? FrustumCull<false, false, true>(Scene, View) : FrustumCull<false, false, false>(Scene, View);
                }
            }
            STAT(NumCulledPrimitives += NumCulledPrimitivesForView);
            UpdatePrimitiveFading(Scene, View);            
        }

        // 처리(Process).
        if (View.HiddenPrimitives.Num())
        {
            for (FSceneSetBitIterator BitIt(View.PrimitiveVisibilityMap); BitIt; ++BitIt)
            {
                if (View.HiddenPrimitives.Contains(Scene->PrimitiveComponentIds[BitIt.GetIndex()]))
                {
                    View.PrimitiveVisibilityMap.AccessCorrespondingBit(BitIt) = false;
                }
            }
        }

        (......)

        // 처리(Process)정적 .
        if (View.bStaticSceneOnly)
        {
            for (FSceneSetBitIterator BitIt(View.PrimitiveVisibilityMap); BitIt; ++BitIt)
            {
                // Reflection captures should only capture objects that won't move, since reflection captures won't update at runtime
                if (!Scene->Primitives[BitIt.GetIndex()]->Proxy->HasStaticLighting())
                {
                    View.PrimitiveVisibilityMap.AccessCorrespondingBit(BitIt) = false;
                }
            }
        }

        (......)

        // , 이면 오직 차폐/오클루전 컬링 .
        if (!View.Family->EngineShowFlags.Wireframe)
        {
            int32 NumOccludedPrimitivesInView = OcclusionCull(RHICmdList, Scene, View, DynamicVertexBuffer);
            STAT(NumOccludedPrimitives += NumOccludedPrimitivesInView);
        }

        // 처리(Process)판정/확인 의 정적 모델 .
        {
            TArray<FPrimitiveSceneInfo*> AddedSceneInfos;
            for (TConstDualSetBitIterator<SceneRenderingBitArrayAllocator, FDefaultBitArrayAllocator> BitIt(View.PrimitiveVisibilityMap, Scene->PrimitivesNeedingStaticMeshUpdate); BitIt; ++BitIt)
            {
                int32 PrimitiveIndex = BitIt.GetIndex();
                AddedSceneInfos.Add(Scene->Primitives[PrimitiveIndex]);
            }

            if (AddedSceneInfos.Num() > 0)
            {
                FPrimitiveSceneInfo::UpdateStaticMeshes(RHICmdList, Scene, AddedSceneInfos);
            }
        }

        (......)
    }

    (......)

    // view의 동적 . .
    {
        GatherDynamicMeshElements(Views, Scene, ViewFamily, DynamicIndexBuffer, DynamicVertexBuffer, DynamicReadBuffer,
            HasDynamicMeshElementsMasks, HasDynamicEditorMeshElementsMasks, HasViewCustomDataMasks, MeshCollector);
    }

    // 생성(Create)개 view의 MeshPass데이터 .
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
    {
        FViewInfo& View = Views[ViewIndex];
        if (!View.ShouldRenderView())
        {
            continue;
        }

        FViewCommands& ViewCommands = ViewCommandsPerView[ViewIndex];
        SetupMeshPass(View, BasePassDepthStencilAccess, ViewCommands);
    }
}

ComputeViewVisibility의 가장 중요한 기능은 프러스텀 클리핑, 폐색 제거, 동적 그리드 정보 수집, 광원 정보 생성 등을 포함한 기본 요소의 가시성을 처리하는 것입니다. 가시성 사전 계산의 대략적인 프로세스를 계속해 보겠습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\SceneOcclusion.cpp

const uint8* FSceneViewState::GetPrecomputedVisibilityData(FViewInfo& View, const FScene* Scene)
{
    const uint8* PrecomputedVisibilityData = NULL;
    if (Scene->PrecomputedVisibilityHandler && GAllowPrecomputedVisibility && View.Family->EngineShowFlags.PrecomputedVisibility)
    {
        const FPrecomputedVisibilityHandler& Handler = *Scene->PrecomputedVisibilityHandler;
        FViewElementPDI VisibilityCellsPDI(&View, nullptr, nullptr);

        // 드로우/렌더(Draw)을(를) 활용하여 의 차폐/오클루전 컬링 .
        if ((GShowPrecomputedVisibilityCells || View.Family->EngineShowFlags.PrecomputedVisibilityCells) && !GShowRelevantPrecomputedVisibilityCells)
        {
            for (int32 BucketIndex = 0; BucketIndex < Handler.PrecomputedVisibilityCellBuckets.Num(); BucketIndex++)
            {
                for (int32 CellIndex = 0; CellIndex < Handler.PrecomputedVisibilityCellBuckets[BucketIndex].Cells.Num(); CellIndex++)
                {
                    const FPrecomputedVisibilityCell& CurrentCell = Handler.PrecomputedVisibilityCellBuckets[BucketIndex].Cells[CellIndex];
                    // Construct the cell's bounds
                    const FBox CellBounds(CurrentCell.Min, CurrentCell.Min + FVector(Handler.PrecomputedVisibilityCellSizeXY, Handler.PrecomputedVisibilityCellSizeXY, Handler.PrecomputedVisibilityCellSizeZ));
                    if (View.ViewFrustum.IntersectBox(CellBounds.GetCenter(), CellBounds.GetExtent()))
                    {
                        DrawWireBox(&VisibilityCellsPDI, CellBounds, FColor(50, 50, 255), SDPG_World);
                    }
                }
            }
        }

        // 계산/산출(Calculate) 및 , 로써 검색 .
        const float FloatOffsetX = (View.ViewMatrices.GetViewOrigin().X - Handler.PrecomputedVisibilityCellBucketOriginXY.X) / Handler.PrecomputedVisibilityCellSizeXY;
        // FMath::TruncToInt rounds toward 0, we want to always round down
        const int32 BucketIndexX = FMath::Abs((FMath::TruncToInt(FloatOffsetX) - (FloatOffsetX < 0.0f ? 1 : 0)) / Handler.PrecomputedVisibilityCellBucketSizeXY % Handler.PrecomputedVisibilityNumCellBuckets);
        const float FloatOffsetY = (View.ViewMatrices.GetViewOrigin().Y -Handler.PrecomputedVisibilityCellBucketOriginXY.Y) / Handler.PrecomputedVisibilityCellSizeXY;
        const int32 BucketIndexY = FMath::Abs((FMath::TruncToInt(FloatOffsetY) - (FloatOffsetY < 0.0f ? 1 : 0)) / Handler.PrecomputedVisibilityCellBucketSizeXY % Handler.PrecomputedVisibilityNumCellBuckets);
        const int32 PrecomputedVisibilityBucketIndex = BucketIndexY * Handler.PrecomputedVisibilityCellBucketSizeXY + BucketIndexX;

        // 드로우/렌더(Draw)의 바운딩 박스(AABB).
        const FPrecomputedVisibilityBucket& CurrentBucket = Handler.PrecomputedVisibilityCellBuckets[PrecomputedVisibilityBucketIndex];
        for (int32 CellIndex = 0; CellIndex < CurrentBucket.Cells.Num(); CellIndex++)
        {
            const FPrecomputedVisibilityCell& CurrentCell = CurrentBucket.Cells[CellIndex];
            // 생성(Create)cell의 바운딩 박스(AABB).
            const FBox CellBounds(CurrentCell.Min, CurrentCell.Min + FVector(Handler.PrecomputedVisibilityCellSizeXY, Handler.PrecomputedVisibilityCellSizeXY, Handler.PrecomputedVisibilityCellSizeZ));
            // Check if ViewOrigin is inside the current cell
            if (CellBounds.IsInside(View.ViewMatrices.GetViewOrigin()))
            {
                // 검사/감지(Detect)는 을(를) 활용하여 캐싱(Cache)의 데이터 .
                if (CachedVisibilityChunk
                    && CachedVisibilityHandlerId == Scene->PrecomputedVisibilityHandler->GetId()
                    && CachedVisibilityBucketIndex == PrecomputedVisibilityBucketIndex
                    && CachedVisibilityChunkIndex == CurrentCell.ChunkIndex)
                {
                    PrecomputedVisibilityData = &(*CachedVisibilityChunk)[CurrentCell.DataOffset];
                }
                else
                {
                    const FCompressedVisibilityChunk& CompressedChunk = Handler.PrecomputedVisibilityCellBuckets[PrecomputedVisibilityBucketIndex].CellDataChunks[CurrentCell.ChunkIndex];
                    CachedVisibilityBucketIndex = PrecomputedVisibilityBucketIndex;
                    CachedVisibilityChunkIndex = CurrentCell.ChunkIndex;
                    CachedVisibilityHandlerId = Scene->PrecomputedVisibilityHandler->GetId();

                    // 압축해제/언패킹(Unpacking)차폐/오클루전 데이터 .
                    if (CompressedChunk.bCompressed)
                    {
                        // Decompress the needed visibility data chunk
                        DecompressedVisibilityChunk.Reset();
                        DecompressedVisibilityChunk.AddUninitialized(CompressedChunk.UncompressedSize);
                        verify(FCompression::UncompressMemory(
                            NAME_Zlib, 
                            DecompressedVisibilityChunk.GetData(),
                            CompressedChunk.UncompressedSize,
                            CompressedChunk.Data.GetData(),
                            CompressedChunk.Data.Num()));
                        CachedVisibilityChunk = &DecompressedVisibilityChunk;
                    }
                    else
                    {
                        CachedVisibilityChunk = &CompressedChunk.Data;
                    }
                    
                    // Return a pointer to the cell containing ViewOrigin's decompressed visibility data
                    PrecomputedVisibilityData = &(*CachedVisibilityChunk)[CurrentCell.DataOffset];
                }

                (......)
            }
            
            (......)
    }
        
    return PrecomputedVisibilityData;
}

가시성 결정은 실제로 컬링에 사용될 때 각 객체의 경계 상자 크기와 같은 일부 디버깅 정보를 그릴 수 있다는 것이 코드를 통해 알려져 있습니다. 또한 폐색의 효율성을 관찰하기 위해 선별 결과를 동결할 수도 있습니다.

가시성 결정에는 사전 계산, 프러스텀 클리핑, 폐색 제거 등이 포함되므로 폐색 제거에만 많은 지식 포인트(도면, 수집, 데이터 압축 및 압축 해제, 저장 구조, 다중 스레드 전송, 프레임 간 상호 작용, 가시성 결정, HLOD 등)가 포함됩니다. 여기서는 가시성 결정에 대한 대략적인 분석만 제공됩니다. 보다 자세한 메커니즘과 프로세스는 후속 렌더링 최적화 주제에서 심층적으로 분석됩니다.

다음으로 InitViews에서 PostVisibilityFrameSetup을 계속 분석합니다.

cpp
// Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp

void FSceneRenderer::PostVisibilityFrameSetup(FILCUpdatePrimTaskData& OutILCTaskData)
{
    // 처리(Process)정렬 및 조정(Adjust)RT.
    {
        QUICK_SCOPE_CYCLE_COUNTER(STAT_PostVisibilityFrameSetup_Sort);
        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
        {        
            FViewInfo& View = Views[ViewIndex];

            View.MeshDecalBatches.Sort();

            if (View.State)
            {
                ((FSceneViewState*)View.State)->TrimHistoryRenderTargets(Scene);
            }
        }
    }

    (......)

    // 처리(Process)라이트(Light).
    {
        QUICK_SCOPE_CYCLE_COUNTER(STAT_PostVisibilityFrameSetup_Light_Visibility);
    // 순회(Traverse/Iterate)라이트(Light), view의 판정/확인 (개 라이트(Light)의 view그러나 view).
    for(TSparseArray<FLightSceneInfoCompact>::TConstIterator LightIt(Scene->Lights);LightIt;++LightIt)
    {
        const FLightSceneInfoCompact& LightSceneInfoCompact = *LightIt;
        const FLightSceneInfo* LightSceneInfo = LightSceneInfoCompact.LightSceneInfo;

        // 을(를) 활용하여 개 view의 클리핑/클램핑(Clipping)라이트(Light).
        for(int32 ViewIndex = 0;ViewIndex < Views.Num();ViewIndex++)
        {        
            const FLightSceneProxy* Proxy = LightSceneInfo->Proxy;
            FViewInfo& View = Views[ViewIndex];
            FVisibleLightViewInfo& VisibleLightViewInfo = View.VisibleLightInfos[LightIt.GetIndex()];
            // 클리핑/클램핑(Clipping), 오직 로컬 라이트(Light)클리핑/클램핑(Clipping)
            if( Proxy->GetLightType() == LightType_Point ||
                Proxy->GetLightType() == LightType_Spot ||
                Proxy->GetLightType() == LightType_Rect )
            {
                FSphere const& BoundingSphere = Proxy->GetBoundingSphere();
                // 판정/확인 view의 는 및 라이트(Light)바운딩 박스(AABB)교차(Intersection).
                if (View.ViewFrustum.IntersectSphere(BoundingSphere.Center, BoundingSphere.W))
                {
                    // 원근 거리보정(Correct), 컬링 거리포인트 의 라이트(Light).
                    if (View.IsPerspectiveProjection())
                    {
                        FSphere Bounds = Proxy->GetBoundingSphere();
                        float DistanceSquared = (Bounds.Center - View.ViewMatrices.GetViewOrigin()).SizeSquared();
                        float MaxDistSquared = Proxy->GetMaxDrawDistance() * Proxy->GetMaxDrawDistance() * GLightMaxDrawDistanceScale * GLightMaxDrawDistanceScale;
                        // 라이트(Light)의 、뷰(View)의 LOD、라이트(Light)라이트(Light)는 드로우/렌더(Draw),로써 컬링 거리의 라이트(Light)。
                        const bool bDrawLight = (FMath::Square(FMath::Min(0.0002f, GMinScreenRadiusForLights / Bounds.W) * View.LODDistanceFactor) * DistanceSquared < 1.0f)
                                                    && (MaxDistSquared == 0 || DistanceSquared < MaxDistSquared);
                            
                        VisibleLightViewInfo.bInViewFrustum = bDrawLight;
                    }
                    else
                    {
                        VisibleLightViewInfo.bInViewFrustum = true;
                    }
                }
            }
            else
            {
                // .
                VisibleLightViewInfo.bInViewFrustum = true;

                (......)
            }

    (......)
}

요약하자면, PostVisibilityFrameSetup의 주요 작업은 뷰의 프러스텀를 사용하여 광원을 클립하여 시선 외부에 있거나 화면 비율이 작거나 빛의 강도가 없는 광원이 셰이더 계산에 입력되지 않도록 하는 것입니다. 또한 이전 프레임의 데칼 정렬, RT 및 안개 효과 조정, 빔 등도 처리됩니다.

InitViews가 끝나면 각 뷰의 RHI 리소스가 초기화되고 RHICommandList 렌더링 시작 이벤트가 통지됩니다. 먼저 FViewInfo::InitRHIResources를 살펴보겠습니다:

cpp
// Engine\Source\Runtime\Renderer\Private\SceneRendering.cpp

void FViewInfo::InitRHIResources()
{
    FBox VolumeBounds[TVC_MAX];

    // 생성(Create) 및 설정(Set)캐싱(Cache)의 뷰(View)Uniform Buffer.
    CachedViewUniformShaderParameters = MakeUnique<FViewUniformShaderParameters>();

    FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(FRHICommandListExecutor::GetImmediateCommandList());

    SetupUniformBufferParameters(
        SceneContext,
        VolumeBounds,
        TVC_MAX,
        *CachedViewUniformShaderParameters);

    // 생성(Create) 및 설정(Set)뷰(View)의 Uniform Buffer.
    ViewUniformBuffer = TUniformBufferRef<FViewUniformShaderParameters>::CreateUniformBufferImmediate(*CachedViewUniformShaderParameters, UniformBuffer_SingleFrame);

    const int32 TranslucencyLightingVolumeDim = GetTranslucencyLightingVolumeDim();

    // 리셋/초기화(Reset)캐싱(Cache)의 Uniform Buffer.
    FScene* Scene = Family->Scene ? Family->Scene->GetRenderScene() : nullptr;
    if (Scene)
    {
        Scene->UniformBuffers.InvalidateCachedView();
    }

    // 초기화(Initialize)반투명(Translucent)라이팅 파라미터 .
    for (int32 CascadeIndex = 0; CascadeIndex < TVC_MAX; CascadeIndex++)
    {
        TranslucencyLightingVolumeMin[CascadeIndex] = VolumeBounds[CascadeIndex].Min;
        TranslucencyVolumeVoxelSize[CascadeIndex] = (VolumeBounds[CascadeIndex].Max.X - VolumeBounds[CascadeIndex].Min.X) / TranslucencyLightingVolumeDim;
        TranslucencyLightingVolumeSize[CascadeIndex] = VolumeBounds[CascadeIndex].Max - VolumeBounds[CascadeIndex].Min;
    }
}

InitViews 끝에서 OnStartRender를 계속 구문 분석합니다.

cpp
// Engine\Source\Runtime\Renderer\Private\SceneRendering.cpp

void FSceneRenderer::OnStartRender(FRHICommandListImmediate& RHICmdList)
{
    // MRT의 초기화(Initialize).
    FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);

    FVisualizeTexturePresent::OnStartRender(Views[0]);
    CompositionGraph_OnStartFrame();
    SceneContext.bScreenSpaceAOIsValid = false;
    SceneContext.bCustomDepthIsValid = false;

    // ViewState초기화(Initialize).
    for (FViewInfo& View : Views)
    {
        if (View.ViewState)
        {
            View.ViewState->OnStartRender(View, ViewFamily);
        }
    }
}

// Engine\Source\Runtime\Renderer\Private\ScenePrivate.h

void FSceneViewState::OnStartRender(FViewInfo& View, FSceneViewFamily& ViewFamily)
{
    // 초기화(Initialize) 및 설정(Set)라이팅 .
    if(!(View.FinalPostProcessSettings.IndirectLightingColor * View.FinalPostProcessSettings.IndirectLightingIntensity).IsAlmostBlack())
    {
        SetupLightPropagationVolume(View, ViewFamily);
    }
    // 할당(Allocate)단계 차폐/오클루전 컬링 (지원 차폐/오클루전 컬링 의 )
    ConditionallyAllocateSceneSoftwareOcclusion(View.GetFeatureLevel());
}

코드 분석을 주의 깊게 읽지 않는 어린이가 InitViews 프로세스를 더 잘 이해할 수 있도록 주요 단계와 프로세스를 간략하게 요약하면 다음과 같습니다.

  • PreVisibilityFrameSetup: 가시성 결정 전처리 단계, 주로 정적 그리드, Groom, SkinCache, 특수 효과, TAA, ViewState 등을 초기화 및 설정합니다.

  • 특수 효과 시스템(FXSystem)을 초기화합니다.

  • ComputeViewVisibility: 뷰 관련 가시성 계산, 프러스텀 클리핑, 오클루전 컬링 수행, 동적 그리드 정보 수집, 광원 정보 생성 등

FPrimitiveSceneInfo::UpdateStaticMeshes: 스태틱 메시 데이터를 업데이트합니다.

  • ViewState::GetPrecomputedVisibilityData: 미리 계산된 가시성 데이터를 가져옵니다.

  • FrustumCull: 프러스텀 클리핑.

  • ComputeAndMarkRelevanceForViewParallel: 뷰에서 병렬로 처리되는 관련 데이터를 계산하고 표시합니다.

  • GatherDynamicMeshElements: 이전 기사에서 분석한 뷰의 동적 표시 요소를 수집합니다.

  • SetupMeshPass: 그리드 패스 데이터를 설정하고 FMeshBatch를 이전 기사에서 분석한 FMeshDrawCommand로 변환합니다.

  • CreateIndirectCapsuleShadows: 캡슐 그림자를 만듭니다.

  • UpdateSkyIrradianceGpuBuffer: 하늘 몸체 환경 조명의 GPU 데이터를 업데이트합니다.

  • InitSkyAtmosphereForViews: 대기 효과를 초기화합니다.

  • PostVisibilityFrameSetup: 가시성 결정의 포스트 프로세스 단계에서 뷰의 프러스텀는 광원을 자르고 데칼 정렬을 처리하며 이전 프레임의 RT, 안개 효과, 빔 등을 조정하는 데 사용됩니다.

  • View.InitRHIResources: 뷰의 일부 RHI 리소스를 초기화합니다.

  • SetupVolumetricFog: 체적 안개를 초기화하고 설정합니다.

  • OnStartRender: 뷰 관련 데이터 및 리소스를 초기화하기 위해 렌더링이 시작되었음을 RHI에 알립니다.

위에서 굵은 글씨로 표시된 단계는 제가 더 주목해야 할 단계라고 생각합니다.

4.3.5 프리패스

Advance Depth Pass, Depth Only Pass 및 Early-Z Pass라고도 알려진 PrePass는 불투명한 물체의 깊이를 렌더링하는 데 사용됩니다. 이 패스는 깊이만 기록하고 색상은 기록하지 않습니다. 쓰기 깊이에는 플랫폼과 기능 수준에 따라 비활성화, 폐색만, 전체 깊이의 세 가지 모드가 있습니다.

PrePass는 DBuffer에 의해 시작되거나 Forward Shading에 의해 트리거될 수 있습니다. 일반적으로 하드웨어의 Early-Z 기술을 활성화하기 위해 Hierarchical-Z를 설정하는 데 사용됩니다. 또한 베이스 패스의 렌더링 효율성을 향상시키기 위해 폐색 제거에 사용할 수도 있습니다.

다음으로 주제를 입력하고 해당 코드를 분석합니다. 먼저 FDeferredShadingSceneRenderer::Render의 PrePass를 살펴보세요.

cpp
void FDeferredShadingSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
    Scene->UpdateAllPrimitiveSceneInfos(RHICmdList, true);
    
    (......)
    
    InitViews(...);
    
    (......)
    
    UpdateGPUScene(RHICmdList, *Scene);
    
    (......)
    
    // 판정/확인 는 PrePass.
    const bool bNeedsPrePass = NeedsPrePass(this);
    
    // The Z-prepass
    
    (......)
    
    if (bNeedsPrePass)
    {
        // 드로우/렌더(Draw)씬 뎁스(SceneDepth), 뎁스 버퍼(Depth Buffer) 및 레벨 단계 Z버퍼 (HiZ).
        bDepthWasCleared = RenderPrePass(RHICmdList, AfterTasksAreStarted);
    }

    (......)

    // Z-Prepass End
}

위 코드에서 볼 수 있듯이 Prepass는 UpdateGPUScene 이후에 실행되며 반드시 실행되는 것은 아닙니다. 이는 다음 조건에 따라 결정됩니다.

cpp
static FORCEINLINE bool NeedsPrePass(const FDeferredShadingSceneRenderer* Renderer)
{
    return (RHIHasTiledGPU(Renderer->ViewFamily.GetShaderPlatform()) == false) && 
        (Renderer->EarlyZPassMode != DDM_None || Renderer->bEarlyZPassMovable != 0);
}

PrePass를 활성화하려면 다음 두 가지 조건을 충족해야 합니다.

  • 하드웨어가 아닌 타일형 GPU. 최신 모바일 GPU는 일반적으로 Tiled와 함께 제공되며 TBDR 아키텍처를 갖습니다. Early-Z는 GPU 레이어에서 이루어졌으므로 명시적으로 그릴 필요가 없습니다.

  • 유효한 EarlyZPassMode가 지정되었거나 렌더러의 bEarlyZPassMovable이 0이 아닙니다.

그런 다음 RenderPrePass를 입력하여 기본 논리를 분석합니다.

cpp
// Engine\Source\Runtime\Renderer\Private\DepthRendering.cpp

bool FDeferredShadingSceneRenderer::RenderPrePass(FRHICommandListImmediate& RHICmdList, TFunctionRef<void()> AfterTasksAreStarted)
{
    bool bDepthWasCleared = false;

    (......)

    bool bDidPrePre = false;
    FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);

    bool bParallel = GRHICommandList.UseParallelAlgorithms() && CVarParallelPrePass.GetValueOnRenderThread();

    // 병렬 .
    if (!bParallel)
    {
        AfterTasksAreStarted();
        bDepthWasCleared = PreRenderPrePass(RHICmdList);
        bDidPrePre = true;
        SceneContext.BeginRenderingPrePass(RHICmdList, false);
    }
    else // 병렬
    {
        // 할당(Allocate)뎁스 버퍼(Depth Buffer).
        SceneContext.GetSceneDepthSurface(); 
    }

    // Draw a depth pass to avoid overdraw in the other passes.
    if(EarlyZPassMode != DDM_None)
    {
        const bool bWaitForTasks = bParallel && (CVarRHICmdFlushRenderThreadTasksPrePass.GetValueOnRenderThread() > 0 || CVarRHICmdFlushRenderThreadTasks.GetValueOnRenderThread() > 0);
        FScopedCommandListWaitForTasks Flusher(bWaitForTasks, RHICmdList);

        // 개 view드로우/렌더(Draw)뎁스 버퍼(Depth Buffer).
        for(int32 ViewIndex = 0;ViewIndex < Views.Num();ViewIndex++)
        {
            const FViewInfo& View = Views[ViewIndex];

            // 생성(Create) 및 설정(Set)Uniform Buffer.
            TUniformBufferRef<FSceneTexturesUniformParameters> PassUniformBuffer;
            CreateDepthPassUniformBuffer(RHICmdList, View, PassUniformBuffer);

            // 처리(Process)렌더링(Render)상태 .
            FMeshPassProcessorRenderState DrawRenderState(View, PassUniformBuffer);

            SetupDepthPassState(DrawRenderState);

            if (View.ShouldRenderView())
            {
                Scene->UniformBuffers.UpdateViewUniformBuffer(View);

                // 병렬
                if (bParallel)
                {
                    check(RHICmdList.IsOutsideRenderPass());
                    bDepthWasCleared = RenderPrePassViewParallel(View, RHICmdList, DrawRenderState, AfterTasksAreStarted, !bDidPrePre) || bDepthWasCleared;
                    bDidPrePre = true;
                }
                else
                {
                    RenderPrePassView(RHICmdList, View, DrawRenderState);
                }
            }

            // Parallel rendering has self contained renderpasses so we need a new one for editor primitives.
            if (bParallel)
            {
                SceneContext.BeginRenderingPrePass(RHICmdList, false);
            }
            RenderPrePassEditorPrimitives(RHICmdList, View, DrawRenderState, EarlyZPassMode, true);
            if (bParallel)
            {
                RHICmdList.EndRenderPass();
            }
        }
    }
    
    (......)

    if (bParallel)
    {
        // In parallel mode there will be no renderpass here. Need to restart.
        SceneContext.BeginRenderingPrePass(RHICmdList, false);
    }

    (......)
    
    SceneContext.FinishRenderingPrePass(RHICmdList);

    return bDepthWasCleared;
}

PrePass의 드로잉 프로세스는 이전 기사에서 분석한 FMeshProcessor 및 Pass 드로잉과 유사하므로 여기서는 분석을 반복하지 않습니다. 하지만 여기서는 PrePass의 렌더링 상태에 집중할 수 있습니다.

cpp
void SetupDepthPassState(FMeshPassProcessorRenderState& DrawRenderState)
{
    // 기록/쓰기(Write)컬러 , 활성화(Enable)뎁스 및 기록/쓰기(Write), 뎁스 비교 함수 는 또는 .
    DrawRenderState.SetBlendState(TStaticBlendState<CW_NONE>::GetRHI());
    DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<true, CF_DepthNearOrEqual>::GetRHI());
}

또한, 심도를 그릴 때 색을 쓸 필요가 없기 때문에 물체를 렌더링할 때 사용하는 재료는 물체 자체의 재료가 아닌 단순한 재료여야 합니다. 추측을 검증하려면 FDepthPassMeshProcessor를 입력하여 Depth Pass에서 사용되는 재질을 알아보세요.

cpp
// Engine\Source\Runtime\Renderer\Private\DepthRendering.cpp

void FDepthPassMeshProcessor::AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId)
{
    (......)

    if (bDraw)
    {
        (......)
        
        // 페치/가져오기(Fetch)Surface머티리얼(Material)의 기본값(Default)머티리얼(Material), 로써 뎁스 Pass의 렌더링(Render)머티리얼(Material).
        const FMaterialRenderProxy& DefaultProxy = *UMaterial::GetDefaultMaterial(MD_Surface)->GetRenderProxy();
        const FMaterial& DefaultMaterial = *DefaultProxy.GetMaterial(FeatureLevel);
        Process<true>(MeshBatch, BatchElementMask, StaticMeshId, BlendMode, PrimitiveSceneProxy, DefaultProxy, DefaultMaterial, MeshFillMode, MeshCullMode);
        
        (......)
    }
}

위의 키 코드에 주의하세요. Depth Pass에 사용된 재질은 UMaterial::GetDefaultMaterial(MD_Surface)에서 가져온 것을 볼 수 있습니다. 계속해서 추적하세요.

cpp
// Engine\Source\Runtime\Engine\Private\Materials\Material.cpp

UMaterial* UMaterial::GetDefaultMaterial(EMaterialDomain Domain)
{
    InitDefaultMaterials();

    UMaterial* Default = GDefaultMaterials[Domain];
    return Default;
}

void UMaterialInterface::InitDefaultMaterials()
{
    static bool bInitialized = false;
    if (!bInitialized)
    {
        (......)

        for (int32 Domain = 0; Domain < MD_MAX; ++Domain)
        {
            if (GDefaultMaterials[Domain] == nullptr)
            {
                FString ResolvedPath = ResolveIniObjectsReference(GDefaultMaterialNames[Domain]);

                GDefaultMaterials[Domain] = FindObject<UMaterial>(nullptr, *ResolvedPath);
                if (GDefaultMaterials[Domain] == nullptr)
                {
                    GDefaultMaterials[Domain] = LoadObject<UMaterial>(nullptr, *ResolvedPath, nullptr, LOAD_DisableDependencyPreloading, nullptr);
                }
                if (GDefaultMaterials[Domain])
                {
                    GDefaultMaterials[Domain]->AddToRoot();
                }
            }
        }
        
        (......)
    }
}

위에서 볼 수 있듯이 머티리얼 시스템의 기본 머티리얼은 GDefaultMaterialNames에 의해 지정됩니다. 선언으로 이동합니다.

cpp
static const TCHAR* GDefaultMaterialNames[MD_MAX] =
{
    // Surface
    TEXT("engine-ini:/Script/Engine.Engine.DefaultMaterialName"),
    // Deferred Decal
    TEXT("engine-ini:/Script/Engine.Engine.DefaultDeferredDecalMaterialName"),
    // Light Function
    TEXT("engine-ini:/Script/Engine.Engine.DefaultLightFunctionMaterialName"),
    // Volume
    //@todo - get a real MD_Volume default material
    TEXT("engine-ini:/Script/Engine.Engine.DefaultMaterialName"),
    // Post Process
    TEXT("engine-ini:/Script/Engine.Engine.DefaultPostProcessMaterialName"),
    // User Interface 
    TEXT("engine-ini:/Script/Engine.Engine.DefaultMaterialName"),
    // Virtual Texture
    TEXT("engine-ini:/Script/Engine.Engine.DefaultMaterialName"),
};

이는engine.ini 구성 파일에서 지정할 수 있는 것으로 나타났습니다. 작성자는 게을러서 VS 중단점 디버깅을 통해 직접 Depth Pass의 최종 자료 경로와 이름을 얻었습니다.

cpp
ResolvedPath = L"/Engine/EngineMaterials/WorldGridMaterial.WorldGridMaterial"

UE 에디터를 통해 WorldGridMaterial을 찾아 엽니다:

UE에 대해 잘 아시는 분들은 놀라실 겁니다. 자료가 잘못되었을 때의 사진 아닌가요? 네, Depth Pass는 이 소재를 사용하고 있는데 색상이 적혀있지 않아 누구도 그 존재를 알아차리지 못합니다.

WorldGridMaterial에서 사용하는 셰이딩 모델은 Default Lit이며 머티리얼에 중복 노드도 있다는 점은 언급할 가치가 있습니다. 궁극적인 최적화를 원할 경우 구성 파일에서 이 머티리얼을 변경하고, 중복 머티리얼 노드를 삭제하고, 바람직하게는 Unlit 모드로 변경하여 셰이더 지침을 최소화하고 렌더링 효율성을 높이는 것이 좋습니다.

깊이 패스를 그릴 때 깊이 그리기 모드를 지정할 수 있다는 점도 언급할 가치가 있습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\DepthRendering.h

enum EDepthDrawingMode
{
    // 드로우/렌더(Draw)뎁스
    DDM_None            = 0,
    // 오직 드로우/렌더(Draw)Opaque머티리얼(Material)(Masked머티리얼(Material))
    DDM_NonMaskedOnly    = 1,
    // Opaque 및 Masked머티리얼(Material), 그러나 비활성화(Disable)bUseAsOccluder의 .
    DDM_AllOccluders    = 2,
    // 반투명(Translucent), 드로우/렌더(Draw), 개 픽셀(Pixel)매칭 Base Pass의 뎁스 .
    DDM_AllOpaque        = 3,
    // Masked.
    DDM_MaskedOnly = 4,
};

구체적으로 깊이 그리기 모드를 결정하는 방법은 다음 인터페이스에 의해 결정됩니다.

cpp
// Engine\Source\Runtime\Renderer\Private\RendererScene.cpp

void FScene::UpdateEarlyZPassMode()
{
    DefaultBasePassDepthStencilAccess = FExclusiveDepthStencil::DepthWrite_StencilWrite;
    EarlyZPassMode = DDM_NonMaskedOnly; // 기본값(Default)오직 드로우/렌더(Draw)Opaque머티리얼(Material).
    bEarlyZPassMovable = false;

    // 레이턴시 렌더링(Render)파이프라인 의 뎁스
    if (GetShadingPath(GetFeatureLevel()) == EShadingPath::Deferred)
    {
        // 에 의해 , 에 의해 설정(Set) 내에서 .
        {
            const int32 CVarValue = CVarEarlyZPass.GetValueOnAnyThread();

                switch (CVarValue)
                {
                case 0: EarlyZPassMode = DDM_None; break;
                case 1: EarlyZPassMode = DDM_NonMaskedOnly; break;
                case 2: EarlyZPassMode = DDM_AllOccluders; break;
                case 3: break;    // Note: 3 indicates "default behavior" and does not specify an override
                }
        }

        const EShaderPlatform ShaderPlatform = GetFeatureLevelShaderPlatform(FeatureLevel);
        if (ShouldForceFullDepthPass(ShaderPlatform))
        {
            // DBuffer 및 스텐실 LOD지터링 .
            EarlyZPassMode = DDM_AllOpaque;
            bEarlyZPassMovable = true;
        }

        if (EarlyZPassMode == DDM_AllOpaque
            && CVarBasePassWriteDepthEvenWithFullPrepass.GetValueOnAnyThread() == 0)
        {
            DefaultBasePassDepthStencilAccess = FExclusiveDepthStencil::DepthRead_StencilWrite;
        }
    }
    
    (......)
}

4.3.6 베이스패스

UE의 BasePass는 디퍼드 렌더링의 기하학 채널로, 노멀, 깊이, 색상, AO, 러프니스, 메탈릭 등을 포함한 불투명 객체의 기하학적 정보를 렌더링하는 데 사용됩니다. 이 기하학적 정보는 여러 GBuffer에 기록됩니다.

FDeferredShadingSceneRenderer::Render의 BasePass 입구와 그에 인접한 중요한 인터페이스는 다음과 같습니다:

cpp
void FDeferredShadingSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
    (......)
    
    // 드로우/렌더(Draw)씬 뎁스(SceneDepth).
    bDepthWasCleared = RenderPrePass(RHICmdList, AfterTasksAreStarted);

    (......)
    
    // 뎁스 텍스처(Depth Texture)(오직 활성화(Enable)MSAA실행(Execute)).
    AddResolveSceneDepthPass(GraphBuilder, Views, SceneDepthTexture);
    
    // 라이트(Light), 을(를) 활용하여 Cluster라이팅 계산/산출(Calculate) 내에서 컬링 라이트(Light).(의 렌더링(Render)최적화 내에서 )
    FSortedLightSetSceneInfo SortedLightSet;
    {
        GatherLightsAndComputeLightGrid(GraphBuilder, bComputeLightGrid, SortedLightSet);
    }
    
    (......)
    
    // 할당(Allocate)의 GBuffer, 을(를) 활용하여 저장(Store)Base Pass의 정보 .
    {
        SceneContext.PreallocGBufferTargets();
        SceneContext.AllocGBufferTargets(RHICmdList);
    }
    
    (......)
    
    // 렌더링(Render)Base Pass.
    RenderBasePass(RHICmdList, BasePassDepthStencilAccess, ForwardScreenSpaceShadowMask.GetReference(), bDoParallelBasePass, bRenderLightmapDensity);

    (......)
}

아래에서 RenderBasePass에 직접 액세스하세요.

cpp
bool FDeferredShadingSceneRenderer::RenderBasePass(FRHICommandListImmediate& RHICmdList, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, IPooledRenderTarget* ForwardScreenSpaceShadowMask, bool bParallelBasePass, bool bRenderLightmapDensity)
{
    (......)
    
    {
        FExclusiveDepthStencil::Type BasePassDepthStencilAccess_NoDepthWrite = FExclusiveDepthStencil::Type(BasePassDepthStencilAccess & ~FExclusiveDepthStencil::DepthWrite);
        // 병렬
        if (bParallelBasePass)
        {
            FScopedCommandListWaitForTasks Flusher(CVarRHICmdFlushRenderThreadTasksBasePass.GetValueOnRenderThread() > 0 || CVarRHICmdFlushRenderThreadTasks.GetValueOnRenderThread() > 0, RHICmdList);
            // 순회(Traverse/Iterate)view렌더링(Render)Base Pass
            for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
            {
                FViewInfo& View = Views[ViewIndex];

                // Uniform Buffer
                TUniformBufferRef<FOpaqueBasePassUniformParameters> BasePassUniformBuffer;
                CreateOpaqueBasePassUniformBuffer(RHICmdList, View, ForwardScreenSpaceShadowMask, nullptr, nullptr, nullptr, BasePassUniformBuffer);

                // Render State
                FMeshPassProcessorRenderState DrawRenderState(View, BasePassUniformBuffer);

                SetupBasePassState(BasePassDepthStencilAccess, ViewFamily.EngineShowFlags.ShaderComplexity, DrawRenderState);

                const bool bShouldRenderView = View.ShouldRenderView();
                if (bShouldRenderView)
                {
                    Scene->UniformBuffers.UpdateViewUniformBuffer(View);

                    // 실행(Execute)렌더링(Render).
                    RenderBasePassViewParallel(View, RHICmdList, BasePassDepthStencilAccess, DrawRenderState);
                }
                
                FSceneRenderTargets::Get(RHICmdList).BeginRenderingGBuffer(RHICmdList, ERenderTargetLoadAction::ELoad, ERenderTargetLoadAction::ELoad, BasePassDepthStencilAccess, this->ViewFamily.EngineShowFlags.ShaderComplexity);
                RenderEditorPrimitives(RHICmdList, View, BasePassDepthStencilAccess, DrawRenderState, bDirty);
                RHICmdList.EndRenderPass();

                (......)
            }

            bDirty = true; // assume dirty since we are not going to wait
        }
        else // 병렬
        {
            (......)
        }    
    }

    (......)
}

Base Pass의 렌더링 로직은 Pre Pass의 렌더링 로직과 매우 유사하므로 여기서는 자세히 설명하지 않겠습니다. 다음으로 Base Pass를 렌더링할 때 사용되는 렌더링 상태와 재료에 중점을 둡니다. 렌더링 상태는 다음과 같습니다.

cpp
void SetupBasePassState(FExclusiveDepthStencil::Type BasePassDepthStencilAccess, const bool bShaderComplexity, FMeshPassProcessorRenderState& DrawRenderState)
{
    DrawRenderState.SetDepthStencilAccess(BasePassDepthStencilAccess);

    (......)
    
    {
        // GBuffer활성화(Enable)블렌딩(Blending).
        static const auto CVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.BasePassOutputsVelocityDebug"));
        if (CVar && CVar->GetValueOnRenderThread() == 2)
        {
            DrawRenderState.SetBlendState(TStaticBlendStateWriteMask<CW_RGBA, CW_RGBA, CW_RGBA, CW_RGBA, CW_RGBA, CW_RGBA, CW_NONE>::GetRHI());
        }
        else
        {
            DrawRenderState.SetBlendState(TStaticBlendStateWriteMask<CW_RGBA, CW_RGBA, CW_RGBA, CW_RGBA>::GetRHI());
        }

        // 활성화(Enable)뎁스 기록/쓰기(Write) 및 , 비교 함수 로 NearOrEqual.
        if (DrawRenderState.GetDepthStencilAccess() & FExclusiveDepthStencil::DepthWrite)
        {
            DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<true, CF_DepthNearOrEqual>::GetRHI());
        }
        else
        {
            DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<false, CF_DepthNearOrEqual>::GetRHI());
        }
    }
}

그런 다음 FBasePassMeshProcessor를 입력하여 베이스 패스에 사용되는 머티리얼을 확인하세요.

cpp
void FBasePassMeshProcessor::AddMeshBatch(const FMeshBatch& RESTRICT MeshBatch, uint64 BatchElementMask, const FPrimitiveSceneProxy* RESTRICT PrimitiveSceneProxy, int32 StaticMeshId)
{
    if (MeshBatch.bUseForMaterial)
    {
        const FMaterialRenderProxy* FallbackMaterialRenderProxyPtr = nullptr;
        const FMaterial& Material = MeshBatch.MaterialRenderProxy->GetMaterialWithFallback(FeatureLevel, FallbackMaterialRenderProxyPtr);
        
    (.....)
}

Base Pass는 일반적으로 그리드 자체의 재질인 FMeshBatch에서 수집한 재질을 사용하는 것을 볼 수 있습니다. 유일한 차이점은 Unlit 모드와 유사하게 셰이더에서 조명 계산이 활성화되지 않는다는 것입니다. BasePass의 도면 중첩 관계를 요약하면 의사 코드는 다음과 같습니다.

cpp
foreach(scene in scenes)
{
    foreach(view in views)
    {
        foreach(mesh in meshes)
        {
            DrawMesh(...) // 회 호출(Call)렌더링(Render)실행(Execute)회 BasePassVertexShader 및 BasePassPixelShader의 코드 .
        }
    }
}

BasePassVertexShader 및 BasePassPixelShader는 특정 셰이더 로직으로, 이 문서에서는 다루지 않으며 다음 문서에서 자세히 설명합니다.

4.3.7 라이팅패스

UE의 LightingPass는 이전 장에서 언급한 조명 채널입니다. 이 단계에서는 그림자를 켜는 광원의 그림자 맵을 계산하고, 화면 공간 픽셀에 대한 각 조명의 기여도를 계산하여 장면 색상에 누적합니다. 또한 반투명 조명 볼륨에 대한 광원의 기여도도 계산됩니다.

FDeferredShadingSceneRenderer::Render의 LightingPass 입구와 인접한 중요한 인터페이스는 다음과 같습니다:

cpp
void FDeferredShadingSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
    (......)
    
    // 렌더링(Render)Base Pass.
    RenderBasePass(RHICmdList, BasePassDepthStencilAccess, ForwardScreenSpaceShadowMask.GetReference(), bDoParallelBasePass, bRenderLightmapDensity);

    (......)
    
    SceneContext.FinishGBufferPassAndResolve(RHICmdList, BasePassDepthStencilAccess);
    
    (......)
    
    // 렌더링(Render)레이턴시 파이프라인 의 라이팅 .
    if (bRenderDeferredLighting)
    {
        // 렌더링(Render)디퓨즈(Diffuse) 및 AO.
        RenderDiffuseIndirectAndAmbientOcclusion(RHICmdList);

        // 렌더링(Render)그림자(Shadow). 수정 로부터 Base Pass출력의 추가(Add)라이팅 의 씬 컬러(SceneColor).
        RenderIndirectCapsuleShadows(
            RHICmdList, 
            SceneContext.GetSceneColorSurface(), 
            SceneContext.bScreenSpaceAOIsValid ? SceneContext.ScreenSpaceAO->GetRenderTargetItem().TargetableTexture : NULL);

        TRefCountPtr<IPooledRenderTarget> DynamicBentNormalAO;
        // 렌더링(Render)거리AO.
        RenderDFAOAsIndirectShadowing(RHICmdList, SceneContext.SceneVelocity, DynamicBentNormalAO);

        // 라이팅 반투명(Translucent)라이팅 .
        if ((GbEnableAsyncComputeTranslucencyLightingVolumeClear && GSupportsEfficientAsyncCompute) == false)
        {
            for(int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
            {
                FViewInfo& View = Views[ViewIndex];
                ClearTranslucentVolumeLighting(RHICmdList, ViewIndex);
            }
        }

        // 렌더링(Render)라이트(Light).
        {
            RenderLights(RHICmdList, SortedLightSet, HairDatas);
        }
        
        // 추가(Add)의 반투명(Translucent)라이팅 .
        for(int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
        {
            FViewInfo& View = Views[ViewIndex];
            InjectAmbientCubemapTranslucentVolumeLighting(RHICmdList, Views[ViewIndex], ViewIndex);
        }

        // 필터링 반투명(Translucent)라이팅 .
        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
        {
            FViewInfo& View = Views[ViewIndex];

            FilterTranslucentVolumeLighting(RHICmdList, View, ViewIndex);
        }

        // 라이팅 스테이지 , LPV(Light Propagation Volumes).
        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
        {
            FViewInfo& View = Views[ViewIndex]; 

            if(IsLpvIndirectPassRequired(View))
            {
                GCompositionLighting.ProcessLpvIndirect(RHICmdList, View);
            }
        }

        // 렌더링(Render)스카이라이트(Skylight)디퓨즈(Diffuse) 및 오직 을(를) 활용하여 반투명(Translucent)의 반사(Reflection).
        RenderDeferredReflectionsAndSkyLighting(RHICmdList, DynamicBentNormalAO, SceneContext.SceneVelocity, HairDatas);

        DynamicBentNormalAO = NULL;

        // 씬 컬러(SceneColor), 이므로 의 SSS씬 컬러(SceneColor)의 RT로 SRV.
        ResolveSceneColor(RHICmdList);

        // 계산/산출(Calculate)SSS.
        ComputeSubsurfaceShim(RHICmdList, Views);

        (......)
    }
    
    (......)
}

Lighting Pass를 담당하는 렌더링 로직은 간접 그림자, 간접 AO, 투명 볼륨 조명, 광원 계산, LPV, 스카이라이트, SSS 등을 포함하여 많고 복잡합니다. 그러나 조명 계산의 핵심 로직은 RenderLights에 있습니다. 아래에서는 이에 대해 중점적으로 다루며, 이 글에서는 다른 부분을 다루지 않습니다. RenderLights의 코드는 다음과 같습니다:

cpp
// Engine\Source\Runtime\Renderer\Private\LightRendering.cpp

void FDeferredShadingSceneRenderer::RenderLights(FRHICommandListImmediate& RHICmdList, FSortedLightSetSceneInfo &SortedLightSet, const FHairStrandsDatas* HairDatas)
{
    (......)
    
    bool bStencilBufferDirty = false;    // The stencil buffer should've been cleared to 0 already

    const FSimpleLightArray &SimpleLights = SortedLightSet.SimpleLights;
    const TArray<FSortedLightSceneInfo, SceneRenderingAllocator> &SortedLights = SortedLightSet.SortedLights;
    // 그림자(Shadow)의 라이트(Light).
    const int32 AttenuationLightStart = SortedLightSet.AttenuationLightStart;
    const int32 SimpleLightsEnd = SortedLightSet.SimpleLightsEnd;

    // 라이팅
    {
        SCOPED_DRAW_EVENT(RHICmdList, DirectLighting);

        FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);

        (......)

        // 그림자(Shadow)라이팅
        if(ViewFamily.EngineShowFlags.DirectLighting)
        {
            SCOPED_DRAW_EVENT(RHICmdList, NonShadowedLights);

            // 그림자(Shadow)의 라이트(Light).
            int32 StandardDeferredStart = SortedLightSet.SimpleLightsEnd;
            bool bRenderSimpleLightsStandardDeferred = SortedLightSet.SimpleLights.InstanceData.Num() > 0;
            
            // 레이턴시 라이팅 .
            if (ShouldUseClusteredDeferredShading() && AreClusteredLightsInLightGrid())
            {
                StandardDeferredStart = SortedLightSet.ClusteredSupportedEnd;
                bRenderSimpleLightsStandardDeferred = false;
                // 추가(Add)레이턴시 렌더링(Render)Pass.
                AddClusteredDeferredShadingPass(RHICmdList, SortedLightSet);
            }
            // 타일드(Tiled)레이턴시 라이팅 .
            else if (CanUseTiledDeferred())
            {
                (......)

                if (ShouldUseTiledDeferred(SortedLightSet.TiledSupportedEnd) && !bAnyViewIsStereo)
                {
                    StandardDeferredStart = SortedLightSet.TiledSupportedEnd;
                    bRenderSimpleLightsStandardDeferred = false;
                    // 렌더링(Render)타일드(Tiled)레이턴시 라이팅 .
                    RenderTiledDeferredLighting(RHICmdList, SortedLights, SortedLightSet.SimpleLightsEnd, SortedLightSet.TiledSupportedEnd, SimpleLights);
                }
            }

            // 라이팅 .
            if (bRenderSimpleLightsStandardDeferred)
            {
                SceneContext.BeginRenderingSceneColor(RHICmdList, ESimpleRenderTargetMode::EExistingColorAndDepth, FExclusiveDepthStencil::DepthRead_StencilWrite);
                // 렌더링(Render)라이팅 .
                RenderSimpleLightsStandardDeferred(RHICmdList, SortedLightSet.SimpleLights);
                SceneContext.FinishRenderingSceneColor(RHICmdList);
            }

            // 레이턴시 라이팅 .
            if (!bUseHairLighting)
            {
                SCOPED_DRAW_EVENT(RHICmdList, StandardDeferredLighting);

                SceneContext.BeginRenderingSceneColor(RHICmdList, ESimpleRenderTargetMode::EExistingColorAndDepth, FExclusiveDepthStencil::DepthRead_StencilWrite, true);

                for (int32 LightIndex = StandardDeferredStart; LightIndex < AttenuationLightStart; LightIndex++)
                {
                    const FSortedLightSceneInfo& SortedLightInfo = SortedLights[LightIndex];
                    const FLightSceneInfo* const LightSceneInfo = SortedLightInfo.LightSceneInfo;

                    // 렌더링(Render)그림자(Shadow)라이팅 .
                    RenderLight(RHICmdList, LightSceneInfo, nullptr, nullptr, false, false);
                }

                SceneContext.FinishRenderingSceneColor(RHICmdList);
            }
            
            (......)
        }

        (......)

        // 그림자(Shadow)의 라이팅
        {
            SCOPED_DRAW_EVENT(RHICmdList, ShadowedLights);

            const int32 DenoiserMode = CVarShadowUseDenoiser.GetValueOnRenderThread();

            const IScreenSpaceDenoiser* DefaultDenoiser = IScreenSpaceDenoiser::GetDefaultDenoiser();
            const IScreenSpaceDenoiser* DenoiserToUse = DenoiserMode == 1 ? DefaultDenoiser : GScreenSpaceDenoiser;

            TArray<TRefCountPtr<IPooledRenderTarget>> PreprocessedShadowMaskTextures;
            TArray<TRefCountPtr<IPooledRenderTarget>> PreprocessedShadowMaskSubPixelTextures;

            const int32 MaxDenoisingBatchSize = FMath::Clamp(CVarMaxShadowDenoisingBatchSize.GetValueOnRenderThread(), 1, IScreenSpaceDenoiser::kMaxBatchSize);
            const int32 MaxRTShadowBatchSize = CVarMaxShadowRayTracingBatchSize.GetValueOnRenderThread();
            const bool bDoShadowDenoisingBatching = DenoiserMode != 0 && MaxDenoisingBatchSize > 1;

            (......)

            bool bDirectLighting = ViewFamily.EngineShowFlags.DirectLighting;
            bool bShadowMaskReadable = false;
            TRefCountPtr<IPooledRenderTarget> ScreenShadowMaskTexture;
            TRefCountPtr<IPooledRenderTarget> ScreenShadowMaskSubPixelTexture;

            // 렌더링(Render)그림자(Shadow)의 라이트(Light) 및 라이팅 함수 라이트(Light).
            for (int32 LightIndex = AttenuationLightStart; LightIndex < SortedLights.Num(); LightIndex++)
            {
                const FSortedLightSceneInfo& SortedLightInfo = SortedLights[LightIndex];
                const FLightSceneInfo& LightSceneInfo = *SortedLightInfo.LightSceneInfo;

                // Note: Skip shadow mask generation for rect light if direct illumination is computed
                //         stochastically (rather than analytically + shadow mask)
                const bool bDrawShadows = SortedLightInfo.SortKey.Fields.bShadowed && !ShouldRenderRayTracingStochasticRectLight(LightSceneInfo);
                bool bDrawLightFunction = SortedLightInfo.SortKey.Fields.bLightFunction;
                bool bDrawPreviewIndicator = ViewFamily.EngineShowFlags.PreviewShadowsIndicator && !LightSceneInfo.IsPrecomputedLightingValid() && LightSceneInfo.Proxy->HasStaticShadowing();
                bool bInjectedTranslucentVolume = false;
                bool bUsedShadowMaskTexture = false;
                const bool bDrawHairShadow = bDrawShadows && bUseHairLighting;
                const bool bUseHairDeepShadow = bDrawShadows && bUseHairLighting && LightSceneInfo.Proxy->CastsHairStrandsDeepShadow();

                FScopeCycleCounter Context(LightSceneInfo.Proxy->GetStatId());

                if ((bDrawShadows || bDrawLightFunction || bDrawPreviewIndicator) && !ScreenShadowMaskTexture.IsValid())
                {
                    SceneContext.AllocateScreenShadowMask(RHICmdList, ScreenShadowMaskTexture);
                    bShadowMaskReadable = false;
                    if (bUseHairLighting)
                    {
                        SceneContext.AllocateScreenShadowMask(RHICmdList, ScreenShadowMaskSubPixelTexture, true);
                    }
                }

                FString LightNameWithLevel;
                GetLightNameForDrawEvent(LightSceneInfo.Proxy, LightNameWithLevel);
                SCOPED_DRAW_EVENTF(RHICmdList, EventLightPass, *LightNameWithLevel);

                if (bDrawShadows)
                {
                    INC_DWORD_STAT(STAT_NumShadowedLights);

                    const FLightOcclusionType OcclusionType = GetLightOcclusionType(*LightSceneInfo.Proxy);

                    (......)
                    
                    // 처리(Process)그림자(Shadow).
                    else // (OcclusionType == FOcclusionType::Shadowmap)
                    {
                        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
                        {
                            const FViewInfo& View = Views[ViewIndex];
                            View.HeightfieldLightingViewInfo.ClearShadowing(View, RHICmdList, LightSceneInfo);
                        }
                    
                        // 그림자(Shadow).
                        auto ClearShadowMask = [&](TRefCountPtr<IPooledRenderTarget>& InScreenShadowMaskTexture)
                        {
                            // Clear light attenuation for local lights with a quad covering their extents
                            const bool bClearLightScreenExtentsOnly = CVarAllowClearLightSceneExtentsOnly.GetValueOnRenderThread() && SortedLightInfo.SortKey.Fields.LightType != LightType_Directional;
                            // All shadows render with min blending
                            bool bClearToWhite = !bClearLightScreenExtentsOnly;

                            FRHIRenderPassInfo RPInfo(InScreenShadowMaskTexture->GetRenderTargetItem().TargetableTexture, ERenderTargetActions::Load_Store);
                            RPInfo.DepthStencilRenderTarget.Action = MakeDepthStencilTargetActions(ERenderTargetActions::Load_DontStore, ERenderTargetActions::Load_Store);
                            RPInfo.DepthStencilRenderTarget.DepthStencilTarget = SceneContext.GetSceneDepthSurface();
                            RPInfo.DepthStencilRenderTarget.ExclusiveDepthStencil = FExclusiveDepthStencil::DepthRead_StencilWrite;
                            if (bClearToWhite)
                            {
                                RPInfo.ColorRenderTargets[0].Action = ERenderTargetActions::Clear_Store;
                            }

                            TransitionRenderPassTargets(RHICmdList, RPInfo);
                            RHICmdList.BeginRenderPass(RPInfo, TEXT("ClearScreenShadowMask"));
                            if (bClearLightScreenExtentsOnly)
                            {
                                SCOPED_DRAW_EVENT(RHICmdList, ClearQuad);
    
                                for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
                                {
                                    const FViewInfo& View = Views[ViewIndex];
                                    FIntRect ScissorRect;

                                    if (!LightSceneInfo.Proxy->GetScissorRect(ScissorRect, View, View.ViewRect))
                                    {
                                        ScissorRect = View.ViewRect;
                                    }

                                    if (ScissorRect.Min.X < ScissorRect.Max.X && ScissorRect.Min.Y < ScissorRect.Max.Y)
                                    {
                                        RHICmdList.SetViewport(ScissorRect.Min.X, ScissorRect.Min.Y, 0.0f, ScissorRect.Max.X, ScissorRect.Max.Y, 1.0f);
                                        DrawClearQuad(RHICmdList, true, FLinearColor(1, 1, 1, 1), false, 0, false, 0);
                                    }
                                    else
                                    {
                                        LightSceneInfo.Proxy->GetScissorRect(ScissorRect, View, View.ViewRect);
                                    }
                                }
                            }
                            RHICmdList.EndRenderPass();
                        };

                        ClearShadowMask(ScreenShadowMaskTexture);
                        if (ScreenShadowMaskSubPixelTexture)
                        {
                            ClearShadowMask(ScreenShadowMaskSubPixelTexture);
                        }

                        RenderShadowProjections(RHICmdList, &LightSceneInfo, ScreenShadowMaskTexture, ScreenShadowMaskSubPixelTexture, HairDatas, bInjectedTranslucentVolume);
                    }

                    bUsedShadowMaskTexture = true;
                }

                for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
                {
                    const FViewInfo& View = Views[ViewIndex];
                    View.HeightfieldLightingViewInfo.ComputeLighting(View, RHICmdList, LightSceneInfo);
                }

                // 처리(Process)라이팅 함수 (light function).
                if (bDirectLighting)
                {
                    if (bDrawLightFunction)
                    {
                        const bool bLightFunctionRendered = RenderLightFunction(RHICmdList, &LightSceneInfo, ScreenShadowMaskTexture, bDrawShadows, false);
                        bUsedShadowMaskTexture |= bLightFunctionRendered;
                    }
                    
                    (......)
                }

                if (bUsedShadowMaskTexture)
                {
                    check(ScreenShadowMaskTexture);
                    RHICmdList.CopyToResolveTarget(ScreenShadowMaskTexture->GetRenderTargetItem().TargetableTexture, ScreenShadowMaskTexture->GetRenderTargetItem().ShaderResourceTexture, FResolveParams(FResolveRect()));
                    if (ScreenShadowMaskSubPixelTexture)
                    {
                        RHICmdList.CopyToResolveTarget(ScreenShadowMaskSubPixelTexture->GetRenderTargetItem().TargetableTexture, ScreenShadowMaskSubPixelTexture->GetRenderTargetItem().ShaderResourceTexture, FResolveParams(FResolveRect()));
                    }

                    if (!bShadowMaskReadable)
                    {
                        RHICmdList.TransitionResource(EResourceTransitionAccess::EReadable, ScreenShadowMaskTexture->GetRenderTargetItem().ShaderResourceTexture);
                        if (ScreenShadowMaskSubPixelTexture)
                        {
                            RHICmdList.TransitionResource(EResourceTransitionAccess::EReadable, ScreenShadowMaskSubPixelTexture->GetRenderTargetItem().ShaderResourceTexture);
                        }
                        bShadowMaskReadable = true;
                    }

                    GVisualizeTexture.SetCheckPoint(RHICmdList, ScreenShadowMaskTexture);
                    if (ScreenShadowMaskSubPixelTexture)
                    {
                        GVisualizeTexture.SetCheckPoint(RHICmdList, ScreenShadowMaskSubPixelTexture);
                    }
                }

                (......)
                
                // 렌더링(Render)레이턴시 라이팅 .
                {
                    SCOPED_DRAW_EVENT(RHICmdList, StandardDeferredLighting);
                    SceneContext.BeginRenderingSceneColor(RHICmdList, ESimpleRenderTargetMode::EExistingColorAndDepth, FExclusiveDepthStencil::DepthRead_StencilWrite, true);

                    // ScreenShadowMaskTexture might have been created for a previous light, but only use it if we wrote valid data into it for this light
                    IPooledRenderTarget* LightShadowMaskTexture = nullptr;
                    IPooledRenderTarget* LightShadowMaskSubPixelTexture = nullptr;
                    if (bUsedShadowMaskTexture)
                    {
                        LightShadowMaskTexture = ScreenShadowMaskTexture;
                        LightShadowMaskSubPixelTexture = ScreenShadowMaskSubPixelTexture;
                    }

                    if (bDirectLighting)
                    {
                        // 렌더링(Render)그림자(Shadow)의 라이트(Light).
                        RenderLight(RHICmdList, &LightSceneInfo, LightShadowMaskTexture, InHairVisibilityViews, false, true);
                    }

                    SceneContext.FinishRenderingSceneColor(RHICmdList);

                    (......)
                }
            }
        }
    }
}

위의 코드를 보면 광원을 렌더링할 때 그림자가 없는 광원이 먼저 그려지고 SortedLightSet.SimpleLightsEnd와 같은 변수에 의해 시작 인덱스가 결정된 다음 그림자가 있는 광원이 그려지고 시작 인덱스가 SortedLightSet.AttenuationLightStart에 의해 결정되는 것을 볼 수 있습니다. 그림자가 없는 광원만 타일링 및 클러스터링을 지원합니다.

음영 처리 여부에 관계없이 광원을 렌더링할 때 RenderLight(RenderLights가 아님)가 결국 호출되어 단일 광원의 조명 계산을 실제로 수행하게 됩니다. RenderLight 코드는 다음과 같습니다:

cpp
void FDeferredShadingSceneRenderer::RenderLight(FRHICommandList& RHICmdList, const FLightSceneInfo* LightSceneInfo, IPooledRenderTarget* ScreenShadowMaskTexture, const FHairStrandsVisibilityViews* InHairVisibilityViews,  bool bRenderOverlap, bool bIssueDrawEvent)
{
    FGraphicsPipelineStateInitializer GraphicsPSOInit;
    RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);

    // 설정(Set)블렌딩(Blending)상태 로 , 로써 라이트(Light)의 라이팅 까지 텍스처(Texture) 내에서 .
    GraphicsPSOInit.BlendState = TStaticBlendState<CW_RGBA, BO_Add, BF_One, BF_One, BO_Add, BF_One, BF_One>::GetRHI();

    GraphicsPSOInit.PrimitiveType = PT_TriangleList;

    const FSphere LightBounds = LightSceneInfo->Proxy->GetBoundingSphere();
    const bool bTransmission = LightSceneInfo->Proxy->Transmission();

    // view, 라이트(Light)개 view드로우/렌더(Draw)회 .
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
    {
        FViewInfo& View = Views[ViewIndex];

        // Ensure the light is valid for this view
        if (!LightSceneInfo->ShouldRenderLight(View))
        {
            continue;
        }

        bool bUseIESTexture = false;

        if(View.Family->EngineShowFlags.TexturedLightProfiles)
        {
            bUseIESTexture = (LightSceneInfo->Proxy->GetIESTextureResource() != 0);
        }

        // Set the device viewport for the view.
        RHICmdList.SetViewport(View.ViewRect.Min.X, View.ViewRect.Min.Y, 0.0f, View.ViewRect.Max.X, View.ViewRect.Max.Y, 1.0f);

        (......)
        
        // 드로우/렌더(Draw).
        if (LightSceneInfo->Proxy->GetLightType() == LightType_Directional)
        {
            // Turn DBT back off
            GraphicsPSOInit.bDepthBounds = false;
            TShaderMapRef<TDeferredLightVS<false> > VertexShader(View.ShaderMap);

            GraphicsPSOInit.RasterizerState = TStaticRasterizerState<FM_Solid, CM_None>::GetRHI();
            GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<false, CF_Always>::GetRHI();

            // 설정(Set)레이턴시 라이트(Light)의 shader 및 pso파라미터 .
            if (bRenderOverlap)
            {
                TShaderMapRef<TDeferredLightOverlapPS<false> > PixelShader(View.ShaderMap);
                GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;
                GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
                GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
                SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);
                PixelShader->SetParameters(RHICmdList, View, LightSceneInfo);
            }
            else
            {
                const bool bAtmospherePerPixelTransmittance = LightSceneInfo->Proxy->IsUsedAsAtmosphereSunLight() && ShouldApplyAtmosphereLightPerPixelTransmittance(Scene, View.Family->EngineShowFlags);

                FDeferredLightPS::FPermutationDomain PermutationVector;
                PermutationVector.Set< FDeferredLightPS::FSourceShapeDim >( ELightSourceShape::Directional );
                PermutationVector.Set< FDeferredLightPS::FIESProfileDim >( false );
                PermutationVector.Set< FDeferredLightPS::FInverseSquaredDim >( false );
                PermutationVector.Set< FDeferredLightPS::FVisualizeCullingDim >( View.Family->EngineShowFlags.VisualizeLightCulling );
                PermutationVector.Set< FDeferredLightPS::FLightingChannelsDim >( View.bUsesLightingChannels );
                PermutationVector.Set< FDeferredLightPS::FTransmissionDim >( bTransmission );
                PermutationVector.Set< FDeferredLightPS::FHairLighting>(bHairLighting ? 1 : 0);
                // Only directional lights are rendered in this path, so we only need to check if it is use to light the atmosphere
                PermutationVector.Set< FDeferredLightPS::FAtmosphereTransmittance >(bAtmospherePerPixelTransmittance);

                TShaderMapRef< FDeferredLightPS > PixelShader( View.ShaderMap, PermutationVector );
                GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;
                GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
                GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();

                SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);
                PixelShader->SetParameters(RHICmdList, View, LightSceneInfo, ScreenShadowMaskTexture, (bHairLighting) ? &RenderLightParams : nullptr);
            }

            VertexShader->SetParameters(RHICmdList, View, LightSceneInfo);

            // 에 의해 는 , 스크린 스페이스의 , 을(를) 활용하여 풀스크린(Full Screen)범위 의 모멘트(Moment)드로우/렌더(Draw).
            DrawRectangle(
                RHICmdList,
                0, 0,
                View.ViewRect.Width(), View.ViewRect.Height(),
                View.ViewRect.Min.X, View.ViewRect.Min.Y,
                View.ViewRect.Width(), View.ViewRect.Height(),
                View.ViewRect.Size(),
                FSceneRenderTargets::Get(RHICmdList).GetBufferSizeXY(),
                VertexShader,
                EDRF_UseTriangleOptimization);
        }
        else // 로컬 라이트(Light)
        {
            // 는 활성화(Enable)뎁스 바운딩 박스(AABB)(DBT).
            GraphicsPSOInit.bDepthBounds = GSupportsDepthBoundsTest && GAllowDepthBoundsTest != 0;

            TShaderMapRef<TDeferredLightVS<true> > VertexShader(View.ShaderMap);

            SetBoundingGeometryRasterizerAndDepthState(GraphicsPSOInit, View, LightBounds);

            // 설정(Set)레이턴시 라이트(Light)의 shader 및 pso파라미터 .
            if (bRenderOverlap)
            {
                TShaderMapRef<TDeferredLightOverlapPS<true> > PixelShader(View.ShaderMap);
                GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector4();
                GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
                GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();

                SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);
                PixelShader->SetParameters(RHICmdList, View, LightSceneInfo);
            }
            else
            {
                FDeferredLightPS::FPermutationDomain PermutationVector;
                PermutationVector.Set< FDeferredLightPS::FSourceShapeDim >( LightSceneInfo->Proxy->IsRectLight() ? ELightSourceShape::Rect : ELightSourceShape::Capsule );
                PermutationVector.Set< FDeferredLightPS::FSourceTextureDim >( LightSceneInfo->Proxy->IsRectLight() && LightSceneInfo->Proxy->HasSourceTexture() );
                PermutationVector.Set< FDeferredLightPS::FIESProfileDim >( bUseIESTexture );
                PermutationVector.Set< FDeferredLightPS::FInverseSquaredDim >( LightSceneInfo->Proxy->IsInverseSquared() );
                PermutationVector.Set< FDeferredLightPS::FVisualizeCullingDim >( View.Family->EngineShowFlags.VisualizeLightCulling );
                PermutationVector.Set< FDeferredLightPS::FLightingChannelsDim >( View.bUsesLightingChannels );
                PermutationVector.Set< FDeferredLightPS::FTransmissionDim >( bTransmission );
                PermutationVector.Set< FDeferredLightPS::FHairLighting>(bHairLighting ? 1 : 0);
                PermutationVector.Set < FDeferredLightPS::FAtmosphereTransmittance >(false);

                TShaderMapRef< FDeferredLightPS > PixelShader( View.ShaderMap, PermutationVector );
                GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector4();
                GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
                GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();

                SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);
                PixelShader->SetParameters(RHICmdList, View, LightSceneInfo, ScreenShadowMaskTexture, (bHairLighting) ? &RenderLightParams : nullptr);
            }

            VertexShader->SetParameters(RHICmdList, View, LightSceneInfo);

            // 뎁스 바운딩 박스(AABB)(DBT)오직 그림자(Shadow)의 라이트(Light) 내에서 유효(Valid), 로써 유효(Valid)컬링 뎁스 범위 의 픽셀(Pixel).
            if (GraphicsPSOInit.bDepthBounds)
            {
                // UE4을(를) 활용하여 역방향(Inverse)의 뎁스 (reversed depth), 따라서 far<near.
                float NearDepth = 1.f;
                float FarDepth = 0.f;
                CalculateLightNearFarDepthFromBounds(View,LightBounds,NearDepth,FarDepth);

                if (NearDepth <= FarDepth)
                {
                    NearDepth = 1.0f;
                    FarDepth = 0.0f;
                }

                // 설정(Set)뎁스 바운딩 박스(AABB)파라미터 .
                RHICmdList.SetDepthBounds(FarDepth, NearDepth);
            }

            // 포인트 라이트(Light) 또는 을(를) 활용하여 드로우/렌더(Draw).
            if( LightSceneInfo->Proxy->GetLightType() == LightType_Point ||
                LightSceneInfo->Proxy->GetLightType() == LightType_Rect )
            {
                StencilingGeometry::DrawSphere(RHICmdList);
            }
            // 을(를) 활용하여 드로우/렌더(Draw).
            else if (LightSceneInfo->Proxy->GetLightType() == LightType_Spot)
            {
                StencilingGeometry::DrawCone(RHICmdList);
            }
        }
    }
}

위에서 알 수 있듯이 평행광원이든 국부광원이든 화면공간에서 드로잉이 이루어지며, 평행광은 화면 전체를 덮는 직사각형을 이용하여 광원을 그리며, 점광원이나 면광원은 구를 이용하여 광원을 그리며, 스포트라이트는 원뿔을 이용하여 그린다. 이것의 장점은 광원 경계 상자 외부의 픽셀을 빠르게 제거할 수 있고 조명 계산 효율성이 가속화된다는 것입니다. LightingPass의 중첩 관계를 요약하면 의사 코드는 다음과 같습니다.

cpp
foreach(scene in scenes)
{
    foreach(light in lights)
    {
        foreach(view in views)
        {
            RenderLight() // 회 호출(Call)렌더링(Render)라이트(Light)실행(Execute)DeferredLightVertexShaders 및 DeferredLightPixelShaders의 코드 .
        }
    }
}

DeferredLightVertexShaders 및 DeferredLightPixelShaders는 특정 셰이더 논리로, 이 문서에서는 다루지 않으며 다음 문서에서 자세히 설명합니다.

4.3.8 반투명도

반투명도는 반투명 객체를 렌더링하는 단계입니다. 모든 반투명 객체는 뷰 공간에서 먼 곳에서 가까운 곳으로 하나씩 오프스크린 렌더링 텍스처(별도의 반투명 렌더 대상)로 그려진 다음 별도의 패스를 사용하여 조명 결과를 올바르게 계산하고 혼합합니다.

FDeferredShadingSceneRenderer::Render 및 그에 인접한 중요한 인터페이스의 반투명 입구는 다음과 같습니다:

cpp
void FDeferredShadingSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
    (......)
    
    RenderLights(RHICmdList, SortedLightSet, ...);
    
    (......)
    
    RenderSkyAtmosphere(RHICmdList);
    
    (......)
    
    RenderFog(RHICmdList, LightShaftOutput);
    
    (......)
    
    // 드로우/렌더(Draw)반투명(Translucent)。
    if (bHasAnyViewsAbovewater && bCanOverlayRayTracingOutput && ViewFamily.EngineShowFlags.Translucency && !ViewFamily.EngineShowFlags.VisualizeLightCulling && !ViewFamily.UseDebugViewPS())
    {
        (......)
        
        {
            // 렌더링(Render)반투명(Translucent).
            RenderTranslucency(RHICmdList);

            static const auto DisableDistortionCVar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.DisableDistortion"));
            const bool bAllowDistortion = DisableDistortionCVar->GetValueOnAnyThread() != 1;

            // 렌더링(Render)텍스처(Texture)。
            if (GetRefractionQuality(ViewFamily) > 0 && bAllowDistortion)
            {
                RenderDistortion(RHICmdList);
            }
        }

        // 렌더링(Render)반투명(Translucent)의 벨로시티 버퍼(Velocity Buffer).
        if (bShouldRenderVelocities)
        {
            RenderVelocities(RHICmdList, SceneContext.SceneVelocity, EVelocityPass::Translucent, bClearVelocityRT);
        }
    }
    
    (......)
}

반투명 단계에서는 반투명 색상, 섭동 텍스처(굴절과 같은 효과에 사용됨) 및 속도 버퍼(TAA 앤티앨리어싱 및 포스트 프로세스 파이프라인에 사용됨)를 렌더링합니다. 트랜스루센트 렌더링을 위한 가장 중요한 로직은 RenderTranslucency입니다:

cpp
// Source\Runtime\Renderer\Private\TranslucentRendering.cpp

void FDeferredShadingSceneRenderer::RenderTranslucency(FRHICommandListImmediate& RHICmdList, bool bDrawUnderwaterViews)
{
    TRefCountPtr<IPooledRenderTarget> SceneColorCopy;
    if (!bDrawUnderwaterViews)
    {
        ConditionalResolveSceneColorForTranslucentMaterials(RHICmdList, SceneColorCopy);
    }

    // Disable UAV cache flushing so we have optimal VT feedback performance.
    RHICmdList.BeginUAVOverlap();

    // 뎁스 오브 필드(DoF)(DoF)렌더링(Render)반투명(Translucent)。
    if (ViewFamily.AllowTranslucencyAfterDOF())
    {
        // 개 Pass렌더링(Render)의 반투명(Translucent)。
        RenderTranslucencyInner(RHICmdList, ETranslucencyPass::TPT_StandardTranslucency, SceneColorCopy, bDrawUnderwaterViews);
        // 개 Pass렌더링(Render)DOF의 반투명(Translucent), 저장(Store)의 반투명(Translucent)RT 내에서 , 로써 을(를) 활용하여 .
        RenderTranslucencyInner(RHICmdList, ETranslucencyPass::TPT_TranslucencyAfterDOF, SceneColorCopy, bDrawUnderwaterViews);
        // 개 Pass반투명(Translucent)의 RT 및 씬 컬러(SceneColor)버퍼 DOF pass블렌딩(Blending).
        RenderTranslucencyInner(RHICmdList, ETranslucencyPass::TPT_TranslucencyAfterDOFModulate, SceneColorCopy, bDrawUnderwaterViews);
    }
    else // , 개 Pass즉, 렌더링(Render)의 반투명(Translucent).
    {
        RenderTranslucencyInner(RHICmdList, ETranslucencyPass::TPT_AllTranslucency, SceneColorCopy, bDrawUnderwaterViews);
    }

    RHICmdList.EndUAVOverlap();
}

RenderTranslucencyInner는 실제로 내부적으로 반투명 개체를 렌더링합니다. 해당 코드는 다음과 같습니다.

cpp
// Source\Runtime\Renderer\Private\TranslucentRendering.cpp

void FDeferredShadingSceneRenderer::RenderTranslucencyInner(FRHICommandListImmediate& RHICmdList, ETranslucencyPass::Type TranslucencyPass, IPooledRenderTarget* SceneColorCopy, bool bDrawUnderwaterViews)
{
    if (!ShouldRenderTranslucency(TranslucencyPass))
    {
        return; // Early exit if nothing needs to be done.
    }
    
    FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);

    // 병렬 렌더링(Render)지원 .
    const bool bUseParallel = GRHICommandList.UseParallelAlgorithms() && CVarParallelTranslucency.GetValueOnRenderThread();
    if (bUseParallel)
    {
        SceneContext.AllocLightAttenuation(RHICmdList); // materials will attempt to get this texture before the deferred command to set it up executes
    }
    FScopedCommandListWaitForTasks Flusher(bUseParallel && (CVarRHICmdFlushRenderThreadTasksTranslucentPass.GetValueOnRenderThread() > 0 || CVarRHICmdFlushRenderThreadTasks.GetValueOnRenderThread() > 0), RHICmdList);

    // 순회(Traverse/Iterate)view.
    for (int32 ViewIndex = 0, NumProcessedViews = 0; ViewIndex < Views.Num(); ViewIndex++)
    {
        FViewInfo& View = Views[ViewIndex];
        if (!View.ShouldRenderView() || (Views[ViewIndex].IsUnderwater() != bDrawUnderwaterViews))
        {
            continue;
        }

        // 갱신(Update)의 Uinform Buffer.
        Scene->UniformBuffers.UpdateViewUniformBuffer(View);

        TUniformBufferRef<FTranslucentBasePassUniformParameters> BasePassUniformBuffer;
        // 갱신(Update)반투명(Translucent)Pass의 Uinform Buffer.
        CreateTranslucentBasePassUniformBuffer(RHICmdList, View, SceneColorCopy, ESceneTextureSetupMode::All, BasePassUniformBuffer, ViewIndex);
        // 렌더링(Render)상태 .
        FMeshPassProcessorRenderState DrawRenderState(View, BasePassUniformBuffer);

        // 렌더링(Render)saparate.
        if (!bDrawUnderwaterViews && RenderInSeparateTranslucency(SceneContext, TranslucencyPass, View.TranslucentPrimCount.DisableOffscreenRendering(TranslucencyPass)))
        {
            FIntPoint ScaledSize;
            float DownsamplingScale = 1.f;
            SceneContext.GetSeparateTranslucencyDimensions(ScaledSize, DownsamplingScale);

            if (DownsamplingScale < 1.f)
            {
                FViewUniformShaderParameters DownsampledTranslucencyViewParameters;
                SetupDownsampledTranslucencyViewParameters(RHICmdList, View, DownsampledTranslucencyViewParameters);
                Scene->UniformBuffers.UpdateViewUniformBufferImmediate(DownsampledTranslucencyViewParameters);
                DrawRenderState.SetViewUniformBuffer(Scene->UniformBuffers.ViewUniformBuffer);

                (......)
            }

            // 렌더링(Render)의 스테이지 .
            if (TranslucencyPass == ETranslucencyPass::TPT_TranslucencyAfterDOF)
            {
                BeginTimingSeparateTranslucencyPass(RHICmdList, View);
                SceneContext.BeginRenderingSeparateTranslucency(RHICmdList, View, *this, NumProcessedViews == 0 || View.Family->bMultiGPUForkAndJoin);
            }
            // 블렌딩(Blending).
            else if (TranslucencyPass == ETranslucencyPass::TPT_TranslucencyAfterDOFModulate)
            {
                BeginTimingSeparateTranslucencyModulatePass(RHICmdList, View);
                SceneContext.BeginRenderingSeparateTranslucencyModulate(RHICmdList, View, *this, NumProcessedViews == 0 || View.Family->bMultiGPUForkAndJoin);
            }
            // .
            else
            {
                SceneContext.BeginRenderingSeparateTranslucency(RHICmdList, View, *this, NumProcessedViews == 0 || View.Family->bMultiGPUForkAndJoin);
            }

            // Draw only translucent prims that are in the SeparateTranslucency pass
            DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<false, CF_DepthNearOrEqual>::GetRHI());

            // 드로우/렌더(Draw)반투명(Translucent).
            if (bUseParallel)
            {
                RHICmdList.EndRenderPass();
                RenderViewTranslucencyParallel(RHICmdList, View, DrawRenderState, TranslucencyPass);
            }
            else
            {
                RenderViewTranslucency(RHICmdList, View, DrawRenderState, TranslucencyPass);
                RHICmdList.EndRenderPass();
            }

            // 렌더링(Render)의 스테이지 .
            if (TranslucencyPass == ETranslucencyPass::TPT_TranslucencyAfterDOF)
            {
                SceneContext.ResolveSeparateTranslucency(RHICmdList, View);
                EndTimingSeparateTranslucencyPass(RHICmdList, View);
            }
            else if (TranslucencyPass == ETranslucencyPass::TPT_TranslucencyAfterDOFModulate)
            {
                SceneContext.ResolveSeparateTranslucencyModulate(RHICmdList, View);
                EndTimingSeparateTranslucencyModulatePass(RHICmdList, View);
            }
            else
            {
                SceneContext.ResolveSeparateTranslucency(RHICmdList, View);
            }

            // 업샘플링(Upsampling)()반투명(Translucent)의 RT.
            if (TranslucencyPass != ETranslucencyPass::TPT_TranslucencyAfterDOF && TranslucencyPass != ETranslucencyPass::TPT_TranslucencyAfterDOFModulate)
            {
                UpsampleTranslucency(RHICmdList, View, false);
            }
        }
        else // .
        {
            SceneContext.BeginRenderingTranslucency(RHICmdList, View, *this, NumProcessedViews == 0 || View.Family->bMultiGPUForkAndJoin);
            DrawRenderState.SetDepthStencilState(TStaticDepthStencilState<false, CF_DepthNearOrEqual>::GetRHI());

            if (bUseParallel && !ViewFamily.UseDebugViewPS())
            {
                RHICmdList.EndRenderPass();
                RenderViewTranslucencyParallel(RHICmdList, View, DrawRenderState, TranslucencyPass);
            }
            else
            {
                RenderViewTranslucency(RHICmdList, View, DrawRenderState, TranslucencyPass);
                RHICmdList.EndRenderPass();
            }

            SceneContext.FinishRenderingTranslucency(RHICmdList);
        }

        // Keep track of number of views not skipped
        NumProcessedViews++;
    }
}

트랜스루센트 렌더링의 C++ 로직과 셰이더 로직은 Base Pass와 유사합니다. 차이점은 반투명은 반투명 객체만 다룬다는 것입니다. 균일 버퍼(Uniform Buffer) 부분도 다르고, 렌더 상태(Render State)도 다르며, 조명 알고리즘도 다르게 계산됩니다. 그러나 그들의 주요 논리는 대략 동일하므로 여기서는 분석하지 않습니다.

게다가 UE4에는 두 개의 주요 트랜스루센트 렌더링 큐가 있습니다. 하나는 표준 큐이고 다른 하나는 분리 큐입니다. 분리된 대기열은 프로젝트 렌더링 설정에서 활성화되어야 합니다(기본적으로 활성화되어 있음):

분리 대기열에 투명한 개체를 추가하려면 사용하는 재질에 대해 DOF 후 렌더링을 켜기만 하면 됩니다(기본적으로 켜져 있음).

프로젝트 구성과 재료 태그 모두 기본적으로 분리 대기열을 활성화하므로 모든 반투명 객체는 기본적으로 분리 대기열에서 렌더링됩니다.

4.3.9 포스트 프로세스

포스트 프로세스 단계는 FDeferredShadingSceneRenderer::Render의 마지막 단계이기도 합니다. GBuffer가 필요하지 않은 Bloom, Tone Mapping, Gamma Correction 등과 GBuffer가 필요한 SSR, SSAO, SSGI 등이 포함됩니다. 이 단계에서는 반투명 렌더 텍스처를 최종 장면 색상에 혼합합니다.

FDeferredShadingSceneRenderer::Render에서의 위치는 다음과 같습니다:

cpp
void FDeferredShadingSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
    (......)
    
    RenderTranslucency(RHICmdList, ...);
    
    (......)
    
    RenderLightShaftBloom(RHICmdList);
    
    (......)
    
    RenderDistanceFieldLighting(RHICmdList, ...);
    
    (......)
    
    CopySceneCaptureComponentToTarget(RHICmdList);
    
    (......)
    
    // 포스트 프로세스 파이프라인 스테이지。
    if (ViewFamily.bResolveScene)
    {
        GRenderTargetPool.AddPhaseEvent(TEXT("PostProcessing"));

        // 포스트 프로세스의 GraphBuilder.
        FRDGBuilder GraphBuilder(RHICmdList);

        FSceneTextureParameters SceneTextures;
        SetupSceneTextureParameters(GraphBuilder, &SceneTextures);

        // Fallback to a black texture if no velocity.
        if (!SceneTextures.SceneVelocityBuffer)
        {
            SceneTextures.SceneVelocityBuffer = GSystemTextures.GetBlackDummy(GraphBuilder);
        }

        // 포스트 프로세스 입력 파라미터.
        FPostProcessingInputs PostProcessingInputs;
        PostProcessingInputs.SceneTextures = &SceneTextures;
        PostProcessingInputs.ViewFamilyTexture = CreateViewFamilyTexture(GraphBuilder, ViewFamily);
        PostProcessingInputs.SceneColor = GraphBuilder.RegisterExternalTexture(SceneContext.GetSceneColor(), TEXT("SceneColor"));
        PostProcessingInputs.CustomDepth = GraphBuilder.TryRegisterExternalTexture(SceneContext.CustomDepth, TEXT("CustomDepth"));
        PostProcessingInputs.SeparateTranslucency = RegisterExternalTextureWithFallback(GraphBuilder, SceneContext.SeparateTranslucencyRT, SceneContext.GetSeparateTranslucencyDummy(), TEXT("SeparateTranslucency"));
        PostProcessingInputs.SeparateModulation = RegisterExternalTextureWithFallback(GraphBuilder, SceneContext.SeparateTranslucencyModulateRT, SceneContext.GetSeparateTranslucencyModulateDummy(), TEXT("SeparateModulate"));

        (......)
        
        {
            for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
            {
                FViewInfo& View = Views[ViewIndex];
                // 추가(Add)포스트 프로세스 패스.
                AddPostProcessingPasses(GraphBuilder, View, PostProcessingInputs);
            }
        }

        // 해제(Release)포스트 프로세스을(를) 활용하여 의 RT.
        SceneContext.FreeSeparateTranslucency();
        SceneContext.FreeSeparateTranslucencyModulate();
        SceneContext.SetSceneColor(nullptr);
        SceneContext.AdjustGBufferRefCount(GraphBuilder.RHICmdList, -1);

        // 실행(Execute)GraphBuilder.
        GraphBuilder.Execute();

        GRenderTargetPool.AddPhaseEvent(TEXT("AfterPostprocessing"));

        // End of frame, we don't need it anymore.
        FSceneRenderTargets::Get(RHICmdList).FreeDownsampledTranslucencyDepth();
    }
    else
    {
        // Release the original reference on the scene render targets
        SceneContext.AdjustGBufferRefCount(RHICmdList, -1);
    }
    
    (......)
}

후처리에서는 RHICommandList를 직접 사용하지 않고 대신 GraphBuilder를 사용합니다. GraphBuilder는 불필요한 패스를 자동으로 잘라내고 패스와 리소스 수명 주기 간의 종속성을 자동으로 관리하며 리소스, PSO 및 렌더링 지침을 최적화할 수 있는 종속성 렌더링 그래프(RDG)입니다.

AddPostProcessingPasses는 패스를 추가하기 위한 특수 로직입니다. 코드는 다음과 같습니다:

cpp
// Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessing.cpp

void AddPostProcessingPasses(FRDGBuilder& GraphBuilder, const FViewInfo& View, const FPostProcessingInputs& Inputs)
{
    Inputs.Validate();

    // 초기화(Initialize)데이터 및 플래그 .
    
    const FIntRect PrimaryViewRect = View.ViewRect;

    const FSceneTextureParameters& SceneTextures = *Inputs.SceneTextures;
    const FScreenPassRenderTarget ViewFamilyOutput = FScreenPassRenderTarget::CreateViewFamilyOutput(Inputs.ViewFamilyTexture, View);
    const FScreenPassTexture SceneDepth(SceneTextures.SceneDepthBuffer, PrimaryViewRect);
    const FScreenPassTexture SeparateTranslucency(Inputs.SeparateTranslucency, PrimaryViewRect);
    const FScreenPassTexture SeparateModulation(Inputs.SeparateModulation, PrimaryViewRect);
    const FScreenPassTexture CustomDepth(Inputs.CustomDepth, PrimaryViewRect);
    const FScreenPassTexture Velocity(SceneTextures.SceneVelocityBuffer, PrimaryViewRect);
    const FScreenPassTexture BlackDummy(GSystemTextures.GetBlackDummy(GraphBuilder));

    // Scene color is updated incrementally through the post process pipeline.
    FScreenPassTexture SceneColor(Inputs.SceneColor, PrimaryViewRect);

    // Assigned before and after the tonemapper.
    FScreenPassTexture SceneColorBeforeTonemap;
    FScreenPassTexture SceneColorAfterTonemap;

    // Unprocessed scene color stores the original input.
    const FScreenPassTexture OriginalSceneColor = SceneColor;

    // Default the new eye adaptation to the last one in case it's not generated this frame.
    const FEyeAdaptationParameters EyeAdaptationParameters = GetEyeAdaptationParameters(View, ERHIFeatureLevel::SM5);
    FRDGTextureRef LastEyeAdaptationTexture = GetEyeAdaptationTexture(GraphBuilder, View);
    FRDGTextureRef EyeAdaptationTexture = LastEyeAdaptationTexture;

    // Histogram defaults to black because the histogram eye adaptation pass is used for the manual metering mode.
    FRDGTextureRef HistogramTexture = BlackDummy.Texture;

    const FEngineShowFlags& EngineShowFlags = View.Family->EngineShowFlags;
    const bool bVisualizeHDR = EngineShowFlags.VisualizeHDR;
    const bool bViewFamilyOutputInHDR = GRHISupportsHDROutput && IsHDREnabled();
    const bool bVisualizeGBufferOverview = IsVisualizeGBufferOverviewEnabled(View);
    const bool bVisualizeGBufferDumpToFile = IsVisualizeGBufferDumpToFileEnabled(View);
    const bool bVisualizeGBufferDumpToPIpe = IsVisualizeGBufferDumpToPipeEnabled(View);
    const bool bOutputInHDR = IsPostProcessingOutputInHDR();

    const FPaniniProjectionConfig PaniniConfig(View);

    // 포스트 프로세스의 Pass.
    enum class EPass : uint32
    {
        MotionBlur,
        Tonemap,
        FXAA,
        PostProcessMaterialAfterTonemapping,
        VisualizeDepthOfField,
        VisualizeStationaryLightOverlap,
        VisualizeLightCulling,
        SelectionOutline,
        EditorPrimitive,
        VisualizeShadingModels,
        VisualizeGBufferHints,
        VisualizeSubsurface,
        VisualizeGBufferOverview,
        VisualizeHDR,
        PixelInspector,
        HMDDistortion,
        HighResolutionScreenshotMask,
        PrimaryUpscale,
        SecondaryUpscale,
        MAX
    };

    // 포스트 프로세스의 Pass의 .
    const TCHAR* PassNames[] =
    {
        TEXT("MotionBlur"),
        TEXT("Tonemap"),
        TEXT("FXAA"),
        TEXT("PostProcessMaterial (AfterTonemapping)"),
        TEXT("VisualizeDepthOfField"),
        TEXT("VisualizeStationaryLightOverlap"),
        TEXT("VisualizeLightCulling"),
        TEXT("SelectionOutline"),
        TEXT("EditorPrimitive"),
        TEXT("VisualizeShadingModels"),
        TEXT("VisualizeGBufferHints"),
        TEXT("VisualizeSubsurface"),
        TEXT("VisualizeGBufferOverview"),
        TEXT("VisualizeHDR"),
        TEXT("PixelInspector"),
        TEXT("HMDDistortion"),
        TEXT("HighResolutionScreenshotMask"),
        TEXT("PrimaryUpscale"),
        TEXT("SecondaryUpscale")
    };

    // 에 따라 플래그 활성화(Enable) 또는 비활성화(Disable)의 포스트 프로세스 패스.
    TOverridePassSequence<EPass> PassSequence(ViewFamilyOutput);
    PassSequence.SetNames(PassNames, UE_ARRAY_COUNT(PassNames));
    PassSequence.SetEnabled(EPass::VisualizeStationaryLightOverlap, EngineShowFlags.StationaryLightOverlap);
    PassSequence.SetEnabled(EPass::VisualizeLightCulling, EngineShowFlags.VisualizeLightCulling);
    PassSequence.SetEnabled(EPass::SelectionOutline, false);
    PassSequence.SetEnabled(EPass::EditorPrimitive, false);

    PassSequence.SetEnabled(EPass::VisualizeShadingModels, EngineShowFlags.VisualizeShadingModels);
    PassSequence.SetEnabled(EPass::VisualizeGBufferHints, EngineShowFlags.GBufferHints);
    PassSequence.SetEnabled(EPass::VisualizeSubsurface, EngineShowFlags.VisualizeSSS);
    PassSequence.SetEnabled(EPass::VisualizeGBufferOverview, bVisualizeGBufferOverview || bVisualizeGBufferDumpToFile || bVisualizeGBufferDumpToPIpe);
    PassSequence.SetEnabled(EPass::VisualizeHDR, EngineShowFlags.VisualizeHDR);
    PassSequence.SetEnabled(EPass::PixelInspector, false);

    PassSequence.SetEnabled(EPass::HMDDistortion, EngineShowFlags.StereoRendering && EngineShowFlags.HMDDistortion);
    PassSequence.SetEnabled(EPass::HighResolutionScreenshotMask, IsHighResolutionScreenshotMaskEnabled(View));
    PassSequence.SetEnabled(EPass::PrimaryUpscale, PaniniConfig.IsEnabled() || (View.PrimaryScreenPercentageMethod == EPrimaryScreenPercentageMethod::SpatialUpscale && PrimaryViewRect.Size() != View.GetSecondaryViewRectSize()));
    PassSequence.SetEnabled(EPass::SecondaryUpscale, View.RequiresSecondaryUpscale());

    // 처리(Process)MotionBlur, Tonemap, PostProcessMaterialAfterTonemapping포스트 프로세스.
    if (IsPostProcessingEnabled(View))
    {
        // 페치/가져오기(Fetch)머티리얼(Material)입력.
        const auto GetPostProcessMaterialInputs = [CustomDepth, SeparateTranslucency, Velocity] (FScreenPassTexture InSceneColor)
        {
            FPostProcessMaterialInputs PostProcessMaterialInputs;
            PostProcessMaterialInputs.SetInput(EPostProcessMaterialInput::SceneColor, InSceneColor);
            PostProcessMaterialInputs.SetInput(EPostProcessMaterialInput::SeparateTranslucency, SeparateTranslucency);
            PostProcessMaterialInputs.SetInput(EPostProcessMaterialInput::Velocity, Velocity);
            PostProcessMaterialInputs.CustomDepthTexture = CustomDepth.Texture;
            return PostProcessMaterialInputs;
        };

        // 처리(Process)플래그 .
        const EStereoscopicPass StereoPass = View.StereoPass;
        const bool bPrimaryView = IStereoRendering::IsAPrimaryView(View);
        
        (......)

        // 활성화(Enable) 또는 비활성화(Disable)포스트 프로세스.
        PassSequence.SetEnabled(EPass::MotionBlur, bVisualizeMotionBlur || bMotionBlurEnabled);
        PassSequence.SetEnabled(EPass::Tonemap, bTonemapEnabled);
        PassSequence.SetEnabled(EPass::FXAA, AntiAliasingMethod == AAM_FXAA);
        PassSequence.SetEnabled(EPass::PostProcessMaterialAfterTonemapping, PostProcessMaterialAfterTonemappingChain.Num() != 0);
        PassSequence.SetEnabled(EPass::VisualizeDepthOfField, bVisualizeDepthOfField);
        PassSequence.Finalize();

        // 반투명(Translucent)블렌딩(Blending)씬 컬러(SceneColor)RT의 포스트 프로세스.
        {
            const FPostProcessMaterialChain MaterialChain = GetPostProcessMaterialChain(View, BL_BeforeTranslucency);

            if (MaterialChain.Num())
            {
                SceneColor = AddPostProcessMaterialChain(GraphBuilder, View, GetPostProcessMaterialInputs(SceneColor), MaterialChain);
            }
        }

        (......)

        // 톤매핑(Tonemapping)의 포스트 프로세스.
        {
            const FPostProcessMaterialChain MaterialChain = GetPostProcessMaterialChain(View, BL_BeforeTonemapping);

            if (MaterialChain.Num())
            {
                SceneColor = AddPostProcessMaterialChain(GraphBuilder, View, GetPostProcessMaterialInputs(SceneColor), MaterialChain);
            }
        }

        FScreenPassTexture HalfResolutionSceneColor;

        // Scene color view rectangle after temporal AA upscale to secondary screen percentage.
        FIntRect SecondaryViewRect = PrimaryViewRect;

        // 템포럴 안티앨리어싱(TAA)(TAA).
        if (AntiAliasingMethod == AAM_TemporalAA)
        {
            // Whether we allow the temporal AA pass to downsample scene color. It may choose not to based on internal context,
            // in which case the output half resolution texture will remain null.
            const bool bAllowSceneDownsample =
                IsTemporalAASceneDownsampleAllowed(View) &&
                // We can only merge if the normal downsample pass would happen immediately after.
                !bMotionBlurEnabled && !bVisualizeMotionBlur &&
                // TemporalAA is only able to match the low quality mode (box filter).
                GetDownsampleQuality() == EDownsampleQuality::Low;

            AddTemporalAAPass(
                GraphBuilder,
                SceneTextures,
                View,
                bAllowSceneDownsample,
                DownsampleOverrideFormat,
                SceneColor.Texture,
                &SceneColor.Texture,
                &SecondaryViewRect,
                &HalfResolutionSceneColor.Texture,
                &HalfResolutionSceneColor.ViewRect);
        }
        // SSR.
        else if (ShouldRenderScreenSpaceReflections(View))
        {
            // If we need SSR, and TAA is enabled, then AddTemporalAAPass() has already handled the scene history.
            // If we need SSR, and TAA is not enable, then we just need to extract the history.
            if (!View.bStatePrevViewInfoIsReadOnly)
            {
                check(View.ViewState);
                FTemporalAAHistory& OutputHistory = View.ViewState->PrevFrameViewInfo.TemporalAAHistory;
                GraphBuilder.QueueTextureExtraction(SceneColor.Texture, &OutputHistory.RT[0]);
            }
        }

        //! SceneColorTexture is now upsampled to the SecondaryViewRect. Use SecondaryViewRect for input / output.
        SceneColor.ViewRect = SecondaryViewRect;

        // 포스트 프로세스 머티리얼 - SSR입력.
        if (View.ViewState && !View.bStatePrevViewInfoIsReadOnly)
        {
            const FPostProcessMaterialChain MaterialChain = GetPostProcessMaterialChain(View, BL_SSRInput);

            if (MaterialChain.Num())
            {
                // Save off SSR post process output for the next frame.
                FScreenPassTexture PassOutput = AddPostProcessMaterialChain(GraphBuilder, View, GetPostProcessMaterialInputs(SceneColor), MaterialChain);
                GraphBuilder.QueueTextureExtraction(PassOutput.Texture, &View.ViewState->PrevFrameViewInfo.CustomSSRInput);
            }
        }

        // 모션 블러(Motion Blur).
        if (PassSequence.IsEnabled(EPass::MotionBlur))
        {
            FMotionBlurInputs PassInputs;
            PassSequence.AcceptOverrideIfLastPass(EPass::MotionBlur, PassInputs.OverrideOutput);
            PassInputs.SceneColor = SceneColor;
            PassInputs.SceneDepth = SceneDepth;
            PassInputs.SceneVelocity = Velocity;
            PassInputs.Quality = GetMotionBlurQuality();
            PassInputs.Filter = GetMotionBlurFilter();

            // Motion blur visualization replaces motion blur when enabled.
            if (bVisualizeMotionBlur)
            {
                SceneColor = AddVisualizeMotionBlurPass(GraphBuilder, View, PassInputs);
            }
            else
            {
                SceneColor = AddMotionBlurPass(GraphBuilder, View, PassInputs);
            }
        }

        (......)

        FScreenPassTexture Bloom;

        // 블룸(Bloom).
        if (bBloomEnabled)
        {
            FSceneDownsampleChain BloomDownsampleChain;

            FBloomInputs PassInputs;
            PassInputs.SceneColor = SceneColor;

            const bool bBloomThresholdEnabled = View.FinalPostProcessSettings.BloomThreshold > -1.0f;

            // Reuse the main scene downsample chain if a threshold isn't required for bloom.
            if (SceneDownsampleChain.IsInitialized() && !bBloomThresholdEnabled)
            {
                PassInputs.SceneDownsampleChain = &SceneDownsampleChain;
            }
            else
            {
                FScreenPassTexture DownsampleInput = HalfResolutionSceneColor;

                if (bBloomThresholdEnabled)
                {
                    const float BloomThreshold = View.FinalPostProcessSettings.BloomThreshold;

                    FBloomSetupInputs SetupPassInputs;
                    SetupPassInputs.SceneColor = DownsampleInput;
                    SetupPassInputs.EyeAdaptationTexture = EyeAdaptationTexture;
                    SetupPassInputs.Threshold = BloomThreshold;

                    DownsampleInput = AddBloomSetupPass(GraphBuilder, View, SetupPassInputs);
                }

                const bool bLogLumaInAlpha = false;
                BloomDownsampleChain.Init(GraphBuilder, View, EyeAdaptationParameters, DownsampleInput, DownsampleQuality, bLogLumaInAlpha);

                PassInputs.SceneDownsampleChain = &BloomDownsampleChain;
            }

            FBloomOutputs PassOutputs = AddBloomPass(GraphBuilder, View, PassInputs);
            SceneColor = PassOutputs.SceneColor;
            Bloom = PassOutputs.Bloom;

            FScreenPassTexture LensFlares = AddLensFlaresPass(GraphBuilder, View, Bloom, *PassInputs.SceneDownsampleChain);

            if (LensFlares.IsValid())
            {
                // Lens flares are composited with bloom.
                Bloom = LensFlares;
            }
        }

        // Tonemapper needs a valid bloom target, even if it's black.
        if (!Bloom.IsValid())
        {
            Bloom = BlackDummy;
        }

        SceneColorBeforeTonemap = SceneColor;

        // 톤매핑(Tonemapping).
        if (PassSequence.IsEnabled(EPass::Tonemap))
        {
            const FPostProcessMaterialChain MaterialChain = GetPostProcessMaterialChain(View, BL_ReplacingTonemapper);

            if (MaterialChain.Num())
            {
                const UMaterialInterface* HighestPriorityMaterial = MaterialChain[0];

                FPostProcessMaterialInputs PassInputs;
                PassSequence.AcceptOverrideIfLastPass(EPass::Tonemap, PassInputs.OverrideOutput);
                PassInputs.SetInput(EPostProcessMaterialInput::SceneColor, SceneColor);
                PassInputs.SetInput(EPostProcessMaterialInput::SeparateTranslucency, SeparateTranslucency);
                PassInputs.SetInput(EPostProcessMaterialInput::CombinedBloom, Bloom);
                PassInputs.CustomDepthTexture = CustomDepth.Texture;

                SceneColor = AddPostProcessMaterialPass(GraphBuilder, View, PassInputs, HighestPriorityMaterial);
            }
            else
            {
                FRDGTextureRef ColorGradingTexture = nullptr;

                if (bPrimaryView)
                {
                    ColorGradingTexture = AddCombineLUTPass(GraphBuilder, View);
                }
                // We can re-use the color grading texture from the primary view.
                else
                {
                    ColorGradingTexture = GraphBuilder.TryRegisterExternalTexture(View.GetTonemappingLUT());
                }

                FTonemapInputs PassInputs;
                PassSequence.AcceptOverrideIfLastPass(EPass::Tonemap, PassInputs.OverrideOutput);
                PassInputs.SceneColor = SceneColor;
                PassInputs.Bloom = Bloom;
                PassInputs.EyeAdaptationTexture = EyeAdaptationTexture;
                PassInputs.ColorGradingTexture = ColorGradingTexture;
                PassInputs.bWriteAlphaChannel = AntiAliasingMethod == AAM_FXAA || IsPostProcessingWithAlphaChannelSupported();
                PassInputs.bOutputInHDR = bTonemapOutputInHDR;

                SceneColor = AddTonemapPass(GraphBuilder, View, PassInputs);
            }
        }

        SceneColorAfterTonemap = SceneColor;

        // FXAA안티앨리어싱(Anti-Aliasing).
        if (PassSequence.IsEnabled(EPass::FXAA))
        {
            FFXAAInputs PassInputs;
            PassSequence.AcceptOverrideIfLastPass(EPass::FXAA, PassInputs.OverrideOutput);
            PassInputs.SceneColor = SceneColor;
            PassInputs.Quality = GetFXAAQuality();

            SceneColor = AddFXAAPass(GraphBuilder, View, PassInputs);
        }

        // 컬러 매핑 의 포스트 프로세스 머티리얼.
        if (PassSequence.IsEnabled(EPass::PostProcessMaterialAfterTonemapping))
        {
            FPostProcessMaterialInputs PassInputs = GetPostProcessMaterialInputs(SceneColor);
            PassSequence.AcceptOverrideIfLastPass(EPass::PostProcessMaterialAfterTonemapping, PassInputs.OverrideOutput);
            PassInputs.SetInput(EPostProcessMaterialInput::PreTonemapHDRColor, SceneColorBeforeTonemap);
            PassInputs.SetInput(EPostProcessMaterialInput::PostTonemapHDRColor, SceneColorAfterTonemap);

            SceneColor = AddPostProcessMaterialChain(GraphBuilder, View, PassInputs, PostProcessMaterialAfterTonemappingChain);
        }
    }
    // 의 반투명(Translucent)RT 및 보정(Correct).
    else
    {
        PassSequence.SetEnabled(EPass::MotionBlur, false);
        PassSequence.SetEnabled(EPass::Tonemap, true);
        PassSequence.SetEnabled(EPass::FXAA, false);
        PassSequence.SetEnabled(EPass::PostProcessMaterialAfterTonemapping, false);
        PassSequence.SetEnabled(EPass::VisualizeDepthOfField, false);
        PassSequence.Finalize();

        SceneColor.Texture = AddSeparateTranslucencyCompositionPass(GraphBuilder, View, SceneColor.Texture, SeparateTranslucency.Texture, SeparateModulation.Texture);

        SceneColorBeforeTonemap = SceneColor;

        if (PassSequence.IsEnabled(EPass::Tonemap))
        {
            FTonemapInputs PassInputs;
            PassSequence.AcceptOverrideIfLastPass(EPass::Tonemap, PassInputs.OverrideOutput);
            PassInputs.SceneColor = SceneColor;
            PassInputs.bOutputInHDR = bViewFamilyOutputInHDR;
            PassInputs.bGammaOnly = true;

            SceneColor = AddTonemapPass(GraphBuilder, View, PassInputs);
        }

        SceneColorAfterTonemap = SceneColor;
    }

    (......)

    // RT메인 업스케일링(Main Upscale).
    if (PassSequence.IsEnabled(EPass::PrimaryUpscale))
    {
        FUpscaleInputs PassInputs;
        PassSequence.AcceptOverrideIfLastPass(EPass::PrimaryUpscale, PassInputs.OverrideOutput);
        PassInputs.SceneColor = SceneColor;
        PassInputs.Method = GetUpscaleMethod();
        PassInputs.Stage = PassSequence.IsEnabled(EPass::SecondaryUpscale) ? EUpscaleStage::PrimaryToSecondary : EUpscaleStage::PrimaryToOutput;

        // Panini projection is handled by the primary upscale pass.
        PassInputs.PaniniConfig = PaniniConfig;

        SceneColor = AddUpscalePass(GraphBuilder, View, PassInputs);
    }

    // RT보조 업스케일링(Secondary Upscale).
    if (PassSequence.IsEnabled(EPass::SecondaryUpscale))
    {
        FUpscaleInputs PassInputs;
        PassSequence.AcceptOverrideIfLastPass(EPass::SecondaryUpscale, PassInputs.OverrideOutput);
        PassInputs.SceneColor = SceneColor;
        PassInputs.Method = View.Family->SecondaryScreenPercentageMethod == ESecondaryScreenPercentageMethod::LowerPixelDensitySimulation ? EUpscaleMethod::SmoothStep : EUpscaleMethod::Nearest;
        PassInputs.Stage = EUpscaleStage::SecondaryToOutput;

        SceneColor = AddUpscalePass(GraphBuilder, View, PassInputs);
    }
}

UE의 모든 후처리는 EPass에 지정되어 있으며 일부 맞춤형 포스트 프로세스 자료도 있습니다. 포스트 프로세스 재료는 렌더링 단계에 따라 다음과 같은 유형으로 나눌 수 있습니다.

cpp
enum EBlendableLocation
{
    // 톤매핑(Tonemapping).
    BL_AfterTonemapping,
    // 톤매핑(Tonemapping).
    BL_BeforeTonemapping,
    // 반투명(Translucent).
    BL_BeforeTranslucency,
    // 톤매핑(Tonemapping).
    BL_ReplacingTonemapper,
    // SSR입력.
    BL_SSRInput,

    BL_MAX,
};

EBlendableLocation은 재질 편집기의 속성 패널에서 지정할 수 있습니다.

기본 혼합 단계는 톤매핑 후입니다. 렌더러에 포스트 프로세스 패스를 추가하는 방법을 더 잘 설명하기 위해 톤매핑을 추가하는 예를 들어 보겠습니다.

cpp
// Engine\Source\Runtime\Renderer\Private\PostProcess\PostProcessing.cpp

static FRCPassPostProcessTonemap* AddTonemapper(
    FPostprocessContext& Context,
    const FRenderingCompositeOutputRef& BloomOutputCombined,
    const FRenderingCompositeOutputRef& EyeAdaptation,
    const EAutoExposureMethod& EyeAdapationMethodId,
    const bool bDoGammaOnly,
    const bool bHDRTonemapperOutput,
    const bool bMetalMSAAHDRDecode,
    const bool bIsMobileDof)
{
    const FViewInfo& View = Context.View;
    const EStereoscopicPass StereoPass = View.StereoPass;

    FRenderingCompositeOutputRef TonemapperCombinedLUTOutputRef;
    // LUT Pass.
    if (IStereoRendering::IsAPrimaryView(View))
    {
        TonemapperCombinedLUTOutputRef = AddCombineLUTPass(Context.Graph);
    }

    const bool bDoEyeAdaptation = IsAutoExposureMethodSupported(View.GetFeatureLevel(), EyeAdapationMethodId) && EyeAdaptation.IsValid();
    // Context.Graph개 Pass, 객체/오브젝트 타입/유형 는 FRCPassPostProcessTonemap.
    FRCPassPostProcessTonemap* PostProcessTonemap = Context.Graph.RegisterPass(new(FMemStack::Get()) FRCPassPostProcessTonemap(bDoGammaOnly, bDoEyeAdaptation, bHDRTonemapperOutput, bMetalMSAAHDRDecode, bIsMobileDof));

    // 설정(Set)입력텍스처(Texture).
    PostProcessTonemap->SetInput(ePId_Input0, Context.FinalOutput);
    PostProcessTonemap->SetInput(ePId_Input1, BloomOutputCombined);
    PostProcessTonemap->SetInput(ePId_Input2, EyeAdaptation);
    PostProcessTonemap->SetInput(ePId_Input3, TonemapperCombinedLUTOutputRef);

    // 설정(Set)출력.
    Context.FinalOutput = FRenderingCompositeOutputRef(PostProcessTonemap);

    return PostProcessTonemap;
}

포스트 프로세싱 유형이 너무 많고 관련된 내부 메커니즘과 세부 사항도 많기 때문에 이 섹션에서는 포스트 프로세싱 렌더링 프로세스에 대한 일반적인 이해만 제공합니다. 나중에 그 원리와 메커니즘을 자세히 설명하는 전용 장이 있을 것입니다.

4.4 이 기사 요약

4.4.1 디퍼드 렌더링 요약

이 문서에서는 디퍼드 렌더링 파이프라인과 그 변형에 중점을 두고 해당 원리, 구현 및 변형을 설명합니다. 그런 다음 UE의 FDeferredShadingSceneRenderer::Render를 사용하여 디퍼드 셰이딩 씬 렌더러의 주요 단계와 프로세스를 설명합니다. 이를 통해 UE를 처음 접하는 사람들이 렌더링 프로세스에 대한 기본 이해와 인상을 가질 수 있습니다.

물론 이 글이 모든 렌더링 기술적인 세부사항을 분석할 수는 없습니다. 이는 단지 출발점 역할을 할 뿐입니다. UE의 소스 코드에서 더 많은 원리, 메커니즘 및 세부 사항을 독자가 직접 연구해야 합니다.

4.4.2 디퍼드 렌더링의 미래

하드웨어 기술과 소프트웨어 기술의 발전으로 UE의 디퍼드 렌더링 파이프라인은 PC 플랫폼에서 계속해서 그 역할을 수행할 것이며 모바일 플랫폼에서도 점진적으로 지원될 것입니다. 지난 UOD 공식 연설에서는 다음 버전의 UE가 모바일 플랫폼의 디퍼드 렌더링 파이프라인을 지원할 것으로 예상되었습니다.

또한, 디퍼드 렌더링 변형 기술의 개발로 상용 게임 엔진에 보다 효율적이고 고성능인 알고리즘과 엔지니어링 기술이 도입될 예정이며, 최신 경량 그래픽 API, RDG, GPU 기반 파이프라인 및 기타 기술이 결합되어 더욱 눈부시게 빛날 것입니다.

4.4.3 이 기사에 대한 생각

또한 이 문서에서는 UE 디퍼드 렌더링 파이프라인에 대한 숙달과 이해를 심화하고 이해하는 데 도움이 되는 몇 가지 작은 생각을 제시합니다.

  • 디퍼드 렌더링의 구체적인 프로세스에 대해 설명해주세요.

  • 디퍼드 렌더링의 변형에는 어떤 것이 있나요? 각각의 특징은 무엇입니까?

  • FDeferredShadingSceneRenderer::Render의 주요 단계를 간략하게 설명해주세요.

  • 카메라에 결합된 광원을 시뮬레이션하기 위해 뷰의 시선과 동일한 평행 조명 조명을 구현합니다.

특별 지침

  • 모든 참고문헌의 저자에게 감사드립니다. 일부 사진은 참고 자료와 인터넷에서 가져온 것이므로 삭제되었습니다.

  • 이 시리즈의 기사는 저자가 직접 작성한 것이며 블로그에만 게시됩니다. 이 글의 링크를 공유하셔도 좋지만, 무단 전재를 금합니다!

  • 계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.

  • 계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.

  • 계속되는 일련의 기사. 전체 목록을 보려면 콘텐츠 개요를 클릭하세요.








📚 참고 자료 및 공식 링크 (References)


🔗 연관 지식 베이스 (Wiki & Tools)

Based on the legendary technical series by Timlly (0向往0 / 毛星云)