일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Wavefront
- SGPR
- hzb
- wave
- texture
- DirectX12
- optimization
- Study
- vulkan
- shader
- Nanite
- ue4
- 번역
- DX12
- Shadow
- rendering
- scattering
- scalar
- VGPR
- GPU Driven Rendering
- SIMD
- Graphics
- atmospheric
- GPU
- deferred
- unrealengine
- UE5
- ShadowMap
- RayTracing
- forward
- Today
- Total
RenderLog
[번역] Temporal Anti-Aliasing(TAA) Tutorial 본문
개인 공부용으로 번역한 거라 잘못 번역된 내용이 있을 수 있습니다.
또한 원작자의 동의 없이 올려서 언제든 글이 내려갈 수 있습니다.
출처 : https://sugulee.wordpress.com/2021/06/21/temporal-anti-aliasingtaa-tutorial/
Temporal Anti-Aliasing(TAA) Tutorial
Posted on June 21, 2021 by Sugu Lee
Overview
이 예제에서, Temporal Anti-Aliasing(TAA) 기술의 구현을 함께 해볼 것입니다.
TAA는 앨리어싱을 줄이기 위한 렌더링 기술입니다. multi-sampled anti-aliasing(MSAA)같은 많은 다른 기술들이 같은 이 같은 목적으로 개발되었습니다.
TAA의 구현에 들어가기 전에, 우리가 해결하고자 하는 문제를 먼저 봅시다.
Aliasing Artifacts
여기에 앨리어싱 artifact의 예제가 있습니다.
이 그림으로 부터 바로 문제를 알아채진 못할 것입니다. 이 그림에서, 앨리어싱 artifact는 3d 모델의 가장자리를 따라 발생합니다. 가장자리에서는 모델의 실루엣이 깨끗하고 부드럽기보다 지글(jagged) 거립니다. artifact를 더 확실히 보기 위해서 그림에서 두 영역을 확대해봅시다.
확대된 이미지에서, 꽤 보기 흉하고 지글거리는 실루엣을 확실히 볼 수 있습니다. TAA는 이러한 흉한 것을 없애고 상당히 발전시킬 수 있습니다.
What causes Aliasing Artifacts?
왜 이런 artifact가 발생할까요? 폴리곤의 가장자리선을 정확히 재생성하기 위해서 픽셀의 사이즈가 너무 크기 때문입니다. 아래 이미지의 렌더링된 파란색 삼각형을 보세요.
위의 이미지 오른쪽에 있는 까만 점선은 실제/이상적인 삼각형의 가장자리 선입니다. 이미지의 오른쪽 면의 각 셀(Cell)은 한개의 픽셀을 나타냅니다. 픽셀의 중심에 삼각형의 경계가 들어오면 그 픽셀은 파란색으로 칠해집니다, 만약 그렇지 않다면 흰색으로 칠해집니다. 이것은 실제 삼각형 실루엣을 재현하는데 실패했습니다. 왜냐하면 픽셀의 크기가 너무 크기 때문입니다.
우리는 이 문제를 2개의 해결법으로 풀수 있습니다.
첫 번째는 픽셀의 크기를 줄여서 픽셀의 밀도를 높이는 것입니다. 픽셀의 크기가 계속 작아지면, artifact는 줄어듭니다. 그래픽스 엔지니어의 관점에서, 이것은 아주 구현하기 쉽습니다. 그냥 프레임 버퍼의 해상도를 높이면 됩니다! 그러나, 이런 접근은 소프트웨어나 하드웨어 관점 둘 다에서 비용이 큽니다. 소프트웨어 관점에서, 앱은 더 많은 메모리를 증가한 픽셀 데이터를 저장하는 데 사용하고 더 많은 프로세서 파워를 증가한 픽셀의 수를 처리하는 데 사용합니다. 하드웨어의 관점에서, 더 높은 해상도를 지원하는 비디오 카드와 디스플레이가 필요할 것입니다. 그래서 이런 접근법은 바람직하지 않습니다.
두 번째는 픽셀 크기는 그대로 두고, “어떻게든” 앨리어싱이 덜하게 보이게 하는 것입니다. 그리고 이것이 TAA가 하는 것입니다(그리고 대부분의 AA 기술 또한 하는 것입니다).
How to reduce Aliasing Artifacts without increasing pixel density?
어떻게 앨리어싱이 덜 하게 보이게 만들 수 있을까요? 우리는 픽셀이 삼각형에 의해 얼마나 많이 가려지는 지를 기반으로 조정된 백그라운드와 삼각형 컬러를 사용하여 만들 수 있습니다. 아래의 이미지를 봅시다.
이전과 같이, 점선은 실제 파란색 삼각형의 가장자리입니다. 이전 이미지와 다르게, 이번에는, 셀이 삼각형에 의해서 얼마나 가려지는지에 따라 다양한 파란색 음영으로 색칠되었습니다.
H1, F6처럼 완전히 삼각형 내에 들어와있는 픽셀은 100% 파란색입니다. G1, E5 같이 반 정도 삼각형 내에 들어온 픽셀은 50% 파란색입니다. F2, D6처럼 10% 정도만 삼각형 내에 들어온 픽셀은 10% 파란색입니다.
이게 여전히 정확한 실제 삼각형 가장자리 선을 재현한 건 아니지만, 인간의 눈으로 볼 때 가장자리 선은 꽤 실제 가장자리 선처럼 보입니다.
아래 이미지는 TAA가 적용된 것과 적용되지 않은 것을 비교하여 어떻게 다른지를 보여줍니다.
TAA Implementation
좋아요. 문제는 충분히 봤습니다, 이제 TAA의 구현을 어떻게 하는지 이야기해봅시다. 나는 먼저 정적인 장면에 대해 TAA의 구현을 설명할 것입니다. 그러고 나서 동적인 장면에 대한 TAA 구현으로 넘어갈 것입니다. 동적인 장면이 더 재미있을 것입니다.
The pipeline for static scene
정적 장면을 위헌 TAA의 파이프라인은 여기 있습니다.
Step 1 – Render Scene With Jittering
TAA의 첫 번째 단계는 약간 흔들린(Jiterring) 상태로 장면을 렌더링 하는 것입니다. 이 단계에서 장면을 렌더링 하는 것은 보통의 장면을 렌더링하는 방법과 거의 일치합니다. 차이점은 장면이 다양한 특정 방향/거리로 약간 이동하는 것입니다, 다른 말로, 지글거리게 한 것(jitered)입니다.
이것을 위해, 버택스 쉐이더가 살짝 수정됩니다. 버택스 쉐이더에서, clip space 이후의 위치가 계산됩니다, 약간 흔들린 오프셋(jitter offset)은 clip space의 위치에 더해집니다.
float4 worldPos = modelMatrix * float4(in.position, 1.0);
float4 clipPos = viewportParams.viewProjectionMatrix * worldPos;
clipPos += viewportParams.jitter*clipPos.w; // Apply Jittering out.position = clipPos;
각 프레임은 이전 프레임의 것이 아닌 다른 흔들린 오프셋을 사용합니다. 그리고 한 프레임 안에서, 전체 장면은 같은 흔들린 오프셋을 사용하여 렌더링 됩니다.
흔들린 오프셋에 관해 말하자면, 많은 선택지가 있습니다. 이 튜토리얼에서 사용하려는 것 중 하나는 Halton sequence라 불리는 것입니다. 아래는 Halton sequence로 생성된 16개의 흔들린 오프셋입니다.
[00] x 0.500000 y 0.333333
[01] x 0.250000 y 0.666667
[02] x 0.750000 y 0.111111
[03] x 0.125000 y 0.444444
[04] x 0.625000 y 0.777778
[05] x 0.375000 y 0.222222
[06] x 0.875000 y 0.555556
[07] x 0.062500 y 0.888889
[08] x 0.562500 y 0.037037
[09] x 0.312500 y 0.370370
[10] x 0.812500 y 0.703704
[11] x 0.187500 y 0.148148
[12] x 0.687500 y 0.481481
[13] x 0.437500 y 0.814815
[14] x 0.937500 y 0.259259
[15] x 0.031250 y 0.592593
그래프에 그려보면, 오프셋은 아래와 같이 보입니다.
Halton sequence로부터의 숫자의 범위는 0-1 사이입니다. 이 숫자를 흔들어진 오프셋으로 사용하기 위해서, 우리는 범위를 양수와 음수 두 방향으로 흔들고 픽셀의 크기보다 더 많이 흔들리지 않도록 조정해야 합니다. 우리는 아래의 코드로 이 범위를 바꾸도록 할 수 있습니다.
// _viewSize is the resolution of the render target.
offset.x = ((offset.x-0.5f) / _viewSize.x) *2;
offset.y = ((offset.y-0.5f) / _viewSize.y) *2;
1600x1200 렌더타겟에 대해, 우리는 아래처럼 최종 흔들린 오프셋을 얻을 수 있습니다.
[00] x 0.000000 y -0.000278
[01] x -0.000312 y 0.000278
[02] x 0.000312 y -0.000648
[03] x -0.000469 y -0.000093
[04] x 0.000156 y 0.000463
[05] x -0.000156 y -0.000463
[06] x 0.000469 y 0.000093
[07] x -0.000547 y 0.000648
[08] x 0.000078 y -0.000772
[09] x -0.000234 y -0.000216
[10] x 0.000391 y 0.000340
[11] x -0.000391 y -0.000586
[12] x 0.000234 y -0.000031
[13] x -0.000078 y 0.000525
[14] x 0.000547 y -0.000401
[15] x -0.000586 y 0.000154
N 프레임은 N%16 오프셋을 사용하여 렌더링 됩니다.
아래의 비디오는 각 프레임을 위한 이 렌더패스의 결과를 보여줍니다. 오른쪽 상단 코너 영역의 이미지는 나뭇잎이 위치한 영역을 확대된 이미지입니다.
유튜브에 의해 필터링과 포스트프로세스 때문에, 비디오는 실제 렌더링과는 약간 다를 수 있습니다. 그러나, 이것은 여전히 흔들린 움직임을 보여줍니다.
이 단계의 색상 출력은 ‘current frame color’라 불리는 중간 렌더타겟에 저장됩니다.
Step 2 – Resolve
이제 우리는 흔들린 장면을 렌더링 하여 ‘current frame color’ 텍스쳐를 갖고 있습니다, 우리는 현재 프레임에 대한 안티 앨리어싱 된 이미지를 생성하기 의해서 이 텍스쳐와 ‘History’라는 다른 텍스쳐를 사용합니다. History 텍스쳐는 간단히 말해서 이전 프레임으로부터 Resolve 단계의 출력입니다.
이 단계에서, 우리는 전체화면을 덮는 사각형을 렌더링 합니다. 픽셀쉐이더에서, 우리는 현재 텍스쳐와 History 텍스쳐를 현재 UV위치로 샘플링 하므로써 현재 컬러와 history 컬러를 읽습니다. 그런뒤, 우리는 두 색상을 혼합하여 새 색상을 생성합니다. 아주 간단하죠. 픽셀쉐이더 코드를 봐주세요.
fragment float4 TAA_ResolveFragment(quadVertexOut in [[stage_in]],
texture2d<float> currentFrameColorBuffer [[texture(0)]],
texture2d<float> historyBuffer [[texture(1)]])
{
constexpr sampler sam_point(min_filter::nearest, mag_filter::nearest, mip_filter::none);
constexpr sampler sam_linear(min_filter::linear, mag_filter::linear, mip_filter::none);
float3 currentColor = currentFrameColorBuffer.sample(sam_point, in.uv).xyz;
float3 historyColor = historyBuffer.sample(sam_linear, in.uv).xyz;
float modulationFactor = 0.9;
float3 color = mix(currentColor, historyColor, modulationFactor);
return float4(color, 1.0f);
}
현재 프레임 컬러와 History 컬러를 샘플링 하기 위해서 서로 다른 필터링을 사용한 것을 염두해주세요. 현재 프레임 컬러에 대해서는, nearest 필터링을 사용하는 반면, history 컬러를 위해서는, linear 필터링이 사용됩니다.
와~, 믿을 수 없는 개선을 하는 정말 간단한 쉐이더입니다!
Step 3 – Update history
이 단계에서, 우리는 resolve 단계의 출력으로 history 텍스쳐의 갱신이 필요합니다. 이것을 위해, 우리는 resolve 단계의 렌더타겟 텍스쳐를 간단히 복사해 history 텍스쳐에 복사합니다.
Step 4 – Render to frame
이 단계는 우리가 resolve 단계의 결과를 최종 프레임 버퍼에 복사하는 것을 제외하면 단계3과 유사합니다. 그리고, 이것은 정적 장면을 위한 TAA입이다. 많이 복잡하진 않죠?
아래의 비디오는 TAA가 정적 장면에서 동작하는 것을, 프레임별로, 보여주고 있습니다. 또다시 고맙게도 유투브의 놀라운 필터링과 포스트프로세싱으로 비디오는 실제 렌더링과 일치하지 않습니다. 다행스럽게도, TAA 효과는 여전히 볼 수 있습니다.
이제 우리는 어떻게 TAA를 정적 장면에서 구현하는지 배웠습니다, 이걸 더 유용하게 그리고 동적 장면을 위해 구현해봅시다.
The pipeline for dynamic scene
동적 장면을 위한 파이프라인은 몇 가지 작은 변화들을 제외하고는 대부분 같습니다.
TAA를 동적 장면에 사용하기 위해서, 우리는 픽셀당 ‘속도(Velocity)’ 버퍼라 불리는 또 다른 데이터를 생성해야 합니다. 이 데이터는 이전 프레임의 픽셀의 위치를 계산할 수 있게 해 줍니다.
Step 1 – Rendering scene with Jittering
이번 단계는 단계1에서 정적 장면을 위해 했던 모든 것을 합니다. 거기에 더해, ‘속도’ 텍스쳐라 불리는 중간 단계 텍스쳐도 생성합니다. 비록 ‘속도’라 불리지만, 텍스쳐에 저장된 값은 실제로 속도가 아닙니다. 속도 텍스쳐에 저장된 텍셀은 각 픽셀의 현재 프레임 위치와 이전 프레임 위치의 차이입니다.
왜 이 데이터가 필요할까요? 이유는 장면이 각 프레임별로 이동하기 때문입니다. 단계2의 컬러 resolve 에서, 우리는 히스토리 텍스쳐로부터 이전 프레임 픽셀의 컬러를 얻어와야 합니다. 왜냐하면, 우리는 이전 프레임이 픽셀이 어디에 있었는지 알아야 하기 때문입니다. 우리는 이 속도 데이터를 픽셀의 이전 위치를 계산하는 데 사용합니다.
아래의 이미지의 예를 보세요.
속도를 쉐이더에서 어떻게 계산하는지 봅시다.
우리는 이전 프레임의 view/projection 매트릭스가 필요합니다. 뿐만 아니라, 이전 프레임으로부터 각 모델의 world 매트릭스도 필요합니다. 만약 당신이 스키닝 되는 모델이 있다면, 또한 이전 프레임의 bone 에서 world로의 변환 매트릭스도 필요합니다. 이 튜토리얼에서는, 스키닝 오브젝트를 렌더링 하지 않습니다.
이전 프레임의 World, view, projection 매트릭스를 가지고, 우리는 아래의 버택스 쉐이더처럼 Clip space에서의 이전 프레임의 버택스의 위치를 계산합니다. 이 계산을 ‘reprojection’ 이라 부릅니다.
float4x4 prevFrame_modelMatrix = actorParams.prevModelMatrix;
float4 prevFrame_worldPos = prevFrame_modelMatrix * float4(in.position, 1.0);
float4 prevFrame_clipPos = viewportParams.prevViewProjMatrix * prevFrame_worldPos;
out.prevFramePosition = prevFrame_clipPos;
그밖에, 우리는 현재 프레임에 대한 흔들리지 않은 버택스 위치를 계산합니다. 그래서, 최종 버택스 쉐이더 코드는 이것과 같습니다.
float4x4 currentFrame_modelMatrix = actorParams.modelMatrix;
float4 currentFrame_worldPos = currentFrame_modelMatrix * float4(in.position, 1.0);
float4 currentFrame_clipPos = viewportParams.viewProjectionMatrix * currentFrame_worldPos;
out.currentFramePosition = currentFrame_clipPos;
float4 currentFrame_clipPos_jittered =
currentFrame_clipPos + float4(viewportParams.jitter*currentFrame_clipPos.w,0,0);
out.position = currentFrame_clipPos_jittered;
float4x4 prevFrame_modelMatrix = actorParams.prevModelMatrix;
float4 prevFrame_worldPos = prevFrame_modelMatrix * float4(in.position, 1.0);
float4 prevFrame_clipPos = viewportParams.prevViewProjMatrix * prevFrame_worldPos;
out.prevFramePosition = prevFrame_clipPos;
우리는 흔들리지 않은 위치로 속도를 계산합니다.
각 버택스들에 대해 현재 프레임의 위치와 이전 프레임의 위치를 생상하는 버택스 쉐이더를 가지고 있기 때문에, 우리는 픽셀 쉐이더에서 위치 정보를 픽셀당 속도 데이터를 생성하는 데 사용할 수 있습니다.
float2 CalcVelocity(float4 newPos, float4 oldPos, float2 viewSize)
{
oldPos /= oldPos.w;
oldPos.xy = (oldPos.xy+1)/2.0f;
oldPos.y = 1 - oldPos.y;
newPos /= newPos.w;
newPos.xy = (newPos.xy+1)/2.0f;
newPos.y = 1 - newPos.y;
return (newPos - oldPos).xy;
}
알아둘 점은 newPos 와 oldPos는 먼저 NDC space 에서 Texture coordinate space로 변환되어야 한다는 것입니다. 이것은 속도 데이터를 텍스쳐 샘플링 위치를 계산하는 데 사용하기 위해서입니다. 코드에서 보듯이, 계산 결과는 실제 속도가 아닙니다. 왜냐하면 거리를 차이를 시간으로 나누지 않았기 때문입니다. 단지 거리 차이를 계산하고 속도 텍스쳐에 저장합니다.
Step 2 – Resolve
단계2는 속도 텍스쳐를 히스토리 텍스쳐를 샘플링하는 좌표를 계산하는 데 사용하는 것을 제외하면, 단계1의 정적 장면과 아주 유사합니다. 이것은 아주 간단합니다. 코드를 봅시다.
fragment float4 TAA_ResolveFragment(quadVertexOut in [[stage_in]],
texture2d<float> currentFrameColorBuffer [[texture(0)]],
texture2d<float> historyBuffer [[texture(1)]],
texture2d<float> velocityBuffer [[texture(2)]])
{
constexpr sampler sam_point(min_filter::nearest, mag_filter::nearest, mip_filter::none);
constexpr sampler sam_linear(min_filter::linear, mag_filter::linear, mip_filter::none);
float2 velocity_sample_pos = in.uv;
float2 velocity = velocityBuffer.sample(sam_point, velocity_sample_pos).xy;
float2 prevousPixelPos = in.uv - velocity;
float3 currentColor = currentFrameColorBuffer.sample(sam_point, in.uv).xyz;
float3 historyColor = historyBuffer.sample(sam_linear, prevousPixelPos).xyz;
float modulationFactor = 0.9;
float3 color = mix(currentColor, historyColor, modulationFactor);
return float4(color, 1.0f);
}
Step 3 and 4
단계3과 4는 정적 장면과 정확히 일치합니다.
여기에 TAA를 동적 장면에 적용하면 어떻게 보이는지가 있습니다.
흠.. 좋아요, 앨리어싱 artifact를 줄이는데 관해서는 작동하는 것처럼 보입니다. 그러나, 동시에, 여러 고스팅 artifact를 유발합니다. 그래서 모델의 이미지가 크게 흐려집니다.
우리는 이런 불편한 부작용을 히스토리 컬러의 Clamping을 적용하여 개선할 수 있습니다. 아래 코드와 같이 개선함.
fragment float4 TAA_ResolveFragment(quadVertexOut in [[stage_in]],
texture2d<float> currentFrameColorBuffer [[texture(0)]],
texture2d<float> historyBuffer [[texture(1)]],
texture2d<float> velocityBuffer [[texture(2)]])
{
constexpr sampler sam_point(min_filter::nearest, mag_filter::nearest, mip_filter::none);
constexpr sampler sam_linear(min_filter::linear, mag_filter::linear, mip_filter::none);
float2 velocity_sample_pos = in.uv;
float2 velocity = velocityBuffer.sample(sam_point, velocity_sample_pos).xy;
float2 prevousPixelPos = in.uv - velocity;
float3 currentColor = currentFrameColorBuffer.sample(sam_point, in.uv).xyz;
float3 historyColor = historyBuffer.sample(sam_linear, prevousPixelPos).xyz;
// Apply clamping on the history color.
float3 NearColor0 = currentFrameColorBuffer.sample(sam_point, in.uv, int2(1, 0)).xyz;
float3 NearColor1 = currentFrameColorBuffer.sample(sam_point, in.uv, int2(0, 1)).xyz;
float3 NearColor2 = currentFrameColorBuffer.sample(sam_point, in.uv, int2(-1, 0)).xyz;
float3 NearColor3 = currentFrameColorBuffer.sample(sam_point, in.uv, int2(0, -1)).xyz;
float3 BoxMin = min(currentColor, min(NearColor0, min(NearColor1, min(NearColor2, NearColor3))));
float3 BoxMax = max(currentColor, max(NearColor0, max(NearColor1, max(NearColor2, NearColor3))));;
historyColor = clamp(historyColor, BoxMin, BoxMax);
float modulationFactor = 0.9;
float3 color = mix(currentColor, historyColor, modulationFactor);
return float4(color, 1.0f);
}
그것이 궁극적으로 하는 것은 만약 히스토리 컬러가 현재 픽셀의 컬러 또는 현재 픽셀의 바로 이웃 컬러와 비교해서 너무 다르면, clamp 합니다. 히스토리 컬러가 더 이상 적절하지 않을 확률이 높이 때문입니다.
이런 약간의 픽셀 쉐이더의 변화로, 결과는 훨씬 좋게 보입니다.
Wrapping up!
지금까지 TAA을 정적 그리고 동적 장면에 대해 어떻게 구현하는지 알아봤습니다. 내가 알기로, TAA는 많은 그래픽스 엔지니어가 여전히 활발히 개선하고 있는 분야입니다. TAA는 특정 경우에 잘 작동합니다만 특정 상황에서는 그렇지 않습니다. 내가 여기서 소개한 구현은 가장 기본적인 접근입니다. 만약 관심이 있다면, 당신의 TAA 렌더러를 더 좋게 만들 수 있도록 해주는 많은 출판물들이 웹에 있습니다!
이 튜토리얼의 샘플 어플리케이션을 위한 전체 코드는 GitHub 이 링크 여기서 받을 수 있습니다!
(샘플 앱은 Objective-C와 Metal API를 사용하여 Mac OS-X 를 위해서 만들어졌습니다.)
이 튜토리얼을 읽어주셔서 감사하고 재미있었으면 좋겠습니다.
'Graphics > 참고자료' 카테고리의 다른 글
[번역] Your Guide to Texture Compression in Unreal Engine (0) | 2021.10.29 |
---|---|
[번역] Visibility Buffer Rendering with Material Graphs – Filmic Worlds (2) | 2021.10.15 |
[번역] Graphics API abstraction – Wicked Engine Net (0) | 2021.05.15 |
[번역] How to read shader assembly – Interplay of Light (0) | 2021.04.24 |
[번역] Implementing FXAA (0) | 2021.02.17 |