[자체 엔진] FXAA

FXAA

1. FXAA란?

Anti Aliasing의 기술 중 하나이다. 풀 네임은 Fast Approximate Anti-Aliasing이다.

이미 렌더링된 화면의 edge를 찾아서 부드럽게 만들어주는 Post Processing Filter이다.
즉, 3D 기하 정보를 사용하지 않고 최종하면의 edge만 감지하기 때문에 속도가 빠른 것이다.

또한 픽셀이 Edge 위에 있는 지 판단하고, 위에 있다면 edge 방향을 따라 샘플링해서 블러효과를 준다.

2. FXAA 구현 방법

FXAA는 매우 단순한 아이디어로 동작한다.

이웃 픽셀의 밝기 변화가 급격하다면, 이를 계단현상으로 인식하고 블러 효과를 주면된다.

사람의 눈은 색의 차보다 명암 경계에 더 민감하다. 때문에 이웃 픽셀의 색이 아닌 밝기로 판단을 한다.

Step1

현재 픽셀 주변 대각 픽셀 4개를 샘플링한다.
대각 픽셀을 sampling하는 이유는 수평,수식일 경우에는 픽셀 격자와 완전 일치하기 때문에 눈에 덜 거스릴 수 있지만,
대각에서 밝기 차이가 난다면 사람의 눈이 잘 인식할 것이기 때문이다.

Sampling(NW)        Sampling(NE)
             Target
Sampling(SW)        Sampling(SE)

Step2

aliasing은 색 차이 보다 밝기 변화에서 잘 나타난다.
luma밝기를 계산한다.

luma = dot(rgb, float3(0.299, 0.587, 0.114))

Step3

edge 판단을 위해서 밝기의 최소/최대값을 구한다.

float lumaMin(5개의 pixel중 최소 luma)
float lumaMax(5개의 pixel중 최대 luma)
float lumaRange = lumaMax - lumaMin

Step4

lumaRange 가 크다면 밝기 변화가 큰 것이고, edge일 가능성이 크다는 뜻이다.

threashold와 비교해서 early return 여부를 정하면 된다.
이 때 절대기준, 상대기준을 모두 사용해서 둘 중 더 큰 것을 사용하도록 구현하였다.

max(0.0625, lumaMax * 0.125)

최소 절대 기준 값: 0.0625, 밝기에 비례한 상대기준 값: 0.125를 사용했다.

절대 기준
너무 작은 밝기 변화까지 edge로 판단한다면 화면이 전체적으로 너무 흐려기기 때문에 하한선이 있어야 한다.

상대 기준
장면 밝기에 따라 Edge 판단을 적응적으로 만들 수 있다.

Step5

Edge 방향 계산 차례이다.
여기 까지 통과했다는 의미는 이제 블러할 가치가 있는 Pixel들이다.

FXAA는 edge 방향으로 블러를 하기 때문에 edge 방향을 알아낼 필요가 있다.

float2 dir;
dir.x = -( (lumaNW + lumaNE) - (lumaSW + lumaSE));
dir.y = (lumaNW + lumaSW) - (lumaNE + lumeSE); 

dir.x: 세로 방향의 edge방향을 보는 것
dir.y: 가로 방향이 edge방향을 보는 것

edgeDir = (-dL/dy , dL/dx) <- 이것을 의미한다.

여기서 구한 방향에 대한 신뢰(?)도 또한 설정가능하도록 하였다.
edge의 방향 벡터 dir를 그대로 사용한다면, 미세한 밝기 차이에도 민감하게 반응할 수 있기 때문에 이를 제어할 필요가 있다.

FXAAReduceMul: 방향에 대한 신뢰도를 조절할 수 있다.
FXAAReduceMin: 하한선을 정해준다.

float dirReduce = max(밝기 평균 * FXAAReduceMul), FXAAReduceMin);

Step6

float rcpDirMin = 1.0f / (min(abs(dir.x) , abs(dir.y)) + dirReduce); // dirReduce를 더해서 부모가 0이되는 것을 방지

위에서 얻은 정보들을 바탕으로 이렇게 사용하면, dir이 적당한 크기로 보정시킬 수 있다.

float2 dirScaled = dir * rcpDirMin;

Step7

dirScaled = clamp(dirScaled, 
float2(-FXAASpanMax, -FXAASpanMax), float2(FXAASpanMax, FXAASpanMax));

dir = dirScaled * InvResolution;

FXAASpanMax 를 정해서 edge의 boundary를 정해준다. (blur 범위가 크다고 만사가 아니닷)

이를 UV좌표로 변경해줘야 실제로 사용할 수 있기 때문에 빠뜨리지 말고 변환까지 해주자

dir = dirScaled * InvResolution;

Step8

이제 dir을 얻었으니 이를 바탕으로 Sampling을 해줘야한다.

여기서도 무작정 sampling을 한다기 보다 dir 방향으로 가까운 pixel에서 더 많은 정보를 멀어질 수록 더 적은 양을 sampling 해오도록 구현하였다.

dir방향의 +- 1/6 지점에서 Sampling 해왔고, 0.5를 곱해서 사용

float3 rgbA = 0.5f * (
    SceneColor.Sample(SceneSampler, tex + dir * (1.0 / 3.0 - 0.5)).rgb +
    SceneColor.Sample(SceneSampler, tex + dir * (2.0 / 3.0 - 0.5)).rgb
);

dir방향의 +- 1/2 지점에서 Sampling 해왔고, 0.25를 곱해서 사용

float3 rgbB = rgbA * 0.5f + 0.25f * (
    SceneColor.Sample(SceneSampler, tex + dir * -0.5).rgb +
    SceneColor.Sample(SceneSampler, tex + dir * 0.5).rgb
);

Step9

계속해서 안전장치를 걸어주자, 위에서 처러 블러를 했을 때 주변 픽셀과 밝기 차이가 크면 눈이 너무 띄는 블러가 될 것이다.

이를 방지하기 위해

float lumaB = Luma(rgbB);
if ((lumaB < lumaMin) || (lumaB > lumaMax))
{
    rgbB = rgbA;
}

밝기의 최소 최대값을 벗어나지 않도록 체크하고, 벗어난다면 보수적인 값을 사용하도록 수정한다.

Step10

이제 이 값을 실제 SceneColor에 Blending을 해주면된다.
SceneColor를 완전 배제시키면 원본 디테일이 완전히 사라질 수 있다.

 

subpix값은 인자로 빼서 사용자가 조절할 수 있도록 하면 된다.

const float subpix = 0.99f;
float3 finalColor = lerp(rgbM, rgbB, subpix);

최종 결과물!

'ComputerGraphics > 자체엔진' 카테고리의 다른 글

[자체 엔진] Decal  (0) 2026.03.09
[자체 엔진] Exponent Height Fog  (0) 2026.03.09
[자체 엔진] Batch Line Rendering  (0) 2026.03.09
[자체 엔진] Billboard  (0) 2026.03.09
[자체 엔진] Features  (0) 2026.03.09
myoskin