| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- VGPR
- ue4
- DX12
- optimization
- GPU Driven Rendering
- deferred
- GPU
- RayTracing
- unrealengine
- ShadowMap
- forward
- atmospheric
- wave
- scalar
- SIMD
- rendering
- Graphics
- Nanite
- Shadow
- Wavefront
- DirectX12
- scattering
- UE5
- vulkan
- texture
- SGPR
- hzb
- 번역
- Study
- shader
- Today
- Total
RenderLog
[번역] Visibility Buffer Rendering with Material Graphs – Filmic Worlds 본문
[번역] Visibility Buffer Rendering with Material Graphs – Filmic Worlds
scahp 2021. 10. 15. 22:00개인 공부용으로 번역한 거라 잘못 번역된 내용이 있을 수 있습니다.
또한 원작자의 동의 없이 올려서 언제든 글이 내려갈 수 있습니다.
출처 : http://filmicworlds.com/blog/visibility-buffer-rendering-with-material-graphs/
Visibility Buffer Rendering with Material Graphs
ByJohn Hable
July 05, 2021

Adventures in Visibility Rendering
- Part 1: Visibility Buffer Rendering with Material Graphs
- Part 2: Decoupled Visibility Multisampling
- Part 3: Software VRS with Visibility Buffer Rendering
- Part 4: Visibility TAA and Upsampling with Subsample History
Introduction
지난 1년 간은 모두에게 이상했습니다. 우리들은 각각 스스로의 방법으로 코로나 격리를 해왔고, 내 경우는 코딩을 하고 있습니다. 당신이 알듯, 작은 삼각형들은 낮은 Quad의 활용을 포함한 다양한 이유로 GPU에서 효율적이지 않습니다. Visibility Buffer는, 이론적으로, Quad 활용률이 낮은 경우 디퍼드보다 더 탄력적이기 때문에, 나는 Visibility Buffer 렌더러가 고전 디퍼드 렌더러보다 더 성능이 좋을 것이라 예감해왔습니다. 약간의 여유가 있는 때에, 이 이론을 Dx12 장난감 엔진에 넣었습니다.
Overview and Prior Art
Visibility buffer의 아이디어는 꽤 간단합니다. 첫번째 패스 동안 당신은 "Visibility Buffer"를 그립니다. 그곳에 object 와 삼각형 id를 단일 값으로 저장합니다, 보통 U32로 묶습니다. 그런 뒤 삼각형/오브젝트 ID만 있으면 당신이 필요한 파라메터들을 얻을 수 있습니다.
초기 주요 논문은 Christopher Burns와 Warren Hnt at Intel[3]가 작성한 것입니다. "Visibility Buffer"라는 용어를 만든 것 외에도, 그들은 삼각형 ID를 저장하는 것과 보간 된 버택스 데이터를 복원하는 것에 대한 내용을 찾을 수 있었던 첫 번째 레퍼런스였습니다. 여러 머터리얼을 처리하기 위해서, 그들은 화면을 타일 단위로 분리하고 그 안의 픽셀로 분류하였습니다. 그런 뒤 그들은 필요한 픽셀을 가지는 타일에 대해서만 머터리얼 당 단일 드로우 콜을 그릴 것입니다. Visibility buffer는 또한 다른 방식으로 렌더링을 최적화하기 위해서 사용되어왔습니다, 멀티 샘플링 압축 알고리즘과 같은 문제로 접근한 Christoph Schied 와 Carsten Dachsbacher [12]와 같이. ConfettiFX의 Wolfgang Engel는 Visibility buffer [5]를 임의의 머터리얼 그래프를 포기하지만 bindless 텍스쳐를 사용하여 시연했습니다. 그들은 임의의 텍스쳐에 접근으로 머터리얼을 하나의 ubershader 처럼 다뤘습니다. 그들은 또한 관대한 라이선스 하에 코드를 제공하였습니다. 그래서 만약 관심이 있다면 [4]를 보는 것을 매우 추천합니다.

물론 가장 최신의 작업은 에픽게임즈의 언리얼5 Nanite. 그들은 이전과는 다르게 문제에 접근합니다. 내가 말하지 못하는 부분은 없고, 높은 수준의 접근방식은 공개되어있습니다.(While I don’t have any secrets to tell you, the high-level approach is publicly known.)GBuffer를Visibility Buffer로 교체하는 것 대신, 그들은 Visibility Buffer를 GBuffer의 생성하는데 좀 더 최적화하는 데 사용합니다. 특별히, GPU 레스터 라이저는 작은 삼각형들에 대해 성능이 충분하지 않습니다, 그래서 Nanite는 그들의 overview videos [7] 처럼 이런 병목을 회피하기 위해서 커스텀 레스터 라이저를 사용합니다. 알아둘 점은 1:00:45로 바로 가서 Triangle size에 대해 빠르게 토론해볼 수 있습니다. UE5 Visibility 레스터 라이저는 Nanaite 전용 레스터 라이저입니다, 그래서 다른 오브젝트는 표면 Deferred 패스를 거칩니다.
이론적으로, Visibility 렌더링에서는 GBuffer가 필요없습니다. 만약 당신이 Normal 이 필요하면, 항상 버택스 파라메터로 부터 직접 가져와야 합니다. 그리고 만약 당신이 그것을 다시 한다면, 당신은 또다시 그것을 가져와야 합니다. 그러나 실제로는, 머터리얼 그래프가 이미 꽤 길며, 매년 점점 더 길어지고 있습니다. 만약 Direct lighting만 원하는 거면, 우리는 GBuffer 가 없어도 괜찮을 수 있습니다. 그러나 우리가 Normal과 같은 값을 한 프레임에 여러 번 필요로 하기 때문에(direct lighting, screen-space reflections, ambient probes, 등등), 머터리얼의 출력을 GBuffer에 저장하는 것은 올바른 방법인 것 같습니다.

여기에서 제안한 변형에서, Visibility Buffer는 GBuffer를 생성하는데 사용될 것입니다, 그리고 우리는 모든 삼각형들을 이 작업을 위해 사용할 것입니다. 우리는 임의의 머터리얼 그래프와 함께 이것을 하려고 합니다, 그것은 우리의 analytic 편미분을 계산한다는 의미입니다. 이 테스트에서, 내가 찾으려는 답에 대한 몇 가지 질문이 있습니다:
- 머터리얼 그래프로 해석적 편미분(analytic partial derivative) 를 효과적으로 계산할 수 있는가?
- 다른 말로, 이 방식이 가능한가? 만약 해석적 편미분을 계산할 수 없다면, 이 접근은 애시당초 가능성이 없습니다.
- 아주 많은 수의 삼각형에서 (삼각형당 픽셀 한개), Visibility 접근법이 더 빠른가?
- 이 접근법은 추후 작업량에서 더 빠른가? 만약 우리가 필름과 같은 품질을 원하면, 최종적으로 우리는 모든 삼각형을 1개의 픽셀 크기로 줄일 필요가 있습니다. 배경, 캐릭터, 잔디, 소도구(Props), 모든 것.
- 일반적인 삼각형 크기는 얼마인가(삼각형당 5-10 픽셀)? Visibility 접근법이 여기에도 역시 빠른가?
- 이 접근법이 현재 AAA 게임의 작업량에서도 더 빠른가?
이 모든 세가지 질문에 대한 대답은 다음과 같습니다: 그렇다! 그러나 이것은 장난감 엔진이고 진짜 AAA 엔진이 아니라는 것에 주의해야 합니다
Forward/Deferred/Visibility Overview
먼저, Forward, Deferred 그리고 Visibility 렌더링을 빠르게 둘러봐야 합니다. Forward 렌더링에서, 모든 것은 단일 픽셀 쉐이더에서 계산합니다. 이것은 아래와 같습니다.
struct Interpolators; // the position, normal, uvs, etc.
struct BrdfData; // normal mapped normal, albedo color, roughness, metalness, etc.
struct LightData; // the output lighting data, usually just a float3
// Pass 0: Render all meshes, output final light color
LightData MainPS(Interpolators interp)
{
BrdfData brdfData = MaterialEval(interp);
LightData lightData = LightingEval(brdfData);
return lightData;
}
기본적으로, 모든 물리 기반 포워드 렌더링 쉐이더는 코드에 없는 숨겨진 단계로 시작합니다:버택스를 보간하는 것. 하드웨어는 버택스 데이터를 보간합니다, 그리고 그것을 당신을 위해 보간 된 버택스 데이터를 마법처럼 전달합니다. 물론, 하드웨어는 마법이 아닙니다, 그러나 그런 단계는 픽셀 쉐이더 코드가 실행되기 전에 일어납니다. 다음 단계에서, MaterialEval() 함수는 surface material param을 계산하기 위해서 math와 texture look up을 수행하려고 보간 된 값을(UVs, normals 그리고 tangents 같은) 얻습니다. 이것들은 일반적으로 노멀맵되어진(normalmapped) 노멀, 베이스 컬러, 기타 등등입니다. 마지막 단계에서, 모든 파라메터에 대해서 라이트를 평가하고 최종 색상을 출력합니다.
그러나, 요즘 게임에서 더 일반적인 접근은 GBuffer를 사용하는 디퍼드입니다. 이것은 2패스를 렌더링 합니다.
// Pass 0: Render all meshes, output material data.
BrdfData MaterialPS(Interpolators interp)
{
BrdfData brdfData = MaterialEval(interp);
return brdfData;
}
// Pass 1: Compute shader (or large quad) to calculate lighting.
LightData LightingCS(float2 screenPos)
{
BrdfData brdfData = FetchMaterial(screenPos);
LightData lightData = LightingEval(brdfData);
return lightData;
}
디퍼드 접근은 포워드처럼 동일한 기본 단계들을 사용합니다. 그러나 라이팅 데이터는 분리된 패스에서 평가됩니다, 그것은 풀 스크린 Quad 또는 컴퓨트 쉐이더에서 평가됩니다. 이점은 다음과 같습니다:
- 라이팅 함수가 정확히 한 픽셀당 한번 실행되는 것이 보장되는 것은 큰 장점입니다. 지오메트리를 레스터 라이징 할 때, MaterialPS()는 픽셀당 한번 이상 실행될 것입니다, 그러나 LightingCS()는 단 한 번만 실행되는 것을 보장합니다.
- MaterialEval() 그리고 LightingEval() 함수가 같은 쉐이더에 있다면, 그들은 두 경우 모두 최악의 레지스터 할당으로 컴파일됩니다, 반면에 그들인 한 개의 패스로 분리되면 더 적은 레지스터들을 사용하게 됩니다. (그리고 더 나은 점유(occupancy)를 얻습니다)
- 디퍼드 접근에서, 우리는 GBuffer를 가지며, 이것은 다른 효과를 위해 사용될 수 있습니다, 스크린 스페이스 리플렉션, SSGI, SSAO 그리고 서브 서피스 스케터링과 같은.
디퍼드의 명백한 단점은 대역폭 사용량의 증가입니다. 당신이 할 수 있는 일반적인 스크린 스페이스 효과와 향상된 쉐이더 성능은 대역폭과 메모리 비용보다 더 큰 효과를 줍니다. 물론, 당신이 MSAA 가 반드시 필요하다면 이러한 장점은 모두 사라집니다, 그러나 이것은 완전히 다른 논의입니다.
Visibility 렌더링은 아주 다른 접근을 합니다. 라이팅 컬러(포워드 같이)를 또는 GBuffer 데이터(디퍼트 같이)를 레스터 라이즈 하는 대신, Visibility는 삼각형과 드로우콜에 대한 ID만 레스터라이즈 합니다. 우리는 무게중심좌표(barycentric co-ordinate) 나 derivative를 저장할 수 있습니다, 그러나 우리는 단일 삼각형 ID를 사용합니다.
// Pass 0: Rasterize all meshes, just output thin visibility
U32 VisibilityPS(U32 drawCallId, U32 triangleId)
{
return (drawCallId << NUM_TRIANGLE_BITS) | triangleId;
}
// Pass 1: In a CS convert from triangle ID to BRDF data
BrdfData MaterialCS(float2 screenPos)
{
U32 drawCallId = FetchVisibility() >> NUM_TRIANGLE_BITS;
U32 triangleId = FetchVisibility() & TRIANGLE_MASK;
Interpolators interp = FetchInterpolators(drawCallId, triangleId);
BrdfData brdfData = MaterialEval(interp);
return brdfData;
}
// Pass 2: In a CS, fetch BRDF data and calculate lighting
LightData LightingCS(float2 screenPos)
{
BrdfData brdfData = FetchMaterial(screenPos);
LightData lightData = LightingEval(brdfData);
return lightData;
}
알아둘 점은 우리는 Material과 Lighting 단계와 분리된 패스를 가지고 있다는 것입니다, 그것은 대부분의 선행 기술[3,13]과 다릅니다. 대부분의 이전 논문은 GBuffer의 대역폭을 줄이기 위해서 두 단계를 함께 수행합니다. 그러나 주어진 머터리얼 쉐이더의 복잡성을 감안할 때, 내 관점에서는 GBuffer는 필요합니다. 머터리얼 쉐이더는 아주 길 수 있습니다만 그들은 기술상(tech-art)의 정당한 이유로 길어질 수 있습니다. 아티스트에 의해 만들어진 쉐이더는 충분하지 않을 수 있습니다 (어쩌면 약간 과소평가되었을 수 있습니다). 그러나 심지어 그것들이 완전히 엉망이 경우에도, 그 효과가 가장 효과적인 방법으로 얻어지지 않더라도 일반적으로 그것들이 좋을 때가 있습니다, 머터리얼이 얻으려고 하는 효과에 대해 타당한 사유에서. 내 개인적인 관점에서 긴 머터리얼 그래프는 계속 남아있고, 그들은 비싸질 것입니다, 우리는 그것을 처리할 수 있는 더 효율적인 방법을 찾아야 합니다.
이런 경우, 우리는 어떻게 여러 머터리얼을, 그리고 특히 머터리얼 그래프를 처리할까요? 우리는 아래의 흐름도를 사용할 것입니다.

- 첫 번째 단계로, 우리는 full screen Visibility buffer를 그립니다.
- 모든 픽셀에 대해서, 각 머터리얼 마다 사용한 픽셀의 수를 계산합니다. 그 결과를 머터리얼 카운터 버퍼에 저장합니다.
- 머터리얼 시작을 파악하기 위해서 구간합(prefix sum)을 수행합니다.
- 다른 패스를 Visibility Buffer를 통해 수행하면서 Pixel XY 버퍼의 적절한 위치에 XY 위치를 각 픽셀에 저장합니다. 알아둘 점은 Pixel XY 버퍼는 Visibility Buffer와 같은 요소의 수를 갖습니다.
- 각 머터리얼에 대해서, GBuffer 데이터를 계산하기 위해서 간접 컴퓨터 쉐이더를 수행합니다.
이것이 패스들을 처리하는 순서입니다, 그리고 이것은 우리가 서로 다른 HLSL 코드로 여러 개의 머터리얼 그래프를 렌더링 할 수 있게 해줍니다. 그러나 GBuffer 데이터를 계산할 때 하나 더 다뤄야 할 문제가 있습니다: 편미분(Partial Deriviatives).
Hardware Partial Derivatives
만약 당신이 픽셀 쉐이더를 만들고 있다면, 어느 지점에서 당신이 텍스쳐로부터 읽을 부분을 작성할 것입니다. 아래와 같이요:
Sampler2D sampler;
Texture2D texture;
...
float2 uv = SomeUv();
float4 value = texture.Sample(sampler,uv);
당신이 이 코드를 실행할 때, GPU는 읽어올 최적의 mipmap level을 알아내고 그 데이터를 필터링할 것입니다. 그러나 어떻게 이 mipmap level을 올바르게 알아낼까요?
핵심은 픽셀 쉐이더는 단일 픽셀에 대해서 수행하지 않는다는 것입니다. 대신, 그들은 픽셀 2x2 그룹을 실행하며, 이것은 quad라고 불립니다. 아래의 보라색 예제에서, 삼각형은 4개의 모든 픽셀을 다룹니다, 모든 4개의 픽셀은 동일한 쉐이더를 lockstep으로 수행합니다, 그리고 텍스쳐를 GPU로부터 읽는 과정에서 GPU는 mipmap level을 결정하기 위해서 4개의 uv 값을 비교합니다. GPU는 오른쪽 픽셀에서 왼쪽 픽셀 빼고, 아래쪽에서 위쪽 픽셀의 빼서 편미분에 관해서 추정할 수 있습니다. 그런 뒤, 이 차이의 log2에서 적절한 level을 결정할 수 있습니다. 이러한 접근을 finite differences라고 부릅니다.

그러나, 만약 삼각형이 삼각형이 3개만 커버하는 경우 같이 4개의 픽셀을 다루지 않는다면 어떻게 될까요? 이 경우, the GPU will extrapolate the triangle onto that missing pixel, 그리고 일반적인 경우처럼 수행합니다. 3개의 픽셀은 실제로 수행되며 이것을 "Active Lanes"라고 부르며 이 3개에 픽셀에 derivatives 를 제공하기 위해서 실행되는 1개의 픽셀을 "Helper Lane"이라 부릅니다.

더 많은 정보를 위해서는, HLSL Shader Model 6.0 wave intrinsics doc [9]를 보세요. 실제로, 거기에는 동일한 2x2 quad 사이에 데이터를 전달하는 intrinsics 가 있습니다. 그런데 만약 여러 삼각형이 2x2 quad에 겹쳐져 있으면 어떻게 될까요?

이 샘플에서, 3개의 다른 삼각형들이 2x2 grid의 중심을 덮고 있습니다. 먼저, 초록 삼각형은 왼쪽 위의 모서리를 덮습니다. 이것을 그리기 위해서, GPU는 왼쪽 위의 픽셀을 Active lane으로 둔 채로 렌더링 할 것입니다. 그리고 나머지 세 개는 active lane에 텍스쳐 derivatives를 제공하는 helper lane으로 쉐이딩 될 것입니다.

다음으로, 파란색 삼각형은 2 active lane과 2 helper lane을 가질 것입니다.

마지막으로, 빨간색 삼각형은 1개의 active lane과 3개의 helper lane을 가질 것입니다.

1개의 삼각형이 4개의 quad 안에서 4개의 픽셀을 덮는다면, 픽셀 쉐이더의 작업량은 이것과 같습니다:

그러나 3개의 삼각형이 4개의 quad 안의 4개의 픽셀을 덮는다면, 픽셀 쉐이더의 작업량은 이것과 같습니다:

만약 우리에게 동일한 2x2 quad를 덮는 3개의 삼각형이 있다면, 우리는 실제로 한 개의 삼각형이 4개의 픽셀을 덮는 것에 비해서 3배 더 많은 시간 픽셀 쉐이더 작업을 합니다. active lane을 total lane으로 나눈 비율은 Quad 활용도입니다. 보라색 quad는 100% quad utilization을 가집니다만 세 삼각형의 작업량은 33% quad 활용도 입니다. 그러면 quad utilization에 영향을 미치는 주요 요인은 뭔가요? 삼각형의 크기.
Quad Utilization Efficiency
이것을 감안할 때, 우리는 오직 1개의 픽셀의 삼각형을 렌더링 한다고 합시다. 심지어 오버드로우도 없는 상태로, 각각의 2x2 quad는 1 active lane 그리고 3 helper lane을 가질 것입니다.

만약 우리가 전체 장면을 1 픽셀의 삼각형들로 그리려고 한다면, 우리는 각 픽셀 당 1번 대신 4번 픽셀 쉐이더가 실행합니다. 포워드와 디퍼드 머터리얼 쉐이더는 모든 픽셀에 대해서 4번 수행될 것입니다, 반면 디퍼드 라이팅과 Visibility Material 그리고 라이팅 패스는 픽셀 당 한번 수행될 것입니다.
1 픽셀 사이즈의 삼각형에 대한 픽셀당 쉐이더 함수의 호출 수:

다른 극단적인 예로 가봅시다, 우리가 크고 넓은 삼각형을 가지고 있다면 어떻게 될까요? 이 경우 더 간단합니다. helper lane은 전체 렌더링 되는 픽셀의 적은 비율일 것입니다, 그리고 이 포스트의 목적에는 이것은 부수적인 것이라 할 수 있습니다. 픽셀 쉐이더는 픽셀당 한 번씩 수행되고 있습니다.

큰 삼각형의 픽셀당 쉐이더 함수 호출 수의 근사치:

앞의 것들은 극단적입니다, 그러나 중간 정도 라면 어떨까요? 중간은 더 복잡합니다. 통설은 삼각형당 약 10개의 픽셀을 목표로 합니다. 10개의 픽셀의 삼각형의 quad utilization은 얼마인가요? 모양에 따라 다양할 것입니다, 그러나 몇 가지 시도를 해서 찾아봅시다. 간단한 10 픽셀 삼각형부터 시작할 것입니다.

첫눈에, 정말 좋아 보임, 10 active lane 과 2 helper lane이 있습니다. 그러나 이 삼각형이 2x2 quad에 정렬될 수 있는 4가지 방법이 있습니다.

계산해보면, 평균적으로 10개의 active lane에 9 helper lane 입니다. 다음으로 이것이 조금 더 길고 얇은 것을 봅시다.

이런 모양에서, 우리는 평균 10 active lane에 11 helper lane이 생깁니다. 여기에 최악의 모양이 있습니다:

내가 그것을 올바르게 계산했다면, 10 active lane에 helper lane 21개 입니다(아프네요..ㅠㅠ). 이제, 이런 모양을 위해서는 삼각형이 완전히 완전히 정렬되어야 하므로 극단적인 경우입니다. 합리적인 추정으로써, 만약 첫 번째(청록색)와 두 번째(오렌지색) 삼각형이 동일하게 발생하고 세 번째 (보라색)이 발생하지 않는다면, 우리는 50%의 quad utilization을 가집니다. 다른 말로, 포워드와 디퍼드 머터리얼 패스는 픽셀당 2배 수행될 것입니다. 다시 한번, 디퍼드 라이팅과 Visibility Material 그리고 라이팅은 픽셀당 한번 수행됩니다.
10픽셀 삼각형의 픽셀당 쉐이더 함수 호출 수 근사치:

한눈에도, Visibility 렌더링이 디퍼드에 비해서 갑자기 아주 매력적으로 보입니다. 10 픽셀 크기의 삼각형에서 디퍼드 머터리얼 패스는 Visibility 머터리얼 패스보다 2배 더 많은 시간을 사용합니다, 그리고 이것은 삼각형이 1픽셀이면 4배로 바뀝니다. 그러나 Visibility 패스는 추가 작업이 있습니다.
Interpolation and Analytic Partial Derivatives
디퍼드 접근법은 하드웨어에 의존하여 보간기(interpolator)를 픽셀 쉐이더에 전달하는 반면, 우리는 데이터를 직접 가져와서 보간해야 합니다. 첫 번째 단계는 데이터를 가져오는 것인데 이것은 상대적으로 단순합니다. 알아둘 점은 당신은 데이터를 공격적으로 패킹하여(packing) 상당한 성과를 얻을 수 있습니다, 그러나 이 테스트에서는 간단히 데이터를 32bit float에 저장합니다. g_dcElemData는 중요한 인스턴스 당 데이터(버택스 버퍼의 시작과 같은 것)를 담고 있는 StructuredBuffer 즉 드로우콜 요소 데이터입니다.
uint3 FetchTriangleIndices(uint dcElemIndex, uint primId)
{
TriangleIndecis ret = (TriangleIndecis)0;
uint startIndex = g_dcElemData[dcElemIndex].m_visStart_index_pos_geo_materialId.x;
return g_visIndexBuffer.Load3(startIndex + 3 * 4 * primId);
}
TrianglePos FetchTrianglePos(uint dcElemIndex, TriangleIndecis triIndices)
{
uint startPos = g_dcElemData[dcElemIndex].m_visStart_index_pos_geo_materialId.y;
TrianglePos triPos = (TrianglePos)0;
triPos.m_pos0.xyz = asfloat(g_visPosBuffer.Load3(startPos + 12 * triIndices.m_idx0));
triPos.m_pos1.xyz = asfloat(g_visPosBuffer.Load3(startPos + 12 * triIndices.m_idx1));
triPos.m_pos2.xyz = asfloat(g_visPosBuffer.Load3(startPos + 12 * triIndices.m_idx2));
return triPos;
}
많은 수의 명령어는 아닙니다만 데이터를 기다리는 부분이 퍼포먼스에 악영향을 줍니다. UVs와 노멀 데이터를 가져오는 것은 거의 동일하므로 여기서는 나열하진 않을 것입니다. 다음 단계는 무게중심좌표계(barycentric coordinate)의 계산입니다. DAIS 논문 [12]는 Appendix A에 아주 편리한 식이 담겨있습니다, 그리고 ConfettiFX 코드 또한 아주 유용한 레퍼런스입니다 [4].
struct BarycentricDeriv
{
float3 m_lambda;
float3 m_ddx;
float3 m_ddy;
};
BarycentricDeriv CalcFullBary(float4 pt0, float4 pt1, float4 pt2, float2 pixelNdc, float2 winSize)
{
BarycentricDeriv ret = (BarycentricDeriv)0;
float3 invW = rcp(float3(pt0.w, pt1.w, pt2.w));
float2 ndc0 = pt0.xy * invW.x;
float2 ndc1 = pt1.xy * invW.y;
float2 ndc2 = pt2.xy * invW.z;
float invDet = rcp(determinant(float2x2(ndc2 - ndc1, ndc0 - ndc1)));
ret.m_ddx = float3(ndc1.y - ndc2.y, ndc2.y - ndc0.y, ndc0.y - ndc1.y) * invDet;
ret.m_ddy = float3(ndc2.x - ndc1.x, ndc0.x - ndc2.x, ndc1.x - ndc0.x) * invDet;
float2 deltaVec = pixelNdc - ndc0;
float interpInvW = (invW.x + deltaVec.x * dot(invW, ret.m_ddx) + deltaVec.y * dot(invW, ret.m_ddy));
float interpW = rcp(interpInvW);
ret.m_lambda.x = interpW * (invW[0] + deltaVec.x * ret.m_ddx.x * invW[0] + deltaVec.y * ret.m_ddy.x * invW[0]);
ret.m_lambda.y = interpW * (0.0f + deltaVec.x * ret.m_ddx.y * invW[1] + deltaVec.y * ret.m_ddy.y * invW[1]);
ret.m_lambda.z = interpW * (0.0f + deltaVec.x * ret.m_ddx.z * invW[2] + deltaVec.y * ret.m_ddy.z * invW[2]);
ret.m_ddx *= (2.0f/winSize.x);
ret.m_ddy *= (2.0f/winSize.y);
ret.m_ddy *= -1.0f;
return ret;
}
입력 점들은 동차 좌표공간입니다(MVP 변환 바로 다음). 알아둘 점은 우리는 무게중심 좌표계의 derivative의 x와 y를 계산합니다. 무게 중심 좌표계 (m_lambda)는 perspective-correction interpolation으로 결정됩니다. 최종적으로, 무게 중심 좌표계의 derivative는 크기 조정을 위해서 NDC 단위(-1 to 1)에서 픽셀 단위로 2/winSize 만큼 조정됩니다. 그리고 마지막으로, m_ddy는 뒤집힙니다. 왜냐하면 NDC는 아래에서 위쪽인 반면 윈도우 좌표계는 위에서 아래이기 때문입니다.
무게중심 그리고 무게 중심의 편미분을 구했을 때, 버택스의 속성을 보간하는 것은 쉽습니다. 주어진 3개의 float가 있다고 하면, 이 함수는 derivative x와 y에 관하여, 보간 된 값 3개가 한 세트로 된 것을 반환합니다.
float3 InterpolateWithDeriv(BarycentricDeriv deriv, float v0, float v1, float v2)
{
float3 mergedV = float3(v0,v1,v2);
float3 ret = 0;
ret.x = dot(deriv.m_lambda,mergedV);
ret.y = dot(deriv.m_ddx * mergedV, float3(1,1,1));
ret.z = dot(deriv.m_ddy * mergedV, float3(1,1,1));
return ret;
}
마지막으로, 보간기에서 머터리얼 그래프의 텍스쳐 샘플로의 경로에 있는 특정 값들에 체인룰(chain rule)을 적용합니다. 텍스쳐는 SampleGrad() 를 사용하여 샘플링되며, 명시적으로 uv derivative를 전달합니다.
또한, 알아둘 점은 이것은 새로운 개념이 입니다. 이 방법 [11] 으로 derivative를 생성하기 위해서 C++ 템플릿으로 구현합니다, 그리고 이것은 Arnold가 사용하는 접근법입니다 [8]. 그러나 derivate code를 생성하기 위해서 템플릿을 사용하는 대신에, 이 장난감 엔진은 derivate 계산을 머터리얼 그래프의 hlsl에서 생성합니다. Arnold는 실제로 derivative가 필요한 노드들을 위해 "derivative sink" 용어를 사용합니다, 그리고 sink를 위한 경로의 임의의 노드는 derivative를 해야 합니다. 그러나, 그들은 오직 노드의 5%~10%만 한다고 추정합니다. 나머지 노드는 derivative를 무시할 수 있습니다
실제로는, 실제 세계에서 대다수의 많은 쉐이더들은 약간의 조정만으로 보간된 UVs를 사용합니다 (scale이나 rotation 같은). 머터리얼에서 대부분의 복잡도는 복잡한 수학과 UV 룩업 후 텍스쳐를 서로 블랜딩 하는 과정에서 발생합니다. 그래서 대부분의 경우 우리는 몇몇의 노드에서만 추가 deriviative 계산을 수행하면 됩니다.
여전히, 그것은 괜찮은 추가 작업입니다.(it’s a healthy chunk of extra work.). 삼각형 밀도에 따라서 GBuffer 머터리얼은 2x 나 4x 더 수행함에도 불구하고, 이러한 추가적인 명령어는 아주 무겁기 때문에 Visibility 머터리얼 함수는 GBuffer 머터리얼 함수보다 느릴까요? 또는 이런 추가 연산은 Visibility 머터리얼 함수가 더 빠를 정도로 충분히 가벼울까요? 알아봅시다.
Performance Tests
테스트를 위해서, heightmap이 있는 단일 모델을 구성했습니다, 그리고 5x3 격자에 복사하였습니다. 카메라의 아래에서 쉐도우를 만드는 여러 개의 메시가 있습니다. 쉐도우 뎁스 패스는 아주 비효율적입니다. 왜냐하면 그것은 무식하게 수많은 삼각형의 깊이 값을 렌더링 합니다, 그러나 모든 3개의 렌더링 타입에 대해서 비용은 모두 같습니다. 그래서 이 숫자는 여전히 유효합니다, 모든 커맨드(복사 포함)가 겹침을 최소화하고 일정한 수를 유지하기 위해서 그래픽스 큐에서 수행됩니다. 이런 모든 캡쳐에 대해서, NVIDIA RTX3070의 1080p를 사용한 나의 머신 PIX로부터 타이밍 캡쳐입니다 (기술적으로는 1088입니다 왜냐하면 프레임 버퍼 사이즈가 16의 배수로 반올림되기 때문입니다).

주요 지면은 5x3 격자로 된 heightmap 메시입니다. 그들은 테셀레이션 된 heightmap이 아닙니다. 오히려 메시 포인트를 생성하는 전처리 단계가 있습니다, 그런 뒤 일반 메시처럼 처리됩니다. 이런 아이디어는 내가 삼각형 밀도의 근사를 통제하기 위해서 필요합니다, 그러나 나는 약간의 실제 사용 케이스와 유사하게 하기 위해서 약간의 오버드로우를 원했습니다. 이러한 관점에서의 드로콜은 아래와 같습니다:

이 단계에서 우리는 카메라를 고정된 상태로 유지할 수 있습니다, 메시의 해상도를 변경, 그리고 삼각형의 개수가 증가했을 때 이것은 우리에게 포워드, 디퍼드, 그리고 Visibility의 트레이드오프에 대한 대략적인 아이디어를 줍니다.
머터리얼 쉐이더에서, 나는 게임에서 사용하는 것과 대략적으로 비슷한 것을 원했습니다. 테스트 모델에서, 빠르게 알베도, 노멀, 스펙큘러를 룩업 하고 출력하는 간단한 PBR 텍스쳐를 사용하는 것은 매우 일반적입니다. 그러나 실제 세상에서 머터리얼 그래프는 스파게티 한 그릇(a bowl of spaghetti) 처럼 보이는 경향이 있습니다. 나는 이 쉐이더를 AmbientCg.com[2]에서 두 세트의 텍스쳐로부터 만듭니다, 그나저나, 만약 당신이 허가된 라이센스의 고품질 텍스쳐가 무료로 필요하다면 적극 권장합니다.
블랜딩을 위해서, 나는 펄린 노이즈의 3 옥타브를 사용합니다(I used 3 octaves of Perlin noise). 나는 또한 내가 원래 계획했던 축축한 층 또한 만들었습니다. 그러나 나의 첫 번째 테스트는 flat red 였습니다, 그것은 멋진 마른 분말 모습이었기 때문에 그것은 선택했습니다. 나는 펄린 노이즈의 한 개의 옥타브를 heightmap과 조합하여 빨간 층이 돌들 사이에 틈에 더 편향(biased)됩니다.
여기는 머터리얼 그래프의 스크린샷입니다. 약간 복잡합니다, 이 머터리얼 에디터가 상용엔진의 적절한 UI 기능보다 대부분(전부?) 부족하기 때문입니다. 나는 노드에 상수를 추가해본 적이 없기 때문에 많은 추가 노드가 있습니다 (0.5를 추가하는 것은 add 노드 그리고 0.5 상수 노드 둘 다 요구합니다). 그러나 이것은 테스트를 위해서는 충분합니다.

Low Triangle Count
첫 테스트를 위해서, 나는 큰 삼각형을 볼 것입니다, 그래서 각각의 메시는 두 개의 삼각형으로 만들어집니다. 여기에 낮은 뷰에서 본 것이 있어서 이것이 얼마나 평평한지 볼 수 있습니다.

여기서 삼각형 ID 뷰가 있습니다. 당신이 보듯이, 각각의 드로콜은 두 개의 삼각형입니다.

그리고 최종 이미지입니다.

숫자 몇 개를 캡쳐해봅시다. 여기에 패스에 대한 설명이 있습니다.
PrePass: 포워드와 디퍼드 패스에서, PrePass는 Depth만 기록합니다. 그러나, Visibility 패스에서는, Visibility drawCallId와 triangleId를 포함하는 U32 또한 기록합니다.
Material: 디퍼드 패스에서, 이 패스는 머터리얼 레스터라이제이션 패스를 말합니다. Visibility에서는, 컴퓨트 패스 시간을 말합니다. 그리고 물론, 포워드 패스에서는 이것이 하나의 숫자에 대한 라이팅 패스입니다.
Lighting: 디퍼드의 경우, 이 패스는 텍스쳐를 읽고 라이팅을 기록하는 컴퓨트 쉐이더입니다. Visibility는 비슷한 연산을 텍스쳐 대신 버퍼로 수행합니다.
VisUtil: 이 카테고리는 Visibility 렌더러의 다른 패스를 참조합니다. 이것은 각 머터리얼의 픽셀의 숫자를 세고, Visibility buffer를 재정렬, 그리고 픽셀이 쉐이딩 되면 그것을 선형 버퍼로 재정렬하는 컴퓨트 쉐이더를 포함합니다.
Other: 이 카테고리는 다른 모든 것에 관한 것입니다. 여기의 메인 패스는 쉐도우 패스, TAA, motion vector, 톤매핑, GUI (이것들은 스크린샷에는 나타나지 않습니다), 그리고 여러 가지 종류의 베리어입니다. 내가 실제로 계산한 방법은 총 GPU 시간에서 다른 모든 카테고리를 빼는 것이었습니다.
다른 카테고리는 약간 까다롭습니다, Raster/Compute 겹치는 것은 렌더링 패스를 구성할 때 중요한 설계 결정 중 하나입니다. 그러나 이 테스트에서 최종 렌더링 시간을 최소화하는 것이 아니라 다른 알고리즘 사이에 상대적인 비용을 결정하는 것이 목적입니다. 세 가지 렌더링 타입에 대한 비용은 대충 비슷합니다, 그래서 별도로 그룹화하는 것이 합리적입니다. 포워드/디퍼드/Visibility 의 선택은 TAA와 쉐도우 같은 것의 비용에 아주 적은 영향을 미칩니다.
저밀도 삼각형에서의 성능

좋아요, 결과가 흥미롭네요. 디퍼드의 경우, 머터리얼 쉐이더의 비용이 1.06ms, 그리고 Visibility 쉐이더 비용은 1.06ms 로 정확히 동일합니다. 게다가, 라이팅 쉐이더는 비용은 0.032ms 정도 약간 더 높습니다, 그래서 그것은 Visibility 패스를 관리하는데 추가적으로 0.322ms 비용이 듭니다. 최종적으로, 포워드 패스는 머터리얼과 라이팅 패스를 약간 더 빠르게 계산할 수 있습니다, 왜냐하면 대역폭을 아낄 수도 있기 때문입니다.
먼저, 면책조항으로, 5x3 quad는 거의 평평합니다, 그래서 z-fighting이 약간 발생합니다, 그래서 GBuffer 패스의 몇몇 픽셀은 올바르게 early-z rejection 될 수 없습니다. 이것은 작은 량의 오버드로우를 유발합니다. 그러나 더 설득력이 있는 것은 패스는 주로 대역폭이 제한이 있다는 것입니다, 그래서 버택스 보간의 추가적인 ALU 비용과 derivative의 계산이 대역폭 비용으로 가려집니다.
그러나, 숫자를 보면, 버택스 속성을 얻어오는 추가 비용과 편미분을 계산하는 것은... 아무것도 아니다? Visibility 라이팅 패스는 약간 더 높습니다, 그리고 추가 관리 패스가 추가됩니다, 그러나 전체적으로 다음 테스트를 위해 아주 고무적인 결과입니다. 또한, VisUtil 패스는 약간 떨어질 수 있습니다. 현재 구현은 텍스쳐 대신에 버퍼를 사용하여 라이팅을 렌더링 합니다, 그리고 이후에 데이터를 정렬합니다. 그러나 명백히 Visibility 데이터의 출력을 UAV인 GBuffer로 직접 저장하는 것이 더 빠를 것입니다.
Medium Triangle Count
다음으로, 중간 해상도의 장면에서 봅시다. 고해상도 이미지는, 500x500 일 것입니다. 픽셀을 10x 더 가볍게 하기 위해서 우리는 해상도를 500/sqrtp(10)=158로 만들 수 있습니다. 그래서, 이 중간 해상도를 위한 메시를 158x158로 설정합니다. 그들은 디테일이 있지만 확실히 울퉁불퉁합니다.

여기에 우리 카메라 각도에서 본 최종 화면입니다:

그리고 당연하게도, 모든 삼각형의 화면입니다. 카메라 각도를 조정할 때, 삼각형 당 10 픽셀이 되도록 노력했습니다, 그러나 얼핏 보면 8에 가까워 보입니다. 최종 목표는 특정 크기가 아니라 방향을 얻는 것이기 때문에 충분합니다.

주어진 삼각형들은 약 8-10 픽셀입니다, 우리는 레스터라이즈 된 패스에서 약 2x의 비용일 것이라 예측할 수 있습니다. 숫자는 어떨까요?

면책조항으로, 쉐도우 뎁스 패스에 의해 변화하기 때문에 Other의 변경사항은 중요하지 않습니다. 쉐도우 패스는 아주 단순합니다, 간단히 모든 지오메트리를 케스케이드와 포인트 라이트 쉐도우 패스로 렌더링 합니다. 지오메트리가 복잡해지기 때문에 쉐도우 패스도 복잡해집니다. 그러나 이 비용의 변화는 모든 세 가지 렌더링 타입에서 사실상 동일합니다. 세가지 다른 알고리즘에 대해서 연관성 있는 패스만 보겠습니다: PrePass, Material, Lighting 그리고 VisUtil.

숫자를 봅시다, 삼각형이 명백히 작아지고 있습니다, Visibility 렌더링이 앞섭니다. 그리고 숫자는 내가 생각한 것과 비슷하지 않습니다. 내 눈에 가장 먼저 보인 것은 PrePass 입니다. 나는 Visibility ID 그리고 Depth (깊이만 있는 것과 대조적으로) 둘 모두를 렌더링 해서 더 큰 성능 향상을 예측했습니다, 그러나 차이는 크지 않았습니다 (0.033ms). 가장 큰 차이는 포워드와 디퍼드의 머터리얼 패스입니다, 여기서 각각 2.43x 그리고 2.78x 더 길어졌습니다. (이전 첫 번째 이미지 보다). Visibility Material 패스는 원본 패스보다 1.56x 더 길어졌습니다.
그러나 왜 Visibility 머터리얼 패스가 큰 삼각형의 경우보다 더 많은 시간이 걸렸을까요? 결국 같은 수의 픽셀에서 수행되는 같은 쉐이더 입니다. 문제는 캐시 일관성(cache coherency) 입니다. 두 개의 시나리오를 봅시다. 왼쪽에는, 8x8 블록의 픽셀이 있고 2개의 삼각형으로 나뉘어 있습니다, 그리고 오른쪽 8x8 블록의 각 픽셀은 서로 다른 삼각형을 가리킵니다.

컴퓨트 쉐이더에서, 모든 64개의 스레드는 첫 번째 버택스에 대해서 데이터를 얻어옵니다. 그러나 왼쪽의 경우, GPU는 전체 8x8 블록 2개의 고유한 버택스 위치를 얻기만 하면 됩니다. 그러나 오른쪽의 경우, GPU는 메모리로 부터 64개의 고유한 위치가 필요할 것입니다. 일관성이 나빠질 뿐만 아니라, 데이터를 얻는 더 많은 기반 대역폭이 필요할 것입니다, 때문에 메모리로 부터 얻어와야 할 바이트 숫자가 더 높습니다. 그래서 이런 추가적인 데이터 얻는 과정은 첫번째 케이스의 큰 삼각형에서 작은 비용을 가집니다, 그들은 이 장면에서 적절한 비용을 가집니다. 그러나, 이 비용은 디퍼드 패스의 나쁜 quad 활용 때문에 지불하는 손실보다 훨씬 적습니다. 그래서, Visibility 접근법이 더 전체적으로 더 빠릅니다.
High Triangle Count
최종적으로, 많은 수의 삼각형인 세 번째 스크린샷을 봅시다. 각 모델은 500x500, 그리고 우리는 삼각형 당 1 픽셀의 경우에 잘 맞습니다. 여기에 바위를 확대한 것입니다.

최종 이미지:

그리고 삼각형 Id들:

그래서 이 숫자는 어떻습니까?

타임라인의 시작에서, PrePass의 비용은 계속 증가하지만 그럴만합니다, 그리고 Visibility U32를 쓰는 비용만 15% 추가됩니다. Other 패스는 더 크게 증가하지만 주로 쉐도우 뎁스 패스에 의해 결정됩니다. Visibility 패스는 Other 카테고리에서 0.21ms 보다 적으며, 이것은 약간 이상합니다. PIX를 실행하여 확인해봅시다, 쉐도우 뎁스 패스는 PrePass와 다소 겹칩니다, 그래서 PrePass에 추가 0.15ms 비용은 쉐도우 패스 0.15ms 를 숨겨줍니다. 그리고 나머지 0.06ms는 VisUtil과 나머지의 겹침으로 숨겨집니다.
그러나 주요 차이점은 머터리얼과 라이팅의 비용입니다. 숫자는 자명합니다. 첫 번째 프레임에 비해서 포워드 비용은 5.76x 커집니다, 그리고 디퍼드 머터리얼 비용은 첫번째 프레임에 비해서 4.38x 커집니다. 그러나, Visibility 머터리얼 비용은 첫번째 프레임의 1.90x 커집니다.
다시 한번, 렌더링 알고리즘 간의 차이에 관해 패스를 분리해봅시다.

숫자는 명확합니다. 이 테스트의 경우, 삼각형의 밀도가 단일 픽셀로 감소하면, Visibility 렌더링의 quad 활용이 더 높을수록 버택스 속성의 보간과 해석적으로 계산한 편미분(analytically calculating partial derivatives)에 드는 추가적인 비용보다 훨씬 더 이득을 봅니다.
Conclusions:
최초의 질문들로 돌아가서:
머터리얼 그래프에서 해석적 편미분의 계산을 효율적으로 할 수 있나요?
이 테스트의 경우, 답은 "yes" 입니다. 그러나 일반적인 경우, 더 나은 대답은 "아마도"입니다. 편미분을 생성하기 위해서 추가적인 계산은 UV scale과 offset과 같은 간단한 경우 사소한 부분입니다. 나는 다른 몇 가지 테스트를 대충 수행해봤습니다(전체 PIX 를 수행하지 않고) 그리고 기반 숫자가 변경되게 하는 성능 저하를 유발하는 심각한 경우(use case)를 한눈에 찾아볼 수 없었습니다.
대부분은 일반적인 경우는 다른 텍스쳐의 UV가 되는 텍스쳐 출력일 것입니다. 만약 4개의 텍스쳐를 읽고 텍스쳐로부터 읽은 UV offset을 더하는 표준 머터리얼에 있다면, 포워드/디퍼트 머터리얼 패스는 1 개의 텍스쳐 읽기를 추가하는 반면 Visibility 머터리얼 패스는 3개를 추가할 것입니다. 그러나 5개와 7개의 텍스쳐 샘플의 차이는 큰 숫자의 변화를 만들기 위해서 강력한 성능 절벽을 만들기에는 충분하지 않을 것입니다. 그리고 나는 이런 두 개의 추가 샘플링이 첫 번째 것과 아주 높은 캐시 일관성을 가지기 때문에 최소한의 비용을 가진다고 예측합니다.
더 문제가 되는 경우는 Parallax Occlusion 매핑입니다. 이론적으로 우리는 각 단계에 대해서 1개 대신 3개의 텍스쳐 읽기가 필요합니다. 그러나 derivative는 실제로 각 단계 별로 많이 변할까요? 그들 모두에 대해서 동일한 derivative/mip-map 단계를 사용하는 것이 가능한가요? 한눈에 봐도 이것은 그럴듯하게 보이지만, 확인하지 않았습니다
물론, 우리는 아주 나쁜 경우도 있습니다. refraction eye 쉐이더는 각막(cornea) 지오메트리의 노멀로 굴절시킬 때 뷰 벡터의 편미분을 따라 전달되어야 합니다. 나는 그 쉐이더가 finite differences 버젼에 비해 3x 더 느려질 수 있다고 생각합니다. 왜냐하면 우리는 뷰 벡터의 derivative의 x, y 그리고 또한 각막 높이의 편미분 그리고 normal의 x, y를 처리하기 위해서입니다. 그러나 내 생각에 근사로 이 비용을 줄일 수 있을 것입니다. 예를 들어, 나는 각막의 곡률이 너무 작아서 관련성이 없다고 가정할 수도 있습니다. 그래서 계산에서 derivative를 0으로 만들 수도 있을 것입니다.
마지막으로, finite differences를 사용하는 표준 derivative 또한 완벽하지 않습니다. branches(분기문), discard(픽셀을 버리는 연산) 같은 문제가 되는 경우가 있는데, 이 부분은 analytic derivative 로 전환하여 잘 해결할 수 있습니다.. 이것은 특히 삼각형의 가장자리를 벗어나서 helper lanes을 사용하는 때에 특히 그렇습니다.
맞아요, 이 경우에는 작동합니다. 그러나 AAA 게임에서의 일반적인 편미분 문제에 대해서, 내 대답은 "조금 더 yes 로 기울어진 아마도" 입니다. 내 결론은 해석적 편미분은 사용 가능합니다, 그러나 확실하게 하기 위해서 더 복잡한 사용 예에 대해서 테스트가 필요합니다.
아주 많은 삼각형의 수에 대해서 (삼각형 당 1 픽셀), Visibility 접근법은 빠른가요?
아주 많은 삼각형 수에서, 각 픽셀은 4번 수행됩니다, Visibility 렌더링이 확실히 승자입니다. 디퍼드의 비용은 6.43ms 인데 비해 Visibility는 4.34ms 입니다. 관련 패스에 대한 전체 GPU 비용 32.5%의 감소는 사소한 것이 아닙니다.
더 일반적인 삼각형의 크기는 어떤가요(삼각형 당 5-10 픽셀)? Visibility 접근법이 여기서도 빠른가요?
중간 정도의 개수에서, 이 테스트에서는, yes, Visibility 접근법이 역시 빠릅니다. 차이는 3.85ms 대 2.96ms로 더 좁지만요. 여전히 23.1%의 감소는 적지 않습니다. 게다가, Visibility 접근법은 좋지 않은 수많은 지오메트리와 오버드로우가 있는 뷰 앵글에서 히칭(spikey)이 덜할 것으로 예측됩니다. 그러나 추측입니다.
Other Considerations:
Visibility 렌더링은 삼각형 수가 많을 때, 디퍼드 렌더링에 비해서 더 확장하기 좋습니다, 그리고 삼각형의 수는 매년 증가합니다, 모든 게임엔진이 모든 것을 버리고 전환해야 되나요? 물론 아닙니다. 주요 구조적인 렌더링 결정사항은 여러 다른 요소들이 있습니다.
Code Complexity:
아마도 Visibilty 렌더링을 반대하는데 가장 좋은 근거는 복잡도와 관련되어 있습니다. Visibility 렌더링은 vertex buffer와 편미분을 관리하는 작업시간이 필요합니다. 작업시간은 유한합니다.
Memory:
Visibility 렌더링을 사용하기 위해서는, 당신의 모든 다이나믹 지오메트리는 쉐이더에 접근 가능한 아주 큰 버퍼(혹은 버퍼들)에 있어야 합니다. 만약 당신의 스크린이 풀잎으로 덥혀있다면, 모든 개개별의 변환 후 버택스가 버퍼 어딘가에 있어야 합니다. 그렇다고 해서, 메모리가 반드시 나쁜 것은 아닙니다 .당신은 아마 위치 XYZ를 각각 16 비트로 압축할 수 있을 것입니다, 그래서 각각의 버택스는 6 bytes 입니다. 만약 당신이 변환 후 탄젠스 공간이 필요하다면 픽셀당 10 바이트에 대해서, 4 byte 쿼터니언에 저장할 수 있습니다. 1080p 에 렌더링 한다고 가정해봅시다 (2 백만 픽셀), 그리고 당신이 픽셀당 버택스 하나를 가집니다. 그러면 이것은 20MB 의 RAM, 또는 당신이 이전 프레임 또한 저장한다면 40MB이 필요합니다. 40MB 의 RAM은 사소하지 않습니다만 그렇다고 말도 안되지 않는 것도 아닙니다. 그래서 레이트레이싱에서 메시를 포함시키려면 당신은 변환후 버택스가 어쨌든 필요합니다.
PSO Switches:
Visibility 렌더링의 사소한 장점 중 하나는 레스터라이제이션 하는 동안 PSO 전환이 더 적다는 것입니다. 포워드와 디퍼드 레스터라이제이션 패스에서, 초기 단계에서 픽셀 쉐이더가 작업하기에 부족하여 버블이 형성될 수 있습니다, 특히 PSO가 계속해서 전환되는 경우에요. 그러나 불투명 지오메트리는 완전히 다른 머터리얼을 가지고 있다 하더라도 동일한 Visibility 픽셀 쉐이더를 공유할 수 있습니다. 반면에 몇 가지 예외가 있습니다 (후면 컬링, 알파테스팅, 등등), 우리는 우리는 모든 볼 수 있는 지오메트리를 삼각형의 ID를 기록하는 패스를 위해서 몇몇의 PSO 로 그룹 지을 수 있습니다. 머터리얼 패스에서 우리는 더 적극적으로 PSO를 그룹화할 수 있습니다. 예를 들면, 불투명 그리고 알파 테스트가 가미되어 렌더링 하는 머터리얼은 머터리얼 데이터를 동일한 Visibility Indirect CS dispatch에서 평가할 수 있습니다, 반면에 디퍼드 머터리얼 패스에서는 그들은 분리된 PSO를 요구할 것입니다. Visibility 파이프라인은 훨씬 더 적은 버블을 가집니다, 그러나 이런 종류의 작업량을 테스트하는 것은 작은 장난감 엔진의 범위를 뛰어넘습니다.
Material Cost:
만약 거대하고, 복잡한 머터리얼로 렌더링을 한다면, Visibility는 더 강력합니다. 왜냐하면 당신의 버택스 데이터를 얻고 보간하는 것이 머터리얼을 평가하는 비용에 의해서 숨겨지기 때문입니다. 만약 당신이 짧은 머터리얼 쉐이더로 렌더링하고 있다면, 높은 고정 비용뿐만 아니라 보간 비용 손실에 더 쉽게 노출될 것입니다.
Min-Spec:
이런 테스트들은 NVIDIA GTX 3070 에서 수행되었습니다, 이 스펙은 곧 다가올 미래의 AAA 게임의 최소사양 보다 더 높습니다. 특히, 이 테스트 케이스는 약 0.34ms의 고정비용이 있습니다. 그러나, 5년 전의 저가의 노트북 GPU는 꽤 비쌀 것입니다. Visibility는 미래의 더 강력한 GPU에 대해서 확장성이 좋습니다, 그러나 동일한 이유로 오랫동안 최소사양에 있을 과거의 GPU에 대해서는 확장성이 좋지 않습니다.
Resolution Upscaling:
두 번째 고려사항은 NVIDIA’s DLSS [10], AMD’s Super Resolution [1], 그리고 언리얼의 Temporal Super Resolution [6] 와 같은 업스케일링 알고리즘의 역할입니다. 물론 의견은 다릅니다만 만약 내가 축소된 쉐이더를 사용한 4k 그대로의 해상도와 높은 수준의 쉐이딩으로 업스케일링된 정말 좋은 1080p 이미지 중 선택할 수 있다면, 나는 업스케일 된 1080p를 선택할 것입니다. 그러나 만약 당신이 삼각형 당 10픽셀을 사용하는 4k 이미지를 대상으로 한다고 가정하고 당신이 1080p로 전환한다고 가정해봅시다. 좋아요, 갑자기 당신의 10 픽셀의 삼각형이 2.5 픽셀의 삼각형이 됩니다. 다른 말로 하면, 만약 우리가 PS4/XB1에서 삼각형의 수 최대한으로 사용하고 해상도를 1080p 프레임 버퍼로 목표한다면, 우리는 PS5/XSX에 아주 작은 삼각형들이 많이 생길 것입니다.
Quad Utilization Matters:
실제로, 여기서의 중요한 결론은 Quad 활용도가 중요하다는 것입니다. 그것은 실제로 오버드로우와 같은 비용입니다! 우리는 1 픽셀 삼각형이 좋지 않다는 것을 알고 있습니다만 10 픽셀의 삼각형이 이상적이지도 않다고 알고 있습니다. 4x는 2x보다 더 나쁩니다, 그러나 2x 또한 1x 보다 더 나쁩니다. Quad 활용도는 먼 미래의 문제가 아닙니다. 이것은 오늘날 실제로 출시하는 게임의 작업부하(workload)에 대한 실제 문제입니다. 그러나 만약 우리가 quad 활용도를 처리한다면, 우리의 실제로 우리의 렌더러를 최적화 하기 위해서 그리고 GPU 사이클을 helper lane 대신 관심 있는 효과를 구현하는 데 사용하는 수많은 여력을 가질 것입니다.
REFERENCES:
[1] AMD FidelityFX, Super Resolution. AMD Inc. (https://www.amd.com/en/technologies/radeon-software-fidelityfx-super-resolution)
[2] AbientCG, (https://www.ambientcg.com)
[3] The Visibility Buffer: A Cache-Friendly Approach to Deferred Shading. Christopher Burns and Warren Hunt. (http://jcgt.org/published/0002/02/04/)
[4] ConfettiFX/The-Forge. ConfettiFX. (https://github.com/ConfettiFX/The-Forge)
[5] 4K Rendering Breakthrough: The Filtered and Culled Visibility Buffer. Wolfgang Engel. (https://www.gdcvault.com/play/1023792/4K-Rendering-Breakthrough-The-Filtered)
[6] Unreal Engine 5 Early Access Release Notes. Epic Games, Inc. (https://docs.unrealengine.com/5.0/en-US/ReleaseNotes/)
[7] Nanite, Inside Unreal. Brian Karis, Chance Ivey, Galen Davis, and Victor Brodin. (https://www.youtube.com/watch?v=TMorJX3Nj6U)
[8] Sony Pictures Imageworks Arnold. Christopher Kulla, Alejandro Conty, Clifford Stein, and Larry Gritz. (https://dl.acm.org/doi/10.1145/3180495)
[9] HLSL Shader Model 6.0, Microsoft Inc. (https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/hlsl-shader-model-6-0-features-for-direct3d-12)
[10] NVIDIA DLSS. NVIDIA Inc. (https://www.nvidia.com/en-us/geforce/technologies/dlss/)
[11] Automatic Differentiation, C++ Template and Photogrammetry. Dan Piponi. (http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.89.7749&rep=rep1&type=pdf)
[12] Deferred Attribute Interpolation for Memory-Efficient Deferred Shading. Cristoph Schied and Carsten Dachsbacher. (http://cg.ivd.kit.edu/publications/2015/dais/DAIS.pdf)
'Graphics > 참고자료' 카테고리의 다른 글
| [번역] Monte Carlo Integration (2) | 2022.03.12 |
|---|---|
| [번역] Your Guide to Texture Compression in Unreal Engine (0) | 2021.10.29 |
| [번역] Temporal Anti-Aliasing(TAA) Tutorial (0) | 2021.06.24 |
| [번역] Graphics API abstraction – Wicked Engine Net (0) | 2021.05.15 |
| [번역] How to read shader assembly – Interplay of Light (0) | 2021.04.24 |