역자의 말: 코드 해설이 부족해서 번역문에 코드 해설을 추가 했습니다.
저자: jackie 偶尔不帅
문제와 요구사항
지형 텍스처를 단순하게 타일링하면 강한 반복감이 생기는데, 이건 늘 골머리를 앓게 만드는 문제다. 그런데 UE4 내장 예제든, iquilezles의 알고리즘이든, 그 외 다른 알고리즘들이든 기본적으로 모두 여러 번 샘플링한 뒤 블렌딩하는 방식이다. UE4의 경우 noise macroVariation 4회 + baseColor 2회 샘플링이고, 지형 법선 맵까지 고려하면 총 6회 샘플링이 필요하다. 성능이 눈에 띄게 떨어진다. iquilezles에는 4회, 9회, 3회 샘플링 알고리즘이 각각 있는데, 법선까지 포함하면 이것도 두 배가 된다.
그래서 나는 샘플링 횟수를 단 1번도 늘리지 않고 이 기능을 만족시키는, 즉 켜도 눈에 띄게 프레임이 떨어지지 않는 고성능 알고리즘을 생각해봤다. 쓸만한 바퀴가 되길 바란다.
iquilezles의 첫 번째 4회 샘플링 알고리즘을 예로 들면, 최종적으로 4번의 샘플링이 이루어지는 것을 볼 수 있다.

iquilezles 원본 알고리즘
코드를 뜯어보면 이렇다. uv를 정수부(iuv, 타일 좌표)와 소수부(fuv, 타일 내 위치)로 분리하는 것부터 시작한다. 그다음, 현재 픽셀 주변의 4개 타일 코너에 대해 각각 hash4()로 무작위 변환값을 뽑아낸다. 여기서 .xy는 UV 오프셋, .zw는 반전(flip) 여부다.
sign( o**.zw - 0.5 ) 이 한 줄이 핵심인데, 해시값 [0,1] 범위를 {-1, +1}로 변환해버린다. 즉 50% 확률로 텍스처를 수평/수직 반전시키는 것이다. 이렇게 하면 타일 경계마다 방향이 달라지니 눈에 띄는 반복 패턴이 자연스럽게 깨진다.
그 다음은 각 코너마다 변환된 UV(uva ~ uvd)와, MIP 레벨 계산을 위한 화면 공간 편미분값(ddx, ddy)까지 같이 변환해서 textureGrad()로 샘플링한다. MIP 계산에 편미분을 직접 넘기지 않으면 UV가 뒤집히거나 늘어날 때 밉맵이 엉뚱하게 선택되니 이 처리는 필수다.
마지막으로 smoothstep(0.25, 0.75, fuv)으로 블렌딩 가중치 b를 구해, 4개 샘플을 bilinear 방식으로 혼합(mix)해서 반환한다. 결국 텍스처 샘플링이 4번 일어나는 구조다. 효과는 확실하지만, 그게 바로 문제다.

처리 전 타일링 문제

iquilezles 원본 알고리즘 결과
화면 반복감은 해소됐지만 604에서 437로 프레임이 뚝 떨어진다. 내 2060 그래픽카드에서 0.63ms를 잡아먹는다. 이 정도 오버헤드는 대부분의 프로젝트에서 받아들이기 어렵다. 더 효율적인 방안이 필요하다.
기본 아이디어
한 마디로, 디더 샘플링(Dithered Sampling)이다. 블렌딩 비율 대신 확률을 쓰는 것이다.
젊은 시절 디더 반투명(Dithered Transparency)을 익힌 이후로, 디더링이라는 사고방식은 항상 내 머릿속에서 맴돌고 있다. 어떤 순간에는, 이 현실 자체도 평행 우주의 디더 샘플링 결과에 불과한 건 아닐까 하는 생각이 들 정도였다. 이 개념을 처음 접하는 분들을 위해 좀 유치한 예를 들어보겠다.
수박 두 쪽을 두 사람이 나눠 먹는다. A는 60%, B는 40%를 가져가야 한다. 수박을 자르지 않으면 1회 샘플링이고, 한 번 자르면 2회 샘플링이다. 수박이 소수라면 각 수박을 4:6으로 잘라서 나누는 게 가장 합리적이다. 하지만 수박이 충분히 많다면 굳이 자를 필요가 없다. AABAB, AABAB... 이런 식으로 나눠가면 수박이 많아질수록 설정한 비율에 점점 가까워진다.
실전 알고리즘
그런데 코드로 구현해보니, 이 방식은 디더 반투명에서 쓰는 디더 배열이나 디더 텍스처를 따로 도입할 필요가 없었다. 알고리즘 자체가 자체 디더링 효과를 갖추고 있어서, 최고의 성능을 달성할 수 있다.

원본 알고리즘과 디더 알고리즘 비교
#if 0 블록이 앞서 설명한 원본 알고리즘이다. 4개 타일 코너에 대해 UV와 편미분을 각각 계산하고, SampleGrad()를 4번 호출한 뒤 mix()로 블렌딩한다. 결과는 예쁘지만 비용이 크다.
#else 블록이 디더 알고리즘이다. 딱 한 줄이 전부를 바꾼다.
b = saturate(sign(b - 0.5));
원본에서 b는 smoothstep으로 만든 [0,1] 범위의 부드러운 블렌딩 가중치였다. 여기서 sign(b - 0.5)를 적용하면 0.5를 기준으로 {-1, 0, +1}, 실질적으로 {0, 1}로 딱 잘려버린다. 부드러운 블렌딩이 아니라 경계를 기준으로 어느 쪽 타일을 쓸지 이진 선택이 되는 것이다.
그 다음 줄이 핵심이다.
vec4 ofDither = lerp(lerp(ofa, ofb, b.x), lerp(ofc, ofd, b.x), b.y);
b가 이제 0 또는 1의 정수값이니까, 이 lerp 중첩은 사실상 ofa / ofb / ofc / ofd 중 하나를 정확히 선택하는 것과 같다. 4개를 섞는 게 아니라 4개 중 1개를 고르는 것이다. 그러면 샘플링도 당연히 1번만 하면 된다.
마지막으로 ddxyScale과 finalUV를 out 파라미터로 내보내는 게 보이는데, 주석에도 써있듯이 albedo / normal / height 등 여러 텍스처가 동일한 UV를 공유하기 위한 구조다. UV 계산을 한 번만 하고 여러 텍스처가 그걸 그대로 재활용하니, 텍스처 수가 늘어날수록 원본 대비 이득이 더 커진다.

디더 블렌딩 방안
보다시피 이렇게 구현하면 프레임이 578FPS까지 올라간다. 0.07ms vs 0.63ms, 거의 한 자릿수 차이다.
ps: 이건 기본 효과를 유지하면서 극한의 성능을 추구하는 방식이다. 효과 품질이 더 좋고 성능 여유가 있는 알고리즘이 필요하다면 이 방안은 참고하지 않는 편이 낫다.
2021/11/16 업데이트
실제 적용 시 발생한 문제와 수정
프로젝트에 실제로 적용하자 아트 팀에서 일부 텍스처에 눈에 띄는 이음매(seam)가 생긴다는 피드백이 들어왔다.
Shadertoy로 빠르게 검증해보니 확실히 이음매가 매우 눈에 띈다. 패턴이 불규칙한 특정 텍스처에서만 결과가 그나마 괜찮은 편이다.

본 방안 이음매 버그
애초 계획대로 디더 랜덤을 추가하면 망 형태의 전환이 나타난다. 이걸 안티에일리어싱과 함께 쓰면 꽤 괜찮아진다.

실제 에셋 + 안티에일리어싱 처리

본 방안 이음매 문제

디더 알고리즘 적용 후
디더 알고리즘을 적용한 후, 즉 b = saturate(sign(b - 0.5)) 대신 b = saturate(sign(b - DITHER_THRESHOLDS[index])) 로 바꾼 뒤, 이음매 문제는 해결됐지만 작은 구멍들이 많이 생겼다. 이건 안티에일리어싱만 켜면 된다. 아래 두 가지 안티에일리어싱 결과를 비교해보자. 이렇게 처리하면 프로젝트에서 실제로 사용 가능한 수준이 된다. TAA를 선택하는 경우에는 TAA 지터 파라미터에 맞춰 동적 오프셋을 적용해야 한다. 그렇지 않으면 이전 프레임과 제대로 혼합되지 않는다.


디더 알고리즘 + FXAA

디더 알고리즘 + TAA
'TECH.ART.FLOW.IO' 카테고리의 다른 글
| [번역] 섀도우맵 압축 테크닉 (0) | 2026.02.20 |
|---|---|
| [번역] 뉴럴 렌더링 탐험기 (0) | 2026.02.19 |
| [번역] Call of Dragons》 렌더링 프레임 캡처 분석과 미스터리 (0) | 2026.02.18 |
| [번역] Impostors 상세 해설 — 종이 한 장으로 만들어낸 아름다운 환상 (0) | 2026.02.17 |
| [번역] Unity CSM 셰도우 개조하기 (0) | 2026.02.13 |