유타 티팟 후속 - 전역 FXAA와 급격한 기하 꺾임의 국소 FXAA

표준 전역 FXAA와 주둥이 구멍·뚜껑 림만 분석하는 적응형 절충 모드 비교

Category: 계산수학과 이산구조 → 컴퓨터 그래픽스와 기하 처리
Difficulty: 고급
Date: 2026-07-24
Read Time: 13 mins read
Views: 불러오는 중 조회

이 글은 패치 용접·국소 곡률 보정·점광원까지 만든 이전 글의 후속이다. 이번에는 두 AA 경로를 분명하게 분리했다. --aa=fxaa는 완성된 화면 전체에 적용하는 일반 FXAA이고, --aa=adaptive는 렌더링 분석 중 급격하게 꺾이는 기하 지점만 골라 FXAA를 적용하는 절충 모드다.

실행 가능한 핵심 소스(main.cpp, CMakeLists.txt, teapot.bpt, 구현 노트)는 utah-teapot-geometry-guided-aa.zip으로 첨부했다.

1. 두 모드의 역할

전역 FXAA는 색의 휘도 변화로 화면 에지를 찾아 모든 픽셀에서 보간 여부를 판단한다. 구현이 간단하고 실루엣·알파 경계·텍스처 경계까지 한 번에 다룰 수 있지만, 기하학적으로 매끈한 곳도 흐려질 수 있다.

적응형 모드는 이 전역 FXAA를 대체하는 다른 종류의 AA가 아니다. 같은 방향성 FXAA 보간을 더 좁은 후보에만 적용하는 절충안이다. 단, 검은 배경과 객체가 맞닿는 실루엣은 실제 coverage 경계이므로 반드시 후보에 포함한다. 그 외의 내부 픽셀은 depth, normal, curvature G-buffer가 실제로 꺾였는지를 판단할 때만 통과한다.

2. 1단계: G-buffer에서 후보 값을 수집한다

먼저 래스터화 단계에서 색뿐 아니라 깊이, 법선, 곡률 proxy를 G-buffer에 저장한다. 다음으로 각 픽셀의 4방향 이웃을 읽어 후보 판정에 필요한 값을 만든다. 이 순서를 지키면 화면 색만 보고 기하학적 의미를 추측하지 않아도 된다.

새 구현은 한 픽셀 p와 4방향 이웃 q에서 다음 값을 수집한다.

depthJump  = max_q |z_p - z_q| / min(z_p, z_q)
normalBend = max_q (1 - dot(N_p, N_q))
backgroundCount = background 이웃의 수
curvature = 래스터화 때 보간해 둔 기하 곡률 proxy

3. 2단계: 배경 경계와 급격한 기하 이벤트를 스텐실에 기록한다

다음 조건 중 하나를 만족하면 적응형 FXAA 후보로 기록한다.

silhouette       = backgroundCount >= 1

apertureOrCorner = backgroundCount >= 2
                OR (backgroundCount >= 1 AND curvature > 0.07 AND normalBend > 0.04)

abruptBend      = curvature > 0.08 AND normalBend > 0.07
occlusionKink   = depthJump > 0.035 AND normalBend > 0.04

backgroundCount >= 1이면 검은 배경과 객체 사이의 경계를 부드럽게 하기 위해 적응형 후보가 된다. 다만 기본 실루엣 점수는 낮게 둔다. 주둥이의 구멍처럼 둘 이상의 방향이 배경과 닿는 개구부, 뚜껑 림처럼 배경 접촉과 강한 방향 변화가 함께 있는 부분, 서로 다른 깊이의 표면이 만나는 가림 경계에는 더 높은 점수를 준다. 따라서 두 방법 모두 검은 배경-객체 경계를 처리하지만, 적응형은 기하학적으로 중요한 곳에 더 큰 보간량을 배정한다.

이 값은 정확한 미분기하학적 주곡률도, 진짜 MSAA coverage도 아니다. 대신 CPU 래스터라이저가 이미 계산한 depth, normal, curvature를 이용해 어느 픽셀에만 비용을 쓸지를 보수적으로 정하는 규칙이다.

4. 3단계: 전역 또는 국소 FXAA를 실행한다

single-sample triangle rasterisation
  -> colour + depth + normal + curvature G-buffer
  -> --aa=fxaa:     full-frame directional FXAA
  -> --aa=adaptive: object/background silhouette + aperture / abrupt-bend / occlusion-kink analysis
                    -> sharp-feature stencil -> local directional FXAA
  -> PPM / X11

두 모드 모두 최종 색의 휘도로 방향을 계산한다.

L = 0.299 R + 0.587 G + 0.114 B

이번 구현은 단순한 상하 또는 좌우 평균을 쓰지 않는다. 먼저 대각선 4개 픽셀의 휘도에서 에지 진행 방향 dir을 계산한다. 그 방향을 따라 bilinear sample을 네 번 읽고, 가까운 두 sample의 평균 rgbA와 넓은 범위까지 포함한 rgbB를 만든다. rgbB의 휘도가 현재 3x3 범위를 벗어나면 rgbA를 택한다. 이 범위 검사가 좁은 하이라이트를 지나치게 넓히거나 서로 다른 에지를 섞는 일을 줄인다.

--aa=fxaa는 이 directional resolve를 화면 전체에 적용한다. --aa=adaptive는 위 기하 분석을 통과한 픽셀에서만 같은 resolve를 계산한 뒤, 분석 점수에 따라 원본과 결과를 섞는다.

featureScore = max(curvature / 0.16,
                   normalBend / 0.20,
                   depthJump / 0.08,
                   silhouette ? 0.35 : 0.0)

rgbA = 0.5 * (sample(dir * -1/6) + sample(dir * 1/6))
rgbB = 0.5 * rgbA + 0.25 * (sample(dir * -1/2) + sample(dir * 1/2))
result = luma(rgbB) in localRange ? rgbB : rgbA

global FXAA:   output = result
adaptive FXAA: output = mix(original, result, 0.45 + 0.55 * featureScore)

둘 다 한 번의 FXAA resolve다. adaptive가 전역 FXAA 뒤에 화면 전체를 다시 블러링하는 두 번째 필터는 아니다. 메시 위치·깊이·법선 자체는 수정하지 않는다.

5. 4단계: 같은 카메라로 결과를 비교한다

AA의 차이를 보려면 카메라가 중요하다. 정면의 낮은 구도에서는 뚜껑과 몸체의 경계가 겹치거나 짧아져서, 보간 전후를 읽기 어렵다. 아래 비교는 위에서 약간 내려다보는 3/4 구도(theta=1.5, phi=0.55, radius=5.4)를 사용했다. 이 시점에서는 뚜껑 림과 몸체 사이의 어두운 틈·밝은 림이 화면을 길게 가로지른다.

패치당 분할 수는 비교를 드러내려고 8로 낮췄다. 이 값은 최종 품질 권장값이 아니라, 단일 샘플 픽셀 계단과 선택적 보간의 차이를 확인하기 위한 시험 조건이다.

단일 샘플 (off) 전역 FXAA 급격한 지점의 적응형 국소 FXAA
AA off Local FXAA Adaptive local FXAA

아래는 새 directional resolve로 다시 렌더한 뚜껑 림을 세 배 확대한 결과다. 왼쪽부터 off, 전역 fxaa, 적응형 adaptive다. 가운데는 화면 전체 FXAA의 결과이고, 오른쪽은 객체-배경 실루엣과 림의 기하 변화로 판정된 픽셀만 보간한 결과다.

뚜껑 림의 국소 FXAA 전후 확대

아래 확대 비교는 검은 배경과 티팟 몸체의 외곽이다. 가운데의 전역 FXAA와 오른쪽의 적응형 FXAA 모두 배경-객체 경계의 날카로운 계단을 보간한다. 차이는 적용 범위다. 전역 FXAA는 화면 전체의 휘도 에지를 검사하고, 적응형은 이 검은 배경 접촉과 분석된 기하 후보에만 계산을 집중한다.

검은 배경과 객체 실루엣의 FXAA 비교

6. 5단계: 실행 명령으로 재현한다

cmake -S . -B build-aa -DCMAKE_BUILD_TYPE=Release
cmake --build build-aa -j

# X11 창에서 보기
./build-aa/teapot --unify=yes --aa=adaptive

# 본문과 같은 비교용 정지 이미지
./build-aa/teapot --still off.ppm      --unify=yes --tess 8 --size 512x384 \
  --camera 1.5 0.55 5.4 --aa=off
./build-aa/teapot --still fxaa.ppm     --unify=yes --tess 8 --size 512x384 \
  --camera 1.5 0.55 5.4 --aa=fxaa
./build-aa/teapot --still adaptive.ppm --unify=yes --tess 8 --size 512x384 \
  --camera 1.5 0.55 5.4 --aa=adaptive

옵션의 의미는 다음과 같다.

  • --aa=off: 후처리 없이 단일 샘플 래스터화만 사용한다.
  • --aa=fxaa: 화면 전체의 휘도 에지에 고정 강도의 일반 FXAA를 적용한다.
  • --aa=adaptive: 검은 배경과 객체의 실루엣, 그리고 곡률·법선 변화·깊이 변화로 찾은 급격한 기하 후보에만 FXAA를 적용한다. 분석 점수가 클수록 혼합량을 조절한다. 기본값이다.

7. 6단계: 실제 비용을 반복 측정한다

품질 비교만으로 적응형 경로의 목적이 달성됐다고 판단하면 안 된다. 따라서 같은 장면에서 applyPostAA() 구간만 따로 반복 측정했다. 이 값에는 메시 생성, 정점 변환, 삼각형 래스터화, PPM 저장, X11 표시 시간은 들어가지 않는다. 즉 아래 표는 전체 프레임 시간이 아니라 AA 후처리 자체의 비용이다.

측정 조건은 Release 빌드, 1024×768, --unify=yes, --tess 12다. 각 모드는 먼저 5회 워밍업했고, 그 뒤 25회를 기록했다.

모드 평균 중앙값 최솟값 최댓값
off 0.000 ms 0.000 ms 0.000 ms 0.000 ms
전역 fxaa 35.835 ms 35.556 ms 35.000 ms 38.443 ms
적응형 adaptive 9.960 ms 9.889 ms 9.223 ms 11.429 ms

적응형 경로는 전역 FXAA의 평균 post-AA 비용을 약 72.2% 줄였다. 따라서 이번 구현에서는 비용 절감에 성공했다. 원인은 적응형이 전체 프레임의 모든 픽셀에서 대각선 bilinear sample을 수행하지 않고, 검은 배경-객체 실루엣과 depth/normal/curvature 분석을 통과한 픽셀에서만 directional resolve를 수행하기 때문이다.

다만 이 수치만으로 전체 FPS가 72.2% 좋아진다고 해석하면 안 된다. 실제 프레임에는 CPU 삼각형 래스터화와 조명 계산도 포함된다. 후처리만 보면 적응형이 유리하다는 결론이며, 전체 프레임 속도는 장면 해상도, 테셀레이션 밀도, 화면에서 객체가 차지하는 면적에 따라 별도로 측정해야 한다.

8. 7단계: FXAA와 적응형에 각각 2x 다운샘플을 적용한다

이번에는 참조 이미지만 고해상도로 만들지 않았다. FXAA와 적응형 모두에 다음 두 파이프라인을 실제로 적용했다.

native:  512x384 render -> AA -> output
2x:      1024x768 render -> AA -> Lanczos downsample -> 512x384 output

품질 참조는 같은 구도에서 2048x1536, --aa=off으로 렌더한 뒤 Lanczos로 512x384로 축소한 4x 이미지다. 따라서 아래의 2x 결과는 고해상도 렌더링과 AA를 둘 다 적용한 뒤 같은 출력 해상도로 내린 결과다.

4x 렌더 후 Lanczos 축소로 만든 품질 측정 기준 이미지

파이프라인 전체 RMSE 뚜껑 림 RMSE 실루엣 RMSE 평균 end-to-end 시간
네이티브 전역 FXAA 0.006583 0.011683 0.002883 25.200 ms
네이티브 적응형 0.006460 0.011310 0.002912 16.267 ms
2x 전역 FXAA 후 축소 0.002915 0.005092 0.001597 135.600 ms
2x 적응형 후 축소 0.002995 0.005126 0.001584 105.467 ms

시간은 Release 빌드, --unify=yes, --tess 8, 같은 카메라에서 각 파이프라인을 3회 워밍업한 뒤 15개 표본으로 측정했다. 표본 하나는 5회 렌더의 평균이다. 이 end-to-end 값에는 실행 파일 시작, 메시 준비, 래스터화, AA, PPM 저장이 포함된다. 2x 행에는 Lanczos 축소 변환도 포함된다. 따라서 위 7절의 post-AA 단독 시간과 섞어 해석하면 안 된다.

왼쪽부터 네이티브 전역 FXAA, 네이티브 적응형, 2x 전역 FXAA 후 축소, 2x 적응형 후 축소

같은 순서의 뚜껑 림 확대 비교

9. 8단계: 가성비를 판단한다

품질만 보면 2x 전역 FXAA가 전체와 뚜껑 림에서 가장 낮은 RMSE를 냈다. 2x 적응형은 전체 오차가 0.000080 더 높지만, 2x 전역 FXAA보다 약 22.2% 빠르고 실루엣 crop에서는 가장 낮은 RMSE를 냈다. 고품질 정지 이미지나 느린 오프라인 출력이라면 2x 적응형이 더 균형 잡힌 선택이다.

실시간 CPU 렌더러에서는 네이티브 적응형이 더 낫다. 네이티브 전역 FXAA보다 전체·림 RMSE가 모두 낮고, end-to-end 시간도 약 35.4% 적다. 2x 적응형은 네이티브 적응형보다 약 6.5배 오래 걸리므로, 2x 품질 이득이 프레임 시간 예산을 정당화할 때만 선택해야 한다. 이 결론은 4x 다운샘플 참조와 RMSE에 근거한 화면 공간 판단이며, 진짜 sub-pixel coverage나 인간 지각 품질의 완전한 측정은 아니다.

정리하면 네 파이프라인의 권장 용도는 다음과 같다.

  • 네이티브 전역 FXAA: 단순한 화면 후처리와 모든 색상 에지의 일괄 완화가 필요한 빠른 확인용 출력에 적합하다. 실루엣은 가장 매끈하지만, 이 CPU 렌더러에서는 적응형보다 비용이 크다.
  • 네이티브 적응형 AA: X11 실시간 미리보기의 기본값으로 적합하다. 검은 배경 실루엣과 중요한 기하 꺾임을 보정하면서도, 네이티브 전역 FXAA보다 더 적은 시간이 든다.
  • 2x 전역 FXAA 후 축소: 시간보다 정지 이미지 품질을 우선할 때 적합하다. 전체와 뚜껑 림 RMSE가 가장 낮으므로, 블로그용 대표 캡처나 오프라인 결과물에 쓸 수 있다.
  • 2x 적응형 AA 후 축소: 고품질 프리뷰와 캡처 사이의 절충안이다. 2x 전역 FXAA와 거의 같은 품질을 유지하면서 비용은 더 낮고, 검은 배경 실루엣 수치는 가장 좋다.

10. 마지막으로 MSAA와의 차이를 확인한다

이 구현은 실제 MSAA가 아니다. MSAA는 래스터화 때 픽셀 내부의 여러 샘플 위치에서 coverage와 깊이를 판정한다. 여기서는 이미 확정된 단일 샘플 색을 국소적으로 보간할 뿐이므로, 가는 선·알파 컷아웃·서브픽셀 기하에 대한 coverage 정보는 되찾지 못한다.

다만 이번 sharp-feature stencil은 다음 단계의 선택적 MSAA에도 그대로 쓸 수 있다. 후보가 있는 8×8 타일만 2×2 sub-pixel coverage로 재래스터화하고 나머지는 단일 샘플로 유지하면, 그때는 실제 선택적 MSAA가 된다. 현재 단계는 그 비용을 들이기 전에 후보 위치와 필터 범위를 검증하는 구현이다.

Document Classification

Keywords
Utah Teapot Software Renderer Local FXAA Anti-Aliasing G-buffer Depth Buffer Normal Buffer Curvature
Difficulty
고급
Permalink
https://gg582.github.io/mathematics/2026-07-24-%EC%9C%A0%ED%83%80-%ED%8B%B0%ED%8C%9F-%EA%B8%89%EA%B2%A9%ED%95%9C-%EA%B8%B0%ED%95%98-%EA%BA%BE%EC%9E%84-%EA%B5%AD%EC%86%8C-FXAA/

Citation

이윤진(Lee Yunjin) (2026). 유타 티팟 후속 - 전역 FXAA와 급격한 기하 꺾임의 국소 FXAA. 윤진의 IT 블로그. Retrieved from https://gg582.github.io/mathematics/2026-07-24-%EC%9C%A0%ED%83%80-%ED%8B%B0%ED%8C%9F-%EA%B8%89%EA%B2%A9%ED%95%9C-%EA%B8%B0%ED%95%98-%EA%BA%BE%EC%9E%84-%EA%B5%AD%EC%86%8C-FXAA/
── 하략 ──