Skip to content

언리얼 렌더링 시스템 분석(12) - 모바일 단말 특별 주제 1부(UE 모바일 단말 렌더링 분석)

🌐 원문 링크: 剖析虚幻渲染体系(12)- 移动端专题Part 1(UE移动端渲染分析) (cnblogs.com/timlly)
📅 원문 발행일: 2021-11-05 | ✍️ 저자: Timlly (00 / )

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


이전 장에서는 모두 PC 측 디퍼드 렌더링 파이프라인을 기반으로 한 UE의 렌더링 시스템을 설명했으며, 특히 Unreal Rendering System(04) 분석 - 디퍼드 렌더링 파이프라인에서는 PC 측 디퍼드 렌더링 파이프라인의 프로세스와 단계를 자세히 설명했습니다.

이 기사에서는 UE 모바일 측의 렌더링 파이프라인에 대해서만 설명하고 마지막으로 모바일과 PC 간의 렌더링 차이점과 특수 최적화 조치를 비교합니다. 이 문서에서는 UE 렌더링 시스템의 다음 내용을 주로 설명합니다.

  • FMobileSceneRenderer의 주요 프로세스 및 단계.

  • 모바일용 순방향 및 디퍼드 렌더링 파이프라인.

  • 모바일을 위한 빛과 그림자.

  • 모바일 단말기와 PC 단말기의 유사점과 차이점, 그리고 관련된 특별한 최적화 기술.

특히 본 글에서 분석한 UE 소스코드가 4.27.1로 업그레이드 되었다는 점에 주목해야 한다. 소스코드를 동시에 읽어야 하는 학생들은 업데이트에 주의하시기 바랍니다.

PC의 UE 에디터에서 모바일 렌더링 파이프라인을 활성화하려면 아래와 같이 메뉴를 선택하면 됩니다:

셰이더 컴파일이 완료될 때까지 기다리면 모바일 단말기의 미리보기 효과가 UE 편집기의 뷰포트에 표시됩니다.

12.1.1 모바일 장치의 특성

PC 데스크톱 플랫폼과 비교할 때 모바일 단말기는 크기, 배터리 용량, 하드웨어 성능 및 기타 여러 측면에서 상당한 차이가 있습니다. 구체적으로는 다음과 같습니다.

  • **더 작은 크기.**모바일 단말기의 휴대성을 위해서는 전체 장치가 가벼워야 하고 손바닥이나 주머니에 넣을 수 있어야 하므로 전체 장치는 아주 작은 크기로만 제한될 수 있습니다.

  • **에너지와 전력이 제한되어 있습니다.**배터리 저장 기술의 한계로 인해 현재 주류 리튬 배터리는 일반적으로 10,000mAh이지만 모바일 장치의 해상도와 이미지 품질은 점점 더 높아지고 있습니다. 충분히 긴 배터리 수명과 열 방출 제약 조건을 충족하려면 모바일 장치의 전체 전력을 일반적으로 5W 이내로 엄격하게 제어해야 합니다.

  • **열 방출 방법은 제한되어 있습니다.**PC 장치에는 일반적으로 냉각 팬이나 수냉 시스템이 장착될 수 있지만 모바일 장치에는 이러한 능동 냉각 방법이 없으며 열 방출을 위해 열 전도에만 의존할 수 있습니다. 열 방출이 부적절할 경우 CPU와 GPU는 과열로 인해 장비 구성 요소가 손상되는 것을 방지하기 위해 주파수를 적극적으로 줄이고 매우 제한된 성능으로 실행됩니다.

  • **하드웨어 성능이 제한되어 있습니다.**모바일 기기의 다양한 구성요소(CPU, 대역폭, 메모리, GPU 등)의 성능은 PC 기기의 10분의 1에 불과합니다.

2018년, 주류 PC 기기(NV GV100-400-A1 Titan V)와 주류 모바일 기기(Samsung Exynos 9 8895)의 성능 비교 차트. 많은 모바일 장치의 하드웨어 성능은 PC 장치의 10분의 1에 불과하지만 해상도는 PC의 절반에 가깝습니다. 이는 모바일 장치의 과제와 딜레마를 더욱 부각시킵니다.

2020년까지 주류 모바일 기기의 성능은 다음과 같습니다.

  • **특수 하드웨어 아키텍처.**예를 들어 CPU와 GPU는 결합 아키텍처라고 불리는 메모리 저장 장치를 공유하며, GPU의 TB(D)R 아키텍처도 있습니다. 그 목적은 낮은 전력 소비 내에서 가능한 한 많은 작업을 완료하는 것입니다.

PC 기기의 분리된 하드웨어 아키텍처와 모바일 기기의 결합된 하드웨어 아키텍처를 비교합니다.

또한 순전히 컴퓨팅 성능만을 추구하는 PC 측 CPU, GPU와 달리 모바일 단말의 성능을 측정하는 지표는 성능(Performance), 에너지(Power), 면적(Area), 흔히 PPA라고 불리는 3가지 지표가 있습니다. (아래 사진)

모바일 장치 측정을 위한 세 가지 기본 매개변수: 성능, 면적, 전력. 컴퓨팅 밀도는 성능과 면적을 의미하며, 에너지 소비율은 성능과 용량 소비를 의미합니다. 클수록 좋습니다.

모바일 기기와 함께 모바일 기기의 중요한 개발 분야인 XR 기기가 떠오르고 있습니다. 현재 다양한 크기, 기능 및 응용 시나리오를 갖춘 다양한 XR 장치가 있습니다.

다양한 형태의 XR 장비.

최근 Metaverse의 폭발적인 증가와 FaceBook의 이름이 Meta로 변경됨에 따라 Apple, Microsoft, NVidia, Google과 같은 거대 기술 기업들은 미래 지향적인 몰입형 경험을 제공하기 위한 노력을 강화하고 있습니다.

12.2 UE 모바일 렌더링 기능

이 장에서는 모바일 단말에서 UE4.27의 렌더링 기능을 설명합니다.

12.2.1 기능 수준

UE는 모바일 단말에서 다음 그래픽 API를 지원합니다.

기능 수준 설명

OpenGL ES 3.1 Android 시스템의 기본 기능 수준인 특정 머티리얼 매개변수는 프로젝트 설정(프로젝트 설정 > 플랫폼 > Android 머티리얼 품질 - ES31)에서 구성할 수 있습니다.

Android Vulkan 일부 특정 Android 장치에 사용할 수 있는 고급 렌더러는 Vulkan 1.2 API를 지원하며 Vulkan의 경량 설계 개념은 대부분의 경우 OpenGL보다 더 효율적입니다.

메탈 2.0 iOS 기기 전용 기능 수준입니다. 머티리얼 매개변수는 프로젝트 설정 > 플랫폼 > iOS 머티리얼 품질에서 구성할 수 있습니다.

현재 주류 Android 기기에서는 Vulkan을 사용하면 더 나은 성능을 얻을 수 있습니다. 그 이유는 Vulkan의 경량 설계 개념을 통해 UE와 같은 애플리케이션이 보다 정확하게 최적화를 수행할 수 있기 때문입니다. 다음은 Vulkan과 OpenGL의 비교표입니다.

불칸 OpenGL

객체 기반 상태, 전역 상태 없음. 단일 전역 상태 머신.

모든 상태 개념은 명령 버퍼에 배치됩니다. 상태는 단일 컨텍스트에 바인딩됩니다.

멀티스레드 인코딩이 가능합니다. 렌더링 작업은 순차적으로만 수행할 수 있습니다.

GPU 메모리 및 동기화를 정확하고 명시적으로 조작할 수 있습니다. GPU 메모리 및 동기화 세부 정보는 드라이버에 의해 숨겨지는 경우가 많습니다.

드라이버에는 런타임 오류 감지 기능이 없지만 개발자를 위한 유효성 검사 계층이 있습니다. 광범위한 런타임 오류 감지.

Windows 플랫폼을 사용하는 경우 UE 에디터는 OpenGL, Vulkan, Metal 시뮬레이터를 시작하여 에디터에서 효과를 미리 볼 수도 있지만, 실제 실행 중인 디바이스 화면과 다를 수 있으므로 이 기능에 전적으로 의존할 수는 없습니다.

Vulkan을 켜기 전에 프로젝트에서 일부 매개변수를 구성해야 합니다. 자세한 내용은 Android Vulkan Mobile Renderer 공식 문서를 참조하세요.

또한 UE는 이전 버전의 창에서 OpenGL 지원을 제거했습니다. 현재 UE 에디터에는 OpenGL 시뮬레이션 옵션이 있지만, 맨 아래 레이어는 실제로 D3D를 사용하여 렌더링됩니다.

12.2.2 디퍼드 렌더링

UE의 디퍼드 셰이딩(Deferred Shading)은 4.26에 추가된 기능으로, 개발자는 고품질 반사, 다중 동적 조명, 데칼, 고급 조명 기능 등 모바일 단말에서 더욱 복잡한 조명 및 그림자 효과를 얻을 수 있습니다.

상단: 포워드 렌더링; 하단: 디퍼드 렌더링(Deferred Shading).

모바일 측에서 디퍼드 렌더링을 활성화하려면 프로젝트 구성 디렉터리의 DefaultEngine.ini에 r.Mobile.ShadingPath=1 필드를 추가한 다음 편집기를 다시 시작해야 합니다.

12.2.3 Ground Truth 앰비언트 오클루전(AO)

**GTAO(Ground Truth Ambient Occlusion)**는 실제 세계에 가까운 환경 폐색 기술입니다. 이는 그림자에 대한 보상이며 간접 조명의 일부를 차단하여 좋은 부드러운 그림자 효과를 얻을 수 있습니다.

GTAO 효과가 켜져 있습니다. 로봇이 벽에 가까우면 벽에 그라데이션과 함께 부드러운 그림자 효과가 남습니다.

GTAO를 활성화하려면 아래 표시된 옵션을 확인해야 합니다.

또한 GTAO는 모바일 HDR 옵션을 사용합니다. 해당 대상 장치에서 이를 활성화하려면 r.Mobile.AmbientOcclusionQuality 필드를 [Platform]Scalability.ini 구성에 추가해야 하며 값은 0보다 커야 합니다. 그렇지 않으면 GTAO가 비활성화됩니다.

GTAO의 최대 Compute Shader 스레드 수가 1024개 미만이기 때문에 Mali 장치에서 성능 문제가 있다는 점은 주목할 가치가 있습니다.

12.2.4 동적 조명 및 그림자

UE가 이동 단말기에서 구현하는 광원 특성은 다음과 같습니다.

  • 선형 공간의 HDR 조명.

  • 방향성 라이트맵(노멀 고려)

  • 태양(방향성 조명)은 거리 필드 그림자 + ​​반사 하이라이트를 해결합니다.

  • IBL 조명: 각 객체는 시차 보정 없이 가장 가까운 반사 캐쳐를 샘플링합니다.

  • 동적 객체는 조명을 올바르게 수신하고 그림자를 투사할 수 있습니다.

UE 모바일 단말이 지원하는 동적 광원의 유형, 수량, 그림자 및 기타 정보는 다음과 같습니다.

광원 유형 최대 수량 그림자 설명

평행광 1 CSM CSM은 기본적으로 레벨 2로 설정되어 있으며 최대 레벨 4까지 지원합니다.

포인트 라이트 4 지원되지 않음 포인트 라이트 섀도우에는 큐브 섀도우 맵이 필요하며, 단일 패스 렌더링 큐브 섀도우(OnePassPointLightShadow) 기술을 지원하려면 GS(SM5에서만 사용 가능)가 필요합니다.

스포트라이트 4 지원하다 기본적으로 비활성화되어 있으며 프로젝트에서 활성화해야 합니다.

지역 조명 0 지원되지 않음 동적 영역 조명 효과는 현재 지원되지 않습니다.

동적 스포트라이트는 프로젝트 구성에서 명시적으로 켜야 합니다.

모바일 BasePass의 픽셀 셰이더에서 스포트라이트 섀도우 맵은 CSM과 동일한 텍스처 샘플러를 공유하며, 스포트라이트 섀도우와 CSM은 동일한 섀도우 맵 아틀라스를 사용합니다. CSM은 충분한 공간이 있는지 확인하고 스포트라이트는 그림자 해상도에 따라 정렬됩니다.

기본적으로 표시되는 그림자의 최대 개수는 8개로 제한되어 있지만 r.Mobile.MaxVisibleMovableSpotLightsShadow 값을 변경하여 상한을 변경할 수 있습니다. 스포트라이트 그림자 해상도는 화면 크기와 r.Shadow.TexelsPerPixelSpotlight를 기반으로 합니다.

포워드 렌더링 경로에서 로컬 라이트(포인트 라이트 및 스포트라이트)의 총 개수는 4개를 초과할 수 없습니다.

모바일 버전은 고정 방향 조명에만 사용할 수 있는 Modulated Shadows라는 특수 그림자 모드도 지원합니다. 변조된 그림자를 켜는 효과는 다음과 같습니다.

그림자 변조는 그림자 색상 및 혼합 비율 변경도 지원합니다.

왼쪽: 동적 그림자; 오른쪽: 변조된 그림자.

모바일 측의 섀도우는 셀프 섀도우, 섀도우 품질 수준(r.shadowquality), 깊이 오프셋과 같은 매개변수 설정도 지원합니다.

또한 모바일 단말기는 기본적으로 GGX의 스페큘러 반사를 사용합니다. 기존의 반사 셰이딩 모델로 전환하려면 다음 구성에서 수정할 수 있습니다.

12.2.5 픽셀 투영 반사

UE는 **PPR(Pixel Projected Reflection)**이라는 모바일 단말에 최적화된 SSR 버전을 만들었습니다. 이는 화면 공간 픽셀을 재사용하는 핵심 아이디어이기도 합니다.

PPR 렌더링.

PPR 효과를 활성화하려면 다음 조건이 충족되어야 합니다.

  • MobileHDR 옵션을 켜세요.

  • r.Mobile.PixelProjectedReflectionQuality 값이 0보다 큽니다.

  • 프로젝트 설정 > 모바일을 설정하고 평면 반사 모드를 올바른 모드로 설정합니다.

평면 반사 모드에는 3가지 옵션이 있습니다.

보통: 평면 반사 액터는 모든 플랫폼에서 동일하게 작동합니다.

  • MobilePPR: 평면 반사 액터는 PC/콘솔 플랫폼에서 잘 작동하지만 모바일 플랫폼에서는 PPR 렌더링을 사용합니다.

  • MobilePPRExclusive: 플레이너 리플렉션 액터는 모바일 플랫폼의 PPR에만 사용되므로 PC 및 콘솔 프로젝트에서 기존 SSR을 사용할 수 있는 여지가 남습니다.

기본적으로 고급형 모바일 기기만 [Project]Scalability.ini에서 r.Mobile.PixelProjectedReflectionQuality를 활성화합니다.

12.2.6 메시 자동 인스턴스화

PC 측의 그리드 그리기 파이프라인은 이미 자동화된 인스턴스화 및 그리드 병합 그리기를 지원합니다. 이 기능은 렌더링 성능을 크게 향상시킬 수 있습니다. 4.27에서는 이미 모바일에서 이 기능을 지원합니다.

이를 활성화하려면 프로젝트 구성 디렉터리에서 DefaultEngine.ini를 열고 다음 필드를 추가해야 합니다:

cpp
r.Mobile.SupportGPUScene=1
r.Mobile.UseGPUSceneTexture=1

편집기를 다시 시작하고 셰이더가 컴파일되어 효과를 미리 볼 때까지 기다립니다.

GPUSceneTexture 지원이 필요하고 Mali 장치의 최대 균일 버퍼가 64kb에 불과하므로 충분한 공간을 지원할 수 없습니다. 따라서 Mali 장치는 버퍼 대신 텍스처를 사용하여 GPUScene 데이터를 저장합니다.

그러나 몇 가지 제한 사항이 있습니다.

  • 모바일 장치의 자동 인스턴스화는 주로 GPU 집약적인 프로젝트보다는 CPU 집약적인 프로젝트에 도움이 됩니다. 자동 인스턴스화를 활성화한다고 해서 GPU 집약적인 프로젝트가 손상될 가능성은 거의 없지만 이를 사용해도 성능이 크게 향상될 가능성은 거의 없습니다.

  • 게임이나 앱에 많은 메모리가 필요한 경우 r.Mobile.UseGPUSceneTexture를 끄고 버퍼를 사용하는 것이 Mali 장치에서 제대로 실행되지 않으므로 더 유리할 수 있습니다.

Mali 장치에서는 r.Mobile.UseGPUSceneTexture를 끌 수도 있지만, 다른 GPU 제조업체의 장치는 정상적으로 사용할 수 있습니다.

자동 인스턴스화의 효율성은 프로젝트의 정확한 사양과 위치에 따라 크게 달라집니다. 따라서 자동 인스턴스화가 활성화된 빌드를 생성하고 프로파일링하여 상당한 성능 향상이 있는지 확인하는 것이 좋습니다.

12.2.7 사후 처리

모바일 장치에는 느린 종속 텍스처 읽기, 제한된 하드웨어 기능, 특수 하드웨어 아키텍처, 추가 렌더링 대상 구문 분석, 제한된 대역폭 및 기타 제한 요소가 있기 때문에 사후 처리는 모바일 장치에서 실행될 때 성능 집약적이며 일부 극단적인 경우 렌더링 파이프라인이 중단될 수 있습니다.

그럼에도 불구하고, 높은 이미지 품질을 요구하는 일부 게임이나 애플리케이션은 여전히 ​​높은 품질을 달성하기 위해 여러 단계를 거쳐 강력한 포스트 프로세스 성능에 크게 의존하고 있습니다. UE는 개발자의 포스트 프로세스 사용을 제한하지 않습니다.

후처리를 활성화하려면 먼저 MobileHDR 옵션을 활성화해야 합니다.

포스트 프로세싱을 활성화한 후 포스트 프로세스 볼륨에서 다양한 포스트 프로세싱 효과를 설정할 수 있습니다.

모바일 단말에서 지원 가능한 후처리에는 Mobile Tonemapper, Color Grading, Lens, Bloom, Dirt Mask, Auto Exposure, Lens Flares, Depth of Field 등이 있습니다.

더 나은 성능을 얻으려면 모바일 단말기에서만 Bloom과 TAA를 활성화하는 것이 공식적인 권장 사항입니다.

12.2.8 기타 기능 및 제한 사항

  • 반사 캡처 압축

모바일 단말기는 반사 캡처 구성 요소의 압축을 지원하여 반사 캡처가 실행 중일 때 메모리와 대역폭을 줄이고 렌더링 효율성을 향상시킬 수 있습니다. 프로젝트 구성에서 활성화해야 합니다.

활성화되면 기본적으로 ETC2가 압축에 사용됩니다. 게다가 각 리플렉션 캡처 컴포넌트에 대해서도 조정할 수 있습니다:

  • 재료 특성

모바일 플랫폼(기능 수준 Open ES 3.1)의 자료는 다른 플랫폼과 동일한 노드 기반 생성 프로세스를 사용하며 대부분의 노드가 모바일에서 지원됩니다.

모바일 플랫폼에서 지원하는 머티리얼 속성은 BaseColor, Roughness, Metallic, Specular, Normal, Emissive, Refraction입니다. 그러나 장면 색상 표현식, 테셀레이션 입력 및 하위 표면 산란 셰이딩 모델은 지원하지 않습니다.

모바일 플랫폼에서 지원하는 자료에는 몇 가지 제한 사항이 있습니다.

  • 하드웨어 제한으로 인해 텍스처 샘플러는 16개만 사용할 수 있습니다.

  • DefaultLit 및 Unlit 셰이딩 모델만 사용할 수 있습니다.

  • 텍스처 읽기(텍스처 UV가 없는 수학적 계산)에 의존하지 않으려면 사용자 정의 UV를 사용해야 합니다.

  • 반투명 및 마스크드 재질은 성능 집약적입니다. 최대한 불투명한 재질을 사용하는 것이 좋습니다.

  • 깊이 페이드는 iOS 플랫폼의 반투명 재질에 사용할 수 있지만 하드웨어가 깊이 버퍼에서 데이터 가져오기를 지원하지 않아 허용할 수 없는 성능 비용이 발생하는 플랫폼에서는 지원되지 않습니다.

재료 속성 패널에는 모바일 단말기를 위한 몇 가지 특별한 옵션이 있습니다.

이러한 속성에 대한 설명은 다음과 같습니다.

  • 모바일 별도 반투명: 모바일 측에서 별도의 트랜스루센트 렌더링 텍스처를 활성화할지 여부입니다.

  • 전체 정밀도 사용: 전체 정밀도를 사용할지 여부입니다. 그렇지 않으면 대역폭 사용량과 에너지 소비가 줄어들고 성능이 향상될 수 있지만 멀리 있는 물체에 결함이 나타날 수 있습니다.

왼쪽: 완전 정밀 소재; 오른쪽: 멀리서 햇빛에 결함이 있는 반정밀 재료.

  • 라이트맵 방향성 사용: 라이트맵의 방향성을 활성화할지 여부입니다. 선택하면 라이트맵의 방향과 픽셀 법선이 고려되지만 성능 소모가 증가합니다.

  • 포괄 범위에 알파 사용: 마스크된 재질에 대해 MSAA 앤티앨리어싱을 활성화할지 여부입니다. 선택하면 MSAA가 활성화됩니다.

  • 완전히 거친: 완전히 거친지 여부입니다. 선택하면 이 재질의 렌더링 효율성이 크게 향상됩니다.

또한 모바일 단말기에서 지원하는 그리드 유형은 다음과 같습니다.

  • 스켈레탈 메시

  • 정적 메시

  • 풍경

  • CPU 입자 스프라이트, 입자 메시

위에 나열된 유형 이외의 유형은 지원되지 않습니다. 기타 제한 사항은 다음과 같습니다.

  • 버텍스 인덱스가 16비트에 불과하기 때문에 단일 메쉬는 최대 65k까지만 가능합니다.

  • 단일 스켈레탈 메시의 본 수는 하드웨어 성능에 따라 제한되므로 75개 이내여야 합니다.

12.3 FMobileSceneRenderer

FMobileSceneRenderer는 모바일 측의 씬 렌더링 프로세스를 담당하는 FSceneRenderer를 상속하고, PC 측 역시 FSceneRenderer를 상속하는 FDeferredShadingSceneRenderer입니다. 상속 관계 다이어그램은 다음과 같습니다.

클래스 다이어그램-v2

cpp
      FSceneRenderer <|-- FMobileSceneRenderer
      FSceneRenderer <|-- FDeferredShadingSceneRenderer

FDeferredShadingSceneRenderer는 앞서 언급한 많은 글에서 언급되었습니다. 복잡한 조명 및 렌더링 단계를 포함하여 렌더링 프로세스가 특히 복잡합니다. 이에 비해 FMobileSceneRenderer의 로직과 단계는 훨씬 간단합니다. 다음은 RenderDoc의 스크린샷입니다.

위의 내용에는 주로 InitViews, ShadowDepths, PrePass, BasePass, OcclusionTest, ShadowProjectionOnOpaque, Translucency, PostProcessing 및 기타 단계가 포함됩니다. 이러한 단계는 모두 PC 측에 존재하지만 구현 프로세스는 다를 수 있습니다. 다음 장의 분석을 참조하세요.

12.3.1 렌더러 주요 프로세스

모바일 씬 렌더러의 주요 프로세스는 FMobileSceneRenderer::Render에서도 발생합니다. 코드와 분석은 다음과 같습니다.

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

void FMobileSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
    // 갱신(Update)정보 。
    Scene->UpdateAllPrimitiveSceneInfos(RHICmdList);

    // 뷰(View)의 렌더링(Render).
    PrepareViewRectsForRendering(RHICmdList);

    // 의 데이터
    if (ShouldRenderSkyAtmosphere(Scene, ViewFamily.EngineShowFlags))
    {
        for (int32 LightIndex = 0; LightIndex < NUM_ATMOSPHERE_LIGHTS; ++LightIndex)
        {
            if (Scene->AtmosphereLights[LightIndex])
            {
                PrepareSunLightProxy(*Scene->GetSkyAtmosphereSceneInfo(), LightIndex, *Scene->AtmosphereLights[LightIndex]);
            }
        }
    }
    else
    {
        Scene->ResetAtmosphereLightsProperties();
    }

    if(!ViewFamily.EngineShowFlags.Rendering)
    {
        return;
    }

    // 차폐/오클루전 컬링 .
    WaitOcclusionTests(RHICmdList);
    FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
    RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);

    // 초기화(Initialize)뷰(View), 탐색/조회(Find/Lookup), 렌더링(Render)의 RT 및 버퍼 데이터 .
    InitViews(RHICmdList);
    
    if (GRHINeedsExtraDeletionLatency || !GRHICommandList.Bypass())
    {
        QUICK_SCOPE_CYCLE_COUNTER(STAT_FMobileSceneRenderer_PostInitViewsFlushDel);

        // 차폐/오클루전 ,따라서 RHI스레드(Thread) 및 GPU. ,실행(Execute)RHI스레드(Thread),는 처리(Process)일시 중지 의 위치 .
        FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
        FRHICommandListExecutor::GetImmediateCommandList().ImmediateFlush(EImmediateFlushType::FlushRHIThreadFlushResources);
    }

    GEngine->GetPreRenderDelegate().Broadcast();

    // 렌더링(Render)글로벌 동적 버퍼 .
    DynamicIndexBuffer.Commit();
    DynamicVertexBuffer.Commit();
    DynamicReadBuffer.Commit();
    RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);

    RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_SceneSim));

    if (ViewFamily.bLateLatchingEnabled)
    {
        BeginLateLatching(RHICmdList);
    }

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

    // 처리(Process)텍스처(Texture)
    if (bUseVirtualTexturing)
    {
        SCOPED_GPU_STAT(RHICmdList, VirtualTextureUpdate);
        
        FVirtualTextureSystem::Get().Update(RHICmdList, FeatureLevel, Scene);
        
        // Clear virtual texture feedback to default value
        FUnorderedAccessViewRHIRef FeedbackUAV = SceneContext.GetVirtualTextureFeedbackUAV();
        RHICmdList.Transition(FRHITransitionInfo(FeedbackUAV, ERHIAccess::SRVMask, ERHIAccess::UAVMask));
        RHICmdList.ClearUAVUint(FeedbackUAV, FUintVector4(~0u, ~0u, ~0u, ~0u));
        RHICmdList.Transition(FRHITransitionInfo(FeedbackUAV, ERHIAccess::UAVMask, ERHIAccess::UAVMask));
        RHICmdList.BeginUAVOverlap(FeedbackUAV);
    }
    
    // 정렬 의 라이트(Light)정보 .
    FSortedLightSetSceneInfo SortedLightSet;
    
    // 레이턴시 렌더링(Render).
    if (bDeferredShading)
    {
        // 및 정렬 라이트(Light).
        GatherAndSortLights(SortedLightSet);
        
        int32 NumReflectionCaptures = Views[0].NumBoxReflectionCaptures + Views[0].NumSphereReflectionCaptures;
        bool bCullLightsToGrid = (NumReflectionCaptures > 0 || GMobileUseClusteredDeferredShading != 0);
        FRDGBuilder GraphBuilder(RHICmdList);
        // 계산/산출(Calculate)라이트(Light).
        ComputeLightGrid(GraphBuilder, bCullLightsToGrid, SortedLightSet);
        GraphBuilder.Execute();
    }

    // /LUT.
    const bool bShouldRenderSkyAtmosphere = ShouldRenderSkyAtmosphere(Scene, ViewFamily.EngineShowFlags);
    if (bShouldRenderSkyAtmosphere)
    {
        FRDGBuilder GraphBuilder(RHICmdList);
        RenderSkyAtmosphereLookUpTables(GraphBuilder);
        GraphBuilder.Execute();
    }

    // 렌더링(Render).
    if (FXSystem && ViewFamily.EngineShowFlags.Particles)
    {
        FXSystem->PreRender(RHICmdList, NULL, !Views[0].bIsPlanarReflection);
        if (FGPUSortManager* GPUSortManager = FXSystem->GetGPUSortManager())
        {
            GPUSortManager->OnPreRender(RHICmdList);
        }
    }
    // 차폐/오클루전 컬링 .
    FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
    RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);

    RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Shadows));

    // 렌더링(Render)그림자(Shadow).
    RenderShadowDepthMaps(RHICmdList);
    FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
    RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);

    // 뷰(View)리스트 .
    TArray<const FViewInfo*> ViewList;
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++) 
    {
        ViewList.Add(&Views[ViewIndex]);
    }

    // 렌더링(Render)정의 뎁스 .
    if (bShouldRenderCustomDepth)
    {
        FRDGBuilder GraphBuilder(RHICmdList);
        FSceneTextureShaderParameters SceneTextures = CreateSceneTextureShaderParameters(GraphBuilder, Views[0].GetFeatureLevel(), ESceneTextureSetupMode::None);
        RenderCustomDepthPass(GraphBuilder, SceneTextures);
        GraphBuilder.Execute();
    }

    // 렌더링(Render)뎁스 PrePass.
    if (bIsFullPrepassEnabled)
    {
        // SDF 및 AO의 PrePass뎁스 .

        FRHIRenderPassInfo DepthPrePassRenderPassInfo(
            SceneContext.GetSceneDepthSurface(),
            EDepthStencilTargetActions::ClearDepthStencil_StoreDepthStencil);

        DepthPrePassRenderPassInfo.NumOcclusionQueries = ComputeNumOcclusionQueriesToBatch();
        DepthPrePassRenderPassInfo.bOcclusionQueries = DepthPrePassRenderPassInfo.NumOcclusionQueries != 0;

        RHICmdList.BeginRenderPass(DepthPrePassRenderPassInfo, TEXT("DepthPrepass"));

        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLM_MobilePrePass));
        
        // 렌더링(Render)의 뎁스 PrePass.
        RenderPrePass(RHICmdList);

        // 차폐/오클루전 컬링 .
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Occlusion));
        RenderOcclusion(RHICmdList);

        RHICmdList.EndRenderPass();

        // SDF그림자(Shadow)
        if (bRequiresDistanceFieldShadowingPass)
        {
            CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderSDFShadowing);
            RenderSDFShadowing(RHICmdList);
        }

        // HZB.
        if (bShouldRenderHZB)
        {
            RenderHZB(RHICmdList, SceneContext.SceneDepthZ);
        }

        // AO.
        if (bRequiresAmbientOcclusionPass)
        {
            RenderAmbientOcclusion(RHICmdList, SceneContext.SceneDepthZ);
        }
    }

    FRHITexture* SceneColor = nullptr;
    
    // 레이턴시 렌더링(Render).
    if (bDeferredShading)
    {
        SceneColor = RenderDeferred(RHICmdList, ViewList, SortedLightSet);
    }
    // 렌더링(Render).
    else
    {
        SceneColor = RenderForward(RHICmdList, ViewList);
    }
    
    // 렌더링(Render)벨로시티 버퍼(Velocity Buffer).
    if (bShouldRenderVelocities)
    {
        FRDGBuilder GraphBuilder(RHICmdList);

        FRDGTextureMSAA SceneDepthTexture = RegisterExternalTextureMSAA(GraphBuilder, SceneContext.SceneDepthZ);
        FRDGTextureRef VelocityTexture = TryRegisterExternalTexture(GraphBuilder, SceneContext.SceneVelocity);

        if (VelocityTexture != nullptr)
        {
            AddClearRenderTargetPass(GraphBuilder, VelocityTexture);
        }

        // 렌더링(Render)의 벨로시티 버퍼(Velocity Buffer).
        AddSetCurrentStatPass(GraphBuilder, GET_STATID(STAT_CLMM_Velocity));
        RenderVelocities(GraphBuilder, SceneDepthTexture.Resolve, VelocityTexture, FSceneTextureShaderParameters(), EVelocityPass::Opaque, false);
        AddSetCurrentStatPass(GraphBuilder, GET_STATID(STAT_CLMM_AfterVelocity));

        // 렌더링(Render)반투명(Translucent)의 벨로시티 버퍼(Velocity Buffer).
        AddSetCurrentStatPass(GraphBuilder, GET_STATID(STAT_CLMM_TranslucentVelocity));
        RenderVelocities(GraphBuilder, SceneDepthTexture.Resolve, VelocityTexture, GetSceneTextureShaderParameters(CreateMobileSceneTextureUniformBuffer(GraphBuilder, EMobileSceneTextureSetupMode::SceneColor)), EVelocityPass::Translucent, false);

        GraphBuilder.Execute();
    }

    // 처리(Process)렌더링(Render)의 로직 .
    {
        FRendererModule& RendererModule = static_cast<FRendererModule&>(GetRendererModule());
        FRDGBuilder GraphBuilder(RHICmdList);
        RendererModule.RenderPostOpaqueExtensions(GraphBuilder, Views, SceneContext);

        if (FXSystem && Views.IsValidIndex(0))
        {
            AddUntrackedAccessPass(GraphBuilder, [this](FRHICommandListImmediate& RHICmdList)
            {
                check(RHICmdList.IsOutsideRenderPass());

                FXSystem->PostRenderOpaque(
                    RHICmdList,
                    Views[0].ViewUniformBuffer,
                    nullptr,
                    nullptr,
                    Views[0].AllowGPUParticleUpdate()
                );
                if (FGPUSortManager* GPUSortManager = FXSystem->GetGPUSortManager())
                {
                    GPUSortManager->OnPostRenderOpaque(RHICmdList);
                }
            });
        }
        GraphBuilder.Execute();
    }

    // /버퍼 .
    if (bSubmitOffscreenRendering)
    {
        RHICmdList.SubmitCommandsHint();
        RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
    }
    
    // 변환(Transform/Convert)씬 컬러(SceneColor)SRV, 로써 단계/스텝 로드/읽기(Read).
    if (!bGammaSpace || bRenderToSceneColor)
    {
        RHICmdList.Transition(FRHITransitionInfo(SceneColor, ERHIAccess::Unknown, ERHIAccess::SRVMask));
    }

    if (bDeferredShading)
    {
        // 해제(Release)렌더링(Render)의 을(를) 활용하여 .
        SceneContext.AdjustGBufferRefCount(RHICmdList, -1);
    }

    RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Post));

    // 처리(Process)텍스처(Texture).
    if (bUseVirtualTexturing)
    {    
        SCOPED_GPU_STAT(RHICmdList, VirtualTextureUpdate);

        // No pass after this should make VT page requests
        RHICmdList.EndUAVOverlap(SceneContext.VirtualTextureFeedbackUAV);
        RHICmdList.Transition(FRHITransitionInfo(SceneContext.VirtualTextureFeedbackUAV, ERHIAccess::UAVMask, ERHIAccess::SRVMask));

        TArray<FIntRect, TInlineAllocator<4>> ViewRects;
        ViewRects.AddUninitialized(Views.Num());
        for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
        {
            ViewRects[ViewIndex] = Views[ViewIndex].ViewRect;
        }
        
        FVirtualTextureFeedbackBufferDesc Desc;
        Desc.Init2D(SceneContext.GetBufferSizeXY(), ViewRects, SceneContext.GetVirtualTextureFeedbackScale());

        SubmitVirtualTextureFeedbackBuffer(RHICmdList, SceneContext.VirtualTextureFeedback, Desc);
    }

    FMemMark Mark(FMemStack::Get());
    FRDGBuilder GraphBuilder(RHICmdList);

    FRDGTextureRef ViewFamilyTexture = TryCreateViewFamilyTexture(GraphBuilder, ViewFamily);
    
    //
    if (ViewFamily.bResolveScene)
    {
        if (!bGammaSpace || bRenderToSceneColor)
        {
            // 개 뷰(View)의 렌더링(Render) 또는 의 버퍼 (만약 활성화(Enable))
            {
                RDG_EVENT_SCOPE(GraphBuilder, "PostProcessing");
                SCOPE_CYCLE_COUNTER(STAT_FinishRenderViewTargetTime);

                TArray<TRDGUniformBufferRef<FMobileSceneTextureUniformParameters>, TInlineAllocator<1, SceneRenderingAllocator>> MobileSceneTexturesPerView;
                MobileSceneTexturesPerView.SetNumZeroed(Views.Num());

                const auto SetupMobileSceneTexturesPerView = [&]()
                {
                    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ++ViewIndex)
                    {
                        EMobileSceneTextureSetupMode SetupMode = EMobileSceneTextureSetupMode::SceneColor;
                        if (Views[ViewIndex].bCustomDepthStencilValid)
                        {
                            SetupMode |= EMobileSceneTextureSetupMode::CustomDepth;
                        }

                        if (bShouldRenderVelocities)
                        {
                            SetupMode |= EMobileSceneTextureSetupMode::SceneVelocity;
                        }

                        MobileSceneTexturesPerView[ViewIndex] = CreateMobileSceneTextureUniformBuffer(GraphBuilder, SetupMode);
                    }
                };

                SetupMobileSceneTexturesPerView();

                FMobilePostProcessingInputs PostProcessingInputs;
                PostProcessingInputs.ViewFamilyTexture = ViewFamilyTexture;

                // 렌더링(Render)포스트 프로세스.
                for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
                {
                    RDG_EVENT_SCOPE_CONDITIONAL(GraphBuilder, Views.Num() > 1, "View%d", ViewIndex);
                    PostProcessingInputs.SceneTextures = MobileSceneTexturesPerView[ViewIndex];
                    AddMobilePostProcessingPasses(GraphBuilder, Views[ViewIndex], PostProcessingInputs, NumMSAASamples > 1);
                }
            }
        }
    }

    GEngine->GetPostRenderDelegate().Broadcast();

    RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_SceneEnd));

    if (bShouldRenderVelocities)
    {
        SceneContext.SceneVelocity.SafeRelease();
    }
    
    if (ViewFamily.bLateLatchingEnabled)
    {
        EndLateLatching(RHICmdList, Views[0]);
    }

    RenderFinish(GraphBuilder, ViewFamilyTexture);
    GraphBuilder.Execute();

    // 차폐/오클루전 컬링 .
    FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
    FRHICommandListExecutor::GetImmediateCommandList().ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
}

언리얼 렌더링 시스템 분석(04) - 디퍼드 렌더링 파이프라인 장을 읽은 학생들은 모바일 씬 렌더링 프로세스가 PC 씬 렌더러의 하위 집합과 동등한 여러 단계를 간소화했다는 것을 알아야 합니다. 물론 모바일 단말기의 고유한 GPU 하드웨어 아키텍처에 적응하기 위해 모바일 단말기의 장면 렌더링도 PC 단말기의 장면 렌더링과 다릅니다. 이에 대해서는 나중에 자세히 분석하겠습니다. 모바일 시나리오의 주요 단계와 프로세스는 다음과 같습니다.

상태 다이어그램-v2 상태 bDeferredShading <<선택>> 상태 bIsFullPrepassEnabled <<선택>>

[*] --> UpdateAllPrimitiveSceneInfos
UpdateAllPrimitiveSceneInfos -->PrepareViewRectsForRendering
prepareViewRectsForRendering --> InitViews
InitViews --> bDeferredShading
bDeferredShading --> RenderSkyAtmosphereLookUpTables* : 아니요
bDeferredShading --> GatherAndSortLights: 예
GatherAndSortLights --> ComputeLightGrid
ComputeLightGrid --> RenderSkyAtmosphereLookUpTables*

RenderSkyAtmosphereLookUpTables* --> RenderShadowDepthMaps
RenderShadowDepthMaps --> RenderCustomDepthPass*
RenderCustomDepthPass* --> bIsFullPrepassEnabled

bIsFullPrepassEnabled --> bDeferredShading2: 아니요 bIsFullPrepassEnabled --> RenderPrePass:예 RenderPrePass --> RenderOcclusion RenderOcclusion --> RenderSDFShadowing* RenderSDFShadowing*--> RenderHZB* RenderHZB*--> RenderAmbientOcclusion*

RenderAmbientOcclusion* --> bDeferredShading2

bDeferredShading2-->RenderForward:No
bDeferredShading2-->RenderDeferred:예

RenderDeferred --> RenderVelocity* RenderForward --> RenderVelocity*

RenderVelocity*--> AddMobilePostProcessingPasses* AddMobilePostProcessingPasses*-->RenderFinish 렌더 마감 --> [*]

위의 흐름도와 관련하여 다음 사항을 설명해야 합니다.

  • 순서도 노드 bDeferredShading과 bDeferredShading2는 동일한 변수입니다. 여기서 차이점은 주로 인어 구문 그리기 오류를 방지하는 것입니다.

  • *로 표시된 노드는 조건부 단계이며 필요하지 않은 단계입니다.

UE4.26에는 모바일측 디퍼드 렌더링 파이프라인이 추가되었으므로 위 코드에는 순방향 렌더링 브랜치 RenderForward와 디퍼드 렌더링 브랜치 RenderDeferred가 포함되어 있으며, 둘 다 렌더링 결과 SceneColor를 반환합니다.

모바일 단말기는 기본 GPU 장면, SDF 그림자, AO, 하늘 대기, 가상 텍스처, 오클루전 컬링과 같은 렌더링 기능도 지원합니다.

UE4.26부터 렌더링 시스템은 RDG 시스템을 널리 사용하고 있으며, 모바일 씬 렌더러도 예외는 아닙니다. 위의 코드에는 총 여러 개의 FRDGBuider 인스턴스가 선언되어 있으며, 이는 광원 그리드를 계산하고 하늘 대기 LUT, 사용자 정의 깊이, 속도 버퍼, 이벤트 후 렌더링, 포스트 프로세스 등을 렌더링하는 데 사용됩니다. 이들은 모두 상대적으로 독립적인 기능 모듈 또는 렌더링 단계입니다.

12.3.2 렌더포워드

RenderForward는 모바일 씬 렌더러의 포워드 렌더링 분기를 담당합니다. 코드와 분석은 다음과 같습니다.

cpp
FRHITexture* FMobileSceneRenderer::RenderForward(FRHICommandListImmediate& RHICmdList, const TArrayView<const FViewInfo*> ViewList)
{
    const FViewInfo& View = *ViewList[0];
    FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);
                
    FRHITexture* SceneColor = nullptr;
    FRHITexture* SceneColorResolve = nullptr;
    FRHITexture* SceneDepth = nullptr;
    ERenderTargetActions ColorTargetAction = ERenderTargetActions::Clear_Store;
    EDepthStencilTargetActions DepthTargetAction = EDepthStencilTargetActions::ClearDepthStencil_DontStoreDepthStencil;

    // 는 활성화(Enable)모바일 플랫폼(Mobile)MSAA.
    bool bMobileMSAA = NumMSAASamples > 1 && SceneContext.GetSceneColorSurface()->GetNumSamples() > 1;

    // 는 활성화(Enable)모바일 플랫폼(Mobile).
    static const auto CVarMobileMultiView = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("vr.MobileMultiView"));
    const bool bIsMultiViewApplication = (CVarMobileMultiView && CVarMobileMultiView->GetValueOnAnyThread() != 0);
    
    // gamma의 렌더링(Render)분기 .
    if (bGammaSpace && !bRenderToSceneColor)
    {
        // 만약 활성화(Enable)MSAA, 이면 로부터 SceneContext페치/가져오기(Fetch)렌더링(Render)텍스처(Texture)(씬 컬러(SceneColor) 및 텍스처(Texture))
        if (bMobileMSAA)
        {
            SceneColor = SceneContext.GetSceneColorSurface();
            SceneColorResolve = ViewFamily.RenderTarget->GetRenderTargetTexture();
            ColorTargetAction = ERenderTargetActions::Clear_Resolve;
            RHICmdList.Transition(FRHITransitionInfo(SceneColorResolve, ERHIAccess::Unknown, ERHIAccess::RTV | ERHIAccess::ResolveDst));
        }
        // MSAA,로부터 뷰(View)페치/가져오기(Fetch)렌더링(Render)텍스처(Texture).
        else
        {
            SceneColor = ViewFamily.RenderTarget->GetRenderTargetTexture();
            RHICmdList.Transition(FRHITransitionInfo(SceneColor, ERHIAccess::Unknown, ERHIAccess::RTV));
        }
        SceneDepth = SceneContext.GetSceneDepthSurface();
    }
    // 선형(Linear) 또는 렌더링(Render)까지 텍스처(Texture).
    else
    {
        SceneColor = SceneContext.GetSceneColorSurface();
        if (bMobileMSAA)
        {
            SceneColorResolve = SceneContext.GetSceneColorTexture();
            ColorTargetAction = ERenderTargetActions::Clear_Resolve;
            RHICmdList.Transition(FRHITransitionInfo(SceneColorResolve, ERHIAccess::Unknown, ERHIAccess::RTV | ERHIAccess::ResolveDst));
        }
        else
        {
            SceneColorResolve = nullptr;
            ColorTargetAction = ERenderTargetActions::Clear_Store;
        }

        SceneDepth = SceneContext.GetSceneDepthSurface();
                
        if (bRequiresMultiPass)
        {    
            // store targets after opaque so translucency render pass can be restarted
            ColorTargetAction = ERenderTargetActions::Clear_Store;
            DepthTargetAction = EDepthStencilTargetActions::ClearDepthStencil_StoreDepthStencil;
        }
                        
        if (bKeepDepthContent)
        {
            // store depth if post-processing/capture needs it
            DepthTargetAction = EDepthStencilTargetActions::ClearDepthStencil_StoreDepthStencil;
        }
    }

    // prepass의 뎁스 텍스처(Depth Texture)상태 .
    if (bIsFullPrepassEnabled)
    {
        ERenderTargetActions DepthTarget = MakeRenderTargetActions(ERenderTargetLoadAction::ELoad, GetStoreAction(GetDepthActions(DepthTargetAction)));
        ERenderTargetActions StencilTarget = MakeRenderTargetActions(ERenderTargetLoadAction::ELoad, GetStoreAction(GetStencilActions(DepthTargetAction)));
        DepthTargetAction = MakeDepthStencilTargetActions(DepthTarget, StencilTarget);
    }

    FRHITexture* ShadingRateTexture = nullptr;
    
    if (!View.bIsSceneCapture && !View.bIsReflectionCapture)
    {
        TRefCountPtr<IPooledRenderTarget> ShadingRateTarget = GVRSImageManager.GetMobileVariableRateShadingImage(ViewFamily);
        if (ShadingRateTarget.IsValid())
        {
            ShadingRateTexture = ShadingRateTarget->GetRenderTargetItem().ShaderResourceTexture;
        }
    }

    // 씬 컬러(SceneColor)렌더링(Render)Pass정보 .
    FRHIRenderPassInfo SceneColorRenderPassInfo(
        SceneColor,
        ColorTargetAction,
        SceneColorResolve,
        SceneDepth,
        DepthTargetAction,
        nullptr, // we never resolve scene depth on mobile
        ShadingRateTexture,
        VRSRB_Sum,
        FExclusiveDepthStencil::DepthWrite_StencilWrite
    );
    SceneColorRenderPassInfo.SubpassHint = ESubpassHint::DepthReadSubpass;
    if (!bIsFullPrepassEnabled)
    {
        SceneColorRenderPassInfo.NumOcclusionQueries = ComputeNumOcclusionQueriesToBatch();
        SceneColorRenderPassInfo.bOcclusionQueries = SceneColorRenderPassInfo.NumOcclusionQueries != 0;
    }
    // 만약 씬 컬러(SceneColor)는 뷰(View),그러나 적용 는 ,렌더링(Render)로 뷰(View)의 뷰(View)셰이딩 .
    SceneColorRenderPassInfo.MultiViewCount = View.bIsMobileMultiViewEnabled ? 2 : (bIsMultiViewApplication ? 1 : 0);

    // 렌더링(Render)씬 컬러(SceneColor).
    RHICmdList.BeginRenderPass(SceneColorRenderPassInfo, TEXT("SceneColorRendering"));
    
    if (GIsEditor && !View.bIsSceneCapture)
    {
        DrawClearQuad(RHICmdList, Views[0].BackgroundColor);
    }

    if (!bIsFullPrepassEnabled)
    {
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLM_MobilePrePass));
        // 렌더링(Render)뎁스 pre-pass
        RenderPrePass(RHICmdList);
    }

    RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Opaque));
    // 렌더링(Render)BasePass: 반투명(Translucent) 및 masked.
    RenderMobileBasePass(RHICmdList, ViewList);
    RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);

    //렌더링(Render)디버그 모드(Debug Mode).
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
    if (ViewFamily.UseDebugViewPS())
    {
        // Here we use the base pass depth result to get z culling for opaque and masque.
        // The color needs to be cleared at this point since shader complexity renders in additive.
        DrawClearQuad(RHICmdList, FLinearColor::Black);
        RenderMobileDebugView(RHICmdList, ViewList);
        RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
    }
#endif // !(UE_BUILD_SHIPPING || UE_BUILD_TEST)

    const bool bAdrenoOcclusionMode = CVarMobileAdrenoOcclusionMode.GetValueOnRenderThread() != 0;
    if (!bIsFullPrepassEnabled)
    {
        // 차폐/오클루전 컬링
        if (!bAdrenoOcclusionMode)
        {
            // 차폐/오클루전 컬링
            RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Occlusion));
            RenderOcclusion(RHICmdList);
            RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
        }
    }

    // 이벤트 , 처리(Process)렌더링(Render).
    {
        CSV_SCOPED_TIMING_STAT_EXCLUSIVE(ViewExtensionPostRenderBasePass);
        QUICK_SCOPE_CYCLE_COUNTER(STAT_FMobileSceneRenderer_ViewExtensionPostRenderBasePass);
        for (int32 ViewExt = 0; ViewExt < ViewFamily.ViewExtensions.Num(); ++ViewExt)
        {
            for (int32 ViewIndex = 0; ViewIndex < ViewFamily.Views.Num(); ++ViewIndex)
            {
                ViewFamily.ViewExtensions[ViewExt]->PostRenderBasePass_RenderThread(RHICmdList, Views[ViewIndex]);
            }
        }
    }
    
    // 만약 렌더링(Render)반투명(Translucent) 또는 픽셀(Pixel)투영/프로젝션 의 반사(Reflection), 이면 pass.
    if (bRequiresMultiPass || bRequiresPixelProjectedPlanarRelfectionPass)
    {
        RHICmdList.EndRenderPass();
    }
       
    RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Translucency));

    // 만약 , 이면 활성화(Enable)반투명(Translucent)렌더링(Render)채널/패스(Pass).
    if (bRequiresMultiPass || bRequiresPixelProjectedPlanarRelfectionPass)
    {
        check(RHICmdList.IsOutsideRenderPass());

        // 만약 현재(Current)지원 의 뎁스 버퍼(Depth Buffer),이면 씬 뎁스(SceneDepth).
        ConditionalResolveSceneDepth(RHICmdList, View);

        if (bRequiresPixelProjectedPlanarRelfectionPass)
        {
            const FPlanarReflectionSceneProxy* PlanarReflectionSceneProxy = Scene ? Scene->GetForwardPassGlobalPlanarReflection() : nullptr;
            RenderPixelProjectedReflection(RHICmdList, SceneContext, PlanarReflectionSceneProxy);

            FRHITransitionInfo TranslucentRenderPassTransitions[] = {
            FRHITransitionInfo(SceneColor, ERHIAccess::SRVMask, ERHIAccess::RTV),
            FRHITransitionInfo(SceneDepth, ERHIAccess::SRVMask, ERHIAccess::DSVWrite)
            };
            RHICmdList.Transition(MakeArrayView(TranslucentRenderPassTransitions, UE_ARRAY_COUNT(TranslucentRenderPassTransitions)));
        }

        DepthTargetAction = EDepthStencilTargetActions::LoadDepthStencil_DontStoreDepthStencil;
        FExclusiveDepthStencil::Type ExclusiveDepthStencil = FExclusiveDepthStencil::DepthRead_StencilRead;
        if (bModulatedShadowsInUse)
        {
            ExclusiveDepthStencil = FExclusiveDepthStencil::DepthRead_StencilWrite;
        }

        // 을(를) 활용하여 모바일 플랫폼(Mobile)픽셀(Pixel)투영/프로젝션 반사(Reflection)의 반투명(Translucent)필수: 뎁스 기록/쓰기(Write)뎁스 RT, 이므로 오직 렌더링(Render)회 (만약 품질 수평(Horizontal) 또는 BestPerformance).
        if (IsMobilePixelProjectedReflectionEnabled(View.GetShaderPlatform())
            && GetMobilePixelProjectedReflectionQuality() == EMobilePixelProjectedReflectionQuality::BestPerformance)
        {
            ExclusiveDepthStencil = FExclusiveDepthStencil::DepthWrite_StencilWrite;
        }

        if (bKeepDepthContent && !bMobileMSAA)
        {
            DepthTargetAction = EDepthStencilTargetActions::LoadDepthStencil_StoreDepthStencil;
        }

#if PLATFORM_HOLOLENS
        if (bShouldRenderDepthToTranslucency)
        {
            ExclusiveDepthStencil = FExclusiveDepthStencil::DepthWrite_StencilWrite;
        }
#endif

        // 반투명(Translucent)렌더링(Render)Pass.
        FRHIRenderPassInfo TranslucentRenderPassInfo(
            SceneColor,
            SceneColorResolve ? ERenderTargetActions::Load_Resolve : ERenderTargetActions::Load_Store,
            SceneColorResolve,
            SceneDepth,
            DepthTargetAction, 
            nullptr,
            ShadingRateTexture,
            VRSRB_Sum,
            ExclusiveDepthStencil
        );
        TranslucentRenderPassInfo.NumOcclusionQueries = 0;
        TranslucentRenderPassInfo.bOcclusionQueries = false;
        TranslucentRenderPassInfo.SubpassHint = ESubpassHint::DepthReadSubpass;
        
        // 렌더링(Render)반투명(Translucent).
        RHICmdList.BeginRenderPass(TranslucentRenderPassInfo, TEXT("SceneColorTranslucencyRendering"));
    }

    // 씬 뎁스(SceneDepth)는 오직 의 ,로써 페치/가져오기(Fetch).
    RHICmdList.NextSubpass();

    if (!View.bIsPlanarReflection)
    {
        // 렌더링(Render).
        if (ViewFamily.EngineShowFlags.Decals)
        {
            CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderDecals);
            RenderDecals(RHICmdList);
        }

        // 렌더링(Render)그림자(Shadow).
        if (ViewFamily.EngineShowFlags.DynamicShadows)
        {
            CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderShadowProjections);
            RenderModulatedShadowProjections(RHICmdList);
        }
    }
    
    // 드로우/렌더(Draw)반투명(Translucent).
    if (ViewFamily.EngineShowFlags.Translucency)
    {
        CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderTranslucency);
        SCOPE_CYCLE_COUNTER(STAT_TranslucencyDrawTime);
        
        RenderTranslucency(RHICmdList, ViewList);
        
        FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
        RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
    }

    if (!bIsFullPrepassEnabled)
    {
        // Adreno차폐/오클루전 컬링 .
        if (bAdrenoOcclusionMode)
        {
            RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Occlusion));
            // flush
            RHICmdList.SubmitCommandsHint();
            bSubmitOffscreenRendering = false; // submit once
            // Issue occlusion queries
            RenderOcclusion(RHICmdList);
            RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
        }
    }

    // MSAA계산/산출(Calculate)톤매핑(Tonemapping)(오직 iOS유효(Valid))
    if (!bGammaSpace)
    {
        PreTonemapMSAA(RHICmdList);
    }

    // 씬 컬러(SceneColor)렌더링(Render).
    RHICmdList.EndRenderPass();

    // 최적화 반환(Return)씬 컬러(SceneColor)의 텍스처(Texture)(활성화(Enable)MSAA).
    return SceneColorResolve ? SceneColorResolve : SceneColor;
}

모바일 측 포워드 렌더링의 주요 단계는 PrePass, BasePass, 특수 렌더링(데칼, AO, 오클루전 컬링 등) 및 반투명 개체를 순차적으로 렌더링하는 것을 포함하여 PC 측의 단계와 유사합니다. 그들의 흐름도는 다음과 같습니다:

상태 다이어그램-v2 [] --> DrawClearQuad

DrawClearQuad*--> RenderPrePass*
RenderPrePass* --> RenderMobileBasePass
RenderMobileBasePass --> RenderOcclusion*

RenderOcclusion*--> RenderDecals*
RenderDecals*--> RenderModulatedShadowProjections*
RenderModulatedShadowProjections*--> RenderTranslucency*
RenderTranslucency*--> PreTonemapMSAA*

PreTonemapMSAA*--> [*]

그 중 폐색 제거는 GPU 제조업체와 관련이 있습니다. 예를 들어, Qualcomm Adreno 시리즈 GPU 칩은 플러시 렌더링 지침과 스위치 FBO 사이에 필요합니다.

불투명 렌더링 -> 트랜스루센트 렌더링 -> 플러시 -> 렌더 쿼리 -> FBO 전환

그런 다음 UE는 Adreno 시리즈 칩의 특수 요구 사항을 따르고 폐색 제거를 위한 특수 처리를 수행합니다.

Adreno 시리즈 칩은 TBDR 아키텍처의 Bin과 일반 Direct의 두 가지 혼합 모드 렌더링을 지원하며 폐색 쿼리 중에 자동으로 Direct 모드로 전환하여 폐색 쿼리 비용을 줄입니다. Flush 렌더링 명령과 Switch FBO 사이에 쿼리가 제출되지 않으면 전체 렌더링 파이프라인이 정체되어 렌더링 성능이 저하됩니다.

자연스러운 하드웨어 지원과 효과와 효율성 사이의 적절한 균형으로 인해 MSAA는 모바일 단말기의 순방향 렌더링에서 UE가 선호하는 앤티앨리어싱입니다. 따라서 위 코드에는 색상 및 깊이 텍스처와 해당 리소스 상태를 포함하여 MSAA를 처리하기 위한 많은 로직이 있습니다. MSAA가 켜져 있으면 기본적으로 장면 색상이 RHICmdList.EndRenderPass()에서 구문 분석되어(그리고 칩 블록의 데이터가 시스템 비디오 메모리에 다시 기록됨) 앤티앨리어싱된 텍스처를 얻습니다. 모바일 단말기의 MSAA는 기본적으로 켜져 있지 않지만 다음 인터페이스에서 설정할 수 있습니다.

포워드 렌더링은 감마 공간과 HDR(선형 공간)의 두 가지 색상 공간 모드를 지원합니다. 선형 공간인 경우 렌더링 후 단계에서 톤매핑과 같은 단계가 필요합니다. 기본값은 HDR이며 프로젝트 구성에서 변경할 수 있습니다.

위 코드의 bRequiresMultiPass는 반투명 객체를 그리는 데 전용 렌더링 패스가 필요한지 여부를 나타냅니다. 해당 값 결정은 다음 코드로 완료됩니다.

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

bool FMobileSceneRenderer::RequiresMultiPass(FRHICommandListImmediate& RHICmdList, const FViewInfo& View) const
{
    // Vulkan uses subpasses
    if (IsVulkanPlatform(ShaderPlatform))
    {
        return false;
    }

    // All iOS support frame_buffer_fetch
    if (IsMetalMobilePlatform(ShaderPlatform))
    {
        return false;
    }

    if (IsMobileDeferredShadingEnabled(ShaderPlatform))
    {
        // TODO: add GL support
        return true;
    }
    
    // Some Androids support frame_buffer_fetch
    if (IsAndroidOpenGLESPlatform(ShaderPlatform) && (GSupportsShaderFramebufferFetch || GSupportsShaderDepthStencilFetch))
    {
        return false;
    }
        
    // Always render reflection capture in single pass
    if (View.bIsPlanarReflection || View.bIsSceneCapture)
    {
        return false;
    }

    // Always render LDR in single pass
    if (!IsMobileHDR())
    {
        return false;
    }

    // MSAA depth can't be sampled or resolved, unless we are on PC (no vulkan)
    if (NumMSAASamples > 1 && !IsSimulatedPlatform(ShaderPlatform))
    {
        return false;
    }

    return true;
}

유사하지만 의미가 다른 bIsMultiViewApplication 및 bIsMobileMultiViewEnabled 태그는 다중 뷰 렌더링 활성화 여부와 다중 뷰 수를 나타냅니다. VR에서만 사용되며 콘솔 변수 vr.MobileMultiView 및 그래픽 API와 같은 요소에 의해 결정됩니다. MultiView는 XR에서 두 번 렌더링되는 상황을 최적화하는 데 사용됩니다. 기본 및 고급의 두 가지 모드가 있습니다.

VR과 같은 렌더링을 최적화하는 데 사용되는 MultiView 비교 차트입니다. 위: MultiView 모드 없이 렌더링, 두 눈 각각 그리기 지침을 제출합니다. 중간: 기본 MultiView 모드, 제출 지침을 재사용하고 GPU 레이어에 추가 명령 목록을 복사합니다. 하단: DC, 명령 목록 및 형상 정보를 재사용할 수 있는 고급 MultiView 모드.

bKeepDepthContent는 깊이 콘텐츠를 유지할지 여부를 나타내고 해당 코드를 결정합니다.

cpp
bKeepDepthContent = 
    bRequiresMultiPass || 
    bForceDepthResolve ||
    bRequiresPixelProjectedPlanarRelfectionPass ||
    bSeparateTranslucencyActive ||
    Views[0].bIsReflectionCapture ||
    (bDeferredShading && bPostProcessUsesSceneDepth) ||
    bShouldRenderVelocities ||
    bIsFullPrepassEnabled;

// MSAA의 뎁스 로부터 .
bKeepDepthContent = (NumMSAASamples > 1 ? false : bKeepDepthContent);

위 코드는 모바일 측 평면 반사의 특수 렌더링 방법인 **PPR(Pixel Projected Reflection)**도 보여줍니다. 구현 원리는 SSR과 유사하지만 데이터가 덜 필요하고 장면 색상, 깊이 버퍼 및 반사 영역만 필요합니다. 핵심 단계:

  • 반사 평면에서 장면 색상의 모든 픽셀의 거울 위치를 계산합니다.

  • 픽셀의 반사가 반사 영역 내에 있는지 테스트합니다.

광선은 미러링된 픽셀 위치로 투사됩니다.

  • 교차점이 반사 영역 내에 있는지 테스트합니다.

  • 교차점이 발견되면 화면에서 픽셀의 대칭 위치를 계산합니다.

  • 미러링된 픽셀의 색상을 교차점에 씁니다.

  • 반사 영역의 교차점이 다른 물체에 의해 가려지면 해당 위치의 반사가 제거됩니다.

PPR 효과를 한눈에 살펴보세요.

PPR은 프로젝트 구성에서 설정할 수 있습니다.

12.3.3 렌더링 지연

UE는 4.26에서 모바일 렌더링 파이프라인에 디퍼드 렌더링 분기를 추가하고 4.27에서 개선 및 최적화를 수행했습니다. 모바일 단말기에서 디퍼드 렌더링을 활성화할지 여부는 다음 코드에 의해 결정됩니다.

cpp
// Engine\Source\Runtime\RenderCore\Private\RenderUtils.cpp

bool IsMobileDeferredShadingEnabled(const FStaticShaderPlatform Platform)
{
    // 비활성화(Disable)OpenGL의 레이턴시 셰이딩 .
    if (IsOpenGLPlatform(Platform))
    {
        // needs MRT framebuffer fetch or PLS
        return false;
    }
    
    // 변수"r.Mobile.ShadingPath"로 1.
    static auto* MobileShadingPathCvar = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("r.Mobile.ShadingPath"));
    return MobileShadingPathCvar->GetValueOnAnyThread() == 1;
}

간단히 말해서 OpenGL이 아닌 그래픽 API이고 콘솔 변수 r.Mobile.ShadingPath가 1로 설정되어 있습니다.

r.Mobile.ShadingPath는 편집기에서 동적으로 설정할 수 없습니다. 프로젝트 루트 디렉터리/Config/DefaultEngine.ini에 다음 필드를 추가해야만 활성화할 수 있습니다:

[/Script/Engine.RendererSettings]

r.Mobile.ShadingPath=1

위 필드를 추가한 후 UE 편집기를 다시 시작하고 셰이더 컴파일이 완료될 때까지 기다려 모바일 디퍼드 렌더링 효과를 미리 봅니다.

다음은 디퍼드 렌더링 브랜치 FMobileSceneRenderer::RenderDeferred의 코드 및 분석입니다.

cpp
FRHITexture* FMobileSceneRenderer::RenderDeferred(FRHICommandListImmediate& RHICmdList, const TArrayView<const FViewInfo*> ViewList, const FSortedLightSetSceneInfo& SortedLightSet)
{
    FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);
    
    // GBuffer.
    FRHITexture* ColorTargets[4] = {
        SceneContext.GetSceneColorSurface(),
        SceneContext.GetGBufferATexture().GetReference(),
        SceneContext.GetGBufferBTexture().GetReference(),
        SceneContext.GetGBufferCTexture().GetReference()
    };

    // RHI는 GBuffer저장(Store)까지 GPU의 메모리 내에서 ,의 렌더링(Render)채널/패스(Pass) 내에서 셰이딩 .
    ERenderTargetActions GBufferAction = bRequiresMultiPass ? ERenderTargetActions::Clear_Store : ERenderTargetActions::Clear_DontStore;
    EDepthStencilTargetActions DepthAction = bKeepDepthContent ? EDepthStencilTargetActions::ClearDepthStencil_StoreDepthStencil : EDepthStencilTargetActions::ClearDepthStencil_DontStoreDepthStencil;
    
    // RT의 load/store.
    ERenderTargetActions ColorTargetsAction[4] = {ERenderTargetActions::Clear_Store, GBufferAction, GBufferAction, GBufferAction};
    if (bIsFullPrepassEnabled)
    {
        ERenderTargetActions DepthTarget = MakeRenderTargetActions(ERenderTargetLoadAction::ELoad, GetStoreAction(GetDepthActions(DepthAction)));
        ERenderTargetActions StencilTarget = MakeRenderTargetActions(ERenderTargetLoadAction::ELoad, GetStoreAction(GetStencilActions(DepthAction)));
        DepthAction = MakeDepthStencilTargetActions(DepthTarget, StencilTarget);
    }
    
    FRHIRenderPassInfo BasePassInfo = FRHIRenderPassInfo();
    int32 ColorTargetIndex = 0;
    for (; ColorTargetIndex < UE_ARRAY_COUNT(ColorTargets); ++ColorTargetIndex)
    {
        BasePassInfo.ColorRenderTargets[ColorTargetIndex].RenderTarget = ColorTargets[ColorTargetIndex];
        BasePassInfo.ColorRenderTargets[ColorTargetIndex].ResolveTarget = nullptr;
        BasePassInfo.ColorRenderTargets[ColorTargetIndex].ArraySlice = -1;
        BasePassInfo.ColorRenderTargets[ColorTargetIndex].MipIndex = 0;
        BasePassInfo.ColorRenderTargets[ColorTargetIndex].Action = ColorTargetsAction[ColorTargetIndex];
    }
    
    if (MobileRequiresSceneDepthAux(ShaderPlatform))
    {
        BasePassInfo.ColorRenderTargets[ColorTargetIndex].RenderTarget = SceneContext.SceneDepthAux->GetRenderTargetItem().ShaderResourceTexture.GetReference();
        BasePassInfo.ColorRenderTargets[ColorTargetIndex].ResolveTarget = nullptr;
        BasePassInfo.ColorRenderTargets[ColorTargetIndex].ArraySlice = -1;
        BasePassInfo.ColorRenderTargets[ColorTargetIndex].MipIndex = 0;
        BasePassInfo.ColorRenderTargets[ColorTargetIndex].Action = GBufferAction;
        ColorTargetIndex++;
    }

    BasePassInfo.DepthStencilRenderTarget.DepthStencilTarget = SceneContext.GetSceneDepthSurface();
    BasePassInfo.DepthStencilRenderTarget.ResolveTarget = nullptr;
    BasePassInfo.DepthStencilRenderTarget.Action = DepthAction;
    BasePassInfo.DepthStencilRenderTarget.ExclusiveDepthStencil = FExclusiveDepthStencil::DepthWrite_StencilWrite;
        
    BasePassInfo.SubpassHint = ESubpassHint::DeferredShadingSubpass;
    if (!bIsFullPrepassEnabled)
    {
        BasePassInfo.NumOcclusionQueries = ComputeNumOcclusionQueriesToBatch();
        BasePassInfo.bOcclusionQueries = BasePassInfo.NumOcclusionQueries != 0;
    }
    BasePassInfo.ShadingRateTexture = nullptr;
    BasePassInfo.bIsMSAA = false;
    BasePassInfo.MultiViewCount = 0;

    RHICmdList.BeginRenderPass(BasePassInfo, TEXT("BasePassRendering"));
    
    if (GIsEditor && !Views[0].bIsSceneCapture)
    {
        DrawClearQuad(RHICmdList, Views[0].BackgroundColor);
    }

    // 뎁스 PrePass
    if (!bIsFullPrepassEnabled)
    {
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLM_MobilePrePass));
        // Depth pre-pass
        RenderPrePass(RHICmdList);
    }

    // BasePass: 반투명(Translucent) 및 .
    RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Opaque));
    RenderMobileBasePass(RHICmdList, ViewList);
    RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);

    // 차폐/오클루전 컬링 .
    if (!bIsFullPrepassEnabled)
    {
        // Issue occlusion queries
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Occlusion));
        RenderOcclusion(RHICmdList);
        RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
    }

    // Pass
    if (!bRequiresMultiPass)
    {
        // 개 Pass: SSceneColor + GBuffer기록/쓰기(Write), SceneDepth오직 .
        RHICmdList.NextSubpass();
        
        // 렌더링(Render).
        if (ViewFamily.EngineShowFlags.Decals)
        {
            CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderDecals);
            RenderDecals(RHICmdList);
        }

        // 개 Pass: SceneColor기록/쓰기(Write), SceneDepth오직
        RHICmdList.NextSubpass();
        
        // 레이턴시 라이팅 셰이딩 .
        MobileDeferredShadingPass(RHICmdList, *Scene, ViewList, SortedLightSet);
        
        // 드로우/렌더(Draw)반투명(Translucent).
        if (ViewFamily.EngineShowFlags.Translucency)
        {
            CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderTranslucency);
            SCOPE_CYCLE_COUNTER(STAT_TranslucencyDrawTime);
            RenderTranslucency(RHICmdList, ViewList);
            FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
            RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
        }
        
        // 렌더링(Render)Pass.
        RHICmdList.EndRenderPass();
    }
    // Pass(PC디바이스의 모바일 플랫폼(Mobile)).
    else
    {
        // pass.
        RHICmdList.NextSubpass();
        RHICmdList.NextSubpass();
        RHICmdList.EndRenderPass();
        
        // SceneColor + GBuffer write, SceneDepth is read only
        {
            for (int32 Index = 0; Index < UE_ARRAY_COUNT(ColorTargets); ++Index)
            {
                BasePassInfo.ColorRenderTargets[Index].Action = ERenderTargetActions::Load_Store;
            }
            BasePassInfo.DepthStencilRenderTarget.Action = EDepthStencilTargetActions::LoadDepthStencil_StoreDepthStencil;
            BasePassInfo.DepthStencilRenderTarget.ExclusiveDepthStencil = FExclusiveDepthStencil::DepthRead_StencilRead;
            BasePassInfo.SubpassHint = ESubpassHint::None;
            BasePassInfo.NumOcclusionQueries = 0;
            BasePassInfo.bOcclusionQueries = false;
            
            RHICmdList.BeginRenderPass(BasePassInfo, TEXT("AfterBasePass"));
            
            // 렌더링(Render).
            if (ViewFamily.EngineShowFlags.Decals)
            {
                CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderDecals);
                RenderDecals(RHICmdList);
            }
            
            RHICmdList.EndRenderPass();
        }

        // SceneColor write, SceneDepth is read only
        {
            FRHIRenderPassInfo ShadingPassInfo(
                SceneContext.GetSceneColorSurface(),
                ERenderTargetActions::Load_Store,
                nullptr,
                SceneContext.GetSceneDepthSurface(),
                EDepthStencilTargetActions::LoadDepthStencil_StoreDepthStencil, 
                nullptr,
                nullptr,
                VRSRB_Passthrough,
                FExclusiveDepthStencil::DepthRead_StencilWrite
            );
            ShadingPassInfo.NumOcclusionQueries = 0;
            ShadingPassInfo.bOcclusionQueries = false;
            
            RHICmdList.BeginRenderPass(ShadingPassInfo, TEXT("MobileShadingPass"));
            
            // 레이턴시 라이팅 셰이딩 .
            MobileDeferredShadingPass(RHICmdList, *Scene, ViewList, SortedLightSet);
            
            // 드로우/렌더(Draw)반투명(Translucent).
            if (ViewFamily.EngineShowFlags.Translucency)
            {
                CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderTranslucency);
                SCOPE_CYCLE_COUNTER(STAT_TranslucencyDrawTime);
                RenderTranslucency(RHICmdList, ViewList);
                FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
                RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
            }

            RHICmdList.EndRenderPass();
        }
    }

    return ColorTargets[0];
}

위에서 볼 수 있듯이 모바일 단말의 디퍼드 렌더링 파이프라인은 PC의 디퍼드 렌더링 파이프라인과 유사합니다. BasePass가 먼저 렌더링되고 GBuffer 지오메트리 정보가 획득된 다음 조명 계산이 수행됩니다. 그들의 흐름도는 다음과 같습니다:

상태 다이어그램-v2 [] --> DrawClearQuad

DrawClearQuad*--> RenderPrePass*
RenderPrePass* --> RenderMobileBasePass
RenderMobileBasePass --> RenderOcclusion*

RenderOcclusion*--> RenderDecals*
RenderDecals*--> MobileDeferredShadingPass*
MobileDeferredShadingPass*--> RenderTranslucency*
RenderTranslucency*--> PreTonemapMSAA*

PreTonemapMSAA*--> [*]

물론 PC와 다른점도 있습니다. 가장 분명한 점은 모바일 단말기가 TB(D)R 아키텍처에 적용된 SubPass 렌더링을 사용한다는 점입니다. 이를 통해 모바일 단말기는 PrePass 깊이, BasePass 및 조명 계산을 렌더링할 때 장면 색상, 깊이, GBuffer 및 기타 정보를 On-Chip 버퍼에 유지하여 렌더링 효율성을 높이고 장치 에너지 소비를 줄일 수 있습니다.

12.3.3.1 MobileDeferredShadingPass

디퍼드 렌더링(Deferred Shading) 조명 프로세스는 MobileDeferredShadingPass에 의해 처리됩니다.

cpp
void MobileDeferredShadingPass(
    FRHICommandListImmediate& RHICmdList, 
    const FScene& Scene, 
    const TArrayView<const FViewInfo*> PassViews, 
    const FSortedLightSetSceneInfo &SortedLightSet)
{
    SCOPED_DRAW_EVENT(RHICmdList, MobileDeferredShading);

    const FViewInfo& View0 = *PassViews[0];

    FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);
    // 생성(Create)Uniform Buffer.
    FUniformBufferRHIRef PassUniformBuffer = CreateMobileSceneTextureUniformBuffer(RHICmdList);
    FUniformBufferStaticBindings GlobalUniformBuffers(PassUniformBuffer);
    SCOPED_UNIFORM_BUFFER_GLOBAL_BINDINGS(RHICmdList, GlobalUniformBuffers);
    // 설정(Set)뷰포트(Viewport).
    RHICmdList.SetViewport(View0.ViewRect.Min.X, View0.ViewRect.Min.Y, 0.0f, View0.ViewRect.Max.X, View0.ViewRect.Max.Y, 1.0f);

    // 라이팅 의 기본값(Default)머티리얼(Material).
    FCachedLightMaterial DefaultMaterial;
    DefaultMaterial.MaterialProxy = UMaterial::GetDefaultMaterial(MD_LightFunction)->GetRenderProxy();
    DefaultMaterial.Material = DefaultMaterial.MaterialProxy->GetMaterialNoFallback(ERHIFeatureLevel::ES3_1);
    check(DefaultMaterial.Material);

    // 드로우/렌더(Draw).
    RenderDirectLight(RHICmdList, Scene, View0, DefaultMaterial);

    if (GMobileUseClusteredDeferredShading == 0)
    {
        // 렌더링(Render)의 라이트(Light).
        RenderSimpleLights(RHICmdList, Scene, PassViews, SortedLightSet, DefaultMaterial);
    }

    // 렌더링(Render)의 로컬 라이트(Light).
    int32 NumLights = SortedLightSet.SortedLights.Num();
    int32 StandardDeferredStart = SortedLightSet.SimpleLightsEnd;
    if (GMobileUseClusteredDeferredShading != 0)
    {
        StandardDeferredStart = SortedLightSet.ClusteredSupportedEnd;
    }

    // 렌더링(Render)로컬 라이트(Light).
    for (int32 LightIdx = StandardDeferredStart; LightIdx < NumLights; ++LightIdx)
    {
        const FSortedLightSceneInfo& SortedLight = SortedLightSet.SortedLights[LightIdx];
        const FLightSceneInfo& LightSceneInfo = *SortedLight.LightSceneInfo;
        RenderLocalLight(RHICmdList, Scene, View0, LightSceneInfo, DefaultMaterial);
    }
}

다양한 유형의 광원을 렌더링하기 위한 인터페이스를 계속 분석해 보겠습니다.

hlsl
// Engine\Source\Runtime\Renderer\Private\MobileDeferredShadingPass.cpp

// 렌더링(Render)
static void RenderDirectLight(FRHICommandListImmediate& RHICmdList, const FScene& Scene, const FViewInfo& View, const FCachedLightMaterial& DefaultLightMaterial)
{
    FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);

    // 탐색/조회(Find/Lookup)개 .
    FLightSceneInfo* DirectionalLight = nullptr;
    for (int32 ChannelIdx = 0; ChannelIdx < UE_ARRAY_COUNT(Scene.MobileDirectionalLights) && !DirectionalLight; ChannelIdx++)
    {
        DirectionalLight = Scene.MobileDirectionalLights[ChannelIdx];
    }

    // 렌더링(Render)상태 .
    FGraphicsPipelineStateInitializer GraphicsPSOInit;
    RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
    // 추가(Add)발광(Emissive) 까지 SceneColor.
    GraphicsPSOInit.BlendState = TStaticBlendState<CW_RGB, BO_Add, BF_One, BF_One>::GetRHI();
    GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
    // 오직 드로우/렌더(Draw)기본값(Default)라이팅 모델 (MSM_DefaultLit)의 픽셀(Pixel).
    uint8 StencilRef = GET_STENCIL_MOBILE_SM_MASK(MSM_DefaultLit);
    GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<
                                        false, CF_Always,
                                        true, CF_Equal, SO_Keep, SO_Keep, SO_Keep,        
                                        false, CF_Always, SO_Keep, SO_Keep, SO_Keep,
                                        GET_STENCIL_MOBILE_SM_MASK(0x7), 0x00>::GetRHI(); // 4 bits for shading models
    
    // 처리(Process)VS.
    TShaderMapRef<FPostProcessVS> VertexShader(View.ShaderMap);
    
    const FMaterialRenderProxy* LightFunctionMaterialProxy = nullptr;
    if (View.Family->EngineShowFlags.LightFunctions && DirectionalLight)
    {
        LightFunctionMaterialProxy = DirectionalLight->Proxy->GetLightFunctionMaterial();
    }
    FMobileDirectLightFunctionPS::FPermutationDomain PermutationVector = FMobileDirectLightFunctionPS::BuildPermutationVector(View, DirectionalLight != nullptr);
    FCachedLightMaterial LightMaterial;
    TShaderRef<FMobileDirectLightFunctionPS> PixelShader;
    GetLightMaterial(DefaultLightMaterial, LightFunctionMaterialProxy, PermutationVector.ToDimensionValueId(), LightMaterial, PixelShader);
    
    GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GFilterVertexDeclaration.VertexDeclarationRHI;
    GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
    GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
    GraphicsPSOInit.PrimitiveType = PT_TriangleList;
    SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);

    // 처리(Process)PS.
    FMobileDirectLightFunctionPS::FParameters PassParameters;
    PassParameters.Forward = View.ForwardLightingResources->ForwardLightDataUniformBuffer;
    PassParameters.MobileDirectionalLight = Scene.UniformBuffers.MobileDirectionalLightUniformBuffers[1];
    PassParameters.ReflectionCaptureData = Scene.UniformBuffers.ReflectionCaptureUniformBuffer;
    FReflectionUniformParameters ReflectionUniformParameters;
    SetupReflectionUniformParameters(View, ReflectionUniformParameters);
    PassParameters.ReflectionsParameters = CreateUniformBufferImmediate(ReflectionUniformParameters, UniformBuffer_SingleDraw);
    PassParameters.LightFunctionParameters = FVector4(1.0f, 1.0f, 0.0f, 0.0f);
    if (DirectionalLight)
    {
        const bool bUseMovableLight = DirectionalLight && !DirectionalLight->Proxy->HasStaticShadowing();
        PassParameters.LightFunctionParameters2 = FVector(DirectionalLight->Proxy->GetLightFunctionFadeDistance(), DirectionalLight->Proxy->GetLightFunctionDisabledBrightness(), bUseMovableLight ? 1.0f : 0.0f);
        const FVector Scale = DirectionalLight->Proxy->GetLightFunctionScale();
        // Switch x and z so that z of the user specified scale affects the distance along the light direction
        const FVector InverseScale = FVector(1.f / Scale.Z, 1.f / Scale.Y, 1.f / Scale.X);
        PassParameters.WorldToLight = DirectionalLight->Proxy->GetWorldToLight() * FScaleMatrix(FVector(InverseScale));
    }
    FMobileDirectLightFunctionPS::SetParameters(RHICmdList, PixelShader, View, LightMaterial.MaterialProxy, *LightMaterial.Material, PassParameters);
    
    RHICmdList.SetStencilRef(StencilRef);
            
    const FIntPoint TargetSize = SceneContext.GetBufferSizeXY();
    
    // 을(를) 활용하여 풀스크린(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(),
        FIntPoint(View.ViewRect.Width(), View.ViewRect.Height()), 
        TargetSize, 
        VertexShader);
}

// 렌더링(Render)의 라이트(Light).
static void RenderSimpleLights(
    FRHICommandListImmediate& RHICmdList, 
    const FScene& Scene, 
    const TArrayView<const FViewInfo*> PassViews, 
    const FSortedLightSetSceneInfo &SortedLightSet, 
    const FCachedLightMaterial& DefaultMaterial)
{
    const FSimpleLightArray& SimpleLights = SortedLightSet.SimpleLights;
    const int32 NumViews = PassViews.Num();
    const FViewInfo& View0 = *PassViews[0];

    // 처리(Process)VS.
    TShaderMapRef<TDeferredLightVS<true>> VertexShader(View0.ShaderMap);
    TShaderRef<FMobileRadialLightFunctionPS> PixelShaders[2];
    {
        const FMaterialShaderMap* MaterialShaderMap = DefaultMaterial.Material->GetRenderingThreadShaderMap();
        FMobileRadialLightFunctionPS::FPermutationDomain PermutationVector;
        PermutationVector.Set<FMobileRadialLightFunctionPS::FSpotLightDim>(false);
        PermutationVector.Set<FMobileRadialLightFunctionPS::FIESProfileDim>(false);
        PermutationVector.Set<FMobileRadialLightFunctionPS::FInverseSquaredDim>(false);
        PixelShaders[0] = MaterialShaderMap->GetShader<FMobileRadialLightFunctionPS>(PermutationVector);
        PermutationVector.Set<FMobileRadialLightFunctionPS::FInverseSquaredDim>(true);
        PixelShaders[1] = MaterialShaderMap->GetShader<FMobileRadialLightFunctionPS>(PermutationVector);
    }

    // 설정(Set)PSO.
    FGraphicsPipelineStateInitializer GraphicsPSOLight[2];
    {
        SetupSimpleLightPSO(RHICmdList, View0, VertexShader, PixelShaders[0], GraphicsPSOLight[0]);
        SetupSimpleLightPSO(RHICmdList, View0, VertexShader, PixelShaders[1], GraphicsPSOLight[1]);
    }
    
    // 설정(Set)스텐실 버퍼(Stencil Buffer).
    FGraphicsPipelineStateInitializer GraphicsPSOLightMask;
    {
        RHICmdList.ApplyCachedRenderTargets(GraphicsPSOLightMask);
        GraphicsPSOLightMask.PrimitiveType = PT_TriangleList;
        GraphicsPSOLightMask.BlendState = TStaticBlendStateWriteMask<CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE>::GetRHI();
        GraphicsPSOLightMask.RasterizerState = View0.bReverseCulling ? TStaticRasterizerState<FM_Solid, CM_CCW>::GetRHI() : TStaticRasterizerState<FM_Solid, CM_CW>::GetRHI();
        // set stencil to 1 where depth test fails
        GraphicsPSOLightMask.DepthStencilState = TStaticDepthStencilState<
            false, CF_DepthNearOrEqual,
            true, CF_Always, SO_Keep, SO_Replace, SO_Keep,        
            false, CF_Always, SO_Keep, SO_Keep, SO_Keep,
            0x00, STENCIL_SANDBOX_MASK>::GetRHI();
        GraphicsPSOLightMask.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector4();
        GraphicsPSOLightMask.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
        GraphicsPSOLightMask.BoundShaderState.PixelShaderRHI = nullptr;
    }
    
    // 순회(Traverse/Iterate)라이트(Light)리스트 , 실행(Execute)셰이딩 계산/산출(Calculate).
    for (int32 LightIndex = 0; LightIndex < SimpleLights.InstanceData.Num(); LightIndex++)
    {
        const FSimpleLightEntry& SimpleLight = SimpleLights.InstanceData[LightIndex];
        for (int32 ViewIndex = 0; ViewIndex < NumViews; ViewIndex++)
        {
            const FViewInfo& View = *PassViews[ViewIndex];
            const FSimpleLightPerViewEntry& SimpleLightPerViewData = SimpleLights.GetViewDependentData(LightIndex, ViewIndex, NumViews);
            const FSphere LightBounds(SimpleLightPerViewData.Position, SimpleLight.Radius);
            
            if (NumViews > 1)
            {
                // set viewports only we we have more than one 
                // otherwise it is set at the start of the pass
                RHICmdList.SetViewport(View.ViewRect.Min.X, View.ViewRect.Min.Y, 0.0f, View.ViewRect.Max.X, View.ViewRect.Max.Y, 1.0f);
            }

            // 렌더링(Render)라이트(Light).
            SetGraphicsPipelineState(RHICmdList, GraphicsPSOLightMask);
            VertexShader->SetSimpleLightParameters(RHICmdList, View, LightBounds);
            RHICmdList.SetStencilRef(1);
            StencilingGeometry::DrawSphere(RHICmdList);
                        
            // 렌더링(Render)라이트(Light).
            FMobileRadialLightFunctionPS::FParameters PassParameters;
            FDeferredLightUniformStruct DeferredLightUniformsValue;
            SetupSimpleDeferredLightParameters(SimpleLight, SimpleLightPerViewData, DeferredLightUniformsValue);
            PassParameters.DeferredLightUniforms = TUniformBufferRef<FDeferredLightUniformStruct>::CreateUniformBufferImmediate(DeferredLightUniformsValue, EUniformBufferUsage::UniformBuffer_SingleFrame);
            PassParameters.IESTexture = GWhiteTexture->TextureRHI;
            PassParameters.IESTextureSampler = GWhiteTexture->SamplerStateRHI;
            if (SimpleLight.Exponent == 0)
            {
                SetGraphicsPipelineState(RHICmdList, GraphicsPSOLight[1]);
                FMobileRadialLightFunctionPS::SetParameters(RHICmdList, PixelShaders[1], View, DefaultMaterial.MaterialProxy, *DefaultMaterial.Material, PassParameters);
            }
            else
            {
                SetGraphicsPipelineState(RHICmdList, GraphicsPSOLight[0]);
                FMobileRadialLightFunctionPS::SetParameters(RHICmdList, PixelShaders[0], View, DefaultMaterial.MaterialProxy, *DefaultMaterial.Material, PassParameters);
            }
            VertexShader->SetSimpleLightParameters(RHICmdList, View, LightBounds);
            
            // 오직 드로우/렌더(Draw)기본값(Default)라이팅 모델 (MSM_DefaultLit)의 픽셀(Pixel).
            uint8 StencilRef = GET_STENCIL_MOBILE_SM_MASK(MSM_DefaultLit);
            RHICmdList.SetStencilRef(StencilRef);

            // 을(를) 활용하여 렌더링(Render)라이트(Light)(포인트 라이트(Light) 및 ), 로써 컬링 라이트(Light)의 픽셀(Pixel).
            StencilingGeometry::DrawSphere(RHICmdList);
        }
    }
}

// 렌더링(Render)로컬 라이트(Light).
static void RenderLocalLight(
    FRHICommandListImmediate& RHICmdList, 
    const FScene& Scene, 
    const FViewInfo& View, 
    const FLightSceneInfo& LightSceneInfo, 
    const FCachedLightMaterial& DefaultLightMaterial)
{
    if (!LightSceneInfo.ShouldRenderLight(View))
    {
        return;
    }

    // 무시/스킵(Ignore)로컬 라이트(Light)(라이트(Light) 및 의 라이트(Light)).
    const uint8 LightType = LightSceneInfo.Proxy->GetLightType();
    const bool bIsSpotLight = LightType == LightType_Spot;
    const bool bIsPointLight = LightType == LightType_Point;
    if (!bIsSpotLight && !bIsPointLight)
    {
        return;
    }
    
    // 드로우/렌더(Draw)라이트(Light)스텐실 .
    if (GMobileUseLightStencilCulling != 0)
    {
        RenderLocalLight_StencilMask(RHICmdList, Scene, View, LightSceneInfo);
    }

    // 처리(Process)IES라이팅 .
    bool bUseIESTexture = false;
    FTexture* IESTextureResource = GWhiteTexture;
    if (View.Family->EngineShowFlags.TexturedLightProfiles && LightSceneInfo.Proxy->GetIESTextureResource())
    {
        IESTextureResource = LightSceneInfo.Proxy->GetIESTextureResource();
        bUseIESTexture = true;
    }
        
    FGraphicsPipelineStateInitializer GraphicsPSOInit;
    RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
    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();
    
    // 설정(Set)라이트(Light)래스터라이제이션 및 뎁스 상태 .
    if (GMobileUseLightStencilCulling != 0)
    {
        SetLocalLightRasterizerAndDepthState_StencilMask(GraphicsPSOInit, View);
    }
    else
    {
        SetLocalLightRasterizerAndDepthState(GraphicsPSOInit, View, LightBounds);
    }

    // 설정(Set)VS
    TShaderMapRef<TDeferredLightVS<true>> VertexShader(View.ShaderMap);
        
    const FMaterialRenderProxy* LightFunctionMaterialProxy = nullptr;
    if (View.Family->EngineShowFlags.LightFunctions)
    {
        LightFunctionMaterialProxy = LightSceneInfo.Proxy->GetLightFunctionMaterial();
    }
    FMobileRadialLightFunctionPS::FPermutationDomain PermutationVector;
    PermutationVector.Set<FMobileRadialLightFunctionPS::FSpotLightDim>(bIsSpotLight);
    PermutationVector.Set<FMobileRadialLightFunctionPS::FInverseSquaredDim>(LightSceneInfo.Proxy->IsInverseSquared());
    PermutationVector.Set<FMobileRadialLightFunctionPS::FIESProfileDim>(bUseIESTexture);
    FCachedLightMaterial LightMaterial;
    TShaderRef<FMobileRadialLightFunctionPS> PixelShader;
    GetLightMaterial(DefaultLightMaterial, LightFunctionMaterialProxy, PermutationVector.ToDimensionValueId(), LightMaterial, PixelShader);
            
    GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector4();
    GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
    GraphicsPSOInit.BoundShaderState.PixelShaderRHI = PixelShader.GetPixelShader();
    SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);

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

    // 설정(Set)PS.
    FMobileRadialLightFunctionPS::FParameters PassParameters;
    PassParameters.DeferredLightUniforms = TUniformBufferRef<FDeferredLightUniformStruct>::CreateUniformBufferImmediate(GetDeferredLightParameters(View, LightSceneInfo), EUniformBufferUsage::UniformBuffer_SingleFrame);
    PassParameters.IESTexture = IESTextureResource->TextureRHI;
    PassParameters.IESTextureSampler = IESTextureResource->SamplerStateRHI;
    const float TanOuterAngle = bIsSpotLight ? FMath::Tan(LightSceneInfo.Proxy->GetOuterConeAngle()) : 1.0f;
    PassParameters.LightFunctionParameters = FVector4(TanOuterAngle, 1.0f /*ShadowFadeFraction*/, bIsSpotLight ? 1.0f : 0.0f, bIsPointLight ? 1.0f : 0.0f);
    PassParameters.LightFunctionParameters2 = FVector(LightSceneInfo.Proxy->GetLightFunctionFadeDistance(), LightSceneInfo.Proxy->GetLightFunctionDisabledBrightness(),    0.0f);
    const FVector Scale = LightSceneInfo.Proxy->GetLightFunctionScale();
    // Switch x and z so that z of the user specified scale affects the distance along the light direction
    const FVector InverseScale = FVector(1.f / Scale.Z, 1.f / Scale.Y, 1.f / Scale.X);
    PassParameters.WorldToLight = LightSceneInfo.Proxy->GetWorldToLight() * FScaleMatrix(FVector(InverseScale));
    FMobileRadialLightFunctionPS::SetParameters(RHICmdList, PixelShader, View, LightMaterial.MaterialProxy, *LightMaterial.Material, PassParameters);

    // 오직 드로우/렌더(Draw)기본값(Default)라이팅 모델 (MSM_DefaultLit)의 픽셀(Pixel).
    uint8 StencilRef = GET_STENCIL_MOBILE_SM_MASK(MSM_DefaultLit);
    RHICmdList.SetStencilRef(StencilRef);

    // 포인트 라이트(Light)을(를) 활용하여 드로우/렌더(Draw).
    if (LightType == LightType_Point)
    {
        StencilingGeometry::DrawSphere(RHICmdList);
    }
    // 을(를) 활용하여 드로우/렌더(Draw).
    else // LightType_Spot
    {
        StencilingGeometry::DrawCone(RHICmdList);
    }
}

광원을 그릴 때 광원의 종류에 따라 방향성 광원, 비클러스터형 단순 광원, 로컬 광원(점 광원 및 스포트라이트)의 3단계로 구분됩니다. 모바일 단말기는 기본 조명 모델(MSM_DefaultLit)의 계산만 지원하며 기타 고급 조명 모델(머리카락, 서브서피스 스캐터링(SSS), 바니시, 눈, 천 등)은 아직 지원되지 않습니다.

평행광을 그릴 때 최대 하나만 그릴 수 있습니다. 전체 화면 직사각형 그리기가 사용되며 여러 수준의 CSM 그림자가 지원됩니다.

클러스터되지 않은 단순 광원을 그릴 때 점 조명이든 스포트라이트이든 구로 그려지며 그림자는 지원되지 않습니다.

로컬 광원을 그릴 때는 훨씬 더 복잡해집니다. 로컬 광원 템플릿 버퍼를 먼저 그린 다음 래스터라이제이션 및 깊이 상태를 설정하고 마지막으로 광원을 그립니다. 점 광원은 구를 사용하여 그려지며 그림자를 지원하지 않습니다. 스포트라이트는 원뿔을 사용하여 그려지며 그림자를 지원할 수 있습니다. 기본적으로 스포트라이트는 동적 빛과 그림자 계산을 지원하지 않으며 프로젝트 구성에서 켜야 합니다.

또한 광원과 교차하지 않는 픽셀을 제거하기 위해 템플릿을 활성화할지 여부는 GMobileUseLightStencilCulling에 의해 결정되고, 이는 다시 r.Mobile.UseLightStencilCulling에 의해 결정되며 기본값은 1(즉, 켜짐)입니다. 광원 렌더링을 위한 스텐실 버퍼 코드는 다음과 같습니다.

cpp
static void RenderLocalLight_StencilMask(FRHICommandListImmediate& RHICmdList, const FScene& Scene, const FViewInfo& View, const FLightSceneInfo& LightSceneInfo)
{
    const uint8 LightType = LightSceneInfo.Proxy->GetLightType();

    FGraphicsPipelineStateInitializer GraphicsPSOInit;
    // 적용 캐싱(Cache)의 RT(컬러 /뎁스 ).
    RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
    GraphicsPSOInit.PrimitiveType = PT_TriangleList;
    // 비활성화(Disable)RT의 .
    GraphicsPSOInit.BlendState = TStaticBlendStateWriteMask<CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE, CW_NONE>::GetRHI();
    GraphicsPSOInit.RasterizerState = View.bReverseCulling ? TStaticRasterizerState<FM_Solid, CM_CCW>::GetRHI() : TStaticRasterizerState<FM_Solid, CM_CW>::GetRHI();
    // 만약 뎁스 , 이면 기록/쓰기(Write)스텐실 버퍼(Stencil Buffer)로 1.
    GraphicsPSOInit.DepthStencilState = TStaticDepthStencilState<
        false, CF_DepthNearOrEqual,
        true, CF_Always, SO_Keep, SO_Replace, SO_Keep,        
        false, CF_Always, SO_Keep, SO_Keep, SO_Keep,
        0x00, 
        // ※ 주의: 오직 기록/쓰기(Write)Pass을(를) 활용하여 의 (SANBOX), 즉, 스텐실 버퍼(Stencil Buffer)의 로 0의 .
        STENCIL_SANDBOX_MASK>::GetRHI();
    
    // 드로우/렌더(Draw)라이트(Light)스텐실 의 VS는 TDeferredLightVS.
    TShaderMapRef<TDeferredLightVS<true> > VertexShader(View.ShaderMap);
    GraphicsPSOInit.BoundShaderState.VertexDeclarationRHI = GetVertexDeclarationFVector4();
    GraphicsPSOInit.BoundShaderState.VertexShaderRHI = VertexShader.GetVertexShader();
    // PS로 .
    GraphicsPSOInit.BoundShaderState.PixelShaderRHI = nullptr;

    SetGraphicsPipelineState(RHICmdList, GraphicsPSOInit);
    VertexShader->SetParameters(RHICmdList, View, &LightSceneInfo);
    // 스텐실 로 1.
    RHICmdList.SetStencilRef(1);

    // 에 따라 라이트(Light)을(를) 활용하여 드로우/렌더(Draw).
    if (LightType == LightType_Point)
    {
        StencilingGeometry::DrawSphere(RHICmdList);
    }
    else // LightType_Spot
    {
        StencilingGeometry::DrawCone(RHICmdList);
    }
}

각 로컬 광원은 먼저 광원 범위 내에 마스크를 그린 다음 스텐실 테스트(Early-Z)를 통과한 픽셀의 조도를 계산합니다. 구체적인 분석 과정은 다음과 같습니다.

상단: 장면에서 렌더링을 기다리는 스포트라이트; 중간: 템플릿 패스를 사용하여 그린 템플릿 마스크(흰색 영역)는 스포트라이트의 모양과 겹치고 깊이가 더 가까운 화면 공간의 픽셀을 표시합니다. 하단: 유효 픽셀에 대한 조명 계산의 효과.

유효 픽셀에 대한 조명 계산을 수행할 때 사용되는 DepthStencil 상태는 다음과 같습니다.

텍스트로 변환하면 조명을 수행하는 픽셀은 광원 모양 내에 있어야 하며 광원 모양 외부의 픽셀은 제거됩니다. 템플릿 패스는 광원 모양의 깊이에 더 가까운 픽셀(광원 모양 몸체 외부의 픽셀)을 표시합니다. 광원 드로잉 패스는 템플릿 테스트를 통해 템플릿 패스로 표시된 픽셀을 제거한 다음 깊이 테스트를 사용하여 광원 형상 본체 내의 픽셀을 찾아 조명 계산 효율성을 향상시킵니다.

모바일 측의 이 Light Stencil Culling 기술은 Siggraph2020의 Unity URP의 Unity 음성 Deferred Shading에서 언급된 템플릿 기반 조명 계산과 유사합니다(아이디어는 동일하지만 접근 방식이 완전히 동일하지 않을 수 있음). 또한 이 논문에서는 광원의 모양에 더 잘 맞는 기하학적 시뮬레이션을 제안합니다.

그리고 PC와 모바일 단말에서 다양한 광원 계산 방식의 성능을 비교하기 위해 Mali GPU의 비교 차트는 다음과 같습니다.

다양한 조명 렌더링 기술을 사용한 Mali GPU의 성능 비교. 모바일 단말기에서는 템플릿 클리핑을 기반으로 한 조명 알고리즘이 기존 및 차단 알고리즘보다 우수하다는 것을 알 수 있습니다.

GPU의 Early-Z 기술과 결합된 광원 템플릿 클리핑 기술은 조명 렌더링 성능을 크게 향상시킬 것이라는 점은 언급할 가치가 있습니다. 현재 주류 모바일 GPU는 모두 Early-Z 기술을 지원하며 이는 또한 광원 템플릿 클리핑 적용의 기반을 마련합니다.

현재 UE가 구현하는 광원 클리핑 알고리즘에는 아직 개선의 여지가 있을 수 있습니다. 예를 들어, 광원에서 멀어지는 픽셀(아래 그림의 빨간색 상자에 표시)은 실제로 계산할 필요가 없습니다. (그러나 광원에서 멀어지는 픽셀을 빠르고 효과적으로 찾는 방법은 또 다른 문제입니다)

12.3.3.2 MobileBasePassShader

이 섹션에서는 VS 및 PS를 포함하여 모바일 단말기의 BasePass와 관련된 셰이더를 주로 설명합니다. 먼저 VS를 살펴보겠습니다.

cpp
// Engine\Shaders\Private\MobileBasePassVertexShader.usf

(......)

struct FMobileShadingBasePassVSToPS
{
    FVertexFactoryInterpolantsVSToPS FactoryInterpolants;
    FMobileBasePassInterpolantsVSToPS BasePassInterpolants;
    float4 Position : SV_POSITION;
};

#define FMobileShadingBasePassVSOutput FMobileShadingBasePassVSToPS
#define VertexFactoryGetInterpolants VertexFactoryGetInterpolantsVSToPS

// VS.
void Main(
    FVertexFactoryInput Input
    , out FMobileShadingBasePassVSOutput Output
#if INSTANCED_STEREO
    , uint InstanceId : SV_InstanceID
    , out uint LayerIndex : SV_RenderTargetArrayIndex
#elif MOBILE_MULTI_VIEW
    , in uint ViewId : SV_ViewID
#endif
    )
{
// 뷰(View).
#if INSTANCED_STEREO
    const uint EyeIndex = GetEyeIndex(InstanceId);
    ResolvedView = ResolveView(EyeIndex);
    LayerIndex = EyeIndex;
    Output.BasePassInterpolants.MultiViewId = float(EyeIndex);
// 뷰(View).
#elif MOBILE_MULTI_VIEW
    #if COMPILER_GLSL_ES3_1
        const int MultiViewId = int(ViewId);
        ResolvedView = ResolveView(uint(MultiViewId));
        Output.BasePassInterpolants.MultiViewId = float(MultiViewId);
    #else
        ResolvedView = ResolveView(ViewId);
        Output.BasePassInterpolants.MultiViewId = float(ViewId);
    #endif
#else
    ResolvedView = ResolveView();
#endif

    // 초기화(Initialize)의 보간(Interpolation)데이터 .
#if PACK_INTERPOLANTS
    float4 PackedInterps[NUM_VF_PACKED_INTERPOLANTS];
    UNROLL 
    for(int i = 0; i < NUM_VF_PACKED_INTERPOLANTS; ++i)
    {
        PackedInterps[i] = 0;
    }
#endif

    // 처리(Process)포인트 데이터 .
    FVertexFactoryIntermediates VFIntermediates = GetVertexFactoryIntermediates(Input);
    float4 WorldPositionExcludingWPO = VertexFactoryGetWorldPosition(Input, VFIntermediates);
    float4 WorldPosition = WorldPositionExcludingWPO;

    // 페치/가져오기(Fetch)머티리얼(Material)의 포인트 데이터 , 처리(Process)좌표 .
    half3x3 TangentToLocal = VertexFactoryGetTangentToLocal(Input, VFIntermediates);    
    FMaterialVertexParameters VertexParameters = GetMaterialVertexParameters(Input, VFIntermediates, WorldPosition.xyz, TangentToLocal);

    half3 WorldPositionOffset = GetMaterialWorldPositionOffset(VertexParameters);
    
    WorldPosition.xyz += WorldPositionOffset;

    float4 RasterizedWorldPosition = VertexFactoryGetRasterizedWorldPosition(Input, VFIntermediates, WorldPosition);
    Output.Position = mul(RasterizedWorldPosition, ResolvedView.TranslatedWorldToClip);
    Output.BasePassInterpolants.PixelPosition = WorldPosition;

#if USE_WORLD_POSITION_EXCLUDING_SHADER_OFFSETS
    Output.BasePassInterpolants.PixelPositionExcludingWPO = WorldPositionExcludingWPO.xyz;
#endif

    // 클리핑/클램핑(Clipping).
#if USE_PS_CLIP_PLANE
    Output.BasePassInterpolants.OutClipDistance = dot(ResolvedView.GlobalClippingPlane, float4(WorldPosition.xyz - ResolvedView.PreViewTranslation.xyz, 1));
#endif

    // 포인트 .
#if USE_VERTEX_FOG
    float4 VertexFog = CalculateHeightFog(WorldPosition.xyz - ResolvedView.TranslatedWorldCameraOrigin);

    #if PROJECT_SUPPORT_SKY_ATMOSPHERE && MATERIAL_IS_SKY==0 // Do not apply aerial perpsective on sky materials
        if (ResolvedView.SkyAtmosphereApplyCameraAerialPerspectiveVolume > 0.0f)
        {
            const float OneOverPreExposure = USE_PREEXPOSURE ? ResolvedView.OneOverPreExposure : 1.0f;
            // Sample the aerial perspective (AP). It is also blended under the VertexFog parameter.
            VertexFog = GetAerialPerspectiveLuminanceTransmittanceWithFogOver(
                ResolvedView.RealTimeReflectionCapture, ResolvedView.SkyAtmosphereCameraAerialPerspectiveVolumeSizeAndInvSize,
                Output.Position, WorldPosition.xyz*CM_TO_SKY_UNIT, ResolvedView.TranslatedWorldCameraOrigin*CM_TO_SKY_UNIT,
                View.CameraAerialPerspectiveVolume, View.CameraAerialPerspectiveVolumeSampler,
                ResolvedView.SkyAtmosphereCameraAerialPerspectiveVolumeDepthResolutionInv,
                ResolvedView.SkyAtmosphereCameraAerialPerspectiveVolumeDepthResolution,
                ResolvedView.SkyAtmosphereAerialPerspectiveStartDepthKm,
                ResolvedView.SkyAtmosphereCameraAerialPerspectiveVolumeDepthSliceLengthKm,
                ResolvedView.SkyAtmosphereCameraAerialPerspectiveVolumeDepthSliceLengthKmInv,
                OneOverPreExposure, VertexFog);
        }
    #endif

    #if PACK_INTERPOLANTS
        PackedInterps[0] = VertexFog;
    #else
        Output.BasePassInterpolants.VertexFog = VertexFog;
    #endif // PACK_INTERPOLANTS
#endif // USE_VERTEX_FOG

    (......)

    // 페치/가져오기(Fetch)보간(Interpolation)의 데이터 .
    Output.FactoryInterpolants = VertexFactoryGetInterpolants(Input, VFIntermediates, VertexParameters);

    Output.BasePassInterpolants.PixelPosition.w = Output.Position.w;

    // 보간(Interpolation)데이터 .
#if PACK_INTERPOLANTS
    VertexFactoryPackInterpolants(Output.FactoryInterpolants, PackedInterps);
#endif // PACK_INTERPOLANTS

#if !OUTPUT_MOBILE_HDR && COMPILER_GLSL_ES3_1
    Output.Position.y *= -1;
#endif
}

위에서 볼 수 있듯이 뷰 인스턴스는 3차원 렌더링, 멀티뷰, 일반 모드에 따라 다르게 처리됩니다. Vertex Fog는 지원되지만 기본적으로 꺼져 있으며 프로젝트 구성에서 켜져야 합니다.

VS와 PS 사이의 보간 소비 및 대역폭을 압축하기 위해 압축 보간 모드가 있습니다. 활성화 여부는 다음과 같이 정의된 PACK_INTERPOLANTS 매크로에 의해 결정됩니다.

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

#define PACK_INTERPOLANTS (USE_VERTEX_FOG && NUM_VF_PACKED_INTERPOLANTS > 0 && (ES3_1_PROFILE))

즉, 패킹된 보간 기능은 버텍스 포그가 켜져 있고, 버텍스 팩토리에 패킹된 보간 데이터가 있고, OpenGLES3.1 셰이딩 플랫폼을 사용하는 경우에만 활성화됩니다. BasePass VS의 PC 버전과 비교하여 모바일 버전은 크게 단순화되었으며 단순히 PC 버전의 작은 하위 집합으로 간주될 수 있습니다. 계속해서 PS를 분석해 보겠습니다.

cpp
// Engine\Shaders\Private\MobileBasePassPixelShader.usf

#include "Common.ush"

// 정의 .
#define MobileSceneTextures MobileBasePass.SceneTextures
#define EyeAdaptationStruct MobileBasePass

(......)

// 렌더링(Render)객체/오브젝트 의 의 정규화(Normalize)(거침(Rough)머티리얼(Material)지원 )
#if !FULLY_ROUGH
    #if HQ_REFLECTIONS
    #define MAX_HQ_REFLECTIONS 3
    TextureCube ReflectionCubemap0;
    SamplerState ReflectionCubemapSampler0;
    TextureCube ReflectionCubemap1;
    SamplerState ReflectionCubemapSampler1;
    TextureCube ReflectionCubemap2;
    SamplerState ReflectionCubemapSampler2;
    // x,y,z - inverted average brightness for 0, 1, 2; w - sky cube texture max mips.
    float4 ReflectionAverageBrigtness;
    float4 ReflectanceMaxValueRGBM;
    float4 ReflectionPositionsAndRadii[MAX_HQ_REFLECTIONS];
        #if ALLOW_CUBE_REFLECTIONS
        float4x4 CaptureBoxTransformArray[MAX_HQ_REFLECTIONS];
        float4 CaptureBoxScalesArray[MAX_HQ_REFLECTIONS];
        #endif
    #endif
#endif

// 반사(Reflection)/IBL인터페이스 .
half4 GetPlanarReflection(float3 WorldPosition, half3 WorldNormal, half Roughness);
half MobileComputeMixingWeight(half IndirectIrradiance, half AverageBrightness, half Roughness);
half3 GetLookupVectorForBoxCaptureMobile(half3 ReflectionVector, ...);
half3 GetLookupVectorForSphereCaptureMobile(half3 ReflectionVector, ...);
void GatherSpecularIBL(FMaterialPixelParameters MaterialParameters, ...);
void BlendReflectionCaptures(FMaterialPixelParameters MaterialParameters, ...)
half3 GetImageBasedReflectionLighting(FMaterialPixelParameters MaterialParameters, ...);

// 인터페이스 .
half3 FrameBufferBlendOp(half4 Source);
bool UseCSM();
void ApplyPixelDepthOffsetForMobileBasePass(inout FMaterialPixelParameters MaterialParameters, FPixelMaterialInputs PixelMaterialInputs, out float OutDepth);

// 누적(Accumulate)동적 포인트 라이트(Light).
#if MAX_DYNAMIC_POINT_LIGHTS > 0
void AccumulateLightingOfDynamicPointLight(
    FMaterialPixelParameters MaterialParameters, 
    FMobileShadingModelContext ShadingModelContext,
    FGBufferData GBuffer,
    float4 LightPositionAndInvRadius, 
    float4 LightColorAndFalloffExponent, 
    float4 SpotLightDirectionAndSpecularScale, 
    float4 SpotLightAnglesAndSoftTransitionScaleAndLightShadowType, 
    #if SUPPORT_SPOTLIGHTS_SHADOW
    FPCFSamplerSettings Settings,
    float4 SpotLightShadowSharpenAndShadowFadeFraction,
    float4 SpotLightShadowmapMinMax,
    float4x4 SpotLightShadowWorldToShadowMatrix,
    #endif
    inout half3 Color)
{
    uint LightShadowType = SpotLightAnglesAndSoftTransitionScaleAndLightShadowType.w;
    float FadedShadow = 1.0f;

    // 계산/산출(Calculate)그림자(Shadow).
#if SUPPORT_SPOTLIGHTS_SHADOW
    if ((LightShadowType & LightShadowType_Shadow) == LightShadowType_Shadow)
    {

        float4 HomogeneousShadowPosition = mul(float4(MaterialParameters.AbsoluteWorldPosition, 1), SpotLightShadowWorldToShadowMatrix);
        float2 ShadowUVs = HomogeneousShadowPosition.xy / HomogeneousShadowPosition.w;
        if (all(ShadowUVs >= SpotLightShadowmapMinMax.xy && ShadowUVs <= SpotLightShadowmapMinMax.zw))
        {
            // Clamp pixel depth in light space for shadowing opaque, because areas of the shadow depth buffer that weren't rendered to will have been cleared to 1
            // We want to force the shadow comparison to result in 'unshadowed' in that case, regardless of whether the pixel being shaded is in front or behind that plane
            float LightSpacePixelDepthForOpaque = min(HomogeneousShadowPosition.z, 0.99999f);
            Settings.SceneDepth = LightSpacePixelDepthForOpaque;
            Settings.TransitionScale = SpotLightAnglesAndSoftTransitionScaleAndLightShadowType.z;

            half Shadow = MobileShadowPCF(ShadowUVs, Settings);

            Shadow = saturate((Shadow - 0.5) * SpotLightShadowSharpenAndShadowFadeFraction.x + 0.5);

            FadedShadow = lerp(1.0f, Square(Shadow), SpotLightShadowSharpenAndShadowFadeFraction.y);
        }
    }
#endif

    // 계산/산출(Calculate)라이팅 .
    if ((LightShadowType & ValidLightType) != 0)
    {
        float3 ToLight = LightPositionAndInvRadius.xyz - MaterialParameters.AbsoluteWorldPosition;
        float DistanceSqr = dot(ToLight, ToLight);
        float3 L = ToLight * rsqrt(DistanceSqr);
        half3 PointH = normalize(MaterialParameters.CameraVector + L);

        half PointNoL = max(0, dot(MaterialParameters.WorldNormal, L));
        half PointNoH = max(0, dot(MaterialParameters.WorldNormal, PointH));

        // 계산/산출(Calculate)라이트(Light)의 감쇠(Falloff/Attenuation).
        float Attenuation;
        if (LightColorAndFalloffExponent.w == 0)
        {
            // Sphere falloff (technically just 1/d2 but this avoids inf)
            Attenuation = 1 / (DistanceSqr + 1);

            float LightRadiusMask = Square(saturate(1 - Square(DistanceSqr * (LightPositionAndInvRadius.w * LightPositionAndInvRadius.w))));
            Attenuation *= LightRadiusMask;
        }
        else
        {
            Attenuation = RadialAttenuation(ToLight * LightPositionAndInvRadius.w, LightColorAndFalloffExponent.w);
        }

#if PROJECT_MOBILE_ENABLE_MOVABLE_SPOTLIGHTS
        if ((LightShadowType & LightShadowType_SpotLight) == LightShadowType_SpotLight)
        {
            Attenuation *= SpotAttenuation(L, -SpotLightDirectionAndSpecularScale.xyz, SpotLightAnglesAndSoftTransitionScaleAndLightShadowType.xy) * FadedShadow;
        }
#endif

        // 누적 합산(Accumulate)라이팅 결과 .
#if !FULLY_ROUGH
        FMobileDirectLighting Lighting = MobileIntegrateBxDF(ShadingModelContext, GBuffer, PointNoL, MaterialParameters.CameraVector, PointH, PointNoH);
        Color += min(65000.0, (Attenuation) * LightColorAndFalloffExponent.rgb * (1.0 / PI) * (Lighting.Diffuse + Lighting.Specular * SpotLightDirectionAndSpecularScale.w));
#else
        Color += (Attenuation * PointNoL) * LightColorAndFalloffExponent.rgb * (1.0 / PI) * ShadingModelContext.DiffuseColor;
#endif
    }
}
#endif

(......)

// 계산/산출(Calculate)라이팅 .
half ComputeIndirect(VTPageTableResult LightmapVTPageTableResult, FVertexFactoryInterpolantsVSToPS Interpolants, float3 DiffuseDir, FMobileShadingModelContext ShadingModelContext, out half IndirectIrradiance, out half3 Color)
{
    //To keep IndirectLightingCache conherence with PC, initialize the IndirectIrradiance to zero.
    IndirectIrradiance = 0;
    Color = 0;

    // 디퓨즈(Diffuse).
#if LQ_TEXTURE_LIGHTMAP
    float2 LightmapUV0, LightmapUV1;
    uint LightmapDataIndex;
    GetLightMapCoordinates(Interpolants, LightmapUV0, LightmapUV1, LightmapDataIndex);

    half4 LightmapColor = GetLightMapColorLQ(LightmapVTPageTableResult, LightmapUV0, LightmapUV1, LightmapDataIndex, DiffuseDir);
    Color += LightmapColor.rgb * ShadingModelContext.DiffuseColor * View.IndirectLightingColorScale;
    IndirectIrradiance = LightmapColor.a;
#elif CACHED_POINT_INDIRECT_LIGHTING
    #if MATERIALBLENDING_MASKED || MATERIALBLENDING_SOLID
        // 노멀(Normal)적용 까지 반투명(Translucent).
        FThreeBandSHVectorRGB PointIndirectLighting;
        PointIndirectLighting.R.V0 = IndirectLightingCache.IndirectLightingSHCoefficients0[0];
        PointIndirectLighting.R.V1 = IndirectLightingCache.IndirectLightingSHCoefficients1[0];
        PointIndirectLighting.R.V2 = IndirectLightingCache.IndirectLightingSHCoefficients2[0];

        PointIndirectLighting.G.V0 = IndirectLightingCache.IndirectLightingSHCoefficients0[1];
        PointIndirectLighting.G.V1 = IndirectLightingCache.IndirectLightingSHCoefficients1[1];
        PointIndirectLighting.G.V2 = IndirectLightingCache.IndirectLightingSHCoefficients2[1];

        PointIndirectLighting.B.V0 = IndirectLightingCache.IndirectLightingSHCoefficients0[2];
        PointIndirectLighting.B.V1 = IndirectLightingCache.IndirectLightingSHCoefficients1[2];
        PointIndirectLighting.B.V2 = IndirectLightingCache.IndirectLightingSHCoefficients2[2];

        FThreeBandSHVector DiffuseTransferSH = CalcDiffuseTransferSH3(DiffuseDir, 1);

        // 계산/산출(Calculate)추가(Add)노멀(Normal)의 디퓨즈(Diffuse)라이팅 .
        half3 DiffuseGI = max(half3(0, 0, 0), DotSH3(PointIndirectLighting, DiffuseTransferSH));

        IndirectIrradiance = Luminance(DiffuseGI);
        Color += ShadingModelContext.DiffuseColor * DiffuseGI * View.IndirectLightingColorScale;
    #else 
        // 반투명(Translucent)을(를) 활용하여 (Non-directional), 디퓨즈(Diffuse)xyz, cpuPI 및 SH디퓨즈(Diffuse).
        half3 PointIndirectLighting = IndirectLightingCache.IndirectLightingSHSingleCoefficient.rgb;
        half3 DiffuseGI = PointIndirectLighting;

        IndirectIrradiance = Luminance(DiffuseGI);
        Color += ShadingModelContext.DiffuseColor * DiffuseGI * View.IndirectLightingColorScale;
    #endif
#endif

    return IndirectIrradiance;
}

// PS.
PIXELSHADER_EARLYDEPTHSTENCIL
void Main( 
    FVertexFactoryInterpolantsVSToPS Interpolants
    , FMobileBasePassInterpolantsVSToPS BasePassInterpolants
    , in float4 SvPosition : SV_Position
    OPTIONAL_IsFrontFace
    , out half4 OutColor    : SV_Target0
#if DEFERRED_SHADING_PATH
    , out half4 OutGBufferA    : SV_Target1
    , out half4 OutGBufferB    : SV_Target2
    , out half4 OutGBufferC    : SV_Target3
#endif
#if USE_SCENE_DEPTH_AUX
    , out float OutSceneDepthAux : SV_Target4
#endif
#if OUTPUT_PIXEL_DEPTH_OFFSET
    , out float OutDepth : SV_Depth
#endif
    )
{
#if MOBILE_MULTI_VIEW
    ResolvedView = ResolveView(uint(BasePassInterpolants.MultiViewId));
#else
    ResolvedView = ResolveView();
#endif

#if USE_PS_CLIP_PLANE
    clip(BasePassInterpolants.OutClipDistance);
#endif

    // 압축해제/언패킹(Unpacking)의 보간(Interpolation)데이터 .
#if PACK_INTERPOLANTS
    float4 PackedInterpolants[NUM_VF_PACKED_INTERPOLANTS];
    VertexFactoryUnpackInterpolants(Interpolants, PackedInterpolants);
#endif

#if COMPILER_GLSL_ES3_1 && !OUTPUT_MOBILE_HDR && !MOBILE_EMULATION
    // LDR Mobile needs screen vertical flipped
    SvPosition.y = ResolvedView.BufferSizeAndInvSize.y - SvPosition.y - 1;
#endif

    // 페치/가져오기(Fetch)머티리얼(Material)의 픽셀(Pixel).
    FMaterialPixelParameters MaterialParameters = GetMaterialPixelParameters(Interpolants, SvPosition);
    FPixelMaterialInputs PixelMaterialInputs;
    {
        float4 ScreenPosition = SvPositionToResolvedScreenPosition(SvPosition);
        float3 WorldPosition = BasePassInterpolants.PixelPosition.xyz;
        float3 WorldPositionExcludingWPO = BasePassInterpolants.PixelPosition.xyz;
        #if USE_WORLD_POSITION_EXCLUDING_SHADER_OFFSETS
            WorldPositionExcludingWPO = BasePassInterpolants.PixelPositionExcludingWPO;
        #endif
        CalcMaterialParametersEx(MaterialParameters, PixelMaterialInputs, SvPosition, ScreenPosition, bIsFrontFace, WorldPosition, WorldPositionExcludingWPO);

#if FORCE_VERTEX_NORMAL
        // Quality level override of material's normal calculation, can be used to avoid normal map reads etc.
        MaterialParameters.WorldNormal = MaterialParameters.TangentToWorld[2];
        MaterialParameters.ReflectionVector = ReflectionAboutCustomWorldNormal(MaterialParameters, MaterialParameters.WorldNormal, false);
#endif
    }

    // 픽셀(Pixel)뎁스 .
#if OUTPUT_PIXEL_DEPTH_OFFSET
    ApplyPixelDepthOffsetForMobileBasePass(MaterialParameters, PixelMaterialInputs, OutDepth);
#endif
      
    // Mask머티리얼(Material).
#if !EARLY_Z_PASS_ONLY_MATERIAL_MASKING
    //Clip if the blend mode requires it.
    GetMaterialCoverageAndClipping(MaterialParameters, PixelMaterialInputs);
#endif

    // 계산/산출(Calculate)캐싱(Cache)GBuffer데이터 , 회 을(를) 활용하여 텍스처(Texture).
    FGBufferData GBuffer = (FGBufferData)0;
    GBuffer.WorldNormal = MaterialParameters.WorldNormal;
    GBuffer.BaseColor = GetMaterialBaseColor(PixelMaterialInputs);
    GBuffer.Metallic = GetMaterialMetallic(PixelMaterialInputs);
    GBuffer.Specular = GetMaterialSpecular(PixelMaterialInputs);
    GBuffer.Roughness = GetMaterialRoughness(PixelMaterialInputs);
    GBuffer.ShadingModelID = GetMaterialShadingModel(PixelMaterialInputs);
    half MaterialAO = GetMaterialAmbientOcclusion(PixelMaterialInputs);

    // 적용 AO.
#if APPLY_AO
    half4 GatheredAmbientOcclusion = Texture2DSample(AmbientOcclusionTexture, AmbientOcclusionSampler, SvPositionToBufferUV(SvPosition));

    MaterialAO *= GatheredAmbientOcclusion.r;
#endif

    GBuffer.GBufferAO = MaterialAO;

    // 에 의해 IEEE 754 (FP16)테이블 의 는 2^-24 = 5.96e-8, 의 러프니스(Roughness) 및 까지 1.0 / Roughness^4의 계산/산출(Calculate), 따라서 를 위해 , Roughness^4 >= 5.96e-8, Clamp거칠기(Roughness)까지 0.015625(0.015625^4 = 5.96e-8).
    // , 를 위해 매칭 PC의 레이턴시 렌더링(Render)(러프니스(Roughness)저장(Store)8의 ), Clamp까지 1.0.
    GBuffer.Roughness = max(0.015625, GetMaterialRoughness(PixelMaterialInputs));
    
    // 초기화(Initialize)모바일 플랫폼(Mobile)셰이딩 모델 FMobileShadingModelContext.
    FMobileShadingModelContext ShadingModelContext = (FMobileShadingModelContext)0;
    ShadingModelContext.Opacity = GetMaterialOpacity(PixelMaterialInputs);

    // 레벨 반투명(Translucent)
#if MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT
    (......)
#endif

    half3 Color = 0;

    // 정의 데이터 .
    half CustomData0 = GetMaterialCustomData0(MaterialParameters);
    half CustomData1 = GetMaterialCustomData1(MaterialParameters);
    InitShadingModelContext(ShadingModelContext, GBuffer, MaterialParameters.SvPosition, MaterialParameters.CameraVector, CustomData0, CustomData1);
    float3 DiffuseDir = MaterialParameters.WorldNormal;

    // 모델 .
#if MATERIAL_SHADINGMODEL_HAIR
    (......)
#endif

    // 라이팅 텍스처(Texture).
    VTPageTableResult LightmapVTPageTableResult = (VTPageTableResult)0.0f;
#if LIGHTMAP_VT_ENABLED
    {
        float2 LightmapUV0, LightmapUV1;
        uint LightmapDataIndex;
        GetLightMapCoordinates(Interpolants, LightmapUV0, LightmapUV1, LightmapDataIndex);
        LightmapVTPageTableResult = LightmapGetVTSampleInfo(LightmapUV0, LightmapDataIndex, SvPosition.xy);
    }
#endif

#if LIGHTMAP_VT_ENABLED
    // This must occur after CalcMaterialParameters(), which is required to initialize the VT feedback mechanism
    // Lightmap request is always the first VT sample in the shader
    StoreVirtualTextureFeedback(MaterialParameters.VirtualTextureFeedback, 0, LightmapVTPageTableResult.PackedRequest);
#endif

    // 계산/산출(Calculate).
    half IndirectIrradiance;
    half3 IndirectColor;
    ComputeIndirect(LightmapVTPageTableResult, Interpolants, DiffuseDir, ShadingModelContext, IndirectIrradiance, IndirectColor);
    Color += IndirectColor;

    // 계산/산출(Calculate)의 그림자(Shadow).
    half Shadow = GetPrimaryPrecomputedShadowMask(LightmapVTPageTableResult, Interpolants).r;

#if DEFERRED_SHADING_PATH
    float4 OutGBufferD;
    float4 OutGBufferE;
    float4 OutGBufferF;
    float4 OutGBufferVelocity = 0;

    GBuffer.IndirectIrradiance = IndirectIrradiance;
    GBuffer.PrecomputedShadowFactors.r = Shadow;

    // GBuffer데이터 .
    EncodeGBuffer(GBuffer, OutGBufferA, OutGBufferB, OutGBufferC, OutGBufferD, OutGBufferE, OutGBufferF, OutGBufferVelocity);
#else

#if !MATERIAL_SHADINGMODEL_UNLIT

    // .
#if ENABLE_SKY_LIGHT
    half3 SkyDiffuseLighting = GetSkySHDiffuseSimple(MaterialParameters.WorldNormal);
    half3 DiffuseLookup = SkyDiffuseLighting * ResolvedView.SkyLightColor.rgb;
    IndirectIrradiance += Luminance(DiffuseLookup);
#endif
            
    Color *= MaterialAO;
    IndirectIrradiance *= MaterialAO;

    float  ShadowPositionZ = 0;
#if DIRECTIONAL_LIGHT_CSM && !MATERIAL_SHADINGMODEL_SINGLELAYERWATER
    // CSM그림자(Shadow).
    if (UseCSM())
    {
        half ShadowMap = MobileDirectionalLightCSM(MaterialParameters.ScreenPosition.xy, MaterialParameters.ScreenPosition.w, ShadowPositionZ);
    #if ALLOW_STATIC_LIGHTING
        Shadow = min(ShadowMap, Shadow);
    #else
        Shadow = ShadowMap;
    #endif
    }
#endif /* DIRECTIONAL_LIGHT_CSM */

    // 거리그림자(Shadow).
#if APPLY_DISTANCE_FIELD
    if (ShadowPositionZ == 0)
    {
        Shadow = Texture2DSample(MobileBasePass.ScreenSpaceShadowMaskTexture, MobileBasePass.ScreenSpaceShadowMaskSampler, SvPositionToBufferUV(SvPosition)).x;
    }
#endif

    half NoL = max(0, dot(MaterialParameters.WorldNormal, MobileDirectionalLight.DirectionalLightDirectionAndShadowTransition.xyz));
    half3 H = normalize(MaterialParameters.CameraVector + MobileDirectionalLight.DirectionalLightDirectionAndShadowTransition.xyz);
    half NoH = max(0, dot(MaterialParameters.WorldNormal, H));

    // + IBL
#if FULLY_ROUGH
    Color += (Shadow * NoL) * MobileDirectionalLight.DirectionalLightColor.rgb * ShadingModelContext.DiffuseColor;
#else
    FMobileDirectLighting Lighting = MobileIntegrateBxDF(ShadingModelContext, GBuffer, NoL, MaterialParameters.CameraVector, H, NoH);
    // MobileDirectionalLight.DirectionalLightDistanceFadeMADAndSpecularScale.z저장(Save)의 SpecularScale.
    Color += (Shadow) * MobileDirectionalLight.DirectionalLightColor.rgb * (Lighting.Diffuse + Lighting.Specular * MobileDirectionalLight.DirectionalLightDistanceFadeMADAndSpecularScale.z);

    // 셰이딩 .
#if    !(MATERIAL_SINGLE_SHADINGMODEL && MATERIAL_SHADINGMODEL_HAIR)
    (......)
#endif
#endif /* FULLY_ROUGH */

    // 로컬 라이트(Light), 4개 .
#if MAX_DYNAMIC_POINT_LIGHTS > 0 && !MATERIAL_SHADINGMODEL_SINGLELAYERWATER

        if(NumDynamicPointLights > 0)
        {

            #if SUPPORT_SPOTLIGHTS_SHADOW
                FPCFSamplerSettings Settings;
                Settings.ShadowDepthTexture = DynamicSpotLightShadowTexture;
                Settings.ShadowDepthTextureSampler = DynamicSpotLightShadowSampler;
                Settings.ShadowBufferSize = DynamicSpotLightShadowBufferSize;
                Settings.bSubsurface = false;
                Settings.bTreatMaxDepthUnshadowed = false;
                Settings.DensityMulConstant = 0;
                Settings.ProjectionDepthBiasParameters = 0;
            #endif

            AccumulateLightingOfDynamicPointLight(MaterialParameters, ...);
        
            if (MAX_DYNAMIC_POINT_LIGHTS > 1 && NumDynamicPointLights > 1)
            {
                AccumulateLightingOfDynamicPointLight(MaterialParameters, ...);

                if (MAX_DYNAMIC_POINT_LIGHTS > 2 && NumDynamicPointLights > 2)
                {
                    AccumulateLightingOfDynamicPointLight(MaterialParameters, ...);

                    if (MAX_DYNAMIC_POINT_LIGHTS > 3 && NumDynamicPointLights > 3)
                    {
                        AccumulateLightingOfDynamicPointLight(MaterialParameters, ...);
                    }
                }
            }
        }

#endif

    // 스카이라이트(Skylight).
#if ENABLE_SKY_LIGHT
    #if MATERIAL_TWOSIDED && LQ_TEXTURE_LIGHTMAP
    if (NoL == 0)
    {
    #endif

    #if MATERIAL_SHADINGMODEL_SINGLELAYERWATER
        ShadingModelContext.WaterDiffuseIndirectLuminance += SkyDiffuseLighting;
    #endif
        Color += SkyDiffuseLighting * half3(ResolvedView.SkyLightColor.rgb) * ShadingModelContext.DiffuseColor * MaterialAO;
    #if MATERIAL_TWOSIDED && LQ_TEXTURE_LIGHTMAP
    }
    #endif
#endif

#endif /* !MATERIAL_SHADINGMODEL_UNLIT */

#if MATERIAL_SHADINGMODEL_SINGLELAYERWATER
    (......)
#endif // MATERIAL_SHADINGMODEL_SINGLELAYERWATER

#endif// DEFERRED_SHADING_PATH

    // 처리(Process)포인트 .
    half4 VertexFog = half4(0, 0, 0, 1);
#if USE_VERTEX_FOG
#if PACK_INTERPOLANTS
    VertexFog = PackedInterpolants[0];
#else
    VertexFog = BasePassInterpolants.VertexFog;
#endif
#endif
    
    // 발광(Emissive) .
    half3 Emissive = GetMaterialEmissive(PixelMaterialInputs);
#if MATERIAL_SHADINGMODEL_THIN_TRANSLUCENT
    Emissive *= TopMaterialCoverage;
#endif
    Color += Emissive;

#if !MATERIAL_SHADINGMODEL_UNLIT && MOBILE_EMULATION
    Color = lerp(Color, ShadingModelContext.DiffuseColor, ResolvedView.UnlitViewmodeMask);
#endif

    // 컬러 까지 출력컬러 .
    #if MATERIALBLENDING_ALPHACOMPOSITE || MATERIAL_SHADINGMODEL_SINGLELAYERWATER
        OutColor = half4(Color * VertexFog.a + VertexFog.rgb * ShadingModelContext.Opacity, ShadingModelContext.Opacity);
    #elif MATERIALBLENDING_ALPHAHOLDOUT
        // not implemented for holdout
        OutColor = half4(Color * VertexFog.a + VertexFog.rgb * ShadingModelContext.Opacity, ShadingModelContext.Opacity);
    #elif MATERIALBLENDING_TRANSLUCENT
        OutColor = half4(Color * VertexFog.a + VertexFog.rgb, ShadingModelContext.Opacity);
    #elif MATERIALBLENDING_ADDITIVE
        OutColor = half4(Color * (VertexFog.a * ShadingModelContext.Opacity.x), 0.0f);
    #elif MATERIALBLENDING_MODULATE
        half3 FoggedColor = lerp(half3(1, 1, 1), Color, VertexFog.aaa * VertexFog.aaa);
        OutColor = half4(FoggedColor, ShadingModelContext.Opacity);
    #else
        OutColor.rgb = Color * VertexFog.a + VertexFog.rgb;

        #if !MATERIAL_USE_ALPHA_TO_COVERAGE
            // Scene color alpha is not used yet so we set it to 1
            OutColor.a = 1.0;

            #if OUTPUT_MOBILE_HDR 
                // Store depth in FP16 alpha. This depth value can be fetched during translucency or sampled in post-processing
                OutColor.a = SvPosition.z;
            #endif
        #else
            half MaterialOpacityMask = GetMaterialMaskInputRaw(PixelMaterialInputs);
            OutColor.a = GetMaterialMask(PixelMaterialInputs) / max(abs(ddx(MaterialOpacityMask)) + abs(ddy(MaterialOpacityMask)), 0.0001f) + 0.5f;
        #endif
    #endif

    #if !MATERIALBLENDING_MODULATE && USE_PREEXPOSURE
        OutColor.rgb *= ResolvedView.PreExposure;
    #endif

    #if MATERIAL_IS_SKY
        OutColor.rgb = min(OutColor.rgb, Max10BitsFloat.xxx * 0.5f);
    #endif

#if USE_SCENE_DEPTH_AUX
    OutSceneDepthAux = SvPosition.z;
#endif

    // 처리(Process)컬러 의 alpha.
#if USE_EDITOR_COMPOSITING && (MOBILE_EMULATION)
    // Editor primitive depth testing
    OutColor.a = 1.0;
    #if MATERIALBLENDING_MASKED
        // some material might have an opacity value
        OutColor.a = GetMaterialMaskInputRaw(PixelMaterialInputs);
    #endif
    clip(OutColor.a - GetMaterialOpacityMaskClipValue());
#else
    #if OUTPUT_GAMMA_SPACE
        OutColor.rgb = sqrt(OutColor.rgb);
    #endif
#endif

#if NUM_VIRTUALTEXTURE_SAMPLES || LIGHTMAP_VT_ENABLED
    FinalizeVirtualTextureFeedback(
        MaterialParameters.VirtualTextureFeedback,
        MaterialParameters.SvPosition,
        ShadingModelContext.Opacity,
        View.FrameNumber,
        View.VTFeedbackBuffer
    );
#endif
}

모바일 단말기에서 BasePassPS의 처리 프로세스는 상대적으로 복잡하며 주로 보간 데이터 압축 해제, 재료 속성 획득 및 계산, GBuffer 계산 및 버퍼링, GBuffer 데이터 처리 또는 조정, 포워드 렌더링 분기의 조명(평행광, 로컬 라이트) 계산, 디스턴스 필드 및 CSM과 같은 그림자 계산, 스카이 라이트 계산, 정적 조명, 간접 조명 및 IBL 처리, 안개 효과 계산, 물, 머리카락 및 얇은 레이어 투명도와 같은 특수 셰이딩 모델 처리를 포함하여 많은 단계로 구성됩니다.

표준 16비트 부동 소수점 수(FP16)로 표현할 수 있는 가장 작은 값은 \cfrac{1.0}{2^{24} = 5.96 \cdot 10^{-8}이므로 후속 조명 계산에는 러프니스(1.0러프니스4)의 4제곱 연산이 포함됩니다. 0으로 나누는 오류를 방지하려면 거칠기를 0.015625(0.0156254=5.96108)로 가로채야 합니다.

cpp
GBuffer.Roughness = max(0.015625, GetMaterialRoughness(PixelMaterialInputs));

이는 또한 모바일 장치용 렌더링 기능을 개발할 때 데이터 정확성에 특별한 주의를 기울이고 제어해야 함을 경고합니다. 그렇지 않으면 저가형 장치에서는 데이터 정확도가 부족하여 다양한 이상한 그림 이상이 발생하는 경우가 많습니다.

위의 코드는 상대적으로 크지만 많은 매크로에 의해 제어되며 실제로 단일 머티리얼을 렌더링하는 데 필요한 코드는 그 중 작은 하위 집합일 수 있습니다. 예를 들어 로컬 광원은 기본적으로 4개가 지원되지만, 프로젝트 구성(아래)에서 2개 이하로 설정할 수 있다면 실제 광원 명령 실행 횟수는 훨씬 줄어들게 됩니다.

포워드 렌더링 브랜치인 경우 GBuffer의 많은 처리가 무시됩니다. 디퍼드 렌더링 분기인 경우 방향성 광원 및 로컬 광원의 계산은 무시되고 디퍼드 렌더링 패스의 셰이더에 의해 수행됩니다.

다음은 중요한 인터페이스 EncodeGBuffer에 대한 분석입니다.

hlsl
void EncodeGBuffer(
    FGBufferData GBuffer,
    out float4 OutGBufferA,
    out float4 OutGBufferB,
    out float4 OutGBufferC,
    out float4 OutGBufferD,
    out float4 OutGBufferE,
    out float4 OutGBufferVelocity,
    float QuantizationBias = 0        // -0.5 to 0.5 random float. Used to bias quantization.
    )
{
    if (GBuffer.ShadingModelID == SHADINGMODELID_UNLIT)
    {
        OutGBufferA = 0;
        SetGBufferForUnlit(OutGBufferB);
        OutGBufferC = 0;
        OutGBufferD = 0;
        OutGBufferE = 0;
    }
    else
    {
        // GBufferA: 압축/패킹(Packing)의 노멀(Normal), 계산/산출(Calculate)그림자(Shadow), 데이터 .
#if MOBILE_DEFERRED_SHADING
        OutGBufferA.rg = UnitVectorToOctahedron( normalize(GBuffer.WorldNormal) ) * 0.5f + 0.5f;
        OutGBufferA.b = GBuffer.PrecomputedShadowFactors.x;
        OutGBufferA.a = GBuffer.PerObjectGBufferData;
#else
        (......)
#endif

        // GBufferB: , , 러프니스(Roughness), 셰이딩 모델 , Mask.
        OutGBufferB.r = GBuffer.Metallic;
        OutGBufferB.g = GBuffer.Specular;
        OutGBufferB.b = GBuffer.Roughness;
        OutGBufferB.a = EncodeShadingModelIdAndSelectiveOutputMask(GBuffer.ShadingModelID, GBuffer.SelectiveOutputMask);

        // GBufferC: , AO 또는 .
        OutGBufferC.rgb = EncodeBaseColor( GBuffer.BaseColor );

#if ALLOW_STATIC_LIGHTING
        // No space for AO. Multiply IndirectIrradiance by AO instead of storing.
        OutGBufferC.a = EncodeIndirectIrradiance(GBuffer.IndirectIrradiance * GBuffer.GBufferAO) + QuantizationBias * (1.0 / 255.0);
#else
        OutGBufferC.a = GBuffer.GBufferAO;
#endif

        OutGBufferD = GBuffer.CustomData;
        OutGBufferE = GBuffer.PrecomputedShadowFactors;
    }

#if WRITES_VELOCITY_TO_GBUFFER
    OutGBufferVelocity = GBuffer.Velocity;
#else
    OutGBufferVelocity = 0;
#endif
}

기본 조명 모델(DefaultLit)에서 BasePass의 출력 결과는 다음 텍스처를 갖습니다.

12.3.3.3 MobileDeferredShading

모바일 측 지연 조명의 VS는 PC 측과 동일하며 둘 다 DeferredLightVertexShaders.usf이지만 PS는 다르며 MobileDeferredShading.usf를 사용합니다. VS는 PC와 동일하고 특별한 동작이 없으므로 여기서는 무시하도록 하겠습니다. 관심이 있으시면 다섯 번째 기사의 5.5.3.1 DeferredLightVertexShader 섹션을 읽어보세요.

PS 코드를 직접 분석해 보겠습니다.

cpp
// Engine\Shaders\Private\MobileDeferredShading.usf

(......)

// 모바일 플랫폼(Mobile)라이트(Light)데이터 구조체 .
struct FMobileLightData
{
    float3 Position;
    float  InvRadius;
    float3 Color;
    float  FalloffExponent;
    float3 Direction;
    float2 SpotAngles;
    float SourceRadius;
    float SpecularScale;
    bool bInverseSquared;
    bool bSpotLight;
};

// 페치/가져오기(Fetch)GBuffer데이터 .
void FetchGBuffer(in float2 UV, out float4 GBufferA, out float4 GBufferB, out float4 GBufferC, out float4 GBufferD, out float SceneDepth)
{
    // Vulkan의 pass페치/가져오기(Fetch)데이터 .
#if VULKAN_PROFILE
    GBufferA = VulkanSubpassFetch1(); 
    GBufferB = VulkanSubpassFetch2(); 
    GBufferC = VulkanSubpassFetch3(); 
    GBufferD = 0;
    SceneDepth = ConvertFromDeviceZ(VulkanSubpassDepthFetch());
    // Metal의 pass페치/가져오기(Fetch)데이터 .
#elif METAL_PROFILE
    GBufferA = SubpassFetchRGBA_1(); 
    GBufferB = SubpassFetchRGBA_2(); 
    GBufferC = SubpassFetchRGBA_3(); 
    GBufferD = 0; 
    SceneDepth = ConvertFromDeviceZ(SubpassFetchR_4());
    // 플랫폼(DX, OpenGL)의 pass페치/가져오기(Fetch)데이터 .
#else
    GBufferA = Texture2DSampleLevel(MobileSceneTextures.GBufferATexture, MobileSceneTextures.GBufferATextureSampler, UV, 0); 
    GBufferB = Texture2DSampleLevel(MobileSceneTextures.GBufferBTexture, MobileSceneTextures.GBufferBTextureSampler, UV, 0);
    GBufferC = Texture2DSampleLevel(MobileSceneTextures.GBufferCTexture, MobileSceneTextures.GBufferCTextureSampler, UV, 0);
    GBufferD = 0;
    SceneDepth = ConvertFromDeviceZ(Texture2DSampleLevel(MobileSceneTextures.SceneDepthTexture, MobileSceneTextures.SceneDepthTextureSampler, UV, 0).r);
#endif
}

// 압축해제/언패킹(Unpacking)GBuffer데이터 .
FGBufferData DecodeGBufferMobile(
    float4 InGBufferA,
    float4 InGBufferB,
    float4 InGBufferC,
    float4 InGBufferD)
{
    FGBufferData GBuffer;
    GBuffer.WorldNormal = OctahedronToUnitVector( InGBufferA.xy * 2.0f - 1.0f );
    GBuffer.PrecomputedShadowFactors = InGBufferA.z;
    GBuffer.PerObjectGBufferData = InGBufferA.a;  
    GBuffer.Metallic    = InGBufferB.r;
    GBuffer.Specular    = InGBufferB.g;
    GBuffer.Roughness    = max(0.015625, InGBufferB.b);
    // Note: must match GetShadingModelId standalone function logic
    // Also Note: SimpleElementPixelShader directly sets SV_Target2 ( GBufferB ) to indicate unlit.
    // An update there will be required if this layout changes.
    GBuffer.ShadingModelID = DecodeShadingModelId(InGBufferB.a);
    GBuffer.SelectiveOutputMask = DecodeSelectiveOutputMask(InGBufferB.a);
    GBuffer.BaseColor = DecodeBaseColor(InGBufferC.rgb);
#if ALLOW_STATIC_LIGHTING
    GBuffer.GBufferAO = 1;
    GBuffer.IndirectIrradiance = DecodeIndirectIrradiance(InGBufferC.a);
#else
    GBuffer.GBufferAO = InGBufferC.a;
    GBuffer.IndirectIrradiance = 1;
#endif
    GBuffer.CustomData = HasCustomGBufferData(GBuffer.ShadingModelID) ? InGBufferD : 0;
    return GBuffer;
}

// 라이팅 .
half3 GetDirectLighting(
    FMobileLightData LightData, 
    FMobileShadingModelContext ShadingModelContext, 
    FGBufferData GBuffer, 
    float3 WorldPosition, 
    half3 CameraVector)
{
    half3 DirectLighting = 0;
    
    float3 ToLight = LightData.Position - WorldPosition;
    float DistanceSqr = dot(ToLight, ToLight);
    float3 L = ToLight * rsqrt(DistanceSqr);
    
    // 라이트(Light)감쇠(Falloff/Attenuation).
    float Attenuation = 0.0;
    if (LightData.bInverseSquared)
    {
        // Sphere falloff (technically just 1/d2 but this avoids inf)
        Attenuation = 1.0f / (DistanceSqr + 1.0f);
        Attenuation *= Square(saturate(1 - Square(DistanceSqr * Square(LightData.InvRadius))));
    }
    else
    {
        Attenuation = RadialAttenuation(ToLight * LightData.InvRadius, LightData.FalloffExponent);
    }

    // 감쇠(Falloff/Attenuation).
    if (LightData.bSpotLight)
    {
        Attenuation *= SpotAttenuation(L, -LightData.Direction, LightData.SpotAngles);
    }
    
    // 만약 감쇠(Falloff/Attenuation)로 0, 이면 계산/산출(Calculate)라이팅 .
    if (Attenuation > 0.0)
    {
        half3 H = normalize(CameraVector + L);
        half NoL = max(0.0, dot(GBuffer.WorldNormal, L));
        half NoH = max(0.0, dot(GBuffer.WorldNormal, H));
        FMobileDirectLighting Lighting = MobileIntegrateBxDF(ShadingModelContext, GBuffer, NoL, CameraVector, H, NoH);
        DirectLighting = (Lighting.Diffuse + Lighting.Specular * LightData.SpecularScale) * (LightData.Color * (1.0 / PI) * Attenuation);
    }
    return DirectLighting;
}

// 라이팅 함수 .
half ComputeLightFunctionMultiplier(float3 WorldPosition);
// 을(를) 활용하여 추가(Add)로컬 라이팅 , 지원 동적 그림자(Shadow), 이므로 라이트(Light)그림자(Shadow).
half3 GetLightGridLocalLighting(const FCulledLightsGridData InLightGridData, ...);

// 의 PS.
void MobileDirectLightPS(
    noperspective float4 UVAndScreenPos : TEXCOORD0, 
    float4 SvPosition : SV_POSITION, 
    out half4 OutColor : SV_Target0)
{
    // 복구 (로드/읽기(Read))GBuffer데이터 .
    FGBufferData GBuffer = (FGBufferData)0;
    float SceneDepth = 0; 
    {
        float4 GBufferA = 0; 
        float4 GBufferB = 0; 
        float4 GBufferC = 0; 
        float4 GBufferD = 0;
        FetchGBuffer(UVAndScreenPos.xy, GBufferA, GBufferB, GBufferC, GBufferD, SceneDepth);
        GBuffer = DecodeGBufferMobile(GBufferA, GBufferB, GBufferC, GBufferD);
    }
    
    // 계산/산출(Calculate)벡터 .
    float2 ScreenPos = UVAndScreenPos.zw;
    float3 WorldPosition = mul(float4(ScreenPos * SceneDepth, SceneDepth, 1), View.ScreenToWorld).xyz;
    half3 CameraVector = normalize(View.WorldCameraOrigin - WorldPosition);
    half NoV = max(0, dot(GBuffer.WorldNormal, CameraVector));
    half3 ReflectionVector = GBuffer.WorldNormal * (NoV * 2.0) - CameraVector;
    
    half3 Color = 0;
    // Check movable light param to determine if we should be using precomputed shadows
    half Shadow = LightFunctionParameters2.z > 0.0f ? 1.0f : GBuffer.PrecomputedShadowFactors.r;

    // CSM그림자(Shadow).
#if APPLY_CSM
    float  ShadowPositionZ = 0;
    float4 ScreenPosition = SvPositionToScreenPosition(float4(SvPosition.xyz,SceneDepth));
    float ShadowMap = MobileDirectionalLightCSM(ScreenPosition.xy, SceneDepth, ShadowPositionZ);
    Shadow = min(ShadowMap, Shadow);
#endif

    // 셰이딩 모델 .
    FMobileShadingModelContext ShadingModelContext = (FMobileShadingModelContext)0;
    {
        half DielectricSpecular = 0.08 * GBuffer.Specular;
        ShadingModelContext.DiffuseColor = GBuffer.BaseColor - GBuffer.BaseColor * GBuffer.Metallic;    // 1 mad
        ShadingModelContext.SpecularColor = (DielectricSpecular - DielectricSpecular * GBuffer.Metallic) + GBuffer.BaseColor * GBuffer.Metallic;    // 2 mad
        // 계산/산출(Calculate)의 BRDF.
        ShadingModelContext.SpecularColor = GetEnvBRDF(ShadingModelContext.SpecularColor, GBuffer.Roughness, NoV);
    }
    
    // 로컬 라이트(Light).
    float2 LocalPosition = SvPosition.xy - View.ViewRectMin.xy;
    uint GridIndex = ComputeLightGridCellIndex(uint2(LocalPosition.x, LocalPosition.y), SceneDepth);
    // 라이트(Light)
#if USE_CLUSTERED
    {
        const uint EyeIndex = 0;
        const FCulledLightsGridData CulledLightGridData = GetCulledLightsGrid(GridIndex, EyeIndex);
        Color += GetLightGridLocalLighting(CulledLightGridData, ShadingModelContext, GBuffer, WorldPosition, CameraVector, EyeIndex, 0);
    }
#endif
            
    // 계산/산출(Calculate).
    half NoL = max(0, dot(GBuffer.WorldNormal, MobileDirectionalLight.DirectionalLightDirectionAndShadowTransition.xyz));
    half3 H = normalize(CameraVector + MobileDirectionalLight.DirectionalLightDirectionAndShadowTransition.xyz);
    half NoH = max(0, dot(GBuffer.WorldNormal, H));
    FMobileDirectLighting Lighting;
    Lighting.Specular = ShadingModelContext.SpecularColor * CalcSpecular(GBuffer.Roughness, NoH);
    Lighting.Diffuse = ShadingModelContext.DiffuseColor;
    Color += (Shadow * NoL) * MobileDirectionalLight.DirectionalLightColor.rgb * (Lighting.Diffuse + Lighting.Specular * MobileDirectionalLight.DirectionalLightDistanceFadeMADAndSpecularScale.z);

    // 처리(Process)반사(Reflection)(IBL, 반사(Reflection)).
#if APPLY_REFLECTION
    uint NumCulledEntryIndex = (ForwardLightData.NumGridCells + GridIndex) * NUM_CULLED_LIGHTS_GRID_STRIDE;
    uint NumLocalReflectionCaptures = min(ForwardLightData.NumCulledLightsGrid[NumCulledEntryIndex + 0], ForwardLightData.NumReflectionCaptures);
    uint DataStartIndex = ForwardLightData.NumCulledLightsGrid[NumCulledEntryIndex + 1];

    float3 SpecularIBL = CompositeReflectionCapturesAndSkylight(
        1.0f,
        WorldPosition,
        ReflectionVector,//RayDirection,
        GBuffer.Roughness,
        GBuffer.IndirectIrradiance,
        1.0f,
        0.0f,
        NumLocalReflectionCaptures,
        DataStartIndex,
        0,
        true);
        
    Color += SpecularIBL * ShadingModelContext.SpecularColor;
#elif APPLY_SKY_REFLECTION
    float SkyAverageBrightness = 1.0f;
    float3 SpecularIBL = GetSkyLightReflection(ReflectionVector, GBuffer.Roughness, SkyAverageBrightness);
    SpecularIBL *= ComputeMixingWeight(GBuffer.IndirectIrradiance, SkyAverageBrightness, GBuffer.Roughness);
    Color += SpecularIBL * ShadingModelContext.SpecularColor;
#endif
    // 스카이라이트(Skylight)디퓨즈(Diffuse).
    half3 SkyDiffuseLighting = GetSkySHDiffuseSimple(GBuffer.WorldNormal);
    Color+= SkyDiffuseLighting * half3(View.SkyLightColor.rgb) * ShadingModelContext.DiffuseColor * GBuffer.GBufferAO;
    half LightAttenuation = ComputeLightFunctionMultiplier(WorldPosition);

#if USE_PREEXPOSURE
    // MobileHDR applies PreExposure in tonemapper
    LightAttenuation *= View.PreExposure;    
#endif
                    
    OutColor.rgb = Color.rgb * LightAttenuation;
    OutColor.a = 1;
}

// 로컬 라이트(Light)의 PS.
void MobileRadialLightPS(
    float4 InScreenPosition : TEXCOORD0,
    float4 SVPos            : SV_POSITION,
    out half4 OutColor        : SV_Target0
)
{
    FGBufferData GBuffer = (FGBufferData)0;
    float SceneDepth = 0; 
    {
        float2 ScreenUV = InScreenPosition.xy / InScreenPosition.w * View.ScreenPositionScaleBias.xy + View.ScreenPositionScaleBias.wz;
        float4 GBufferA = 0;  
        float4 GBufferB = 0; 
        float4 GBufferC = 0; 
        float4 GBufferD = 0;
        FetchGBuffer(ScreenUV, GBufferA, GBufferB, GBufferC, GBufferD, SceneDepth);
        GBuffer = DecodeGBufferMobile(GBufferA, GBufferB, GBufferC, GBufferD);
    }
    
    // With a perspective projection, the clip space position is NDC * Clip.w
    // With an orthographic projection, clip space is the same as NDC
    float2 ClipPosition = InScreenPosition.xy / InScreenPosition.w * (View.ViewToClip[3][3] < 1.0f ? SceneDepth : 1.0f);
    float3 WorldPosition = mul(float4(ClipPosition, SceneDepth, 1), View.ScreenToWorld).xyz;
    half3 CameraVector = normalize(View.WorldCameraOrigin - WorldPosition);
    half NoV = max(0, dot(GBuffer.WorldNormal, CameraVector));
    
    // 라이트(Light)데이터 구조체 .
    FMobileLightData LightData = (FMobileLightData)0;
    {
        LightData.Position = DeferredLightUniforms.Position;
        LightData.InvRadius = DeferredLightUniforms.InvRadius;
        LightData.Color = DeferredLightUniforms.Color;
        LightData.FalloffExponent = DeferredLightUniforms.FalloffExponent;
        LightData.Direction = DeferredLightUniforms.Direction;
        LightData.SpotAngles = DeferredLightUniforms.SpotAngles;
        LightData.SpecularScale = 1.0;
        LightData.bInverseSquared = INVERSE_SQUARED_FALLOFF; 
        LightData.bSpotLight = IS_SPOT_LIGHT; 
    }

    FMobileShadingModelContext ShadingModelContext = (FMobileShadingModelContext)0;
    {
        half DielectricSpecular = 0.08 * GBuffer.Specular;
        ShadingModelContext.DiffuseColor = GBuffer.BaseColor - GBuffer.BaseColor * GBuffer.Metallic;    // 1 mad
        ShadingModelContext.SpecularColor = (DielectricSpecular - DielectricSpecular * GBuffer.Metallic) + GBuffer.BaseColor * GBuffer.Metallic;    // 2 mad
        // 계산/산출(Calculate)BRDF.
        ShadingModelContext.SpecularColor = GetEnvBRDF(ShadingModelContext.SpecularColor, GBuffer.Roughness, NoV);
    }
    
    // 계산/산출(Calculate).
    half3 Color = GetDirectLighting(LightData, ShadingModelContext, GBuffer, WorldPosition, CameraVector);
    
    // IES, 라이팅 함수 .
    half LightAttenuation = ComputeLightProfileMultiplier(WorldPosition, DeferredLightUniforms.Position, -DeferredLightUniforms.Direction, DeferredLightUniforms.Tangent);
    LightAttenuation*= ComputeLightFunctionMultiplier(WorldPosition);

#if USE_PREEXPOSURE
    // MobileHDR applies PreExposure in tonemapper
    LightAttenuation*= View.PreExposure;    
#endif

    OutColor.rgb = Color * LightAttenuation;
    OutColor.a = 1;
}

위에서 볼 수 있듯이 평행광과 국부광원의 PS는 입구가 다른데, 그 이유는 둘 사이의 차이가 크기 때문이다. 평행광은 주 입구에서 직접 조명을 계산하고 반사(IBL, 캐처) 및 하늘빛 디퓨즈도 계산합니다. 로컬 광원은 광원 구조를 구축하고 직접 조명 계산 기능에 들어간 다음 마지막으로 로컬 광원 고유의 IES 및 조명 기능을 처리합니다.

또한 GBuffer를 얻을 때 SubPass의 고유한 읽기 모드가 사용되는데, 이는 다양한 셰이딩 플랫폼에 따라 다릅니다.

hlsl
// Vulkan
[[vk::input_attachment_index(1)]]
SubpassInput<float4> GENERATED_SubpassFetchAttachment0;
#define VulkanSubpassFetch0() GENERATED_SubpassFetchAttachment0.SubpassLoad()

// Metal
Texture2D<float4> gl_LastFragDataRGBA_1;
#define SubpassFetchRGBA_1() gl_LastFragDataRGBA_1.Load(uint3(0, 0, 0), 0)

// DX / OpenGL
Texture2DSampleLevel(GBufferATexture, GBufferATextureSampler, UV, 0);

팀 모집

블로거 팀은 UE4를 사용하여 새로운 몰입형 경험 제품을 개발하고 있으며, 큰 목표를 달성하기 위해 각계각층의 전문가가 함께 참여하고 협력할 필요가 시급합니다. 현재 아래와 같은 직무를 긴급 모집하고 있습니다.

  • UE 로직 개발.

  • UE 엔진 프로그램.

  • UE 그래픽 렌더링.

  • TA(기술, 예술).

요구사항: 기술에 대한 열정, 탄탄한 기술 기반, 원활한 의사소통 및 협력 능력, UE 또는 모바일 개발 경험을 선호합니다.

관심이 있거나 더 알고 싶으시면 블로거의 WeChat 계정을 추가하세요: 81079389(Blog Park에 지원한다고 표시). 또는 이력서를 블로거의 이메일 주소: 81079389#qq.com(#을 @로 바꾸세요)으로 보내주세요.

각계각층의 영웅들이 만나기를 기다립니다.

특별 지침

  • 1부가 끝났고, 2부의 내용은 다음과 같습니다.

모바일 렌더링 기술

  • 모바일 최적화 팁

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

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

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

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

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








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


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

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