래스터화 파이프라인 (3) - 디스플레이와 안티앨리어싱 - soo:bak
작성일 :
버퍼에서 화면으로
래스터화 파이프라인 (2)에서는 프래그먼트가 깊이 테스트, 스텐실 테스트, 블렌딩을 거쳐 프레임 버퍼에 기록되는 과정을 살펴봤습니다. 한 프레임의 드로우 콜이 모두 끝나면 프레임 버퍼에는 화면에 표시할 이미지가 완성됩니다.
하지만 프레임 버퍼에 이미지가 완성되었다고 해서 그 이미지가 즉시 화면에 보이는 것은 아닙니다. 디스플레이는 프레임 버퍼 전체를 한 번에 표시하지 않고, 위에서 아래로 한 줄씩 읽어 화면을 갱신합니다. GPU가 새 프레임을 준비하는 시점과 디스플레이가 현재 프레임을 읽는 시점이 어긋나면, 한 화면 안에 서로 다른 두 프레임이 섞이는 티어링(Tearing)이 나타날 수 있습니다.
이 문제를 다루기 위해 더블 버퍼링, VSync, 트리플 버퍼링 같은 방법이 쓰입니다. 셋 모두 티어링이나 GPU 대기 시간을 줄이는 대신, 입력 지연, 메모리 사용량, 프레임 페이싱 같은 다른 비용이 따릅니다.
화질 문제는 디스플레이 동기화에만 있지 않습니다. 래스터화는 연속적인 삼각형을 유한한 픽셀 격자로 샘플링하므로, 비스듬한 경계선이 계단 모양으로 보이는 앨리어싱(Aliasing)이 생깁니다. 해상도를 높이면 계단은 작아지지만, 유한한 픽셀 격자로 연속적인 경계를 표현한다는 한계는 남습니다.
그래서 계단을 덜 보이게 만드는 안티앨리어싱(Anti-Aliasing)이 필요합니다.
이번 글에서는 완성된 프레임이 화면에 닿기까지의 동기화 문제를 먼저 따라가 본 뒤, 래스터화의 표본화 한계를 메우는 안티앨리어싱 기법까지 차례로 살펴봅니다.
디스플레이 스캔아웃
모니터나 스마트폰 화면은 완성된 프레임 버퍼를 한 번에 통째로 표시하지 않습니다. 프레임 버퍼의 픽셀을 왼쪽 위부터 오른쪽으로, 한 줄이 끝나면 다음 줄로 순서대로 읽어 디스플레이에 보냅니다. 이렇게 순차적으로 읽어 화면을 갱신하는 과정을 스캔아웃(Scanout)이라고 부릅니다.
이 읽기 순서는 CRT(브라운관) 모니터에서 이어진 방식입니다. CRT에서는 전자빔이 화면을 왼쪽에서 오른쪽으로 한 줄씩 지나가고, 줄이 끝나면 아래 줄로 내려가며 형광체를 발광시켰습니다. 픽셀을 갱신하는 순서가 전자빔의 이동 경로와 같았습니다.
LCD나 OLED는 전자빔을 사용하지 않지만, 디스플레이 컨트롤러가 프레임 버퍼를 읽는 순서는 여전히 비슷한 구조를 따릅니다.
디스플레이가 스캔아웃을 초당 반복하는 횟수를 주사율(Refresh Rate)이라고 하며, 헤르츠(Hz)로 표시합니다. 60Hz 화면은 초당 60번 전체 화면을 갱신하므로, 한 번의 갱신 주기는 약 16.67ms입니다.
한 번의 갱신 주기는 크게 두 구간으로 나뉩니다. 픽셀 데이터를 실제로 전송하는 활성 스캔아웃 구간이 있고, 한 프레임의 스캔이 끝난 뒤 다음 프레임을 다시 읽기 시작하기 전의 짧은 유휴 구간인 수직 귀선 구간(VBlank, Vertical Blanking Interval)이 있습니다.
VBlank라는 이름도 CRT에서 왔습니다. 전자빔이 화면 아래까지 내려간 뒤 다시 위로 돌아가는 동안에는 화면을 그리지 않았고, 그 빈 시간이 VBlank였습니다. 지금도 이 구간은 버퍼를 교환하기 좋은 타이밍으로 쓰이며, 뒤에서 다룰 VSync가 이 VBlank를 기준으로 동작합니다.
티어링
디스플레이는 프레임 버퍼를 위에서 아래로 한 줄씩 읽습니다. 이 스캔아웃이 끝나기 전에 디스플레이가 읽고 있는 버퍼가 새 프레임으로 바뀌면 문제가 생깁니다.
버퍼가 하나뿐이라면 GPU는 디스플레이가 읽는 버퍼에 다음 프레임을 직접 그릴 수밖에 없습니다. 스캔아웃이 화면 중간까지 진행된 시점에 버퍼 내용이 바뀌면, 이미 읽힌 위쪽은 이전 프레임이고 이후 읽히는 아래쪽은 새 프레임이 됩니다. 그 결과 한 화면 안에서 두 프레임이 가로 경계를 기준으로 나뉘어 보입니다.
이렇게 화면이 가로 방향으로 나뉘어 어긋나 보이는 현상을 티어링(Tearing)이라고 부릅니다.
티어링의 원인은 GPU의 프레임 완성 시점과 디스플레이의 스캔아웃 시점이 서로 동기화되어 있지 않다는 데 있습니다.
한 스캔아웃 안에서 버퍼 내용이 여러 번 바뀌면 티어링 경계도 여러 줄 생길 수 있습니다. 카메라가 빠르게 움직이거나 장면 변화가 큰 경우에는 프레임 간 차이가 커서 경계가 쉽게 보이고, 정적인 화면에서는 두 프레임이 비슷해 경계가 잘 드러나지 않습니다.
더블 버퍼링
티어링은 디스플레이가 읽는 버퍼와 GPU가 그리는 버퍼가 같을 때 발생합니다. 이를 막기 위해 표시용 버퍼와 렌더링용 버퍼를 분리하는 방식이 더블 버퍼링(Double Buffering)입니다. 이름 그대로 프레임 버퍼를 두 개 둡니다.
두 버퍼는 역할이 다릅니다. 프론트 버퍼(Front Buffer)는 디스플레이가 지금 읽고 있는 버퍼이며, 화면에 표시 중인 프레임을 저장합니다. 백 버퍼(Back Buffer)는 GPU가 새 프레임을 그리는 버퍼이며, 렌더링이 끝나기 전까지 화면에 표시되지 않습니다.
GPU가 백 버퍼에 한 프레임을 모두 그리면 프론트 버퍼와 백 버퍼의 역할을 바꿉니다. 이 작업을 교환(Swap)이라고 부릅니다. 교환은 픽셀 데이터를 복사하는 작업이 아니라, 디스플레이 컨트롤러가 어느 버퍼를 읽을지 가리키는 참조를 바꾸는 작업에 가깝습니다.
버퍼가 둘이면 메모리 사용량은 그만큼 늘어납니다. 래스터화 파이프라인 (2)에서 다룬 RGBA8 Full HD 기준으로 색상 버퍼 한 장이 약 8MB이므로 두 장은 약 16MB입니다. 현대 GPU에서는 대체로 감당 가능한 수준이지만, 모바일에서는 해상도와 버퍼 형식에 따라 여전히 고려할 비용입니다.
메모리 비용을 감수하더라도, 버퍼를 둘로 나누는 것만으로는 티어링이 완전히 사라지지 않습니다. 스캔아웃 도중에 Swap이 일어나면 디스플레이가 읽는 버퍼가 중간에 바뀌고, 다시 한 화면 안에 두 프레임이 섞일 수 있습니다.
따라서 남은 문제는 Swap을 언제 허용할 것인가입니다. 이 시점을 디스플레이 갱신 주기에 맞추는 방식이 VSync입니다.
VSync
VSync(Vertical Synchronization, 수직 동기화)는 버퍼 교환을 VBlank 구간에 맞춰 실행하는 방식입니다. 디스플레이가 한 화면을 모두 읽고 다음 화면을 읽기 시작하기 전의 짧은 구간에서만 Swap을 허용합니다.
백 버퍼가 먼저 완성되더라도 스캔아웃 중에는 Swap을 보류합니다. 스캔아웃이 끝난 뒤 VBlank에 들어갔을 때만 버퍼를 교환하므로, 하나의 스캔아웃 동안 디스플레이는 같은 프레임만 읽게 됩니다. 그 결과 티어링은 사라집니다.
VSync는 티어링을 없애지만, 버퍼 교환을 기다리게 만드는 만큼 몇 가지 비용이 생깁니다.
먼저 입력 지연(Input Lag)이 늘어납니다. GPU가 프레임을 다 그려도 다음 VBlank가 오기 전까지는 그 프레임이 화면에 표시되지 않습니다. 프레임 완성 시점과 표시 시점 사이에 최대 한 프레임 주기, 60Hz 기준 약 16.67ms가 더해질 수 있습니다.
그만큼 조작이 화면에 늦게 반영되므로, 빠른 반응이 중요한 게임에서는 이 지연을 체감할 수 있습니다.
다음으로, 프레임 레이트가 디스플레이 갱신 주기에 묶입니다. 교환은 VBlank 구간에서만 가능하므로, 프레임을 다 그린 시점이 두 VBlank 사이에 떨어지면 그다음 VBlank가 올 때까지 기다려야 합니다. 이 때문에 프레임 레이트는 갱신 주기를 정수로 나눈 값으로 제한되어, 60Hz에서는 매 VBlank마다 교환하면 60fps, 두 VBlank마다 교환하면 30fps, 세 VBlank마다 교환하면 20fps가 됩니다.
GPU가 한 프레임을 16.67ms 안에 끝내면 60fps가 그대로 유지되지만, 18ms가 걸리면 바로 다음 VBlank를 놓쳐 그다음 VBlank까지 기다려야 합니다. 그러면 이 프레임의 표시 간격이 33.33ms로 늘어나, 체감 프레임 레이트가 30fps로 떨어집니다.
이처럼 프레임 표시 간격이 갑자기 커지는 문제는 프레임 페이싱과 직접 연결됩니다. 60fps를 안정적으로 유지하기 어려운 모바일 게임에서는 목표를 30fps로 낮추고, 대신 프레임 간격을 일정하게 유지하는 전략을 선택하기도 합니다.
프레임 페이싱은 게임 루프의 원리 (2)에서 자세히 다룹니다.
Unity에서 VSync는 QualitySettings.vSyncCount로 설정합니다. 값을 1로 두면 매 VBlank마다 교환하고, 2로 두면 두 번째 VBlank마다 교환합니다. 60Hz 디스플레이 기준으로 각각 60fps와 30fps에 해당합니다. 0으로 두면 Unity의 VSync 설정은 꺼집니다.
모바일에서는 상황이 다릅니다. Android와 iOS는 여러 앱과 시스템 UI의 화면을 한 장으로 합성하는 운영체제의 컴포지터(Compositor)와 프레임 스케줄링 시스템이 이미 VSync 기반으로 동작합니다. Android의 SurfaceFlinger·Choreographer, iOS의 CADisplayLink가 대표적입니다. 그래서 vSyncCount를 0으로 두어도 실제 프레임 제출은 디스플레이 주기에 맞춰지는 경우가 많습니다.
따라서 모바일에서는 vSyncCount보다 Application.targetFrameRate로 목표 프레임 레이트를 지정하는 쪽이 실질적인 제어 수단에 가깝습니다.
프레임 레이트 고정 문제를 디스플레이 쪽에서 완화하는 기술도 있습니다. 가변 주사율(VRR, Variable Refresh Rate)은 디스플레이 갱신 주기를 고정하지 않고, GPU가 프레임을 완료하는 시점에 맞춰 주기를 조정합니다. G-Sync, FreeSync, Adaptive Sync가 이 범주에 속합니다.
VRR을 지원하는 환경에서는 VSync에서 생기는 입력 지연과 프레임 레이트 급락이 함께 완화될 수 있습니다. 다만 지원 여부와 동작 범위는 디스플레이, 플랫폼, 그래픽스 API에 따라 달라집니다.
트리플 버퍼링
VSync를 켠 더블 버퍼링에서는 GPU가 백 버퍼를 완성한 뒤 다음 VBlank까지 기다릴 수 있습니다. 백 버퍼가 하나뿐이라, 그 버퍼가 프론트 버퍼로 교환되기 전에는 다음 프레임을 그릴 공간이 없기 때문입니다. 이 대기 시간을 줄이기 위해 백 버퍼를 하나 더 두는 방식이 트리플 버퍼링(Triple Buffering)입니다.
트리플 버퍼링에서는 프론트 버퍼 1개와 백 버퍼 2개를 사용합니다. GPU가 한 백 버퍼에 프레임을 완성하면, 그 버퍼가 VBlank에서 표시되기를 기다리는 동안 다른 백 버퍼에 다음 프레임을 그릴 수 있습니다. 빈 백 버퍼가 하나 더 있으므로 GPU가 대기하는 시간이 줄어듭니다.
버퍼를 하나 더 두면 두 가지 이점이 있습니다.
먼저 GPU 활용률이 올라갑니다. VBlank를 기다리며 멈추는 대신 빈 백 버퍼에 다음 프레임을 그릴 수 있으므로, 더블 버퍼링에서 생기던 대기 시간이 줄어듭니다.
둘째, 프레임 레이트가 60fps에서 곧바로 30fps로 떨어지는 상황을 완화할 수 있습니다. 예를 들어 60Hz에서 한 프레임이 18ms 걸리면 더블 버퍼링에서는 다음 VBlank를 놓쳐 30fps로 내려갈 수 있습니다. 트리플 버퍼링에서는 GPU가 계속 다음 프레임을 그리므로 처리량은 약 55fps(1000/18ms)에 가깝게 유지될 수 있습니다. 디스플레이는 60Hz로 동작하지만, VBlank마다 가장 최근에 완성된 프레임을 표시할 수 있으므로 더 많은 갱신 시점에 새 프레임을 올릴 수 있습니다. 다만 GPU 처리량과 디스플레이 주기가 정확히 맞지 않으면 같은 프레임이 두 번 표시되는 미세한 끊김이 생길 수 있습니다.
대신 입력 지연이 늘 수 있습니다. 백 버퍼가 하나 더 있으면 화면에 표시 중인 프레임과 GPU가 현재 그리는 프레임 사이의 간격이 더블 버퍼링보다 커질 수 있습니다. 사용자의 입력이 화면에 반영되기까지 최대 한 프레임 정도 더 걸릴 수 있다는 뜻입니다.
다만 실제 지연은 완성된 프레임을 화면에 올리는 방식, 즉 프레젠트 모드에 따라 달라집니다. VBlank마다 가장 최근에 완성된 백 버퍼만 표시하고 나머지는 버리는 mailbox 계열에서는 지연이 거의 늘지 않습니다. 반면 완성된 프레임을 큐에 쌓아 순서대로 표시하는 render-ahead 계열에서는 오래된 프레임이 먼저 나가 그만큼 지연이 커집니다.
GPU 메모리도 더 필요합니다. 색상 버퍼가 2개에서 3개로 늘어나므로, Full HD RGBA8 기준으로 약 8MB가 추가됩니다. 모바일에서는 해상도와 HDR 형식에 따라 이 비용이 더 커질 수 있습니다.
트리플 버퍼링은 Unity에서 직접 켜고 끄는 설정이 아니라, 플랫폼과 그래픽스 API, 운영체제의 프레젠트 방식이 함께 결정합니다. 특히 Android와 iOS에서는 운영체제의 컴포지터가 화면을 합성하면서 이미 여러 버퍼를 사용하므로, 트리플 버퍼링도 앱이 아니라 운영체제 차원에서 다뤄집니다.
앨리어싱
디스플레이 동기화로 티어링 없이 프레임을 내보내더라도, 그 프레임 자체의 화질은 렌더링 단계에서 이미 제한됩니다.
매끄러운 삼각형 가장자리를 유한한 픽셀 격자에 맞춰 표현하면, 비스듬한 경계선이 계단 모양으로 보입니다. 이렇게 연속적인 도형을 이산적인 격자로 샘플링할 때 생기는 왜곡을 앨리어싱(Aliasing)이라고 하며, 톱니처럼 보이는 가장자리를 jaggies라고 부릅니다.
나이퀴스트 정리와 앨리어싱
계단이 생기는 근본 원인은 샘플링 자체의 한계입니다. 연속적인 대상을 일정한 간격의 샘플로 표현하는 한, 샘플 간격보다 더 세밀한 변화는 정확히 복원할 수 없습니다.
이 한계를 신호 처리 이론에서 설명하는 것이 나이퀴스트 정리(Nyquist Theorem)입니다. 연속 신호를 이산 샘플로 정확히 복원하려면, 샘플링 주파수가 원래 신호의 최대 주파수의 2배 이상이어야 합니다. 이 조건을 만족하지 못하면 원래 신호에는 없던 가짜 패턴, 즉 앨리어스가 결과에 섞입니다.
래스터화도 같은 관점에서 볼 수 있습니다. 연속적인 장면을 픽셀 격자 위의 샘플로 표현하는 공간적 샘플링입니다. 여기서 샘플링 주파수는 픽셀이 얼마나 촘촘한지, 즉 해상도에 해당합니다. 신호의 주파수는 화면에서 색이나 밝기가 얼마나 짧은 거리 안에 바뀌는지를 뜻하는 공간 주파수에 해당합니다.
공간 주파수는 장면의 부분마다 다릅니다. 넓은 하늘처럼 색이 천천히 변하는 영역은 공간 주파수가 낮습니다. 체크무늬 바닥, 얇은 선, 삼각형 가장자리처럼 색이 짧은 거리 안에서 급격히 바뀌는 영역은 공간 주파수가 높습니다.
삼각형 가장자리는 특히 문제가 큽니다. 삼각형 안쪽과 바깥쪽이 한 경계에서 불연속적으로 바뀌기 때문에, 이론적으로 공간 주파수가 무한히 높습니다. 어떤 유한한 해상도도 이런 불연속 경계를 완전히 정확하게 샘플링할 수는 없습니다.
그래서 해상도를 올려도 앨리어싱이 완전히 사라지지는 않습니다. 픽셀이 촘촘해지면 계단의 크기는 작아지고 눈에 덜 띄지만, 유한한 샘플로 불연속 경계를 표현한다는 한계는 남습니다.
해상도를 올리면 프래그먼트 수와 메모리 대역폭이 함께 늘어나므로, 화질을 위해 무작정 높일 수만은 없습니다.
안티앨리어싱 기법들
표본화의 한계로 생기는 계단 현상을 줄이는 보정 기법을 통틀어 안티앨리어싱이라고 부릅니다.
안티앨리어싱 기법은 가장자리를 어디서, 무엇으로 다듬느냐에 따라 달라집니다. MSAA는 래스터화 단계에서 픽셀마다 표본을 더 찍고, FXAA와 SMAA는 완성된 이미지의 가장자리를 후처리로 분석해 다듬으며, TAA는 여러 프레임에 걸쳐 표본을 누적합니다. 여기서는 이 네 기법을 차례로 살펴봅니다.
MSAA (Multi-Sample Anti-Aliasing)
MSAA는 한 픽셀 안에 서브 샘플(Sub-sample)이라 부르는 표본 위치를 여러 개 두는 하드웨어 안티앨리어싱입니다. 삼각형이 픽셀을 얼마나 덮는지 판정할 때, 픽셀 중심 한 곳만 보지 않고 이 서브 샘플 각각이 삼각형 안에 들어오는지를 따로 확인합니다. 가장자리가 픽셀을 가로지르면 서브 샘플 가운데 일부만 삼각형에 덮이는데, 그중 몇 개가 덮였는지를 래스터화 단계에서 세어 둡니다. 이렇게 구한 덮인 비율을 커버리지(Coverage)라고 하며, 2x·4x·8x는 픽셀마다 두는 서브 샘플의 개수를 뜻합니다.
커버리지는 이렇게 서브 샘플 단위로 구하지만, 정작 색을 만드는 프래그먼트 셰이더는 보통 픽셀당 한 번만 실행됩니다. 서브 샘플마다 따로 계산되는 값은 커버리지와 깊이, 스텐실뿐입니다. 셰이더가 만든 하나의 색은 삼각형이 덮은 서브 샘플에만 기록되고, 덮지 않은 서브 샘플에는 기존 색이 그대로 남습니다. 출력할 픽셀 색을 정할 때는 이 서브 샘플 값들을 평균해 하나로 합치는데, 이 단계를 리졸브(Resolve)라고 합니다. 가장자리 픽셀에서는 삼각형 색과 배경색이 커버리지 비율만큼 섞여, 완전히 켜지거나 꺼지는 대신 중간색을 띱니다. 계단이 부드러워지는 것은 이 때문입니다.
가장자리에서 색이 섞이려면 서브 샘플 가운데 일부만 삼각형에 덮여야 합니다. 면 안쪽 픽셀은 사정이 다릅니다. 삼각형 경계가 걸치지 않아 모든 서브 샘플이 빠짐없이 삼각형에 덮이므로, 커버리지는 늘 가득 차고 섞을 색도 생기지 않습니다. 즉 MSAA가 보는 것은 삼각형이 픽셀을 얼마나 덮느냐일 뿐, 그 안에서 색이 어떻게 달라지는지가 아닙니다. 따라서 MSAA가 다듬어 주는 대상은 삼각형의 윤곽선에 한정됩니다.
면 안쪽의 색이 한결같다는 뜻은 아닙니다. 한 픽셀보다 잘게 변하는 색은 면 안쪽에도 얼마든지 있는데, MSAA는 이 색을 픽셀당 한 번만 계산하므로 그 변화를 담아내지 못합니다. 그래서 촘촘한 텍스처에는 모아레가 어른거리고, 좁은 스페큘러 하이라이트는 카메라가 조금만 움직여도 프레임마다 깜박입니다. 이것은 가장자리가 아니라 색 자체에서 생기는 앨리어싱이라, 커버리지만 다루는 MSAA로는 서브 샘플을 아무리 늘려도 줄지 않습니다.
MSAA의 비용은 두 곳에서 나옵니다. 색을 계산하는 셰이더 연산과, 그 결과를 메모리로 실어 나르는 대역폭입니다. 연산 쪽부터 보면 이 부담은 생각보다 가볍습니다. 앞서 설명한 대로 MSAA에서 셰이더는 픽셀당 한 번만 실행되기 때문입니다. 같은 목적의 SSAA(Super-Sample Anti-Aliasing)는 서브 샘플마다 셰이더를 실행해 4x에서는 셰이더 비용이 이론상 네 배가 되지만, MSAA는 커버리지만 서브 샘플별로 따로 구하고 색은 한 번만 계산하므로 셰이더 비용이 거의 그대로입니다.
연산이 이렇게 가벼운 대신, MSAA에서 실제로 부담이 되는 쪽은 대역폭입니다. 서브 샘플 수만큼 불어난 데이터를 메모리로 실어 나르는 일이 비용의 핵심이고, 이 부담이 얼마나 커지는지는 데이터가 머무는 위치, 곧 GPU 아키텍처에 따라 달라집니다.
GPU 곁에 붙은 온칩(on-chip) 메모리는 접근이 빠른 대신 용량이 작고, 화면을 통째로 담는 외부 메모리는 용량이 크지만 멀리 있어 데이터를 주고받는 데 시간과 전력이 더 듭니다. 그래서 아껴야 하는 것은 외부 메모리를 오가는 데이터의 양, 곧 대역폭입니다. 모바일 GPU가 주로 쓰는 TBDR(Tile-Based Deferred Rendering) 아키텍처는 화면을 작은 타일로 나눠 온칩 메모리에서 한 타일씩 그립니다. 늘어난 서브 샘플 데이터도 이 온칩에 머물다가, 리졸브로 하나의 색으로 합쳐진 뒤 최종 색만 외부 메모리로 나갑니다. 서브 샘플이 외부 메모리를 오가지 않으니 4x MSAA를 켜도 대역폭은 거의 늘지 않습니다. 반면 데스크톱 GPU에서 흔한 IMR(Immediate Mode Rendering) 방식은 서브 샘플 데이터를 외부 메모리에 그대로 저장하므로, 오가는 데이터가 샘플 수만큼 늘어 대역폭 비용도 그에 비례해 커집니다.
TBDR이 타일을 온칩에서 처리하는 방식은 GPU 아키텍처 (2)에서 자세히 다룹니다.
물론 TBDR에서도 비용이 아주 없지는 않습니다. 온칩 타일 메모리 사용량은 서브 샘플 수에 비례해 늘고, 리졸브에도 추가 연산이 듭니다. 타일 메모리가 커진 만큼 GPU에 따라 한 번에 처리하는 타일 크기가 줄기도 합니다. 그래도 대역폭에서 얻는 이점이 이 비용을 넘어서기 때문에, TBDR 기반 모바일에서는 MSAA가 첫 번째 선택지가 되는 경우가 많습니다.
Unity URP에서는 렌더 파이프라인 에셋의 Quality 설정에서 MSAA를 켜고 단계를 정합니다. 모바일에서는 보통 2x나 4x를 먼저 검토합니다.
다만 후처리(Post-Processing)와 함께 쓸 때는 한 가지 유의할 점이 있습니다. 후처리 효과는 렌더링된 화면을 텍스처로 읽어 가공하는데, 서브 샘플이 그대로 남아 있는 MSAA 버퍼는 이 상태로 일반 텍스처처럼 읽을 수 없습니다. 그래서 URP은 후처리 앞에 MSAA 버퍼를 하나로 합치는 리졸브 패스를 자동으로 넣습니다. 이 리졸브로 서브 샘플 정보가 사라지므로, 블룸이나 톤매핑처럼 그 뒤에 적용되는 후처리에는 MSAA의 가장자리 정보가 반영되지 않습니다.
FXAA (Fast Approximate Anti-Aliasing)
FXAA는 래스터화에는 개입하지 않고, 렌더링이 끝난 최종 이미지를 다듬는 후처리 기반 안티앨리어싱입니다. 픽셀마다 커버리지를 따로 구하던 MSAA와 달리, FXAA는 삼각형이나 깊이 같은 장면의 기하 정보를 전혀 참고하지 않습니다.
그 대신 화면에 이미 그려진 색에서 밝기 성분인 휘도(Luma)를 추출하여, 휘도가 급격히 변하는 자리를 경계로 판정합니다. 그렇게 찾은 경계 부근만 주변 픽셀과 부드럽게 섞어 계단을 누그러뜨립니다.
이렇게 완성된 이미지 한 장만 처리하므로 FXAA는 비용이 낮습니다. 화면 전체를 한 차례 훑는 풀스크린 후처리 한 패스로 끝나기 때문에, 비용은 주로 화면 해상도에 비례할 뿐 삼각형 수나 장면의 복잡도와는 거의 무관합니다.
비용이 낮다는 점과 함께 살펴야 할 것이 보정 범위입니다. 경계를 휘도 하나로만 가려내기 때문에, 그 범위는 삼각형 가장자리에 머물지 않습니다. 텍스처의 무늬든 셰이더가 만들어 낸 고대비 경계든, 화면에서 밝기가 가파르게 달라지는 자리라면 출처를 가리지 않고 모두 보정 대상이 됩니다. 커버리지만 보던 MSAA로는 다루지 못하던 자리까지 함께 누그러지는 것은 이 때문입니다.
출처를 가리지 않는다는 바로 그 점이 선명도에는 손해가 됩니다. 휘도 대비만 볼 뿐 기하 정보가 없으니, FXAA는 실제 물체의 경계와 또렷하게 그려 넣은 무늬를 가려내지 못합니다. 그래서 흐려져서는 안 될 글자나 UI의 가는 선, 텍스처의 미세한 무늬까지 경계로 오인해 함께 뭉개기도 합니다.
이러한 장단점 때문에, 어느 기법이 알맞은지는 환경에 따라 달라집니다. 모바일에서는 FXAA보다 MSAA를 먼저 검토하는 경우가 많습니다. 앞서 설명한 대로 TBDR 기반 GPU에서는 MSAA의 대역폭 비용이 낮게 유지되는 반면, FXAA에서는 텍스처 디테일이 흐려지는 손실이 더 두드러지기 때문입니다. 반대로 이미 후처리 파이프라인을 거치고 있거나 MSAA를 적용하기 어려운 환경이라면, 패스 하나로 끝나는 FXAA가 가벼운 대안이 됩니다.
TAA (Temporal Anti-Aliasing)
MSAA와 FXAA가 한 프레임 안에서 문제를 해결하려는 방식이라면, TAA는 여러 프레임의 정보를 누적해 사용하는 시간적(Temporal) 안티앨리어싱입니다.
출발점은 지터링(Jittering)입니다. 프레임마다 카메라의 투영 행렬에 서브 픽셀 크기의 작은 오프셋을 더하면, 장면이 화면에서 1픽셀보다 작은 거리만큼 이동한 것처럼 샘플링됩니다. 그러면 삼각형 가장자리가 픽셀 격자와 만나는 위치도 프레임마다 조금씩 달라집니다.
TAA는 이렇게 서로 다른 서브 픽셀 위치에서 얻은 결과를 여러 프레임에 걸쳐 누적합니다. 한 프레임에서는 샘플 수가 부족해도, 시간 축으로 여러 샘플을 모으면 더 매끄러운 가장자리를 만들 수 있습니다.
샘플을 한 프레임에 몰지 않고 시간 축에 나누어 모으기 때문에, TAA는 프레임당 샘플 수를 크게 늘리지 않고도 품질을 높일 수 있습니다. 삼각형 가장자리는 물론, 셰이더 앨리어싱이나 스페큘러 하이라이트의 깜박임처럼 프레임마다 흔들리는 요소까지 누그러뜨립니다.
하지만 여러 프레임을 누적하기 때문에 생기는 약점도 있습니다. 대표적인 문제가 고스팅(Ghosting)과 흐려짐입니다.
고스팅은 빠르게 움직이는 오브젝트 뒤로 이전 프레임의 잔상이 따라붙는 현상입니다. 이런 잔상이 생기는 이유는 TAA가 이전 프레임까지 누적한 히스토리를 현재 프레임에 다시 쓰기 때문입니다. 오브젝트나 카메라가 움직이면 같은 대상이라도 화면에 맺히는 위치가 프레임마다 달라지는데, 화면 좌표가 같다는 이유로 이전 색을 그대로 누적하면 이미 옮겨 간 위치와 어긋난 색이 남아 잔상처럼 번집니다.
이 어긋남을 바로잡으려면 누적해 둔 히스토리를 대상이 옮겨 간 위치까지 따라가게 해야 합니다. 이렇게 이전 프레임의 픽셀을 현재 프레임의 좌표로 옮기는 과정이 재투영(Reprojection)이고, 그 좌표를 알려 주는 정보가 모션 벡터(Motion Vector)입니다. 모션 벡터는 각 픽셀이 이전 프레임에서 현재 프레임으로 얼마나 이동했는지 기록하며, TAA는 이를 따라 히스토리 값을 현재 위치에서 끌어옵니다.
그래도 모든 어긋남이 사라지지는 않습니다. 이전 프레임에서는 다른 오브젝트에 가려져 있다가 현재 프레임에서 새로 드러난 영역(disocclusion)은 끌어올 과거 데이터가 없습니다. 이런 영역에서는 잘못된 히스토리가 섞이거나, 히스토리를 버리면서 노이즈가 생기기도 합니다.
두 번째 약점인 흐려짐은 여러 프레임을 평균하는 과정에서 생깁니다. 카메라가 정지해 있으면 샘플이 안정적으로 누적되지만, 카메라나 오브젝트가 움직이는 동안에는 누적된 히스토리가 현재 프레임과 완전히 맞지 않아 이미지가 부드럽지만 흐릿하게 보일 수 있습니다.
그래서 TAA 뒤에는 보통 샤프닝(Sharpening) 패스를 추가해 선명도를 보정합니다. Unity URP의 TAA도 샤프닝 옵션을 함께 제공합니다.
TAA는 데스크톱과 콘솔에서 두루 쓰이며, Unity URP에서도 카메라 컴포넌트의 Anti-aliasing 설정에서 TAA를 선택할 수 있습니다.
모바일에서도 사용할 수 있지만, 모션 벡터 생성과 히스토리 버퍼 저장이 필요합니다. 메모리와 대역폭, 후처리 비용을 실제 기기에서 확인해야 합니다.
SMAA (Subpixel Morphological Anti-Aliasing)
SMAA도 FXAA처럼 최종 이미지를 대상으로 하는 후처리 기반 안티앨리어싱입니다. 다만 경계를 찾고 보정하는 방식이 더 정교합니다.
FXAA가 인접 픽셀의 밝기 차이만으로 경계를 추정한다면, SMAA는 검출한 경계가 어떤 형태(morphology)를 띠는지까지 분석합니다. 경계가 어느 방향으로 이어지는지, L자나 Z자 같은 패턴을 이루는지 판단한 뒤, 그 형태에 맞춰 픽셀마다 적용할 블렌딩 가중치를 계산합니다.
이렇게 윤곽의 생김새를 따라 색을 섞기 때문에, SMAA는 밝기만 보고 뭉뚱그리던 FXAA보다 가장자리를 또렷하게 남기고 흐려짐도 적은 편입니다. 대신 경계 검출, 블렌딩 가중치 계산, 최종 블렌딩의 세 단계 패스로 나뉘므로 비용은 FXAA보다 높습니다.
Unity URP에서는 카메라 컴포넌트의 Anti-aliasing 설정에서 SMAA를 선택할 수 있고, Low·Medium·High의 품질 단계를 함께 지정합니다.
안티앨리어싱 기법 비교
네 기법의 가장 큰 차이는 앨리어싱을 어디서 다루느냐에 있습니다. MSAA는 래스터화 단계에서 삼각형 가장자리만 직접 처리해 흐려짐이 없는 대신 셰이더가 만든 경계는 잡지 못하고, 나머지 셋은 완성된 이미지를 손봐 그런 경계까지 다루는 대신 흐려짐을 어느 정도 감수합니다. 지금까지 살펴본 내용을 표로 정리하면 다음과 같습니다.
| MSAA | FXAA | TAA | SMAA | |
|---|---|---|---|---|
| 유형 | 하드웨어 (래스터화 단계) | 후처리 (이미지 기반) | 시간적 (프레임 누적) | 후처리 (형태 분석) |
| 작동 위치 | 래스터화 | 최종 이미지 (1 패스) | 최종 이미지 (1 패스 + 히스토리) | 최종 이미지 (3 패스) |
| 가장자리 품질 | 좋음 (기하 정보 기반) | 보통 (밝기 차이 기반) | 좋음 (서브 픽셀 누적) | 좋음 (형태 기반 분석) |
| 셰이더 앨리어싱 | 효과 없음 (커버리지만) | 효과 있음 (모든 경계) | 효과 있음 (모든 경계) | 효과 있음 (모든 경계) |
| 흐려짐 | 없음 | 있음 | 있음 (움직임) | 적음 |
| 비용 | TBDR에서 낮음 / IMR에서 중간 | 낮음 | 중간 | 중간~높음 |
| 모바일 적합도 | 권장 (TBDR 이점) | 대안 (비용 낮음) | 조건부 사용 (메모리 비용) | 제한적 (패스 비용) |
선택 기준은 대상 플랫폼과 문제의 종류입니다. TBDR 기반 모바일에서는 MSAA의 대역폭 비용이 낮은 편이므로 MSAA 2x 또는 4x를 먼저 검토할 만합니다. 셰이더 앨리어싱이나 텍스처 내부의 고주파 패턴까지 줄여야 한다면 FXAA, SMAA, TAA 같은 후처리 기반 기법이 후보가 됩니다. 셋 중에서는 비용을 가장 아끼려면 FXAA, 흐려짐을 더 줄이려면 SMAA, 모션 벡터와 히스토리 버퍼를 감당할 수 있고 움직이는 장면의 시간적 안정성까지 얻으려면 TAA가 적합합니다. 어느 쪽이든 최종 선택은 실제 기기에서 품질과 프레임 시간을 직접 재 보고 결정하는 것이 가장 안전합니다.
마무리
이번 글에서는 완성된 프레임이 디스플레이에 표시되는 과정과, 래스터화의 샘플링 한계로 생기는 앨리어싱을 줄이는 방법을 정리했습니다. 핵심은 다음과 같습니다.
- 스캔아웃은 프레임 버퍼를 왼쪽 위부터 한 줄씩 읽어 디스플레이로 보내는 과정입니다. 한 프레임의 스캔이 끝난 뒤 다음 스캔이 시작되기 전의 짧은 구간이 VBlank입니다.
- 티어링은 스캔아웃 도중 버퍼가 교환되어 한 화면에 서로 다른 프레임이 섞이는 현상입니다.
- 더블 버퍼링은 프론트 버퍼와 백 버퍼를 분리해 렌더링 중인 프레임이 화면에 직접 노출되지 않도록 합니다.
- VSync는 버퍼 교환을 VBlank에 맞춰 티어링을 막습니다. 대신 입력 지연이 늘 수 있고, 프레임 시간이 VBlank 간격을 넘기면 60fps에서 30fps처럼 표시 간격이 크게 늘어날 수 있습니다.
- 트리플 버퍼링은 백 버퍼를 하나 더 두어 GPU 대기 시간을 줄입니다. 대신 메모리 사용량이 늘고, 프레젠트 방식에 따라 입력 지연이 더 커질 수 있습니다.
- 앨리어싱은 연속적인 도형을 유한한 픽셀 격자로 샘플링할 때 생기는 계단 현상입니다. 해상도를 높이면 줄어들지만 완전히 사라지지는 않습니다.
- 나이퀴스트 정리는 샘플링 주파수가 신호의 최대 주파수보다 충분히 높아야 한다는 기준을 제시합니다. 삼각형 경계처럼 불연속적인 영역은 유한한 픽셀 격자로 완벽히 복원할 수 없으므로 안티앨리어싱이 필요합니다.
- MSAA는 픽셀 안의 여러 서브 샘플로 커버리지를 계산해 삼각형 가장자리를 부드럽게 만듭니다. TBDR 기반 모바일에서는 대역폭 비용이 낮을 수 있어 먼저 검토할 만한 기법입니다.
- FXAA는 최종 이미지의 대비를 보고 경계를 부드럽게 만드는 후처리 기법입니다. 비용은 낮지만 텍스트나 텍스처 디테일이 흐려질 수 있습니다.
- SMAA는 후처리 기반이면서도 경계의 형태까지 분석해 FXAA보다 선명한 결과를 냅니다. 대신 여러 패스를 거치므로 비용이 더 높습니다.
- TAA는 프레임마다 서브 픽셀 위치를 바꾸고 이전 프레임을 누적해 품질을 높입니다. 셰이더 앨리어싱에도 효과가 있지만, 고스팅과 흐려짐을 관리해야 합니다.
이 글이 다룬 두 문제는 모두 디스플레이가 공간과 시간 양쪽에서 잘게 나뉘어 있다는 데서 비롯됩니다. 화면은 픽셀이라는 격자로 공간을 나누고, 갱신 주기라는 간격으로 시간을 나눕니다. 픽셀 격자로 샘플링하는 데서 생기는 계단은 안티앨리어싱이, 갱신 주기와 어긋나 생기는 티어링은 VSync와 버퍼링이 다룹니다.
관련 글
전체 시리즈
- 하드웨어 기초 (1) - CPU 아키텍처와 파이프라인
- 하드웨어 기초 (2) - 메모리 계층 구조
- 하드웨어 기초 (3) - GPU의 탄생과 발전
- 하드웨어 기초 (4) - 모바일 SoC
- 그래픽스 수학 (1) - 벡터와 벡터 연산
- 그래픽스 수학 (2) - 행렬과 변환
- 그래픽스 수학 (3) - 좌표 공간의 전환
- 그래픽스 수학 (4) - 투영
- C# 런타임 기초 (1) - 값 타입과 참조 타입
- C# 런타임 기초 (2) - .NET 런타임과 IL2CPP
- C# 런타임 기초 (3) - 가비지 컬렉션의 기초
- C# 런타임 기초 (4) - 스레딩과 비동기
- 색과 빛 (1) - 빛의 물리적 원리
- 색과 빛 (2) - 색 표현과 색공간
- 색과 빛 (3) - 셰이딩 모델
- 래스터화 파이프라인 (1) - 삼각형에서 프래그먼트까지
- 래스터화 파이프라인 (2) - 출력 병합
- 래스터화 파이프라인 (3) - 디스플레이와 안티앨리어싱 (현재 글)
- Unity 엔진 핵심 (1) - GameObject와 Component
- Unity 엔진 핵심 (2) - Transform 계층과 씬 그래프
- Unity 엔진 핵심 (3) - Unity 실행 순서
- Unity 엔진 핵심 (4) - Unity의 스레딩 모델
- Unity 에셋 시스템 (1) - Asset Import Pipeline
- Unity 에셋 시스템 (2) - Serialization과 Instantiation
- Unity 에셋 시스템 (3) - Scene Management
- Unity 렌더링 (1) - Camera와 Rendering Layer
- Unity 렌더링 (2) - Render Target과 Frame Buffer
- Unity 렌더링 (3) - Render Pipeline 개요