TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역] 언리얼 렌더링 시스템 해부하기 (12) - 모바일 파트 1 (UE 모바일 렌더링 분석)

jplee 2024. 5. 21. 12:47

역자의 말.
가끔은 언제쯤이면 모바일 하드웨어 지원 게임을 만들지 않을 수 있을까? 라는 생각 정도는 거의 15년 가까이 모바일 게임과 멀티플레폼 게임을 개발 하다보면 충분히 갖을 수 있는 생각이지 않을까 싶네요. 중국 CNBLOG 는 쯔후 보다 더 오래 된 기술 전문 블로그 플레폼 입니다. 쯔후는 약간 네이버 지식인 + 블로그 개념인데 반해 CNBLOG 는 프로그래머들이 주로 사용하던 테크 티스토리 같은 거라고 할까요? 아무튼... 언리얼 엔진으로 게임을 개발 하고 멀티플레폼을 지원 해야하니까 언제나 리마인드 하는 마음으로 또 복기 해 보고자 했습니다. 


저자

 

向往 - 知乎

UE微信技术群加81079389(注明知乎) 回答数 3,获得 2,384 次赞同

www.zhihu.com

 
그래픽 렌더링, 게임 엔진、GPU。知乎:www.zhihu.com/people/timlly-chang。UE技术群:943549515,위챗 그룹 먼저 추가 81079389(블로거 참고 사항)

파트 11 RDG 는 여기서... 

 

[번역] 언리얼 렌더링 시스템 해부하기(11)- RDG

역자의 말.RDG 는 RHI 와 더불어 언리얼 렌더링의 구조적 근간을 받치고 있는 두 개의 커다란 기둥입니다.테크니컬 아티스트가 커스텀 Pass 또는 지오메트리 셰이딩 처리등을 새롭게 개발해야 한다

techartnomad.tistory.com


12.1 이 문서의 개요

앞의 모든 챕터는 PC 측의 지연 렌더링 파이프라인을 기반으로 UE의 렌더링 시스템을 설명하며, 특히 언리얼 렌더링 시스템 해부 (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)와 메인스트림 모바일 디바이스(삼성 엑시노스 9 8895)의 성능 비교 차트. 모바일 디바이스의 하드웨어 성능은 PC 디바이스의 10분의 1에 불과하지만 해상도는 PC의 절반에 가깝다는 점에서 모바일 디바이스의 도전과 딜레마가 더욱 부각됩니다.
2020년까지 주류 모바일 디바이스의 성능은 다음과 같습니다:

특수 하드웨어 아키텍처. 커플링 아키텍처로 알려진 CPU와 GPU가 메모리 저장 장치를 공유하는 것과 GPU용 TB(TB) 아키텍처 등이 있습니다. 저전력으로 최대한 많은 연산을 수행하는 것을 목표로 CPU와 GPU가 메모리 저장 장치를 공유하는 커플링 아키텍처, GPU용 TB(D)R 아키텍처 등이 있습니다. 

PC 디바이스의 분리형 하드웨어 아키텍처와 모바일 디바이스의 결합형 하드웨어 아키텍처를 비교한 다이어그램.

이 외에도 PC 단말의 CPU 및 GPU 위치추적 연산 능력, 용량 이동 단말의 성능 세 가지 지표가 있습니다(P성능), 능력(P능률: 파워)、면적(Area), 속칭PPA。(아래 차트)

모바일 디바이스를 측정하는 세 가지 기본 매개변수는 성능, 면적, 전력이며, 컴퓨팅 밀도는 성능 및 면적과 관련이 있고 에너지 비율은 성능 및 용량 소비와 관련이 있으므로 클수록 좋습니다.
모바일 디바이스의 부상과 함께 모바일 디바이스의 중요한 발전 분야인 XR 디바이스의 부상도 함께 이루어지고 있습니다. 크기, 기능 및 애플리케이션 시나리오가 다양한 다양한 XR 디바이스가 존재합니다:

다양한 형태의 XR 장비.
최근 메타버스의 폭발적인 성장과 함께 페이스북이 메타로 이름을 바꾸고, Apple, Microsoft, NVidia, Google 등 거대 기술 기업들이 미래 지향적인 몰입형 경험의 레이아웃을 강화하면서, 메타버스 비전에 가장 가까운 매개체이자 입구인 XR 디바이스는 자연스럽게 향후 거대 기업이 등장할 가능성이 높은 새로운 트랙이 되었습니다. 메타우주의 비전에 가장 가까운 매개체이자 포털인 XR 디바이스는 자연스럽게 거대 기업이 등장할 가능성이 큰 새로운 트랙이 되었습니다.

12.2 UE 모바일 렌더링 기능

이 장에서는 모바일에서 UE4.27 의 렌더링 특성을 설명합니다.

12.2.1 기능 수준

UE는 모바일에서 다음과 같은 그래픽 API를 지원합니다:

기능 수준 설명
OpenGL ES 3.1 Android의 기본 기능 레벨은 프로젝트 세팅(프로젝트 세팅 &t;  플랫폼 &t;  )에서 구성할 수 있으며, 특정 머티리얼 파라미터를 구성할 수 있습니다. 안드로이드 머티리얼 품질 - ES31)을 참조하여 특정 머티리얼 파라미터를 구성할 수 있습니다.
Android Vulkan 벌칸 1.2 API를 지원하는 일부 특정 Android 기기에서 사용할 수 있는 하이엔드 렌더러로, 경량 설계 철학이 적용된 벌칸이 대부분의 경우 OpenGL보다 효율적입니다.
Metal 2.0 iOS 기기 전용 기능 레벨. 머티리얼 파라미터는 프로젝트 설정 &t:  플랫폼 에서 구성할 수 있습니다. iOS 머티리얼 품질 머티리얼 파라미터를 구성합니다.

현재 주류 안드로이드 기기에서 벌칸의 성능이 더 뛰어난 이유는 벌칸의 경량 설계 철학으로 인해 UE와 같은 앱이 더 정밀하게 최적화를 수행할 수 있기 때문입니다. 다음은 벌칸과 OpenGL의 비교표입니다:

VulkanOpenGL
객체 기반 상태, 전역 상태 없음. 단일 글로벌 스테이트 머신.
모든 상태 개념은 명령 버퍼에 배치됩니다. 상태는 단일 컨텍스트에 바인딩됩니다.
멀티 스레드 코딩이 가능합니다. 렌더링 작업은 순차적으로만 실행할 수 있습니다.
GPU 메모리와 동기화를 정확하고 명시적으로 조작할 수 있습니다. GPU 메모리 및 동기화 세부 정보는 일반적으로 드라이버에 의해 숨겨집니다.
드라이버에는 런타임 오류 감지 기능이 없지만 개발자를 위한 유효성 검사 계층이 존재합니다. 광범위한 런타임 오류 감지.

Windows 플랫폼의 경우, UE 에디터에서 OpenGL, 벌칸, 메탈용 에뮬레이터를 실행하여 에디터에서 이펙트를 미리 볼 수 있지만, 실제 실행 중인 디바이스의 화면과 다를 수 있으므로 전적으로 의존해서는 안 됩니다.

Vulkan을 켜기 전에 프로젝트에서 몇 가지 파라미터를 구성해야 하며, 공식 문서 안드로이드 벌칸 모바일 렌더러.

또한 UE는 몇 번의 릴리스에서 윈도우에 대한 OpenGL 지원을 제거했으며, 현재 UE 에디터에는 여전히 OpenGL 에뮬레이션 옵션이 있지만 실제로는 그 아래에서 D3D로 렌더링됩니다.

12.2.2 Deferred Shading

UE의 디퍼드 셰이딩은 4.26에만 추가된 기능으로, 개발자는 모바일에서 고품질 리플렉션, 멀티 다이내믹 라이팅, 데칼, 고급 라이팅 기능 등 보다 복잡한 라이팅 이펙트를 구현할 수 있습니다.

위: 포워드 렌더링, 아래: 지연 렌더링.
모바일에서 디퍼드 렌더링을 활성화하려면 프로젝트 구성 디렉터리의 DefaultEngine.ini에 r.Mobile.ShadingPath=1 필드를 추가하고 에디터를 다시 시작해야 합니다.

12.2.3 그라운드 트루스 앰비언트 오클루전

그라운드 트루스 앰비언트 오클루전(GTAO)는 실제에 가까운 앰비언트 오클루전 기법으로, 부드러운 그림자 효과를 위해 비직사광의 일부를 가려주는 일종의 그림자 보정 기법입니다.

GTAO 효과를 켜면 로봇이 벽 근처에 있을 때 벽에 그라데이션이 있는 부드러운 그림자 효과를 남기는 것을 볼 수 있습니다.
GTAO를 사용하려면 아래 표시된 옵션을 선택해야 합니다:

또한 GTAO는 모바일 HDR 옵션에 의존하므로 해당 타겟 디바이스에서 활성화하려면 [Platform]Scalability.ini의 구성에 r.Mobile.AmbientOcclusionQuality 필드를 추가해야 하며 이 값은 0보다 커야 합니다. 그렇지 않으면 GTAO가 비활성화됩니다. .
최대 컴퓨트 셰이더 스레드 수가 1024개 미만인 말리 디바이스에서는 GTAO의 성능 문제가 발생한다는 점에 유의할 필요가 있습니다.

12.2.4 다이내믹 라이팅 및 그림자

UE가 모바일에서 구현하는 광원 특성은 다음과 같습니다:

  • 선형 공간에서의 HDR 조명.
  • 방향이 있는 라이트 맵(노멀 고려).
  • 태양(평행광)은 디스턴스 필드 섀도 + 해상도 높은 스페큘러 하이라이트를 지원합니다.
  • IBL 조명: 각 오브젝트는 시차 보정 없이 반사 캐처 중 가장 가까운 것을 샘플링합니다.
  • 동적 오브젝트는 빛을 올바르게 수신하고 그림자를 드리울 수도 있습니다.

UE 모바일에서 지원하는 다이내믹 광원의 유형, 수, 그림자에 대한 정보는 다음과 같습니다:

평행 조명 1 CSM CSM은 기본적으로 2단계로 설정되어 있으며 최대 4단계까지 지원합니다.
포인트 소스 4 지원되지 않음 포인트 라이트 섀도에는 큐브 섀도 맵이 필요하며 원패스 렌더링 큐브 섀도(원패스포인트 라이트 섀도) 기술을 사용하려면 GS(SM5만 해당)가 이를 지원해야 합니다.
스포트라이트 4 지원 기본적으로 비활성화되어 있으며 프로젝트에서 활성화해야 합니다.
영역 조명 0 미지원 동적 영역 조명 효과는 현재 지원되지 않습니다.

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

모바일 베이스패스용 픽셀 셰이더에서 스포트라이트 섀도 맵은 CSM과 동일한 텍스처 샘플러를 공유하고, 스포트라이트 섀도와 CSM은 동일한 섀도 맵 세트를 사용하며, CSM은 충분한 공간을 확보할 수 있는 반면 스포트라이트는 섀도 해상도별로 정렬됩니다.
기본적으로 보이는 그림자의 최대 개수는 8개로 제한되지만, r.Mobile.MaxVisibleMovableSpotLightsShadow의 값을 변경하여 상한을 변경할 수 있습니다. 스포트라이트 그림자의 해상도는 화면 크기와 r.Shadow.TexelsPerPixelSpotlight에 따라 결정됩니다.
포워드 렌더링 경로의 로컬 광원(포인트 및 스폿)의 총 개수는 4개를 초과할 수 없습니다.
모바일에서는 고정(스테이셔너리) 병렬 조명에만 사용할 수 있는 특수 그림자 모드인 변조된 그림자도 지원합니다. 모듈레이티드 섀도를 켰을 때의 효과는 아래와 같습니다:

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

왼쪽: 동적 그림자, 오른쪽: 변조된 그림자.
모바일의 그림자는 자체 그림자, 그림자 품질 수준(r.shadowquality), 깊이 오프셋과 같은 매개변수 설정도 지원합니다.
또한 모바일에서는 기본적으로 GGX의 하이라이트 리플렉션을 사용하므로 기존 하이라이트 셰이딩 모델로 전환하려면 다음 구성에서 변경할 수 있습니다:

12.2.5 픽셀 투영 반사

UE는 화면 공간 픽셀을 재사용하는 핵심 아이디어인 픽셀 투영 반사(PPR)라는 모바일에 최적화된 SSR 버전을 만들었습니다.

PPR效果图。
PPR 효과를 사용하려면 다음 조건을 충족해야 합니다:

  • MobileHDR 옵션을 켭니다.
  • r.Mobile.PixelProjectedReflectionQuality의 값이 0보다 큽니다.
  • 프로젝트 설정 을 설정하고  모바일 을 설정합니다. 평면 반사 모드를 올바른 모드로 설정합니다:

    • 평면 반사 모드에는 3가지 옵션이 있습니다:
      • 일반: 평면 반사형 액터는 모든 플랫폼에서 동일하게 작동합니다.
      • MobilePPR: 평면 반사 액터는 PC/콘솔 플랫폼에서 정상 작동하지만 모바일 플랫폼에서는 에서는 작동하지 않습니다.
      • MobilePPRExclusive: 평면 리플렉션 액터는 모바일 플랫폼에서 PPR을 사용할 때만 작동합니다. 에 대해 모바일 플랫폼에서만 작동하며, PC 및 콘솔 프로젝트가 기존 SSR 을 사용할 수 있는 여지를 남깁니다.

기본적으로 하이엔드 모바일 디바이스만 [Project]Scalability.ini에서 r.Mobile.PixelProjectedReflectionQuality가 켜져 있습니다.

12.2.6 메시 자동 인스턴싱

PC 메시 드로잉 파이프라인는 렌더링 성능을 크게 향상시킬 수 있는 메시의 자동 인스턴싱 및 병합 기능을 지원합니다. 4.27은 이미 모바일에서 이 기능을 지원하고 있습니다.
이 기능을 켜려면 프로젝트 구성 디렉터리에서 DefaultEngine.ini를 열고 다음 필드를 추가해야 합니다:

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

에디터를 다시 시작하고 셰이더 컴파일이 완료될 때까지 기다렸다가 효과를 미리 봅니다.
GPUSceneTexture 지원이 필요하고 Mali 디바이스의 유니폼 버퍼는 최대 64KB에 불과하여 충분히 큰 공간을 지원할 수 없으므로, Mali 디바이스는 버퍼 대신 텍스처를 사용하여 GPUScene 데이터를 저장합니다.
하지만 몇 가지 제한 사항이 있습니다:

  • 모바일 디바이스에서의 자동 인스턴스화는 주로 GPU를 많이 사용하는 프로젝트보다는 CPU를 많이 사용하는 프로젝트에 유리합니다. 자동 인스턴스화를 활성화해도 GPU 집약적인 프로젝트에 해를 끼칠 가능성은 낮지만, 이 기능을 사용한다고 해서 성능이 크게 향상되지는 않을 것입니다.
  • 게임이나 앱에 많은 메모리가 필요한 경우, Mali 기기에서 제대로 실행되지 않으므로 r.Mobile.UseGPUSceneTexture를 끄고 버퍼를 사용하는 것이 더 유리할 수 있습니다.
  • 다른 GPU 공급업체의 장치는 정상적으로 작동하지만 Mali 장치에 대해서는 r.Mobile.UseGPUSceneTexture를 해제할 수도 있습니다.

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

12.2.7 포스트 프로세스 처리(후처리)

느린 종속 텍스처 읽기, 제한된 하드웨어 기능, 특수 하드웨어 아키텍처, 추가 렌더 타깃 파싱, 제한된 대역폭 및 기타 제약으로 인해 모바일 디바이스에서 포스트 프로세싱은 성능 집약적일 수 있으며, 극단적인 경우 렌더 파이프라인을 방해할 수도 있습니다.
UE는 개발자가 고퀄리티 이미지가 필요한 게임이나 앱에 포스트 프로세싱을 사용하는 것을 제한하지 않습니다.
후처리를 사용하려면 먼저 MobileHDR 옵션을 활성화해야 합니다:

후처리가 켜지면 후처리 볼륨>에서 다양한 후처리 효과를 설정할 수 있습니다.
모바일에서 지원할 수 있는 포스트 프로세싱은 모바일 톤매퍼, 컬러 그레이딩, 렌즈, 블룸, 더트 마스크, 자동 노출, 렌즈 플레어, 피사계 심도 등입니다.
더 나은 성능을 위해 공식적으로는 모바일에서만 블룸과 TAA를 사용하도록 권장하고 있습니다.

12.2.8 기타 기능 및 제한 사항

  • 리플렉션 캡처 압축

리플렉션 캡처 컴포넌트(리플렉션 캡처 컴포넌트) 압축에 대한 모바일 지원은 리플렉션 캡처 런타임의 메모리와 대역폭을 줄이고 렌더링 효율성을 향상시킬 수 있습니다. 프로젝트 구성에서 이 기능을 활성화해야 합니다:

켜면 기본적으로 압축에 ETC2가 사용됩니다. 또는 각 리플렉션 캡처 컴포넌트별로 조정할 수 있습니다:

    • 재료 특성

모바일 플랫폼의 머티리얼(기능 수준 Open ES 3.1)은 다른 플랫폼과 동일한 노드 기반 생성 프로세스를 사용하며, 대부분의 노드가 모바일에서 지원됩니다.
모바일 플랫폼에서 지원되는 머티리얼 프로퍼티는 다음과 같습니다: 베이스 컬러, 러프니스, 메탈릭, 스페큘러, 노멀, 이미시브, 굴절, 단, 다음 머티리얼 프로퍼티는 지원되지 않습니다. 씬 컬러 표현식, 테셀레이션 입력, 서브서피스 스캐터링 컬러링 모델이 지원됩니다.
모바일 플랫폼에서 지원하는 자료에는 몇 가지 제한 사항이 있습니다:

      • 하드웨어 제한으로 인해 텍스처 샘플러는 16개만 사용할 수 있습니다.
      • 기본 조명 및 조명 해제 색상 모델만 사용할 수 있습니다.
      • (텍스처 UV의 연산 없이) 텍스처 판독에 의존하지 않으려면 커스텀 UV를 사용해야 합니다.
      • 반투명 및 마스크드 머티리얼은 성능 집약적이므로 가급적 불투명 머티리얼을 사용하는 것이 좋습니다.
      • 뎁스 페이드 기능은 iOS 플랫폼의 반투명 머티리얼에 사용할 수 있지만 하드웨어가 뎁스 버퍼에서 데이터 가져오기를 지원하지 않는 플랫폼에서는 지원되지 않으며 허용할 수 없는 성능 비용을 초래할 수 있습니다.

머티리얼 속성 패널에는 모바일을 위한 몇 가지 특별한 옵션이 있습니다:

이러한 속성은 아래에 설명되어 있습니다:

  • 모바일 별도 반투명: 모바일에서 별도의 반투명 렌더링 텍스처를 활성화할지 여부입니다.
  • 전체 정밀도 사용>: 전체 정밀도를 사용할지 여부, 사용하지 않을 경우 대역폭 사용량과 에너지 소비를 줄이고 성능을 향상시킬 수 있지만 멀리 있는 물체의 결함을 표시할 수 있습니다:

    • 왼쪽: 고정밀 소재, 오른쪽: 멀리 있는 태양에 흠집이 있는 반정밀 소재.
    • 라이트맵 방향성 사용: 라이트맵 방향성을 활성화할지 여부로, 체크하면 라이트맵 방향과 픽셀 노멀을 고려하지만 성능 소비가 증가합니다.
    • 적용 범위에 알파 사용: 마스크된 머티리얼에 대해 MSAA 앤티앨리어싱을 활성화할지 여부를 설정하며, 선택하면 MSAA가 활성화됩니다.
    • Fully Rough: 이 옵션을 선택하면 이 머티리얼의 렌더링 효율이 크게 향상됩니다.

또한 모바일에서 지원되는 그리드 유형은 다음과 같습니다:

      • 스켈레탈 메시
      • 스태틱 메시
      • 랜드스케이프
      • CPU 파티클 스프라이트, 파티클 메시

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

    • 단일 메시의 경우 버텍스 인덱스가 16비트에 불과하기 때문에 최대 65k까지만 올라갈 수 있습니다.
    • 하드웨어 성능 제한으로 인해 단일 스켈레탈 메시의 본 수는 75개 이하여야 합니다.

12.3 FMobileSceneRenderer

FMobileSceneRenderer는 모바일 쪽의 씬 렌더링 프로세스를 담당하는 FSceneRenderer에서 상속되며, PC 쪽은 FSceneRenderer 에서 상속된 것과 동일합니다. 상속 관계 다이어그램은 아래와 같습니다:

앞서 언급한 여러 글에서 언급된 FDeferredShadingSceneRenderer는 복잡한 라이팅 및 렌더링 단계를 포함하는 특히 복잡한 렌더링 프로세스를 가지고 있습니다. 이에 비해 FMobileSceneRenderer의 로직과 단계는 훨씬 간단하며, 아래는 RenderDoc의 컷오프 프레임입니다:

위에는 주로 InitViews, ShadowDepths, PrePass, BasePass, OcclusionTest, ShadowProjectionOnOpaque, Translucency, PostProcessing과 같은 단계가 포함되어 있습니다. 이러한 단계는 PC 측에 존재하지만 구현 프로세스는 다를 수 있습니다. 프로파일링에 대해서는 다음 섹션을 참조하세요.

12.3.1 렌더러 주요 프로세스

모바일 씬 렌더러의 주요 흐름은 다음 코드와 파싱을 통해 FMobileSceneRenderer::Render에서도 이루어집니다:

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

void FMobileSceneRenderer::Render(FRHICommandListImmediate& RHICmdList)
{
    // 튜플 씬 정보를 업데이트합니다.
    Scene->UpdateAllPrimitiveSceneInfos(RHICmdList);

    // 뷰의 렌더링 영역을 준비합니다.
    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);

    // 뷰를 초기화하고, 표시되는 요소를 찾고, 렌더링할 RT 및 버퍼 데이터를 준비합니다.
    InitViews(RHICmdList);
    
    if (GRHINeedsExtraDeletionLatency || !GRHICommandList.Bypass())
    {
        QUICK_SCOPE_CYCLE_COUNTER(STAT_FMobileSceneRenderer_PostInitViewsFlushDel);

        // 마스킹 쿼리를 일시 중지할 수 있으므로 대기하는 동안 RHI 스레드와 GPU가 작동하도록 하는 것이 가장 좋습니다. 또한 RHI 스레드를 실행할 때 보류 중인 삭제를 처리할 수 있는 유일한 위치입니다.
        FRHICommandListExecutor::GetImmediateCommandList().PollOcclusionQueries();
        FRHICommandListExecutor::GetImmediateCommandList().ImmediateFlush(EImmediateFlushType::FlushRHIThreadFlushResources);
    }

    GEngine->GetPreRenderDelegate().Broadcast();

    // 렌더링이 시작되기 전에 글로벌 동적 버퍼를 커밋합니다.
    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);

    // 가상 텍스처 처리
    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);
    }
    
    // 정렬된 광원 정보.
    FSortedLightSetSceneInfo SortedLightSet;
    
    // 延迟渲染.
    if (bDeferredShading)
    {
        // 광원 수집 및 시퀀싱.
        GatherAndSortLights(SortedLightSet);
        
        int32 NumReflectionCaptures = Views[0].NumBoxReflectionCaptures + Views[0].NumSphereReflectionCaptures;
        bool bCullLightsToGrid = (NumReflectionCaptures > 0 || GMobileUseClusteredDeferredShading != 0);
        FRDGBuilder GraphBuilder(RHICmdList);
        // 광원 그리드를 계산합니다.
        ComputeLightGrid(GraphBuilder, bCullLightsToGrid, SortedLightSet);
        GraphBuilder.Execute();
    }

    // 스카이/애트머스피어 LUT를 생성합니다.
    const bool bShouldRenderSkyAtmosphere = ShouldRenderSkyAtmosphere(Scene, ViewFamily.EngineShowFlags);
    if (bShouldRenderSkyAtmosphere)
    {
        FRDGBuilder GraphBuilder(RHICmdList);
        RenderSkyAtmosphereLookUpTables(GraphBuilder);
        GraphBuilder.Execute();
    }

    // 씬을 렌더링할 준비가 되었음을 VFX 시스템에 알립니다.
    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));

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

    // 조회수 목록을 수집합니다.
    TArray<const FViewInfo*> ViewList;
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++) 
    {
        ViewList.Add(&Views[ViewIndex]);
    }

    // 커스텀 심도를 렌더링합니다.
    if (bShouldRenderCustomDepth)
    {
        FRDGBuilder GraphBuilder(RHICmdList);
        FSceneTextureShaderParameters SceneTextures = CreateSceneTextureShaderParameters(GraphBuilder, Views[0].GetFeatureLevel(), ESceneTextureSetupMode::None);
        RenderCustomDepthPass(GraphBuilder, SceneTextures);
        GraphBuilder.Execute();
    }

    // 렌더링 깊이 프리패스.
    if (bIsFullPrepassEnabled)
    {
        // SDF 및 AO에는 전체 프리패스 깊이가 필요합니다.

        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));
        
        // 전체 뎁스 프리패스를 렌더링합니다.
        RenderPrePass(RHICmdList);

        // 마스킹 삼진을 제출합니다.
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Occlusion));
        RenderOcclusion(RHICmdList);

        RHICmdList.EndRenderPass();

        // SDF 섀도우
        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;
    
    // 렌더링 지연.
    if (bDeferredShading)
    {
        SceneColor = RenderDeferred(RHICmdList, ViewList, SortedLightSet);
    }
    // 포워드 렌더링.
    else
    {
        SceneColor = RenderForward(RHICmdList, ViewList);
    }
    
    // 렌더링 속도 버퍼.
    if (bShouldRenderVelocities)
    {
        FRDGBuilder GraphBuilder(RHICmdList);

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

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

        // 무버블 오브젝트 렌더링을 위한 속도 버퍼입니다.
        AddSetCurrentStatPass(GraphBuilder, GET_STATID(STAT_CLMM_Velocity));
        RenderVelocities(GraphBuilder, SceneDepthTexture.Resolve, VelocityTexture, FSceneTextureShaderParameters(), EVelocityPass::Opaque, false);
        AddSetCurrentStatPass(GraphBuilder, GET_STATID(STAT_CLMM_AfterVelocity));

        // 투명 오브젝트 렌더링을 위한 속도 버퍼입니다.
        AddSetCurrentStatPass(GraphBuilder, GET_STATID(STAT_CLMM_TranslucentVelocity));
        RenderVelocities(GraphBuilder, SceneDepthTexture.Resolve, VelocityTexture, GetSceneTextureShaderParameters(CreateMobileSceneTextureUniformBuffer(GraphBuilder, EMobileSceneTextureSetupMode::SceneColor)), EVelocityPass::Translucent, false);

        GraphBuilder.Execute();
    }

    // 렌더링 후 씬을 처리하는 로직입니다.
    {
        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);
    }
    
    // 이후 단계에서 읽을 수 있도록 장면 색상을 SRV로 변환합니다.
    if (!bGammaSpace || bRenderToSceneColor)
    {
        RHICmdList.Transition(FRHITransitionInfo(SceneColor, ERHIAccess::Unknown, ERHIAccess::SRVMask));
    }

    if (bDeferredShading)
    {
        // 씬 렌더링 타깃에 대한 원본 참조를 해제합니다.
        SceneContext.AdjustGBufferRefCount(RHICmdList, -1);
    }

    RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Post));

    // 가상 텍스처 처리.
    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)
        {
            // 뷰당 렌더링 완료 또는 전체 스테레오 버퍼(활성화된 경우)
            {
                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;

                // 렌더링 후 효과.
                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와 다른 점이 있습니다. 이에 대해서는 나중에 자세히 분석하겠습니다. 모바일 씬의 주요 단계와 프로세스는 아래와 같습니다:

위의 순서도에 대해 다음 사항을 명확히 해야 합니다:

  • 플로차트 노드 bDeferredShading과 bDeferredShading2는 동일한 변수이며, 여기서는 주로 머메이드 구문 그리기 오류를 방지하기 위해 구분합니다.
  • 가 표시된 노드는 조건부이며 반드시 실행되지 않는 단계입니다.

UE4.26에는 모바일용 지연 렌더링 파이프라인이 추가되어, 위 코드에는 포워드 렌더링 브랜치인 RenderForward 와 지연 렌더링 브랜치인 RenderDeferred 가 있으며, 둘 다 렌더링 결과인 SceneColor 를 반환합니다.
메트릭 GPU 씬, SDF 섀도, AO, 하늘 분위기, 가상 텍스처, 오클루전 컬링 등과 같은 렌더링 기능도 모바일에서 지원됩니다.
UE 4.26부터 렌더링 시스템은 RDG 시스템을 광범위하게 활용하고 있습니다. cnblogs.com/timlly/p/15217090.html" data-mce-style="colour: #000000;">RDG 시스템이 사용되었으며, 모바일 씬 렌더러도 예외는 아닙니다. 위의 코드에는 광원 그리드를 계산하고 하늘 대기 LUT, 사용자 지정 깊이, 속도 버퍼, 렌더링 후 이벤트, 포스트 프로세싱 등을 렌더링하기 위해 총 여러 개의 FRDGBuilder 인스턴스가 선언되어 있으며, 이는 모두 비교적 독립적인 기능 모듈 또는 렌더링 단계입니다.

12.3.2 RenderForward

RenderForward는 모바일 씬 렌더러에서 포워드 렌더링 브랜치를 담당하며, 코드와 파싱은 다음과 같습니다:

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;

    // 모바일 MSAA 활성화 여부.
    bool bMobileMSAA = NumMSAASamples > 1 && SceneContext.GetSceneColorSurface()->GetNumSamples() > 1;

    // 모바일에서 여러 번 시도 모드를 활성화할지 여부입니다.
    static const auto CVarMobileMultiView = IConsoleManager::Get().FindTConsoleVariableDataInt(TEXT("vr.MobileMultiView"));
    const bool bIsMultiViewApplication = (CVarMobileMultiView && CVarMobileMultiView->GetValueOnAnyThread() != 0);
    
    // 감마 공간에서 브랜치 렌더링.
    if (bGammaSpace && !bRenderToSceneColor)
    {
        // MSAA가 활성화된 경우 SceneContext에서 렌더링된 텍스처를 가져옵니다(씬 컬러 및 파싱된 텍스처 포함).
        if (bMobileMSAA)
        {
            SceneColor = SceneContext.GetSceneColorSurface();
            SceneColorResolve = ViewFamily.RenderTarget->GetRenderTargetTexture();
            ColorTargetAction = ERenderTargetActions::Clear_Resolve;
            RHICmdList.Transition(FRHITransitionInfo(SceneColorResolve, ERHIAccess::Unknown, ERHIAccess::RTV | ERHIAccess::ResolveDst));
        }
        // 비 MSAA, 뷰 패밀리에서 렌더 텍스처를 가져옵니다.
        else
        {
            SceneColor = ViewFamily.RenderTarget->GetRenderTargetTexture();
            RHICmdList.Transition(FRHITransitionInfo(SceneColor, ERHIAccess::Unknown, ERHIAccess::RTV));
        }
        SceneDepth = SceneContext.GetSceneDepthSurface();
    }
    // 선형 공간 또는 씬 텍스처에 렌더링합니다.
    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;
        }
    }

    // 프리패스의 뎁스 텍스처 상태입니다.
    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;
        }
    }

    // 씬 컬러 렌더링패스 정보.
    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;
    }
    // 씬 컬러는 멀티뷰가 아니지만 애플리케이션이 멀티뷰인 경우 셰이더에 멀티뷰의 단일 뷰로 렌더링해야 합니다.
    SceneColorRenderPassInfo.MultiViewCount = View.bIsMobileMultiViewEnabled ? 2 : (bIsMultiViewApplication ? 1 : 0);

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

    if (!bIsFullPrepassEnabled)
    {
        RHICmdList.SetCurrentStat(GET_STATID(STAT_CLM_MobilePrePass));
        // 렌더링 깊이 프리패스
        RenderPrePass(RHICmdList);
    }

    RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Opaque));
    // 베이스패스: 불투명하고 마스킹된 오브젝트를 렌더링합니다.
    RenderMobileBasePass(RHICmdList, ViewList);
    RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);

    //렌더링 디버그 모드.
#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);
        }
    }

    // 이벤트를 게시하고 플러그인 렌더링을 처리합니다.
    {
        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]);
            }
        }
    }
    
    // 투명한 오브젝트나 픽셀 투영의 리플렉션을 렌더링해야 하는 경우 패스를 분할해야 합니다.
    if (bRequiresMultiPass || bRequiresPixelProjectedPlanarRelfectionPass)
    {
        RHICmdList.EndRenderPass();
    }
       
    RHICmdList.SetCurrentStat(GET_STATID(STAT_CLMM_Translucency));

    // 필요한 경우 투명 렌더링 채널을 다시 활성화합니다.
    if (bRequiresMultiPass || bRequiresPixelProjectedPlanarRelfectionPass)
    {
        check(RHICmdList.IsOutsideRenderPass());

        // 현재 하드웨어가 동일한 뎁스 버퍼 읽기 또는 쓰기를 지원하지 않는 경우 씬 뎁스가 복사됩니다.
        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;
        }

        // 모바일에서 픽셀 투영 리플렉션에 사용되는 불투명 메시의 경우 메시가 한 번만 렌더링되므로(품질 수준이 BestPerformance보다 낮거나 같은 경우) 깊이를 깊이 RT에 기록해야 합니다.
        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

        // 투명 오브젝트 렌더링 패스.
        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;
        
        // 반투명 오브젝트 렌더링을 시작합니다.
        RHICmdList.BeginRenderPass(TranslucentRenderPassInfo, TEXT("SceneColorTranslucencyRendering"));
    }

    // 씬 깊이는 읽기 전용이며 가져올 수 있습니다.
    RHICmdList.NextSubpass();

    if (!View.bIsPlanarReflection)
    {
        // 데칼을 렌더링합니다.
        if (ViewFamily.EngineShowFlags.Decals)
        {
            CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderDecals);
            RenderDecals(RHICmdList);
        }

        // 변조된 그림자 드리우기를 렌더링합니다.
        if (ViewFamily.EngineShowFlags.DynamicShadows)
        {
            CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderShadowProjections);
            RenderModulatedShadowProjections(RHICmdList);
        }
    }
    
    // 반투명 그리기.
    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)
    {
        // 아드레노 오클루전 거부 모드.
        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가 해결되기 전에 미리 계산된 톤 매핑(iOS에서만 작동)
    if (!bGammaSpace)
    {
        PreTonemapMSAA(RHICmdList);
    }

    // 엔드 씬 컬러 렌더링.
    RHICmdList.EndRenderPass();

    // 텍스처 파싱을 최적화하여 씬 컬러를 반환합니다(MSAA가 켜져 있는 경우에만 해당).
    return SceneColorResolve ? SceneColorResolve : SceneColor;
}

오클루전 컬링 중 하나는 GPU 공급업체와 관련이 있는데, 예를 들어 퀄컴 아드레노 시리즈 GPU 칩은 플러시 렌더링 인스트럭션과 스위치 FBO 사이에 필요합니다:

Render Opaque -> Render Translucent -> Flush -> Render Queries -> Switch FBO

그런 다음 UE는 오클루전 컬링에 대한 특별한 처리를 통해 Adreno 시리즈 칩의 특별한 요구 사항을 따릅니다.

Adreno 칩 제품군은 TBDR 아키텍처에 대해 Bin과 일반 Direct의 두 가지 혼합 렌더링 모드를 지원하며, 오클루전 쿼리 중에 자동으로 Direct 모드로 전환하여 오클루전 쿼리의 오버헤드를 줄입니다. 플러시 렌더링 명령과 스위치 FBO 사이에 쿼리를 제출하지 않으면 전체 렌더링 파이프라인이 정체되어 렌더링 성능이 저하될 수 있습니다.

MSAA는 자연스러운 하드웨어 지원과 효과와 효율성의 좋은 균형으로 인해 모바일에서 UE 포워드 렌더링을 위한 안티앨리어싱으로 선택되고 있습니다. 따라서 위 코드에는 컬러 및 뎁스 텍스처와 그 리소스 상태를 포함하여 MSAA 처리를 위한 로직이 상당히 많이 등장합니다. MSAA가 켜져 있으면 기본값은 (칩 청크의 데이터를 시스템 그래픽 메모리에 다시 쓰는 동안) RHICmdList.EndRenderPass()에서 씬 컬러를 파싱하여 앤티앨리어싱 텍스처를 가져오는 것입니다. MSAA는 모바일에서 기본적으로 활성화되지 않지만 다음 인터페이스에서 설정할 수 있습니다:

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

위 코드의 bRequiresMultiPass는 반투명 오브젝트를 그리기 위해 전용 렌더링 패스가 필요한지 여부를 나타내며, 그 값은 다음 코드를 통해 결정됩니다:

// 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와 같은 요소에 의해 결정됩니다. 멀티뷰는 렌더링이 두 번 최적화되는 상황에서 XR에 사용되며 기본 및 고급 모드에 모두 존재합니다:

VR 등의 렌더링을 최적화하는 데 사용되는 멀티뷰 비교 차트. 위: 멀티뷰 모드가 없는 렌더링, 두 눈이 각각 드로잉 명령을 제출하는 경우, 가운데: 제출 명령을 다중화하고 GPU 레이어에 명령 목록의 추가 복사본을 복제하는 기본 멀티뷰 모드, 아래: DC, 명령 목록 및 지오메트리 정보를 다중화하는 고급 멀티뷰 모드.
bKeepDepthContent는 깊이 콘텐츠를 보존할지 여부를 표시하여 해당 코드를 결정합니다:

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

// 带MSAA的深度从不保留.
bKeepDepthContent = (NumMSAASamples > 1 ? false : bKeepDepthContent);

위의 코드는 모바일에서 평면 반사를 렌더링하는 특별한 방법인 픽셀 투영 반사(PPR)도 보여줍니다. SSR과 유사하게 구현되지만 씬 색상, 뎁스 버퍼 및 반사 영역만 필요한 데이터가 적습니다. 핵심 단계는 다음과 같습니다:

  • 반사 평면에서 씬 컬러의 모든 픽셀의 미러 위치를 계산합니다.
  • 픽셀의 반사가 반사 영역 내에 있는지 테스트합니다.
    • 빛이 미러링된 픽셀 위치로 투사됩니다.
    • 교차점이 반사 영역 내에 있는지 테스트합니다.
  • 교차점이 발견되면 화면에서 픽셀의 미러링된 위치를 계산합니다.
  • 교차점에 있는 미러 픽셀의 색상을 작성합니다.

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

 

PPR 효과 한눈에 보기.
PPR은 프로젝트 구성에서 설정할 수 있습니다:

12.3.3 RenderDeferred

UE는 4.26에서 모바일 렌더링 파이프라인에 디퍼드 렌더링 브랜치를 추가하고 4.27에서 개선 및 최적화를 진행했습니다. 모바일에서 디퍼드 셰이딩 기능의 활성화 여부는 다음 코드에 의해 결정됩니다:

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

bool IsMobileDeferredShadingEnabled(const FStaticShaderPlatform Platform)
{
    // 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의 코드와 구문 분석입니다:

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存储到GPU的系统内存中,并在单独的渲染通道中进行着色.
    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: 不透明和镂空物体.
    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写入, SceneDepth只读.
        RHICmdList.NextSubpass();
        
        // 渲染贴花.
        if (ViewFamily.EngineShowFlags.Decals)
        {
            CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RenderDecals);
            RenderDecals(RHICmdList);
        }

        // 下个子Pass: SceneColor写入, SceneDepth只读
        RHICmdList.NextSubpass();
        
        // 延迟光照着色.
        MobileDeferredShadingPass(RHICmdList, *Scene, ViewList, SortedLightSet);
        
        // 绘制半透明.
        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);
        }
        
        // 结束渲染Pass.
        RHICmdList.EndRenderPass();
    }
    // 多Pass模式(PC设备模拟的移动端).
    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"));
            
            // 渲染贴花.
            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);
            
            // 绘制半透明.
            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];
}

위의 내용을 보면 모바일용 디퍼드 렌더링 파이프라인은 먼저 베이스패스를 렌더링하고 GBuffer 지오메트리 정보를 얻은 다음 조명 계산을 실행하는 등 PC와 더 유사하다는 것을 알 수 있습니다. 그 흐름도는 다음과 같습니다:

물론 PC와 다른 부분도 있는데, 가장 눈에 띄는 것은 모바일 쪽에서 TB(D)R 아키텍처에 맞게 조정된 서브패스 렌더링을 사용하여 프리패스 깊이, 베이스패스, 조명 계산을 모바일 쪽에서 렌더링하여 장면 색상, 깊이, GBuffer 및 기타 정보를 온칩의 버퍼에 넣어 렌더링 효율성을 높이고 장치의 에너지 소비를 줄였다는 점입니다.

12.3.3.1 MobileDeferredShadingPass

라이팅의 디퍼드 렌더링 프로세스는 MobileDeferredShadingPass가 처리합니다:

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);
    // 创建Uniform Buffer.
    FUniformBufferRHIRef PassUniformBuffer = CreateMobileSceneTextureUniformBuffer(RHICmdList);
    FUniformBufferStaticBindings GlobalUniformBuffers(PassUniformBuffer);
    SCOPED_UNIFORM_BUFFER_GLOBAL_BINDINGS(RHICmdList, GlobalUniformBuffers);
    // 设置视口.
    RHICmdList.SetViewport(View0.ViewRect.Min.X, View0.ViewRect.Min.Y, 0.0f, View0.ViewRect.Max.X, View0.ViewRect.Max.Y, 1.0f);

    // 光照的默认材质.
    FCachedLightMaterial DefaultMaterial;
    DefaultMaterial.MaterialProxy = UMaterial::GetDefaultMaterial(MD_LightFunction)->GetRenderProxy();
    DefaultMaterial.Material = DefaultMaterial.MaterialProxy->GetMaterialNoFallback(ERHIFeatureLevel::ES3_1);
    check(DefaultMaterial.Material);

    // 绘制平行光.
    RenderDirectLight(RHICmdList, Scene, View0, DefaultMaterial);

    if (GMobileUseClusteredDeferredShading == 0)
    {
        // 渲染非分簇的简单光源.
        RenderSimpleLights(RHICmdList, Scene, PassViews, SortedLightSet, DefaultMaterial);
    }

    // 渲染非分簇的局部光源.
    int32 NumLights = SortedLightSet.SortedLights.Num();
    int32 StandardDeferredStart = SortedLightSet.SimpleLightsEnd;
    if (GMobileUseClusteredDeferredShading != 0)
    {
        StandardDeferredStart = SortedLightSet.ClusteredSupportedEnd;
    }

    // 渲染局部光源.
    for (int32 LightIdx = StandardDeferredStart; LightIdx < NumLights; ++LightIdx)
    {
        const FSortedLightSceneInfo& SortedLight = SortedLightSet.SortedLights[LightIdx];
        const FLightSceneInfo& LightSceneInfo = *SortedLight.LightSceneInfo;
        RenderLocalLight(RHICmdList, Scene, View0, LightSceneInfo, DefaultMaterial);
    }
}

다양한 유형의 광원을 렌더링하는 인터페이스에 대한 분석은 아래에서 계속 이어집니다:

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

// 渲染平行光
static void RenderDirectLight(FRHICommandListImmediate& RHICmdList, const FScene& Scene, const FViewInfo& View, const FCachedLightMaterial& DefaultLightMaterial)
{
    FSceneRenderTargets& SceneContext = FSceneRenderTargets::Get(RHICmdList);

    // 查找第一个平行光.
    FLightSceneInfo* DirectionalLight = nullptr;
    for (int32 ChannelIdx = 0; ChannelIdx < UE_ARRAY_COUNT(Scene.MobileDirectionalLights) && !DirectionalLight; ChannelIdx++)
    {
        DirectionalLight = Scene.MobileDirectionalLights[ChannelIdx];
    }

    // 渲染状态.
    FGraphicsPipelineStateInitializer GraphicsPSOInit;
    RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
    // 增加自发光到SceneColor.
    GraphicsPSOInit.BlendState = TStaticBlendState<CW_RGB, BO_Add, BF_One, BF_One>::GetRHI();
    GraphicsPSOInit.RasterizerState = TStaticRasterizerState<>::GetRHI();
    // 只绘制默认光照模型(MSM_DefaultLit)的像素.
    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
    
    // 处理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);

    // 处理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();
    
    // 用全屏幕的矩形绘制.
    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);
}

// 渲染非分簇模式的简单光源.
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];

    // 处理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);
    }

    // 设置PSO.
    FGraphicsPipelineStateInitializer GraphicsPSOLight[2];
    {
        SetupSimpleLightPSO(RHICmdList, View0, VertexShader, PixelShaders[0], GraphicsPSOLight[0]);
        SetupSimpleLightPSO(RHICmdList, View0, VertexShader, PixelShaders[1], GraphicsPSOLight[1]);
    }
    
    // 设置模板缓冲.
    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;
    }
    
    // 遍历所有简单光源列表, 执行着色计算.
    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);
            }

            // 渲染光源遮罩.
            SetGraphicsPipelineState(RHICmdList, GraphicsPSOLightMask);
            VertexShader->SetSimpleLightParameters(RHICmdList, View, LightBounds);
            RHICmdList.SetStencilRef(1);
            StencilingGeometry::DrawSphere(RHICmdList);
                        
            // 渲染光源.
            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);
            
            // 只绘制默认光照模型(MSM_DefaultLit)的像素.
            uint8 StencilRef = GET_STENCIL_MOBILE_SM_MASK(MSM_DefaultLit);
            RHICmdList.SetStencilRef(StencilRef);

            // 用球体渲染光源(点光源和聚光灯), 以快速剔除光源影响之外的像素.
            StencilingGeometry::DrawSphere(RHICmdList);
        }
    }
}

// 渲染局部光源.
static void RenderLocalLight(
    FRHICommandListImmediate& RHICmdList, 
    const FScene& Scene, 
    const FViewInfo& View, 
    const FLightSceneInfo& LightSceneInfo, 
    const FCachedLightMaterial& DefaultLightMaterial)
{
    if (!LightSceneInfo.ShouldRenderLight(View))
    {
        return;
    }

    // 忽略非局部光源(光源和聚光灯之外的光源).
    const uint8 LightType = LightSceneInfo.Proxy->GetLightType();
    const bool bIsSpotLight = LightType == LightType_Spot;
    const bool bIsPointLight = LightType == LightType_Point;
    if (!bIsSpotLight && !bIsPointLight)
    {
        return;
    }
    
    // 绘制光源模板.
    if (GMobileUseLightStencilCulling != 0)
    {
        RenderLocalLight_StencilMask(RHICmdList, Scene, View, LightSceneInfo);
    }

    // 处理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();
    
    // 设置光源光栅化和深度状态.
    if (GMobileUseLightStencilCulling != 0)
    {
        SetLocalLightRasterizerAndDepthState_StencilMask(GraphicsPSOInit, View);
    }
    else
    {
        SetLocalLightRasterizerAndDepthState(GraphicsPSOInit, View, LightBounds);
    }

    // 设置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);

    // 设置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);

    // 只绘制默认光照模型(MSM_DefaultLit)的像素.
    uint8 StencilRef = GET_STENCIL_MOBILE_SM_MASK(MSM_DefaultLit);
    RHICmdList.SetStencilRef(StencilRef);

    // 点光源用球体绘制.
    if (LightType == LightType_Point)
    {
        StencilingGeometry::DrawSphere(RHICmdList);
    }
    // 聚光灯用锥体绘制.
    else // LightType_Spot
    {
        StencilingGeometry::DrawCone(RHICmdList);
    }
}

광원을 그릴 때는 광원 유형별로 평행 광원, 비분할 클러스터 단순 광원, 국소 광원(점 및 스폿)의 세 가지 단계가 있습니다. 모바일에서는 기본 광원 모델(MSM_DefaultLit)의 계산만 지원되며, 다른 고급 광원 모델(헤어, 표면 산란, 광택, 눈, 천 등)은 현재 지원되지 않는다는 점에 유의하세요.
평행 조명을 그릴 때는 최대 1개까지 그릴 수 있고, 전체 화면 직사각형 그림이 사용되며, 여러 단계의 CSM 음영이 지원됩니다.
분할 클러스터가 아닌 단순 광원(점 또는 점)을 그릴 때는 구를 사용하여 그리며 그림자는 지원되지 않습니다.
로컬 광원을 그리려면 먼저 로컬 광원 템플릿 버퍼를 그린 다음 래스터화 및 깊이 상태를 설정하고 마지막으로 광원을 그리는 것이 훨씬 더 복잡합니다. 그중 점 광원은 그림자를 지원하지 않는 구체로 그려지고, 면광은 그림자를 지원할 수 있는 원뿔로 그려지며, 기본적으로 면광은 동적 조명 및 그림자 계산을 지원하지 않으므로 프로젝트 구성에서 이를 켜야 합니다:

또한 광원이 교차하지 않는 픽셀의 스텐실 컬링을 켤지 여부는 기본값이 1(즉, 켜짐 상태)인 r.Mobile.UseLightStencilCulling에 의해 결정되며, 이는 다시 GMobileUseLightStencilCulling에 의해 결정됩니다. 광원 렌더링을 위한 스텐실 버퍼 코드는 다음과 같습니다:

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

    FGraphicsPipelineStateInitializer GraphicsPSOInit;
    // 应用缓存好的RT(颜色/深度等).
    RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);
    GraphicsPSOInit.PrimitiveType = PT_TriangleList;
    // 禁用所有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();
    // 如果深度测试失败, 则写入模板缓冲值为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, 
        // 注意只写入Pass专用的沙盒(SANBOX)位, 即模板缓冲的索引为0的位.
        STENCIL_SANDBOX_MASK>::GetRHI();
    
    // 绘制光源模板的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);

    // 根据不同光源用不同形状绘制.
    if (LightType == LightType_Point)
    {
        StencilingGeometry::DrawSphere(RHICmdList);
    }
    else // LightType_Spot
    {
        StencilingGeometry::DrawCone(RHICmdList);
    }
}

각 로컬 광원은 먼저 광원 범위 내에서 마스크를 그린 다음 스텐실 테스트(Early-Z)를 통과한 픽셀의 조도를 계산합니다. 정확한 프로파일링 프로세스는 다음 그림의 스포트라이트에 설명되어 있습니다:

위: 렌더링을 기다리는 장면의 스포트라이트, 가운데: 템플릿패스를 사용하여 그려진 템플릿 마스크(흰색 영역)로, 화면 공간에서 스포트라이트 모양과 겹치고 깊이가 더 가까운 픽셀을 표시, 아래: 조명 계산이 유효 픽셀에 미치는 영향입니다.
유효한 픽셀의 광원 계산에 사용되는 DepthStencil 상태는 다음과 같습니다:

광원 드로잉을 수행하는 픽셀은 광원 모양 본체 내에 있어야 하며, 광원 모양을 벗어난 픽셀은 제거됩니다. 템플릿 패스는 광원 형상의 깊이보다 가까운 픽셀(광원 형상 본체 외부의 픽셀)을 표시하고, 광원 드로잉 패스는 템플릿 테스트를 통해 템플릿 패스로 표시된 픽셀을 제거한 후 깊이 테스트를 통해 광원 형상 본체 내부에 있는 픽셀을 찾아 광원 계산의 효율을 높입니다.
모바일용 라이트 스텐실 컬링 기법과 시그그래프2020의 Unity 강연 유니티 URP의 디퍼드 셰이딩에서 유사한 템플릿 기반 조명 계산을 언급합니다(아이디어는 동일하지만 접근 방식이 완전히 같지는 않을 수 있음). 이 논문에서는 광원의 모양에 더 잘 맞는 지오메트리 시뮬레이션도 제안합니다:

다음은 PC와 모바일에서 다양한 광원 계산 방법의 성능을 비교한 것으로, 말리 GPU에 대한 비교 차트입니다:

다양한 조명 렌더링 기법을 사용한 Mali GPU의 성능 비교 결과, 템플릿 자르기 기반 조명 알고리즘이 모바일 측에서 일반 및 청킹 알고리즘보다 성능이 뛰어난 것으로 나타났습니다.

광원 템플릿 자르기 기술과 GPU의 Early-Z 기술을 결합하면 광원 렌더링 성능이 크게 향상된다는 점을 언급할 가치가 있습니다. 현재 주류 모바일 GPU는 모두 Early-Z 기술을 지원하며, 이는 광원 템플릿 자르기 적용을 위한 토대를 마련합니다.

현재 UE에서 구현된 광원 자르기 알고리즘은 여전히 개선의 여지가 있는데, 예를 들어 아래 그림의 빨간색 상자에 표시된 후방 광원의 픽셀이 실제로 계산에서 누락될 수 있습니다. (하지만 후방 광원의 픽셀을 빠르고 효율적으로 찾는 방법은 또 다른 문제입니다.)

12.3.3.2 MobileBasePassShader

이 섹션에서는 VS와 PS를 포함한 모바일용 BasePass에 관련된 셰이더에 초점을 맞춥니다. 먼저 VS를 살펴보세요:

// 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
    )
{
// 스테레오스코픽 보기 모드.
#if INSTANCED_STEREO
    const uint EyeIndex = GetEyeIndex(InstanceId);
    ResolvedView = ResolveView(EyeIndex);
    LayerIndex = EyeIndex;
    Output.BasePassInterpolants.MultiViewId = float(EyeIndex);
// 멀티뷰 모드.
#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

    // 패킹된 보간 데이터를 초기화합니다.
#if PACK_INTERPOLANTS
    float4 PackedInterps[NUM_VF_PACKED_INTERPOLANTS];
    UNROLL 
    for(int i = 0; i < NUM_VF_PACKED_INTERPOLANTS; ++i)
    {
        PackedInterps[i] = 0;
    }
#endif

    // 버텍스 팩토리 데이터 처리.
    FVertexFactoryIntermediates VFIntermediates = GetVertexFactoryIntermediates(Input);
    float4 WorldPositionExcludingWPO = VertexFactoryGetWorldPosition(Input, VFIntermediates);
    float4 WorldPosition = WorldPositionExcludingWPO;

    // 머티리얼의 버텍스 데이터를 가져오고 좌표를 처리하는 등의 작업을 수행합니다.
    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

    // 절두체부.
#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

    (......)

    // 보간할 데이터를 가져옵니다.
    Output.FactoryInterpolants = VertexFactoryGetInterpolants(Input, VFIntermediates, VertexParameters);

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

    // 보간된 데이터를 패킹합니다.
#if PACK_INTERPOLANTS
    VertexFactoryPackInterpolants(Output.FactoryInterpolants, PackedInterps);
#endif // PACK_INTERPOLANTS

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

위에서 볼 수 있듯이 뷰 인스턴스는 스테레오 드로잉, 멀티뷰 및 일반 모드에 따라 다르게 처리됩니다. 버텍스 포그는 지원되지만 기본적으로 꺼져 있으며 프로젝트 구성에서 켜야 합니다.
패킹 보간 모드는 VS에서 PS 사이의 보간 소비량과 대역폭을 압축하기 위해 존재합니다. 이 모드의 켜짐 여부는 다음과 같이 정의된 매크로 PACK_INTERPOLANTS에 의해 결정됩니다:

// Engine\Shaders\Private\MobileBasePassCommon.ush

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

즉, 패킹 보간 기능은 버텍스 포그가 활성화되어 있고, 버텍스 팩토리 패킹 보간 데이터가 존재하며, 플랫폼이 OpenGLES 3.1 컬러인 경우에만 활성화됩니다. PC의 베이스패스와 비교했을 때, 모바일 쪽은 많이 단순화되었으며, 단순히 PC 쪽의 아주 작은 하위 집합으로 간주할 수 있습니다. 여기서는 PS를 계속 분석합니다:

// Engine\Shaders\Private\MobileBasePassPixelShader.usf

#include "Common.ush"

// 各类宏定义.
#define MobileSceneTextures MobileBasePass.SceneTextures
#define EyeAdaptationStruct MobileBasePass

(......)

// 最接近被渲染对象的场景的预归一化捕获(完全粗糙材质不支持)
#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

// 反射球/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);

// 累积动态点光源.
#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;

    // 计算聚光灯阴影.
#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

    // 计算光照.
    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));

        // 计算光源的衰减.
        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

        // 累加光照结果.
#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

(......)

// 计算非直接光照.
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;

    // 非直接漫反射.
#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
        // 将法线应用到半透明物体.
        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);

        // 计算加入了法线影响的漫反射光照.
        half3 DiffuseGI = max(half3(0, 0, 0), DotSH3(PointIndirectLighting, DiffuseTransferSH));

        IndirectIrradiance = Luminance(DiffuseGI);
        Color += ShadingModelContext.DiffuseColor * DiffuseGI * View.IndirectLightingColorScale;
    #else 
        // 半透明使用无方向(Non-directional), 漫反射被打包在xyz, 已经在cpu端除了PI和SH漫反射.
        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

    // 解压打包的插值数据.
#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

    // 获取材质的像素属性.
    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
    }

    // 像素深度偏移.
#if OUTPUT_PIXEL_DEPTH_OFFSET
    ApplyPixelDepthOffsetForMobileBasePass(MaterialParameters, PixelMaterialInputs, OutDepth);
#endif
      
    // Mask材质.
#if !EARLY_Z_PASS_ONLY_MATERIAL_MASKING
    //Clip if the blend mode requires it.
    GetMaterialCoverageAndClipping(MaterialParameters, PixelMaterialInputs);
#endif

    // 计算并缓存GBuffer数据, 防止后续多次采用纹理.
    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, 而后面的粗糙度涉及到1.0 / Roughness^4的计算, 所以为了防止除零错误, 需保证Roughness^4 >= 5.96e-8, 此处直接Clamp粗糙度到0.015625(0.015625^4 = 5.96e-8).
    // 另外, 为了匹配PC端的延迟渲染(粗糙度存储在8位的值), 因此也自动Clamp到1.0.
    GBuffer.Roughness = max(0.015625, GetMaterialRoughness(PixelMaterialInputs));
    
    // 初始化移动端着色模型上下文FMobileShadingModelContext.
    FMobileShadingModelContext ShadingModelContext = (FMobileShadingModelContext)0;
    ShadingModelContext.Opacity = GetMaterialOpacity(PixelMaterialInputs);

    // 薄层透明度物
#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

    // 光照图虚拟纹理.
    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

    // 计算非直接光.
    half IndirectIrradiance;
    half3 IndirectColor;
    ComputeIndirect(LightmapVTPageTableResult, Interpolants, DiffuseDir, ShadingModelContext, IndirectIrradiance, IndirectColor);
    Color += IndirectColor;

    // 预计算的阴影图.
    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阴影.
    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 */

    // 距离场阴影.
#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保存了平行光的SpecularScale.
    Color += (Shadow) * MobileDirectionalLight.DirectionalLightColor.rgb * (Lighting.Diffuse + Lighting.Specular * MobileDirectionalLight.DirectionalLightDistanceFadeMADAndSpecularScale.z);

    // 头发着色.
#if    !(MATERIAL_SINGLE_SHADINGMODEL && MATERIAL_SHADINGMODEL_HAIR)
    (......)
#endif
#endif /* FULLY_ROUGH */

    // 局部光源, 最多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

    // 天空光.
#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

    // 处理顶点雾.
    half4 VertexFog = half4(0, 0, 0, 1);
#if USE_VERTEX_FOG
#if PACK_INTERPOLANTS
    VertexFog = PackedInterpolants[0];
#else
    VertexFog = BasePassInterpolants.VertexFog;
#endif
#endif
    
    // 自发光.
    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

    // 处理颜色的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, 포그 효과 계산, 물체, 머리카락, 얇은 투명도 등 특수 셰이딩 모델 처리 등의 작업을 수행합니다.

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

또한 모바일 단말기의 렌더링 특성을 개발할 때 데이터 정확도에 각별한 주의를 기울이고 제어해야 하며, 그렇지 않으면 데이터 정확도 부족으로 인해 저사양 기기에서 온갖 종류의 이상한 화면 이상이 자주 나타날 수 있음을 경고합니다.

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

포워드 렌더링 브랜치인 경우 대부분의 GBuffer 처리가 무시되고, 지연 렌더링 브랜치인 경우 병렬 광원 및 로컬 광원 계산은 무시되고 지연 렌더링 패스의 셰이더가 수행합니다.
중요한 인터페이스인 EncodeGBuffer는 아래에 프로파일링되어 있습니다:

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: 八面体压缩后的法线, 预计算阴影因子, 逐物体数据.
#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: 金属度, 高光度, 粗糙度, 着色模型, 其它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 라이팅 모델에서 베이스패스는 다음과 같은 텍스처를 출력합니다:

12.3.3.3 MobileDeferredShading

모바일 지연 조명 VS와 PC는 동일하고, DeferredLightVertexShaders.usf이지만, PS는 동일하지 않으며, MobileDeferredShading.usf로 인해 VS와 PC가 동일하고 특별한 조작이 없기 때문에 여기서는 무시되며, 학생들이 관심이 있다면 섹션의 다섯 번째 기사를 볼 수있는 경우 무시됩니다. 5.5.3.1 DeferredLightVertexShader.
下面直接分析PS代码:

// Engine\Shaders\Private\MobileDeferredShading.usf

(......)

// 移动端光源数据结构体.
struct FMobileLightData
{
    float3 Position;
    float  InvRadius;
    float3 Color;
    float  FalloffExponent;
    float3 Direction;
    float2 SpotAngles;
    float SourceRadius;
    float SpecularScale;
    bool bInverseSquared;
    bool bSpotLight;
};

// 获取GBuffer数据.
void FetchGBuffer(in float2 UV, out float4 GBufferA, out float4 GBufferB, out float4 GBufferC, out float4 GBufferD, out float SceneDepth)
{
    // Vulkan的子pass获取数据.
#if VULKAN_PROFILE
    GBufferA = VulkanSubpassFetch1(); 
    GBufferB = VulkanSubpassFetch2(); 
    GBufferC = VulkanSubpassFetch3(); 
    GBufferD = 0;
    SceneDepth = ConvertFromDeviceZ(VulkanSubpassDepthFetch());
    // Metal的子pass获取数据.
#elif METAL_PROFILE
    GBufferA = SubpassFetchRGBA_1(); 
    GBufferB = SubpassFetchRGBA_2(); 
    GBufferC = SubpassFetchRGBA_3(); 
    GBufferD = 0; 
    SceneDepth = ConvertFromDeviceZ(SubpassFetchR_4());
    // 其它平台(DX, OpenGL)的子pass获取数据.
#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
}

// 解压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);
    
    // 光源衰减.
    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);
    }

    // 聚光灯衰减.
    if (LightData.bSpotLight)
    {
        Attenuation *= SpotAttenuation(L, -LightData.Direction, LightData.SpotAngles);
    }
    
    // 如果衰减不为0, 则计算直接光照.
    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);
// 使用光网格添加局部光照, 不支持动态阴影, 因为需要逐光源阴影图.
half3 GetLightGridLocalLighting(const FCulledLightsGridData InLightGridData, ...);

// 平行光的PS主入口.
void MobileDirectLightPS(
    noperspective float4 UVAndScreenPos : TEXCOORD0, 
    float4 SvPosition : SV_POSITION, 
    out half4 OutColor : SV_Target0)
{
    // 恢复(读取)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);
    }
    
    // 计算基础向量.
    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阴影.
#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
        // 计算环境的BRDF.
        ShadingModelContext.SpecularColor = GetEnvBRDF(ShadingModelContext.SpecularColor, GBuffer.Roughness, NoV);
    }
    
    // 局部光源.
    float2 LocalPosition = SvPosition.xy - View.ViewRectMin.xy;
    uint GridIndex = ComputeLightGridCellIndex(uint2(LocalPosition.x, LocalPosition.y), SceneDepth);
    // 分簇光源
#if USE_CLUSTERED
    {
        const uint EyeIndex = 0;
        const FCulledLightsGridData CulledLightGridData = GetCulledLightsGrid(GridIndex, EyeIndex);
        Color += GetLightGridLocalLighting(CulledLightGridData, ShadingModelContext, GBuffer, WorldPosition, CameraVector, EyeIndex, 0);
    }
#endif
            
    // 计算平行光.
    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);

    // 处理反射(IBL, 反射捕捉器).
#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
    // 天空光漫反射.
    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;
}

// 局部光源的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));
    
    // 组装光源数据结构体.
    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
        // 计算环境BRDF.
        ShadingModelContext.SpecularColor = GetEnvBRDF(ShadingModelContext.SpecularColor, GBuffer.Roughness, NoV);
    }
    
    // 计算直接光.
    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;
}

팀 모집

블로거 팀은 UE4로 새로운 몰입형 경험을 개발 중이며, 이 야심찬 도전에 함께할 인재를 찾고 있습니다. 현재 다음과 같은 직책을 채용하고 있습니다:

  • UE逻辑开发。
  • UE引擎程序。
  • UE图形渲染。
  • TA(技术向、美术向)。

요구 사항: 기술에 대한 열정, 탄탄한 기술 기반, 원활한 커뮤니케이션 및 협업 능력, UE 또는 모바일 개발 사용 경험은 장점입니다.
 
 
 

특별 참고 사항

  • 파트 1이 끝나고 파트 2가 시작됩니다:
    • 모바일 렌더링 기술
    • 모바일 최적화 팁

 

참고 문헌

  • Unreal Engine Source
  • Rendering and Graphics
  • Materials
  • Graphics Programming
  • Mobile Rendering
  • Qualcomm® Adreno™ GPU
  • PowerVR Developer Documentation
  • Arm Mali GPU Best Practices Developer Guide
  • Arm Mali GPU Graphics and Gaming Development
  • Moving Mobile Graphics
  • GDC Vault
  • Siggraph Conference Content
  • GameDev Best Practices
  • Accelerating Mobile XR
  • Frequently Asked Questions
  • Google Developer Contributes Universal Bandwidth Compression To Freedreno Driver
  • Using pipeline barriers efficiently
  • Optimized pixel-projected reflections for planar reflectors
  • UE4画面表现移动端较PC端差异及最小化差异的分享
  • Deferred Shading in Unity URP
  • 移动游戏性能优化通用技法
  • 深入GPU硬件架构及运行机制
  • Adaptive Performance in Call of Duty Mobile
  • Jet Set Vulkan : Reflecting on the move to Vulkan
  • Vulkan Best Practices - Memory limits with Vulkan on Mali GPUs
  • A Year in a Fortnite
  • The Challenges of Porting Traha to Vulkan
  • L2M - Binding and Format Optimization
  • Adreno Best Practices
  • 移动设备GPU架构知识汇总
  • Mali GPU Architectures
  • Cyclic Redundancy Check
  • Arm Guide for Unreal Engine
  • Arm Virtual Reality
  • Best Practices for VR on Unreal Engine
  • Optimizing Assets for Mobile VR
  • Arm® Guide for Unreal Engine 4 Optimizing Mobile Gaming Graphics
  • Adaptive Scalable Texture Compression
  • Tile-Based Rendering
  • Understanding Render Passes
  • Intro to Moving Mobile Graphics
  • Mobile Graphics 101
  • Intro to Moving Mobile Graphics
  • Mobile Graphics 101
  • Vulkan API
  • Best Practices for Shaders

원문
https://www.cnblogs.com/timlly/p/15511402.html#1226-mesh-auto-instancing

 

剖析虚幻渲染体系(12)- 移动端专题Part 1(UE移动端渲染分析) - 0向往0 - 博客园

12.1 本篇概述 前面的所有篇章都是基于PC端的延迟渲染管线阐述UE的渲染体系的,特别是剖析虚幻渲染体系(04)- 延迟渲染管线详尽地阐述了在PC端的延迟渲染管线的流程和步骤。 此篇只要针对UE

www.cnblogs.com