[번역] Advanced Shading Techniques with Pixel Local Storage
역자 주 : 2023년 3월 바이트덴스에서 마지막 근무를 할 때까지 진행 했거나 출시 한 프로젝트에서 자체적으로 개발한 디퍼드 렌더링 및 PLS 를 사용했었습니다.
유니티 엔진 2020의 엔진소스코드를 구매 하고 내부적으로 PRP 라는 파이프라인을 만들었어요.
모바일 게임에 전사적으로 디퍼드 렌더링을 사용하게 되었기 때문에 엔진팀과 많은 것들에 대해서 토론 했었습니다.
중국 미호요는 원신모바일 역시 2022년 중반기에 이미 디퍼드 랜더링으로 모두 전환 했습니다.
ARM 에서 말 하고 있는 PLS 에 대한 기사를 살펴보도록 하죠.
이 블로그 시리즈의 1부에서는 2014년 처음 출시되었을 때와 현재의 관점에서 픽셀 로컬 스토리지(PLS) 확장 기능에 대한 일반적인 개요를 제공했습니다. 2부에서는 PLS를 통해 가능한 고급 셰이딩 기법에 대해 자세히 살펴봅니다.
Translucency
시그라프 2013에서 PLS의 개발이 공개적으로 공유된 순간부터 모바일에서 고급 렌더링 및 포스트 프로세싱 효과를 구현하는 데 있어 그 중요성이 강조되었습니다. 이전에는 반투명과 같은 효과를 구현하려면 여러 번의 패스를 거쳐 메모리를 플러시하고 메인 메모리에서 데이터를 다시 읽어야 했습니다. 대역폭 소비는 모바일에서 복잡한 렌더링 기법을 사용하는 데 있어 실질적인 장애물이었습니다.
PLS를 사용하여 이러한 고급 효과를 구현하는 방법을 살펴 보겠습니다. 반투명이란 완전히 불투명한 것과 완전히 투명한 것의 중간 정도인 빛이 소재를 약간 통과하는 효과를 말합니다. 양초의 왁스나 나뭇잎은 반투명 머티리얼의 일반적인 예입니다. 이러한 머티리얼을 사실적으로 렌더링하려면 표면 산란(SSS)을 고려해야 합니다. 이는 반투명 물체의 표면을 투과한 빛이 머티리얼과 상호 작용하여 다른 지점에서 표면을 빠져나가 산란되는 방식을 설명하는 빛 전달 메커니즘입니다. 예를 들어 빛이 귀를 통과할 때 우리는 모두 이 효과를 쉽게 인식할 수 있습니다.
모바일에서 SSS를 대략적으로 구현하면 빛이 머티리얼을 통과할 때 감쇠되는 방식을 고려합니다. 감쇠는 여러 가지 요인에 의해 영향을 받을 수 있습니다. 여기에는 물체의 다양한 두께, 보기 방향 및 빛의 속성이 포함됩니다. 실제로는 빛이 음영 처리할 지점에 도달하기 전에 빛이 물체 내부에서 얼마나 멀리 이동하는지를 결정해야 합니다. 공정한 근사치는 빛이 실제 이동한 거리(s) 대신 카메라에서 본 물체의 두께(t)를 계산하는 것입니다.
이 접근 방식에 따라 개체의 최대 뷰 공간 깊이와 최소 뷰 공간 깊이의 차이로 두께를 계산합니다. 이 작업은 두 번에 걸쳐 수행됩니다.
첫 번째 패스는 가장 가까운 오브젝트와 최소 뷰 스페이스 깊이를 결정합니다. 불투명 개체와 반투명 개체는 스텐실 버퍼에 각각 0과 >= 1의 ID 값을 기록하여 구분됩니다. 아래 PLS 블록이 할당됩니다. 구조의 총 크기는 128비트입니다.
__pixel_localEXT FragDataLocal {
layout(rgb10_a2) vec4 lighting; // RGBA
layout(rg16f) vec2 minMaxDepth; // View-space depths
layout(rgb10_a2) vec4 albedo; // RGB and sign(normal.z)
layout(rg16f) vec2 normalXY; // View-space normal components
} storage;
이 첫 번째 패스에서 프레그먼트 셰이더는 머티리얼 프로퍼티를 쓰고 최소 및 최대 깊이를 모두 들어오는 깊이로 설정합니다. 조명 변수는 두 번째 패스에서 조명을 누적하는 데 사용되므로 지워집니다.
uniform vec3 albedo;
in vec4 vClipPos;
in vec4 vPosition;
in vec3 vNormal;
void main()
{
vec3 n = normalize(vNormal);
storage.lighting = vec4(0.0);
storage.minMaxDepth = vec2(-vPosition.z, -vPosition.z);
storage.albedo.rgb = albedo;
storage.albedo.a = sign(n.z);
storage.normalXY = n.xy;
}
두 번째 패스에서는 동일한 PLS 블록이 사용되며 셰이더는 이전에 결정된 가장 가까운 오브젝트의 최대 깊이를 찾습니다. 이번에는 씬(그림 3 참조)이 깊이 테스트 없이 렌더링되지만 스텐실 테스트가 각 오브젝트의 ID와 동일하게 설정됩니다. 가장 가까운 오브젝트의 ID가 스텐실 버퍼에 저장되므로 동일한 오브젝트만 통과하는 프레그먼트를 갖게 됩니다.
void main()
{
float depth = -vPosition.z;
storage.minMaxDepth.y = max(depth, storage.minMaxDepth.y);
}
아래 왼쪽 그림은 반투명 객체의 결과 두께를 렌더링한 것입니다.
머티리얼 속성과 오브젝트 뷰 공간 두께가 계산되어 PLS 블록에 저장되면 모든 반투명 지오메트리에 대한 마지막 셰이딩 패스가 수행됩니다(중앙 그림). 이 패스의 기본 개념은 두께가 빛 투과율을 감쇠시킨다는 것입니다. 두께가 클수록 투과되는 빛의 강도가 작아집니다. 매우 얇은 물체의 경우 큰 강도를 볼 수 있습니다. 이 패스에 대한 자세한 설명과 소스 코드는Android용 Arm OpenGL ES SDK 에서 확인할 수 있습니다. 반투명 예제에서는 두 개의 조명이 서로 반대 방향으로 움직이며 둘 다 큐브를 통과합니다. 일부 매개변수를 변경하여 반투명 예제를 가지고 놀면서 결과 효과를 확인할 수 있습니다(예: 오른쪽 그림).
Order Independent Transparency (OIT)
일반적으로 약간의 투명도가 있는 지오메트리는 알파 합성을 사용하여 렌더링됩니다. 각 반투명 지오메트리는 앞의 지오메트리를 가리고 알파 값에 따라 자체 색상을 추가합니다. 지오메트리가 블렌딩되는 순서는 관련이 있습니다. 예를 들어,Arm Mali GPU 모범 사례 가이드에서는 먼저 모든 불투명 메시를 앞뒤 렌더링 순서로 렌더링한 다음 블렌딩이 올바르게 작동하도록 모든 투명 메시를 불투명 지오메트리 위에 뒤에서 앞 순서로 렌더링할 것을 권장합니다 .
반투명 지오메트리의 수와 복잡성에 따라 주문하는 데 상당한 시간이 걸리고 항상 올바른 결과가 나오지 않을 수 있습니다. 주문 독립 투명도(IOT)로 알려진 대체 접근 방식이 구현되었습니다. OIT는 래스터화 후 지오메트리를 픽셀 단위로 정렬합니다. 최종 색상을 정확하게 계산하는 정확한 솔루션은 모든 프레그먼트를 정렬해야 하므로 복잡한 장면에서는 병목 현상이 발생할 수 있습니다. 보다 모바일 친화적인 근사 솔루션은 품질, 성능 및 메모리 대역폭 간의 균형을 제공합니다. 이 중멀티 레이어 알파 블렌딩 ( MLAB), 적응형 투명도 및 뎁스 필링을 언급할 수 있습니다.
MLAB은 단일 렌더링 패스와 제한된 메모리에서 작동하는 실시간 근사 OIT 솔루션입니다. 즉, PLS에 완벽하게 적합합니다. 투과율과 깊이, 누적된 색상을 고정된 수의 레이어에 저장하는 방식으로 작동합니다. 새 프레그먼트는 가능한 경우 순서대로 삽입되고 필요한 경우 병합됩니다. 이렇게 하면 알파 블렌딩의 엄격한 순서 요구 사항을 다소 완화할 수 있습니다. 가장 간단한 두 개의 레이어의 경우, 블렌딩된 레이어를 PLS 구조에 누적하여 마지막에 단일 출력 색상으로 해결할 수 있습니다. 시그라프 2014의 프레젠테이션에서는 OIT에 대한 다양한 접근 방식을 비교하고 MLAB의 PLS 구현에서 색상과 알파에는 RGBA8을, 깊이에는 32비트 플로트를 사용할 것을 권장하고 있습니다.
Frag0 Colour | Frag1 Depth | Frag1 Colour | Frag1 Depth |
RGBA8 | R32F | RGBA8 | R32F |
표 1. 멀티 레이어 알파 블렌딩(2개 레이어) 구현을 위한 PLS 구조.
깊이 값은 첫 번째 프레그먼트 0이 두 번째 프레그먼트 1보다 뒤에 있는지 또는 앞에 있는지를 결정하는 데 사용됩니다. 그런 다음 그에 따라 오버/언더 연산자가 적용됩니다. 아래의 의사 코드는 셰이더에서 구현할 연산을 보여줍니다. 그림 8은 두 개의 레이어에 대해 여기에 설명된 MLAB 접근 방식을 사용한 결과 합성 이미지와 참조 이미지를 보여줍니다. 이 방법은 가장 간단한 MLAB 접근 방식이며 일반적인 알파 블렌딩보다 개선되었지만 왼쪽 아래 영역에서 명백한 인공물이 관찰됩니다.
If(frag0Depth < frag1Depth)
{
//Fragment 0 covers fragment 1
accumColorRGB = frag0ColorRGB + frag1ColorRGB * (1 – frag0Alpha)
AccumAlpha = frag0Alpha + frag1Alpha *(1 - frag0Alpha)
}
else
{
//Fragment 1 covers fragment 0
accumColorRGB = frag1ColorRGB + frag0ColorRGB * (1 – frag1Alpha)
AccumAlpha = frag1Alpha + frag0Alpha *(1 – frag1Alpha)
}
Shader Frame Buffer Fetch (FBF)
이 블로그를 마무리하기 전에 개발자에게 Mali 고속 온칩 메모리를 노출하여 대역폭을 크게 절약할 수 있는 또 다른 두 가지 확장 기능을 강조하고 싶습니다. 첫 번째 확장 기능인 Arm 프레임 버퍼 페치는 이전 프레그먼트 색상에 대한 프레그먼트 셰이더 읽기 액세스를 제공합니다. 이를 통해 프래그먼트 셰이더가 기존 프레임버퍼 데이터를 입력으로 읽을 수 있으며, 프로그래밍 가능한 블렌딩 및 고정 함수 블렌딩으로는 구현할 수 없는 기타 작업과 같은 사용 사례를 구현하는 데 적합합니다. 이 확장 기능은 OpenGL ES 2.0 이상에서 지원되며 단일 프레임버퍼 읽기백을 제공하지만 EXT_shader_framebuffer_fetch보다 더 효율적인 대략적인 MSAA 경로를 사용합니다.
이 확장 기능은 새로 내장된 gl_LastFragColorARM(mediump vec4)을 통해 작동하므로 사용법은 간단합니다. 아래 예시는 프로그래밍 가능한 블렌딩을 보여줍니다.
#extension GL_ARM_shader_framebuffer_fetch : enable
precision mediump float;
uniform vec4 uBlend0;
uniform vec4 uBlend1;
void main(void)
{
vec4 color = gl_LastFragColorARM;
color = lerp(color, uBlend0, color.w * uBlend0.w);
color *= uBlend1;
gl_FragColor = color;
}
두 번째 확장은 프레임 버퍼에서 현재 깊이와 스텐실 값을 읽을 수 있는프레임 버퍼 깊이 및 스텐실 가져오기(Arm Frame Buffer Fetch Depth and Stencil) 입니다. 사용법은 스텐실 버퍼에 액세스하기 위한 lowp int gl_LastFragStencilARM과 깊이 버퍼에 액세스하기 위한 gl_LastFragDepthARM이라는 두 개의 새로운 읽기 전용 내장 함수를 통해 간단합니다. 깊이의 경우 정밀도는 GL_FRAGMENT_PRECISION_HIGH가 정의되었는지 여부(highp float)에 따라 달라집니다(mediump float). 이 변수는 각각 현재 프레그먼트이 대상인 픽셀의 현재 깊이 및 스텐실 버퍼 값을 포함합니다. 이 확장 기능을 사용하면 프로그래밍 가능한 깊이 및 스텐실 테스트, 섀도 변조, 소프트 파티클 및 단일 렌더링 패스에서 분산 섀도 맵 생성 기능과 같은 사용 사례를 구현할 수 있습니다. 또한 이 확장 기능은 뎁스와 카메라 매트릭스를 사용하여 화면의 모든 픽셀의 3D 위치를 재구성하는 매우 편리한 방법을 애플리케이션에 제공합니다. 아래 예시[1 ] 는 소프트 파티클을 렌더링하기 위해 뎁스 리드백을 사용하는 방법을 보여줍니다. 일반적으로는 배경 지오메트리의 뎁스 값을 뎁스 텍스처에 쓰는 렌더 패스와 블렌딩을 위해 뎁스 텍스처를 읽으면서 파티클을 렌더링하는 두 번의 렌더 패스가 필요합니다. 이 확장 기능을 사용하면 이 모든 작업을 한 번의 패스로 수행할 수 있습니다.
#extension GL_ARM_shader_framebuffer_fetch_depth_stencil : enable
precision mediump float;
uniform float uTwoXNear; // 2.0 * near
uniform float uFarPlusNear; // far + near
uniform float uFarMinusNear; // far - near
uniform float uInvParticleSize;
uniform sampler2D uParticleTexture;
varying float vLinearDepth;
varying vec2 vTexCoord;
void main(void)
{
vec4 ParticleColor = texture2D(uParticleTexture, vTexCoord);
//Convert from exponential depth to linear
float LinearDepth = uTwoXNear / (uFarPlusNear - gl_LastFragDepthARM *
uFarMinusNear);
//Compute blend weight by substracting current fragment depth with
//depth buffer value
float Weight = clamp (( LinearDepth – vLinearDepth) * uInvParticleSize,
0.0, 1.0);
// Modulate with particle alpha
ParticleColor.w *= Weight;
gl_FragColor = ParticleColor;
}
확장 사양에 따르면 gl_LastFragDepthARM 및 gl_LastFragStencilARM에서 읽으려면 현재 픽셀을 대상으로 하는 모든 이전 프레그먼트의 처리가 완료될 때까지 기다려야 한다고 명시되어 있습니다. 따라서 최상의 성능을 얻으려면 이러한 내장 변수 중 하나에서 읽기를 프래그먼트 셰이더 실행의 가능한 한 늦게 수행하는 것이 좋습니다. 멀티 샘플링이 활성화된 경우와 같은 다른 특정 권장 사항은 확장 기능 설명서를 참조하세요.
주의해야 할 사항
지금까지 살펴본 바와 같이 PLS 및 FBF 확장은 OpenGL ES 개발자에게 여러 가지 이점을 제공합니다. 그럼에도 불구하고 이러한 확장 기능을 사용할 계획이라면 몇 가지 주의해야 할 사항이 있습니다. 첫 번째는 모든 공급업체가 PLS를 지원하는 것은 아니라는 점입니다. 실제로 이 확장 기능은 현재 Arm과 Imagination에서만 지원되므로 Arm Mali 및 Imagination PowerVR GPU에서만 사용할 수 있습니다.
역자 주 : 이 글이 쓰여진 시점을 기준으로 본다면 2025년 현 시점에서는 거의 모든 디바이스가 지원하거나 유사한 기능을 사용하여 지원 하고 있습니다. ARM 은 PLS, 퀄컴에서는 FrameBufferFetch 라는 이름의 API 입니다. 아드레노 700 이상부터는 FBF 도 성능개선이 많이 되었습니다.
솔직히 삼성 엑시노스는 사업 자체를 접는다는 소리가 있으니 별외로 두도록 합니다. 2025년 기준으로 삼성의 엑시노스는 의미가 없다고 봅니다. 또한 2025년 기준 메인스트림 모바일 폰 역시 2019년 이후 생산된 기종이거나 2022년 기종이라고 할 수 있습니다.
이처럼 공급업체에 따라 부분적으로 지원되기 때문에 단일 코드 기반으로 모든 Android를 타겟팅하려는 경우 PLS를 채택하기가 더 어려워집니다. 그래픽스 커뮤니티가 모든 벤더가 지원할 수 있는 새로운 API를 찾기 시작한 이유 중 하나가 바로 이러한 문제 때문이었으며, 그 결과 Vulkan이 탄생했습니다. 벌칸의 렌더 패스는 비록 더 제한적이고 암시적인 형태이긴 하지만 PLS와 유사한 의미를 크로스 플랫폼 방식으로 표현할 수 있는 기회를 제공했습니다. Vulkan에서는 렌더 패스에 서브패스 로드 기능을 사용하여 패스 간에 픽셀당 데이터를 전달하는 여러 개의 서브패스가 포함된 경우 GPU는 이 데이터를 온칩 스토리지에 보관하고 메모리로의 저장/로드 왕복 작업을 피하기에 충분한 정보를 가지고 있습니다. 이를 '서브패스 퓨전'이라고 하며 모든 Mali Vulkan 드라이버는 이를 지원합니다. 그러나 GPU가 DRAM을 사용하는지 온칩 스토리지를 사용하는지는 애플리케이션에서 명시적으로 제어하지 않으므로 이 동작은 공급업체마다 다릅니다. 그럼에도 불구하고 현재로서는 Vulkan에서는 OpenGL ES 확장 프로그램에서처럼 타일 스토리지에 직접 액세스할 수 없습니다.
또한 OpenGL ES와 벌칸 모두에서 온칩 메모리를 사용하는 것은 각 픽셀이 해당 입력 픽셀의 정보만 사용해야 한다는 전제를 기반으로 합니다. 두 기술 모두 개발자가 임의의 픽셀 위치에서 샘플링하는 것을 허용하지 않습니다. 이는 모바일에서 고도로 최적화된 새로운 효과를 구현할 수 있는 문을 열어줄 것입니다.
마지막으로, 모든 그래픽 API는 기본 하드웨어를 최대한 활용하도록 설계되었지만 고성능 저전력 모바일 앱 구현을 보장하지는 않습니다. 프로파일링과 테스트를 대신할 수 있는 것은 없습니다. 인내심을 가지고 프로파일링 작업을 수행해야만 GPU가 멈추는지 또는 명령에 목이 마르는지, 따라서 타일 메모리 확장을 통해 얻은 이점을 취소할 수 있는지 알 수 있습니다. 이를 통해 궁극적으로 병목 지점을 파악하고 런타임 리소스를 최적으로 사용하기 위한 결정을 내릴 수 있습니다. Arm Mali GPU의 경우, Arm 모바일 스튜디오는 다음과 같은 여러 프로파일링 및 모니터링 툴에 대한 액세스를 제공합니다 ( ).Streamline 성능 분석기, 성능 카운터, 성능 어드바이저 및 Mali 오프라인 컴파일러. 이러한 리소스는Mali GPU 모범 사례를 따르는 것과 함께 그래픽 개발자가 Arm Mali GPU를 최대한 활용하는 데 사용할 수 있는 가장 강력한 툴입니다 .
Conclusions
이 시점에서 OpenGL ES 개발자 독자가 모바일에서 효율적인 그래픽을 구현하기 위해 PLS 및 FBF 확장을 사용하는 방법에 대한 아이디어를 얻을 수 있기를 바랍니다. 하지만 두 블로그에서 설명한 다양한 예제를 살펴볼 때 PLS의 주요 개념을 이해하는 것이 중요합니다. PLS는 그래픽 분야에서 처음으로 온칩 스토리지를 개발자에게 노출시켰고, 이를 통해 영구 메모리를 사용하여 셰이딩 파이프라인을 구축할 수 있는 가능성을 보여주었습니다. 대역폭의 획기적인 감소 덕분에 모바일에서는 불가능했던 수많은 사용 사례가 갑자기 가능해졌습니다. 모바일 플랫폼에서 전력은 귀중한 자원입니다. 온칩 메모리에 데이터를 저장하고 읽을 수 있게 되면서 메인 메모리로 데이터를 주고받는 데 사용되는 전력이 줄어들었습니다. 즉, 배터리가 더 오래 지속됩니다. 마지막으로, 데이터를 온칩 메모리에 보관함으로써 데이터 트래픽에 사용되는 런타임 리소스를 대폭 줄여 성능을 향상시킵니다. OpenGL ES를 사용하여 다음 애플리케이션을 계획할 때 PLS를 사용하면 이러한 모든 이점을 무료로 누릴 수 있다는 점을 명심하세요.
Annex
아래 표는 블로그에 설명된 확장 기능을 언제 사용해야 하는지에 대한 가이드를 개발자에게 제공하기 위한 것입니다. 확장 기능에서 다루는 사용 사례를 요약하고 유용한 기능을 강조합니다.
Extension | OpenGL ES version | Use when | Relevant feature |
EXT_shader_pixel_local_storage | 3.0 or higher | Your application can pass information between fragment shaders invocations covering the same pixel. Ex. Deferred rendering, translucency, OIT. | The data in PLS is not written back to main memory. Massive bandwidth saving. |
ARM_shader_framebuffer_fetch | 2.0 or higher | You have a programmable blending like use case. Fixed- function blending doesn’t support the operation you need. | Enables fragment shaders to read existing framebuffer colour data as input at pixel or fragment location. No bandwidth cost associated since the data is in the tile buffer. |
ARM_shader_framebuffer_fetch_depth_stencil | 2.0 or higher | Your app needs to read existing framebuffer depth and stencil of the current pixel being processed in raster order access. | Enables fragment shaders to read existing framebuffer depth/stencil data as input at pixel or fragment location. No bandwidth cost associated since the data is in the tile buffer. |
References
[1] - 대역폭 효율적인 그래픽을 위한 Arm Mali GPU, Marius Bjorge, GPU Pro 5, 275페이지.
https://ubm-twvideo01.s3.amazonaws.com/o1/vault/GDC2014/Presentations/Martin_Sam_The_Revolution_in.pdf
엮어서 읽어보기.
테크니컬 아티스트 이상윤님 글.