[번역] UE5.1 모바일 지연 렌더링 파이프라인 테스트 및 프로파일링
UE5.1은 모바일 지연 렌더링에 대한 대대적인 업데이트를 진행했으며, 단 3개의 gbuffer(SceneColor와 Depth 제외)만 사용하면서도 데스크톱의 모든 shadingmodel을 지원하고, Vulkan, Metal은 물론 GLES에도 On Chip Memory 대역폭 최적화를 적용했습니다. 마침 제가 Unity 2021.3의 지연 렌더링 파이프라인을 살펴본 직후라 UE5.1에는 어떤 새로운 기능이 있는지 확인해 보려고 합니다.
공식 발표:
테스트 및 소스 코드 검토 후 확인된 기능
- directional light는 여러 개 사용 가능(각각 다른 light channel에 위치해야 함), 그 중 하나만 그림자 투사 가능
- point light 개수에 제한이 없어짐, 그림자 미지원
- spot light는 그림자 투사 가능
- 각 조명은 하나의 light channel만 가질 수 있음
- 오브젝트는 여러 light channel을 가질 수 있음(즉, 여러 light channel의 광원 영향을 동시에 받을 수 있음)
- 반투명 효과 정상 작동
- gles/vulkan 모두 On Chip Memory 최적화 지원, gles에서는 하드웨어에 따라 framebuffer fetch/pixel local storage 방식 선택(Mali 플랫폼의 PLS 미테스트, Metal 플랫폼 미테스트)
- 정적 조명을 비활성화한 상태에서 세 개의 gbuffer로 데스크톱의 모든 shadingmodel 지원, 소스 코드에 네 번째 gbuffer 스위치 예약됨(미활성화)
테스트 장면
- 여섯 개의 점광원: 왼쪽 구체 주변에 네 개(channel2), 오른쪽 의자 위에 각각 하나씩(channel0)
- 한 개의 스포트라이트: 왼쪽 구체 좌측, channel1, 그림자 투사
- 두 개의 방향광: 주 광원은 흰색(channel0), 보조 광원은 빨간색(channel1)
- 왼쪽 구체는 channel012, 왼쪽 바닥은 channel01, 오른쪽 바닥 및 기타 오브젝트는 모두 channel0
지연 렌더링 관련 소스 코드 분석
API 호환성
다양한 API와 플랫폼의 On-Chip Memory 최적화 방안을 호환하기 위해 Basepass에서 매크로를 통해 처리합니다.
USE_GLES_FBF_DEFERRED와 MOBILE_EXTENDED_GBUFFER는 ShaderCompiler.cpp에서 정의됩니다:
여기서 USE_GLES_FBF_DEFERRED는 단지 GLES 플랫폼인지 여부만 결정하며, FBF/PLS 판단은 다른 곳에서 이루어집니다. 이에 대해서는 나중에 자세히 설명하겠습니다.
LightPass에서 gbuffer 읽기는 On-Chip Memory 최적화의 핵심이며, 이 부분 코드는 MobileDeferredShading.usf에 정의되어 있습니다:
코드가 Vulkan/Metal/GLES의 상황을 명확히 구분하고 있으며, 최악의 경우 기본 구현은 전통적인 MRT 방식입니다.
subpass 관련 gbuffer 읽기는 다음 세 파일에 정의되어 있습니다:
Vulkan 부분을 열어보니, HLSL로 셰이더를 작성한 다음 DXC로 컴파일하여 Vulkan에서 사용하는 일반적인 방식입니다.
깊이를 RT에 렌더링할지 여부를 결정하는 USE_SCENE_DEPTH_AUX에 대한 셰이더 측 정의는 다음과 같으며, 모바일 지연 렌더링의 경우 Metal/GLES에서 활성화됩니다.
C++ 쪽은 FMobileSceneRenderer::RenderDeferred() 함수에서 bRequiresSceneDepthAux로 판단합니다:
bRequiresSceneDepthAux의 값은 MobileRequiresSceneDepthAux() 함수에 의존합니다:
GLES의 FrameBuffer Fetch/Pixel Local Storage
GLES 자체의 PLS와 FBF 구분에 관해서는, Renderdoc을 사용하여 GLES 실제 기기(Adreno 플랫폼) 패키지의 프레임을 캡처하여 얻은 GLSL 코드에서 다음과 같은 코드 구조를 확인할 수 있습니다(대량의 관련 없는 계산 코드 생략):
Basepass:
#version 320 es#define UE_MRT_FRAMEBUFFER_FETCH 1#ifdef UE_MRT_FRAMEBUFFER_FETCH
//...layout(location= 0)outvec4 out_var_SV_Target0;
layout(location= 1)outvec4 out_var_SV_Target1;
layout(location= 2)outvec4 out_var_SV_Target2;
layout(location= 3)outvec4 out_var_SV_Target3;
#ifndef GL_ARM_shader_framebuffer_fetch_depth_stencil
layout(location= 4)outhighpfloat out_var_SV_Target4;
#endif
//...
int main()
{
//...out_var_SV_Target0=vec4(_335.x, _335.y, _335.z, _157.w);
out_var_SV_Target1= _103;
out_var_SV_Target2= _104;
out_var_SV_Target3= _105;
#ifndef GL_ARM_shader_framebuffer_fetch_depth_stencil
out_var_SV_Target4=gl_FragCoord.z;
#endif
}
#else // UE_MRT_FRAMEBUFFER_FETCH__pixel_local_outEXT _PLSOut// PLS, 因为是第一个pass, 不输入只输出, 所以用out修饰{
layout(rgb10_a2)mediumpvec4 out_var_SV_Target0;
layout(rgba8)mediumpvec4 out_var_SV_Target1;
layout(rgba8)mediumpvec4 out_var_SV_Target2;
layout(rgba8)mediumpvec4 out_var_SV_Target3;
};
#ifndef GL_ARM_shader_framebuffer_fetch_depth_stencil// 是否支持depth的fbflayout(location= 4)outhighpfloat out_var_SV_Target4;
#endif
//...int main()
{
//...out_var_SV_Target0=vec4(_335.x, _335.y, _335.z, _157.w);
out_var_SV_Target1= _103;
out_var_SV_Target2= _104;
out_var_SV_Target3= _105;
#ifndef GL_ARM_shader_framebuffer_fetch_depth_stencil
out_var_SV_Target4=gl_FragCoord.z;
#endif
}
#endif
LightPass:
#version 320 es#define UE_MRT_FRAMEBUFFER_FETCH 1#ifdef UE_MRT_FRAMEBUFFER_FETCH
//...highpvec4 GENERATED_SubpassFetchAttachment1;
highpvec4 GENERATED_SubpassFetchAttachment2;
highpvec4 GENERATED_SubpassFetchAttachment3;
//...layout(location= 0)outvec4 out_var_SV_Target0;
layout(location= 1)inoutvec4 out_var_SV_Target1;//注意被修饰为inout, 既为fbf的语法layout(location= 2)inoutvec4 out_var_SV_Target2;
layout(location= 3)inoutvec4 out_var_SV_Target3;
//...#if!defined(GL_ARM_shader_framebuffer_fetch_depth_stencil)&&defined(GL_EXT_shader_framebuffer_fetch)
layout(location= 4)inouthighpvec4 out_var_SV_Target4;
#endif
float GLFetchDepthBuffer()
{
#ifdefined(GL_ARM_shader_framebuffer_fetch_depth_stencil)
return gl_LastFragDepthARM;
#elifdefined(GL_EXT_shader_framebuffer_fetch)
return out_var_SV_Target4.x;
#elsereturn 0.0f;
#endif
}
void main()
{
GENERATED_SubpassFetchAttachment1= out_var_SV_Target1;
GENERATED_SubpassFetchAttachment2= out_var_SV_Target2;
GENERATED_SubpassFetchAttachment3= out_var_SV_Target3;
highpvec4 _609= GENERATED_SubpassFetchAttachment1;
highpvec4 _611= GENERATED_SubpassFetchAttachment2;
highpvec4 _613= GENERATED_SubpassFetchAttachment3;
highpfloat _621= (int(1u!= 0u)== 0)? 0.0: GLFetchDepthBuffer();
//...out_var_SV_Target0=vec4(_1505.x, _1505.y, _1505.z, _573.w);
}
#else // UE_MRT_FRAMEBUFFER_FETCH__pixel_localEXT _PLSOut// PLS, 既输入又输出{
layout(rgb10_a2)highpvec4 GENERATED_SubpassFetchAttachment0;
layout(rgba8)highpvec4 GENERATED_SubpassFetchAttachment1;
layout(rgba8)highpvec4 GENERATED_SubpassFetchAttachment2;
layout(rgba8)highpvec4 GENERATED_SubpassFetchAttachment3;
};
#if!defined(GL_ARM_shader_framebuffer_fetch_depth_stencil)&&defined(GL_EXT_shader_framebuffer_fetch)
layout(location= 4)inouthighpvec4 out_var_SV_Target4;
#endif
float GLFetchDepthBuffer()
{
#ifdefined(GL_ARM_shader_framebuffer_fetch_depth_stencil)
return gl_LastFragDepthARM;
#elifdefined(GL_EXT_shader_framebuffer_fetch)
return out_var_SV_Target4.x;
#elsereturn 0.0f;
#endif
}
void main()
{
highpvec4 _609= GENERATED_SubpassFetchAttachment1;
highpvec4 _611= GENERATED_SubpassFetchAttachment2;
highpvec4 _613= GENERATED_SubpassFetchAttachment3;
highpfloat _621= (int(1u!= 0u)== 0)? 0.0: GLFetchDepthBuffer();
//...GENERATED_SubpassFetchAttachment0+=vec4(_1505.x, _1505.y, _1505.z, _573.w);
}
#endif
GLSL 코드에서 UE_MRT_FRAMEBUFFER_FETCH에 따라 fbf와 pls를 구분하는 것을 직관적으로 볼 수 있으며, 소스 코드를 살펴보면 이 매크로가 OpenGLShaders.cpp에 정의되어 있음을 확인할 수 있습니다:
이 파일 외에도 OpenGLShaderCompiler.cpp와 GlslBackend.cpp도 모두 GLSL 코드 생성과 관련이 있으니 직접 찾아보시기 바랍니다.
또한, RenderDoc으로 GLES 패키지를 분석할 때, LightPass의 input 항목에서 BasePass가 계산한 gbuffer를 볼 수 없었는데, 이는 아마도 fbf 특성 때문인 것으로 추측됩니다.
Vulkan 실기기 RenderDoc 분석
Vulkan 실기기는 Unity와 유사하게 renderpass/subpass 최적화를 모두 사용하고 있습니다:
이미지에서 비어 있는 subpass1은 데칼(Decal)입니다:
리소스 인스펙터를 통해 렌더패스의 다양한 구체적인 매개변수를 볼 수 있습니다. 예를 들어 여기서 attachment[1], 즉 GBufferA의 loadOp와 storeOp는 각각 Clear와 Don't Care입니다.
쉐이딩 모델 관련
모바일에서 gbuffer를 인코딩/디코딩하는 함수는 DeferredShadingCommon.ush에 정의되어 있으며, 다양한 쉐이딩 모델에 대해 모두 구분되어 있습니다. 반드시 MOBILE_SHADINGMODEL_SUPPORT를 통해 다중 쉐이딩 모델 지원을 활성화해야 합니다(그렇지 않으면 기본 Default Lit만 사용됨):
모바일 지연 렌더링에서의 MOBILE_SHADINGMODEL_SUPPORT는 ENABLE_SHADINGMODEL_SUPPORT_MOBILE_DEFERRED에 의존합니다:
ENABLE_SHADINGMODEL_SUPPORT_MOBILE_DEFERRED는 MobileBasePassRendering.h에 정의되어 있습니다:
관련 함수인 MobileUsesGBufferCustomData()와 MobileUsesExtenedGBuffer()는 모두 RenderUtils.cpp에 정의되어 있습니다:
UE5.1 모바일 지연 렌더링 쉐이딩 모델의 구현은 정적 라이팅을 비활성화한 상태에서 Custom Data를 정적 라이팅 관련 채널의 GBuffer에 기록하는 방식으로 구현되었음을 확인할 수 있습니다.
또한, 셰이더에는 추가적인 네 번째 GBufferD를 정의하기 위한 MOBILE_EXTENDED_GBUFFER가 정의되어 있으며, 이는 RenderUtils.cpp에 정의된 MobileUsesExtenedGBuffer() 함수에 의존합니다:
기본적으로 공식 발표 내용과 일치합니다. GLES는 확실히 GBufferD를 활성화할 수 없고, Vulkan은 특정 하드웨어 지원 여부에 따라 다르며, Metal은 하드웨어가 통합되어 있어 A8 이상만 있으면 괜찮습니다.
하지만 현재 코드에서는 false로 하드코딩되어 있는데, 아마도 미래를 위해 남겨두거나 사용자가 직접 확장할 수 있게 한 것 같습니다. 추후에 소스 코드를 받아서 수정해 테스트해 봐야겠습니다.
TL; DR
UE5.1의 모바일 지연 렌더링 파이프라인은 정적 라이팅을 비활성화한 상태에서 데스크톱의 모든 쉐이딩 모델을 지원하며, Vulkan과 Metal뿐만 아니라 GLES까지 모두 온칩 메모리 대역폭 최적화(FBF/PLS)를 사용하고 있어 Vulkan/Metal만 지원하는 Unity보다 훨씬 우수합니다.
또한 테스트 결과, UE5.1의 지연 렌더링 파이프라인은 Unity2021.3 URP의 지연 파이프라인보다 더 안정적이고 사용하기 쉬우며, 문제점이 적고 호환성도 더 좋습니다.
구체적인 확장성과 실용성은 UE5.1이 정식 출시된 후 소스 코드 버전을 가져와 수정 후 테스트해 봐야 할 것입니다.
원문
https://zhuanlan.zhihu.com/p/575618981