[번역] 언리얼 렌더링 시스템 해부하기 (12) - 모바일 파트 1 (UE 모바일 렌더링 분석)
jplee2024. 5. 21. 12:47
역자의 말. 가끔은 언제쯤이면 모바일 하드웨어 지원 게임을 만들지 않을 수 있을까? 라는 생각 정도는 거의 15년 가까이 모바일 게임과 멀티플레폼 게임을 개발 하다보면 충분히 갖을 수 있는 생각이지 않을까 싶네요. 중국 CNBLOG 는 쯔후 보다 더 오래 된 기술 전문 블로그 플레폼 입니다. 쯔후는 약간 네이버 지식인 + 블로그 개념인데 반해 CNBLOG 는 프로그래머들이 주로 사용하던 테크 티스토리 같은 거라고 할까요? 아무튼... 언리얼 엔진으로 게임을 개발 하고 멀티플레폼을 지원 해야하니까 언제나 리마인드 하는 마음으로 또 복기 해 보고자 했습니다.
앞의 모든 챕터는 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를 열고 다음 필드를 추가해야 합니다:
에디터를 다시 시작하고 셰이더 컴파일이 완료될 때까지 기다렸다가 효과를 미리 봅니다. 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)은 다른 플랫폼과 동일한 노드 기반 생성 프로세스를 사용하며, 대부분의 노드가 모바일에서 지원됩니다. 모바일 플랫폼에서 지원되는 머티리얼 프로퍼티는 다음과 같습니다: 베이스 컬러, 러프니스, 메탈릭, 스페큘러, 노멀, 이미시브, 굴절, 단, 다음 머티리얼 프로퍼티는 지원되지 않습니다. 씬 컬러 표현식, 테셀레이션 입력, 서브서피스 스캐터링 컬러링 모델이 지원됩니다. 모바일 플랫폼에서 지원하는 자료에는 몇 가지 제한 사항이 있습니다:
적용 범위에 알파 사용: 마스크된 머티리얼에 대해 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 사이에 필요합니다:
그런 다음 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는 깊이 콘텐츠를 보존할지 여부를 표시하여 해당 코드를 결정합니다:
위의 내용을 보면 모바일용 디퍼드 렌더링 파이프라인은 먼저 베이스패스를 렌더링하고 GBuffer 지오메트리 정보를 얻은 다음 조명 계산을 실행하는 등 PC와 더 유사하다는 것을 알 수 있습니다. 그 흐름도는 다음과 같습니다:
물론 PC와 다른 부분도 있는데, 가장 눈에 띄는 것은 모바일 쪽에서 TB(D)R 아키텍처에 맞게 조정된 서브패스 렌더링을 사용하여 프리패스 깊이, 베이스패스, 조명 계산을 모바일 쪽에서 렌더링하여 장면 색상, 깊이, GBuffer 및 기타 정보를 온칩의 버퍼에 넣어 렌더링 효율성을 높이고 장치의 에너지 소비를 줄였다는 점입니다.
광원을 그릴 때는 광원 유형별로 평행 광원, 비분할 클러스터 단순 광원, 국소 광원(점 및 스폿)의 세 가지 단계가 있습니다. 모바일에서는 기본 광원 모델(MSM_DefaultLit)의 계산만 지원되며, 다른 고급 광원 모델(헤어, 표면 산란, 광택, 눈, 천 등)은 현재 지원되지 않는다는 점에 유의하세요. 평행 조명을 그릴 때는 최대 1개까지 그릴 수 있고, 전체 화면 직사각형 그림이 사용되며, 여러 단계의 CSM 음영이 지원됩니다. 분할 클러스터가 아닌 단순 광원(점 또는 점)을 그릴 때는 구를 사용하여 그리며 그림자는 지원되지 않습니다. 로컬 광원을 그리려면 먼저 로컬 광원 템플릿 버퍼를 그린 다음 래스터화 및 깊이 상태를 설정하고 마지막으로 광원을 그리는 것이 훨씬 더 복잡합니다. 그중 점 광원은 그림자를 지원하지 않는 구체로 그려지고, 면광은 그림자를 지원할 수 있는 원뿔로 그려지며, 기본적으로 면광은 동적 조명 및 그림자 계산을 지원하지 않으므로 프로젝트 구성에서 이를 켜야 합니다:
또한 광원이 교차하지 않는 픽셀의 스텐실 컬링을 켤지 여부는 기본값이 1(즉, 켜짐 상태)인 r.Mobile.UseLightStencilCulling에 의해 결정되며, 이는 다시 GMobileUseLightStencilCulling에 의해 결정됩니다. 광원 렌더링을 위한 스텐실 버퍼 코드는 다음과 같습니다:
각 로컬 광원은 먼저 광원 범위 내에서 마스크를 그린 다음 스텐실 테스트(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를 살펴보세요:
위에서 볼 수 있듯이 뷰 인스턴스는 스테레오 드로잉, 멀티뷰 및 일반 모드에 따라 다르게 처리됩니다. 버텍스 포그는 지원되지만 기본적으로 꺼져 있으며 프로젝트 구성에서 켜야 합니다. 패킹 보간 모드는 VS에서 PS 사이의 보간 소비량과 대역폭을 압축하기 위해 존재합니다. 이 모드의 켜짐 여부는 다음과 같이 정의된 매크로 PACK_INTERPOLANTS에 의해 결정됩니다:
즉, 패킹 보간 기능은 버텍스 포그가 활성화되어 있고, 버텍스 팩토리 패킹 보간 데이터가 존재하며, 플랫폼이 OpenGLES 3.1 컬러인 경우에만 활성화됩니다. PC의 베이스패스와 비교했을 때, 모바일 쪽은 많이 단순화되었으며, 단순히 PC 쪽의 아주 작은 하위 집합으로 간주할 수 있습니다. 여기서는 PS를 계속 분석합니다:
모바일용 BasePassPS의 처리는 복잡하고 많은 단계가 있으며, 주요 단계는 보간 데이터 압축 해제, 머티리얼 속성 가져오기 및 계산, 마을 GBuffer 계산 및 완화, GBuffer 데이터 처리 또는 조정, 전방 렌더링 분기(병렬 및 로컬라이즈드 라이트)의 조명 계산, 디스턴스 필드 및 CSM 등의 그림자 계산, 스카이 라이트 계산, 정적 조명 처리, 디렉티드 라이트, 간접광 및 IBL, 포그 효과 계산, 물체, 머리카락, 얇은 투명도 등 특수 셰이딩 모델 처리 등의 작업을 수행합니다.
또한 모바일 단말기의 렌더링 특성을 개발할 때 데이터 정확도에 각별한 주의를 기울이고 제어해야 하며, 그렇지 않으면 데이터 정확도 부족으로 인해 저사양 기기에서 온갖 종류의 이상한 화면 이상이 자주 나타날 수 있음을 경고합니다.
위에 더 많은 코드가 있지만, 이는 여러 매크로에 의해 제어되며 실제로 단일 머티리얼을 렌더링하는 데 필요한 코드는 그 중 아주 작은 부분일 수 있습니다. 예를 들어 기본적으로 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代码: