이전글 : Forward Rendering
원문 : https://www.3dgep.com/forward-plus/#deferred-shading
Forward+ Rendering
Comparing Forward, Deferred, and Forward+ rendering algorithms using DirectX 11.
www.3dgep.com
Deferred Shading
디퍼드 셰이딩 기술은 세 가지 패스로 구성된다.
- G-buffer pass
- Lighting pass
- Transparent pass
g-버퍼 패스는 서론에서 설명한 g-버퍼 텍스처를 채운다. 조명 패스는 각 광원을 기하학적 객체로 렌더링하고 가려진 픽셀에 대한 조명을 계산한다. 투명 패스는 표준 포워드 렌더링 기법을 사용하여 투명한 장면 객체를 렌더링한다.
G-Buffer Pass
디퍼드 셰이딩 기법의 첫 번째 단계에서는 G-버퍼 텍스처가 생성된다. 먼저 G-버퍼의 레이아웃에 대해 설명한다.
G-Buffer Layout
G-버퍼의 레이아웃은 이 웹사이트의 전체 글에서 다룰 수 있는 주제가 될 수 있다. 이 데모를 위해 제가 선택한 레이아웃은 단순성과 필요성을 기반으로 한다. 일부 데이터는 더 작은 버퍼에 더 잘 패킹될 수 있기 때문에 이 레이아웃은 가장 효율적인 G-버퍼 레이아웃은 아니다. G-버퍼에 속성을 패킹하는 것에 대한 논의가 있었지만, 다양한 패킹 방법을 사용할 때의 효과에 대한 분석은 수행하지 않았다.
G-버퍼에 저장해야 하는 속성은 다음과 같다.
- Depth/Stencil
- Light Accumulation
- Diffuse
- Specular
- Normals
Depth/Stencil Buffer
깊이/스텐실 텍스처는 픽셀당 32비트로 저장되며, 깊이 값은 부호 없는 정규화 값(UNORM)으로 24비트, 스텐실 값은 부호 없는 정수(UINT)로 8비트를 갖는다. 깊이 버퍼의 텍스처 리소스는 R24G8_TYPELESS 텍스처 형식을 사용하여 생성되고, 깊이/스텐실 뷰는 D24_UNORM_S8_UINT 텍스처 형식을 사용하여 생성된다. 픽셀 셰이더에서 깊이 버퍼에 접근할 때, 스텐실 값은 사용되지 않으므로 셰이더 리소스 뷰는 R24_UNORM_X8_TYPELESS 텍스처 형식을 사용하여 생성된다.
깊이/스텐실 버퍼는 출력 병합 단계에 연결되며 G 버퍼 픽셀 셰이더에서 직접 계산되지 않는다. 버텍스 셰이더의 결과는 깊이/스텐실 버퍼에 직접 기록된다.
Light Accumulation Buffer
조명 누적 버퍼는 조명 패스의 최종 결과를 저장하는 데 사용된다. 이는 화면의 백 버퍼와 동일한 버퍼로, G-버퍼 텍스처가 화면과 동일한 크기인 경우, 조명 누적 버퍼에 추가 버퍼를 할당할 필요가 없으며 화면의 백 버퍼를 바로 사용할 수 있다.
조명 누적 버퍼는 텍스처 리소스와 셰이더 리소스 뷰 모두에 R8G8B8A8_UNORM 텍스처 형식을 사용하는 32비트 4-컴포넌트 부호 없는 정규화 텍스처로 저장된다.
G-버퍼 패스 이후, 빛 축적 버퍼는 처음에는 조명 방정식의 앰비언트와 에미시브 항만 저장한다. 이 이미지는 더 잘 보이도록 상당히 밝아졌다.
장면에서 완전히 불투명한 객체만 렌더링되는 것을 확인할 수 있다. 디퍼드 셰이딩은 투명한 객체를 지원하지 않으므로 G-버퍼 패스에서는 불투명한 객체만 렌더링된다.
최적화를 위해 G-버퍼 패스에서 디렉셔널 라이트를 축적하고 조명 패스에서는 디렉셔널 라이트를 건너뛸 수도 있다. 조명 패스에서 디렉셔널 라이트는 전체 화면 쿼드로 렌더링되므로, 필레이트가 문제가 될 경우 G-버퍼 패스에서 디렉셔널 라이트를 축적하면 셰이더 사이클을 절약할 수 있다. 이 실험에서는 이 최적화를 활용하지 않는다. 디렉셔널 라이트를 별도의 버퍼에 저장해야 하는데, 이는 포워드 및 포워드+ 픽셀 셰이더가 조명을 처리하는 방식과 일치하지 않기 때문이다.
Diffuse Buffer
디퓨즈 버퍼는 32비트 4-컴포넌트 부호 없는 정규화(UNORM) 텍스처로 저장된다. 디퍼드 셰이딩에서는 불투명 객체만 렌더링되므로 이 버퍼의 알파 채널은 필요하지 않으며, 이 실험에서는 사용되지 않는다. 텍스처 리소스와 셰이더 리소스 뷰는 모두 R8G8B8A8_UNORM 텍스처 형식을 사용한다.
위 이미지는 G-버퍼 패스 후의 디퓨즈 버퍼 결과를 보여준다.
Specular Buffer
빛 축적 및 확산 버퍼와 유사하게, 스페큘러 색상 버퍼는 R8G8B8A8_UNORM 형식을 사용하는 32비트 4-구성요소 부호 없는 정규화된 텍스처로 저장된다. 빨간색, 녹색 및 파란색 채널은 반사 색상을 저장하는 데 사용되고 알파 채널은 반사강도(specular Power)를 저장하는 데 사용된다. 반사강도 값은 일반적으로 (1…256](또는 그 이상) 범위로 표현되지만 텍스처에 저장하려면 [0…1] 범위에 패킹해야 한다. 반사강도를 텍스처에 패킹하기 위해 Michiel van der Leeuw가 발표한 "Killzone 2의 지연 렌더링"이라는 프레젠테이션에 설명된 방법을 사용한다. 해당 프레젠테이션에서 그는 다음 방정식을 사용하여 반사 강도값을 패킹한다. 1
이 함수는 [1~1448.15] 범위의 스페큘러 강도 값을 패킹할 수 있도록 하며, 일반 스페큘러 범위(1~256)의 값에 대해 우수한 정밀도를 제공한다. 아래 그래프는 패킹된 반사광 값의 진행 과정을 보여준다.
그리고 G-버퍼 패스 이후의 스페큘러 버퍼의 결과는 다음과 같다.
Normal Buffer
뷰 공간 노멀은 R32G32B32A32_FLOAT 텍스처 형식을 사용하는 128비트 4-컴포넌트 부동 소수점 버퍼에 저장된다. 이 크기의 노멀 버퍼는 실제로 필요하지 않으며, 노멀의 X 및 Y 컴포넌트를 32비트 2-컴포넌트 반정밀도 부동 소수점 버퍼에 패킹하고 조명 패스에서 Z 컴포넌트를 다시 계산할 수도 있다. 이 실험에서는 효율성보다 정밀도와 단순성을 중시했으며, GPU는 텍스처 메모리의 제약을 받지 않으므로 가장 높은 정밀도를 가진 가장 큰 버퍼를 사용했다.
노멀 버퍼에 다른 텍스처 형식을 적용하고 품질과 성능 간의 상충 관계를 분석하는 것이 가치 있을 것이다. 가설은 노멀 버퍼에 더 작은 텍스처 형식(예: R16G16_FLOAT)을 사용하면 비슷한 품질의 결과를 얻을 수 있으면서 성능도 향상될 것이라는 것이다.
위 이미지는 G-버퍼 패스 후 노멀 버퍼의 결과를 보여준다.
Layout Summary
전체 G-버퍼 레이아웃은 아래 표와 비슷하다.
R | G | B | A | |
Depth/Stencil | D24_UNORM | S8_UINT | ||
Light Accumulation | R8_UNORM | G8_UNORM | B8_UNORM | A8_UNORM |
Diffuse | R8_UNORM | G8_UNORM | B8_UNORM | A8_UNORM |
Specular | R8_UNORM | G8_UNORM | B8_UNORM | A8_UNORM |
Normal | R32_FLOAT | G32_FLOAT | B32_FLOAT | A32_FLOAT |
Layout of the G-buffer.
Pixel Shader
G-버퍼 패스의 픽셀 셰이더는 포워드 렌더러의 픽셀 셰이더와 매우 유사하다. 주요 차이점은 G-버퍼 패스에서는 조명 계산이 수행되지 않는다는 것. 머티리얼 속성을 수집하는 방식은 포워드 렌더링 기법과 동일하므로 셰이더 코드의 해당 부분은 여기서 다시 설명하지 않는다.
G-버퍼 데이터를 텍스처로 출력하기 위해 각 G-버퍼 텍스처는 PixelShaderOutput 구조체를 사용하여 렌더 타겟 출력에 바인딩된다.
DeferredRendering.hlsl |
struct PixelShaderOutput { float4 LightAccumulation : SV_Target0; float4 Diffuse : SV_Target1; float4 Specular : SV_Target2; float4 NormalVS : SV_Target3; }; |
깊이/스텐실 버퍼가 출력 병합 단계에 바인딩되어 있으므로 픽셀 셰이더에서 깊이 값을 출력할 필요가 없다.
이제 픽셀 셰이더에서 G 버퍼 텍스처를 채우게 된다.
DeferredRendering.hlsl |
[earlydepthstencil] PixelShaderOutput PS_Geometry( VertexShaderOutput IN ) { PixelShaderOutput OUT; // Get emissive, ambient, diffuse, specular and normal values // In the same way as the forward rendering pixel shader. // The source code is not shown here for the sake of brevity. OUT.LightAccumulation = ( ambient + emissive ); OUT.Diffuse = diffuse; OUT.Specular = float4( specular.rgb, log2( specularPower ) / 10.5f ); OUT.NormalVS = N; return OUT; } |
모든 머티리얼 속성을 가져와서 해당 속성을 적절한 렌더 타겟에 저장하기만 하면 된다. 모든 머티리얼 속성을 읽는 소스 코드는 간결성을 위해 생략. 이 글의 끝부분에서 소스 코드를 다운로드하여 전체 픽셀 셰이더를 확인할 수 있다.
Lighting Pass (Guerrilla)
이 실험에서 사용하는 디퍼드 셰이딩 기법의 라이팅 패스에 대한 주요 영감의 원천은 2007년 8월 캘리포니아 팔로알토에서 열린 소니 컴퓨터 엔터테인먼트 그래픽 세미나에서 Michiel van der Leeuw가 발표한 "Killzone 2의 디퍼드 렌더링"이라는 발표에서 비롯되었다. 2 Michiel의 발표에서 그는 라이팅 패스를 네 단계로 설명한다.
- Clear stencil buffer to 0,
- Mark pixels in front of the far light boundary,
- Count number of lit pixels inside the light volume,
- Shade the lit pixels
마지막 세 단계를 간략하게 설명하면, 디퍼드 셰이딩 기법의 라이팅 패스를 구현하기 위해 선택한 방법을 제시하고, Michiel의 프레젠테이션에서 설명한 것과 다른 방법을 선택한 이유를 설명하도록 한다.
Determine Lit Pixels
Michiel의 발표에 따르면, 어떤 픽셀이 조명되는지 확인하려면 먼저 조명 볼륨의 뒷면을 렌더링하고 먼 빛 경계선 앞에 있는 픽셀을 표시해야 한다. 그런 다음 조명 볼륨의 앞면 뒤에 있는 픽셀의 개수를 세고, 마지막으로 조명 볼륨의 앞면 뒤에 표시되어 있는 픽셀을 음영 처리하게 된다.
Mark Pixels
첫 번째 단계에서는 조명 볼륨의 뒷면 앞에 있는 픽셀이 스텐실 버퍼에 표시된다. 이를 위해 먼저 스텐실 버퍼를 0으로 비운 후 다음 설정을 사용해 파이프라인 상태를 구성하게된다.
- 버텍스 셰이더만 바인딩한다(픽셀 셰이더는 필요하지 않음)
- 깊이/스텐실 버퍼만 출력 병합 단계에 바인딩(픽셀 셰이더가 바인딩되지 않으므로 색상 버퍼가 필요하지 않음)
- 래스터라이저 상태 :
- Cull mode를 FRONT로 설정하여 조명 볼륨의 뒷면만 렌더링.
- Depth/Stencil 상태 :
- Enable depth testing
- Disable depth writes
- Set the depth function to GREATER_EQUAL
- Enable stencil operations
- Set stencil reference to 1
- Set stencil function to ALWAYS
- Set stencil operation to REPLACE on depth pass.
그리고 빛의 볼륨을 렌더링한다. 아래 이미지는 이 작업의 효과를 보여준다.
라이트 볼륨의 점선은 컬링되고 뒷면 폴리곤만 렌더링된다. 녹색 볼륨은 스텐실 참조 값으로 스텐실 버퍼가 표시될 위치를 나타낸다. 다음 단계는 라이트 볼륨 내부의 픽셀 수를 카운팅 하게 된다.
Count Pixels
다음 단계는 이전 단계에서 표시되는 라이트 볼륨 내부에 있는 픽셀의 수를 세는 것이다. 이는 라이트 볼륨의 앞면을 렌더링하고, 이전 단계에서 스텐실 표시되고 라이트 볼륨 앞면 뒤에 있는 픽셀의 수를 세는 방식으로 수행된다. 이 경우, 파이프라인 상태는 다음과 같이 구성되어야 한다.
- 버텍스 셰이더만 바인딩(픽셀 셰이더는 필요하지 않음).
- 깊이/스텐실 버퍼만 출력 병합 단계에 바인딩(픽셀 셰이더가 바인딩되지 않으므로 컬러 버퍼가 필요하지 않음)
- 래스터라이저 상태 구성(Configure the Rasterizer State) :
- Cull mode를 BACK으로 설정하여 조명 볼륨의 앞면만 렌더링.
- Depth/Stencil State :
- Enable depth testing
- Disable depth writes
- Set the depth function to LESS_EQUAL
- Enable stencil operations
- Set stencil reference to 1
- Set stencil operations to KEEP (stencil buffer를 수정하지 않음)
- Set stencil function to EQUAL
그리고 오클루전 픽셀 쿼리를 사용하여 조명 볼륨을 다시 렌더링하여 깊이 연산과 스텐실 연산을 모두 통과하는 픽셀 수를 계산한다. 아래 이미지는 이 연산의 효과를 보여준다.
이미지의 빨간색 볼륨은 이 단계에서 계산되는 픽셀을 나타낸다.
래스터화된 픽셀 수가 특정 임계값보다 적으면 셰이딩 단계를 건너뛸 수 있다. 래스터화된 픽셀 수가 특정 임계값보다 많으면 픽셀을 셰이딩하게 된다.
Michiel의 발표에서는 설명했지만 이 실험에서는 생략된 단계 중 하나는 빛 그림자 맵을 생성하는 것이다. 픽셀 쿼리의 주요 목적은 그림자 맵 생성을 건너뛰는 것이다. 이 실험에서는 그림자 매핑을 사용하지 않으므로, 구현에서는 이 단계를 완전히 건너뛴다(나중에 설명). |
Shade Pixels
Michiel의 방법에 따르면 마지막 단계는 조명 볼륨 내부에 있는 픽셀을 셰이딩하는 것이다. 이를 위해 파이프라인 상태 구성은 픽셀 카운트 단계의 파이프라인 구성과 동일해야 하며, additive blending 활성화, 픽셀 셰이더 바인딩, 그리고 출력 병합 단계에 컬러 버퍼 연결 등의 작업이 추가로 필요하다.
- 버텍스 셰이더와 픽셀 셰이더를 모두 바인딩.
- 깊이/스텐실 및 조명 누적 버퍼를 출력 병합 단계에 바인딩.
- 래스터라이저 상태 구성(Configure the Rasterizer State) :
- Cull mode를 BACK으로 설정하여 조명 볼륨의 앞면만 렌더링.
- Depth/Stencil State:
- Enable depth testing
- Disable depth writes
- Set the depth function to LESS_EQUAL
- Enable stencil operations
- Set stencil reference to 1
- Set stencil operations to KEEP (don’t modify the stencil buffer)
- Set stencil function to EQUAL
- Blend State:
- Enable blend operations
- Set source factor to ONE
- Set destination factor to ONE
- Set blend operation to ADD
결과적으로 빛의 볼륨 내에 포함된 픽셀만 음영 처리된다.
Lighting Pass (My Implementation)
Michiel의 프레젠테이션에서 설명된 조명 패스의 문제점은 CPU가 GPU 쿼리 결과가 반환될 때까지 기다려야 하는 동안 픽셀 쿼리 작업이 지연을 일으킬 가능성이 매우 높다는 것이다. 시간적 일관성 이론에 따라 현재 프레임의 쿼리 결과 대신 이전 프레임(또는 이전 2개 프레임)의 쿼리 결과를 사용하면 이러한 지연을 방지할 수 있다. 3 이 경우 쿼리 객체가 여러 프레임에 걸쳐 지속되어야 하는 경우 재사용할 수 없으므로 각 광원에 대해 여러 개의 쿼리 객체를 생성해야 한다.
이 구현에서는 그림자 매핑을 하지 않기 때문에 Michiel의 프레젠테이션에 설명된 픽셀 오클루전 쿼리를 수행할 필요성이 전혀 없었고, 따라서 쿼리 작업에서 발생할 수 있는 잠재적인 지연을 피할 수 있었다.
Michiel의 프레젠테이션에서 설명한 방법의 또 다른 문제는 눈이 빛의 영역 안에 있는 경우 카운트 픽셀과 셰이딩 픽셀 단계에서 픽셀이 계산되거나 셰이딩되지 않는다는 것이다.
이미지에 표시된 녹색 볼륨은 첫 번째 단계에서 표시된 스텐실 버퍼의 픽셀을 나타낸다. 라이트 볼륨의 앞면이 뷰 프러스텀에 의해 잘려나가기 때문에 음영 처리된 픽셀을 보여주는 빨간색 볼륨은 없다. 깊이 클리핑을 비활성화하여 이 문제를 해결하려고 했지만, 뷰어 앞에 있는 픽셀만 클리핑되는 것을 방지했다(눈 뒤쪽의 픽셀은 여전히 클리핑됨).
이 문제를 해결하기 위해 Michiel의 방법을 반대로 적용했다.
- 스텐실 버퍼를 1로 클리어한다.
- 근거리 조명 경계 앞의 픽셀 표시를 해제한다.
- 원거리 조명 경계 앞의 픽셀을 음영 처리한다.
구현의 마지막 두 단계를 설명하고 픽셀을 음영 처리하는 데 사용된 방법을 설명한다.
Unmark Pixels
- M. van der Leeuw, ‘Deferred Rendering in Killzone 2’, SCE Graphics Seminar, Palo Alto, California, 2007. [본문으로]
- M. van der Leeuw, ‘Deferred Rendering in Killzone 2’, SCE Graphics Seminar, Palo Alto, California, 2007. [본문으로]
- Electron9.phys.utk.edu, ‘Coherence’, 2015. [Online]. Available: http://electron9.phys.utk.edu/optics421/modules/m5/Coherence.htm. [Accessed: 14- Aug- 2015]. [본문으로]
'Technical Report > Graphics Tech Reports' 카테고리의 다른 글
WGSL vs GLSL vs HLSL 문법 차이 (0) | 2025.05.01 |
---|---|
Texture Array vs Texture Atlas and UDIM (0) | 2025.04.27 |
Bent Normal 이란? (0) | 2025.04.27 |
hlsl : 고급쉐이더 언어(High Level Shader language)란? (0) | 2025.04.27 |
Fast GPU Matrix multiplication (0) | 2025.02.19 |