![]() |
Главная
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
При построении изображений и анимаций естественным образом постоянно возникает задача устранения алиасинга. С тем, что такое алиасинг с точки зрения математики, можно ознакомиться здесь. В компьютерной графике реального времени эта задача особенно актуальна, поскольку мы сильно ограничены по ресурсам, которые мы можем потратить на борьбу с ним.
Вообще aliasing это явление, возникающее когда мы строим выборку значений быстроменяющиейся функции со слишком большим шагом - мы просто не успеваем отследить изменения функции, что приводит к возникновению различные хорошо заметных артефактов.
Рис 1. Алиасинг и способ уменьшить его
Классическим вариантом антиалиасинга для GPU является мультисэмплинг или суперсэмплинг. К сожалению оказалось, что для случая отложенного освещения (deferred shading), очень часто применяемого сейчас, он оказывается крайне дорогостоящим. Одна из первых игр, успешно применявших отложенное освещение - STALKER: Shadow Of Chernobyl - для реализации антиалиасинга использовала специальную screen-space технику.
Основная ее идея заключалась в том, что обычно алиасинг возникает там, где имеют место границы граней или объектов. Поэтому сперва по буферу глубины и буферу нормалей из G-буфера запускался фильтр для обнаружения разрывов (edge detect). Только там где были обнаружены границы выполнялось сглаживание (т.е. размытие изображения).
Для своего времени (а игра вышла в 2007 году) этот прием был довольно хорош но сейчас этого уже не достаточно. И одной из часто применяемых техник является темпоральный антиалиасинг (Temporal Anti-Aliasing, TAA). Его основная идея заключается в том, что мы используемый полученные ранее кадры для сглаживания текущего кадра.
Основная идея суперсэмплинга состоит в том, что мы для каждого пиксела строим несколько сэмплов, распределенных внутри пиксела, и для них отдельно проводим расчет.
Рис 2.Суперсэмплинг - несколько сэмплов на пиксел
Классический суперсэмплинг заключается в том, что мы для каждого пиксела строим распределение сэмплов в пространстве (внутри пиксела). Основная идея ТАА заключается в том, что мы распределяем эти сэмплы не только в пространстве, но и во времени - для каждого кадра для пиксела выбирается свой единственный сэмпл, но при этом его положение внутри пиксела меняется от кадра к кадру.
Хотя сейчас все чаще применяются приемы антиалиасинга, основанные при применении нейросетей (DLSS), но во первых они также используют предыдущие кадры и, во-вторых, сама техника TAA содержит много полезных элементов и приемов. Кроме того, используемые сейчас нейросетевые алгоритмы антиалиасинга по сути привязаны к конкретной платформе - NVidia или AMD.
Первое, с чего начинается ТАА - это добавление к координатам субпикселольного случайного шума (jitter). Обычно заранее считается небольшое число смещений и дальше просто по номеру кадра выбирается одно из них. Таким образом у нас на каждый кадр камера будет слегка смещена на случайную величину \( (j_x, j_y ) \), определяемую номером кадра.
Есть несколько способов получения этого смещения.
Самый прост - мы просто используем генератор псевдослучайных чисел, создавая новое смещение для каждого кадра.
Но обычно идут несколько другим путем - через последовательность Холтона>
строится массив квазислучайных пар чисел - точек в \( [-1,1]^2 \).
Далее по номеру кадра (по модулю размера массива) мы просто выбираем очередную точку и покомпонентно умножаем на \( (\frac{1}{width}, \frac{1}{height}) \).
Здесь через \(width\) и \(height\) обозначены размеры фреймбуфера, куда мы производим рендеринг.
Ниже приводится пример фрагментного шейдера, использующего переданное через uniform-переменную значение смещения.
Для работы алгоритма нам нужно знать историю, т.е. предыдущие кадры.
Обычно вместо набора предыдущих кадров мы храним так называемый history buffer,
в котором содержится результирующее изображение для предыдущего кадра.
При этом после рендеринга текущего кадра мы накладываем на него history buffer, строя тем
самым history buffer для следующего кадра.
На практике нам нужны два таких буфера - один будет содержать буфер для предыдущего кадра и
в другой мы будем строить сглаженное изображение.
И на каждый кадр мы меняем их местами.
После этого мы комбинируем получившийся буфер цвета с буфером истории (history buffer).
В самом начале буфер истории это будет просто первый кадр, потом в нем будут накапливаться последующие кадры.
Сама схема комбинации (и получения нового значения для буфера истории) задается следующей формулой:
\[
c_{out} = c_{hist} \cdot (1 - \alpha) + c_{curr} \cdot \alpha
\]
Ниже приводится фрагментный шейдер, реализующий данное поведение.
Если у нас все неподвижно (и камера и объекты сцены), то на этом можно остановиться - мы получим хорошую картинку
с подавленным алиасингом.
Но обычно у нас присутствует движение - как самой камеры, так и различных объектов, составляющих сцену.
Поэтому если мы просто будем накладывать текущий буфер цвета на буфер истории как показано выше, то
мы получим размытые образы объектов, так называемый ghosting.
Рис. 3. Пример ghosting
Самым простым способом борьбы с этим будет следующий.
Мы сохраняем матрицы, которыми мы преобразуем вершины в предыдущем кадре (обычно это произведение MVP).
Тогда мы можем взять буфер глубины от текущего кадра и по значению глубины из него восстановить исходные
трехмерные координаты соответствующей точки (здесь рассказывается как этом можно сделать).
Рис 4. Восстановление трехмерных координат.
Далее мы умножаем получившиеся трехмерные координаты на матрицы с предыдущего кадра, выполняем перспективное деление
и получаем текстурные координаты, соответствующие данной точке на предыдущем кадре.
И для смешивания мы возьмем цвет из буфера истории именно по этим текстурным координатам.
Подобный прием называется reprojecting.
Ниже приводится соответствующих шейдер на GLSL.
Этот подход очень прост, но его применимость сильно ограничена.
Если у нас в сцене есть объекты с разными модельными матрицами, то этот подход уже не сработает, так как
нам нужно знать модельную матрицу для текущего и предыдущего кадра для объекта, которому соответствует данный фрагмент.
Но есть альтернативный вариант, хорошо работающий и в данном случае.
Мы в вершинном шейдере берем текущие матрицы преобразования и матрицы с предыдущего кадра,
находим xy-смещение вершины за кадр (но без учета jitter) и выдаем его как атрибут вершины для последующей интерполяции.
В фрагментном шейдере мы записываем проинтерполированное значение в специальную текстуру скорости (velocity texture).
Для нее хорошо подходит 16-битовый формат
Обратите внимание, что перспективное деление и сам расчет скорости нужно выполнять именно в фрагментном шейдере.
В противном случае мы получим артефакты для граней, обрезаемых по плоскостям отсечения.
В этом случае для получения текстурных координат для обращения в буфер истории мы просто смещаем текущие
текстурные координаты на значение из текстуры скорости.
Если мы просто слегка поворачиваем камеру (без ее переноса, т.е. относительные видимости объектов при этом не меняются),
то этот прием отлично работает.
Но в общем случае у нас происходят (пусть и небольшие) изменения видимости за кадр - ранее невидимые объекты
становятся видимыми, а часть ранее видимых становятся невидимыми.
И в этом случае описанное выше решение приводит к ряду артефактов, это называется disocclusion.
Для борьбы с этим эффектов есть несколько методов и мы их сейчас рассмотрим.
Фактически нам нужно проверить, не произошло ли что-то в данном пикселе, сильно меняющее ситуацию.
Для подобных проверок есть несколько подходов и мы их сейчас рассмотрим.
Рис.5. Точка, видимая в кадре N-1, становится невидимой в кадре N.
Основная идея здесь заключается в том, что если взять небольшую окрестность текущего фрагмента (с текущего кадра) - обычно достаточно 3х3,
то правильный цвет из буфера истории не должен сильно отличаться от них.
Т.е. в самом простом случае мы проверяем цвет из буфера истории относительно этой окрестности и отбрасываем его, если он не подходит.
В идеале мы просто строим по цветам из окрестности их выпуклую оболочку (convex hull) в трехмерном RGB-пространстве и проверяем
цвет из буфера истории на попадание внутрь. Если не попал - просто отбрасываем.
Более удачным оказывается вариант - не отбрасывать, а проектировать на выпуклую оболочку.
К сожалению считать выпуклую оболочку и проектировать цвет на нее для каждого фрагмента оказывается слишком дорогостоящим, поэтому обычно используются
более простые варианты.
Самым простым вариантом является построение по цветам из окрестности 3х3 AABB - т.е. мы находим для нее покомпонентные
минимум и максимум. После чего обрезаем цвет из буфера истории по ним (clamp).
Есть слегка отличающийся вариант, когда мы по окрестности считаем среднее \( \overline{c} \) и среднеквадратичное отклонение \( \sigma \).
После это мы строим границы AABB через них по следующим формулам (здесь \( \gamma \) это задаваемый параметр, обычно лежит от 0.75 до 1.25)
\[
\begin{matrix}
\begin{aligned}
c_{min} = \overline{c} - \gamma \cdot \sigma \\
c_{max} = \overline{c} + \gamma \cdot \sigma \\
\end{aligned}
\end{matrix}
\]
Можно вместо цвета в пространстве RGB использовать рассмотренный выше подход используя цветовое пространство YCoCg.
Преимуществом этого пространства является то, что в нем разделяется яркость Y и хроматические координаты CoCg, что дает
более компактную оценку.
Для перевода между RGB и YCoCg используются следующие формулы
\[
\begin{pmatrix}
Y \\
Co \\
Cg
\end{pmatrix}
=
\begin{pmatrix}
\frac{1}{4} & \frac{1}{2} & \frac{1}{4} \\
\frac{1}{2} & 0 & -\frac{1}{2} \\
-\frac{1}{4} & \frac{1}{2} & -\frac{1}{4} \\
\end{pmatrix}
\begin{pmatrix}
R \\
G \\
B
\end{pmatrix}, Y \in [0,1], Co, Cg \in [-0.5, 0.5]
\]
\[
\begin{pmatrix}
R \\
G \\
B
\end{pmatrix}
=
\begin{pmatrix}
1 & 1 & -1 \\
1 & 0 & 1 \\
1 & -1 & -1 \\
\end{pmatrix}
\begin{pmatrix}
Y \\
Co \\
Cg
\end{pmatrix}
\]
Ниже приводится код на GLSL, осуществляющий перевод цвета между цветовыми пространствами RGB и YCoCg.
В презентации по Unreal Engine предлагается еще один вариант - берутся сразу две окрестности, по ним считается min/max
и усредняются между ними.
В качестве используемых окрестностей предлагается классическая 3х3 окрестность и ее "скругленная" версия.
Рис 6. Используемые окрестности.
Вместо простого отсечения цвета по границам AABB (как в пространстве RGB, так и в пространстве YCoCg) можно пойти слегка другим путем.
Мы строим отрезок от цвета из буфера истории до цвета из текущего кадра и находим его пересечение с границей нашего AABB.
И именно эту точку мы и берем в качестве цвета из истории.
Если предыдущий подход называется clamping, то этот подход называется clipping.
Рис 7. clip vs clamp
Еще одним (правда более сложным) вариантом является использование вместо AABB другого ограничивающего тела - OBB.
Тогда по цветам из окрестности считается OBB и цвет из буфера истории проектируется уже на него.
Но в этом случае мы получаем заметно более сложный код.
Если мы сохраним значение буфера глубины с предыдущего кадра, то его можно использовать для определения того,
имеет ли место disocclusion - если оно имеет место, то скорее всего, значение глубины сильно изменится.
Поэтому мы берем старое и новое значения глубины и сравниваем их между собой, при разнице выше некоторого порога
мы отбрасываем цвет из буфера истории.
Очень эффективным приемом является использование текстуры скорости - за счет нее мы не просто легко получаем текстурные
координаты для обращения к буферу истории, но и можем оценить валидность значения из буфера истории.
Для этого нам нужно сохранить текстуру истории с предыдущего кадра.
Мы просто сравниваем значения скоростей для предыдущего кадра и текущего, большая разница говорит о том, что
имеет место disocclusion и значение цвета из буфера истории лучше отбросить
На самом деле есть еще один довольно важный момент - когда мы применяем ТАА - до tonemapping или после.
У каждого варианта есть свои плюсы и минусы.
В результате пришли к комбинированному варианту - мы сперва применяем простой tonemapping (обычно Reinhard),
после него применяем ТАА и уже после него мы инвертируем использованный tonemapping и передаем получившееся изображение
на настоящий tonemapping и постобработку.
Для Reinhard tonemapping есть очень простая формула
\[
c' = \frac{c}{1 + l}
\]
\[
l = (c, L), L = (0.2126, 0.7152, 0.0722)
\]
Теперь давайте рассмотрим как можно обратить это преобразование.
Давайте посмотрим чему равна яркость \(l'\) для преобразованного цвета \(c'\):
\[
l' = (c', L ) = \frac{(c, L)}{1+(c,L)} = \frac{l}{1 + l} = 1 - \frac{1}{1 + l}
\]
Отсюда
\[
1 + l = \frac{1}{1 - l'}
\]
\[
c'' = c' \cdot ( 1 + l ) = \frac{c'}{1 - (c',L)}
\]
Одним из недостатков TAA является то, что он добавляет некоторую размытость (blurriness) в получившееся изображение.
Это связано с тем, что когда мы обращаемся по рассчитанным текстурным координатам к буферу истории, то мы практически
никогда не попадаем точно в центр тексела.
Т.е. здесь имеет место (обычно линейная) фильтрация, которая и создает небольшое размытие.
Для борьбы с этим является использование более сложным схем восстановления значения (интерполяции) при чтении из текстуры.
Часто для этого применяется фильтр Катмулла-Рома, содержащий отрицательные веса (см рис), что делает получающееся
изображение более четким.
Традиционная схема билинейной интерполяции, использующая положительные веса при смешивании цветов,
не очень хорошо подходит здесь, так как приводит к размытию изображения.
Более удачной схемой является использование сплайнов Катмулла-Рома,
использующего как положительные, так и отрицательные веса.
Рис. 8. Схема Катмулла-Рома
Сам одномерный сплайн Катмулла-Рома задается следующей формулой
\[
q(t) = \frac{1}{2} \left(
2p_1 + (-p_0 + p_2) \cdot t + (2p_0 - 5p_1 + 4p_2 - p_3) \cdot t + (-p_0 + 3p_1 -3p_2 + p_3) \cdot t
\right)
\]
В двухмерном случае нам уже нужно не 4, а 16 точек, т.е. чтений из текстуры.
Для нас 16 чтений из текстуры это слишком много (хотя можно использовать вычислительный шейдер и там использовать shared-память).
Есть оптимизированные варианты, дающие вполне приемлемую точность при заметно меньшем числе точек.
Классическая схема Катмулла-Рома требует 16 чтений из текстуры, что довольно много.
Можно оптимизировать по приводимой ниже схема - чтение по точкам, указанным на рисунке позволяет получить значение при
всего 9 обращениях к текстуре.
Рис. 9. Паттерн с 9 точками
Ниже приводится функция на GLSL, осуществляющая интерполяцию Катмулла-Рома по 9 точкам.
Можно отбросить значения текстуры в углах, сведя схему к 5 обращениям к текстуры, на следующем рисунке приводится соответствующий шаблон.
Рис. 10. Паттерн с 5 точками
Еще одним местом, где может иметь место размытие, является само текстурирование.
При наложении текстуры на геометрии обычно уже применяется пирамидальное фильтрование дающее
требуемое размытие.
Но за счет использование jitter'а мы размываем текстуру еще больше.
Самым простым способом борьбы с этим является добавление отрицательного смещения (bias) к значению LOD
используемому при текстурировании.
Если мы рассмотрим производные текстурных координат (по которым собственно и считается LOD) как радиус размытия,
то в самом простом случае (всего два различных значения для jitter) нам достаточно задать смещение LOD на \( -\frac{\sqrt{2}}{2} \).
Для задания смещение LOD, которое будет добавляться при любом обращении к текстуре, можно использовать следующий фрагмент кода.
Другим важным моментом является то, что в отличии от буфера истории текстура скоростей (как и буфер глубины) уже
содержат в себе алиасинг.
И с ним нужно как-то бороться.
Самым простым способом борьбы будет вместо чтения одного значения по заданным координатам читать сразу
некоторую окрестность (3х3 или 5-точечную окрестность).
И в этой окрестности мы выбираем значение скорости с наибольшей длиной (для скорости) или ближайшее
значения (для буфера глубины).
Очень интересная модификация TAA была использована в игре Horizon: Zero Dawn.
Там были использованы сразу два алгоритма антиалиасинга - FXAA и TAA.
Вместо буфера истории мы просто используем предыдущий кадр в том виде, в котором он подан на вход TAA (после применения FXAA).
Смешение текущего кадра с предыдущим происходит с весом 0.5 (если фрагмент с предыдущего кадра не был отброшен).
При этом используется всего два значения для субпиксельного смещения (jitter) - одно для четных кадров и одно для нечетных.
В одном случае мы сдвигаемся на полпиксела по горизонтали, в другом - на полпиксела по вертикали.
За счет этого вместо сложного фильтра Катмулла-Рома достаточно использовать простую 4-точечную схему для фильтрации.
Рис 11. Фильтрация для 4-х точечной схемы по горизонтали
Исходный код к этой статье можно скачать по следующей ссылке.
Temporal AA and the quest for the Holy Trail
DECIMA: Advances in Lighting and AA
A Survey of Temporal Antialiasing Techniques
Temporal Anti-Aliasing(TAA) Tutorial
Temporal Anti Aliasing – Step by Step
High Quality Temporal Supersampling
Temporal Reprojection Anti-Aliasing in INSIDE
Temporal Reprojection Anti-Aliasing
Filmic SMAA: Sharp Morphological and Temporal Antialiasing
Temporal Reprojection Anti-Aliasing in INSIDE
Filmic SMAA. Sharp Morphological and Temporal Antialiasing
Temporal Antialiasing Starter Pack
HIGH-QUALITY TEMPORAL SUPERSAMPLING
Temporal supersampling and antialiasing
Temporal Antialiasing in Uncharted 4
#version 330 core
uniform mat4 mv;
uniform mat4 proj;
uniform vec2 jitter;
layout ( location = 0 ) in vec3 pos;
layout ( location = 1 ) in vec2 texCoord;
out vec2 tex;
void main(void)
{
vec4 p = proj * mv * vec4 ( pos, 1.0 );
tex = texCoord;
p.xy += jitter * p.w;
gl_Position = p;
}
#version 330 core
uniform sampler2D curMap;
uniform sampler2D historyMap;
uniform vec2 jitter;
in vec2 tex;
in vec4 viewRay;
out vec4 color;
const float alpha = 1.0 - 0.2;
void main(void)
{
color = mix ( texture ( curMap, tex - jitter ), texture ( historyMap, tex ), alpha );
}
#version 330 core
layout(location = 0) in vec4 pos;
out vec2 tex;
void main(void)
{
tex = 0.5 * ( pos.xy + vec2 ( 1.0 ) );
gl_Position = vec4 ( pos.xy, 0.0, 1.0 );
}
#version 330 core
uniform mat4 proj; // we will need it in pos reconstruction
uniform mat4 viewInv; // inverted view matrix from current frame
uniform mat4 projOld; // projection matrix from previous frame
uniform mat4 viewOld; // view matrix from previous frame
uniform sampler2D historyMap;
in vec2 tex;
out vec4 color;
// Note: it assumes proj is classical projection matrix
// (not multiplied by smth else)
// uv - current texture coordinates (glFragCoord.xy / screenSize)
vec3 getViewPos ( in vec2 uv, in float zEye )
{
vec2 a = vec2 ( -2.0 / proj [0][0], -2.0 / proj [1][1] );
vec2 b = vec2 ( 1.0 / proj [0][0], 1.0 / proj [1][1] );
return zEye * vec3 ( a*uv + b, 1.0 );
}
// takes depth from depth buffer
// return z in [zNear, zFar]
float linearDepth ( float d )
{
return -zFar * zNear / (d * (zFar - zNear) - zFar );
}
void main ()
{
// get depth value from depth buffer
float depth = texture ( depthMap, tex ).r;
// get camera-space coordinates of current proj
vec4 pos = vec4 ( getViewPos ( tex ), 1.0 );
// now transform into world space by inverse
// current view matrix
vec4 posWorld = viewInv * pos;
// now use view and projection from
// previous frame to get old projection
vec4 posOld = projOld * viewOld * posWorld;
// now convert to NDC and texture coordinates
vec2 uv = 0.5 * ( posOld.xy / posOld.w ) + vec2 ( 0.5 );
color = texture ( historyMap, uv );
}
GL_RG16F
.
#version 330 core
uniform mat4 mv;
uniform mat4 proj;
uniform mat4 mvPrev;
uniform mat4 projPrev;
uniform vec2 jitter;
layout ( location = 0 ) in vec3 pos;
layout ( location = 1 ) in vec2 texCoord;
out vec2 tex;
out vec4 pCurr; // vertex position for current frame
out vec4 pPrev; // vertex position for previous frame
void main(void)
{
vec4 p = proj * mv * vec4 ( pos, 1.0 );
pPrev = projPrev * mvPrev * vec4 ( pos, 1.0 );
pCurr = p;
tex = texCoord;
p.xy += jitter * p.w;
gl_Position = p;
}
#version 330 core
uniform sampler2D image;
in vec2 tex;
in vec4 pCurr;
in vec4 pPrev;
out vec4 color [2];
void main(void)
{
// we have to do this per-fragment due to
// parts of triangles being cut
// doing this in vertex shader leads to artifacts
vec2 uvCurr = (pCurr.xy / pCurr.w + vec2 ( 1.0 ) ) * 0.5;
vec2 uvPrev = (pPrev.xy / pPrev.w + vec2 ( 1.0 ) ) * 0.5;
vec2 velocity = uvCurr - uvPrev;
color [0] = texture ( image, tex );
color [1] = vec4 ( velocity, 0, 1 );
}
#version 330 core
uniform sampler2D curMap;
uniform sampler2D velMap;
uniform sampler2D historyMap;
uniform vec2 jitter;
in vec2 tex;
in vec4 viewRay;
out vec4 color;
const float alpha = 1.0 - 0.2;
void main(void)
{
vec2 vel = texture ( velMap, tex ).xy;
color = mix ( texture ( curMap, tex - jitter ), texture ( historyMap, tex - vel ), alpha );
}
Color clipping
uniform sampler2D curMap;
uniform sampler2D velMap;
uniform sampler2D historyMap;
uniform vec2 jitter;
in vec2 tex;
in vec4 viewRay;
out vec4 color;
const float alpha = 1.0 - 0.2;
const float EPS = 0.001;
const mat3 RGBToYCoCgMatrix = mat3 ( 0.25, 0.5, -0.25, 0.5, 0.0, 0.5, 0.25, -0.5, -0.25 );
const mat3 YCoCgToRGBMatrix = mat3 ( 1.0, 1.0, 1.0, 1.0, 0.0, -1.0, -1.0, 1.0, -1.0 );
// note: clips towards aabb center + p.w
// p = colorCurr, q = colorHist
vec4 clipAABB ( vec3 pMin, vec3 pMax, vec4 p, vec4 q )
{
vec3 p_clip = 0.5 * (pMax + pMin);
vec3 e_clip = 0.5 * (pMax - pMin) + EPS;
vec4 v_clip = q - vec4 ( p_clip, p.w );
vec3 v_unit = v_clip.xyz / e_clip;
vec3 a_unit = abs ( v_unit );
float ma_unit = max ( a_unit.x, max ( a_unit.y, a_unit.z ) );
if ( ma_unit > 1.0 )
return vec4 ( p_clip, p.w ) + v_clip / ma_unit;
else
return q; // point inside aabb
}
void computeStats ( out vec3 mean, out vec3 sigma )
{
vec3 m2 = vec3 ( 0.0 );
mean = vec3 ( 0.0 );
for ( int i = -1; i <= 1; i++ )
for ( int j = -1; j <= 1; j++ )
{
vec3 c = textureOffset ( curMap, tex, ivec2 ( i, j ) ).rgb;
mean += c;
m2 += c*c;
}
mean /= 9;
sigma = sqrt ( m2 / 9 - mean * mean );
}
void computeMinMax ( out vec3 minColor, out vec3 maxColor )
{
minColor = vec3 ( 100.0 ); // too big for a minColor
maxColor = vec3 ( -100.0 ); // too small for a maxColor
for ( int i = -1; i <= 1; i++ )
for ( int j = -1; j <= 1; j++ )
{
vec3 c = textureOffset ( curMap, tex, ivec2 ( i, j ) ).rgb;
minColor = min ( minColor, c );
maxColor = max ( maxColor, c );
}
}
void main(void)
{
vec2 vel = texture ( velMap, tex ).xy;
vec3 minColor, maxColor;
computeMinMax ( minColor, maxColor );
vec3 historyColor = texture ( historyMap, tex - vel ).rgb;
historyColor = clamp ( historyColor, minColor, maxColor );
color = vec4 ( mix ( texture ( curMap, tex - jitter ).rgb, historyColor, alpha ), 1.0 );
}
vec3 mean, sigma;
computeStarts ( mean, sigma );
vec3 cMin = mean - sigma * gamma;
vec3 cMin = mean + sigma * gamma;
vec3 rgbToYCbCg ( float r, float g, float b )
{
float Co = r - b;
float tmp = b + Co * 0.5;
float Cg = g - tmp;
float Y = tmp + Cg * 0.5;
return vec3 ( Y, Co, Cg );
}
vec3 yCoCgToRgb ( float y, float co, float cg )
{
float tmp = y - cg * 0.5;
float g = cg + tmp;
float b = tmp - co * 0.5;
float r = B + co;
return vec3 ( r, g, b );
}
Depth rejection
Color Velocity rejection
TAA и tonemapping
float luminance ( in vec3 color )
{
return dot ( color, vec3 ( 0.2126, 0.7152, 0.0722 ) );
}
vec3 reinhard ( in vec3 color )
{
return color / ( 1.0 + luminance ( color ) );
}
vec3 reinhardInv ( in vec3 color )
{
return color / ( 1.0 - luminance ( color ) );
}
Размытие
// 9-tap bicubic filtering
vec3 bicubicFilter9 ( vec2 texcoord )
{
vec2 size = vec2 ( textureSize ( historyMap, 0 ) );
vec2 invSize = 1.0 / size;
float REPROJECTION_SHARPNESS = 50;
// from [0,1] to texels
vec2 position = size * texcoord;
vec2 centerPosition = floor ( position - 0.5 ) + 0.5;
vec2 f = position - centerPosition;
vec2 f2 = f * f;
vec2 f3 = f * f2;
float c = REPROJECTION_SHARPNESS / 100.0;
vec2 w0 = -c * f3 + 2.0 * c * f2 - c * f;
vec2 w1 = (2.0 - c) * f3 - (3.0 - c) * f2 + 1.0;
vec2 w2 = -(2.0 - c) * f3 + (3.0 - 2.0 * c) * f2 + c * f;
vec2 w3 = c * f3 - c * f2;
vec2 w12 = w1 + w2;
vec2 tc12 = invSize * ( centerPosition + w2 / w12 );
vec3 centerColor = texture ( historyMap, vec2 ( tc12.x, tc12.y ) ).rgb;
// from texels to [0,1]
vec2 tc0 = invSize * (centerPosition - 1.0);
vec2 tc3 = invSize * (centerPosition + 2.0);
vec4 color = vec4 ( texture ( historyMap, vec2 ( tc12.x, tc0.y ) ).rgb, 1.0) * (w12.x * w0.y ) +
vec4 ( texture ( historyMap, vec2 ( tc0.x, tc12.y ) ).rgb, 1.0) * (w0.x * w12.y) +
vec4 ( centerColor, 1.0) * (w12.x * w12.y) +
vec4 ( texture ( historyMap, vec2 ( tc3.x, tc12.y ) ).rgb, 1.0) * (w3.x * w12.y) +
vec4 ( texture ( historyMap, vec2 ( tc12.x, tc3.y ) ).rgb, 1.0) * (w12.x * w3.y );
return color.rgb / color.a; // assume A=1, then color.a is normalization
}
glTextureParameteri ( texture, GL_TEXTURE_LOD_BIAS, bias );
vec2 dilateVelocity ( in vec2 uv )
{
float vLenMax = 0.0;
uv uvMax = uv;
vec2 scale = 1.0 / vec2 ( textureSize ( velocityMap, 0 ) );
for ( int i = -1; i <= 1; i++ )
for ( int j = -1; j <= 1; j++ )
{
vec4 v = textureOffset ( velocityMap, uv, ivec2 ( i, j ) );
float vl = dot ( v, v );
if ( vl > vLenMax )
{
vLenMax = vl;
uvMax = uv + vec2 ( i, j ) * scale;
}
}
return uvMax;
}
TAA в игре Horizon: Zero Dawn
Полезные ссылки