개인 공부용으로 번역한 거라 잘못 번역된 내용이 있을 수 있습니다.
또한 원작자의 동의 없이 올려서 언제든 글이 내려갈 수 있습니다.
출처 : blog.simonrodriguez.fr/articles/30-07-2016_implementing_fxaa.html
Implementing FXAA
30/07/2016 - Simon Rodriguez
컴퓨터 스크린의 3D 장면을 그릴 때, 앨리어싱이 일어날 수 있습니다. 각각의 픽셀은 화면 영역을 덮는 특정 오브젝트에 속하며, 유니크 컬러를 받기 때문에. jagged와 saw-toothed 효과가 오브젝트의 가장자리에 보일 수 있습니다. 같은 효과가 얇은 선에도 보일 수 있습니다(철망)
Fighting aliasing
여러 안티-앨리어싱 기술이 이런 종류의 시각적 Artifact를 완화시키기 위해서 개발되었습니다. 몇몇 기술은 Super-sampled anti-aliasing (SSAA) 처럼, 다운 샘플링하고 결과를 화면에 표시하기 전에 장면으로부터 더 세부사항을 얻기 위해서 더 큰 그림을 그리는 것에 의존합니다. 성능과 메모리 비용을 줄이기 위해서 여러 개량된 방식이 개발되었습니다만, 여전히 몇몇 제약에 갇혀있습니다. Multi-sampled anti-aliasing (MSAA)이 그중 하나입니다, OpenGL에서 이것을 활성화하는 것은 아주 쉽지만 현대의 디퍼드 렌더링 파이프라인에서 사용하기 어렵습니다(이곳에서는 라이팅 계산이 특정 렌더링 패스에서 수행됩니다).
다른 기술들은 이전 프레임의 정보를 사용하여 현재 프레임의 품질을 강화합니다, 이러한 종류의 알고리즘을 Temporal anti-aliasing 라 부릅니다. 몇몇 다른 방법들은 최종 렌더링 결과에 포스트프로세스 효과를 적용한 것입니다. 그중 Subpixel morphological anti-aliasing (SMAA)는 가장 최신 기술 중 하나입니다, 그러나 구현이 아주 복잡합니다. 더 간단하지만 여전히 극도로 효율적인 알고리즘이 Timothy Lottes from Nvidia in 2009 에서 소개되었고 여러 게임들에 적용되었습니다: Fast approximate anti-aliasing, FXAA.
Enters FXAA
FXAA는 기존 렌더러에 간단하게 추가할 수 있습니다: 최종 렌더링 패스[1]에 적용됩니다. 이 패스는 렌더링 된 이미지를 입력으로 사용하고, 출력이 안티-앨리어싱 버젼입니다. 핵심 아이디어는 렌더링 된 그림에 가장자리를 검출하고 부드럽게 만드는 것입니다. 이 방법은 빠르고 효과적이지만 텍스쳐의 디테일을 흐리게 할 수 있습니다. 단계별로 알고리즘[2]을 설명할 것입니다, 먼저, 예제입니다.
그리고 확대한 것입니다: 안티-앨리어싱이 켜진 때는, 모든 가장자리가 약간의 텍스쳐 디테일과 함께 매끄럽습니다(특히 용의 피부에).
Prerequisites
설명을 위해서, 전체 장면이 창의 해상도와 동일한 텍스쳐 이미지에 렌더링 된다고 가정합니다. 그런뒤 전체 창을 덮는 사각형이 이 텍스쳐를 표시하도록 그려집니다, FXAA 알고리즘은 프래그먼트 쉐이더 라고 불리는 곳에서 실행됩니다, 작은 프로그램이 각 팍셀에 대해서 GPU에서 실행됩니다.
Luma
FXAA 쉐이더에서 계산의 대부분은 픽셀의 Luminosity를 텍스쳐로 부터 읽는데 의존할 것입니다. 그것은 0과 1 사이의 Grey levels로 표현됩니다. luma를 사용하기 위해서, 공식 L = 0.299 * R + 0.587 * G + 0.114 * B 를 정의합니다.
이것은 Red, Green 그리고 Blue 컴포넌트의 가중합(Weighted sum) 입니다. 그것은 우리 눈의 각 파장 범위에 대한 민감도를 고려합니다. 게다가 우리는 그 값을 지각(Perceptual) 공간(선형이 아닌)에서 사용하고, 제곱근으로 Inverse gamma transformation[3]을 근사합니다. 따라서 아래의 유틸리티 함수가 쉐이더에 정의됩니다.
float rgb2luma(vec3 rgb){
return sqrt(dot(rgb, vec3(0.299, 0.587, 0.114)));
}
(역주 : Luma가 공식 단어인지는 모르겠는데, Luma 는 감마 컬렉션이 적용된 Luminance라고 하네요. 사실 맞는 내용인지 모르겠지만 rgb2luma의 식이 그런 식이고 아래 첨부한 추가 슬라이드에도 나와있어서 이 글에서는 그렇게 이해하고 넘어가면 좋을 것 같습니다.)
Texture filtering
OpenGL 에서 텍스쳐를 읽기 위해서, 우리는 보통 [0,1] 범위의 부동소수점으로 된 UV 좌표계를 사용합니다. 그러나 텍스쳐는 각 차원마다 유한한 수의 픽셀로 구성되어있습니다, 이들 각각은 상수 색상을 가집니다; 만약 UV 좌표가 두 개의 픽셀 사이에 있다면 어떤 색상을 읽으려고 할까요? 이것을 처리하기 위해서 2가지 주요 방법이 있습니다.
- 가장 가까운 픽셀로 결정하고, 그 컬러를 사용. 이것은 Nearest neighbor 알고리즘입니다, 아래 그림의 왼쪽에 있습니다.
- 가장 가까운 4개의 픽셀 컬러를 각각의 거리에 따라서 가중(weighted)하여 선형 보간. 이것이 Bilinear filtering 입니다, 오른쪽에 있습니다.
알아둬야 할 점은, 같은 크기의 창에 렌더링된 장면 텍스쳐를 표시할 때, 여기서 사용된 필터링 방식은 모양을 수정하지 않습니다[4]. FXAA 계산중에 읽은 값에만 영향을 미치고, 비용이 들지 않는 보간을 얻기 위해서 Bilinear filtering를 선택합니다.
(역주 : Bilinear Filter에 추가비용이 들지 않는다고 하네요. 출처 : forum.beyond3d.com/threads/is-bilinear-filtering-free.43256/#:~:text=Yes%20bilinear%20filtering%20is%20efectively,t%20trash%20the%20texture%20cache.)
Step-by-step
입력은 screenTexture 텍스쳐, 프래그먼트의 UV 좌표 In.uv 그리고 창 크기의 역수 inverseScreenSize (1.0/width, 1.0/height) 입니다; 출력은 RGB 벡터 fragColor 입니다.
나는 간단한 예제로 아래의 이미지를 사용할 것입니다: 검은색과 하얀색, 8x5 픽셀 그리드, 가장자리에 Clamp됨[5] (역주 : UV가 0.0~1.0 사이로 고정되었다는 뜻); 우리는 빨간색으로 강조된 픽셀에 중점을 둘 것입니다.
Detecting where to apply AA
첫째로, 가장자리를 찾아야 합니다: 이걸 하기 위해서, 현재 프래그먼트와 그것의 4방향 이웃에 있는 lumas를 계산합니다. 최소와 최대 lumas가 추출되고, 이 둘의 차이점은 로컬 대비(Local contrast) 값을 줍니다. 대비는 가장자리를 따라서 강합니다, 왜냐하면 큰 색의 차이가 있기 때문입니다. 그래서 만약 대비가 최대 luma 값에 비례하는 임계값(threshold) 보다 낮다면, 안티-앨리어싱은 수행되지 않을 것입니다. 게다가 어두운 영역의 앨리어싱은 덜 보일 것입니다, 그래서 만약 대비가 절대 임계값보다 낮다면, 우리는 또한 안티-앨리어싱을 수행하지 않습니다. 이러한 경우, 현재 픽셀에서 텍스쳐로부터 읽은 색상이 출력입니다.
vec3 colorCenter = texture(screenTexture,In.uv).rgb;
// Luma at the current fragment
float lumaCenter = rgb2luma(colorCenter);
// Luma at the four direct neighbours of the current fragment.
float lumaDown = rgb2luma(textureOffset(screenTexture,In.uv,ivec2(0,-1)).rgb);
float lumaUp = rgb2luma(textureOffset(screenTexture,In.uv,ivec2(0,1)).rgb);
float lumaLeft = rgb2luma(textureOffset(screenTexture,In.uv,ivec2(-1,0)).rgb);
float lumaRight = rgb2luma(textureOffset(screenTexture,In.uv,ivec2(1,0)).rgb);
// Find the maximum and minimum luma around the current fragment.
float lumaMin = min(lumaCenter,min(min(lumaDown,lumaUp),min(lumaLeft,lumaRight)));
float lumaMax = max(lumaCenter,max(max(lumaDown,lumaUp),max(lumaLeft,lumaRight)));
// delta 계산
float lumaRange = lumaMax - lumaMin;
// 만약 luma 차이가 임계값보다 작다면(또는 우리가 정말 어두운 영역내에 있다면),
// 우리는 가장자리에 있지 않는 것이므로, AA를 수행하지 않는다.
if(lumaRange < max(EDGE_THRESHOLD_MIN,lumaMax*EDGE_THRESHOLD_MAX)){
fragColor = colorCenter;
return;
}
두 개의 대문자 상수에 대한 권장 값은 EDGE_THRESHOLD_MIN = 0.0312 와 EDGE_THRESHOLD_MAX = 0.125입니다.
우리의 예제 픽셀에, 최소는 0, 최대는 1, 이라 범위가 1입니다, 그리고 1.0 > max(1*0.125,0.0312) 일 때, 우리는 AA를 수행합니다.
Estimating gradient and choosing edge direction
그런 뒤 가장자리의 일부로 탐지된 픽셀에 대해서, 우리는 가장자리가 수평인지 수직인지를 확인합니다. 이것을 위해, 중심의 luma와 8개의 이웃을 아래의 공식을 사용하여 수평과 수직방향으로 일련의 Local delta 값을 계산하기 위해 사용합니다:
- 수평: |(upleft - left) - (left - downleft)| + 2 * |(up - center) - (center - down)| + |(upright - right) - (right - downright)|
- 수직: |(upright - up) - (up - upleft)| + 2 * |(right - center) - (center - left)| + |(downright - down) - (down - downleft)|
두 개의 양 중에 더 큰 것이 가장자리의 주요 방향입니다.
// 남은 4개의 코너 lumas 를 얻음
float lumaDownLeft = rgb2luma(textureOffset(screenTexture,In.uv,ivec2(-1,-1)).rgb);
float lumaUpRight = rgb2luma(textureOffset(screenTexture,In.uv,ivec2(1,1)).rgb);
float lumaUpLeft = rgb2luma(textureOffset(screenTexture,In.uv,ivec2(-1,1)).rgb);
float lumaDownRight = rgb2luma(textureOffset(screenTexture,In.uv,ivec2(1,-1)).rgb);
4 개의 가장자리의 lumas를 조합 (같은 값으로 추후 계산을 위해 중간 변수로 사용)
float lumaDownUp = lumaDown + lumaUp;
float lumaLeftRight = lumaLeft + lumaRight;
// 코너도 동일
float lumaLeftCorners = lumaDownLeft + lumaUpLeft;
float lumaDownCorners = lumaDownLeft + lumaDownRight;
float lumaRightCorners = lumaDownRight + lumaUpRight;
float lumaUpCorners = lumaUpRight + lumaUpLeft;
// 수평과 수직 축을 따르는 변화도의 추정치 계산
float edgeHorizontal = abs(-2.0 * lumaLeft + lumaLeftCorners) + abs(-2.0 * lumaCenter + lumaDownUp ) * 2.0 + abs(-2.0 * lumaRight + lumaRightCorners);
float edgeVertical = abs(-2.0 * lumaUp + lumaUpCorners) + abs(-2.0 * lumaCenter + lumaLeftRight) * 2.0 + abs(-2.0 * lumaDown + lumaDownCorners);
// 로컬 가장자리가 수평인가? 수직인가?
bool isHorizontal = (edgeHorizontal >= edgeVertical);
우리의 예제에서, 우리는 다음을 얻습니다:
- 수평 = |-2*0+0+1| + 2*|-2*0+0+1| + |-2*0+1+0| = 4
- 수직 = |-2*0+0+0| + 2*|-2*1+1+1| + |-2*0+0+0| = 0
그래서 가장자리는 수평입니다.
Choosing edge orientation
현재 픽셀 반드시 가장자리에 정확히 있지 않습니다. 다음 단계는 가장자리의 방향에 직교하는 방향이 실제 가장자리의 경계인지 판정합니다. 현재 픽셀의 각 측면의 변화도(Gradient)가 계산되며, 차이가 가장 가파른 곳이 아마도 가장자리 테두리에 있을 것입니다.
// 로컬 가장자리의 반대방향으로 2개의 이웃 텍셀을 선택함
float luma1 = isHorizontal ? lumaDown : lumaLeft;
float luma2 = isHorizontal ? lumaUp : lumaRight;
// 이 방향으로 변화도를 계산
float gradient1 = luma1 - lumaCenter;
float gradient2 = luma2 - lumaCenter;
// 어느 방향이 변화가 더 가파른가?
bool is1Steepest = abs(gradient1) >= abs(gradient2);
// 해당 방향의 변화도, 정규화
float gradientScaled = 0.25*max(abs(gradient1),abs(gradient2));
우리의 예에서, 우리는 gradient1 = 0 - 0 = 0 그리고 gradient2 = 1 - 0 = 1 입니다, 그래서 차이는 위쪽 이웃으로 더 강합니다, 그리고 gradientScaled = 0.25입니다.
마침내, 우리는 이 방향으로 반 픽셀 이동했습니다, 그리고 이 점의 평균 luma를 계산합니다.
// 가장자리의 방향에 따라서 스텝 크기(한 픽셀)를 선택
float stepLength = isHorizontal ? inverseScreenSize.y : inverseScreenSize.x;
// 올바른 방향의 평균 luma
float lumaLocalAverage = 0.0;
if(is1Steepest){
// 방향을 바꿈
stepLength = -stepLength;
lumaLocalAverage = 0.5*(luma1 + lumaCenter);
} else {
lumaLocalAverage = 0.5*(luma2 + lumaCenter);
}
// UV 를 올바른 방향으로 반픽셀 이동시킴
vec2 currentUv = In.uv;
if(isHorizontal){
currentUv.y += stepLength * 0.5;
} else {
currentUv.x += stepLength * 0.5;
}
우리의 픽셀에 대해서, 평균 로컬 luma는 0.5*(1+0) = 0.5 이며, Y축 양의 방향 0.5 만큼의 Offset이 적용됩니다.
First iteration exploration
다음 단계는 가장자리의 주축(Main axis)를 따라서 탐색하는 것입니다. 우리는 두 방향으로 한 픽셀 이동합니다, 새로운 좌표의 lumas를 얻고, 이전 단계의 평균 luma에 대해서 변화한 luma를 계산합니다. 만약 이 차이가 로컬 변화도(Gradient) 보다 큰 경우, 우리는 이 방향의 가장자리의 끝에 도달한 것이며 정지합니다. 그렇지 않다면, UV Offset을 한 픽셀씩 계속해서 증가시킵니다.
// 오른쪽 방향으로 offset (각 반복 스탭에 대해) 을 계산.
vec2 offset = isHorizontal ? vec2(inverseScreenSize.x,0.0) : vec2(0.0,inverseScreenSize.y);
// 가장자리의 각 측면을 직교하여 탐색하기 위해서 UV를 계산. QUALITY 는 스탭을 더 빠르게 함.
vec2 uv1 = currentUv - offset;
vec2 uv2 = currentUv + offset;
// 탐색하는 세그먼트의 양쪽 끝에서 lumas를 읽고, delta 를 계산하고 로컬 평균 luma에 기록
float lumaEnd1 = rgb2luma(texture(screenTexture,uv1).rgb);
float lumaEnd2 = rgb2luma(texture(screenTexture,uv2).rgb);
lumaEnd1 -= lumaLocalAverage;
lumaEnd2 -= lumaLocalAverage;
// 현재 끝점에서 luma delta가 로컬 변화도 보다 크면, 우리는 가장자리의 측면에 도달한 것임
bool reached1 = abs(lumaEnd1) >= gradientScaled;
bool reached2 = abs(lumaEnd2) >= gradientScaled;
bool reachedBoth = reached1 && reached2;
// 측면에 도달하지 못했다면, 우리는 계속해서 이 방향으로 탐색함.
if(!reached1){
uv1 -= offset;
}
if(!reached2){
uv2 += offset;
}
예제에서, 우리는 lumaEnd1 = 0.5 - 0.5 = lumaEnd2 = 0.0 < gradientScaled 를 얻습니다 (텍스쳐에서 읽을 때 bilinear 보간 때문에 luma는 0.5 입니다), 따라서 우리는 양쪽으로 반복합니다.
Iterating
우리는 가장자리 양쪽 끝에 도달할 때까지 또는 최대 반복 수(12)에 도달할 때까지 반복합니다. 더 빠르게 처리하기 위해서, 우리는 이동하는 픽셀의 양 QUALITY(i)을 5번의 반복 후에 증가시킵니다: 1.5, 2.0, 2.0, 2.0, 2.0, 4.0, 8.0.
// 만약 양 방향 모두 측면에 도달하지 않았다면, 계속해서 탐색
if(!reachedBoth){
for(int i = 2; i < ITERATIONS; i++){
// 필요하다면, 첫번재 방향의 luma를 읽음, delta 계산
if(!reached1){
lumaEnd1 = rgb2luma(texture(screenTexture, uv1).rgb);
lumaEnd1 = lumaEnd1 - lumaLocalAverage;
}
// 필요하다면, 반대 방향의 luma를 읽음, delta 계산
if(!reached2){
lumaEnd2 = rgb2luma(texture(screenTexture, uv2).rgb);
lumaEnd2 = lumaEnd2 - lumaLocalAverage;
}
// 만약 현재 끝점의 luma delta가 로컬 변화도 보다 크다면, 우리는 가장자리 측면에 도달한 것임
reached1 = abs(lumaEnd1) >= gradientScaled;
reached2 = abs(lumaEnd2) >= gradientScaled;
reachedBoth = reached1 && reached2;
// 만약 측면에 도달하지 않았다면, 우리는 이 방향으로 계속 탐색,
// 가변 품질로 진행 (역주 : 스탭을 반복에 따라 조정하겠다는 의미)
if(!reached1){
uv1 -= offset * QUALITY(i);
}
if(!reached2){
uv2 += offset * QUALITY(i);
}
// 두 측면에 도착했다면, 탐색을 중단
if(reachedBoth){ break;}
}
}
최상의 경우, lumaEnd1 와 lumaEnd2 는 가장자리의 양쪽 끝 사이의 delta 와 지역 평균 luma 를 포함합니다, 그리고 uv1 그리고 uv2 는 UV 좌표와 일치합니다.
예제에서, 우리는 lumaEnd1 = 1-0.5 = 0.5 >= gradientScaled 를 얻습니다, 그래서 우리는 왼쪽으로의 탐색을 중단할 수 있습니다. 오른쪽으로는 조건을 충족하기 위해서 2번 더 반복합니다.
Estimating offset
다음으로 우리는 도달한 두 방향에 대한 거리를 계산합니다, 그리고 가장 가까운 끝점을 찾습니다. 가장자리의 길이는 가장 가까운 끝점에 대한 거리의 비율로 정합니다.(The edge length is estimated, as is the ratio of the distance to the closest extremity over the edge length) 이것은 현재 픽셀이 가장자리의 중간이 있는지 또는 끝점에 가까이 있는지 여부를 알려줍니다. 끝점에 더 가까울수록, 적용되는 UV Offset이 커집니다. (역주 : 이 Offset은 0.5 픽셀에 Scale 되는 값)
// 양 끝 가장자리까지의 거리를 계산
float distance1 = isHorizontal ? (In.uv.x - uv1.x) : (In.uv.y - uv1.y);
float distance2 = isHorizontal ? (uv2.x - In.uv.x) : (uv2.y - In.uv.y);
// 어떤 방향의 가장자리의 끝이 더 가깝나?
bool isDirection1 = distance1 < distance2;
float distanceFinal = min(distance1, distance2);
// 가장자리의 길이
float edgeThickness = (distance1 + distance2);
// UV offset: 가장자리의 측면까지 가장 가까운 방향으로 읽음
float pixelOffset = -distanceFinal / edgeThickness + 0.5;
예제 픽셀에서, 우리는 distance1 = 2, distance2 = 4 을 얻습니다, 그래서 가장자리 끝은 왼쪽 방향 (direction1)으로 더 가깝습니다 그리고 우리는 pixelOffset = - 2 / 6 + 0.5 = 0.1666 를 가집니다. (역주 : pixelOffset이라고 쓰지만 마지막 코드에 보면 Scale 값을 사용함)
현재 픽셀의 luma와 끝점에서 관찰된 luma의 차이가 일관되는지(coherent) 확인하기 위한 추가 검사가 있습니다. 그렇지 않으면 너무 멀리 이동해왔을 수 있어서, 오프셋을 적용하지 않습니다.
(역주 : 중앙 luma가 로컬 luma 평균보다 작다면, 끝점도 로컬 luma 평균보다 작은지 확인함. 즉, 처음과 끝만 확인)
// 중앙 luma가 로별 평균보다 더 작나?
bool isLumaCenterSmaller = lumaCenter < lumaLocalAverage;
// 만약 중앙 luma가 그것의 이웃보다 더 작다면, 양 끝의 delta luma가 양수여야 합니다. (같은 변형)
// (가장자리의 측면과 더 가까운 방향으로)
bool correctVariation = ((isDirection1 ? lumaEnd1 : lumaEnd2) < 0.0) != isLumaCenterSmaller;
// 만약 luma 차이가 잘못된 경우, offset 을 적용하지 않음
float finalOffset = correctVariation ? pixelOffset : 0.0;
픽셀에 대해서, luma의 중심이 더 작습니다, 그리고 마지막 luma는 음수가 아니기 때문에 우리는 실제로 (0.5 < 0.0) != isLumaCenterSmaller 를 가집니다. 그리고 Offset 계산은 유효합니다.
Subpixel antialiasing
추가 계산 단계는 서브픽셀 앨리어싱을 처리하게 해 줍니다, 예를 들면 화면에 앨리어싱이 발생한 얇은 선이 있을 때입니다. 이런 경우 평균 luma는 3x3 이웃에 대해 계산합니다. 중앙 luma를 빼고, 첫 번째 단계의 luma 범위에 의해 나눈 후에 이것은 서브픽셀의 오프셋이 됩니다. 전체 이웃들의 범위와 비교해서 평균과 중앙값의 더 작은 대비 차이가 작을수록 영역(Area)이 더 균일해집니다(즉, 단일 픽셀 점이 없음), 그리고 Offset이 더 작아집니다. 이 Offset은 다듬어지고, 우리는 이전 단계와 이 단계의 Offset 중 더 큰 것을 유지합니다.
// Sub-pixel shifting
// 3x3 이웃에 대한 luma의 전체 가중 평균
float lumaAverage = (1.0/12.0) * (2.0 * (lumaDownUp + lumaLeftRight) + lumaLeftCorners + lumaRightCorners);
// 3x3 이웃의 루마 범위에 대한, 전역 평균과 중앙 luma 간의 delta의 비율
float subPixelOffset1 = clamp(abs(lumaAverage - lumaCenter)/lumaRange,0.0,1.0);
float subPixelOffset2 = (-2.0 * subPixelOffset1 + 3.0) * subPixelOffset1 * subPixelOffset1;
// 이 delta에 기반한 sub-pixel의 offset 계산
float subPixelOffsetFinal = subPixelOffset2 * subPixelOffset2 * SUBPIXEL_QUALITY;
// 두 offset 중 더 큰것을 고름
finalOffset = max(finalOffset,subPixelOffsetFinal);
SUBPIXEL_QUALITY = 0.75 일 때.
예제에서, lumaAverage = (1/12)(2*(1+0+0+0)+1+1+0+0) = 4/12 = 0.333 와 subPixelOffset1 = 0.333-0.0/1.0 = 0.333, 따라서 subPixelOffsetFinal = 0.75*((-2*0.333+3.0)*(0.3333)^2)^2 = 0.0503, 그리고 최대 Offset은 0.1666 입니다. 따라서 서브픽셀 앨리어싱이 감지되지도 처리되지 않습니다.
Final read
최종적으로, 우리는 UV 를 가장자리에 직교하는 방향으로 이동하고 마지막으로 텍스쳐를 읽습니다.
// 최종 UV 좌표 계산
vec2 finalUv = In.uv;
if(isHorizontal){
finalUv.y += finalOffset * stepLength;
} else {
finalUv.x += finalOffset * stepLength;
}
// 새로운 UV 좌표에서 컬러를 읽고 사용함
vec3 finalColor = texture(screenTexture,finalUv).rgb;
fragColor = finalColor;
조사한 픽셀의 강도(Intensity)는 0.1666*1 + (1-0.1666)*0 ≈ 0.1666 입니다.
만약 우리가 이 방식을 우리의 작은 이미지의 모든 픽셀에 적용한다면, 우리는 아리의 값을 얻습니다.
그리고 다음과 같은 시각적 결과:
픽셀은 가장자리에 대한 근접성 여부와 위치에 따라서 부드러워집니다. 예제가 단수해서, 결과를 검증하기 어렵습니다. 더 복잡하고 잘 정의된 이미지에서, 다음의 안티-앨리어싱을 얻을 수 있습니다: 텍스쳐 디테일에 미치는 영향을 최소화하면서 부드럽고 효과적으로 가장자리를 부드럽게 함, 그리고 좋은 Temporal consistency. (왼쪽이 AA가 없는 것, 오른쪽이 FXAA 적용, 클릭해서 보면 차이가 더 잘 보임)
Sources
- FXAA, NVIDIA Corporation white paper, Timothy Lottes
- Fast approximate anti-aliasing, CodingHorror.com
- DirectX 11 Rendering in Battlefield 3, Frostbite
- Copy of the original T. Lottes source code for FXAA 3.11
- 스크린에 표시되는 UI 컴포넌트를 제외, 이런 것은 버튼과 레이블에 블러를 만드는 안티-앨리어싱을 피하고 싶어 합니다.
- v3.11 more precisely.
- ≈ x^(1.0/2.22)
- 입력 픽셀이 정확히 출력 픽셀과 일치하기 때문에
- 즉, UV 좌표가 (0.0, 1.0)으로 잘림
'Graphics > 참고자료' 카테고리의 다른 글
[번역] Graphics API abstraction – Wicked Engine Net (0) | 2021.05.15 |
---|---|
[번역] How to read shader assembly – Interplay of Light (0) | 2021.04.24 |
[번역] Screen Space Reflections : Implementation and optimization – Part 2 (0) | 2021.01.30 |
[번역] Screen Space Reflections : Implementation and optimization – Part 1 (0) | 2021.01.28 |
[번역] Lecture 13: Radiosity - Principles (0) | 2020.12.30 |