Стаття Алгоритми рендерингу · ≈ 11 хв читання

Temporal Anti-Aliasing (TAA) пояснено

TAA — причина, чому сучасні ігри виглядають чітко за долю вартості 8x MSAA (Multisample Anti-Aliasing — старіша техніка, яка бере кілька семплів покриття на піксель у межах одного кадру) — розподіляючи роботу зі згладжування на багато кадрів замість того, щоб робити все за один. Це також причина, чому ті ж самі ігри іноді показують розмиті «привиди» позаду швидких об'єктів. Ось як саме працює цей компроміс.

Коротко: TAA щокадру трохи зміщує камеру (jitter) і змішує поточне зображення з попереднім кадром, репроєктованим за допомогою векторів руху, — так вартість згладжування розподіляється на багато кадрів замість одного. Обмеження сусідства (neighbourhood clamping) не дає цій змішаній історії розмазуватися, хоча на тонких швидкорухомих об'єктах усе одно можуть з'являтися артефакти-«привиди».

1. Проблема aliasing

Край трикутника — ідеально чітка математична лінія, але піксель — квадратна область семплювання. Коли край перетинає піксель, рендерер має вирішити: цей піксель «всередині» трикутника чи «зовні»? Один точковий семпл у центрі пікселя дає бінарну відповідь, що створює зубчастий сходовий візерунок, відомий як aliasing.

Класичне рішення, MSAA (Multisample Anti-Aliasing), бере кілька семплів покриття на піксель у межах одного кадру — 4x чи 8x — і усереднює їх. Це точно, але дорого: 8x MSAA означає приблизно 8-кратну кількість тестів покриття геометрії та значно більший G-buffer, і це не допомагає з aliasing всередині шейдера (спекулярне мерехтіння, alpha-tested листя).

Темпоральна ідея

Що якщо замість 8 семплів за один кадр брати 1 трохи інший семпл кожен кадр і змішувати його з історією останніх 8 кадрів? При 60 fps ця історія охоплює лише ~130 мс — невидиме як мерехтіння, але дає ефективну частоту суперсемплювання безкоштовно, бо навантаження на GPU за кадр залишається таким самим, як без AA. Це основна ідея TAA, впроваджена у виробництво Карісом (Epic, 2014) і тепер стандартне рішення AA в Unreal Engine, Unity URP/HDRP та більшості AAA-ігор.

2. Субпіксельне тремтіння (jitter)

Кожен кадр матриця проекції камери зміщується на крихітну субпіксельну величину — jitter — за низькодисперсійною послідовністю (стандартна — Halton 2,3), яка рівномірно покриває площу пікселя за N кадрів, не повторюючи візерунки, які могло б помітити око.

Зміщення jitter Halton(2,3), кадр i jitterX(i) = Halton(i, base=2) − 0.5
jitterY(i) = Halton(i, base=3) − 0.5

зміщення у просторі NDC: (jitterX / renderWidth · 2, jitterY / renderHeight · 2)

Це зміщення додається безпосередньо в матрицю проекції перед рендерингом, тому кожен край трикутника, семпл тіні та спекулярний відблиск обчислюються у трохи іншій субпіксельній позиції кожен кадр — те саме, що MSAA робить просторово в межах одного кадру, TAA робить темпорально через багато кадрів.

3. Репроекція історії через вектори руху

Змішування цього кадру з буфером кольору попереднього кадру працює тільки якщо ви змішуєте ту саму точку поверхні. Якщо камера чи об'єкт рухалися, точка поверхні, що була на пікселі (400, 300) минулого кадру, може бути на (410, 295) цього кадру. TAA виправляє це через буфер векторів руху — для кожного пікселя, зміщення в екранному просторі між позицією цього кадру та позицією минулого кадру тієї самої точки світу:

Вектор руху (на піксель, на об'єкт) clipPosCurrent = ViewProjпоточна · worldPos
clipPosPrevious = ViewProjпопередня · (worldPos − objectVelocity · dt)
motionVector = (clipPosCurrent.xy / clipPosCurrent.w) − (clipPosPrevious.xy / clipPosPrevious.w)

Resolve-прохід TAA потім семплює буфер історії за координатами currentUV − motionVector замість currentUV — це і є репроекція. Для статичної геометрії достатньо власного руху камери; для скінованих чи анімованих мешів рушій повинен явно записувати швидкість на вершину в буфер векторів руху.

4. Обмеження сусідства (clamping)

Навіть з коректними векторами руху, disocclusion (нова геометрія, яка не була видима минулого кадру — уявіть край стіни, що розкриває те, що було позаду об'єкта) не має валідного семпла історії. Змішування там сміттєвої історії дало б розмазування. Стандартне рішення — обмеження сусідства: побудувати вирівняний по осях обмежувальний бокс (AABB) — або точніший еліпсоїд на основі дисперсії — з сусідства 3×3 пікселя поточного кадру у просторі кольору, і обмежити репроекований семпл історії цим боксом перед змішуванням.

Variance clipping (Salvi 2016) μ = mean(сусідство 3×3), σ = stddev(сусідство 3×3)
colorMin = μ − γ·σ, colorMax = μ + γ·σ   (γ ≈ 1)
historyClamped = clamp(history, colorMin, colorMax)

Це змушує колір історії залишатися правдоподібним відносно того, що поточний кадр насправді бачить, ціною невеликого зменшення ефективної кількості темпоральних семплів біля різких країв та disocclusion — необхідний компроміс, бо необмежена історія помітно «привиділа» б.

5. Привиди (ghosting) та disocclusion

Ghosting — це видимий шлейф-артефакт позаду швидкорухомих тонких об'єктів (гвинт пропелера, ланцюгова огорожа, ефекти часток) — класична скарга на TAA. Це трапляється, коли обмеження сусідства не може повністю відкинути невалідну історію, бо бокс, побудований з тонкого, високочастотного сусідства, сам по собі ненадійний.

  • Коефіцієнт змішування, зважений на швидкість: зменшуйте вагу історії (зазвичай ~0.9), коли величина вектора руху велика, надаючи перевагу поточному кадру для швидкорухомих пікселів.
  • Відхилення на основі глибини: відхиляйте історію, якщо репроекована глибина не збігається з поточною глибиною в межах порогу — сильний сигнал disocclusion.
  • Гостріші патерни jitter + змішування з урахуванням тонмапінгу: змішуйте у перцептивному (тонмапованому) просторі кольору, щоб яскраві, контрастні відблиски не домінували в боксі variance clip.

6. Мінімальний resolve-прохід TAA

Спрощений фрагментний шейдер для кроку resolve:

uniform sampler2D uCurrentColor;
uniform sampler2D uHistoryColor;
uniform sampler2D uMotionVectors;
uniform float uBlendFactor; // ~0.9 статично, менше при швидкому русі

vec3 clipAABB(vec3 aabbMin, vec3 aabbMax, vec3 historyColor) {
  vec3 center  = 0.5 * (aabbMax + aabbMin);
  vec3 extents = 0.5 * (aabbMax - aabbMin) + 0.0001;
  vec3 offset  = historyColor - center;
  vec3 unitOffset = abs(offset / extents);
  float maxUnit = max(unitOffset.x, max(unitOffset.y, unitOffset.z));
  return maxUnit > 1.0 ? center + offset / maxUnit : historyColor;
}

void main() {
  vec2 motion  = texture(uMotionVectors, vUv).xy;
  vec2 prevUv  = vUv - motion;

  vec3 current = texture(uCurrentColor, vUv).rgb;
  vec3 history = texture(uHistoryColor, prevUv).rgb;

  // Побудувати AABB сусідства 3x3 з поточного кадру
  vec3 aabbMin = current, aabbMax = current;
  for (int x = -1; x <= 1; x++)
  for (int y = -1; y <= 1; y++) {
    vec3 s = textureOffset(uCurrentColor, vUv, ivec2(x, y)).rgb;
    aabbMin = min(aabbMin, s);
    aabbMax = max(aabbMax, s);
  }

  history = clipAABB(aabbMin, aabbMax, history);

  bool validHistory = all(lessThan(abs(prevUv - 0.5), vec2(0.5)));
  float blend = validHistory ? uBlendFactor : 0.0;
  fragColor = vec4(mix(current, history, blend), 1.0);
}
Прохід шарпінгу

Оскільки темпоральне змішування — це фільтр низьких частот, вихід TAA виглядає трохи м'якшим за сирий кадр з jitter. Більшість рушіїв виконують після resolve легкий прохід unsharp-mask (наприклад, Contrast Adaptive Sharpening, AMD CAS), щоб відновити сприйняту деталізацію без повторного внесення aliasing.

7. TAA проти MSAA, FXAA та DLSS

  • MSAA: найвища якість AA країв, без ghosting, але дороге і сліпе до aliasing шейдера/спекулярності; несумісне з deferred-рендерингом без додаткової роботи над G-buffer.
  • FXAA: однопрохідне пост-процесне розмиття країв — дешеве, не потребує jitter чи історії, але може розмити тонкі деталі та текст, бо не має субпіксельної інформації для роботи.
  • TAA: дешеве за кадр, обробляє aliasing шейдера, але вносить ризик ghosting і легку м'якість, яку треба компенсувати шарпінгом.
  • DLSS / FSR / TAAU (темпоральний апскейл): розширюють механізм репроекції TAA також для апскейлу — рендерять при нижчій внутрішній роздільній здатності і дозволяють темпоральному накопиченню реконструювати деталі при вихідній роздільній здатності, використовуючи навчену нейромережу (DLSS) чи вручну налаштовану евристику (FSR2) замість простого AABB clamp.

🔺 Дослідити якість країв у Ray Marching

SDF-поверхні, рендерені через ray marching, природно схильні до aliasing на силуетах — гарне місце побачити, чому антіаліасинг має значення.

Читати статтю про Ray Marching →

🔗 Схожі статті

🔺BRDF Кука-Торренса 🌈Path Tracing 🖥️WebGL2 Advanced 🔺Ray Marching