Псевдо Lens Flare

Привет, Хабр! Представляю вашему вниманию перевод статьи «Pseudo Lens Flare» автора John Chapman.

image

Lens flare (блики на линзах) ― это фотографический артефакт, возникающий при рассеивании и преломлении света в системе линз. Хотя он является артефактом, существует множество поводов использовать lens flare в компьютерной графике:

  • он увеличивает воспринимаемую яркость и видимый динамический диапазон изображения.
  • lens flare часто встречается на фотографиях, поэтому его отсутствие может бросаться в глаза
  • он может играть важную роль в стилистике или драматизме, или может быть частью геймплея в играх (представьте блики, ослепляющие игрока)


Традиционно lens flare в realtime реализовывался с помощью технологий, основанных на спрайтах. Хотя спрайты дают легко контролируемые и очень реалистичные результаты, они должны быть размещены явно и требуют данные окклюзии для корректного отображения. Здесь я опишу простой и относительно дешевый screen space эффект, который создает псевдо lens flare из входного color buffer. Он не основан на физике, поэтому результат немного отличается от фотореалистичного, но его можно использовать в комбинации с (или как замену) традиционными sprite-based эффектами.

Алгоритм


Состоит из 4 этапов:

  1. Downsample / threshold.
  2. Генерация элементов lens flare.
  3. Размытие.
  4. Upscale / смешивание с оригинальным изображением.


1. Downsample / Threshold


Downsampling (уменьшение разрешения) ― оптимизация, чтобы уменьшить стоимость последующих этапов. Кроме того, мы хотим выбрать подмножество самых ярких пикселей исходного изображения. Использование scale/bias (масштаб/смещение) обеспечивает гибкий способ достичь этого:

uniform sampler2D uInputTex;

uniform vec4 uScale;
uniform vec4 uBias;

noperspective in vec2 vTexcoord;

out vec4 fResult;

void main() 
{
      fResult = max(vec4(0.0), texture(uInputTex, vTexcoord) + uBias) * uScale;
}


image

Регулировка scale/bias является основным способом настройки эффекта; самые лучшие настройки будут зависеть от динамического диапазона color buffer, а также от того, насколько тонким вы хотите видеть результат. Из-за того, что техника является аппроксимацией, тонкость вероятней будет выглядеть лучше.

2. Генерация элементов lens flare


Элементы lens flare имеют тенденцию вращаться относительно центра изображения. Имитируя этот эффект, мы можем развернуть результат предыдущего этапа горизонтально/вертикально. Это легко сделать на этапе генерации элементов, разворачивая текстурные координаты:

vec2 texcoord = -vTexcoords + vec2(1.0);


Делать это не обязательно; генерация элементов работает отлично как с этим, так и без этого. Однако результат изменения текстурных координат помогает визуально отделить эффект lens flare от исходного изображения.

GHOSTS


»Ghosts» (призраки) ― это повторяющиеся блики, которые отражают яркие области в color buffer, разворачиваясь относительно центра изображения. Подход, который я выбрал для генерации, заключается в получении вектора от текущего пикселя к центру экрана, а затем совершении нескольких выборок вдоль этого вектора.

image

uniform sampler2D uInputTex;

uniform int uGhosts; // number of ghost samples
uniform float uGhostDispersal; // dispersion factor

noperspective in vec2 vTexcoord;

out vec4 fResult;

void main() 
{
    vec2 texcoord = -vTexcoord + vec2(1.0);
    vec2 texelSize = 1.0 / vec2(textureSize(uInputTex, 0));
    
// ghost vector to image centre:
    vec2 ghostVec = (vec2(0.5) - texcoord) * uGhostDispersal;
    
// sample ghosts:  
    vec4 result = vec4(0.0);
    for (int i = 0; i < uGhosts; ++i) 
    { 
        vec2 offset = fract(texcoord + ghostVec * float(i));
        result += texture(uInputTex, offset);
    }
    
    fResult = result;
}


Обратите внимание, что я использую fract (), чтобы гарантировать, что текстурные координаты обернуться вокруг; эквивалентно вы можете использовать для текстуры wrap mode GL_REPEAT.

Вот результат:

image

Вы можете улучшить результат, позволяя только ярким областям ближе к центру изображения генерировать призраков. Мы можем этого добиться, добавляя веса, которые будут уменьшаться от центра, для сэмплов:

vec4 result = vec4(0.0);
for (int i = 0; i < uGhosts; ++i) 
{ 
    vec2 offset = fract(texcoord + ghostVec * float(i));
    
    float weight = length(vec2(0.5) - offset) / length(vec2(0.5));
    weight = pow(1.0 - weight, 10.0);
    
    result += texture(uInputTex, offset) * weight;
}


Weight функция проста настолько, насколько это возможно ― линейная. Причина, по которой мы вычисляем weight внутри цикла, заключается в том, что яркие области в центре входного изображения могут «отбрасывать» призраков на границы, но яркие области на границах не могут отбрасывать призраков в центр.

image

Окончательное улучшение ― радиальное изменение цвета призрака, в соответствии с 1D текстурой:

image

Оно применяется после цикла, чтобы повлиять на финальный цвет призрака:

result *= texture(uLensColor, length(vec2(0.5) - texcoord) / length(vec2(0.5)));


HALOS (ореолы)


Если мы возьмем вектор к центру изображения, как в расчете ghost, но зафиксируем длину вектора, мы получим другой эффект: исходное изображение деформируется радиально:

image
Мы можем это использовать для того, чтобы создать «ореол», умножая вес на сэмпл, тем самым ограничивая вклад деформированного изображения кольцом, радиус которого контролируется uHaloWidth:

// sample halo:
    vec2 haloVec = normalize(ghostVec) * uHaloWidth;
    float weight = length(vec2(0.5) - fract(texcoord + haloVec)) / length(vec2(0.5));
    weight = pow(1.0 - weight, 5.0);
    result += texture(uInputTex, texcoord + haloVec) * weight;


image

CHROMATIC DISTORTION (цветовое искажение)


Некоторые блики на линзах имеют цветовое искажение, вызванное вариацией преломлений света на разных длинах волн. Мы можем смоделировать это, создав функцию, которая выбирает красный, зеленый и синий каналы отдельно с немного разными смещениями вдоль вектора выборки:

vec3 textureDistorted(
    in sampler2D tex,
    in vec2 texcoord,
    in vec2 direction, // direction of distortion
    in vec3 distortion // per-channel distortion factor
) 
{
    return vec3(
        texture(tex, texcoord + direction * distortion.r).r,
        texture(tex, texcoord + direction * distortion.g).g,
        texture(tex, texcoord + direction * distortion.b).b
    );
}


Ее можно использовать как прямую замену для вызова texture () в предыдущем листинге. Я рассчитываю direction и distortion следующим образом:

vec2 texelSize = 1.0 / vec2(textureSize(uInputTex, 0));
vec3 distortion = vec3(-texelSize.x * uDistortion, 0.0, texelSize.x * uDistortion);

vec3 direction = normalize(ghostVec);


Хотя функция выборки простая, она стоит x3 выборок из текстуры, хотя все они должны быть cache-friendly, если вы не зададите uDistortion какое-нибудь гигантское значение.

С генерацией элементов все. Вот результат:

image

3. Размытие


Без размытия элементы lens flare (в частности, призраки), как правило, сохраняют внешний вид изображения. Добавляя размытие на элементы lens flare, мы ослабляем высокие частоты и тем самым уменьшаем контраст с входным изображением, что помогает нам продать эффект.

image

Я не буду рассказывать, как сделать размытие; об этом можно почитать на различных интернет ресурсах (Gaussian blur).

4. Upscale / смешивание с оригинальным изображением


Итак, у нас есть наши элементы lens flare, хорошо размытые. Как мы можем объединить их с оригинальным исходным изображением? Есть несколько важных соображений относительно всего render pipeline:

  • Любой последующий motion blur или depth of field должен быть применен до объединения с lens flare, таким образом элементы lens flare не будут участвовать в этих эффектах.
  • Lens flare должен быть применен до любого tonemapping. Это имеет физический смысл, поскольку tonemapping имитирует реакцию пленки / CMOS на входящий свет, составной частью которой является lens flare.


Имея это в виду, есть пару вещей, которые мы можем сделать на этом этапе, чтобы улучшить результат:

LENS DIRT


Во-первых, нужно модифицировать элементы lens flare с помощью «грязной» текстуры в полном разрешении (которая широко используется в Battlefield 3):

image

uniform sampler2D uInputTex; // source image
uniform sampler2D uLensFlareTex; // input from the blur stage
uniform sampler2D uLensDirtTex; // full resolution dirt texture

noperspective in vec2 vTexcoord;

out vec4 fResult;

void main() 
{
    vec4 lensMod = texture(uLensDirtTex, vTexcoord);
    vec4 lensFlare = texture(uLensFlareTex, vTexcoord) * lensMod;
    fResult = texture(uInputTex, vTexcoord) + lensflare;
}


Ключ к этому ― сама текстура грязи на линзах. Если контрастность низкая, формы элементов lens flare, как правило, доминируют в результате. По мере увеличения контрастности, элементы lens flare приглушаются, что дает другой эстетический вид, а также скрывает некоторые дефекты.

DIFFRACTION STARBURST


В качестве дополнительного улучшения мы можем использовать starburst текстуру добавляя ее к lens dirt:

image
В виде текстуры starburst выглядит не очень хорошо. Тем не менее, мы можем передать матрицу преобразования в шейдер, которая позволит нам вращать / деформировать starburst каждый кадр и получить желаемый эффект динамичности:

uniform sampler2D uInputTex; // source image
uniform sampler2D uLensFlareTex; // input from the blur stage
uniform sampler2D uLensDirtTex; // full resolution dirt texture

uniform sampler2D uLensStarTex; // diffraction starburst texture
uniform mat3 uLensStarMatrix; // transforms texcoords

noperspective in vec2 vTexcoord;

out vec4 fResult;

void main() 
{
    vec4 lensMod = texture(uLensDirtTex, vTexcoord);
    
    vec2 lensStarTexcoord = (uLensStarMatrix * vec3(vTexcoord, 1.0)).xy;
    lensMod += texture(uLensStarTex, lensStarTexcoord);
    
    vec4 lensFlare = texture(uLensFlareTex, vTexcoord) * lensMod;
    fResult = texture(uInputTex, vTexcoord) + lensflare;
}


Матрица преобразования uLensStarMatrix основана на значении, полученном из ориентации камеры следующим образом:

vec3 camx = cam.getViewMatrix().col(0); // camera x (left) vector
vec3 camz = cam.getViewMatrix().col(1); // camera z (forward) vector
float camrot = dot(camx, vec3(0,0,1)) + dot(camz, vec3(0,1,0));


Есть и другие способы получения значения camrot; главное оно должно меняться непрерывно при повороте камеры. Сама матрица строится следующим образом:

mat3 scaleBias1 = (
    2.0f,   0.0f,  -1.0f,
    0.0f,   2.0f,  -1.0f,
    0.0f,   0.0f,   1.0f,
);
mat3 rotation = (
    cos(camrot), -sin(camrot), 0.0f,
    sin(camrot), cos(camrot),  0.0f,
    0.0f,        0.0f,         1.0f
);
mat3 scaleBias2 = (
    0.5f,   0.0f,   0.5f,
    0.0f,   0.5f,   0.5f,
    0.0f,   0.0f,   1.0f,
);

mat3 uLensStarMatrix = scaleBias2 * rotation * scaleBias1;


Scale и bias матрицы нужны смещения начала координат текстуры, чтобы мы могли поворачивать starburst относительно центра изображения.

Заключение


Итак, теперь все! Этот метод демонстрирует как относительно упрощенный пост процесс дает прилично выглядящий lens flare. Он не совсем фотореалистичный, но при правильном применении может дать прекрасный результат.

image

© Habrahabr.ru