TAA 고스팅 원인 찾기: 카메라 모션 벡터 오류 자동 캡처와 에셋 추적
Unreal Engine 5.x에서 카메라 모션 벡터와 이전 프레임 행렬이 어긋날 때 발생하는 TAA 고스팅을 자동 감지·캡처하고 velocity·depth·object ID로 원인 에셋을 추적하는 실전 가이드입니다.

핵심 요약
TAA 고스팅은 현재 프레임과 이전 프레임의 화면 좌표를 연결하는 모션 벡터가 실제 움직임과 다를 때 발생한다. Unreal Engine 5.x에서는 카메라의 현재·이전 View-Projection 행렬로 계산한 예상 velocity와 렌더러가 출력한 실제 velocity를 비교하면 오류 픽셀을 자동으로 검출할 수 있다.
오류 픽셀에 32비트 Object ID 또는 Asset Debug ID를 함께 기록하고 velocity·depth·color·메타데이터를 짧은 프레임 구간으로 저장하면 문제가 발생한 메시, 스켈레탈 메시, 머티리얼, LOD 또는 VFX 에셋을 재현 과정 없이 추적할 수 있다.
TAA 고스팅은 어떤 원리로 발생하는가?
TAA는 현재 프레임의 픽셀에 이전 프레임의 색상을 재투영해 안티앨리어싱한다. 이때 일반적인 흐름은 다음과 같다.
- 현재 픽셀의 화면 좌표를 구한다.
- 모션 벡터로 이전 프레임의 좌표를 계산한다.
- 이전 프레임 색상을 샘플링한다.
- 현재 색상과 이력을 가중 평균한다.
정적인 월드 오브젝트라도 카메라가 움직이면 화면 안에서는 위치가 변한다. 따라서 카메라 이동을 누락한 velocity는 정적 오브젝트를 화면상 정지한 것으로 판단하게 만들고 잘못된 위치의 history color를 섞는다. 그 결과 밝은 가장자리, 나뭇잎, 파티클, 캐릭터 실루엣에 잔상이 남는다.
현재 프레임과 이전 프레임의 클립 좌표는 다음처럼 표현할 수 있다.
여기서 는 Projection Matrix, 는 View Matrix, 은 오브젝트 Transform이다. 정적 오브젝트라면 월드 좌표 는 같지만 카메라가 움직이므로 와 가 달라진다.
NDC를 UV로 변환한 뒤 velocity를 계산한다.
단, 엔진의 TAA 구현에 따라 부호가 반대일 수 있다. 예를 들어 history UV를 로 계산한다면 위와 같은 방향을 사용한다. 가장 먼저 velocity의 부호와 UV 원점 방향부터 검증해야 한다.
카메라 모션 벡터 오류의 대표적인 원인은 무엇인가?
| 증상 | 주요 원인 | 우선 확인할 항목 |
|---|---|---|
| 카메라를 회전할 때 모든 정적 메시가 고스팅됨 | 이전 카메라 행렬 누락 또는 잘못된 행렬 사용 | Previous View-Projection Matrix, velocity 부호 |
| 카메라 이동 시 화면 전체가 이중으로 흔들림 | 현재·이전 프레임의 jitter를 중복 적용 | Temporal Jitter 처리 방식 |
| 캐릭터만 잔상이 남음 | 이전 본 포즈 또는 이전 스킨 변환 미제공 | Previous Bone Transform, Previous Pose |
| 나뭇잎과 천만 고스팅됨 | World Position Offset 또는 버텍스 변형의 이전 상태 누락 | 현재·이전 WPO 값 |
| LOD 전환 순간에 긴 잔상이 생김 | 현재와 이전 프레임의 기하 구조가 달라짐 | LOD 전환 시 history invalidation |
| 파티클 일부만 이상하게 움직임 | GPU 파티클의 이전 위치 또는 spawn 상태 누락 | Particle ID, Previous Position |
| 반투명 효과에만 문제가 있음 | 별도 velocity pass가 없거나 TAA 처리 대상이 다름 | Translucency velocity와 history 정책 |
특히 World Position Offset을 사용하는 foliage, cloth, wind, vertex animation은 카메라 모션 벡터 문제로 오인하기 쉽다. 실제로는 카메라 행렬이 아니라 머티리얼이 이전 프레임의 변형 위치를 제공하지 않는 경우가 많다.
예상 velocity와 실제 velocity를 어떻게 비교할까?
정적 오브젝트의 예상 velocity는 현재 depth로 복원한 월드 위치를 이전 카메라 행렬에 다시 투영해 계산할 수 있다. 핵심은 현재 velocity pass가 사용하는 좌표계와 동일한 좌표계로 비교하는 것이다.
float2 NdcToUv(float2 ndc)
{
return float2(0.5f * (ndc.x + 1.0f), 0.5f * (1.0f - ndc.y));
}
float2 ComputeExpectedVelocity(
float3 worldPosition,
float4x4 currentViewProjection,
float4x4 previousViewProjection)
{
float4 currentClip = mul(currentViewProjection, float4(worldPosition, 1.0f));
float4 previousClip = mul(previousViewProjection, float4(worldPosition, 1.0f));
float2 currentNdc = currentClip.xy / max(abs(currentClip.w), 0.0001f);
float2 previousNdc = previousClip.xy / max(abs(previousClip.w), 0.0001f);
float2 currentUv = NdcToUv(currentNdc);
float2 previousUv = NdcToUv(previousNdc);
return currentUv - previousUv;
}
실제 구현에서는 다음 조건을 반드시 맞춰야 한다.
- 현재·이전 행렬에 Temporal Jitter를 포함할지 제외할지 일관되게 결정한다.
- velocity의 단위를 픽셀 단위와 UV 단위 중 하나로 통일한다.
- velocity buffer가 8비트로 압축되어 있다면 비교용 패스에는 R16G16 또는 R32G32 형식을 사용한다.
- 하늘, depth discontinuity, 화면 밖 좌표는 오류 통계에서 제외한다.
- 스키닝이나 WPO가 있는 오브젝트에는 현재 위치를 이전 위치로 재사용하지 않는다.
실제 velocity와 예상 velocity의 차이는 다음과 같이 계산한다.
오류 임계값 를 정해 인 픽셀을 후보로 만든다. 카메라가 거의 움직이지 않는 프레임은 작은 수치 오차도 크게 보일 수 있으므로 카메라 이동량 또는 화면상 속도가 일정 수준 이상일 때만 검사하는 편이 안정적이다.

TAA 고스팅을 자동 캡처하는 방법은?
Step 1. 고스팅 감지 조건을 정의한다
프레임마다 다음 조건을 계산한다.
- 카메라 회전 또는 이동량이 임계값 이상인가?
- 유효 depth 픽셀 중 velocity 오류 픽셀의 비율이 임계값 이상인가?
- 오류가 한 프레임이 아니라 2프레임 이상 연속되는가?
- 색상 차이가 velocity 오류 방향으로 길게 이어지는가?
간단한 트리거는 다음과 같이 구성할 수 있다.
captureTrigger =
cameraMotion > cameraThreshold
and errorPixelRatio > errorThreshold
and consecutiveErrorFrames >= 2
고스팅은 문제가 보이는 프레임보다 한두 프레임 앞에서 잘못된 history가 만들어지는 경우가 많다. 따라서 매 프레임 결과를 8~16프레임 링 버퍼에 보관한 뒤 트리거가 발생하면 이전 프레임과 이후 3~4프레임을 함께 저장한다.
Step 2. 디버그 렌더 타깃을 함께 출력한다
Unreal Engine 5.x에서는 일반 색상 캡처만으로 원인을 찾기 어렵다. 자체 Render Dependency Graph 디버그 패스나 Movie Render Queue 기반 캡처 도구에서 다음 출력을 같은 프레임 번호로 저장한다.
| 출력 | 용도 |
|---|---|
| Color | 실제 고스팅이 보이는 화면 |
| Depth | 월드 위치 복원과 유효 픽셀 판정 |
| Actual Velocity | 렌더러가 제공한 모션 벡터 |
| Expected Velocity | 현재 카메라와 depth로 계산한 기준값 |
| Velocity Error | 실제값과 기준값의 차이 |
| Object ID | 오류 픽셀을 렌더 프리미티브에 연결 |
| TAA History 또는 History Weight | history가 얼마나 사용됐는지 확인 |
빠른 시각화에는 Custom Depth와 Custom Stencil을 사용할 수 있다. 그러나 Unreal의 8비트 Custom Stencil은 복잡한 장면에서 고유 에셋 ID를 표현하기에 부족할 수 있으므로 자동 분석 패스에는 R32_UINT Object ID를 권장한다.
Step 3. Object ID를 에셋 정보로 매핑한다
각 렌더 프리미티브 또는 인스턴스에 다음 정보를 연결한다.
- Asset Path 또는 Soft Object Path
- Actor와 Component 이름
- Material Slot
- Skeletal Mesh 여부
- 현재 LOD와 HLOD 상태
- Instanced Mesh의 Instance ID
- WPO, Cloth, GPU Particle 사용 여부
인스턴스 메시가 모두 같은 ID를 사용하면 문제가 발생한 풀이나 나무 묶음만 찾을 수 있다. 인스턴스별 분석이 필요하다면 Per-Instance Custom Data나 Draw ID를 별도 디버그 버퍼에 기록해야 한다.
Step 4. 에셋별 오류 점수를 계산한다
오류 픽셀을 Object ID별로 집계한다. 큰 메시가 단순히 화면을 많이 차지한다는 이유로 항상 1순위가 되지 않도록 평균 오류와 커버리지 비율을 함께 사용한다.
여기서 는 에셋 의 우선순위 점수이고 는 오류 크기의 95백분위수다. 프로젝트에 맞춰 가중치를 조정하면 된다.
분석기의 의사 코드는 다음과 같다.
for frame in captureWindow:
for pixel in validPixels(frame):
assetId = objectId[pixel]
stats[assetId].totalPixels += 1
error = length(actualVelocity[pixel] - expectedVelocity[pixel])
if error <= velocityThreshold:
continue
stats[assetId].errorSum += error
stats[assetId].errorValues.append(error)
stats[assetId].errorPixels += 1
for assetId in stats:
stats[assetId].meanError = stats[assetId].errorSum / max(stats[assetId].errorPixels, 1)
stats[assetId].coverage = stats[assetId].errorPixels / max(stats[assetId].totalPixels, 1)
stats[assetId].score = Rank(stats[assetId])

Step 5. 캡처 결과에 원인 후보를 기록한다
캡처 파일만 저장하지 말고 한 번의 오류를 하나의 진단 이벤트로 기록한다. 예시는 다음과 같은 YAML 형식으로 충분하다.
capture:
frame: 18420
cameraMotion: 0.037
errorPixelRatio: 0.084
threshold: 0.002
topCandidates:
- assetId: 417
assetPath: /Game/Environment/Foliage/SM_Birch_03
component: BirchCluster_12
material: M_Foliage_Wind
lod: 1
meanError: 0.018
coverage: 0.42
suspectedCause: missing_previous_wpo
- assetId: 91
assetPath: /Game/Characters/Enemy/SK_Enemy_A
component: Enemy_A_07
material: M_Enemy
suspectedCause: previous_pose_mismatch
이 정보가 있으면 아티스트는 긴 플레이 영상을 다시 확인하지 않고 해당 에셋의 머티리얼, 애니메이션, LOD 설정부터 점검할 수 있다.
오류 유형별로 어떤 수정이 필요한가?
카메라 행렬 문제
모든 정적 오브젝트에서 같은 방향의 오류가 발생한다면 다음 순서로 확인한다.
- 현재 프레임과 이전 프레임의 View Matrix가 올바른 순서로 저장되는지 확인한다.
- Projection Matrix의 FOV, Aspect Ratio, Near/Far Plane이 프레임 사이에 일치하는지 확인한다.
- TAA jitter를 velocity에 중복 적용하지 않는지 확인한다.
- 멀티플레이어 또는 컷신 카메라 교체 시 이전 카메라를 무효화하는지 확인한다.
- 카메라 컷, 텔레포트, 큰 FOV 변화에서는 history를 폐기한다.
스켈레탈 메시 문제
캐릭터에만 문제가 있다면 현재 본 포즈와 이전 본 포즈가 같은 버텍스에 대응하는지 확인한다. 애니메이션 업데이트 순서가 velocity pass보다 늦으면 이전 포즈가 현재 포즈로 덮어써질 수 있다. 이런 경우 렌더러가 사용할 Previous Pose를 별도로 보존해야 한다.
WPO와 버텍스 애니메이션 문제
바람, 천, 물결, 파괴 효과처럼 머티리얼에서 위치를 변경한다면 이전 프레임의 변형값도 계산해야 한다. 시간 기반 함수에 현재 시간만 넣고 이전 시간 또는 이전 변형 상태를 제공하지 않으면 실제 움직임과 velocity가 달라진다.
LOD와 파티클 문제
LOD가 바뀌는 프레임에는 이전 메시와 현재 메시의 버텍스 대응이 보장되지 않는다. 이때는 잘못된 velocity를 억지로 계산하기보다 해당 프레임의 history를 부분적으로 무효화하는 편이 안정적이다. GPU 파티클은 spawn, kill, teleport 이벤트에서 이전 위치를 현재 위치로 초기화해야 불필요한 긴 velocity가 생기지 않는다.
자동 검증 도구를 운영할 때의 체크리스트
- 카메라 이동, 회전, FOV 변화가 포함된 고정 테스트 시퀀스가 있는가?
- 최소 8프레임 이상의 링 버퍼가 있는가?
- Color, Depth, Actual Velocity, Expected Velocity를 같은 타임스탬프로 저장하는가?
- velocity 비교 시 jitter, UV 방향, 단위를 통일했는가?
- 하늘과 depth 경계 픽셀을 오류 통계에서 제외하는가?
- Object ID가 에셋 경로와 컴포넌트 정보로 역조회되는가?
- 인스턴스, 스켈레탈 메시, WPO 에셋을 구분하는가?
- 카메라 컷과 텔레포트 프레임에서 history를 무효화하는가?
- 오류 발생 전후의 프레임을 함께 저장하는가?
자주 묻는 질문 FAQ
TAA 고스팅과 모션 블러는 어떻게 구분하나요?
모션 블러는 의도된 움직임 방향으로 화면이 흐려진다. TAA 고스팅은 오브젝트 뒤에 이전 위치의 색상이 남거나 정지한 오브젝트가 카메라 이동과 반대 방향으로 끌려간다. Actual Velocity와 Expected Velocity를 비교하면 두 현상을 빠르게 분리할 수 있다.
카메라가 움직이지 않는데도 velocity 오류가 커지는 이유는 무엇인가요?
Temporal Jitter, 애니메이션, WPO, GPU 파티클 또는 이전 프레임 버퍼 초기화 문제가 원인일 수 있다. 카메라 이동량이 작은 프레임은 별도 기준으로 검사하고 오브젝트별 velocity를 함께 확인해야 한다.
Object ID를 Custom Stencil만으로 충분히 만들 수 있나요?
간단한 오브젝트 선택에는 충분하지만 8비트 값만으로는 대규모 장면이나 인스턴스별 분석에 한계가 있다. 자동 원인 추적에는 R32_UINT 디버그 버퍼와 Asset Path 매핑을 사용하는 편이 안전하다.
정리
TAA 고스팅은 화면 품질 문제처럼 보이지만 실제 원인은 현재·이전 프레임의 위치를 연결하는 velocity 데이터인 경우가 많다. 카메라 행렬로 계산한 Expected Velocity를 기준으로 Actual Velocity를 비교하고 Object ID와 에셋 메타데이터를 결합하면 문제를 장면 단위가 아니라 메시, 머티리얼, 애니메이션, LOD 단위로 좁힐 수 있다.
가장 실용적인 구성은 8~16프레임 링 버퍼, velocity error 마스크, depth, R32_UINT Object ID 그리고 에셋별 오류 점수를 하나의 자동 캡처 이벤트로 묶는 것이다. 이렇게 만들면 TAA 디버깅이 감각적인 화면 확인에서 재현 가능한 렌더링 검증 프로세스로 바뀐다.



